From 78430c1cd95f5808012296bc44be1ac4b34f8b35 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 11 Sep 2023 10:06:46 -0700 Subject: [PATCH 1/3] csdl2Paths library and demo --- CsdlToPaths.Demo/CsdlToPathsDemo.csproj | 18 +++ CsdlToPaths.Demo/GlobalUsings.cs | 0 CsdlToPaths.Demo/Program.cs | 110 +++++++++++++ CsdlToPaths.Demo/data/directory.csdl.xml | 60 +++++++ CsdlToPaths.Demo/data/example89.csdl.xml | 152 ++++++++++++++++++ CsdlToPaths.Demo/data/users.csdl.xml | 27 ++++ CsdlToPaths.Tests/CsdlToPaths.Tests.csproj | 29 ++++ CsdlToPaths.Tests/GlobalUsings.cs | 1 + CsdlToPaths.Tests/UnitTest1.cs | 10 ++ CsdlToPaths.sln | 34 ++++ CsdlToPaths/CsdlToPaths.csproj | 13 ++ CsdlToPaths/GlobalUsings.cs | 5 + CsdlToPaths/ModelAnalyzer.cs | 136 ++++++++++++++++ CsdlToPaths/Node.cs | 39 +++++ CsdlToPaths/SchemaAnalyzer.csprojxxx | 14 ++ CsdlToPaths/TreeWriter.cs | 77 +++++++++ .../extensions/EnumerableExtensions.cs | 54 +++++++ CsdlToPaths/extensions/IEdmModelExtensions.cs | 67 ++++++++ CsdlToPaths/extensions/StringExtensions.cs | 25 +++ 19 files changed, 871 insertions(+) create mode 100644 CsdlToPaths.Demo/CsdlToPathsDemo.csproj create mode 100644 CsdlToPaths.Demo/GlobalUsings.cs create mode 100644 CsdlToPaths.Demo/Program.cs create mode 100644 CsdlToPaths.Demo/data/directory.csdl.xml create mode 100644 CsdlToPaths.Demo/data/example89.csdl.xml create mode 100644 CsdlToPaths.Demo/data/users.csdl.xml create mode 100644 CsdlToPaths.Tests/CsdlToPaths.Tests.csproj create mode 100644 CsdlToPaths.Tests/GlobalUsings.cs create mode 100644 CsdlToPaths.Tests/UnitTest1.cs create mode 100644 CsdlToPaths.sln create mode 100644 CsdlToPaths/CsdlToPaths.csproj create mode 100644 CsdlToPaths/GlobalUsings.cs create mode 100644 CsdlToPaths/ModelAnalyzer.cs create mode 100644 CsdlToPaths/Node.cs create mode 100644 CsdlToPaths/SchemaAnalyzer.csprojxxx create mode 100644 CsdlToPaths/TreeWriter.cs create mode 100644 CsdlToPaths/extensions/EnumerableExtensions.cs create mode 100644 CsdlToPaths/extensions/IEdmModelExtensions.cs create mode 100644 CsdlToPaths/extensions/StringExtensions.cs diff --git a/CsdlToPaths.Demo/CsdlToPathsDemo.csproj b/CsdlToPaths.Demo/CsdlToPathsDemo.csproj new file mode 100644 index 0000000000..2987f8ed1f --- /dev/null +++ b/CsdlToPaths.Demo/CsdlToPathsDemo.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + Exe + net7.0 + enable + enable + + + \ No newline at end of file diff --git a/CsdlToPaths.Demo/GlobalUsings.cs b/CsdlToPaths.Demo/GlobalUsings.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/CsdlToPaths.Demo/Program.cs b/CsdlToPaths.Demo/Program.cs new file mode 100644 index 0000000000..663e888445 --- /dev/null +++ b/CsdlToPaths.Demo/Program.cs @@ -0,0 +1,110 @@ +using System.Xml; +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; + +class Program +{ + + private static void Main() + { + var args = Args.Parse(); + + + + using var reader = XmlReader.Create(args.InputFile); + if (!CsdlReader.TryParse(reader, out var model, out var errors)) + { + Console.WriteLine(string.Join(Environment.NewLine, errors)); + return; + } + + var analyzer = new ModelAnalyzer(model); + var tree = analyzer.CreateTree(); + + // format tree + if (!args.HideTree) + { + using var writer = new TreeWriter(Console.Out, true); + writer.Display(tree); + } + + // list of paths + if (args.ShowPaths) + { + foreach (var path in tree.Paths()) + { + // just path + // Console.WriteLine("{0}", path.Segments.SeparatedBy("/")); + + // path and response type + Console.WriteLine("{0} \x1b[36m{1}\x1b[m", path.Segments.SeparatedBy("/"), path.ResponseType.Format()); + + // // path, response type and a procedure like signature + // Console.WriteLine("{0}\n\t\x1b[36m{1}\x1b[m", + // path.Segments.SeparatedBy("/"), + // Signature(path)); + } + } + } + + + static string Signature((IEnumerable Segments, IEdmType ResponseType) path) + { + var parameters = string.Join(", ", path.Segments.Where(s => s.StartsWith('{')).Select(w => w.Trim('{', '}'))); + var name = string.Join("Of", path.Segments.Where(s => !s.StartsWith('{')).Select(s => s.Capitalize()).Reverse()); + + return $"{name}({parameters}) -> {path.ResponseType.Format()}"; + } +} + +class Args +{ + + + const string DEFAULT_INPUT = "data/directory.csdl.xml"; + + + public string InputFile { get; private set; } = null!; + public bool ShowPaths { get; private set; } + public bool HideTree { get; private set; } + + public static Args Parse() + { + var result = new Args(); + var defaultArgProvided = false; + var args = Environment.GetCommandLineArgs(); + for (int i = 1; i < args.Length; i++) + { + var arg = args[i]; + switch (arg) + { + case "--paths": + case "-p": + result.ShowPaths = true; + break; + case "--no-tree": + case "-t": + result.HideTree = true; + break; + case string s when s.StartsWith("--"): + throw new Exception($"unknown option {arg}"); + default: + if (defaultArgProvided) + { + throw new Exception($"two default arguments provided"); + } + else + { + result.InputFile = arg; + defaultArgProvided = true; + } + break; + } + } + if (result.InputFile == null) + { + result.InputFile = DEFAULT_INPUT; + } + return result; + } +} \ No newline at end of file diff --git a/CsdlToPaths.Demo/data/directory.csdl.xml b/CsdlToPaths.Demo/data/directory.csdl.xml new file mode 100644 index 0000000000..443041eda3 --- /dev/null +++ b/CsdlToPaths.Demo/data/directory.csdl.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Concurrency + + + + + + + + + + \ No newline at end of file diff --git a/CsdlToPaths.Demo/data/example89.csdl.xml b/CsdlToPaths.Demo/data/example89.csdl.xml new file mode 100644 index 0000000000..a9bd6c4985 --- /dev/null +++ b/CsdlToPaths.Demo/data/example89.csdl.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Concurrency + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CsdlToPaths.Demo/data/users.csdl.xml b/CsdlToPaths.Demo/data/users.csdl.xml new file mode 100644 index 0000000000..97c5f50662 --- /dev/null +++ b/CsdlToPaths.Demo/data/users.csdl.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj b/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj new file mode 100644 index 0000000000..bd2ee17293 --- /dev/null +++ b/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/CsdlToPaths.Tests/GlobalUsings.cs b/CsdlToPaths.Tests/GlobalUsings.cs new file mode 100644 index 0000000000..8c927eb747 --- /dev/null +++ b/CsdlToPaths.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/CsdlToPaths.Tests/UnitTest1.cs b/CsdlToPaths.Tests/UnitTest1.cs new file mode 100644 index 0000000000..47d403c570 --- /dev/null +++ b/CsdlToPaths.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace CsdlToPaths.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/CsdlToPaths.sln b/CsdlToPaths.sln new file mode 100644 index 0000000000..02106d7641 --- /dev/null +++ b/CsdlToPaths.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPaths", "CsdlToPaths\CsdlToPaths.csproj", "{234DF902-1948-44C0-B435-BA6EC36F69D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPaths.Tests", "CsdlToPaths.Tests\CsdlToPaths.Tests.csproj", "{195D745C-0447-4196-9D8E-68AD96169E2D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPathsDemo", "CsdlToPaths.Demo\CsdlToPathsDemo.csproj", "{F7F19C88-F9BA-4E91-AF0C-44D802219AF7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {234DF902-1948-44C0-B435-BA6EC36F69D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {234DF902-1948-44C0-B435-BA6EC36F69D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {234DF902-1948-44C0-B435-BA6EC36F69D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {234DF902-1948-44C0-B435-BA6EC36F69D5}.Release|Any CPU.Build.0 = Release|Any CPU + {195D745C-0447-4196-9D8E-68AD96169E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {195D745C-0447-4196-9D8E-68AD96169E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {195D745C-0447-4196-9D8E-68AD96169E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {195D745C-0447-4196-9D8E-68AD96169E2D}.Release|Any CPU.Build.0 = Release|Any CPU + {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/CsdlToPaths/CsdlToPaths.csproj b/CsdlToPaths/CsdlToPaths.csproj new file mode 100644 index 0000000000..d2c44da5b6 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/CsdlToPaths/GlobalUsings.cs b/CsdlToPaths/GlobalUsings.cs new file mode 100644 index 0000000000..b0d32bc328 --- /dev/null +++ b/CsdlToPaths/GlobalUsings.cs @@ -0,0 +1,5 @@ + +global using System.Text; +global using System.Collections.Immutable; +global using System.Diagnostics.CodeAnalysis; +global using Microsoft.OData.Edm; diff --git a/CsdlToPaths/ModelAnalyzer.cs b/CsdlToPaths/ModelAnalyzer.cs new file mode 100644 index 0000000000..f7a23ab5e0 --- /dev/null +++ b/CsdlToPaths/ModelAnalyzer.cs @@ -0,0 +1,136 @@ +public class ModelAnalyzer +{ + private readonly IEdmModel Model; + + public ModelAnalyzer(IEdmModel model) + { + Model = model; + } + + public Node CreateTree() + { + return Unfold(Model.EntityContainer, ImmutableList<(string, IEdmType)>.Empty); + } + + private Node Unfold(IEdmEntityContainer entityContainer, ImmutableList<(string, IEdmType)> visited) + { + var node = new Node("", EdmUntypedStructuredType.Instance); + + // instead of iterating twice over container elements via EntitySets and Singletons extension methods + // iterate once and do the type-check/cast inline in a switch + foreach (var element in entityContainer.Elements) + { + switch (element) + { + case IEdmEntitySet entitySet: + node.Add(UnfoldEntitySet(entitySet, visited)); + break; + case IEdmSingleton singleton: + node.Add(UnfoldSingleton(visited, singleton)); + break; + default: + // intentionally left blank. other container elements are not supported yet. + Console.Error.WriteLine("ignoring {0} {1}", element.ContainerElementKind, element.Name); + break; + } + } + return node; + } + + private Node UnfoldSingleton(ImmutableList<(string, IEdmType)> visited, IEdmSingleton singleton) + { + if (singleton.Type is IEdmEntityType singletonType) + { + return UnfoldStructuredType(singleton.Name, singletonType, visited); + } + throw new NotSupportedException("singleton type not a entity type"); + } + + private Node UnfoldEntitySet(IEdmEntitySet entitySet, ImmutableList<(string, IEdmType)> visited) + { + if (entitySet.Type is IEdmCollectionType collectionType) + { + return UnfoldCollectionType(entitySet.Name, collectionType, visited); + } + throw new NotSupportedException("EntitySet type not a collection of entity types"); + } + + + private Node UnfoldStructuredType(string segment, IEdmStructuredType structuredType, ImmutableList<(string, IEdmType)> visited) + { + var node = new Node(segment, structuredType); + visited = visited.Add((segment, structuredType)); + + if (BreakLoop(visited, out var loop)) + { + node.Add(loop); + return node; + } + + foreach (var subtype in Model.FindAllDerivedTypes(structuredType)) + { + node.Add(UnfoldStructuredType(subtype.FullTypeName(), subtype, visited)); + } + + // Get all properties, not just navigation properties + // This will generate navigation paths that navigate through a structural property (e.g. /Suppliers/{ID}/Address/Country: in example 89) + // If the property type is neither complex nor entity, nothing will be (yield) returned in the switch statement + // TODO: deal with type definitions + foreach (var property in structuredType.Properties()) + { + // var node = new Node(property.Name, property.Type.Definition); + switch (property.Type.Definition) + { + case IEdmStructuredType propertyStructuredType: + node.Add(UnfoldStructuredType(property.Name, propertyStructuredType, visited)); + break; + + case IEdmCollectionType collectionType: + node.Add(UnfoldCollectionType(property.Name, collectionType, visited)); + break; + } + } + return node; + } + + private bool BreakLoop(ImmutableList<(string, IEdmType)> visited, [MaybeNullWhen(false)] out Node node) + { + // if we visited the type, return one last path and stop recursion + // this can only happen if there is al least two elements + if (visited.Count >= 2) + { + var type = visited.Last().Item2; + var ix = visited.FindLastIndex(visited.Count - 2, p => p.Item2 == type); + if (ix >= 0) + { + var tail = visited.Skip(ix + 1).Select(p => p.Item1).ToList(); + node = new Node($"{{ ({string.Join("/", tail)})+ }}", type); + return true; + } + } + node = default; + return false; + } + + private Node UnfoldCollectionType(string segment, IEdmCollectionType collectionType, ImmutableList<(string, IEdmType)> visited) + { + var node = new Node(segment, collectionType); + visited = visited.Add((segment, collectionType)); + + if (!(collectionType.ElementType.Definition is IEdmEntityType elementType)) + { + throw new NotSupportedException("IEdmCollectionType's element type is not a entity type"); + } + + var keys = elementType.Key(); + if (!keys.TryGetSingle(out var key)) + { + throw new NotSupportedException("multipart keys are not supported"); + } + + node.Add(UnfoldStructuredType($"{{{key.Name}}}", elementType, visited)); + + return node; + } +} + diff --git a/CsdlToPaths/Node.cs b/CsdlToPaths/Node.cs new file mode 100644 index 0000000000..68dcceb916 --- /dev/null +++ b/CsdlToPaths/Node.cs @@ -0,0 +1,39 @@ + + +public record class Node(string Name, IEdmType Type) +{ + + public List Nodes { get; } = new List(); + + + public void Add(Node node) + { + Nodes.Add(node); + } + + public void AddRange(IEnumerable node) + { + Nodes.AddRange(node); + } + + public IEnumerable<(IEnumerable Segments, IEdmType ResponseType)> Paths() + { + return + from node in Nodes + from path in node.Paths(ImmutableList.Empty) + select path; + } + + public IEnumerable<(IEnumerable, IEdmType)> Paths(ImmutableList path) + { + path = path.Add(Name); + yield return (path, Type); + foreach (var node in Nodes) + { + foreach (var child in node.Paths(path)) + { + yield return child; + } + } + } +} diff --git a/CsdlToPaths/SchemaAnalyzer.csprojxxx b/CsdlToPaths/SchemaAnalyzer.csprojxxx new file mode 100644 index 0000000000..0354afc875 --- /dev/null +++ b/CsdlToPaths/SchemaAnalyzer.csprojxxx @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + 10.0 + + + + + + + \ No newline at end of file diff --git a/CsdlToPaths/TreeWriter.cs b/CsdlToPaths/TreeWriter.cs new file mode 100644 index 0000000000..080a3f268a --- /dev/null +++ b/CsdlToPaths/TreeWriter.cs @@ -0,0 +1,77 @@ + +public class TreeWriter : IDisposable +{ + + private readonly TextWriter writer; + private readonly bool color; + + public TreeWriter(TextWriter writer, bool color) + { + this.writer = writer; + this.color = color; + } + + public void Display(Node node) + { + foreach (var (child, isLast) in node.Nodes.WithLast()) + { + WriteNode(child, indent: "", isLast: isLast); + } + writer.WriteLine(); + } + + // adapted from https://andrewlock.net/creating-an-ascii-art-tree-in-csharp/ + private void WriteNode(Node node, string indent, bool isLast) + { + // Print the provided pipes/spaces indent + Console.Write(indent); + + // Depending if this node is a last child, print the corner or cross, and + // calculate the indent that will be passed to its children + if (isLast) + { + writer.Write(CONFIG.LastChild); + indent += CONFIG.Space; + } + else + { + writer.Write(CONFIG.Child); + indent += CONFIG.Vertical; + } + if (color) + { + writer.WriteLine("{0} \x1b[36m{1}\x1b[m", node.Name, node.Type.Format()); + } + else + { + writer.WriteLine("{0} {1}", node.Name, node.Type.Format()); + } + + // Loop through the children recursively, passing in the + // indent, and the isLast parameter + foreach (var (child, isLastChild) in node.Nodes.WithLast()) + { + WriteNode(child, indent, isLastChild); + } + } + + public void Dispose() + { + ((IDisposable)writer).Dispose(); + } + + record struct Config( + string Child, string LastChild, string Vertical, string Space) + { } + + // Constants for indentation + // https://unicode-table.com/en/blocks/box-drawing/ + static Config[] CONFIGS = new[]{ + new Config("+- ", "+- ", "| ", " "), + new Config("├─ ", "└─ ", "│ ", " "), + new Config("╠═ ", "╚═ ", "║ ", " "), + new Config("╟─ ", "╙─ ", "║ ", " "), + }; + + static Config CONFIG = CONFIGS[1]; +} \ No newline at end of file diff --git a/CsdlToPaths/extensions/EnumerableExtensions.cs b/CsdlToPaths/extensions/EnumerableExtensions.cs new file mode 100644 index 0000000000..be9e6ea6b1 --- /dev/null +++ b/CsdlToPaths/extensions/EnumerableExtensions.cs @@ -0,0 +1,54 @@ + + + + +static class EnumerableExtensions +{ + public static IEnumerable<(T, FirstLast)> WithFirstLast(this IEnumerable items) + { + var enumerator = items.GetEnumerator(); + if (!enumerator.MoveNext()) { yield break; }; + var state = FirstLast.First; + var current = enumerator.Current; + while (enumerator.MoveNext()) + { + yield return (current, state); + state &= ~FirstLast.First; // not the first anymore since MoveNext succeeded twice + current = enumerator.Current; + } + state |= FirstLast.Last; // add the Last flag + yield return (current, state); + } + + public static IEnumerable<(T, bool)> WithLast(this IEnumerable items) + { + var enumerator = items.GetEnumerator(); + if (!enumerator.MoveNext()) { yield break; }; + var isLast = false; + var current = enumerator.Current; + while (enumerator.MoveNext()) + { + yield return (current, isLast); + current = enumerator.Current; + } + isLast = true; // add the isLast flag + yield return (current, isLast); + } + + public static bool TryGetSingle(this IEnumerable items, [MaybeNullWhen(false)] out T single) + { + using var enumerator = items.GetEnumerator(); + if (!enumerator.MoveNext()) + { + single = default; + return false; + } + single = enumerator.Current; + return !enumerator.MoveNext(); + } +} + + + +[Flags] +enum FirstLast { None = 0, First = 1, Last = 2 } diff --git a/CsdlToPaths/extensions/IEdmModelExtensions.cs b/CsdlToPaths/extensions/IEdmModelExtensions.cs new file mode 100644 index 0000000000..1f45c68b11 --- /dev/null +++ b/CsdlToPaths/extensions/IEdmModelExtensions.cs @@ -0,0 +1,67 @@ + +public static class IEdmModelExtensions +{ + + public static bool TryFindDeclaredType(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEdmType type) + { + type = model.FindDeclaredType(fqn); + return type != null; + } + + public static bool TryFindDeclaredEntityType(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEdmEntityType entityType) + { + if (model.TryFindDeclaredType(fqn, out var type) && type is IEdmEntityType entityType1) + { + entityType = entityType1; + return true; + } + entityType = default; + return false; + } + + + public static bool TryFindAllDerivedTypes(this IEdmModel model, IEdmEntityType baseType, out IEnumerable subTypes) + { + subTypes = model.FindAllDerivedTypes(baseType).Cast(); + return true; + } + + + + public static bool TryFindAllDerivedTypes(this IEdmModel model, IEdmComplexType baseType, out IEnumerable subTypes) + { + subTypes = model.FindAllDerivedTypes(baseType).Cast(); + return true; + } + + public static bool TryFindDeclaredEntitySubTypes(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEnumerable subtypes) + { + if (model.TryFindDeclaredEntityType(fqn, out var baseType)) + { + subtypes = model.FindAllDerivedTypes(baseType).Cast(); + return true; + } + + subtypes = default; + return false; + } + + public static bool TryGetCollectionElementType(this IEdmType type, [MaybeNullWhen(false)] out IEdmEntityType elementType) + { + if (type is IEdmCollectionType collectionType && collectionType.ElementType.Definition is IEdmEntityType et) + { + elementType = et; + return true; + } + elementType = default; + return false; + } + + public static string Format(this IEdmType type) => type switch + { + IEdmCollectionType collectionType => $"[{Format(collectionType.ElementType.Definition)}]", + IEdmEntityType entityType => entityType.Name, + IEdmComplexType complexType => complexType.Name, + _ => type?.ToString() ?? "", + }; +} \ No newline at end of file diff --git a/CsdlToPaths/extensions/StringExtensions.cs b/CsdlToPaths/extensions/StringExtensions.cs new file mode 100644 index 0000000000..5c3b90939b --- /dev/null +++ b/CsdlToPaths/extensions/StringExtensions.cs @@ -0,0 +1,25 @@ + + + + + + + +public static class StringExtensions +{ + public static string SeparatedBy(this IEnumerable items, string separator) + { + var sb = new StringBuilder(); + foreach (var item in items) + { + sb.Append(separator); + sb.Append(item); + } + return sb.ToString(); + } + + + public static string Capitalize(this string word) + => word[0..1].ToUpper() + word[1..]; + +} \ No newline at end of file From e0ba8c38d9e1d4d1464e9bd6c2a1b21e828301f6 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 11 Sep 2023 12:20:35 -0700 Subject: [PATCH 2/3] one folder --- CsdlToPaths.Demo/CsdlToPathsDemo.csproj | 18 --- CsdlToPaths.Demo/GlobalUsings.cs | 0 CsdlToPaths.Demo/Program.cs | 110 ------------- CsdlToPaths.Demo/data/directory.csdl.xml | 60 ------- CsdlToPaths.Demo/data/example89.csdl.xml | 152 ------------------ CsdlToPaths.Demo/data/users.csdl.xml | 27 ---- CsdlToPaths.Tests/CsdlToPaths.Tests.csproj | 29 ---- CsdlToPaths.Tests/GlobalUsings.cs | 1 - CsdlToPaths.Tests/UnitTest1.cs | 10 -- CsdlToPaths.sln | 34 ---- CsdlToPaths/CsdlToPaths.csproj | 13 -- CsdlToPaths/GlobalUsings.cs | 5 - CsdlToPaths/ModelAnalyzer.cs | 136 ---------------- CsdlToPaths/Node.cs | 39 ----- CsdlToPaths/SchemaAnalyzer.csprojxxx | 14 -- CsdlToPaths/TreeWriter.cs | 77 --------- .../extensions/EnumerableExtensions.cs | 54 ------- CsdlToPaths/extensions/IEdmModelExtensions.cs | 67 -------- CsdlToPaths/extensions/StringExtensions.cs | 25 --- 19 files changed, 871 deletions(-) delete mode 100644 CsdlToPaths.Demo/CsdlToPathsDemo.csproj delete mode 100644 CsdlToPaths.Demo/GlobalUsings.cs delete mode 100644 CsdlToPaths.Demo/Program.cs delete mode 100644 CsdlToPaths.Demo/data/directory.csdl.xml delete mode 100644 CsdlToPaths.Demo/data/example89.csdl.xml delete mode 100644 CsdlToPaths.Demo/data/users.csdl.xml delete mode 100644 CsdlToPaths.Tests/CsdlToPaths.Tests.csproj delete mode 100644 CsdlToPaths.Tests/GlobalUsings.cs delete mode 100644 CsdlToPaths.Tests/UnitTest1.cs delete mode 100644 CsdlToPaths.sln delete mode 100644 CsdlToPaths/CsdlToPaths.csproj delete mode 100644 CsdlToPaths/GlobalUsings.cs delete mode 100644 CsdlToPaths/ModelAnalyzer.cs delete mode 100644 CsdlToPaths/Node.cs delete mode 100644 CsdlToPaths/SchemaAnalyzer.csprojxxx delete mode 100644 CsdlToPaths/TreeWriter.cs delete mode 100644 CsdlToPaths/extensions/EnumerableExtensions.cs delete mode 100644 CsdlToPaths/extensions/IEdmModelExtensions.cs delete mode 100644 CsdlToPaths/extensions/StringExtensions.cs diff --git a/CsdlToPaths.Demo/CsdlToPathsDemo.csproj b/CsdlToPaths.Demo/CsdlToPathsDemo.csproj deleted file mode 100644 index 2987f8ed1f..0000000000 --- a/CsdlToPaths.Demo/CsdlToPathsDemo.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - Exe - net7.0 - enable - enable - - - \ No newline at end of file diff --git a/CsdlToPaths.Demo/GlobalUsings.cs b/CsdlToPaths.Demo/GlobalUsings.cs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/CsdlToPaths.Demo/Program.cs b/CsdlToPaths.Demo/Program.cs deleted file mode 100644 index 663e888445..0000000000 --- a/CsdlToPaths.Demo/Program.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Xml; -using Microsoft.OData.Edm; -using Microsoft.OData.Edm.Csdl; - -class Program -{ - - private static void Main() - { - var args = Args.Parse(); - - - - using var reader = XmlReader.Create(args.InputFile); - if (!CsdlReader.TryParse(reader, out var model, out var errors)) - { - Console.WriteLine(string.Join(Environment.NewLine, errors)); - return; - } - - var analyzer = new ModelAnalyzer(model); - var tree = analyzer.CreateTree(); - - // format tree - if (!args.HideTree) - { - using var writer = new TreeWriter(Console.Out, true); - writer.Display(tree); - } - - // list of paths - if (args.ShowPaths) - { - foreach (var path in tree.Paths()) - { - // just path - // Console.WriteLine("{0}", path.Segments.SeparatedBy("/")); - - // path and response type - Console.WriteLine("{0} \x1b[36m{1}\x1b[m", path.Segments.SeparatedBy("/"), path.ResponseType.Format()); - - // // path, response type and a procedure like signature - // Console.WriteLine("{0}\n\t\x1b[36m{1}\x1b[m", - // path.Segments.SeparatedBy("/"), - // Signature(path)); - } - } - } - - - static string Signature((IEnumerable Segments, IEdmType ResponseType) path) - { - var parameters = string.Join(", ", path.Segments.Where(s => s.StartsWith('{')).Select(w => w.Trim('{', '}'))); - var name = string.Join("Of", path.Segments.Where(s => !s.StartsWith('{')).Select(s => s.Capitalize()).Reverse()); - - return $"{name}({parameters}) -> {path.ResponseType.Format()}"; - } -} - -class Args -{ - - - const string DEFAULT_INPUT = "data/directory.csdl.xml"; - - - public string InputFile { get; private set; } = null!; - public bool ShowPaths { get; private set; } - public bool HideTree { get; private set; } - - public static Args Parse() - { - var result = new Args(); - var defaultArgProvided = false; - var args = Environment.GetCommandLineArgs(); - for (int i = 1; i < args.Length; i++) - { - var arg = args[i]; - switch (arg) - { - case "--paths": - case "-p": - result.ShowPaths = true; - break; - case "--no-tree": - case "-t": - result.HideTree = true; - break; - case string s when s.StartsWith("--"): - throw new Exception($"unknown option {arg}"); - default: - if (defaultArgProvided) - { - throw new Exception($"two default arguments provided"); - } - else - { - result.InputFile = arg; - defaultArgProvided = true; - } - break; - } - } - if (result.InputFile == null) - { - result.InputFile = DEFAULT_INPUT; - } - return result; - } -} \ No newline at end of file diff --git a/CsdlToPaths.Demo/data/directory.csdl.xml b/CsdlToPaths.Demo/data/directory.csdl.xml deleted file mode 100644 index 443041eda3..0000000000 --- a/CsdlToPaths.Demo/data/directory.csdl.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Concurrency - - - - - - - - - - \ No newline at end of file diff --git a/CsdlToPaths.Demo/data/example89.csdl.xml b/CsdlToPaths.Demo/data/example89.csdl.xml deleted file mode 100644 index a9bd6c4985..0000000000 --- a/CsdlToPaths.Demo/data/example89.csdl.xml +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Concurrency - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/CsdlToPaths.Demo/data/users.csdl.xml b/CsdlToPaths.Demo/data/users.csdl.xml deleted file mode 100644 index 97c5f50662..0000000000 --- a/CsdlToPaths.Demo/data/users.csdl.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj b/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj deleted file mode 100644 index bd2ee17293..0000000000 --- a/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net7.0 - enable - enable - - false - true - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/CsdlToPaths.Tests/GlobalUsings.cs b/CsdlToPaths.Tests/GlobalUsings.cs deleted file mode 100644 index 8c927eb747..0000000000 --- a/CsdlToPaths.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/CsdlToPaths.Tests/UnitTest1.cs b/CsdlToPaths.Tests/UnitTest1.cs deleted file mode 100644 index 47d403c570..0000000000 --- a/CsdlToPaths.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CsdlToPaths.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} \ No newline at end of file diff --git a/CsdlToPaths.sln b/CsdlToPaths.sln deleted file mode 100644 index 02106d7641..0000000000 --- a/CsdlToPaths.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPaths", "CsdlToPaths\CsdlToPaths.csproj", "{234DF902-1948-44C0-B435-BA6EC36F69D5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPaths.Tests", "CsdlToPaths.Tests\CsdlToPaths.Tests.csproj", "{195D745C-0447-4196-9D8E-68AD96169E2D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPathsDemo", "CsdlToPaths.Demo\CsdlToPathsDemo.csproj", "{F7F19C88-F9BA-4E91-AF0C-44D802219AF7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {234DF902-1948-44C0-B435-BA6EC36F69D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {234DF902-1948-44C0-B435-BA6EC36F69D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {234DF902-1948-44C0-B435-BA6EC36F69D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {234DF902-1948-44C0-B435-BA6EC36F69D5}.Release|Any CPU.Build.0 = Release|Any CPU - {195D745C-0447-4196-9D8E-68AD96169E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {195D745C-0447-4196-9D8E-68AD96169E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {195D745C-0447-4196-9D8E-68AD96169E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {195D745C-0447-4196-9D8E-68AD96169E2D}.Release|Any CPU.Build.0 = Release|Any CPU - {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/CsdlToPaths/CsdlToPaths.csproj b/CsdlToPaths/CsdlToPaths.csproj deleted file mode 100644 index d2c44da5b6..0000000000 --- a/CsdlToPaths/CsdlToPaths.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net7.0 - enable - enable - - - - - - - diff --git a/CsdlToPaths/GlobalUsings.cs b/CsdlToPaths/GlobalUsings.cs deleted file mode 100644 index b0d32bc328..0000000000 --- a/CsdlToPaths/GlobalUsings.cs +++ /dev/null @@ -1,5 +0,0 @@ - -global using System.Text; -global using System.Collections.Immutable; -global using System.Diagnostics.CodeAnalysis; -global using Microsoft.OData.Edm; diff --git a/CsdlToPaths/ModelAnalyzer.cs b/CsdlToPaths/ModelAnalyzer.cs deleted file mode 100644 index f7a23ab5e0..0000000000 --- a/CsdlToPaths/ModelAnalyzer.cs +++ /dev/null @@ -1,136 +0,0 @@ -public class ModelAnalyzer -{ - private readonly IEdmModel Model; - - public ModelAnalyzer(IEdmModel model) - { - Model = model; - } - - public Node CreateTree() - { - return Unfold(Model.EntityContainer, ImmutableList<(string, IEdmType)>.Empty); - } - - private Node Unfold(IEdmEntityContainer entityContainer, ImmutableList<(string, IEdmType)> visited) - { - var node = new Node("", EdmUntypedStructuredType.Instance); - - // instead of iterating twice over container elements via EntitySets and Singletons extension methods - // iterate once and do the type-check/cast inline in a switch - foreach (var element in entityContainer.Elements) - { - switch (element) - { - case IEdmEntitySet entitySet: - node.Add(UnfoldEntitySet(entitySet, visited)); - break; - case IEdmSingleton singleton: - node.Add(UnfoldSingleton(visited, singleton)); - break; - default: - // intentionally left blank. other container elements are not supported yet. - Console.Error.WriteLine("ignoring {0} {1}", element.ContainerElementKind, element.Name); - break; - } - } - return node; - } - - private Node UnfoldSingleton(ImmutableList<(string, IEdmType)> visited, IEdmSingleton singleton) - { - if (singleton.Type is IEdmEntityType singletonType) - { - return UnfoldStructuredType(singleton.Name, singletonType, visited); - } - throw new NotSupportedException("singleton type not a entity type"); - } - - private Node UnfoldEntitySet(IEdmEntitySet entitySet, ImmutableList<(string, IEdmType)> visited) - { - if (entitySet.Type is IEdmCollectionType collectionType) - { - return UnfoldCollectionType(entitySet.Name, collectionType, visited); - } - throw new NotSupportedException("EntitySet type not a collection of entity types"); - } - - - private Node UnfoldStructuredType(string segment, IEdmStructuredType structuredType, ImmutableList<(string, IEdmType)> visited) - { - var node = new Node(segment, structuredType); - visited = visited.Add((segment, structuredType)); - - if (BreakLoop(visited, out var loop)) - { - node.Add(loop); - return node; - } - - foreach (var subtype in Model.FindAllDerivedTypes(structuredType)) - { - node.Add(UnfoldStructuredType(subtype.FullTypeName(), subtype, visited)); - } - - // Get all properties, not just navigation properties - // This will generate navigation paths that navigate through a structural property (e.g. /Suppliers/{ID}/Address/Country: in example 89) - // If the property type is neither complex nor entity, nothing will be (yield) returned in the switch statement - // TODO: deal with type definitions - foreach (var property in structuredType.Properties()) - { - // var node = new Node(property.Name, property.Type.Definition); - switch (property.Type.Definition) - { - case IEdmStructuredType propertyStructuredType: - node.Add(UnfoldStructuredType(property.Name, propertyStructuredType, visited)); - break; - - case IEdmCollectionType collectionType: - node.Add(UnfoldCollectionType(property.Name, collectionType, visited)); - break; - } - } - return node; - } - - private bool BreakLoop(ImmutableList<(string, IEdmType)> visited, [MaybeNullWhen(false)] out Node node) - { - // if we visited the type, return one last path and stop recursion - // this can only happen if there is al least two elements - if (visited.Count >= 2) - { - var type = visited.Last().Item2; - var ix = visited.FindLastIndex(visited.Count - 2, p => p.Item2 == type); - if (ix >= 0) - { - var tail = visited.Skip(ix + 1).Select(p => p.Item1).ToList(); - node = new Node($"{{ ({string.Join("/", tail)})+ }}", type); - return true; - } - } - node = default; - return false; - } - - private Node UnfoldCollectionType(string segment, IEdmCollectionType collectionType, ImmutableList<(string, IEdmType)> visited) - { - var node = new Node(segment, collectionType); - visited = visited.Add((segment, collectionType)); - - if (!(collectionType.ElementType.Definition is IEdmEntityType elementType)) - { - throw new NotSupportedException("IEdmCollectionType's element type is not a entity type"); - } - - var keys = elementType.Key(); - if (!keys.TryGetSingle(out var key)) - { - throw new NotSupportedException("multipart keys are not supported"); - } - - node.Add(UnfoldStructuredType($"{{{key.Name}}}", elementType, visited)); - - return node; - } -} - diff --git a/CsdlToPaths/Node.cs b/CsdlToPaths/Node.cs deleted file mode 100644 index 68dcceb916..0000000000 --- a/CsdlToPaths/Node.cs +++ /dev/null @@ -1,39 +0,0 @@ - - -public record class Node(string Name, IEdmType Type) -{ - - public List Nodes { get; } = new List(); - - - public void Add(Node node) - { - Nodes.Add(node); - } - - public void AddRange(IEnumerable node) - { - Nodes.AddRange(node); - } - - public IEnumerable<(IEnumerable Segments, IEdmType ResponseType)> Paths() - { - return - from node in Nodes - from path in node.Paths(ImmutableList.Empty) - select path; - } - - public IEnumerable<(IEnumerable, IEdmType)> Paths(ImmutableList path) - { - path = path.Add(Name); - yield return (path, Type); - foreach (var node in Nodes) - { - foreach (var child in node.Paths(path)) - { - yield return child; - } - } - } -} diff --git a/CsdlToPaths/SchemaAnalyzer.csprojxxx b/CsdlToPaths/SchemaAnalyzer.csprojxxx deleted file mode 100644 index 0354afc875..0000000000 --- a/CsdlToPaths/SchemaAnalyzer.csprojxxx +++ /dev/null @@ -1,14 +0,0 @@ - - - - net6.0 - enable - enable - 10.0 - - - - - - - \ No newline at end of file diff --git a/CsdlToPaths/TreeWriter.cs b/CsdlToPaths/TreeWriter.cs deleted file mode 100644 index 080a3f268a..0000000000 --- a/CsdlToPaths/TreeWriter.cs +++ /dev/null @@ -1,77 +0,0 @@ - -public class TreeWriter : IDisposable -{ - - private readonly TextWriter writer; - private readonly bool color; - - public TreeWriter(TextWriter writer, bool color) - { - this.writer = writer; - this.color = color; - } - - public void Display(Node node) - { - foreach (var (child, isLast) in node.Nodes.WithLast()) - { - WriteNode(child, indent: "", isLast: isLast); - } - writer.WriteLine(); - } - - // adapted from https://andrewlock.net/creating-an-ascii-art-tree-in-csharp/ - private void WriteNode(Node node, string indent, bool isLast) - { - // Print the provided pipes/spaces indent - Console.Write(indent); - - // Depending if this node is a last child, print the corner or cross, and - // calculate the indent that will be passed to its children - if (isLast) - { - writer.Write(CONFIG.LastChild); - indent += CONFIG.Space; - } - else - { - writer.Write(CONFIG.Child); - indent += CONFIG.Vertical; - } - if (color) - { - writer.WriteLine("{0} \x1b[36m{1}\x1b[m", node.Name, node.Type.Format()); - } - else - { - writer.WriteLine("{0} {1}", node.Name, node.Type.Format()); - } - - // Loop through the children recursively, passing in the - // indent, and the isLast parameter - foreach (var (child, isLastChild) in node.Nodes.WithLast()) - { - WriteNode(child, indent, isLastChild); - } - } - - public void Dispose() - { - ((IDisposable)writer).Dispose(); - } - - record struct Config( - string Child, string LastChild, string Vertical, string Space) - { } - - // Constants for indentation - // https://unicode-table.com/en/blocks/box-drawing/ - static Config[] CONFIGS = new[]{ - new Config("+- ", "+- ", "| ", " "), - new Config("├─ ", "└─ ", "│ ", " "), - new Config("╠═ ", "╚═ ", "║ ", " "), - new Config("╟─ ", "╙─ ", "║ ", " "), - }; - - static Config CONFIG = CONFIGS[1]; -} \ No newline at end of file diff --git a/CsdlToPaths/extensions/EnumerableExtensions.cs b/CsdlToPaths/extensions/EnumerableExtensions.cs deleted file mode 100644 index be9e6ea6b1..0000000000 --- a/CsdlToPaths/extensions/EnumerableExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ - - - - -static class EnumerableExtensions -{ - public static IEnumerable<(T, FirstLast)> WithFirstLast(this IEnumerable items) - { - var enumerator = items.GetEnumerator(); - if (!enumerator.MoveNext()) { yield break; }; - var state = FirstLast.First; - var current = enumerator.Current; - while (enumerator.MoveNext()) - { - yield return (current, state); - state &= ~FirstLast.First; // not the first anymore since MoveNext succeeded twice - current = enumerator.Current; - } - state |= FirstLast.Last; // add the Last flag - yield return (current, state); - } - - public static IEnumerable<(T, bool)> WithLast(this IEnumerable items) - { - var enumerator = items.GetEnumerator(); - if (!enumerator.MoveNext()) { yield break; }; - var isLast = false; - var current = enumerator.Current; - while (enumerator.MoveNext()) - { - yield return (current, isLast); - current = enumerator.Current; - } - isLast = true; // add the isLast flag - yield return (current, isLast); - } - - public static bool TryGetSingle(this IEnumerable items, [MaybeNullWhen(false)] out T single) - { - using var enumerator = items.GetEnumerator(); - if (!enumerator.MoveNext()) - { - single = default; - return false; - } - single = enumerator.Current; - return !enumerator.MoveNext(); - } -} - - - -[Flags] -enum FirstLast { None = 0, First = 1, Last = 2 } diff --git a/CsdlToPaths/extensions/IEdmModelExtensions.cs b/CsdlToPaths/extensions/IEdmModelExtensions.cs deleted file mode 100644 index 1f45c68b11..0000000000 --- a/CsdlToPaths/extensions/IEdmModelExtensions.cs +++ /dev/null @@ -1,67 +0,0 @@ - -public static class IEdmModelExtensions -{ - - public static bool TryFindDeclaredType(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEdmType type) - { - type = model.FindDeclaredType(fqn); - return type != null; - } - - public static bool TryFindDeclaredEntityType(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEdmEntityType entityType) - { - if (model.TryFindDeclaredType(fqn, out var type) && type is IEdmEntityType entityType1) - { - entityType = entityType1; - return true; - } - entityType = default; - return false; - } - - - public static bool TryFindAllDerivedTypes(this IEdmModel model, IEdmEntityType baseType, out IEnumerable subTypes) - { - subTypes = model.FindAllDerivedTypes(baseType).Cast(); - return true; - } - - - - public static bool TryFindAllDerivedTypes(this IEdmModel model, IEdmComplexType baseType, out IEnumerable subTypes) - { - subTypes = model.FindAllDerivedTypes(baseType).Cast(); - return true; - } - - public static bool TryFindDeclaredEntitySubTypes(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEnumerable subtypes) - { - if (model.TryFindDeclaredEntityType(fqn, out var baseType)) - { - subtypes = model.FindAllDerivedTypes(baseType).Cast(); - return true; - } - - subtypes = default; - return false; - } - - public static bool TryGetCollectionElementType(this IEdmType type, [MaybeNullWhen(false)] out IEdmEntityType elementType) - { - if (type is IEdmCollectionType collectionType && collectionType.ElementType.Definition is IEdmEntityType et) - { - elementType = et; - return true; - } - elementType = default; - return false; - } - - public static string Format(this IEdmType type) => type switch - { - IEdmCollectionType collectionType => $"[{Format(collectionType.ElementType.Definition)}]", - IEdmEntityType entityType => entityType.Name, - IEdmComplexType complexType => complexType.Name, - _ => type?.ToString() ?? "", - }; -} \ No newline at end of file diff --git a/CsdlToPaths/extensions/StringExtensions.cs b/CsdlToPaths/extensions/StringExtensions.cs deleted file mode 100644 index 5c3b90939b..0000000000 --- a/CsdlToPaths/extensions/StringExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - -public static class StringExtensions -{ - public static string SeparatedBy(this IEnumerable items, string separator) - { - var sb = new StringBuilder(); - foreach (var item in items) - { - sb.Append(separator); - sb.Append(item); - } - return sb.ToString(); - } - - - public static string Capitalize(this string word) - => word[0..1].ToUpper() + word[1..]; - -} \ No newline at end of file From 353d04572478a88fe589be8835b20b03980f6195 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 11 Sep 2023 14:06:10 -0700 Subject: [PATCH 3/3] demo --- .../CsdlToPaths.Demo/CsdlToPathsDemo.csproj | 30 ++++ CsdlToPaths/CsdlToPaths.Demo/GlobalUsings.cs | 0 CsdlToPaths/CsdlToPaths.Demo/Program.cs | 109 +++++++++++++ .../CsdlToPaths.Demo/data/directory.csdl.xml | 60 +++++++ .../CsdlToPaths.Demo/data/example89.csdl.xml | 152 ++++++++++++++++++ .../CsdlToPaths.Demo/data/users.csdl.xml | 27 ++++ .../CsdlToPaths.Tests.csproj | 29 ++++ CsdlToPaths/CsdlToPaths.Tests/GlobalUsings.cs | 1 + CsdlToPaths/CsdlToPaths.Tests/UnitTest1.cs | 10 ++ CsdlToPaths/CsdlToPaths.sln | 34 ++++ CsdlToPaths/CsdlToPaths/CsdlToPaths.csproj | 13 ++ CsdlToPaths/CsdlToPaths/GlobalUsings.cs | 5 + CsdlToPaths/CsdlToPaths/ModelAnalyzer.cs | 136 ++++++++++++++++ CsdlToPaths/CsdlToPaths/Node.cs | 39 +++++ .../CsdlToPaths/SchemaAnalyzer.csprojxxx | 14 ++ CsdlToPaths/CsdlToPaths/TreeWriter.cs | 77 +++++++++ .../extensions/EnumerableExtensions.cs | 54 +++++++ .../extensions/IEdmModelExtensions.cs | 67 ++++++++ .../extensions/StringExtensions.cs | 25 +++ 19 files changed, 882 insertions(+) create mode 100644 CsdlToPaths/CsdlToPaths.Demo/CsdlToPathsDemo.csproj create mode 100644 CsdlToPaths/CsdlToPaths.Demo/GlobalUsings.cs create mode 100644 CsdlToPaths/CsdlToPaths.Demo/Program.cs create mode 100644 CsdlToPaths/CsdlToPaths.Demo/data/directory.csdl.xml create mode 100644 CsdlToPaths/CsdlToPaths.Demo/data/example89.csdl.xml create mode 100644 CsdlToPaths/CsdlToPaths.Demo/data/users.csdl.xml create mode 100644 CsdlToPaths/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj create mode 100644 CsdlToPaths/CsdlToPaths.Tests/GlobalUsings.cs create mode 100644 CsdlToPaths/CsdlToPaths.Tests/UnitTest1.cs create mode 100644 CsdlToPaths/CsdlToPaths.sln create mode 100644 CsdlToPaths/CsdlToPaths/CsdlToPaths.csproj create mode 100644 CsdlToPaths/CsdlToPaths/GlobalUsings.cs create mode 100644 CsdlToPaths/CsdlToPaths/ModelAnalyzer.cs create mode 100644 CsdlToPaths/CsdlToPaths/Node.cs create mode 100644 CsdlToPaths/CsdlToPaths/SchemaAnalyzer.csprojxxx create mode 100644 CsdlToPaths/CsdlToPaths/TreeWriter.cs create mode 100644 CsdlToPaths/CsdlToPaths/extensions/EnumerableExtensions.cs create mode 100644 CsdlToPaths/CsdlToPaths/extensions/IEdmModelExtensions.cs create mode 100644 CsdlToPaths/CsdlToPaths/extensions/StringExtensions.cs diff --git a/CsdlToPaths/CsdlToPaths.Demo/CsdlToPathsDemo.csproj b/CsdlToPaths/CsdlToPaths.Demo/CsdlToPathsDemo.csproj new file mode 100644 index 0000000000..8a06a95d5d --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.Demo/CsdlToPathsDemo.csproj @@ -0,0 +1,30 @@ + + + + + + + + + + + + + Always + + + Always + + + Always + + + + + Exe + net7.0 + enable + enable + + + \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths.Demo/GlobalUsings.cs b/CsdlToPaths/CsdlToPaths.Demo/GlobalUsings.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/CsdlToPaths/CsdlToPaths.Demo/Program.cs b/CsdlToPaths/CsdlToPaths.Demo/Program.cs new file mode 100644 index 0000000000..9cdac987e7 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.Demo/Program.cs @@ -0,0 +1,109 @@ +using System.Xml; +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; + +class Program +{ + + private static void Main() + { + var args = Args.Parse(); + + + using var reader = XmlReader.Create(args.InputFile); + if (!CsdlReader.TryParse(reader, out var model, out var errors)) + { + Console.WriteLine(string.Join(Environment.NewLine, errors)); + return; + } + + var analyzer = new ModelAnalyzer(model); + var tree = analyzer.CreateTree(); + + // format tree + if (!args.HideTree) + { + using var writer = new TreeWriter(Console.Out, true); + writer.Display(tree); + } + + // list of paths + if (args.ShowPaths) + { + foreach (var path in tree.Paths()) + { + // just path + // Console.WriteLine("{0}", path.Segments.SeparatedBy("/")); + + // path and response type + Console.WriteLine("{0} \x1b[36m{1}\x1b[m", path.Segments.SeparatedBy("/"), path.ResponseType.Format()); + + // // path, response type and a procedure like signature + // Console.WriteLine("{0}\n\t\x1b[36m{1}\x1b[m", + // path.Segments.SeparatedBy("/"), + // Signature(path)); + } + } + } + + + static string Signature((IEnumerable Segments, IEdmType ResponseType) path) + { + var parameters = string.Join(", ", path.Segments.Where(s => s.StartsWith('{')).Select(w => w.Trim('{', '}'))); + var name = string.Join("Of", path.Segments.Where(s => !s.StartsWith('{')).Select(s => s.Capitalize()).Reverse()); + + return $"{name}({parameters}) -> {path.ResponseType.Format()}"; + } +} + +class Args +{ + + + const string DEFAULT_INPUT = "data/directory.csdl.xml"; + + + public string InputFile { get; private set; } = null!; + public bool ShowPaths { get; private set; } + public bool HideTree { get; private set; } + + public static Args Parse() + { + var result = new Args(); + var defaultArgProvided = false; + var args = Environment.GetCommandLineArgs(); + for (int i = 1; i < args.Length; i++) + { + var arg = args[i]; + switch (arg) + { + case "--paths": + case "-p": + result.ShowPaths = true; + break; + case "--no-tree": + case "-t": + result.HideTree = true; + break; + case string s when s.StartsWith("--"): + throw new Exception($"unknown option {arg}"); + default: + if (defaultArgProvided) + { + throw new Exception($"two default arguments provided"); + } + else + { + result.InputFile = arg; + defaultArgProvided = true; + } + break; + } + } + if (result.InputFile == null) + { + result.InputFile = DEFAULT_INPUT; + } + return result; + } +} \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths.Demo/data/directory.csdl.xml b/CsdlToPaths/CsdlToPaths.Demo/data/directory.csdl.xml new file mode 100644 index 0000000000..443041eda3 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.Demo/data/directory.csdl.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Concurrency + + + + + + + + + + \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths.Demo/data/example89.csdl.xml b/CsdlToPaths/CsdlToPaths.Demo/data/example89.csdl.xml new file mode 100644 index 0000000000..a9bd6c4985 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.Demo/data/example89.csdl.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Concurrency + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths.Demo/data/users.csdl.xml b/CsdlToPaths/CsdlToPaths.Demo/data/users.csdl.xml new file mode 100644 index 0000000000..97c5f50662 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.Demo/data/users.csdl.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj b/CsdlToPaths/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj new file mode 100644 index 0000000000..bd2ee17293 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.Tests/CsdlToPaths.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/CsdlToPaths/CsdlToPaths.Tests/GlobalUsings.cs b/CsdlToPaths/CsdlToPaths.Tests/GlobalUsings.cs new file mode 100644 index 0000000000..8c927eb747 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths.Tests/UnitTest1.cs b/CsdlToPaths/CsdlToPaths.Tests/UnitTest1.cs new file mode 100644 index 0000000000..47d403c570 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace CsdlToPaths.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths.sln b/CsdlToPaths/CsdlToPaths.sln new file mode 100644 index 0000000000..02106d7641 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPaths", "CsdlToPaths\CsdlToPaths.csproj", "{234DF902-1948-44C0-B435-BA6EC36F69D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPaths.Tests", "CsdlToPaths.Tests\CsdlToPaths.Tests.csproj", "{195D745C-0447-4196-9D8E-68AD96169E2D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsdlToPathsDemo", "CsdlToPaths.Demo\CsdlToPathsDemo.csproj", "{F7F19C88-F9BA-4E91-AF0C-44D802219AF7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {234DF902-1948-44C0-B435-BA6EC36F69D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {234DF902-1948-44C0-B435-BA6EC36F69D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {234DF902-1948-44C0-B435-BA6EC36F69D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {234DF902-1948-44C0-B435-BA6EC36F69D5}.Release|Any CPU.Build.0 = Release|Any CPU + {195D745C-0447-4196-9D8E-68AD96169E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {195D745C-0447-4196-9D8E-68AD96169E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {195D745C-0447-4196-9D8E-68AD96169E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {195D745C-0447-4196-9D8E-68AD96169E2D}.Release|Any CPU.Build.0 = Release|Any CPU + {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7F19C88-F9BA-4E91-AF0C-44D802219AF7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/CsdlToPaths/CsdlToPaths/CsdlToPaths.csproj b/CsdlToPaths/CsdlToPaths/CsdlToPaths.csproj new file mode 100644 index 0000000000..d2c44da5b6 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/CsdlToPaths.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/CsdlToPaths/CsdlToPaths/GlobalUsings.cs b/CsdlToPaths/CsdlToPaths/GlobalUsings.cs new file mode 100644 index 0000000000..b0d32bc328 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/GlobalUsings.cs @@ -0,0 +1,5 @@ + +global using System.Text; +global using System.Collections.Immutable; +global using System.Diagnostics.CodeAnalysis; +global using Microsoft.OData.Edm; diff --git a/CsdlToPaths/CsdlToPaths/ModelAnalyzer.cs b/CsdlToPaths/CsdlToPaths/ModelAnalyzer.cs new file mode 100644 index 0000000000..f7a23ab5e0 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/ModelAnalyzer.cs @@ -0,0 +1,136 @@ +public class ModelAnalyzer +{ + private readonly IEdmModel Model; + + public ModelAnalyzer(IEdmModel model) + { + Model = model; + } + + public Node CreateTree() + { + return Unfold(Model.EntityContainer, ImmutableList<(string, IEdmType)>.Empty); + } + + private Node Unfold(IEdmEntityContainer entityContainer, ImmutableList<(string, IEdmType)> visited) + { + var node = new Node("", EdmUntypedStructuredType.Instance); + + // instead of iterating twice over container elements via EntitySets and Singletons extension methods + // iterate once and do the type-check/cast inline in a switch + foreach (var element in entityContainer.Elements) + { + switch (element) + { + case IEdmEntitySet entitySet: + node.Add(UnfoldEntitySet(entitySet, visited)); + break; + case IEdmSingleton singleton: + node.Add(UnfoldSingleton(visited, singleton)); + break; + default: + // intentionally left blank. other container elements are not supported yet. + Console.Error.WriteLine("ignoring {0} {1}", element.ContainerElementKind, element.Name); + break; + } + } + return node; + } + + private Node UnfoldSingleton(ImmutableList<(string, IEdmType)> visited, IEdmSingleton singleton) + { + if (singleton.Type is IEdmEntityType singletonType) + { + return UnfoldStructuredType(singleton.Name, singletonType, visited); + } + throw new NotSupportedException("singleton type not a entity type"); + } + + private Node UnfoldEntitySet(IEdmEntitySet entitySet, ImmutableList<(string, IEdmType)> visited) + { + if (entitySet.Type is IEdmCollectionType collectionType) + { + return UnfoldCollectionType(entitySet.Name, collectionType, visited); + } + throw new NotSupportedException("EntitySet type not a collection of entity types"); + } + + + private Node UnfoldStructuredType(string segment, IEdmStructuredType structuredType, ImmutableList<(string, IEdmType)> visited) + { + var node = new Node(segment, structuredType); + visited = visited.Add((segment, structuredType)); + + if (BreakLoop(visited, out var loop)) + { + node.Add(loop); + return node; + } + + foreach (var subtype in Model.FindAllDerivedTypes(structuredType)) + { + node.Add(UnfoldStructuredType(subtype.FullTypeName(), subtype, visited)); + } + + // Get all properties, not just navigation properties + // This will generate navigation paths that navigate through a structural property (e.g. /Suppliers/{ID}/Address/Country: in example 89) + // If the property type is neither complex nor entity, nothing will be (yield) returned in the switch statement + // TODO: deal with type definitions + foreach (var property in structuredType.Properties()) + { + // var node = new Node(property.Name, property.Type.Definition); + switch (property.Type.Definition) + { + case IEdmStructuredType propertyStructuredType: + node.Add(UnfoldStructuredType(property.Name, propertyStructuredType, visited)); + break; + + case IEdmCollectionType collectionType: + node.Add(UnfoldCollectionType(property.Name, collectionType, visited)); + break; + } + } + return node; + } + + private bool BreakLoop(ImmutableList<(string, IEdmType)> visited, [MaybeNullWhen(false)] out Node node) + { + // if we visited the type, return one last path and stop recursion + // this can only happen if there is al least two elements + if (visited.Count >= 2) + { + var type = visited.Last().Item2; + var ix = visited.FindLastIndex(visited.Count - 2, p => p.Item2 == type); + if (ix >= 0) + { + var tail = visited.Skip(ix + 1).Select(p => p.Item1).ToList(); + node = new Node($"{{ ({string.Join("/", tail)})+ }}", type); + return true; + } + } + node = default; + return false; + } + + private Node UnfoldCollectionType(string segment, IEdmCollectionType collectionType, ImmutableList<(string, IEdmType)> visited) + { + var node = new Node(segment, collectionType); + visited = visited.Add((segment, collectionType)); + + if (!(collectionType.ElementType.Definition is IEdmEntityType elementType)) + { + throw new NotSupportedException("IEdmCollectionType's element type is not a entity type"); + } + + var keys = elementType.Key(); + if (!keys.TryGetSingle(out var key)) + { + throw new NotSupportedException("multipart keys are not supported"); + } + + node.Add(UnfoldStructuredType($"{{{key.Name}}}", elementType, visited)); + + return node; + } +} + diff --git a/CsdlToPaths/CsdlToPaths/Node.cs b/CsdlToPaths/CsdlToPaths/Node.cs new file mode 100644 index 0000000000..68dcceb916 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/Node.cs @@ -0,0 +1,39 @@ + + +public record class Node(string Name, IEdmType Type) +{ + + public List Nodes { get; } = new List(); + + + public void Add(Node node) + { + Nodes.Add(node); + } + + public void AddRange(IEnumerable node) + { + Nodes.AddRange(node); + } + + public IEnumerable<(IEnumerable Segments, IEdmType ResponseType)> Paths() + { + return + from node in Nodes + from path in node.Paths(ImmutableList.Empty) + select path; + } + + public IEnumerable<(IEnumerable, IEdmType)> Paths(ImmutableList path) + { + path = path.Add(Name); + yield return (path, Type); + foreach (var node in Nodes) + { + foreach (var child in node.Paths(path)) + { + yield return child; + } + } + } +} diff --git a/CsdlToPaths/CsdlToPaths/SchemaAnalyzer.csprojxxx b/CsdlToPaths/CsdlToPaths/SchemaAnalyzer.csprojxxx new file mode 100644 index 0000000000..0354afc875 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/SchemaAnalyzer.csprojxxx @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + 10.0 + + + + + + + \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths/TreeWriter.cs b/CsdlToPaths/CsdlToPaths/TreeWriter.cs new file mode 100644 index 0000000000..080a3f268a --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/TreeWriter.cs @@ -0,0 +1,77 @@ + +public class TreeWriter : IDisposable +{ + + private readonly TextWriter writer; + private readonly bool color; + + public TreeWriter(TextWriter writer, bool color) + { + this.writer = writer; + this.color = color; + } + + public void Display(Node node) + { + foreach (var (child, isLast) in node.Nodes.WithLast()) + { + WriteNode(child, indent: "", isLast: isLast); + } + writer.WriteLine(); + } + + // adapted from https://andrewlock.net/creating-an-ascii-art-tree-in-csharp/ + private void WriteNode(Node node, string indent, bool isLast) + { + // Print the provided pipes/spaces indent + Console.Write(indent); + + // Depending if this node is a last child, print the corner or cross, and + // calculate the indent that will be passed to its children + if (isLast) + { + writer.Write(CONFIG.LastChild); + indent += CONFIG.Space; + } + else + { + writer.Write(CONFIG.Child); + indent += CONFIG.Vertical; + } + if (color) + { + writer.WriteLine("{0} \x1b[36m{1}\x1b[m", node.Name, node.Type.Format()); + } + else + { + writer.WriteLine("{0} {1}", node.Name, node.Type.Format()); + } + + // Loop through the children recursively, passing in the + // indent, and the isLast parameter + foreach (var (child, isLastChild) in node.Nodes.WithLast()) + { + WriteNode(child, indent, isLastChild); + } + } + + public void Dispose() + { + ((IDisposable)writer).Dispose(); + } + + record struct Config( + string Child, string LastChild, string Vertical, string Space) + { } + + // Constants for indentation + // https://unicode-table.com/en/blocks/box-drawing/ + static Config[] CONFIGS = new[]{ + new Config("+- ", "+- ", "| ", " "), + new Config("├─ ", "└─ ", "│ ", " "), + new Config("╠═ ", "╚═ ", "║ ", " "), + new Config("╟─ ", "╙─ ", "║ ", " "), + }; + + static Config CONFIG = CONFIGS[1]; +} \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths/extensions/EnumerableExtensions.cs b/CsdlToPaths/CsdlToPaths/extensions/EnumerableExtensions.cs new file mode 100644 index 0000000000..be9e6ea6b1 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/extensions/EnumerableExtensions.cs @@ -0,0 +1,54 @@ + + + + +static class EnumerableExtensions +{ + public static IEnumerable<(T, FirstLast)> WithFirstLast(this IEnumerable items) + { + var enumerator = items.GetEnumerator(); + if (!enumerator.MoveNext()) { yield break; }; + var state = FirstLast.First; + var current = enumerator.Current; + while (enumerator.MoveNext()) + { + yield return (current, state); + state &= ~FirstLast.First; // not the first anymore since MoveNext succeeded twice + current = enumerator.Current; + } + state |= FirstLast.Last; // add the Last flag + yield return (current, state); + } + + public static IEnumerable<(T, bool)> WithLast(this IEnumerable items) + { + var enumerator = items.GetEnumerator(); + if (!enumerator.MoveNext()) { yield break; }; + var isLast = false; + var current = enumerator.Current; + while (enumerator.MoveNext()) + { + yield return (current, isLast); + current = enumerator.Current; + } + isLast = true; // add the isLast flag + yield return (current, isLast); + } + + public static bool TryGetSingle(this IEnumerable items, [MaybeNullWhen(false)] out T single) + { + using var enumerator = items.GetEnumerator(); + if (!enumerator.MoveNext()) + { + single = default; + return false; + } + single = enumerator.Current; + return !enumerator.MoveNext(); + } +} + + + +[Flags] +enum FirstLast { None = 0, First = 1, Last = 2 } diff --git a/CsdlToPaths/CsdlToPaths/extensions/IEdmModelExtensions.cs b/CsdlToPaths/CsdlToPaths/extensions/IEdmModelExtensions.cs new file mode 100644 index 0000000000..1f45c68b11 --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/extensions/IEdmModelExtensions.cs @@ -0,0 +1,67 @@ + +public static class IEdmModelExtensions +{ + + public static bool TryFindDeclaredType(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEdmType type) + { + type = model.FindDeclaredType(fqn); + return type != null; + } + + public static bool TryFindDeclaredEntityType(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEdmEntityType entityType) + { + if (model.TryFindDeclaredType(fqn, out var type) && type is IEdmEntityType entityType1) + { + entityType = entityType1; + return true; + } + entityType = default; + return false; + } + + + public static bool TryFindAllDerivedTypes(this IEdmModel model, IEdmEntityType baseType, out IEnumerable subTypes) + { + subTypes = model.FindAllDerivedTypes(baseType).Cast(); + return true; + } + + + + public static bool TryFindAllDerivedTypes(this IEdmModel model, IEdmComplexType baseType, out IEnumerable subTypes) + { + subTypes = model.FindAllDerivedTypes(baseType).Cast(); + return true; + } + + public static bool TryFindDeclaredEntitySubTypes(this IEdmModel model, string fqn, [MaybeNullWhen(false)] out IEnumerable subtypes) + { + if (model.TryFindDeclaredEntityType(fqn, out var baseType)) + { + subtypes = model.FindAllDerivedTypes(baseType).Cast(); + return true; + } + + subtypes = default; + return false; + } + + public static bool TryGetCollectionElementType(this IEdmType type, [MaybeNullWhen(false)] out IEdmEntityType elementType) + { + if (type is IEdmCollectionType collectionType && collectionType.ElementType.Definition is IEdmEntityType et) + { + elementType = et; + return true; + } + elementType = default; + return false; + } + + public static string Format(this IEdmType type) => type switch + { + IEdmCollectionType collectionType => $"[{Format(collectionType.ElementType.Definition)}]", + IEdmEntityType entityType => entityType.Name, + IEdmComplexType complexType => complexType.Name, + _ => type?.ToString() ?? "", + }; +} \ No newline at end of file diff --git a/CsdlToPaths/CsdlToPaths/extensions/StringExtensions.cs b/CsdlToPaths/CsdlToPaths/extensions/StringExtensions.cs new file mode 100644 index 0000000000..5c3b90939b --- /dev/null +++ b/CsdlToPaths/CsdlToPaths/extensions/StringExtensions.cs @@ -0,0 +1,25 @@ + + + + + + + +public static class StringExtensions +{ + public static string SeparatedBy(this IEnumerable items, string separator) + { + var sb = new StringBuilder(); + foreach (var item in items) + { + sb.Append(separator); + sb.Append(item); + } + return sb.ToString(); + } + + + public static string Capitalize(this string word) + => word[0..1].ToUpper() + word[1..]; + +} \ No newline at end of file