tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/TokenSerializer.cs (500 lines of code) (raw):

using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text.Json; using SwaggerApiParser.SwaggerApiView; namespace SwaggerApiParser { public class IteratorPath { private LinkedList<string> paths; public IteratorPath() { this.paths = new LinkedList<string>(); this.Add("#"); } public IteratorPath(IteratorPath parent) { this.paths = new LinkedList<string>(); foreach (var path in parent.paths) { this.paths.AddLast(path); } } public string rootPath() { LinkedList<string> root = new LinkedList<string>(); root.AddLast("#"); root.AddLast(this.paths.ElementAt(1)); return Utils.BuildDefinitionId(root); } public void Add(string node) { this.paths.AddLast(node); } public void AddRange(IEnumerable<string> nodes) { foreach (var node in nodes) { this.Add(node); } } public void PopMulti(int number) { for (int i = 0; i < number; i++) { this.Pop(); } } public void Pop() { this.paths.RemoveLast(); } public string CurrentPath() { return Utils.BuildDefinitionId(this.paths); } public string CurrentNextPath(string nextPath) { this.Add(nextPath); var ret = this.CurrentPath(); this.Pop(); return ret; } } public class SerializeContext { public int indent = 0; public readonly IteratorPath IteratorPath; public List<string> definitionsNames { get; set; } public SerializeContext() { this.IteratorPath = new IteratorPath(); } public SerializeContext(int indent, IteratorPath iteratorPath) { this.indent = indent; this.IteratorPath = new IteratorPath(iteratorPath); } public SerializeContext(int indent, IteratorPath iteratorPath, List<string> definitionNames) { this.indent = indent; this.IteratorPath = new IteratorPath(iteratorPath); this.definitionsNames = definitionNames; } } public static class TokenSerializer { private const String IntentText = " "; public static CodeFileToken[] TokenSerializeAsJson(JsonElement jsonElement, bool isFoldable = false) { List<CodeFileToken> ret = new List<CodeFileToken>(); Visitor visitor = new Visitor(); visitor.Visit(jsonElement); if (isFoldable) { ret.Add(TokenSerializer.FoldableContentStart()); } ret.AddRange(visitor.Writer.ToTokens()); ret.Add(TokenSerializer.NewLine()); if (isFoldable) { ret.Add(TokenSerializer.FoldableContentEnd()); } return ret.ToArray(); } public static CodeFileToken[] TokenSerializeJsonToCodeLines(JsonElement jsonElement) { List<CodeFileToken> ret = new List<CodeFileToken>(); Visitor visitor = new Visitor(); visitor.Visit(jsonElement, excludeJsonPunctuations: true); ret.AddRange(visitor.Writer.ToTokens()); ret.Add(TokenSerializer.NewLine()); return ret.ToArray(); } /* * TokenSerialize obj into CodeFileTokens. * Each line begin with indent * One line format: <indent> <token 1> <token 2> ... <newline> */ public static CodeFileToken[] TokenSerialize(object obj, SerializeContext context, String[] serializePropertyName = null) { List<CodeFileToken> ret = new List<CodeFileToken>(); Type t = obj.GetType(); PropertyInfo[] props = t.GetProperties(); if (t.IsPrimitive || t == typeof(Decimal) || t == typeof(String)) { // ret.Add(Intent(intent)); ret.Add(new CodeFileToken(obj.ToString(), CodeFileTokenKind.Literal)); ret.Add(TokenSerializer.NewLine()); return ret.ToArray(); } if (t.Name == "JsonElement") { ret.AddRange(TokenSerializer.TokenSerializeAsJson((JsonElement)obj, true)); return ret.ToArray(); } foreach (var prop in props) { object value = prop.GetValue(obj); if (value == null || (serializePropertyName != null && (serializePropertyName.All(s => prop.Name != s)))) { continue; } Type propType = prop.PropertyType; // ret.Add(Intent(intent)); ret.Add(new CodeFileToken(prop.Name, CodeFileTokenKind.Literal) { DefinitionId = context.IteratorPath.CurrentNextPath(prop.Name) }); ret.Add(Colon()); string navigationToId = null; var valueKind = CodeFileTokenKind.Literal; if (prop.Name == "@ref") { navigationToId = context.IteratorPath.rootPath() + Utils.GetRefDefinitionIdPath(value.ToString()); valueKind = CodeFileTokenKind.MemberName; } if (propType.IsPrimitive || propType == typeof(Decimal) || propType == typeof(String)) { ret.Add(new CodeFileToken(value.ToString(), valueKind) { NavigateToId = navigationToId }); ret.Add(NewLine()); } else if (propType.IsGenericType || propType.IsArray) { ret.Add(NewLine()); if (prop.Name.Equals("patterenedObjects")) { Utils.SerializePatternedObjects((value as Dictionary<string, JsonElement>), ret); } else { foreach (var item in (IEnumerable)value) { var child = TokenSerializer.TokenSerialize(item, new SerializeContext(indent: context.indent + 1, context.IteratorPath)); ret.AddRange(child); } } } else { ret.Add(NewLine()); var child = TokenSerializer.TokenSerialize(value, new SerializeContext(indent: context.indent + 1, context.IteratorPath)); ret.AddRange(child); } } return ret.ToArray(); } public static CodeFileToken[] TokenSerializeAsTableFormat(int rowCount, int columnCount, IEnumerable<String> columnNames, CodeFileToken[] rows, string tableDefinitionId) { List<CodeFileToken> ret = new List<CodeFileToken>(); ret.Add(TokenSerializer.TableBegin(tableDefinitionId)); ret.AddRange(TokenSerializer.TableSize(rowCount, columnCount)); ret.AddRange(columnNames.Select(TokenSerializer.TableColumnName)); ret.AddRange(rows); ret.Add(TokenSerializer.TableEnd()); return ret.ToArray(); } public static CodeFileToken Intent(int intent) { // var ret = new CodeFileToken(String.Concat(Enumerable.Repeat(IntentText, intent)), CodeFileTokenKind.Whitespace); var ret = new CodeFileToken(intent.ToString(), CodeFileTokenKind.Whitespace); return ret; } public static CodeFileToken[] TableCell(IEnumerable<CodeFileToken> tokens) { List<CodeFileToken> ret = new List<CodeFileToken>(); ret.Add(new CodeFileToken(null, CodeFileTokenKind.TableCellBegin)); ret.AddRange(tokens); ret.Add(new CodeFileToken(null, CodeFileTokenKind.TableCellEnd)); return ret.ToArray(); } public static CodeFileToken[] OneLineToken(int intent, IEnumerable<CodeFileToken> contentTokens) { List<CodeFileToken> ret = new List<CodeFileToken>(); // ret.Add(TokenSerializer.Intent(intent)); ret.AddRange(contentTokens); ret.Add(NewLine()); return ret.ToArray(); } public static CodeFileToken NewLine() { return new CodeFileToken("", CodeFileTokenKind.Newline); } public static CodeFileToken Colon() { return new CodeFileToken(": ", CodeFileTokenKind.Punctuation); } public static CodeFileToken NavigableToken(String value, CodeFileTokenKind kind, String definitionId) { var ret = new CodeFileToken(value, kind) { DefinitionId = definitionId }; return ret; } public static CodeFileToken[] KeyValueTokens(String key, String value, bool newLine = true, string keyDefinitionId = null) { List<CodeFileToken> ret = new List<CodeFileToken>(); ret.Add(TokenSerializer.NavigableToken(key, CodeFileTokenKind.Literal, keyDefinitionId)); ret.Add(TokenSerializer.Colon()); ret.Add(new CodeFileToken(value, CodeFileTokenKind.Literal)); if (newLine) { ret.Add(TokenSerializer.NewLine()); } return ret.ToArray(); } public static CodeFileToken FoldableParentToken(String value) { var ret = new CodeFileToken(value, CodeFileTokenKind.FoldableSectionHeading); return ret; } public static CodeFileToken FoldableContentStart() { var ret = new CodeFileToken(null, CodeFileTokenKind.FoldableSectionContentStart); return ret; } public static CodeFileToken TableBegin(string definitionId) { var ret = new CodeFileToken(null, CodeFileTokenKind.TableBegin) { DefinitionId = definitionId }; return ret; } public static CodeFileToken TableEnd() { var ret = new CodeFileToken(null, CodeFileTokenKind.TableEnd); return ret; } public static CodeFileToken[] TableSize(int row, int column) { var ret = new List<CodeFileToken>(); ret.Add(new CodeFileToken(row.ToString(), CodeFileTokenKind.TableRowCount)); ret.Add(new CodeFileToken(column.ToString(), CodeFileTokenKind.TableColumnCount)); return ret.ToArray(); } public static CodeFileToken TableColumnName(string columnName) { var ret = new CodeFileToken(columnName, CodeFileTokenKind.TableColumnName); return ret; } public static CodeFileToken FoldableContentEnd() { var ret = new CodeFileToken(null, CodeFileTokenKind.FoldableSectionContentEnd); return ret; } } public class Visitor { public SwaggerTokenSerializer.IndentWriter Writer = new(); private IteratorPath iteratorPath = new(); public static CodeFileToken[] GenerateCodeFileTokens(JsonDocument document, Boolean isCurObjCollapsible = false) { Visitor visitor = new(); visitor.Visit(document.RootElement); return visitor.Writer.ToTokens(); } /// <summary> /// Generate the listing for a JSON value. /// </summary> /// <param name="element">The JSON value.</param> /// <param name="scopeStart"></param> /// <param name="scopeEnd"></param> public void Visit(JsonElement element, string scopeStart = "{ ", string scopeEnd = " }", bool excludeJsonPunctuations = false) { switch (element.ValueKind) { case JsonValueKind.Object: VisitObject(element, scopeStart, scopeEnd, excludeJsonPunctuations); break; case JsonValueKind.Array: VisitArray(element, excludeJsonPunctuations: excludeJsonPunctuations); break; case JsonValueKind.False: case JsonValueKind.Null: case JsonValueKind.Number: case JsonValueKind.String: case JsonValueKind.True: case JsonValueKind.Undefined: VisitLiteral(element, excludeJsonPunctuations: excludeJsonPunctuations); break; default: break; } } /// <summary> /// Generate the listing for an object. /// </summary> /// <param name="obj">The JSON object.</param> /// <param name="scopeStart"></param> /// <param name="scopeEnd"></param> private void VisitObject(JsonElement obj, string scopeStart = "{", string scopeEnd = "}", bool excludeJsonPunctuations = false) { bool multiLine = AlwaysMultiLine(obj); if (excludeJsonPunctuations) { scopeStart = scopeEnd = String.Empty; } using (this.Writer.Scope(scopeStart, scopeEnd, multiLine)) { // Optionally sort the values IEnumerable<JsonProperty> values = obj.EnumerateObject(); SwaggerTokenSerializer.Fenceposter fencepost = new(); // Generate the listing for each property foreach (JsonProperty property in values) { // Add the property to the current path iteratorPath.Add(property.Name); var isCollapsible = IsCurObjCollapsible(property.Name); // Write the property name if (!excludeJsonPunctuations) this.Writer.Write(CodeFileTokenKind.Punctuation, "\""); var propertyType = isCollapsible ? CodeFileTokenKind.TypeName : CodeFileTokenKind.MemberName; this.Writer.Write(propertyType, property.Name); // Create an ID for this property string id = this.iteratorPath.CurrentPath(); this.Writer.AnnotateDefinition(id); if (isCollapsible) { this.Writer.Write(CodeFileTokenKind.FoldableSectionHeading, id); } // Visit the value if (isCollapsible) { var punctuation = excludeJsonPunctuations ? ": " : "\": "; this.Writer.Write(CodeFileTokenKind.Punctuation, punctuation); this.Writer.WriteLine(); this.Writer.Write(CodeFileTokenKind.FoldableSectionHeading, null); Visit(property.Value, excludeJsonPunctuations: excludeJsonPunctuations); if (property.Name != values.Last().Name) { if (!excludeJsonPunctuations) this.Writer.Write(CodeFileTokenKind.Punctuation, ", "); if (multiLine) { this.Writer.WriteLine(); } } this.Writer.Write(CodeFileTokenKind.FoldableSectionHeading, null); } else { var punctuation = excludeJsonPunctuations ? ": " : "\": "; this.Writer.Write(CodeFileTokenKind.Punctuation, punctuation); Visit(property.Value, excludeJsonPunctuations: excludeJsonPunctuations); if (property.Name != values.Last().Name) { if (!excludeJsonPunctuations) this.Writer.Write(CodeFileTokenKind.Punctuation, ", "); if (multiLine) { this.Writer.WriteLine(); } } } // Remove the property from the current path this.iteratorPath.Pop(); } } } private static bool IsCurObjCollapsible(string propertyName) { return false; // return propertyName.Equals("General"); } /// <summary> /// Generate the listing for an array. /// </summary> /// <param name="array">The array.</param> /// <param name="scopeStart"></param> /// <param name="scopeEnd"></param> private void VisitArray(JsonElement array, string scopeStart = "[ ", string scopeEnd = " ]", bool excludeJsonPunctuations = false) { bool multiLine = AlwaysMultiLine(array); if (excludeJsonPunctuations) { scopeStart = scopeEnd = String.Empty; } using (this.Writer.Scope(scopeStart, scopeEnd, multiLine)) { int index = 0; SwaggerTokenSerializer.Fenceposter fencepost = new(); foreach (JsonElement child in array.EnumerateArray()) { if (fencepost.RequiresSeparator) { this.Writer.Write(CodeFileTokenKind.Punctuation, ", "); if (multiLine) { this.Writer.WriteLine(); } } this.iteratorPath.Add(index.ToString()); Visit(child, excludeJsonPunctuations: excludeJsonPunctuations); this.iteratorPath.Pop(); index++; } } } /// <summary> /// Generate the listing for a literal value. /// </summary> /// <param name="value">The literal value.</param> private void VisitLiteral(JsonElement value, bool excludeJsonPunctuations = false) { switch (value.ValueKind) { case JsonValueKind.Null: this.Writer.Write(CodeFileTokenKind.Keyword, "null"); break; case JsonValueKind.Undefined: this.Writer.Write(CodeFileTokenKind.Keyword, "undefined"); break; case JsonValueKind.True: this.Writer.Write(CodeFileTokenKind.Keyword, "true"); break; case JsonValueKind.False: this.Writer.Write(CodeFileTokenKind.Keyword, "false"); break; case JsonValueKind.Number: this.Writer.Write(CodeFileTokenKind.Literal, value.GetDouble().ToString()); break; case JsonValueKind.String: if (!excludeJsonPunctuations) this.Writer.Write(CodeFileTokenKind.Punctuation, "\""); this.Writer.Write(CodeFileTokenKind.StringLiteral, value.GetString()); this.iteratorPath.Add(value.GetString()); this.Writer.AnnotateDefinition(this.iteratorPath.CurrentPath()); this.iteratorPath.Pop(); if (!excludeJsonPunctuations) this.Writer.Write(CodeFileTokenKind.Punctuation, "\""); break; default: throw new InvalidOperationException($"Expected a literal JSON element, not {value.ValueKind}."); } } private bool AlwaysMultiLine(JsonElement element) { return true; } /// <summary> /// Crude heuristic to determine whether a JsonElement can be /// rendered on a single line. /// </summary> /// <param name="element">The JSON to render.</param> /// <returns>Whether it fits on a single line.</returns> private bool FitsOnOneLine(JsonElement element) { const int maxObjectProperties = 2; const int maxArrayElements = 3; const int maxStringLength = 50; switch (element.ValueKind) { case JsonValueKind.Object: int properties = 0; foreach (JsonProperty property in element.EnumerateObject()) { if (property.Value.ValueKind == JsonValueKind.Array || property.Value.ValueKind == JsonValueKind.Object || !FitsOnOneLine(property.Value) || properties++ > maxObjectProperties) { return false; } } return true; case JsonValueKind.Array: int values = 0; foreach (JsonElement value in element.EnumerateArray()) { if (value.ValueKind == JsonValueKind.Array || value.ValueKind == JsonValueKind.Object || !FitsOnOneLine(value) || values++ > maxArrayElements) { return false; } } return true; case JsonValueKind.String: return element.GetString().Length <= maxStringLength; case JsonValueKind.False: case JsonValueKind.Null: case JsonValueKind.Number: case JsonValueKind.True: case JsonValueKind.Undefined: default: return true; } } /// <summary> /// Get a string in a JSON document. /// </summary> /// <param name="doc">The JSON document.</param> /// <param name="path">Path to the string.</param> /// <returns>The desired string, or null.</returns> private static string GetString(JsonDocument doc, params string[] path) => GetString(doc.RootElement, path); /// <summary> /// Get a string in a JSON document. /// </summary> /// <param name="element">The element to start at.</param> /// <param name="path">Path to the string.</param> /// <returns>The desired string, or null.</returns> private static string GetString(JsonElement element, params string[] path) { foreach (string part in path) { if (element.ValueKind != JsonValueKind.Object || !element.TryGetProperty(part, out element)) { return null; } } return element.ValueKind == JsonValueKind.String ? element.GetString() : null; } /// <summary> /// Comparer to sort the operations within a path. It puts /// OperationIds before things like parameters. /// </summary> private class OperationComparer : IComparer<string> { /// <summary> /// Compare two path entry names. /// </summary> /// <param name="x">The first path entry name.</param> /// <param name="y">The second path entry name.</param> /// <returns>-1 if the first is smaller, 0 if equal, or 1 if the first is larger.</returns> public int Compare([AllowNull] string x, [AllowNull] string y) => (x?.Contains('_'), y?.Contains('_')) switch { (null, null) => 0, (null, _) => 1, (_, null) => -1, (true, false) => -1, (false, true) => 1, _ => string.Compare(x, y) }; } } }