- Refactored code

- Implemented basic JSON Path interpreter
- Clean up
This commit is contained in:
mdnapo 2024-09-22 15:25:38 +02:00
parent 78ebafb409
commit 82a59eebd2
60 changed files with 1003 additions and 475 deletions

View File

@ -1,4 +1,6 @@
namespace DevDisciples.Json.Parser.Tests;
using DevDisciples.Json.Parser.Syntax;
namespace DevDisciples.Json.Parser.Tests;
public class JsonParserTests
{
@ -12,7 +14,7 @@ public class JsonParserTests
{
const string source = "null";
var node = JsonParser.Parse(nameof(Can_parse_null), source);
Assert.That(node is JsonNull);
Assert.That(node is JsonNullSyntax);
}
[Test]
@ -25,8 +27,8 @@ public class JsonParserTests
Assert.Multiple(() =>
{
Assert.That(trueNode is JsonBool { Value: true });
Assert.That(falseNode is JsonBool { Value: false });
Assert.That(trueNode is JsonBoolSyntax { Value: true });
Assert.That(falseNode is JsonBoolSyntax { Value: false });
});
}
@ -35,15 +37,15 @@ public class JsonParserTests
{
const string source = "1";
var node = JsonParser.Parse(nameof(Can_parse_an_integer), source);
Assert.That(node is JsonNumber { Value: 1 });
Assert.That(node is JsonNumberSyntax { Value: 1 });
}
[Test]
public void Can_lex_a_decimal()
public void Can_parse_a_decimal()
{
const string source = "1.0";
var node = JsonParser.Parse(nameof(Can_lex_a_decimal), source);
Assert.That(node is JsonNumber { Value: 1.0 });
var node = JsonParser.Parse(nameof(Can_parse_a_decimal), source);
Assert.That(node is JsonNumberSyntax { Value: 1.0 });
}
[Test]
@ -51,7 +53,7 @@ public class JsonParserTests
{
const string source = "\"Hello world!\"";
var tokens = JsonParser.Parse(nameof(Can_parse_a_string), source);
Assert.That(tokens is JsonString { Value: "Hello world!" });
Assert.That(tokens is JsonStringSyntax { Value: "Hello world!" });
}
[Test]
@ -59,7 +61,7 @@ public class JsonParserTests
{
const string source = "[]";
var tokens = JsonParser.Parse(nameof(Can_parse_an_empty_array), source);
Assert.That(tokens is JsonArray);
Assert.That(tokens is JsonArraySyntax);
}
[Test]
@ -68,9 +70,9 @@ public class JsonParserTests
const string source = "[1]";
var tokens = JsonParser.Parse(nameof(Can_parse_a_single_item_array), source);
Assert.That(tokens is JsonArray
Assert.That(tokens is JsonArraySyntax
{
Elements: [JsonNumber { Value: 1 }]
Elements: [JsonNumberSyntax { Value: 1 }]
});
}
@ -80,15 +82,15 @@ public class JsonParserTests
const string source = "[1,true,{},[],null]";
var node = JsonParser.Parse(nameof(Can_parse_an_array_with_multiple_items), source);
Assert.That(node is JsonArray
Assert.That(node is JsonArraySyntax
{
Elements:
[
JsonNumber { Value: 1 },
JsonBool { Value: true },
JsonObject { Properties.Length: 0 },
JsonArray { Elements.Length: 0 },
JsonNull
JsonNumberSyntax { Value: 1 },
JsonBoolSyntax { Value: true },
JsonObjectSyntax { Properties.Length: 0 },
JsonArraySyntax { Elements.Length: 0 },
JsonNullSyntax
]
});
}
@ -98,7 +100,7 @@ public class JsonParserTests
{
const string source = "{}";
var tokens = JsonParser.Parse(nameof(Can_parse_an_empty_object), source);
Assert.That(tokens is JsonObject { Properties.Length: 0 });
Assert.That(tokens is JsonObjectSyntax { Properties.Length: 0 });
}
[Test]
@ -107,10 +109,10 @@ public class JsonParserTests
const string source = "{\"first_name\":\"John\"}";
var node = JsonParser.Parse(nameof(Can_parse_an_object_with_one_entry), source);
Assert.That(node is JsonObject { Properties.Length: 1 });
var @object = (JsonObject)node;
Assert.That(@object.Properties.Any(property => property.Key == "first_name"));
Assert.That(@object.Properties.First(property => property.Key == "first_name").Value is JsonString { Value: "John" });
Assert.That(node is JsonObjectSyntax { Properties.Length: 1 });
var @object = (JsonObjectSyntax)node;
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" });
}
[Test]
@ -121,12 +123,12 @@ public class JsonParserTests
Assert.Multiple(() =>
{
Assert.That(node is JsonObject { Properties.Length: 2 });
var @object = (JsonObject)node;
Assert.That(@object.Properties.Any(property => property.Key == "first_name"));
Assert.That(@object.Properties.First(property => property.Key == "first_name").Value is JsonString { Value: "John" });
Assert.That(@object.Properties.Any(property => property.Key == "last_name"));
Assert.That(@object.Properties.First(property => property.Key == "last_name").Value is JsonString { Value: "Doe" });
Assert.That(node is JsonObjectSyntax { Properties.Length: 2 });
var @object = (JsonObjectSyntax)node;
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.Any(property => property.Key.Lexeme == "last_name"));
Assert.That(@object.Properties.First(property => property.Key.Lexeme == "last_name").Value is JsonStringSyntax { Value: "Doe" });
});
}
}

View File

@ -1,15 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser;
public readonly struct JsonArray : ISyntaxNode
{
public Lexer<JsonToken>.Token Token { get; }
public ISyntaxNode[] Elements { get; }
public JsonArray(Lexer<JsonToken>.Token token, ISyntaxNode[] elements)
{
Token = token;
Elements = elements;
}
}

View File

@ -1,14 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser;
public readonly struct JsonBool : ISyntaxNode
{
public Lexer<JsonToken>.Token Token { get; }
public bool Value => bool.TryParse(Token.Lexeme, out var val) && val;
public JsonBool(Lexer<JsonToken>.Token token)
{
Token = token;
}
}

View File

@ -1,13 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser;
public readonly struct JsonNull : ISyntaxNode
{
public Lexer<JsonToken>.Token Token { get; }
public JsonNull(Lexer<JsonToken>.Token token)
{
Token = token;
}
}

View File

@ -1,14 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser;
public readonly struct JsonNumber : ISyntaxNode
{
public Lexer<JsonToken>.Token Token { get; }
public double Value => double.TryParse(Token.Lexeme.Replace('.', ','), out var val) ? val : default;
public JsonNumber(Lexer<JsonToken>.Token token)
{
Token = token;
}
}

View File

@ -1,18 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser;
public readonly partial struct JsonObject
{
public readonly struct Property : ISyntaxNode
{
public string Key { get; }
public ISyntaxNode Value { get; }
public Property(string key, ISyntaxNode value)
{
Key = key;
Value = value;
}
}
}

View File

@ -1,15 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser;
public readonly partial struct JsonObject : ISyntaxNode
{
public Lexer<JsonToken>.Token Token { get; }
public Property[] Properties { get; }
public JsonObject(Lexer<JsonToken>.Token token, Property[] properties)
{
Token = token;
Properties = properties;
}
}

View File

@ -1,10 +1,11 @@
using DevDisciples.Parsing;
using DevDisciples.Json.Parser.Syntax;
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser;
public static partial class JsonParser
{
public static ISyntaxNode Parse(string file, string source)
public static IJsonSyntax Parse(string file, string source)
{
var tokens = JsonLexer.Default.Lex(file, source).ToArray();
var context = new Context(tokens);
@ -12,33 +13,33 @@ public static partial class JsonParser
return nodes;
}
private static ISyntaxNode Expression(ParserContext<JsonToken> ctx)
private static IJsonSyntax Expression(ParserContext<JsonToken> ctx)
{
if (ctx.Match(JsonToken.LeftBracket))
return Array(ctx);
return ArrayExpression(ctx);
if (ctx.Match(JsonToken.LeftBrace))
return Object(ctx);
return ObjectExpression(ctx);
if (ctx.Match(JsonToken.Minus) || ctx.Match(JsonToken.Number))
return Number(ctx);
return NumberExpression(ctx);
if (ctx.Match(JsonToken.String))
return String(ctx);
return StringExpression(ctx);
if (ctx.Match(JsonToken.Null))
return Null(ctx);
return NullExpression(ctx);
if (ctx.Match(JsonToken.True) || ctx.Match(JsonToken.False))
return Bool(ctx);
return BoolExpression(ctx);
throw Report.Error(ctx.Current, $"Expected a JSON expression, got '{ctx.Current.Lexeme}'");
}
private static ISyntaxNode Array(ParserContext<JsonToken> ctx)
private static IJsonSyntax ArrayExpression(ParserContext<JsonToken> ctx)
{
var previous = ctx.Previous();
List<ISyntaxNode>? elements = null;
List<IJsonSyntax>? elements = null;
if (!ctx.Check(JsonToken.RightBracket))
{
@ -51,13 +52,13 @@ public static partial class JsonParser
ctx.Consume(JsonToken.RightBracket, "Expected ']'");
return new JsonArray(previous, elements?.ToArray() ?? System.Array.Empty<ISyntaxNode>());
return new JsonArraySyntax(previous, elements?.ToArray() ?? System.Array.Empty<IJsonSyntax>());
}
private static ISyntaxNode Object(ParserContext<JsonToken> ctx)
private static IJsonSyntax ObjectExpression(ParserContext<JsonToken> ctx)
{
var previous = ctx.Previous();
Dictionary<string, ISyntaxNode>? properties = null;
Dictionary<Lexer<JsonToken>.Token, IJsonSyntax>? properties = null;
if (!ctx.Check(JsonToken.RightBrace))
{
@ -66,32 +67,32 @@ public static partial class JsonParser
var key = ctx.Consume(JsonToken.String, "Expected property name");
ctx.Consume(JsonToken.Colon, "Expected ':' after property name");
properties ??= new();
properties[key.Lexeme] = Expression(ctx);
properties[key] = Expression(ctx);
} while (ctx.Match(JsonToken.Comma));
}
ctx.Consume(JsonToken.RightBrace, "Expected '}'");
var propertiesArray = properties?.Select(kv => new JsonObject.Property(kv.Key, kv.Value)).ToArray();
var propertiesArray = properties?.Select(kv => new JsonPropertySyntax(kv.Key, kv.Value)).ToArray();
return new JsonObject(previous, propertiesArray ?? System.Array.Empty<JsonObject.Property>());
return new JsonObjectSyntax(previous, propertiesArray ?? System.Array.Empty<JsonPropertySyntax>());
}
private static ISyntaxNode Number(ParserContext<JsonToken> ctx)
private static IJsonSyntax NumberExpression(ParserContext<JsonToken> ctx)
{
if (ctx.Previous().Type != JsonToken.Minus) return new JsonNumber(ctx.Previous());
if (ctx.Previous().Type != JsonToken.Minus) return new JsonNumberSyntax(ctx.Previous());
var minus = ctx.Previous();
var number = ctx.Consume(JsonToken.Number, "Expected a number after '-'.");
return new JsonNumber(
return new JsonNumberSyntax(
new Lexer<JsonToken>.Token(minus.File, JsonToken.Number, $"-{number.Lexeme}", minus.Line, minus.Column)
);
}
private static ISyntaxNode String(ParserContext<JsonToken> ctx) => new JsonString(ctx.Previous());
private static IJsonSyntax StringExpression(ParserContext<JsonToken> ctx) => new JsonStringSyntax(ctx.Previous());
private static ISyntaxNode Null(ParserContext<JsonToken> ctx) => new JsonNull(ctx.Previous());
private static IJsonSyntax NullExpression(ParserContext<JsonToken> ctx) => new JsonNullSyntax(ctx.Previous());
private static ISyntaxNode Bool(ParserContext<JsonToken> ctx) => new JsonBool(ctx.Previous());
private static IJsonSyntax BoolExpression(ParserContext<JsonToken> ctx) => new JsonBoolSyntax(ctx.Previous());
}

View File

@ -1,14 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser;
public readonly struct JsonString : ISyntaxNode
{
public Lexer<JsonToken>.Token Token { get; }
public string Value => Token.Lexeme;
public JsonString(Lexer<JsonToken>.Token token)
{
Token = token;
}
}

View File

@ -0,0 +1,8 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser.Syntax;
public interface IJsonSyntax
{
public Lexer<JsonToken>.Token Token { get; }
}

View File

@ -0,0 +1,15 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser.Syntax;
public readonly struct JsonArraySyntax : IJsonSyntax
{
public Lexer<JsonToken>.Token Token { get; }
public IJsonSyntax[] Elements { get; }
public JsonArraySyntax(Lexer<JsonToken>.Token token, IJsonSyntax[] elements)
{
Token = token;
Elements = elements;
}
}

View File

@ -0,0 +1,14 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser.Syntax;
public readonly struct JsonBoolSyntax : IJsonSyntax
{
public Lexer<JsonToken>.Token Token { get; }
public bool Value => bool.TryParse(Token.Lexeme, out var val) && val;
public JsonBoolSyntax(Lexer<JsonToken>.Token token)
{
Token = token;
}
}

View File

@ -0,0 +1,13 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser.Syntax;
public readonly struct JsonNullSyntax : IJsonSyntax
{
public Lexer<JsonToken>.Token Token { get; }
public JsonNullSyntax(Lexer<JsonToken>.Token token)
{
Token = token;
}
}

View File

@ -0,0 +1,14 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser.Syntax;
public readonly struct JsonNumberSyntax : IJsonSyntax
{
public Lexer<JsonToken>.Token Token { get; }
public double Value => double.TryParse(Token.Lexeme.Replace('.', ','), out var val) ? val : default;
public JsonNumberSyntax(Lexer<JsonToken>.Token token)
{
Token = token;
}
}

View File

@ -0,0 +1,15 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser.Syntax;
public readonly partial struct JsonObjectSyntax : IJsonSyntax
{
public Lexer<JsonToken>.Token Token { get; }
public JsonPropertySyntax[] Properties { get; }
public JsonObjectSyntax(Lexer<JsonToken>.Token token, JsonPropertySyntax[] properties)
{
Token = token;
Properties = properties;
}
}

View File

@ -0,0 +1,16 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser.Syntax;
public readonly struct JsonPropertySyntax : IJsonSyntax
{
public Lexer<JsonToken>.Token Token => Key;
public Lexer<JsonToken>.Token Key { get; }
public IJsonSyntax Value { get; }
public JsonPropertySyntax(Lexer<JsonToken>.Token key, IJsonSyntax value)
{
Key = key;
Value = value;
}
}

View File

@ -0,0 +1,14 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Parser.Syntax;
public readonly struct JsonStringSyntax : IJsonSyntax
{
public Lexer<JsonToken>.Token Token { get; }
public string Value => Token.Lexeme;
public JsonStringSyntax(Lexer<JsonToken>.Token token)
{
Token = token;
}
}

View File

@ -40,4 +40,11 @@ public class TransformController : ControllerBase
return Ok(new { Result = result });
}
[HttpPost("jsonpath")]
public IActionResult JsonPath([FromBody] JsonPathRequest request)
{
var result = Tools.JsonPath.Interpreter.Evaluate(request.Source, request.Path);
return Ok(new { Result = JsonFormatter.Format(result, new() { Beautify = true }) });
}
}

View File

@ -2,6 +2,6 @@
public class Json2CsharpRequest : TransformRequest
{
public string RootClassName { get; } = Json2CSharpTranslator.Context.DefaultRootClassName;
public string Namespace { get; } = Json2CSharpTranslator.Context.DefaultNamespace;
public string RootClassName { get; init; } = Json2CSharpTranslator.Context.DefaultRootClassName;
public string Namespace { get; init; } = Json2CSharpTranslator.Context.DefaultNamespace;
}

View File

@ -0,0 +1,6 @@
namespace DevDisciples.Json.Tools.API.Requests;
public class JsonPathRequest : TransformRequest
{
public string Path { get; init; } = string.Empty;
}

View File

@ -2,5 +2,5 @@
public class PrettifyRequest : TransformRequest
{
public int IndentSize { get; } = JsonFormatter.Context.DefaultIndentSize;
public int IndentSize { get; init; } = JsonFormatter.Context.DefaultIndentSize;
}

View File

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

View File

@ -0,0 +1,13 @@
<h1>JSON to C#</h1>
<mat-form-field>
<mat-label>Path</mat-label>
<input matInput placeholder="$.*" [value]="$path.value" (input)="handlePathChange($event)">
</mat-form-field>
<app-input-output [input]="$input.value"
[inputOptions]="inputOptions"
(onInputChange)="handleInputChange($event)"
[output]="output"
[outputOptions]="outputOptions">
</app-input-output>

View File

@ -0,0 +1,3 @@
mat-form-field {
width: 100%;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JsonPathComponent } from './json-path.component';
describe('JsonPathComponent', () => {
let component: JsonPathComponent;
let fixture: ComponentFixture<JsonPathComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [JsonPathComponent]
})
.compileComponents();
fixture = TestBed.createComponent(JsonPathComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,69 @@
import {Component, OnInit} from '@angular/core';
import {InputOutputComponent} from "../input-output/input-output.component";
import {DebounceTime, GenerateDefaultJsonObjectString, MonacoJsonConfig, ReadOnlyMonacoCSharpConfig} from "../defaults";
import {BehaviorSubject, debounceTime} from "rxjs";
import {JsonTransformService} from "../json-transform.service";
import {MatFormField} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
@Component({
selector: 'app-json-path',
standalone: true,
imports: [
InputOutputComponent,
MatFormField,
MatInputModule
],
templateUrl: './json-path.component.html',
styleUrl: './json-path.component.scss'
})
export class JsonPathComponent implements OnInit {
$path: BehaviorSubject<string> = new BehaviorSubject<string>("$.*");
$input: BehaviorSubject<string> = new BehaviorSubject<string>(GenerateDefaultJsonObjectString(2));
inputOptions = MonacoJsonConfig;
output: string = "";
outputOptions = ReadOnlyMonacoCSharpConfig;
constructor(private service: JsonTransformService) {
}
ngOnInit(): void {
this.$input
.pipe(debounceTime(DebounceTime))
.subscribe(input => this.update(input, this.$path.value));
this.$path
.pipe(debounceTime(DebounceTime))
.subscribe(path => this.update(this.$input.value, path));
this.update(this.$input.value, this.$path.value);
}
update(input: string, path: string): void {
this.service
.jsonPath(input, path)
.subscribe({
next: response => {
console.log(response);
this.output = response.body.result;
},
error: response => {
console.log(response)
if (response.status === 499) {
this.output = response.error.detail;
console.log(response.error.detail);
}
}
});
}
handlePathChange($event: any): void {
console.log($event);
this.$path.next($event.target.value);
}
handleInputChange($event: any): void {
console.log($event);
this.$input.next($event);
}
}

View File

@ -22,4 +22,8 @@ export class JsonTransformService {
public json2csharp(source: string): Observable<HttpResponse<any>> {
return this.http.post(`${this.url}/api/transform/json2csharp`, {Source: source}, {observe: "response"});
}
public jsonPath(source: string, path: string): Observable<HttpResponse<any>> {
return this.http.post(`${this.url}/api/transform/jsonpath`, {source: source, path: path}, {observe: "response"});
}
}

View File

@ -1,6 +1,6 @@
<h1>JSON to C#</h1>
<app-input-output [input]="input"
<app-input-output [input]="$input.value"
[inputOptions]="inputOptions"
(onInputChange)="handleInputChange($event)"
[output]="output"

View File

@ -7,7 +7,7 @@ import {
MonacoJsonConfig,
ReadOnlyMonacoCSharpConfig
} from "../defaults";
import {debounceTime, Subject} from "rxjs";
import {BehaviorSubject, debounceTime} from "rxjs";
@Component({
selector: 'app-json2csharp',
@ -19,8 +19,7 @@ import {debounceTime, Subject} from "rxjs";
styleUrl: './json2-csharp.component.scss'
})
export class Json2CsharpComponent implements OnInit {
input: string = GenerateDefaultJsonObjectString(2);
$input: Subject<string> = new Subject<string>();
$input: BehaviorSubject<string> = new BehaviorSubject<string>(GenerateDefaultJsonObjectString(2));
inputOptions = MonacoJsonConfig;
output: string = "";
outputOptions = ReadOnlyMonacoCSharpConfig;
@ -33,7 +32,7 @@ export class Json2CsharpComponent implements OnInit {
.pipe(debounceTime(DebounceTime))
.subscribe(input => this.update(input));
this.update(this.input);
this.update(this.$input.value);
}
update(input: string): void {

View File

@ -6,6 +6,7 @@
<mat-list-item routerLink="/prettify">Prettify</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="/jsonpath">JSON Path</mat-list-item>
</mat-nav-list>
</mat-sidenav>

View File

@ -12,4 +12,5 @@ mat-sidenav {
main {
height: 100%;
padding: 20px;
}

View File

@ -1,6 +1,6 @@
<h1>JSON Prettify</h1>
<app-input-output [input]="input"
<app-input-output [input]="$input.value"
[inputOptions]="inputOptions"
(onInputChange)="handleInputChange($event)"
[output]="output"

View File

@ -9,7 +9,7 @@ import {
MonacoJsonConfig,
ReadOnlyMonacoJsonConfig
} from "../defaults";
import {debounceTime, Subject} from "rxjs";
import {BehaviorSubject, debounceTime} from "rxjs";
@Component({
selector: 'app-prettify',
@ -23,12 +23,12 @@ import {debounceTime, Subject} from "rxjs";
styleUrl: './prettify.component.scss'
})
export class PrettifyComponent implements OnInit {
input: string = GenerateDefaultJsonObjectString();
$input: Subject<string> = new Subject<string>();
// input: string = ;
$input: BehaviorSubject<string> = new BehaviorSubject<string>(GenerateDefaultJsonObjectString());
inputOptions = MonacoJsonConfig;
output: string = GenerateDefaultJsonObjectString(2);
outputOptions = ReadOnlyMonacoJsonConfig;
error: string = "";
// error: string = "";
constructor(private service: JsonTransformService) {
}

View File

@ -1,6 +1,6 @@
<h1>JSON Uglify</h1>
<app-input-output [input]="input"
<app-input-output [input]="$input.value"
[inputOptions]="inputOptions"
(onInputChange)="handleInputChange($event)"
[output]="output"

View File

@ -7,7 +7,7 @@ import {
MonacoJsonConfig,
ReadOnlyMonacoJsonConfig
} from "../defaults";
import {debounceTime, Subject} from "rxjs";
import {debounceTime, BehaviorSubject} from "rxjs";
@Component({
selector: 'app-uglify',
@ -19,8 +19,8 @@ import {debounceTime, Subject} from "rxjs";
styleUrl: './uglify.component.scss'
})
export class UglifyComponent implements OnInit {
input: string = GenerateDefaultJsonObjectString(2);
$input: Subject<string> = new Subject<string>();
// input: string = ;
$input: BehaviorSubject<string> = new BehaviorSubject<string>(GenerateDefaultJsonObjectString(2));
inputOptions = MonacoJsonConfig;
output: string = GenerateDefaultJsonObjectString();
outputOptions = ReadOnlyMonacoJsonConfig;

View File

@ -11,6 +11,8 @@ public static partial class Json2CSharpTranslator
public string RootClassName { get; init; } = DefaultRootClassName;
public string Namespace { get; init; } = DefaultNamespace;
public string CurrentName { get; set; } = string.Empty;
public List<ClassTranslation> Classes { get; } = new();
public readonly StringBuilder Builder = new();
}

View File

@ -1,63 +0,0 @@
using DevDisciples.Json.Parser;
using DevDisciples.Parsing;
using Humanizer;
namespace DevDisciples.Json.Tools;
public static partial class Json2CSharpTranslator
{
public static class JsonArrayTranslator
{
public static ITranslation Translate(ISyntaxNode visitee, object[] args)
{
var context = ContextFromArgs(args);
var name = NameFromArgs(args);
var array = (JsonArray)visitee;
var type = "object";
if (array.Elements.All(e => e is JsonObject))
{
context.Classes.Add(SquashObjects(name, array.Elements, args));
type = context.Classes.Last().Name;
}
else if (array.Elements.All(e => e is JsonString es && DateTime.TryParse(es.Value, out _)))
{
type = "DateTime";
}
else if (array.Elements.All(e => e is JsonString))
{
type = "string";
}
else if (array.Elements.All(e => e is JsonNumber))
{
type = array.Elements.Any(e => ((JsonNumber)e).Token.Lexeme.Contains('.')) ? "double" : "int";
}
return new PropertyTranslation
{
Type = $"List<{type}>",
Name = name,
};
}
private static ClassTranslation SquashObjects(string className, IEnumerable<ISyntaxNode> objects, object[] args)
{
var classes = objects
.Select(@object => JsonObjectTranslator.Translate(@object, args))
.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,18 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class Json2CSharpTranslator
{
public static class JsonBoolTranslator
{
public static ITranslation Translate(ISyntaxNode visitee, object[] args)
{
return new PropertyTranslation
{
Type = "bool",
Name = NameFromArgs(args),
};
}
}
}

View File

@ -1,18 +0,0 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class Json2CSharpTranslator
{
public static class JsonNullTranslator
{
public static ITranslation Translate(ISyntaxNode visitee, object[] args)
{
return new PropertyTranslation
{
Type = "object",
Name = NameFromArgs(args),
};
}
}
}

View File

@ -1,19 +0,0 @@
using DevDisciples.Json.Parser;
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class Json2CSharpTranslator
{
public static class JsonNumberTranslator
{
public static ITranslation Translate(ISyntaxNode visitee, object[] args)
{
return new PropertyTranslation
{
Type = ((JsonNumber)visitee).Token.Lexeme.Contains('.') ? "double" : "int",
Name = NameFromArgs(args),
};
}
}
}

View File

@ -1,42 +0,0 @@
using DevDisciples.Json.Parser;
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class Json2CSharpTranslator
{
public static class JsonObjectTranslator
{
public static ITranslation Translate(ISyntaxNode visitee, object[] args)
{
var context = ContextFromArgs(args);
var name = NameFromArgs(args);
var @class = new ClassTranslation { Name = name, Properties = new() };
var @object = (JsonObject)visitee;
context.Classes.Add(@class);
foreach (var prop in @object.Properties)
{
var visitor = Visitors[prop.Value.GetType()];
var translation = visitor(prop.Value, context, prop.Key);
switch (translation)
{
case ClassTranslation:
@class.Properties.Add(new PropertyTranslation
{
Type = translation.Name,
Name = translation.Name,
});
break;
case PropertyTranslation property:
@class.Properties.Add(property);
break;
}
}
return @class;
}
}
}

View File

@ -1,22 +0,0 @@
using DevDisciples.Json.Parser;
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class Json2CSharpTranslator
{
public static class JsonStringTranslator
{
public static ITranslation Translate(ISyntaxNode visitee, object[] args)
{
var @string = (JsonString)visitee;
var type = DateTime.TryParse(@string.Value, out _) ? "DateTime" : "string";
return new PropertyTranslation
{
Type = type,
Name = NameFromArgs(args),
};
}
}
}

View File

@ -1,4 +1,5 @@
using DevDisciples.Json.Parser;
using DevDisciples.Json.Parser.Syntax;
using DevDisciples.Parsing;
using Humanizer;
@ -6,28 +7,28 @@ namespace DevDisciples.Json.Tools;
public static partial class Json2CSharpTranslator
{
public static readonly VisitorContainer<ISyntaxNode, ITranslation> Visitors;
private static readonly VisitorContainer<IJsonSyntax, Context, ITranslation> Visitors = new();
static Json2CSharpTranslator()
{
Visitors = new();
Visitors.Register<JsonObject>(JsonObjectTranslator.Translate);
Visitors.Register<JsonArray>(JsonArrayTranslator.Translate);
Visitors.Register<JsonString>(JsonStringTranslator.Translate);
Visitors.Register<JsonNumber>(JsonNumberTranslator.Translate);
Visitors.Register<JsonBool>(JsonBoolTranslator.Translate);
Visitors.Register<JsonNull>(JsonNullTranslator.Translate);
Visitors.Register<JsonObjectSyntax>(Object);
Visitors.Register<JsonArraySyntax>(Array);
Visitors.Register<JsonStringSyntax>(String);
Visitors.Register<JsonNumberSyntax>(Number);
Visitors.Register<JsonBoolSyntax>(Bool);
Visitors.Register<JsonNullSyntax>(Null);
}
public static string Translate(string source, Context? context = null)
{
if (JsonParser.Parse("<source>", source) is not JsonObject root)
if (JsonParser.Parse("<source>", source) is not JsonObjectSyntax root)
throw new ParsingException("Expected a JSON object.");
context ??= new();
context.CurrentName = context.RootClassName;
var visitor = Visitors[typeof(JsonObject)];
visitor(root, context, context.RootClassName);
var visitor = Visitors[typeof(JsonObjectSyntax)];
visitor(root, context);
context.Builder.Append("//using System;\n");
context.Builder.Append("//using System.Collections.Generic;\n");
@ -38,7 +39,127 @@ public static partial class Json2CSharpTranslator
return context.Builder.ToString();
}
private static Context ContextFromArgs(object[] args) => (args[0] as Context)!;
private static ITranslation Object(IJsonSyntax visitee, Context context)
{
var @object = (JsonObjectSyntax)visitee;
var @class = new ClassTranslation
{
Name = context.CurrentName,
Properties = new()
};
private static string NameFromArgs(object[] args) => ((string)args[1]).Camelize();
context.Classes.Add(@class);
foreach (var prop in @object.Properties)
{
context.CurrentName = prop.Key.Lexeme;
var visitor = Visitors[prop.Value.GetType()];
var translation = visitor(prop.Value, context);
switch (translation)
{
case ClassTranslation:
@class.Properties.Add(new PropertyTranslation
{
Type = translation.Name,
Name = translation.Name,
});
break;
case PropertyTranslation property:
@class.Properties.Add(property);
break;
}
}
return @class;
}
private static ITranslation Array(IJsonSyntax visitee, Context context)
{
var array = (JsonArraySyntax)visitee;
var type = "object";
if (array.Elements.All(e => e is JsonObjectSyntax))
{
context.Classes.Add(SquashObjects(context.CurrentName, array.Elements, context));
type = context.Classes.Last().Name;
}
else if (array.Elements.All(e => e is JsonStringSyntax es && DateTime.TryParse(es.Value, out _)))
{
type = "DateTime";
}
else if (array.Elements.All(e => e is JsonStringSyntax))
{
type = "string";
}
else if (array.Elements.All(e => e is JsonNumberSyntax))
{
type = array.Elements.Any(e => ((JsonNumberSyntax)e).Token.Lexeme.Contains('.')) ? "double" : "int";
}
return new PropertyTranslation
{
Type = $"List<{type}>",
Name = context.CurrentName,
};
}
private static ITranslation String(IJsonSyntax visitee, Context context)
{
var @string = (JsonStringSyntax)visitee;
var type = DateTime.TryParse(@string.Value, out _) ? "DateTime" : "string";
return new PropertyTranslation
{
Type = type,
Name = context.CurrentName,
};
}
private static ITranslation Number(IJsonSyntax visitee, Context context)
{
return new PropertyTranslation
{
Type = ((JsonNumberSyntax)visitee).Token.Lexeme.Contains('.') ? "double" : "int",
Name = context.CurrentName,
};
}
private static ITranslation Bool(IJsonSyntax visitee, Context context)
{
return new PropertyTranslation
{
Type = "bool",
Name = context.CurrentName,
};
}
private static ITranslation Null(IJsonSyntax visitee, Context context)
{
return new PropertyTranslation
{
Type = "object",
Name = context.CurrentName,
};
}
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,45 +1,39 @@
using DevDisciples.Json.Parser;
using DevDisciples.Json.Parser.Syntax;
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class JsonFormatter
{
public static VisitorContainer Visitors { get; }
private static VisitorContainer<IJsonSyntax, Context> Visitors { get; } = new();
static JsonFormatter()
{
Visitors = new();
Visitors.Register<JsonArray>(PrintArray);
Visitors.Register<JsonObject>(PrintObject);
Visitors.Register<JsonString>(PrintString);
Visitors.Register<JsonNumber>(PrintNumber);
Visitors.Register<JsonBool>(PrintBool);
Visitors.Register<JsonNull>(PrintNull);
Visitors.Register<JsonArraySyntax>(PrintArray);
Visitors.Register<JsonObjectSyntax>(PrintObject);
Visitors.Register<JsonStringSyntax>(PrintString);
Visitors.Register<JsonNumberSyntax>(PrintNumber);
Visitors.Register<JsonBoolSyntax>(PrintBool);
Visitors.Register<JsonNullSyntax>(PrintNull);
}
public static string Format(string source, Context? context)
{
var nodes = JsonParser.Parse("<source>", source);
return Format(nodes, context);
var node = JsonParser.Parse("<source>", source);
return Format(node, context);
}
private static string Format(ISyntaxNode visitee, Context? context = null)
public static string Format(IJsonSyntax visitee, Context? context = null)
{
var ctx = context ?? new();
Visitors[visitee.GetType()](visitee, ctx);
return context!.Builder.ToString();
context ??= new();
Visitors[visitee.GetType()](visitee, context);
return context.Builder.ToString();
}
private static Context ContextFromArgs(object[] args)
private static void PrintArray(IJsonSyntax visitee, Context context)
{
return (args[0] as Context)!;
}
private static void PrintArray(object visitee, object[] args)
{
var context = ContextFromArgs(args);
var array = (JsonArray)visitee;
var array = (JsonArraySyntax)visitee;
context.Builder.Append($"[{context.NewLine}");
context.IncrementDepth();
@ -48,7 +42,7 @@ public static partial class JsonFormatter
{
var node = array.Elements[i];
context.Builder.Append(context.Indent);
Visitors[node.GetType()](node, args);
Visitors[node.GetType()](node, context);
if (i < array.Elements.Length - 1) context.Builder.Append($",{context.NewLine}");
}
@ -56,10 +50,9 @@ public static partial class JsonFormatter
context.Builder.Append($"{context.NewLine}{context.Indent}]");
}
private static void PrintObject(object visitee, object[] args)
private static void PrintObject(IJsonSyntax visitee, Context context)
{
var context = ContextFromArgs(args);
var @object = (JsonObject)visitee;
var @object = (JsonObjectSyntax)visitee;
context.Builder.Append($"{{{context.NewLine}");
context.IncrementDepth();
@ -68,8 +61,8 @@ public static partial class JsonFormatter
for (var i = 0; i < count; i++)
{
var property = @object.Properties.ElementAt(i);
context.Builder.Append($"{context.Indent}\"{property.Key}\":{context.Space}");
Visitors[property.Value.GetType()](property.Value, args);
context.Builder.Append($"{context.Indent}\"{property.Key.Lexeme}\":{context.Space}");
Visitors[property.Value.GetType()](property.Value, context);
if (i < count - 1) context.Builder.Append($",{context.NewLine}");
}
@ -77,30 +70,26 @@ public static partial class JsonFormatter
context.Builder.Append($"{context.NewLine}{context.Indent}}}");
}
private static void PrintString(object visitee, object[] args)
private static void PrintString(IJsonSyntax visitee, Context context)
{
var context = ContextFromArgs(args);
var @string = (JsonString)visitee;
var @string = (JsonStringSyntax)visitee;
context.Builder.Append($"\"{@string.Token.Lexeme}\"");
}
private static void PrintNumber(object visitee, object[] args)
private static void PrintNumber(IJsonSyntax visitee, Context context)
{
var context = ContextFromArgs(args);
var number = (JsonNumber)visitee;
var number = (JsonNumberSyntax)visitee;
context.Builder.Append($"{number.Value}");
}
private static void PrintBool(object visitee, object[] args)
private static void PrintBool(IJsonSyntax visitee, Context context)
{
var context = ContextFromArgs(args);
var @bool = (JsonBool)visitee;
var @bool = (JsonBoolSyntax)visitee;
context.Builder.Append($"{@bool.Value.ToString().ToLower()}");
}
private static void PrintNull(object visitee, object[] args)
private static void PrintNull(IJsonSyntax visitee, Context context)
{
var context = ContextFromArgs(args);
context.Builder.Append("null");
}
}

View File

@ -0,0 +1,14 @@
using DevDisciples.Json.Parser.Syntax;
namespace DevDisciples.Json.Tools;
public static partial class JsonPath
{
public static partial class Interpreter
{
public class Context
{
public IJsonSyntax Target { get; set; } = default!;
}
}
}

View File

@ -0,0 +1,167 @@
using DevDisciples.Json.Parser;
using DevDisciples.Json.Parser.Syntax;
using DevDisciples.Parsing;
using DevDisciples.Parsing.Extensions;
namespace DevDisciples.Json.Tools;
public static partial class JsonPath
{
public static partial class Interpreter
{
private static readonly VisitorContainer<IJsonPathSyntax, Context> Visitors = new();
static Interpreter()
{
Visitors.Register<WildCardSyntax>(WildCardExpression);
Visitors.Register<PropertyAccessorSyntax>(PropertyAccessorExpression);
Visitors.Register<PropertySyntax>(PropertyExpression);
Visitors.Register<ArrayIndexSyntax>(ArrayIndexExpression);
Visitors.Register<ArrayIndexListSyntax>(ArrayIndexListExpression);
Visitors.Register<ObjectIndexSyntax>(ObjectIndexExpression);
Visitors.Register<ObjectIndexListSyntax>(ObjectIndexListExpression);
}
public static IJsonSyntax Evaluate(string source, string path)
{
var root = JsonParser.Parse("<json>", source);
if (root is not JsonArraySyntax && root is not JsonObjectSyntax)
throw Report.Error(root.Token, "Expected a JSON array or object.");
var expressions = Parser.Parse(path);
var context = new Context { Target = root };
foreach (var expr in expressions)
Visitors[expr.GetType()](expr, context);
return context.Target;
}
private static void Evaluate(IJsonPathSyntax node, Context context) => Visitors[node.GetType()](node, context);
private static void WildCardExpression(IJsonPathSyntax visitee, Context context)
{
switch (context.Target)
{
case JsonObjectSyntax @object:
var elements = @object.Properties.Select(p => p.Value).ToArray();
var target = new JsonArraySyntax(@object.Token, elements);
context.Target = target;
break;
case JsonArraySyntax array:
context.Target = array;
break;
default:
var syntax = visitee.As<WildCardSyntax>();
throw Report.Error(syntax.Token, "Invalid target for '*'.");
}
}
private static void PropertyAccessorExpression(IJsonPathSyntax visitee, Context context)
{
var accessor = visitee.As<PropertyAccessorSyntax>();
Evaluate(accessor.Getter, context);
}
private static void PropertyExpression(IJsonPathSyntax visitee, Context context)
{
var property = visitee.As<PropertySyntax>();
switch (context.Target)
{
case JsonObjectSyntax @object:
foreach (var objectProperty in @object.Properties)
{
if (objectProperty.Key.Lexeme != property.Token.Lexeme) continue;
context.Target = objectProperty.Value;
return;
}
context.Target = new JsonNullSyntax();
break;
default:
context.Target = new JsonNullSyntax();
break;
}
}
private static void ArrayIndexExpression(IJsonPathSyntax visitee, Context context)
{
if (context.Target is not JsonArraySyntax array)
throw Report.Error(context.Target.Token, "Integer indexes are only allowed on arrays.");
var index = visitee.As<ArrayIndexSyntax>();
var value = index.IndexAsInt;
if (value >= 0 && value < array.Elements.Length)
context.Target = array.Elements[value];
else throw Report.Error(index.Token, "Index out of range.");
}
private static void ArrayIndexListExpression(IJsonPathSyntax visitee, Context context)
{
var indices = visitee.As<ArrayIndexListSyntax>();
if (context.Target is not JsonArraySyntax array)
throw Report.Error(indices.Token, "Integer indices are only allowed on arrays.");
var list = new List<IJsonSyntax>();
for (var i = 0; i < indices.Indices.Length; i++)
{
var index = indices.ValueAt(i);
if (index >= 0 && index < array.Elements.Length)
list.Add(array.Elements.ElementAt(index));
}
context.Target = new JsonArraySyntax(array.Token, list.ToArray());
}
private static void ObjectIndexExpression(IJsonPathSyntax visitee, Context context)
{
if (context.Target is not JsonObjectSyntax @object)
throw Report.Error(context.Target.Token, "String indices are only allowed on objects.");
var index = visitee.As<ObjectIndexSyntax>();
foreach (var property in @object.Properties)
{
if (property.Key.Lexeme != index.Index.Lexeme) continue;
context.Target = property.Value;
return;
}
context.Target = new JsonNullSyntax(@object.Token);
}
private static void ObjectIndexListExpression(IJsonPathSyntax visitee, Context context)
{
if (context.Target is not JsonObjectSyntax @object)
throw Report.Error(context.Target.Token, "Index strings are only allowed on objects.");
var indices = visitee.As<ObjectIndexListSyntax>();
var elements = new List<IJsonSyntax>();
foreach (var index in indices.Indexes)
{
foreach (var property in @object.Properties)
{
if (property.Key.Lexeme != index.Lexeme) continue;
elements.Add(property.Value);
break;
}
}
context.Target = new JsonArraySyntax(@object.Token, elements.ToArray());
}
}
}

View File

@ -0,0 +1,38 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class JsonPath
{
public class Lexer : Lexer<JsonPathToken>
{
public static readonly Lexer Default = new();
protected override JsonPathToken EndOfSource => JsonPathToken.EndOfSource;
public Lexer()
{
Rules =
[
DefaultRule.NewLine,
DefaultRule.Number(JsonPathToken.Number),
DefaultRule.Identifier(JsonPathToken.Identifier),
DefaultRule.DoubleQuoteString(JsonPathToken.String),
ctx => DefaultRule.IgnoreWhitespace(ctx),
ctx => Match(ctx, JsonPathToken.False, "false"),
ctx => Match(ctx, JsonPathToken.True, "true"),
ctx => Match(ctx, JsonPathToken.Null, "null"),
ctx => Match(ctx, JsonPathToken.DotDot, ".."),
ctx => Match(ctx, JsonPathToken.DollarSign, '$'),
ctx => Match(ctx, JsonPathToken.Asterisk, '*'),
ctx => Match(ctx, JsonPathToken.Dot, '.'),
ctx => Match(ctx, JsonPathToken.Minus, '-'),
ctx => Match(ctx, JsonPathToken.Colon, ':'),
ctx => Match(ctx, JsonPathToken.LeftBracket, '['),
ctx => Match(ctx, JsonPathToken.Comma, ','),
ctx => Match(ctx, JsonPathToken.RightBracket, ']'),
];
}
}
}

View File

@ -0,0 +1,16 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class JsonPath
{
public static partial class Parser
{
public class Context : ParserContext<JsonPathToken>
{
public Context(Memory<Lexer<JsonPathToken>.Token> tokens) : base(tokens, JsonPathToken.EndOfSource)
{
}
}
}
}

View File

@ -0,0 +1,110 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
public static partial class JsonPath
{
public static partial class Parser
{
public static List<IJsonPathSyntax> Parse(string path)
{
var pathTokens = Lexer.Default.Lex("<path>", path);
var context = new Context(pathTokens.ToArray());
context.Match(JsonPathToken.DollarSign); // Match the '$' eagerly. This will make it optional by default.
var syntax = new List<IJsonPathSyntax>();
while (!context.Ended())
syntax.Add(Expression(context));
return syntax;
}
private static IJsonPathSyntax Expression(Context context)
{
if (context.Match(JsonPathToken.Dot))
return PropertyAccessorExpression(context);
if (context.Match(JsonPathToken.LeftBracket))
return IndexAccessorExpression(context);
throw context.Error("Invalid expression.");
}
private static IJsonPathSyntax PropertyAccessorExpression(Context context)
{
var token = context.Previous();
IJsonPathSyntax getter;
if (context.Match(JsonPathToken.Asterisk))
getter = new WildCardSyntax(context.Previous());
else if (context.Match(JsonPathToken.Identifier))
getter = new PropertySyntax(context.Previous());
else throw context.Error("Expected a getter expression");
return new PropertyAccessorSyntax(token, getter);
}
private static IJsonPathSyntax IndexAccessorExpression(Context context)
{
var token = context.Previous();
IJsonPathSyntax syntax;
if (context.Match(JsonPathToken.Asterisk))
syntax = new WildCardSyntax(context.Previous());
else if (context.Match(JsonPathToken.Number))
syntax = ArrayIndexExpression(token, context);
else if (context.Match(JsonPathToken.String))
syntax = ObjectIndexExpression(token, context);
else throw context.Error("Expected an index expression.");
context.Consume(JsonPathToken.RightBracket, "Expected ']' after index expression.");
return syntax;
}
private static IJsonPathSyntax ArrayIndexExpression(Lexer<JsonPathToken>.Token token, Context context)
{
var index = context.Previous();
if (!context.Match(JsonPathToken.Comma)) return new ArrayIndexSyntax(token, index);
var indexes = new List<Lexer<JsonPathToken>.Token> { index };
do
{
index = context.Consume(JsonPathToken.Number, "Invalid array index.");
if (!int.TryParse(index.Lexeme, out _)) throw context.Error(index, "Invalid array index.");
indexes.Add(index);
} while (!context.Ended() && context.Match(JsonPathToken.Comma));
return new ArrayIndexListSyntax(token, indexes.ToArray());
}
private static IJsonPathSyntax ObjectIndexExpression(Lexer<JsonPathToken>.Token token, Context context)
{
var index = context.Previous();
if (!context.Match(JsonPathToken.Comma)) return new ObjectIndexSyntax(token, index);
var indexes = new List<Lexer<JsonPathToken>.Token> { index };
do
{
index = context.Consume(JsonPathToken.String, "Invalid object index.");
indexes.Add(index);
} while (!context.Ended() && context.Match(JsonPathToken.Comma));
return new ObjectIndexListSyntax(token, indexes.ToArray());
}
}
}

View File

@ -0,0 +1,96 @@
using DevDisciples.Parsing;
namespace DevDisciples.Json.Tools;
// See https://docs.hevodata.com/sources/engg-analytics/streaming/rest-api/writing-jsonpath-expressions/
public static partial class JsonPath
{
public interface IJsonPathSyntax
{
public Lexer<JsonPathToken>.Token Token { get; }
}
public readonly struct WildCardSyntax : IJsonPathSyntax
{
public Lexer<JsonPathToken>.Token Token { get; }
public WildCardSyntax(Lexer<JsonPathToken>.Token token)
{
Token = token;
}
}
public readonly struct ArrayIndexSyntax : IJsonPathSyntax
{
public Lexer<JsonPathToken>.Token Token { get; }
public Lexer<JsonPathToken>.Token Index { get; }
public int IndexAsInt => int.Parse(Token.Lexeme);
public ArrayIndexSyntax(Lexer<JsonPathToken>.Token token, Lexer<JsonPathToken>.Token index)
{
Token = token;
Index = index;
}
}
public readonly struct ArrayIndexListSyntax : IJsonPathSyntax
{
public Lexer<JsonPathToken>.Token Token { get; }
public Lexer<JsonPathToken>.Token[] Indices { get; }
public int ValueAt(int index) => int.Parse(Indices[index].Lexeme);
public ArrayIndexListSyntax(Lexer<JsonPathToken>.Token token, Lexer<JsonPathToken>.Token[] indices)
{
Token = token;
Indices = indices;
}
}
public readonly struct ObjectIndexSyntax : IJsonPathSyntax
{
public Lexer<JsonPathToken>.Token Token { get; }
public Lexer<JsonPathToken>.Token Index { get; }
public ObjectIndexSyntax(Lexer<JsonPathToken>.Token token, Lexer<JsonPathToken>.Token index)
{
Token = token;
Index = index;
}
}
public readonly struct ObjectIndexListSyntax : IJsonPathSyntax
{
public Lexer<JsonPathToken>.Token Token { get; }
public Lexer<JsonPathToken>.Token[] Indexes { get; }
public ObjectIndexListSyntax(Lexer<JsonPathToken>.Token token, Lexer<JsonPathToken>.Token[] indexes)
{
Token = token;
Indexes = indexes;
}
}
public readonly struct PropertyAccessorSyntax : IJsonPathSyntax
{
public Lexer<JsonPathToken>.Token Token { get; }
public IJsonPathSyntax Getter { get; }
public PropertyAccessorSyntax(Lexer<JsonPathToken>.Token token, IJsonPathSyntax getter)
{
Token = token;
Getter = getter;
}
}
public readonly struct PropertySyntax : IJsonPathSyntax
{
public Lexer<JsonPathToken>.Token Token { get; }
public PropertySyntax(Lexer<JsonPathToken>.Token token)
{
Token = token;
}
}
}

View File

@ -0,0 +1,21 @@
namespace DevDisciples.Json.Tools;
public enum JsonPathToken
{
DollarSign,
Asterisk,
Dot,
DotDot,
LeftBracket,
RightBracket,
Identifier,
String,
Minus,
Number,
Comma,
Colon,
Null,
True,
False,
EndOfSource,
}

View File

@ -0,0 +1,10 @@
namespace DevDisciples.Parsing.Extensions;
public static class CastingExtensions
{
public static T As<T>(this object @object)
{
ArgumentNullException.ThrowIfNull(@object);
return (T)@object;
}
}

View File

@ -1,3 +0,0 @@
namespace DevDisciples.Parsing;
public interface ISyntaxNode { }

View File

@ -187,10 +187,8 @@ public abstract partial class Lexer<TToken> where TToken : Enum
ProcessDigits(src);
if (src.Peek() == '.' && IsDigit(src.Peek(1)))
if (IsDigit(src.Peek(1)) && src.Match('.'))
{
// Consume the "."
src.Advance();
ProcessDigits(src);
}

View File

@ -6,11 +6,11 @@ public abstract class ParsableStream<T>
protected ReadOnlySpan<T> Tokens => _tokens.Span;
public int Position { get; set; }
protected int Position { get; set; }
public T Current => Position < Tokens.Length ? Tokens[Position] : default!;
public ParsableStream(ReadOnlyMemory<T> tokens)
protected ParsableStream(ReadOnlyMemory<T> tokens)
{
_tokens = tokens;
}

View File

@ -2,9 +2,9 @@
public class ParserContext<TToken> : ParsableStream<Lexer<TToken>.Token> where TToken : Enum
{
protected readonly TToken _endOfSource;
private readonly TToken _endOfSource;
public ParserContext(Memory<Lexer<TToken>.Token> tokens, TToken endOfSource) : base(tokens)
protected ParserContext(Memory<Lexer<TToken>.Token> tokens, TToken endOfSource) : base(tokens)
{
_endOfSource = endOfSource;
}
@ -98,9 +98,4 @@ public class ParserContext<TToken> : ParsableStream<Lexer<TToken>.Token> where T
{
return new ParsingException(Report.FormatMessage(token, message));
}
public void Halt(Lexer<TToken>.Token token, string message)
{
throw new ParsingException(Report.FormatMessage(token, message));
}
}

View File

@ -2,7 +2,7 @@
public static class Report
{
public static Exception Error(ISourceLocation token, string message)
public static ParsingException Error(ISourceLocation token, string message)
{
return new ParsingException(FormatMessage(token, message));
}

View File

@ -1,8 +1,10 @@
namespace DevDisciples.Parsing;
namespace DevDisciples.Parsing;
public static class Visitor
{
public delegate void Visit(object visitee, params object[] args);
public delegate TOut Visit<TOut>(object visitee, params object[] args);
public delegate TOut Visit<TIn, TOut>(TIn visitee, params object[] args);
public delegate void Visit<TVisitee>(TVisitee visitee);
public delegate void Visit<TVisitee, TContext>(TVisitee visitee, TContext context);
public delegate TOut Visit<TVisitee, TContext, TOut>(TVisitee visitee, TContext context);
}

View File

@ -1,42 +1,40 @@
namespace DevDisciples.Parsing;
namespace DevDisciples.Parsing;
public class VisitorContainer
public class VisitorContainer<TBase>
{
private Dictionary<Type, Visitor.Visit> Visitors { get; } = new();
private Visitor.Visit Default { get; set; } = default!;
private Dictionary<Type, Visitor.Visit<TBase>> Visitors { get; } = new();
private Visitor.Visit<TBase> Default { get; set; } = default!;
public void Register<TVisitee>(Visitor.Visit visitor)
public void Register<T>(Visitor.Visit<TBase> visitor)
{
Visitors[typeof(TVisitee)] = visitor;
Visitors[typeof(T)] = visitor;
}
public Visitor.Visit this[Type type] => Visitors.GetValueOrDefault(type, Default);
public Visitor.Visit<TBase> this[Type type] => Visitors.GetValueOrDefault(type, Default);
}
public class VisitorContainer<T>
public class VisitorContainer<TBase, TContext>
{
protected Dictionary<Type, Visitor.Visit<T>> Visitors { get; } = new();
public Visitor.Visit<T> Default { get; } = default!;
private Dictionary<Type, Visitor.Visit<TBase, TContext>> Visitors { get; } = new();
private Visitor.Visit<TBase, TContext> Default { get; set; } = default!;
public VisitorContainer<T> Register<TVisitee>(Visitor.Visit<T> visitor)
public void Register<T>(Visitor.Visit<TBase, TContext> visitor)
{
Visitors[typeof(TVisitee)] = visitor;
return this;
Visitors[typeof(T)] = visitor;
}
public Visitor.Visit<T> this[Type type] => Visitors.ContainsKey(type) ? Visitors[type] : Default;
public Visitor.Visit<TBase, TContext> this[Type type] => Visitors.GetValueOrDefault(type, Default);
}
public class VisitorContainer<TIn, TOut>
public class VisitorContainer<TBase, TContext, TResult>
{
protected Dictionary<Type, Visitor.Visit<TIn, TOut>> Visitors { get; } = new();
public Visitor.Visit<TIn, TOut> Default { get; } = default!;
private Dictionary<Type, Visitor.Visit<TBase, TContext, TResult>> Visitors { get; } = new();
private Visitor.Visit<TBase, TContext, TResult> Default { get; set; } = default!;
public void Register<TVisitee>(Visitor.Visit<TIn, TOut> visitor)
public void Register<T>(Visitor.Visit<TBase, TContext, TResult> visitor)
{
Visitors[typeof(TVisitee)] = visitor;
Visitors[typeof(T)] = visitor;
}
public Visitor.Visit<TIn, TOut> this[Type type] => Visitors.ContainsKey(type) ? Visitors[type] : Default;
public Visitor.Visit<TBase, TContext, TResult> this[Type type] => Visitors.GetValueOrDefault(type, Default);
}