using System; using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Amazon.Lambda.Annotations.SourceGenerator.Writers { /// /// This contains the functionality to manipulate a JSON blob /// public class JsonWriter : ITemplateWriter { private JObject _rootNode; public JsonWriter() { _rootNode = new JObject(); } /// /// Checks if the dot(.) seperated jsonPath exists in the json blob stored at the _rootNode /// /// dot(.) seperated path. Example "Person.Name.FirstName" /// true if the path exist, else false /// Thrown if the jsonPath is invalid public bool Exists(string jsonPath) { if (!IsValidPath(jsonPath)) { throw new InvalidDataException($"'{jsonPath}' is not a valid '{nameof(jsonPath)}'"); } JToken currentNode = _rootNode; foreach (var property in jsonPath.Split('.')) { if (currentNode == null) { return false; } currentNode = currentNode[property]; } return currentNode != null; } /// /// This method converts the supplied token it into a type and sets it at the dot(.) seperated jsonPath. /// Any non-existing nodes in the jsonPath are created on the fly. /// All non-terminal nodes in the jsonPath need to be of type . /// /// dot(.) seperated path. Example "Person.Name.FirstName" /// The object to set at the specified jsonPath /// This does not play any role while setting a token for the JsonWriter /// Thrown if the jsonPath is invalid /// Thrown if the terminal property in the jsonPath is null/empty or if any non-terminal nodes in the jsonPath cannot be converted to public void SetToken(string jsonPath, object token, TokenType tokenType = TokenType.Other) { if (!IsValidPath(jsonPath)) { throw new InvalidDataException($"'{jsonPath}' is not a valid '{nameof(jsonPath)}'"); } if (token == null) { return; } var pathList = jsonPath.Split('.'); var lastProperty = pathList.LastOrDefault(); if (string.IsNullOrEmpty((lastProperty))) { throw new InvalidOperationException($"Cannot set a token at '{jsonPath}' because the terminal property is null or empty"); } var terminalToken = GetDeserializedToken(token); var currentNode = _rootNode; for (var i = 0; i < pathList.Length-1; i++) { if (currentNode == null) { throw new InvalidOperationException($"Cannot set a token at '{jsonPath}' because one of the nodes in the path is null"); } var property = pathList[i]; if (!currentNode.ContainsKey(property)) { currentNode[property] = new JObject(); } currentNode = currentNode[property] as JObject; if (currentNode == null) { throw new InvalidOperationException($"Cannot set a value at '{jsonPath}' because the token at {property} does not represent a {typeof(JObject)}"); } } currentNode[lastProperty] = terminalToken; } /// /// Gets the object stored at the dot(.) seperated jsonPath. 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 jsonPath does not exist. /// Thrown if the jsonPath does not exist and the defaultToken is null public object GetToken(string jsonPath, object defaultToken = null) { if (!Exists(jsonPath)) { if (defaultToken != null) { return defaultToken; } throw new InvalidOperationException($"'{jsonPath}' does not exist in the JSON model"); } JToken currentNode = _rootNode; foreach (var property in jsonPath.Split('.')) { currentNode = currentNode[property]; } return currentNode; } /// /// Gets the object stored at the dot(.) seperated jsonPath. 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 jsonPath does not exist in the JSON blob. It will be convert to type T before being returned. /// Thrown if the jsonPath does not exist and the defaultToken is null public T GetToken(string jsonPath, object defaultToken = null) { var token = GetToken(jsonPath, defaultToken); if (token == null) { throw new InvalidOperationException($"'{jsonPath}' points to a null token"); } return GetDeserializedToken(token); } /// /// Deletes the token found at the dot(.) separated jsonPath. It does not do anything if the jsonPath does not exist. /// /// dot(.) seperated path. Example "Person.Name.FirstName" /// Thrown if the terminal property in jsonPath is null or empty public void RemoveToken(string jsonPath) { if (!Exists(jsonPath)) { return; } var pathList = jsonPath.Split('.'); var lastProperty = pathList.LastOrDefault(); if (string.IsNullOrEmpty(lastProperty)) { throw new InvalidOperationException( $"Cannot remove the token at '{jsonPath}' because the terminal property is null or empty"); } var currentNode = _rootNode; for (var i = 0; i < pathList.Length-1; i++) { var property = pathList[i]; currentNode = currentNode[property] as JObject; } currentNode.Remove(lastProperty); } /// /// Returns the template as a string /// public string GetContent() { return JsonConvert.SerializeObject(_rootNode, formatting: Formatting.Indented); } /// /// Converts the JSON string into a /// /// public void Parse(string content) { _rootNode = string.IsNullOrEmpty(content) ? new JObject() : JObject.Parse(content); } /// /// 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 jsonNode = new JObject(); jsonNode["Ref"] = value.Substring(1); return jsonNode; } /// /// Validates that the jsonPath 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 jsonPath) { if (string.IsNullOrWhiteSpace(jsonPath)) return false; return !jsonPath.Split('.').Any(string.IsNullOrWhiteSpace); } private T GetDeserializedToken(object token) { if (token is T deserializedToken) { return deserializedToken; } return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(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 JSON path '{path}'.", ex); } } } }