Refactored Json2CSharpTranslator

This commit is contained in:
mdnapo 2024-09-29 19:57:31 +02:00
parent dc7fda1eed
commit 585447193d
8 changed files with 122 additions and 59 deletions

View File

@ -1,9 +1,21 @@
using System.Threading.RateLimiting;
using DevDisciples.Json.Tools.API; using DevDisciples.Json.Tools.API;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // 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.AddEndpointsApiExplorer();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
@ -22,6 +34,8 @@ builder.Services.AddSwaggerGen();
var app = builder.Build(); var app = builder.Build();
app.UseRateLimiter();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {
@ -35,6 +49,6 @@ app.UseHttpsRedirection();
app.UseCors("default"); app.UseCors("default");
app.MapControllers(); app.MapControllers().RequireRateLimiting("sliding");
app.Run(); app.Run();

View File

@ -12,7 +12,7 @@
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": false,
"launchUrl": "swagger", "launchUrl": "swagger",
"applicationUrl": "http://localhost:5000", "applicationUrl": "http://localhost:5000",
"environmentVariables": { "environmentVariables": {
@ -22,7 +22,7 @@
"https": { "https": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": false,
"launchUrl": "swagger", "launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000", "applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": { "environmentVariables": {

View File

@ -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<TranslationScope> stack, TranslationScope type)
{
return stack.Count > 1 && stack.ElementAt(1) == type;
}
}

View File

@ -11,19 +11,21 @@ public static partial class Json2CSharpTranslator
public void Translate(Context context) public void Translate(Context context)
{ {
if (Properties.Count == 0) return;
context.Builder.Append($"public class {Name.Pascalize()}\n"); context.Builder.Append($"public class {Name.Pascalize()}\n");
context.Builder.Append("{\n"); context.Builder.Append("{\n");
var last = Properties.Last(); if (Properties.Count != 0)
foreach (var property in Properties)
{ {
property.Translate(context); var last = Properties.Last();
context.Builder.Append(property.Equals(last) ? string.Empty : "\n"); 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");
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System.Text; using System.Text;
using Humanizer;
namespace DevDisciples.Json.Tools; namespace DevDisciples.Json.Tools;
@ -6,14 +7,32 @@ public static partial class Json2CSharpTranslator
{ {
public class Context public class Context
{ {
public enum TranslationScope
{
Array,
Object,
}
public const string DefaultRootClassName = "Root"; public const string DefaultRootClassName = "Root";
public const string DefaultNamespace = "My.Namespace"; public const string DefaultNamespace = "My.Namespace";
public string RootClassName { get; init; } = DefaultRootClassName; public string RootClassName { get; init; } = DefaultRootClassName;
public string Namespace { get; init; } = DefaultNamespace; public string Namespace { get; init; } = DefaultNamespace;
public string CurrentName { get; set; } = string.Empty; public Stack<string> Name { get; init; } = new();
public Stack<ClassTranslation> Class { get; init; } = new();
public Stack<TranslationScope> Scope { get; init; } = new();
public List<ClassTranslation> Classes { get; } = new(); public List<ClassTranslation> Classes { get; } = new();
public readonly StringBuilder Builder = 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()
);
} }
} }

View File

@ -1,5 +1,6 @@
using DevDisciples.Json.Parser; using DevDisciples.Json.Parser;
using DevDisciples.Json.Parser.Syntax; using DevDisciples.Json.Parser.Syntax;
using DevDisciples.Json.Tools.Extensions;
using DevDisciples.Parsing; using DevDisciples.Parsing;
using DevDisciples.Parsing.Extensions; using DevDisciples.Parsing.Extensions;
using Humanizer; using Humanizer;
@ -26,7 +27,7 @@ public static partial class Json2CSharpTranslator
throw new SyntaxException("Expected a JSON object."); throw new SyntaxException("Expected a JSON object.");
context ??= new(); context ??= new();
context.CurrentName = context.RootClassName; context.Name.Push(context.RootClassName);
var visitor = Visitors[typeof(JsonObjectSyntax)]; var visitor = Visitors[typeof(JsonObjectSyntax)];
visitor(root, context); visitor(root, context);
@ -35,59 +36,93 @@ public static partial class Json2CSharpTranslator
context.Builder.Append("//using System.Collections.Generic;\n"); context.Builder.Append("//using System.Collections.Generic;\n");
context.Builder.Append('\n'); context.Builder.Append('\n');
context.Builder.Append($"namespace {context.Namespace};\n\n"); context.Builder.Append($"namespace {context.Namespace};\n\n");
context.Classes.Reverse();
context.Classes.ForEach(@class => @class.Translate(context)); context.Classes.ForEach(@class => @class.Translate(context));
context.Name.Pop();
return context.Builder.ToString(); 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) private static ITranslation Object(IJsonSyntax visitee, Context context)
{ {
var @object = visitee.As<JsonObjectSyntax>(); var @object = visitee.As<JsonObjectSyntax>();
var @class = new ClassTranslation var @class = new ClassTranslation { Name = context.CurrentClassName, Properties = new() };
{
Name = context.CurrentName,
Properties = new()
};
context.Classes.Add(@class); context.Class.Push(@class);
context.Scope.Push(Context.TranslationScope.Object);
foreach (var prop in @object.Properties) foreach (var prop in @object.Properties)
{ {
context.CurrentName = prop.Key.Lexeme; context.Name.Push(prop.Key.Lexeme);
var visitor = Visitors[prop.Value.GetType()];
var translation = visitor(prop.Value, context); var translation = Translate(prop.Value, context);
switch (translation) switch (translation)
{ {
case ClassTranslation: case ClassTranslation:
@class.Properties.Add(new PropertyTranslation // TODO: Handle class exists
context.Class.Peek().Properties.Add(new PropertyTranslation
{ {
Type = translation.Name, Type = translation.Name,
Name = translation.Name, Name = prop.Key.Lexeme,
}); });
break; break;
case PropertyTranslation property: case PropertyTranslation property:
@class.Properties.Add(property); // TODO: Handle property exists
context.Class.Peek().Properties.Add(property);
break; 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) private static ITranslation Array(IJsonSyntax visitee, Context context)
{ {
var array = visitee.As<JsonArraySyntax>(); var array = visitee.As<JsonArraySyntax>();
var type = "object"; var type = "object";
context.Scope.Push(Context.TranslationScope.Array);
if (array.Elements.Length == 0) if (array.Elements.Length == 0)
{ {
type = "object"; type = "object";
} }
else if (array.Elements.All(e => e is JsonObjectSyntax)) else if (array.Elements.All(e => e is JsonObjectSyntax))
{ {
context.Classes.Add(SquashObjects(context.CurrentName, array.Elements, context)); type = context.CurrentClassName;
type = context.Classes.Last().Name;
var composite = array.Elements
.Select(el => Translate(el, context))
.Cast<ClassTranslation>();
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 _))) 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"; type = array.Elements.Any(e => ((JsonNumberSyntax)e).Token.Lexeme.Contains('.')) ? "double" : "int";
} }
context.Scope.Pop();
return new PropertyTranslation return new PropertyTranslation
{ {
Type = $"List<{type}>", Type = $"List<{type}>",
Name = context.CurrentName, Name = context.Name.Peek(),
}; };
} }
@ -117,7 +154,7 @@ public static partial class Json2CSharpTranslator
return new PropertyTranslation return new PropertyTranslation
{ {
Type = type, Type = type,
Name = context.CurrentName, Name = context.Name.Peek(),
}; };
} }
@ -126,7 +163,7 @@ public static partial class Json2CSharpTranslator
return new PropertyTranslation return new PropertyTranslation
{ {
Type = visitee.As<JsonNumberSyntax>().Token.Lexeme.Contains('.') ? "double" : "int", Type = visitee.As<JsonNumberSyntax>().Token.Lexeme.Contains('.') ? "double" : "int",
Name = context.CurrentName, Name = context.Name.Peek(),
}; };
} }
@ -135,7 +172,7 @@ public static partial class Json2CSharpTranslator
return new PropertyTranslation return new PropertyTranslation
{ {
Type = "bool", Type = "bool",
Name = context.CurrentName, Name = context.Name.Peek(),
}; };
} }
@ -144,27 +181,7 @@ public static partial class Json2CSharpTranslator
return new PropertyTranslation return new PropertyTranslation
{ {
Type = "object", Type = "object",
Name = context.CurrentName, Name = context.Name.Peek(),
}; };
} }
private static ClassTranslation SquashObjects(string className, IEnumerable<IJsonSyntax> 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;
}
} }

View File

@ -28,7 +28,7 @@ public static partial class JsonPath
if (context.Match(JsonPathToken.LeftBracket)) if (context.Match(JsonPathToken.LeftBracket))
return IndexAccessorExpression(context); 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) private static IJsonPathSyntax PropertyAccessorExpression(Context context)
@ -43,7 +43,7 @@ public static partial class JsonPath
else if (context.Match(JsonPathToken.Identifier)) else if (context.Match(JsonPathToken.Identifier))
getter = new PropertySyntax(context.Previous()); 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); return new PropertyAccessorSyntax(token, getter);
} }
@ -63,7 +63,7 @@ public static partial class JsonPath
else if (context.Match(JsonPathToken.String)) else if (context.Match(JsonPathToken.String))
syntax = ObjectIndexExpression(token, context); 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."); 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."); 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); indexes.Add(index);
} while (!context.Ended() && context.Match(JsonPathToken.Comma)); } while (!context.Ended() && context.Match(JsonPathToken.Comma));

View File

@ -86,15 +86,15 @@ public class ParserContext<TToken> : ParsableStream<Lexer<TToken>.Token> where T
public Lexer<TToken>.Token Consume(TToken type, string message) public Lexer<TToken>.Token Consume(TToken type, string message)
{ {
if (Check(type)) return Advance(); 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)); return new SyntaxException(Report.FormatMessage(Current, message));
} }
public Exception Error(Lexer<TToken>.Token token, string message) public Exception SyntaxException(Lexer<TToken>.Token token, string message)
{ {
return new SyntaxException(Report.FormatMessage(token, message)); return new SyntaxException(Report.FormatMessage(token, message));
} }