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);
}
}
}
}