Compare commits

...

2 Commits

Author SHA1 Message Date
585447193d Refactored Json2CSharpTranslator 2024-09-29 19:57:31 +02:00
dc7fda1eed Add JSON path to CLI and refactored 2024-09-28 13:29:53 +02:00
43 changed files with 308 additions and 181 deletions

View File

@ -1,4 +1,5 @@
using DevDisciples.Json.Parser.Syntax; using DevDisciples.Json.Parser.Syntax;
using DevDisciples.Parsing.Extensions;
namespace DevDisciples.Json.Parser.Tests; namespace DevDisciples.Json.Parser.Tests;
@ -110,7 +111,7 @@ public class JsonParserTests
var node = JsonParser.Parse(nameof(Can_parse_an_object_with_one_entry), source); var node = JsonParser.Parse(nameof(Can_parse_an_object_with_one_entry), source);
Assert.That(node is JsonObjectSyntax { Properties.Length: 1 }); Assert.That(node is JsonObjectSyntax { Properties.Length: 1 });
var @object = (JsonObjectSyntax)node; var @object = node.As<JsonObjectSyntax>();
Assert.That(@object.Properties.Any(property => property.Key.Lexeme == "first_name")); Assert.That(@object.Properties.Any(property => property.Key.Lexeme == "first_name"));
Assert.That(@object.Properties.First(property => property.Key.Lexeme == "first_name").Value is JsonStringSyntax { Value: "John" }); Assert.That(@object.Properties.First(property => property.Key.Lexeme == "first_name").Value is JsonStringSyntax { Value: "John" });
} }
@ -124,7 +125,7 @@ public class JsonParserTests
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(node is JsonObjectSyntax { Properties.Length: 2 }); Assert.That(node is JsonObjectSyntax { Properties.Length: 2 });
var @object = (JsonObjectSyntax)node; var @object = node.As<JsonObjectSyntax>();
Assert.That(@object.Properties.Any(property => property.Key.Lexeme == "first_name")); Assert.That(@object.Properties.Any(property => property.Key.Lexeme == "first_name"));
Assert.That(@object.Properties.First(property => property.Key.Lexeme == "first_name").Value is JsonStringSyntax { Value: "John" }); Assert.That(@object.Properties.First(property => property.Key.Lexeme == "first_name").Value is JsonStringSyntax { Value: "John" });
Assert.That(@object.Properties.Any(property => property.Key.Lexeme == "last_name")); Assert.That(@object.Properties.Any(property => property.Key.Lexeme == "last_name"));

View File

@ -33,7 +33,7 @@ public static partial class JsonParser
if (ctx.Match(JsonToken.True) || ctx.Match(JsonToken.False)) if (ctx.Match(JsonToken.True) || ctx.Match(JsonToken.False))
return BoolExpression(ctx); return BoolExpression(ctx);
throw Report.Error(ctx.Current, $"Expected a JSON expression, got '{ctx.Current.Lexeme}'"); throw Report.SyntaxException(ctx.Current, $"Expected a JSON expression, got '{ctx.Current.Lexeme}'");
} }
private static IJsonSyntax ArrayExpression(ParserContext<JsonToken> ctx) private static IJsonSyntax ArrayExpression(ParserContext<JsonToken> ctx)

View File

@ -17,8 +17,8 @@ public class TransformController : ControllerBase
return Ok(new { Result = result }); return Ok(new { Result = result });
} }
[HttpPost("prettify")] [HttpPost("beautify")]
public IActionResult Prettify([FromBody] PrettifyRequest request) public IActionResult Beautify([FromBody] BeautifyRequest request)
{ {
var context = new JsonFormatter.Context { Beautify = true, IndentSize = request.IndentSize }; var context = new JsonFormatter.Context { Beautify = true, IndentSize = request.IndentSize };

View File

@ -23,7 +23,7 @@ public class GlobalExceptionHandler : IExceptionHandler
{ {
switch (exception) switch (exception)
{ {
case ParsingException: case SyntaxException:
_logger.LogError(exception, "An exception occurred: {Message}", exception.Message); _logger.LogError(exception, "An exception occurred: {Message}", exception.Message);
var problem = new ProblemDetails var problem = new ProblemDetails

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

@ -1,6 +1,6 @@
namespace DevDisciples.Json.Tools.API.Requests; namespace DevDisciples.Json.Tools.API.Requests;
public class PrettifyRequest : TransformRequest public class BeautifyRequest : TransformRequest
{ {
public int IndentSize { get; init; } = JsonFormatter.Context.DefaultIndentSize; public int IndentSize { get; init; } = JsonFormatter.Context.DefaultIndentSize;
} }

View File

@ -1,13 +1,13 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import {HomeComponent} from "./home/home.component"; import {HomeComponent} from "./home/home.component";
import {PrettifyComponent} from "./prettify/prettify.component"; import {BeautifyComponent} from "./beautify/beautify.component";
import {UglifyComponent} from "./uglify/uglify.component"; import {UglifyComponent} from "./uglify/uglify.component";
import {Json2CsharpComponent} from "./json2csharp/json2-csharp.component"; import {Json2CsharpComponent} from "./json2csharp/json2-csharp.component";
import {JsonPathComponent} from "./json-path/json-path.component"; import {JsonPathComponent} from "./json-path/json-path.component";
export const routes: Routes = [ export const routes: Routes = [
{ path: 'home', component: HomeComponent }, { path: 'home', component: HomeComponent },
{ path: 'prettify', component: PrettifyComponent }, { path: 'beautify', component: BeautifyComponent },
{ path: 'uglify', component: UglifyComponent }, { path: 'uglify', component: UglifyComponent },
{ path: 'json2csharp', component: Json2CsharpComponent }, { path: 'json2csharp', component: Json2CsharpComponent },
{ path: 'jsonpath', component: JsonPathComponent }, { path: 'jsonpath', component: JsonPathComponent },

View File

@ -1,4 +1,4 @@
<h1>JSON Prettify</h1> <h1>JSON Beautify</h1>
<app-input-output [input]="$input.value" <app-input-output [input]="$input.value"
[inputOptions]="inputOptions" [inputOptions]="inputOptions"

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PrettifyComponent } from './prettify.component'; import { BeautifyComponent } from './beautify.component';
describe('PrettifyComponent', () => { describe('BeautifyComponent', () => {
let component: PrettifyComponent; let component: BeautifyComponent;
let fixture: ComponentFixture<PrettifyComponent>; let fixture: ComponentFixture<BeautifyComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PrettifyComponent] imports: [BeautifyComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(PrettifyComponent); fixture = TestBed.createComponent(BeautifyComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -12,17 +12,17 @@ import {
import {BehaviorSubject, debounceTime} from "rxjs"; import {BehaviorSubject, debounceTime} from "rxjs";
@Component({ @Component({
selector: 'app-prettify', selector: 'app-beautify',
standalone: true, standalone: true,
imports: [ imports: [
EditorComponent, EditorComponent,
FormsModule, FormsModule,
InputOutputComponent InputOutputComponent
], ],
templateUrl: './prettify.component.html', templateUrl: './beautify.component.html',
styleUrl: './prettify.component.scss' styleUrl: './beautify.component.scss'
}) })
export class PrettifyComponent implements OnInit { export class BeautifyComponent implements OnInit {
// input: string = ; // input: string = ;
$input: BehaviorSubject<string> = new BehaviorSubject<string>(GenerateDefaultJsonObjectString()); $input: BehaviorSubject<string> = new BehaviorSubject<string>(GenerateDefaultJsonObjectString());
inputOptions = MonacoJsonConfig; inputOptions = MonacoJsonConfig;
@ -43,7 +43,7 @@ export class PrettifyComponent implements OnInit {
update(input: string): void { update(input: string): void {
this.service this.service
.prettify(input) .beautify(input)
.subscribe({ .subscribe({
next: response => { next: response => {
console.log(response); console.log(response);

View File

@ -11,8 +11,8 @@ export class JsonTransformService {
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
} }
public prettify(source: string): Observable<HttpResponse<any>> { public beautify(source: string): Observable<HttpResponse<any>> {
return this.http.post(`${this.url}/api/transform/prettify`, {Source: source}, {observe: "response"}); return this.http.post(`${this.url}/api/transform/beautify`, {Source: source}, {observe: "response"});
} }
public uglify(source: string): Observable<HttpResponse<any>> { public uglify(source: string): Observable<HttpResponse<any>> {

View File

@ -3,7 +3,7 @@
<mat-toolbar>Menu</mat-toolbar> <mat-toolbar>Menu</mat-toolbar>
<mat-nav-list> <mat-nav-list>
<mat-list-item routerLink="/home">Home</mat-list-item> <mat-list-item routerLink="/home">Home</mat-list-item>
<mat-list-item routerLink="/prettify">Prettify</mat-list-item> <mat-list-item routerLink="/beautify">Beautify</mat-list-item>
<mat-list-item routerLink="/uglify">Uglify</mat-list-item> <mat-list-item routerLink="/uglify">Uglify</mat-list-item>
<mat-list-item routerLink="/json2csharp">JSON to C#</mat-list-item> <mat-list-item routerLink="/json2csharp">JSON to C#</mat-list-item>
<mat-list-item routerLink="/jsonpath">JSON Path</mat-list-item> <mat-list-item routerLink="/jsonpath">JSON Path</mat-list-item>

View File

@ -2,7 +2,7 @@ namespace DevDisciples.Json.Tools.CLI;
public class CommandOptions public class CommandOptions
{ {
public InputOptions Input { get; set; } public InputSource Input { get; set; }
public FileInfo? InputFile { get; set; } public FileInfo? InputFile { get; set; }
public string? InputArgument { get; set; } public string? InputArgument { get; set; }
public OutputOptions Output { get; set; } public OutputOptions Output { get; set; }

View File

@ -4,13 +4,12 @@ namespace DevDisciples.Json.Tools.CLI.Extensions;
public static class CommandExtensions public static class CommandExtensions
{ {
public static void AddIOCommandOptions(this Command command) public static void AddInputOutputCommandOptions(this Command command)
{ {
command.AddOption(SharedCommandOptions.InputOption); command.AddOption(SharedCommandOptions.InputOption);
command.AddOption(SharedCommandOptions.InputFileOption); command.AddOption(SharedCommandOptions.InputFileOption);
command.AddArgument(SharedCommandOptions.InputArgument);
command.AddOption(SharedCommandOptions.OutputOption); command.AddOption(SharedCommandOptions.OutputOption);
command.AddOption(SharedCommandOptions.OutputFileOption); command.AddOption(SharedCommandOptions.OutputFileOption);
command.AddArgument(SharedCommandOptions.InputArgument);
} }
} }

View File

@ -2,28 +2,28 @@
namespace DevDisciples.Json.Tools.CLI; namespace DevDisciples.Json.Tools.CLI;
public static class IOHandler public static class InputOutputHandler
{ {
public static async Task<string> HandleInput(InputOptions input, string? inputArgument, FileInfo? inputFile) public static async Task<string> HandleInput(InputSource input, string? inputArgument, FileInfo? inputFile)
{ {
string json; string json;
switch (input) switch (input)
{ {
case InputOptions.s: case InputSource.s:
case InputOptions.std: case InputSource.std:
case InputOptions.stdin: case InputSource.stdin:
json = inputArgument ?? string.Empty; json = inputArgument ?? string.Empty;
break; break;
case InputOptions.c: case InputSource.c:
case InputOptions.clip: case InputSource.clip:
case InputOptions.clipboard: case InputSource.clipboard:
json = await ClipboardService.GetTextAsync() ?? string.Empty; json = await ClipboardService.GetTextAsync() ?? string.Empty;
break; break;
case InputOptions.f: case InputSource.f:
case InputOptions.file: case InputSource.file:
if (inputFile is null) if (inputFile is null)
throw new ArgumentException("Input file was not specified."); throw new ArgumentException("Input file was not specified.");

View File

@ -1,6 +1,6 @@
namespace DevDisciples.Json.Tools.CLI; namespace DevDisciples.Json.Tools.CLI;
public enum InputOptions public enum InputSource
{ {
// Standard input // Standard input
stdin, stdin,

View File

@ -23,7 +23,7 @@ public partial class Json2CSharpCommand : Command
AddAlias("2cs"); AddAlias("2cs");
AddAlias("2c#"); AddAlias("2c#");
this.AddIOCommandOptions(); this.AddInputOutputCommandOptions();
AddOption(RootClassNameOption); AddOption(RootClassNameOption);
AddOption(NamespaceOption); AddOption(NamespaceOption);
@ -32,7 +32,7 @@ public partial class Json2CSharpCommand : Command
private static async Task ExecuteAsync(CommandOptions options) private static async Task ExecuteAsync(CommandOptions options)
{ {
var json = await IOHandler.HandleInput(options.Input, options.InputArgument, options.InputFile); var json = await InputOutputHandler.HandleInput(options.Input, options.InputArgument, options.InputFile);
var output = Json2CSharpTranslator.Translate(json, new() var output = Json2CSharpTranslator.Translate(json, new()
{ {
@ -40,6 +40,6 @@ public partial class Json2CSharpCommand : Command
Namespace = options.Namespace ?? Json2CSharpTranslator.Context.DefaultNamespace Namespace = options.Namespace ?? Json2CSharpTranslator.Context.DefaultNamespace
}); });
await IOHandler.HandleOutput(options.Output, options.OutputFile, output); await InputOutputHandler.HandleOutput(options.Output, options.OutputFile, output);
} }
} }

View File

@ -1,6 +1,6 @@
namespace DevDisciples.Json.Tools.CLI; namespace DevDisciples.Json.Tools.CLI;
public partial class JsonPrettifyCommand public partial class JsonBeautifyCommand
{ {
public class CommandOptions : CLI.CommandOptions public class CommandOptions : CLI.CommandOptions
{ {

View File

@ -3,7 +3,7 @@ using DevDisciples.Json.Tools.CLI.Extensions;
namespace DevDisciples.Json.Tools.CLI; namespace DevDisciples.Json.Tools.CLI;
public partial class JsonPrettifyCommand public partial class JsonBeautifyCommand
{ {
public class CommandOptionsBinder : BinderBase<CommandOptions> public class CommandOptionsBinder : BinderBase<CommandOptions>
{ {

View File

@ -3,23 +3,22 @@ using DevDisciples.Json.Tools.CLI.Extensions;
namespace DevDisciples.Json.Tools.CLI; namespace DevDisciples.Json.Tools.CLI;
public partial class JsonPrettifyCommand : Command public partial class JsonBeautifyCommand : Command
{ {
private static readonly Option<int> IndentSizeOption = private static readonly Option<int> IndentSizeOption =
new(aliases: ["--indent", "--is"], description: "The indent size", getDefaultValue: () => 2); new(aliases: ["--indent", "--is"], description: "The indent size", getDefaultValue: () => 2);
public JsonPrettifyCommand() : base("prettify", "Prettify JSON") public JsonBeautifyCommand() : base("beautify", "Beautify JSON")
{ {
AddAlias("p"); AddAlias("b");
this.AddIOCommandOptions(); this.AddInputOutputCommandOptions();
AddOption(IndentSizeOption); AddOption(IndentSizeOption);
this.SetHandler(ExecuteAsync, new CommandOptionsBinder()); this.SetHandler(ExecuteAsync, new CommandOptionsBinder());
} }
private static async Task ExecuteAsync(CommandOptions options) private static async Task ExecuteAsync(CommandOptions options)
{ {
var json = await IOHandler.HandleInput(options.Input, options.InputArgument, options.InputFile); var json = await InputOutputHandler.HandleInput(options.Input, options.InputArgument, options.InputFile);
var output = JsonFormatter.Format(json, new() var output = JsonFormatter.Format(json, new()
{ {
@ -27,6 +26,6 @@ public partial class JsonPrettifyCommand : Command
IndentSize = options.IndentSize IndentSize = options.IndentSize
}); });
await IOHandler.HandleOutput(options.Output, options.OutputFile, output); await InputOutputHandler.HandleOutput(options.Output, options.OutputFile, output);
} }
} }

View File

@ -0,0 +1,9 @@
namespace DevDisciples.Json.Tools.CLI;
public partial class JsonPathCommand
{
public class CommandOptions : CLI.CommandOptions
{
public string PathExpression { get; init; }
}
}

View File

@ -0,0 +1,16 @@
using System.CommandLine.Binding;
using DevDisciples.Json.Tools.CLI.Extensions;
namespace DevDisciples.Json.Tools.CLI;
public partial class JsonPathCommand
{
public class CommandOptionsBinder : BinderBase<CommandOptions>
{
protected override CommandOptions GetBoundValue(BindingContext context) =>
new CommandOptions
{
PathExpression = context.ParseResult.GetValueForOption(PathExpressionOption)
}.WithCommonBindings(context);
}
}

View File

@ -0,0 +1,32 @@
using System.CommandLine;
using DevDisciples.Json.Tools.CLI.Extensions;
namespace DevDisciples.Json.Tools.CLI;
public partial class JsonPathCommand : Command
{
private static readonly Option<string> PathExpressionOption =
new(
aliases: ["--expr", "-e"],
description: "The path expression", getDefaultValue: () => "$"
) { IsRequired = true };
public JsonPathCommand() : base("path", "Evaluate a JSON path expression")
{
AddAlias("p");
this.AddInputOutputCommandOptions();
this.AddOption(PathExpressionOption);
this.SetHandler(ExecuteAsync, new CommandOptionsBinder());
}
private static async Task ExecuteAsync(CommandOptions options)
{
var json = await InputOutputHandler.HandleInput(options.Input, options.InputArgument, options.InputFile);
var output = JsonPath.Interpreter.Evaluate(json, options.PathExpression);
var beautifiedOutput = JsonFormatter.Format(output, new() { Beautify = true });
await InputOutputHandler.HandleOutput(options.Output, options.OutputFile, beautifiedOutput);
}
}

View File

@ -8,16 +8,16 @@ public class JsonUglifyCommand : Command
public JsonUglifyCommand() : base("uglify", "Uglify JSON") public JsonUglifyCommand() : base("uglify", "Uglify JSON")
{ {
AddAlias("u"); AddAlias("u");
this.AddIOCommandOptions(); this.AddInputOutputCommandOptions();
this.SetHandler(ExecuteAsync, new CommandOptionsBinder()); this.SetHandler(ExecuteAsync, new CommandOptionsBinder());
} }
private static async Task ExecuteAsync(CommandOptions options) private static async Task ExecuteAsync(CommandOptions options)
{ {
var json = await IOHandler.HandleInput(options.Input, options.InputArgument, options.InputFile); var json = await InputOutputHandler.HandleInput(options.Input, options.InputArgument, options.InputFile);
var output = JsonFormatter.Format(json, new() { Beautify = false }); var output = JsonFormatter.Format(json, new() { Beautify = false });
await IOHandler.HandleOutput(options.Output, options.OutputFile, output); await InputOutputHandler.HandleOutput(options.Output, options.OutputFile, output);
} }
} }

View File

@ -7,7 +7,8 @@ public class RootCommand : System.CommandLine.RootCommand
public RootCommand() : base("A JSON transform CLI tool by DevDisciples.") public RootCommand() : base("A JSON transform CLI tool by DevDisciples.")
{ {
AddCommand(new JsonUglifyCommand()); AddCommand(new JsonUglifyCommand());
AddCommand(new JsonPrettifyCommand()); AddCommand(new JsonBeautifyCommand());
AddCommand(new Json2CSharpCommand()); AddCommand(new Json2CSharpCommand());
AddCommand(new JsonPathCommand());
} }
} }

View File

@ -4,8 +4,8 @@ namespace DevDisciples.Json.Tools.CLI;
public static class SharedCommandOptions public static class SharedCommandOptions
{ {
public static readonly Option<InputOptions> InputOption = public static readonly Option<InputSource> InputOption =
new(aliases: ["--input", "-i"], getDefaultValue: () => InputOptions.stdin) { IsRequired = false }; new(aliases: ["--input", "-i"], getDefaultValue: () => InputSource.stdin) { IsRequired = false };
public static readonly Argument<string?> InputArgument = public static readonly Argument<string?> InputArgument =
new(name: "input", description: "The input argument.", getDefaultValue: () => default); new(name: "input", description: "The input argument.", getDefaultValue: () => default);

0
DevDisciples.Json.Tools.CLI/scripts/publish_as_tool.sh Normal file → Executable file
View File

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,17 +11,21 @@ public static partial class Json2CSharpTranslator
public void Translate(Context context) public void Translate(Context context)
{ {
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,6 +1,8 @@
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 Humanizer; using Humanizer;
namespace DevDisciples.Json.Tools; namespace DevDisciples.Json.Tools;
@ -19,13 +21,13 @@ public static partial class Json2CSharpTranslator
Visitors.Register<JsonNullSyntax>(Null); Visitors.Register<JsonNullSyntax>(Null);
} }
public static string Translate(string source, Context? context = null) public static string Translate(string input, Context? context = null)
{ {
if (JsonParser.Parse("<source>", source) is not JsonObjectSyntax root) if (JsonParser.Parse("<input>", input) is not JsonObjectSyntax root)
throw new ParsingException("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);
@ -34,55 +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 = (JsonObjectSyntax)visitee; 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 = (JsonArraySyntax)visitee; var array = visitee.As<JsonArraySyntax>();
var type = "object"; var type = "object";
if (array.Elements.All(e => e is JsonObjectSyntax)) context.Scope.Push(Context.TranslationScope.Array);
if (array.Elements.Length == 0)
{ {
context.Classes.Add(SquashObjects(context.CurrentName, array.Elements, context)); type = "object";
type = context.Classes.Last().Name; }
else if (array.Elements.All(e => e is JsonObjectSyntax))
{
type = context.CurrentClassName;
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 _)))
{ {
@ -97,22 +137,24 @@ 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(),
}; };
} }
private static ITranslation String(IJsonSyntax visitee, Context context) private static ITranslation String(IJsonSyntax visitee, Context context)
{ {
var @string = (JsonStringSyntax)visitee; var @string = visitee.As<JsonStringSyntax>();
var type = DateTime.TryParse(@string.Value, out _) ? "DateTime" : "string"; var type = DateTime.TryParse(@string.Value, out _) ? "DateTime" : "string";
return new PropertyTranslation return new PropertyTranslation
{ {
Type = type, Type = type,
Name = context.CurrentName, Name = context.Name.Peek(),
}; };
} }
@ -120,8 +162,8 @@ public static partial class Json2CSharpTranslator
{ {
return new PropertyTranslation return new PropertyTranslation
{ {
Type = ((JsonNumberSyntax)visitee).Token.Lexeme.Contains('.') ? "double" : "int", Type = visitee.As<JsonNumberSyntax>().Token.Lexeme.Contains('.') ? "double" : "int",
Name = context.CurrentName, Name = context.Name.Peek(),
}; };
} }
@ -130,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(),
}; };
} }
@ -139,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

@ -1,6 +1,7 @@
using DevDisciples.Json.Parser; using DevDisciples.Json.Parser;
using DevDisciples.Json.Parser.Syntax; using DevDisciples.Json.Parser.Syntax;
using DevDisciples.Parsing; using DevDisciples.Parsing;
using DevDisciples.Parsing.Extensions;
namespace DevDisciples.Json.Tools; namespace DevDisciples.Json.Tools;
@ -18,9 +19,9 @@ public static partial class JsonFormatter
Visitors.Register<JsonNullSyntax>(PrintNull); Visitors.Register<JsonNullSyntax>(PrintNull);
} }
public static string Format(string source, Context? context) public static string Format(string input, Context? context)
{ {
var node = JsonParser.Parse("<source>", source); var node = JsonParser.Parse("<input>", input);
return Format(node, context); return Format(node, context);
} }
@ -33,7 +34,7 @@ public static partial class JsonFormatter
private static void PrintArray(IJsonSyntax visitee, Context context) private static void PrintArray(IJsonSyntax visitee, Context context)
{ {
var array = (JsonArraySyntax)visitee; var array = visitee.As<JsonArraySyntax>();
context.Builder.Append($"[{context.NewLine}"); context.Builder.Append($"[{context.NewLine}");
context.IncrementDepth(); context.IncrementDepth();
@ -52,7 +53,7 @@ public static partial class JsonFormatter
private static void PrintObject(IJsonSyntax visitee, Context context) private static void PrintObject(IJsonSyntax visitee, Context context)
{ {
var @object = (JsonObjectSyntax)visitee; var @object = visitee.As<JsonObjectSyntax>();
context.Builder.Append($"{{{context.NewLine}"); context.Builder.Append($"{{{context.NewLine}");
context.IncrementDepth(); context.IncrementDepth();
@ -72,19 +73,19 @@ public static partial class JsonFormatter
private static void PrintString(IJsonSyntax visitee, Context context) private static void PrintString(IJsonSyntax visitee, Context context)
{ {
var @string = (JsonStringSyntax)visitee; var @string = visitee.As<JsonStringSyntax>();
context.Builder.Append($"\"{@string.Token.Lexeme}\""); context.Builder.Append($"\"{@string.Token.Lexeme}\"");
} }
private static void PrintNumber(IJsonSyntax visitee, Context context) private static void PrintNumber(IJsonSyntax visitee, Context context)
{ {
var number = (JsonNumberSyntax)visitee; var number = visitee.As<JsonNumberSyntax>();
context.Builder.Append($"{number.Value}"); context.Builder.Append($"{number.Value}");
} }
private static void PrintBool(IJsonSyntax visitee, Context context) private static void PrintBool(IJsonSyntax visitee, Context context)
{ {
var @bool = (JsonBoolSyntax)visitee; var @bool = visitee.As<JsonBoolSyntax>();
context.Builder.Append($"{@bool.Value.ToString().ToLower()}"); context.Builder.Append($"{@bool.Value.ToString().ToLower()}");
} }

View File

@ -13,21 +13,21 @@ public static partial class JsonPath
static Interpreter() static Interpreter()
{ {
Visitors.Register<WildCardSyntax>(WildCardExpression); Visitors.Register<WildCardSyntax>(WildCard);
Visitors.Register<PropertyAccessorSyntax>(PropertyAccessorExpression); Visitors.Register<PropertyAccessorSyntax>(PropertyAccessor);
Visitors.Register<PropertySyntax>(PropertyExpression); Visitors.Register<PropertySyntax>(Property);
Visitors.Register<ArrayIndexSyntax>(ArrayIndexExpression); Visitors.Register<ArrayIndexSyntax>(ArrayIndex);
Visitors.Register<ArrayIndexListSyntax>(ArrayIndexListExpression); Visitors.Register<ArrayIndexListSyntax>(ArrayIndexList);
Visitors.Register<ObjectIndexSyntax>(ObjectIndexExpression); Visitors.Register<ObjectIndexSyntax>(ObjectIndex);
Visitors.Register<ObjectIndexListSyntax>(ObjectIndexListExpression); Visitors.Register<ObjectIndexListSyntax>(ObjectIndexList);
} }
public static IJsonSyntax Evaluate(string source, string path) public static IJsonSyntax Evaluate(string input, string path)
{ {
var root = JsonParser.Parse("<json>", source); var root = JsonParser.Parse("<input>", input);
if (root is not JsonArraySyntax && root is not JsonObjectSyntax) if (root is not JsonArraySyntax && root is not JsonObjectSyntax)
throw Report.Error(root.Token, "Expected a JSON array or object."); throw Report.SyntaxException(root.Token, "Expected a JSON array or object.");
var expressions = Parser.Parse(path); var expressions = Parser.Parse(path);
@ -41,7 +41,7 @@ public static partial class JsonPath
private static void Evaluate(IJsonPathSyntax node, Context context) => Visitors[node.GetType()](node, context); private static void Evaluate(IJsonPathSyntax node, Context context) => Visitors[node.GetType()](node, context);
private static void WildCardExpression(IJsonPathSyntax visitee, Context context) private static void WildCard(IJsonPathSyntax visitee, Context context)
{ {
switch (context.Target) switch (context.Target)
{ {
@ -57,17 +57,17 @@ public static partial class JsonPath
default: default:
var syntax = visitee.As<WildCardSyntax>(); var syntax = visitee.As<WildCardSyntax>();
throw Report.Error(syntax.Token, "Invalid target for '*'."); throw Report.SyntaxException(syntax.Token, "Invalid target for '*'.");
} }
} }
private static void PropertyAccessorExpression(IJsonPathSyntax visitee, Context context) private static void PropertyAccessor(IJsonPathSyntax visitee, Context context)
{ {
var accessor = visitee.As<PropertyAccessorSyntax>(); var accessor = visitee.As<PropertyAccessorSyntax>();
Evaluate(accessor.Getter, context); Evaluate(accessor.Getter, context);
} }
private static void PropertyExpression(IJsonPathSyntax visitee, Context context) private static void Property(IJsonPathSyntax visitee, Context context)
{ {
var property = visitee.As<PropertySyntax>(); var property = visitee.As<PropertySyntax>();
@ -90,10 +90,10 @@ public static partial class JsonPath
} }
} }
private static void ArrayIndexExpression(IJsonPathSyntax visitee, Context context) private static void ArrayIndex(IJsonPathSyntax visitee, Context context)
{ {
if (context.Target is not JsonArraySyntax array) if (context.Target is not JsonArraySyntax array)
throw Report.Error(context.Target.Token, "Integer indexes are only allowed on arrays."); throw Report.SyntaxException(context.Target.Token, "Integer indexes are only allowed on arrays.");
var index = visitee.As<ArrayIndexSyntax>(); var index = visitee.As<ArrayIndexSyntax>();
@ -102,15 +102,15 @@ public static partial class JsonPath
if (value >= 0 && value < array.Elements.Length) if (value >= 0 && value < array.Elements.Length)
context.Target = array.Elements[value]; context.Target = array.Elements[value];
else throw Report.Error(index.Token, "Index out of range."); else throw Report.SyntaxException(index.Token, "Index out of range.");
} }
private static void ArrayIndexListExpression(IJsonPathSyntax visitee, Context context) private static void ArrayIndexList(IJsonPathSyntax visitee, Context context)
{ {
var indices = visitee.As<ArrayIndexListSyntax>(); var indices = visitee.As<ArrayIndexListSyntax>();
if (context.Target is not JsonArraySyntax array) if (context.Target is not JsonArraySyntax array)
throw Report.Error(indices.Token, "Integer indices are only allowed on arrays."); throw Report.SyntaxException(indices.Token, "Integer indices are only allowed on arrays.");
var list = new List<IJsonSyntax>(); var list = new List<IJsonSyntax>();
@ -125,10 +125,10 @@ public static partial class JsonPath
context.Target = new JsonArraySyntax(array.Token, list.ToArray()); context.Target = new JsonArraySyntax(array.Token, list.ToArray());
} }
private static void ObjectIndexExpression(IJsonPathSyntax visitee, Context context) private static void ObjectIndex(IJsonPathSyntax visitee, Context context)
{ {
if (context.Target is not JsonObjectSyntax @object) if (context.Target is not JsonObjectSyntax @object)
throw Report.Error(context.Target.Token, "String indices are only allowed on objects."); throw Report.SyntaxException(context.Target.Token, "String indices are only allowed on objects.");
var index = visitee.As<ObjectIndexSyntax>(); var index = visitee.As<ObjectIndexSyntax>();
@ -142,10 +142,10 @@ public static partial class JsonPath
context.Target = new JsonNullSyntax(@object.Token); context.Target = new JsonNullSyntax(@object.Token);
} }
private static void ObjectIndexListExpression(IJsonPathSyntax visitee, Context context) private static void ObjectIndexList(IJsonPathSyntax visitee, Context context)
{ {
if (context.Target is not JsonObjectSyntax @object) if (context.Target is not JsonObjectSyntax @object)
throw Report.Error(context.Target.Token, "Index strings are only allowed on objects."); throw Report.SyntaxException(context.Target.Token, "Index strings are only allowed on objects.");
var indices = visitee.As<ObjectIndexListSyntax>(); var indices = visitee.As<ObjectIndexListSyntax>();

View File

@ -8,7 +8,7 @@ public static partial class JsonPath
{ {
public static List<IJsonPathSyntax> Parse(string path) public static List<IJsonPathSyntax> Parse(string path)
{ {
var pathTokens = Lexer.Default.Lex("<path>", path); var pathTokens = Lexer.Default.Lex("<path_expression>", path);
var context = new Context(pathTokens.ToArray()); var context = new Context(pathTokens.ToArray());
context.Match(JsonPathToken.DollarSign); // Match the '$' eagerly. This will make it optional by default. context.Match(JsonPathToken.DollarSign); // Match the '$' eagerly. This will make it optional by default.
@ -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."); 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

@ -6,7 +6,6 @@ namespace DevDisciples.Json.Tools;
public static partial class JsonPath public static partial class JsonPath
{ {
public interface IJsonPathSyntax public interface IJsonPathSyntax
{ {
public Lexer<JsonPathToken>.Token Token { get; } public Lexer<JsonPathToken>.Token Token { get; }

View File

@ -73,7 +73,7 @@ public abstract partial class Lexer<TToken> where TToken : Enum
if (src.Ended()) if (src.Ended())
{ {
throw new ParsingException( throw new SyntaxException(
$"[line: {src.Line}, column: {src.Column}] Unterminated string near '{src.Last}'." $"[line: {src.Line}, column: {src.Column}] Unterminated string near '{src.Last}'."
); );
} }
@ -117,7 +117,7 @@ public abstract partial class Lexer<TToken> where TToken : Enum
if (src.Ended()) if (src.Ended())
{ {
throw new ParsingException( throw new SyntaxException(
$"[line: {src.Line}, column: {src.Column}] Unterminated string near '{src.Last}'." $"[line: {src.Line}, column: {src.Column}] Unterminated string near '{src.Last}'."
); );
} }

View File

@ -25,7 +25,7 @@ public abstract partial class Lexer<TToken> where TToken : Enum
if (!matched) if (!matched)
{ {
Report.Halt(ctx.Source, $"Unexpected character '{ctx.Source.Current}'."); Report.SyntaxHalt(ctx.Source, $"Unexpected character '{ctx.Source.Current}'.");
} }
} }

View File

@ -86,16 +86,16 @@ 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 ParsingException(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 ParsingException(Report.FormatMessage(token, message)); return new SyntaxException(Report.FormatMessage(token, message));
} }
} }

View File

@ -1,16 +0,0 @@
namespace DevDisciples.Parsing;
public class ParsingException : Exception
{
public ParsingException()
{
}
public ParsingException(string? message) : base(message)
{
}
public ParsingException(string? message, Exception? innerException) : base(message, innerException)
{
}
}

View File

@ -2,14 +2,14 @@
public static class Report public static class Report
{ {
public static ParsingException Error(ISourceLocation token, string message) public static SyntaxException SyntaxException(ISourceLocation token, string message)
{ {
return new ParsingException(FormatMessage(token, message)); return new SyntaxException(FormatMessage(token, message));
} }
public static void Halt(ISourceLocation token, string message) public static void SyntaxHalt(ISourceLocation token, string message)
{ {
throw new ParsingException(FormatMessage(token, message)); throw new SyntaxException(FormatMessage(token, message));
} }
public static string FormatMessage(ISourceLocation token, string msg) public static string FormatMessage(ISourceLocation token, string msg)

View File

@ -0,0 +1,16 @@
namespace DevDisciples.Parsing;
public class SyntaxException : Exception
{
public SyntaxException()
{
}
public SyntaxException(string? message) : base(message)
{
}
public SyntaxException(string? message, Exception? innerException) : base(message, innerException)
{
}
}