using System; using System.Collections.Generic; using System.IO; using System.Linq; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; namespace Amazon.Lambda.Annotations.SourceGenerator.Writers { /// /// This contains the functionality to manipulate a YAML blob /// public class YamlWriter : ITemplateWriter { private YamlMappingNode _rootNode; private readonly Serializer _serializer = new Serializer(); private readonly Deserializer _deserializer = new Deserializer(); private readonly SerializerBuilder _serializerBuilder = new SerializerBuilder(); public YamlWriter() { _rootNode = new YamlMappingNode(); } /// /// Checks if the dot(.) seperated yamlPath exists in the YAML blob stored at the _rootNode /// /// dot(.) seperated path. Example "Person.Name.FirstName" /// true if the path exist, else false /// Thrown if the yamlPath is invalid public bool Exists(string yamlPath) { if (!IsValidPath(yamlPath)) { throw new InvalidDataException($"'{yamlPath}' is not a valid {nameof(yamlPath)}"); } YamlNode currentNode = _rootNode; foreach (var property in yamlPath.Split('.')) { if (currentNode == null) { return false; } try { currentNode = currentNode[property]; } catch (KeyNotFoundException) { return false; } } return true; } /// /// Gets the object stored at the dot(.) seperated yamlPath. If the path does not exist then return the defaultToken. /// The defaultToken is only returned if it holds a non-null value. /// /// dot(.) seperated path. Example "Person.Name.FirstName" /// The object that is returned if yamlPath does not exist. /// Thrown if the yamlPath does not exist and the defaultToken is null public object GetToken(string yamlPath, object defaultToken = null) { if (!Exists(yamlPath)) { if (defaultToken != null) { return defaultToken; } throw new InvalidOperationException($"'{yamlPath}' does not exist in the JSON model"); } YamlNode currentNode = _rootNode; foreach (var property in yamlPath.Split('.')) { currentNode = currentNode[property]; } return currentNode; } /// /// Gets the object stored at the dot(.) seperated yamlPath. If the path does not exist then return the defaultToken. /// The defaultToken is only returned if it holds a non-null value. /// The object is deserialized into type T before being returned. /// /// dot(.) seperated path. Example "Person.Name.FirstName" /// The object that is returned if yamlPath does not exist in the YAML blob. It will be convert to type T before being returned. /// Thrown if the yamlPath does not exist and the defaultToken is null public T GetToken(string yamlPath, object defaultToken = null) { var token = GetToken(yamlPath, defaultToken); if (token == null) { throw new InvalidOperationException($"'{yamlPath}' points to a null token"); } return GetDeserializedToken(token); } /// /// This method converts the supplied token it into a concrete type and sets it at the dot(.) seperated yamlPath. /// Any non-existing nodes in the yamlPath are created on the fly. /// All non-terminal nodes in the yamlPath need to be of type . /// /// dot(.) seperated path. Example "Person.Name.FirstName" /// The object to set at the specified yamlPath /// /// Thrown if the yamlPath is invalid /// Thrown if the terminal property in the yamlPath is null/empty or if any non-terminal nodes in the yamlPath cannot be converted to public void SetToken(string yamlPath, object token, TokenType tokenType = TokenType.Other) { if (!IsValidPath(yamlPath)) { throw new InvalidDataException($"'{yamlPath}' is not a valid '{nameof(yamlPath)}'"); } if (token == null) { return; } var pathList = yamlPath.Split('.'); var lastProperty = pathList.LastOrDefault(); if (string.IsNullOrEmpty(lastProperty)) { throw new InvalidOperationException($"Cannot set a token at '{yamlPath}' because the terminal property is null or empty"); } YamlNode terminalToken; if (token is YamlNode yamlNode) { terminalToken = yamlNode; } else { switch (tokenType) { case TokenType.List: terminalToken = GetDeserializedToken(token); break; case TokenType.KeyVal: case TokenType.Object: terminalToken = GetDeserializedToken(token); break; case TokenType.Other: terminalToken = GetDeserializedToken(token); break; default: throw new InvalidOperationException($"Failed to deserialize token because {nameof(tokenType)} is invalid"); } } var currentNode = _rootNode; for (var i = 0; i < pathList.Length - 1; i++) { if (currentNode == null) { throw new InvalidOperationException($"Cannot set a token at '{yamlPath}' because one of the nodes in the path is null"); } var property = pathList[i]; try { currentNode = (YamlMappingNode)currentNode[property]; } catch (KeyNotFoundException) { currentNode.Children[property] = new YamlMappingNode(); currentNode = (YamlMappingNode)currentNode[property]; } catch (InvalidCastException) { throw new InvalidOperationException($"Cannot set a token at '{yamlPath}' because one of the nodes in the path cannot be converted to {nameof(YamlMappingNode)}"); } } currentNode.Children[lastProperty] = terminalToken; } /// /// Deletes the token found at the dot(.) separated yamlPath. It does not do anything if the yamlPath does not exist. /// /// dot(.) seperated path. Example "Person.Name.FirstName" /// public void RemoveToken(string yamlPath) { if (!Exists(yamlPath)) { return; } var pathList = yamlPath.Split('.'); var lastProperty = pathList.LastOrDefault(); if (string.IsNullOrEmpty(lastProperty)) { throw new InvalidOperationException( $"Cannot remove the token at '{yamlPath}' because the terminal property is null or empty"); } YamlNode currentNode = _rootNode; for (var i = 0; i < pathList.Length - 1; i++) { var property = pathList[i]; currentNode = currentNode[property]; } var terminalNode = (YamlMappingNode)currentNode; terminalNode.Children.Remove(lastProperty); } /// /// Parses the YAML string as a /// /// public void Parse(string content) { _rootNode = string.IsNullOrEmpty(content) ? new YamlMappingNode() : _deserializer.Deserialize(content); } /// /// Converts the to a YAML string /// public string GetContent() { return _serializerBuilder .WithIndentedSequences() .Build() .Serialize(_rootNode); } /// /// If the string does not start with '@', return it as is. /// If a string value starts with '@' then a reference node is created and returned. /// public object GetValueOrRef(string value) { if (!value.StartsWith("@")) return value; var yamlNode = new YamlMappingNode(); yamlNode.Children["Ref"] = value.Substring(1); return yamlNode; } /// /// Validates that the yamlPath is not null or comprises only of white spaces. Also ensures that it does not have consecutive dots(.) /// /// /// true if the path is valid, else fail private bool IsValidPath(string yamlPath) { if (string.IsNullOrWhiteSpace(yamlPath)) return false; return !yamlPath.Split('.').Any(string.IsNullOrWhiteSpace); } private T GetDeserializedToken(object token) { if (token is T deserializedToken) { return deserializedToken; } return _deserializer.Deserialize(_serializer.Serialize(token)); } public IList GetKeys(string path) { try { return GetToken>(path).Keys.ToList(); } catch (Exception ex) { throw new InvalidOperationException($"Unable to retrieve keys for the specified YAML path '{path}'.", ex); } } } }