From 585447193dd950c8b691c80c8acdf43231ae637a Mon Sep 17 00:00:00 2001 From: mdnapo Date: Sun, 29 Sep 2024 19:57:31 +0200 Subject: [PATCH] Refactored Json2CSharpTranslator --- DevDisciples.Json.Tools.API/Program.cs | 16 ++- .../Properties/launchSettings.json | 4 +- .../Extensions/TranslationScopeExtensions.cs | 11 +++ .../Json2CSharpTranslator.ClassTranslation.cs | 14 +-- .../Json2CSharpTranslator.Context.cs | 23 ++++- .../Json2CSharpTranslator.cs | 99 +++++++++++-------- DevDisciples.Json.Tools/JsonPath.Parser.cs | 8 +- DevDisciples.Parsing/ParserContext.cs | 6 +- 8 files changed, 122 insertions(+), 59 deletions(-) create mode 100644 DevDisciples.Json.Tools/Extensions/TranslationScopeExtensions.cs diff --git a/DevDisciples.Json.Tools.API/Program.cs b/DevDisciples.Json.Tools.API/Program.cs index 78710ca..461f008 100644 --- a/DevDisciples.Json.Tools.API/Program.cs +++ b/DevDisciples.Json.Tools.API/Program.cs @@ -1,9 +1,21 @@ +using System.Threading.RateLimiting; using DevDisciples.Json.Tools.API; +using Microsoft.AspNetCore.RateLimiting; var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddRateLimiter(o => o + .AddSlidingWindowLimiter(policyName: "sliding", options => + { + options.PermitLimit = 10; // Maximum 10 requests per 1-second window + options.Window = TimeSpan.FromSeconds(1); + options.SegmentsPerWindow = 10; + options.QueueLimit = 2; + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + })); + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddControllers(); builder.Services.AddExceptionHandler(); @@ -22,6 +34,8 @@ builder.Services.AddSwaggerGen(); var app = builder.Build(); +app.UseRateLimiter(); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -35,6 +49,6 @@ app.UseHttpsRedirection(); app.UseCors("default"); -app.MapControllers(); +app.MapControllers().RequireRateLimiting("sliding"); app.Run(); \ No newline at end of file diff --git a/DevDisciples.Json.Tools.API/Properties/launchSettings.json b/DevDisciples.Json.Tools.API/Properties/launchSettings.json index 59e9482..338bdfe 100644 --- a/DevDisciples.Json.Tools.API/Properties/launchSettings.json +++ b/DevDisciples.Json.Tools.API/Properties/launchSettings.json @@ -12,7 +12,7 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "http://localhost:5000", "environmentVariables": { @@ -22,7 +22,7 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { diff --git a/DevDisciples.Json.Tools/Extensions/TranslationScopeExtensions.cs b/DevDisciples.Json.Tools/Extensions/TranslationScopeExtensions.cs new file mode 100644 index 0000000..4c2c98e --- /dev/null +++ b/DevDisciples.Json.Tools/Extensions/TranslationScopeExtensions.cs @@ -0,0 +1,11 @@ +using TranslationScope = DevDisciples.Json.Tools.Json2CSharpTranslator.Context.TranslationScope; + +namespace DevDisciples.Json.Tools.Extensions; + +public static class TranslationScopeExtensions +{ + public static bool IsChildOf(this Stack stack, TranslationScope type) + { + return stack.Count > 1 && stack.ElementAt(1) == type; + } +} \ No newline at end of file diff --git a/DevDisciples.Json.Tools/Json2CSharpTranslator.ClassTranslation.cs b/DevDisciples.Json.Tools/Json2CSharpTranslator.ClassTranslation.cs index 4714d7f..a36acbd 100644 --- a/DevDisciples.Json.Tools/Json2CSharpTranslator.ClassTranslation.cs +++ b/DevDisciples.Json.Tools/Json2CSharpTranslator.ClassTranslation.cs @@ -11,19 +11,21 @@ public static partial class Json2CSharpTranslator public void Translate(Context context) { - if (Properties.Count == 0) return; context.Builder.Append($"public class {Name.Pascalize()}\n"); context.Builder.Append("{\n"); - var last = Properties.Last(); - foreach (var property in Properties) + if (Properties.Count != 0) { - property.Translate(context); - context.Builder.Append(property.Equals(last) ? string.Empty : "\n"); + var last = Properties.Last(); + foreach (var property in Properties) + { + property.Translate(context); + context.Builder.Append(property.Equals(last) ? string.Empty : "\n"); + } } - context.Builder.Append("}\n"); + context.Builder.Append("}\n\n"); } } } \ No newline at end of file diff --git a/DevDisciples.Json.Tools/Json2CSharpTranslator.Context.cs b/DevDisciples.Json.Tools/Json2CSharpTranslator.Context.cs index a37b763..ced67b1 100644 --- a/DevDisciples.Json.Tools/Json2CSharpTranslator.Context.cs +++ b/DevDisciples.Json.Tools/Json2CSharpTranslator.Context.cs @@ -1,4 +1,5 @@ using System.Text; +using Humanizer; namespace DevDisciples.Json.Tools; @@ -6,14 +7,32 @@ public static partial class Json2CSharpTranslator { public class Context { + public enum TranslationScope + { + Array, + Object, + } + public const string DefaultRootClassName = "Root"; public const string DefaultNamespace = "My.Namespace"; public string RootClassName { get; init; } = DefaultRootClassName; public string Namespace { get; init; } = DefaultNamespace; - public string CurrentName { get; set; } = string.Empty; - + public Stack Name { get; init; } = new(); + public Stack Class { get; init; } = new(); + public Stack Scope { get; init; } = new(); public List Classes { get; } = new(); public readonly StringBuilder Builder = new(); + + public string CurrentClassName => + string.Join( + string.Empty, + Class + .SkipLast(1) + .Select(cls => cls.Name.Pascalize().Singularize()) + .Prepend(Name.Peek().Pascalize().Singularize()) + .Reverse() + .ToArray() + ); } } \ No newline at end of file diff --git a/DevDisciples.Json.Tools/Json2CSharpTranslator.cs b/DevDisciples.Json.Tools/Json2CSharpTranslator.cs index a8c4776..b9f6a63 100644 --- a/DevDisciples.Json.Tools/Json2CSharpTranslator.cs +++ b/DevDisciples.Json.Tools/Json2CSharpTranslator.cs @@ -1,5 +1,6 @@ using DevDisciples.Json.Parser; using DevDisciples.Json.Parser.Syntax; +using DevDisciples.Json.Tools.Extensions; using DevDisciples.Parsing; using DevDisciples.Parsing.Extensions; using Humanizer; @@ -26,7 +27,7 @@ public static partial class Json2CSharpTranslator throw new SyntaxException("Expected a JSON object."); context ??= new(); - context.CurrentName = context.RootClassName; + context.Name.Push(context.RootClassName); var visitor = Visitors[typeof(JsonObjectSyntax)]; visitor(root, context); @@ -35,59 +36,93 @@ public static partial class Json2CSharpTranslator context.Builder.Append("//using System.Collections.Generic;\n"); context.Builder.Append('\n'); context.Builder.Append($"namespace {context.Namespace};\n\n"); + context.Classes.Reverse(); context.Classes.ForEach(@class => @class.Translate(context)); + context.Name.Pop(); + return context.Builder.ToString(); } + private static ITranslation Translate(IJsonSyntax visitee, Context context) + { + return Visitors[visitee.GetType()](visitee, context); + } + private static ITranslation Object(IJsonSyntax visitee, Context context) { var @object = visitee.As(); - var @class = new ClassTranslation - { - Name = context.CurrentName, - Properties = new() - }; + var @class = new ClassTranslation { Name = context.CurrentClassName, Properties = new() }; - context.Classes.Add(@class); + context.Class.Push(@class); + + context.Scope.Push(Context.TranslationScope.Object); foreach (var prop in @object.Properties) { - context.CurrentName = prop.Key.Lexeme; - var visitor = Visitors[prop.Value.GetType()]; - var translation = visitor(prop.Value, context); + context.Name.Push(prop.Key.Lexeme); + + var translation = Translate(prop.Value, context); switch (translation) { case ClassTranslation: - @class.Properties.Add(new PropertyTranslation + // TODO: Handle class exists + context.Class.Peek().Properties.Add(new PropertyTranslation { Type = translation.Name, - Name = translation.Name, + Name = prop.Key.Lexeme, }); break; + case PropertyTranslation property: - @class.Properties.Add(property); + // TODO: Handle property exists + context.Class.Peek().Properties.Add(property); break; } + + context.Name.Pop(); } - return @class; + if (!context.Scope.IsChildOf(Context.TranslationScope.Array)) + { + context.Classes.Add(@class); + } + + context.Scope.Pop(); + + return context.Class.Pop(); } private static ITranslation Array(IJsonSyntax visitee, Context context) { var array = visitee.As(); + var type = "object"; + context.Scope.Push(Context.TranslationScope.Array); + if (array.Elements.Length == 0) { type = "object"; } else if (array.Elements.All(e => e is JsonObjectSyntax)) { - context.Classes.Add(SquashObjects(context.CurrentName, array.Elements, context)); - type = context.Classes.Last().Name; + type = context.CurrentClassName; + + var composite = array.Elements + .Select(el => Translate(el, context)) + .Cast(); + + var @class = new ClassTranslation + { + Name = type, + Properties = composite + .SelectMany(cls => cls.Properties) + .ToList(), + }; + + context.Classes.Add(@class); } else if (array.Elements.All(e => e is JsonStringSyntax es && DateTime.TryParse(es.Value, out _))) { @@ -102,10 +137,12 @@ public static partial class Json2CSharpTranslator type = array.Elements.Any(e => ((JsonNumberSyntax)e).Token.Lexeme.Contains('.')) ? "double" : "int"; } + context.Scope.Pop(); + return new PropertyTranslation { Type = $"List<{type}>", - Name = context.CurrentName, + Name = context.Name.Peek(), }; } @@ -117,7 +154,7 @@ public static partial class Json2CSharpTranslator return new PropertyTranslation { Type = type, - Name = context.CurrentName, + Name = context.Name.Peek(), }; } @@ -126,7 +163,7 @@ public static partial class Json2CSharpTranslator return new PropertyTranslation { Type = visitee.As().Token.Lexeme.Contains('.') ? "double" : "int", - Name = context.CurrentName, + Name = context.Name.Peek(), }; } @@ -135,7 +172,7 @@ public static partial class Json2CSharpTranslator return new PropertyTranslation { Type = "bool", - Name = context.CurrentName, + Name = context.Name.Peek(), }; } @@ -144,27 +181,7 @@ public static partial class Json2CSharpTranslator return new PropertyTranslation { Type = "object", - Name = context.CurrentName, + Name = context.Name.Peek(), }; } - - private static ClassTranslation SquashObjects(string className, IEnumerable objects, Context context) - { - var classes = objects - .Select(@object => Object(@object, context)) - .ToArray(); - - var squashed = new ClassTranslation - { - Name = className.Singularize(), - Properties = new() - }; - - foreach (var @class in classes) - foreach (var prop in ((ClassTranslation)@class).Properties) - if (squashed.Properties.All(p => p.Name != prop.Name)) - squashed.Properties.Add(prop); - - return squashed; - } } \ No newline at end of file diff --git a/DevDisciples.Json.Tools/JsonPath.Parser.cs b/DevDisciples.Json.Tools/JsonPath.Parser.cs index 78b5820..461d0d8 100644 --- a/DevDisciples.Json.Tools/JsonPath.Parser.cs +++ b/DevDisciples.Json.Tools/JsonPath.Parser.cs @@ -28,7 +28,7 @@ public static partial class JsonPath if (context.Match(JsonPathToken.LeftBracket)) return IndexAccessorExpression(context); - throw context.Error($"Invalid expression '{context.Current.Lexeme}'."); + throw context.SyntaxException($"Invalid expression '{context.Current.Lexeme}'."); } private static IJsonPathSyntax PropertyAccessorExpression(Context context) @@ -43,7 +43,7 @@ public static partial class JsonPath else if (context.Match(JsonPathToken.Identifier)) getter = new PropertySyntax(context.Previous()); - else throw context.Error("Expected a getter expression"); + else throw context.SyntaxException("Expected a getter expression"); return new PropertyAccessorSyntax(token, getter); } @@ -63,7 +63,7 @@ public static partial class JsonPath else if (context.Match(JsonPathToken.String)) syntax = ObjectIndexExpression(token, context); - else throw context.Error("Expected an index expression."); + else throw context.SyntaxException("Expected an index expression."); context.Consume(JsonPathToken.RightBracket, "Expected ']' after index expression."); @@ -82,7 +82,7 @@ public static partial class JsonPath { index = context.Consume(JsonPathToken.Number, "Invalid array index."); - if (!int.TryParse(index.Lexeme, out _)) throw context.Error(index, "Invalid array index."); + if (!int.TryParse(index.Lexeme, out _)) throw context.SyntaxException(index, "Invalid array index."); indexes.Add(index); } while (!context.Ended() && context.Match(JsonPathToken.Comma)); diff --git a/DevDisciples.Parsing/ParserContext.cs b/DevDisciples.Parsing/ParserContext.cs index 2b7a716..8571367 100644 --- a/DevDisciples.Parsing/ParserContext.cs +++ b/DevDisciples.Parsing/ParserContext.cs @@ -86,15 +86,15 @@ public class ParserContext : ParsableStream.Token> where T public Lexer.Token Consume(TToken type, string message) { if (Check(type)) return Advance(); - throw Error(message); + throw SyntaxException(message); } - public Exception Error(string message) + public Exception SyntaxException(string message) { return new SyntaxException(Report.FormatMessage(Current, message)); } - public Exception Error(Lexer.Token token, string message) + public Exception SyntaxException(Lexer.Token token, string message) { return new SyntaxException(Report.FormatMessage(token, message)); }