Initial commit
This commit is contained in:
commit
8d1069e477
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
*.swp
|
||||||
|
*.*~
|
||||||
|
project.lock.json
|
||||||
|
.DS_Store
|
||||||
|
*.pyc
|
||||||
|
nupkg/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Rider
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Visual Studio
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
# Fleet
|
||||||
|
.fleet/
|
||||||
|
|
||||||
|
# Code Rush
|
||||||
|
.cr/
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
build/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Oo]ut/
|
||||||
|
msbuild.log
|
||||||
|
msbuild.err
|
||||||
|
msbuild.wrn
|
@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
|
||||||
|
<PackageReference Include="NUnit" Version="3.13.3"/>
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DevDisciples.Json.Parser\DevDisciples.Json.Parser.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
1
DevDisciples.Json.Parser.Tests/GlobalUsings.cs
Normal file
1
DevDisciples.Json.Parser.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1 @@
|
|||||||
|
global using NUnit.Framework;
|
193
DevDisciples.Json.Parser.Tests/JsonLexerTests.cs
Normal file
193
DevDisciples.Json.Parser.Tests/JsonLexerTests.cs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
namespace DevDisciples.Json.Parser.Tests;
|
||||||
|
|
||||||
|
public class JsonLexerTests
|
||||||
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_null()
|
||||||
|
{
|
||||||
|
const string source = "null";
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_null), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.Null, Lexeme: "null" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_booleans()
|
||||||
|
{
|
||||||
|
const string trueSource = "true";
|
||||||
|
const string falseSource = "false";
|
||||||
|
var trueTokens = JsonLexer.Default.Lex(nameof(Can_lex_booleans), trueSource);
|
||||||
|
var falseTokens = JsonLexer.Default.Lex(nameof(Can_lex_booleans), falseSource);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(trueTokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.True, Lexeme: "true" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
]);
|
||||||
|
|
||||||
|
Assert.That(falseTokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.False, Lexeme: "false" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_an_integer()
|
||||||
|
{
|
||||||
|
const string source = "1";
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_an_integer), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.Number, Lexeme: "1" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_a_decimal()
|
||||||
|
{
|
||||||
|
const string source = "1.0";
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_a_decimal), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.Number, Lexeme: "1.0" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_a_string()
|
||||||
|
{
|
||||||
|
const string source = "\"Hello world!\"";
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_a_string), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.String, Lexeme: "Hello world!" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_an_empty_array()
|
||||||
|
{
|
||||||
|
const string source = "[]";
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_an_empty_array), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.LeftBracket, Lexeme: "[" },
|
||||||
|
{ Type: JsonToken.RightBracket, Lexeme: "]" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
], Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_a_single_item_array()
|
||||||
|
{
|
||||||
|
const string source = "[1]";
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_a_single_item_array), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.LeftBracket, Lexeme: "[" },
|
||||||
|
{ Type: JsonToken.Number, Lexeme: "1" },
|
||||||
|
{ Type: JsonToken.RightBracket, Lexeme: "]" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
], Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_an_array_with_multiple_items()
|
||||||
|
{
|
||||||
|
const string source = "[1,true,{},[],null]";
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_an_array_with_multiple_items), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.LeftBracket, Lexeme: "[" },
|
||||||
|
{ Type: JsonToken.Number, Lexeme: "1" },
|
||||||
|
{ Type: JsonToken.Comma, Lexeme: "," },
|
||||||
|
{ Type: JsonToken.True, Lexeme: "true" },
|
||||||
|
{ Type: JsonToken.Comma, Lexeme: "," },
|
||||||
|
{ Type: JsonToken.LeftBrace, Lexeme: "{" },
|
||||||
|
{ Type: JsonToken.RightBrace, Lexeme: "}" },
|
||||||
|
{ Type: JsonToken.Comma, Lexeme: "," },
|
||||||
|
{ Type: JsonToken.LeftBracket, Lexeme: "[" },
|
||||||
|
{ Type: JsonToken.RightBracket, Lexeme: "]" },
|
||||||
|
{ Type: JsonToken.Comma, Lexeme: "," },
|
||||||
|
{ Type: JsonToken.Null, Lexeme: "null" },
|
||||||
|
{ Type: JsonToken.RightBracket, Lexeme: "]" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
], Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_an_empty_object()
|
||||||
|
{
|
||||||
|
const string source = "{}";
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_an_empty_object), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.LeftBrace, Lexeme: "{" },
|
||||||
|
{ Type: JsonToken.RightBrace, Lexeme: "}" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
], Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_an_object_with_one_entry()
|
||||||
|
{
|
||||||
|
const string source = "{\"first_name\":\"John\"}";
|
||||||
|
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_an_object_with_one_entry), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.LeftBrace, Lexeme: "{" },
|
||||||
|
{ Type: JsonToken.String, Lexeme: "first_name" },
|
||||||
|
{ Type: JsonToken.Colon, Lexeme: ":" },
|
||||||
|
{ Type: JsonToken.String, Lexeme: "John" },
|
||||||
|
{ Type: JsonToken.RightBrace, Lexeme: "}" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
], Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_an_object_with_multiple_entries()
|
||||||
|
{
|
||||||
|
const string source = "{\"first_name\":\"John\", \"last_name\": \"Doe\"}";
|
||||||
|
|
||||||
|
var tokens = JsonLexer.Default.Lex(nameof(Can_lex_an_object_with_multiple_entries), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is
|
||||||
|
[
|
||||||
|
{ Type: JsonToken.LeftBrace, Lexeme: "{" },
|
||||||
|
{ Type: JsonToken.String, Lexeme: "first_name" },
|
||||||
|
{ Type: JsonToken.Colon, Lexeme: ":" },
|
||||||
|
{ Type: JsonToken.String, Lexeme: "John" },
|
||||||
|
{ Type: JsonToken.Comma, Lexeme: "," },
|
||||||
|
{ Type: JsonToken.String, Lexeme: "last_name" },
|
||||||
|
{ Type: JsonToken.Colon, Lexeme: ":" },
|
||||||
|
{ Type: JsonToken.String, Lexeme: "Doe" },
|
||||||
|
{ Type: JsonToken.RightBrace, Lexeme: "}" },
|
||||||
|
{ Type: JsonToken.EndOfSource }
|
||||||
|
], Is.True);
|
||||||
|
}
|
||||||
|
}
|
132
DevDisciples.Json.Parser.Tests/JsonParserTests.cs
Normal file
132
DevDisciples.Json.Parser.Tests/JsonParserTests.cs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
namespace DevDisciples.Json.Parser.Tests;
|
||||||
|
|
||||||
|
public class JsonParserTests
|
||||||
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_null()
|
||||||
|
{
|
||||||
|
const string source = "null";
|
||||||
|
var node = JsonParser.Parse(nameof(Can_parse_null), source);
|
||||||
|
Assert.That(node is JsonNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_booleans()
|
||||||
|
{
|
||||||
|
const string trueSource = "true";
|
||||||
|
const string falseSource = "false";
|
||||||
|
var trueNode = JsonParser.Parse(nameof(Can_parse_booleans), trueSource);
|
||||||
|
var falseNode = JsonParser.Parse(nameof(Can_parse_booleans), falseSource);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(trueNode is JsonBool { Value: true });
|
||||||
|
Assert.That(falseNode is JsonBool { Value: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_an_integer()
|
||||||
|
{
|
||||||
|
const string source = "1";
|
||||||
|
var node = JsonParser.Parse(nameof(Can_parse_an_integer), source);
|
||||||
|
Assert.That(node is JsonNumber { Value: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_lex_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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_a_string()
|
||||||
|
{
|
||||||
|
const string source = "\"Hello world!\"";
|
||||||
|
var tokens = JsonParser.Parse(nameof(Can_parse_a_string), source);
|
||||||
|
Assert.That(tokens is JsonString { Value: "Hello world!" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_an_empty_array()
|
||||||
|
{
|
||||||
|
const string source = "[]";
|
||||||
|
var tokens = JsonParser.Parse(nameof(Can_parse_an_empty_array), source);
|
||||||
|
Assert.That(tokens is JsonArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_a_single_item_array()
|
||||||
|
{
|
||||||
|
const string source = "[1]";
|
||||||
|
var tokens = JsonParser.Parse(nameof(Can_parse_a_single_item_array), source);
|
||||||
|
|
||||||
|
Assert.That(tokens is JsonArray
|
||||||
|
{
|
||||||
|
Elements: [JsonNumber { Value: 1 }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_an_array_with_multiple_items()
|
||||||
|
{
|
||||||
|
const string source = "[1,true,{},[],null]";
|
||||||
|
var node = JsonParser.Parse(nameof(Can_parse_an_array_with_multiple_items), source);
|
||||||
|
|
||||||
|
Assert.That(node is JsonArray
|
||||||
|
{
|
||||||
|
Elements:
|
||||||
|
[
|
||||||
|
JsonNumber { Value: 1 },
|
||||||
|
JsonBool { Value: true },
|
||||||
|
JsonObject { Properties: null},
|
||||||
|
JsonArray { Elements: null },
|
||||||
|
JsonNull
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_an_empty_object()
|
||||||
|
{
|
||||||
|
const string source = "{}";
|
||||||
|
var tokens = JsonParser.Parse(nameof(Can_parse_an_empty_object), source);
|
||||||
|
Assert.That(tokens is JsonObject { Properties: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_an_object_with_one_entry()
|
||||||
|
{
|
||||||
|
const string source = "{\"first_name\":\"John\nDoe\"}";
|
||||||
|
var node = JsonParser.Parse(nameof(Can_parse_an_object_with_one_entry), source);
|
||||||
|
|
||||||
|
Assert.That(node is JsonObject { Properties.Count: 1 });
|
||||||
|
var @object = (JsonObject)node;
|
||||||
|
Assert.That(@object.Properties.ContainsKey("first_name"));
|
||||||
|
Assert.That(@object.Properties["first_name"] is JsonString { Value: "John" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Can_parse_an_object_with_multiple_entries()
|
||||||
|
{
|
||||||
|
const string source = "{\"first_name\":\"John\", \"last_name\": \"Doe\"}";
|
||||||
|
var node = JsonParser.Parse(nameof(Can_parse_an_object_with_one_entry), source);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(node is JsonObject { Properties.Count: 2 });
|
||||||
|
var @object = (JsonObject)node;
|
||||||
|
Assert.That(@object.Properties.ContainsKey("first_name"));
|
||||||
|
Assert.That(@object.Properties["first_name"] is JsonString { Value: "John" });
|
||||||
|
Assert.That(@object.Properties.ContainsKey("last_name"));
|
||||||
|
Assert.That(@object.Properties["last_name"] is JsonString { Value: "Doe" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
17
DevDisciples.Json.Parser/DevDisciples.Json.Parser.csproj
Normal file
17
DevDisciples.Json.Parser/DevDisciples.Json.Parser.csproj
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DevDisciples.Parsing\DevDisciples.Parsing.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Humanizer.Core" Version="3.0.0-beta.54" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
15
DevDisciples.Json.Parser/JsonArray.cs
Normal file
15
DevDisciples.Json.Parser/JsonArray.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public struct JsonArray : ISyntaxNode
|
||||||
|
{
|
||||||
|
public Lexer<JsonToken>.Token Token { get; }
|
||||||
|
public List<ISyntaxNode> Elements { get; }
|
||||||
|
|
||||||
|
public JsonArray(Lexer<JsonToken>.Token token, List<ISyntaxNode>? elements)
|
||||||
|
{
|
||||||
|
Token = token;
|
||||||
|
Elements = elements ?? new();
|
||||||
|
}
|
||||||
|
}
|
14
DevDisciples.Json.Parser/JsonBool.cs
Normal file
14
DevDisciples.Json.Parser/JsonBool.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
}
|
32
DevDisciples.Json.Parser/JsonLexer.cs
Normal file
32
DevDisciples.Json.Parser/JsonLexer.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public class JsonLexer : Lexer<JsonToken>
|
||||||
|
{
|
||||||
|
public static readonly JsonLexer Default = new();
|
||||||
|
|
||||||
|
protected override JsonToken EndOfSource => JsonToken.EndOfSource;
|
||||||
|
|
||||||
|
public JsonLexer()
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
DefaultRule.NewLine,
|
||||||
|
DefaultRule.Number(JsonToken.Number),
|
||||||
|
DefaultRule.DoubleQuoteString(JsonToken.String),
|
||||||
|
ctx => DefaultRule.IgnoreWhitespace(ctx),
|
||||||
|
|
||||||
|
ctx => Match(ctx, JsonToken.Minus, '-'),
|
||||||
|
ctx => Match(ctx, JsonToken.LeftBrace, '{'),
|
||||||
|
ctx => Match(ctx, JsonToken.Colon, ':'),
|
||||||
|
ctx => Match(ctx, JsonToken.RightBrace, '}'),
|
||||||
|
ctx => Match(ctx, JsonToken.LeftBracket, '['),
|
||||||
|
ctx => Match(ctx, JsonToken.Comma, ','),
|
||||||
|
ctx => Match(ctx, JsonToken.RightBracket, ']'),
|
||||||
|
ctx => Match(ctx, JsonToken.True, "true"),
|
||||||
|
ctx => Match(ctx, JsonToken.False, "false"),
|
||||||
|
ctx => Match(ctx, JsonToken.Null, "null"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
13
DevDisciples.Json.Parser/JsonNull.cs
Normal file
13
DevDisciples.Json.Parser/JsonNull.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public struct JsonNull : ISyntaxNode
|
||||||
|
{
|
||||||
|
public Lexer<JsonToken>.Token Token { get; }
|
||||||
|
|
||||||
|
public JsonNull(Lexer<JsonToken>.Token token)
|
||||||
|
{
|
||||||
|
Token = token;
|
||||||
|
}
|
||||||
|
}
|
14
DevDisciples.Json.Parser/JsonNumber.cs
Normal file
14
DevDisciples.Json.Parser/JsonNumber.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
}
|
15
DevDisciples.Json.Parser/JsonObject.cs
Normal file
15
DevDisciples.Json.Parser/JsonObject.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public struct JsonObject : ISyntaxNode
|
||||||
|
{
|
||||||
|
public Lexer<JsonToken>.Token Token { get; }
|
||||||
|
public Dictionary<string, ISyntaxNode> Properties { get; set; }
|
||||||
|
|
||||||
|
public JsonObject(Lexer<JsonToken>.Token token, Dictionary<string, ISyntaxNode>? properties)
|
||||||
|
{
|
||||||
|
Token = token;
|
||||||
|
Properties = properties ?? new();
|
||||||
|
}
|
||||||
|
}
|
13
DevDisciples.Json.Parser/JsonParser.Context.cs
Normal file
13
DevDisciples.Json.Parser/JsonParser.Context.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public static partial class JsonParser
|
||||||
|
{
|
||||||
|
private class Context : ParserContext<JsonToken>
|
||||||
|
{
|
||||||
|
public Context(Memory<Lexer<JsonToken>.Token> tokens) : base(tokens, JsonToken.EndOfSource)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
DevDisciples.Json.Parser/JsonParser.cs
Normal file
95
DevDisciples.Json.Parser/JsonParser.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public static partial class JsonParser
|
||||||
|
{
|
||||||
|
public static ISyntaxNode Parse(string file, string source)
|
||||||
|
{
|
||||||
|
var tokens = JsonLexer.Default.Lex(file, source).ToArray();
|
||||||
|
var context = new Context(tokens);
|
||||||
|
var nodes = Expression(context);
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ISyntaxNode Expression(ParserContext<JsonToken> ctx)
|
||||||
|
{
|
||||||
|
if (ctx.Match(JsonToken.LeftBracket))
|
||||||
|
return Array(ctx);
|
||||||
|
|
||||||
|
if (ctx.Match(JsonToken.LeftBrace))
|
||||||
|
return Object(ctx);
|
||||||
|
|
||||||
|
if (ctx.Match(JsonToken.Minus) || ctx.Match(JsonToken.Number))
|
||||||
|
return Number(ctx);
|
||||||
|
|
||||||
|
if (ctx.Match(JsonToken.String))
|
||||||
|
return String(ctx);
|
||||||
|
|
||||||
|
if (ctx.Match(JsonToken.Null))
|
||||||
|
return Null(ctx);
|
||||||
|
|
||||||
|
if (ctx.Match(JsonToken.True) || ctx.Match(JsonToken.False))
|
||||||
|
return Bool(ctx);
|
||||||
|
|
||||||
|
throw Report.Error(ctx.Current, $"Expected a JSON expression, got '{ctx.Current.Lexeme}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ISyntaxNode Array(ParserContext<JsonToken> ctx)
|
||||||
|
{
|
||||||
|
var previous = ctx.Previous();
|
||||||
|
List<ISyntaxNode>? elements = null;
|
||||||
|
|
||||||
|
if (!ctx.Check(JsonToken.RightBracket))
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
elements ??= new();
|
||||||
|
elements.Add(Expression(ctx));
|
||||||
|
} while (ctx.Match(JsonToken.Comma));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Consume(JsonToken.RightBracket, "Expected ']'");
|
||||||
|
|
||||||
|
return new JsonArray(previous, elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ISyntaxNode Object(ParserContext<JsonToken> ctx)
|
||||||
|
{
|
||||||
|
var previous = ctx.Previous();
|
||||||
|
Dictionary<string, ISyntaxNode>? properties = null;
|
||||||
|
|
||||||
|
if (!ctx.Check(JsonToken.RightBrace))
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var key = ctx.Consume(JsonToken.String, "Expected property name");
|
||||||
|
ctx.Consume(JsonToken.Colon, "Expected ':' after property name");
|
||||||
|
properties ??= new();
|
||||||
|
properties[key.Lexeme] = Expression(ctx);
|
||||||
|
} while (ctx.Match(JsonToken.Comma));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Consume(JsonToken.RightBrace, "Expected '}'");
|
||||||
|
|
||||||
|
return new JsonObject(previous, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ISyntaxNode Number(ParserContext<JsonToken> ctx)
|
||||||
|
{
|
||||||
|
if (ctx.Previous().Type != JsonToken.Minus) return new JsonNumber(ctx.Previous());
|
||||||
|
|
||||||
|
var minus = ctx.Previous();
|
||||||
|
var number = ctx.Consume(JsonToken.Number, "Expected a number after '-'.");
|
||||||
|
|
||||||
|
return new JsonNumber(
|
||||||
|
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 ISyntaxNode Null(ParserContext<JsonToken> ctx) => new JsonNull(ctx.Previous());
|
||||||
|
|
||||||
|
private static ISyntaxNode Bool(ParserContext<JsonToken> ctx) => new JsonBool(ctx.Previous());
|
||||||
|
}
|
14
DevDisciples.Json.Parser/JsonString.cs
Normal file
14
DevDisciples.Json.Parser/JsonString.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public struct JsonString : ISyntaxNode
|
||||||
|
{
|
||||||
|
public Lexer<JsonToken>.Token Token { get; }
|
||||||
|
public string Value => Token.Lexeme;
|
||||||
|
|
||||||
|
public JsonString(Lexer<JsonToken>.Token token)
|
||||||
|
{
|
||||||
|
Token = token;
|
||||||
|
}
|
||||||
|
}
|
18
DevDisciples.Json.Parser/JsonToken.cs
Normal file
18
DevDisciples.Json.Parser/JsonToken.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace DevDisciples.Json.Parser;
|
||||||
|
|
||||||
|
public enum JsonToken
|
||||||
|
{
|
||||||
|
LeftBracket,
|
||||||
|
RightBracket,
|
||||||
|
LeftBrace,
|
||||||
|
RightBrace,
|
||||||
|
String,
|
||||||
|
Minus,
|
||||||
|
Number,
|
||||||
|
Comma,
|
||||||
|
Colon,
|
||||||
|
Null,
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
EndOfSource
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0"/>
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
44
DevDisciples.Json.Tools.API/Program.cs
Normal file
44
DevDisciples.Json.Tools.API/Program.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
var summaries = new[]
|
||||||
|
{
|
||||||
|
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||||
|
};
|
||||||
|
|
||||||
|
app.MapGet("/weatherforecast", () =>
|
||||||
|
{
|
||||||
|
var forecast = Enumerable.Range(1, 5).Select(index =>
|
||||||
|
new WeatherForecast
|
||||||
|
(
|
||||||
|
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||||
|
Random.Shared.Next(-20, 55),
|
||||||
|
summaries[Random.Shared.Next(summaries.Length)]
|
||||||
|
))
|
||||||
|
.ToArray();
|
||||||
|
return forecast;
|
||||||
|
})
|
||||||
|
.WithName("GetWeatherForecast")
|
||||||
|
.WithOpenApi();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||||
|
{
|
||||||
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
}
|
41
DevDisciples.Json.Tools.API/Properties/launchSettings.json
Normal file
41
DevDisciples.Json.Tools.API/Properties/launchSettings.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:57723",
|
||||||
|
"sslPort": 44385
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5238",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7137;http://localhost:5238",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
DevDisciples.Json.Tools.API/appsettings.Development.json
Normal file
8
DevDisciples.Json.Tools.API/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
DevDisciples.Json.Tools.API/appsettings.json
Normal file
9
DevDisciples.Json.Tools.API/appsettings.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
10
DevDisciples.Json.Tools.CLI/CommandOptions.cs
Normal file
10
DevDisciples.Json.Tools.CLI/CommandOptions.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public class CommandOptions
|
||||||
|
{
|
||||||
|
public InputOptions Input { get; set; }
|
||||||
|
public FileInfo? InputFile { get; set; }
|
||||||
|
public string? InputArgument { get; set; }
|
||||||
|
public OutputOptions Output { get; set; }
|
||||||
|
public FileInfo? OutputFile { get; set; }
|
||||||
|
}
|
10
DevDisciples.Json.Tools.CLI/CommandOptionsBinder.cs
Normal file
10
DevDisciples.Json.Tools.CLI/CommandOptionsBinder.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
using DevDisciples.Json.Tools.CLI.Extensions;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public class CommandOptionsBinder : BinderBase<CommandOptions>
|
||||||
|
{
|
||||||
|
protected override CommandOptions GetBoundValue(BindingContext context) =>
|
||||||
|
new CommandOptions().WithCommonBindings(context);
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<PackAsTool>true</PackAsTool>
|
||||||
|
<ToolCommandName>jtr</ToolCommandName>
|
||||||
|
<PackageOutputPath>./nupkg</PackageOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
|
<PackageReference Include="TextCopy" Version="6.2.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DevDisciples.Json.Parser\DevDisciples.Json.Parser.csproj" />
|
||||||
|
<ProjectReference Include="..\DevDisciples.Json.Tools\DevDisciples.Json.Tools.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
16
DevDisciples.Json.Tools.CLI/Extensions/CommandExtensions.cs
Normal file
16
DevDisciples.Json.Tools.CLI/Extensions/CommandExtensions.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI.Extensions;
|
||||||
|
|
||||||
|
public static class CommandExtensions
|
||||||
|
{
|
||||||
|
public static void AddIOCommandOptions(this Command command)
|
||||||
|
{
|
||||||
|
command.AddOption(SharedCommandOptions.InputOption);
|
||||||
|
command.AddOption(SharedCommandOptions.InputFileOption);
|
||||||
|
command.AddOption(SharedCommandOptions.OutputOption);
|
||||||
|
command.AddOption(SharedCommandOptions.OutputFileOption);
|
||||||
|
|
||||||
|
command.AddArgument(SharedCommandOptions.InputArgument);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI.Extensions;
|
||||||
|
|
||||||
|
public static class CommandOptionsExtensions
|
||||||
|
{
|
||||||
|
public static T WithCommonBindings<T>(this T options, BindingContext context)
|
||||||
|
where T : CommandOptions
|
||||||
|
{
|
||||||
|
options.Input = context.ParseResult.GetValueForOption(SharedCommandOptions.InputOption);
|
||||||
|
options.InputFile = context.ParseResult.GetValueForOption(SharedCommandOptions.InputFileOption);
|
||||||
|
options.InputArgument = context.ParseResult.GetValueForArgument(SharedCommandOptions.InputArgument);
|
||||||
|
options.Output = context.ParseResult.GetValueForOption(SharedCommandOptions.OutputOption);
|
||||||
|
options.OutputFile = context.ParseResult.GetValueForOption(SharedCommandOptions.OutputFileOption);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
74
DevDisciples.Json.Tools.CLI/IOHandler.cs
Normal file
74
DevDisciples.Json.Tools.CLI/IOHandler.cs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
using TextCopy;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public static class IOHandler
|
||||||
|
{
|
||||||
|
public static async Task<string> HandleInput(InputOptions input, string? inputArgument, FileInfo? inputFile)
|
||||||
|
{
|
||||||
|
string json;
|
||||||
|
|
||||||
|
switch (input)
|
||||||
|
{
|
||||||
|
case InputOptions.s:
|
||||||
|
case InputOptions.std:
|
||||||
|
case InputOptions.stdin:
|
||||||
|
json = inputArgument ?? string.Empty;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InputOptions.c:
|
||||||
|
case InputOptions.clip:
|
||||||
|
case InputOptions.clipboard:
|
||||||
|
json = await ClipboardService.GetTextAsync() ?? string.Empty;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InputOptions.f:
|
||||||
|
case InputOptions.file:
|
||||||
|
if (inputFile is null)
|
||||||
|
throw new ArgumentException("Input file was not specified.");
|
||||||
|
|
||||||
|
if (!inputFile.Exists)
|
||||||
|
throw new FileNotFoundException($"File {inputFile.FullName} does not exist.");
|
||||||
|
|
||||||
|
json = await File.ReadAllTextAsync(inputFile.FullName);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception($"Input type '{input}' is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task HandleOutput(OutputOptions output, FileInfo? outputFile, string result)
|
||||||
|
{
|
||||||
|
switch (output)
|
||||||
|
{
|
||||||
|
case OutputOptions.s:
|
||||||
|
case OutputOptions.std:
|
||||||
|
case OutputOptions.stdout:
|
||||||
|
Console.WriteLine(result);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OutputOptions.c:
|
||||||
|
case OutputOptions.clip:
|
||||||
|
case OutputOptions.clipboard:
|
||||||
|
await ClipboardService.SetTextAsync(result);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OutputOptions.f:
|
||||||
|
case OutputOptions.file:
|
||||||
|
if (outputFile is null)
|
||||||
|
throw new ArgumentException("Output file was not specified.");
|
||||||
|
|
||||||
|
if (outputFile.Exists)
|
||||||
|
throw new Exception($"File {outputFile.FullName} already exists.");
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(outputFile.FullName, result);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception($"Output type '{output}' is not supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
DevDisciples.Json.Tools.CLI/InputOptions.cs
Normal file
18
DevDisciples.Json.Tools.CLI/InputOptions.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public enum InputOptions
|
||||||
|
{
|
||||||
|
// Standard input
|
||||||
|
stdin,
|
||||||
|
std,
|
||||||
|
s,
|
||||||
|
|
||||||
|
// Clipboard
|
||||||
|
clipboard,
|
||||||
|
clip,
|
||||||
|
c,
|
||||||
|
|
||||||
|
// File
|
||||||
|
file,
|
||||||
|
f,
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public partial class Json2CSharpCommand
|
||||||
|
{
|
||||||
|
public class CommandOptions : CLI.CommandOptions
|
||||||
|
{
|
||||||
|
public string? RootClassName { get; set; }
|
||||||
|
public string? Namespace { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
using DevDisciples.Json.Tools.CLI.Extensions;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public partial class Json2CSharpCommand
|
||||||
|
{
|
||||||
|
public class CommandOptionsBinder : BinderBase<CommandOptions>
|
||||||
|
{
|
||||||
|
protected override CommandOptions GetBoundValue(BindingContext context) =>
|
||||||
|
new CommandOptions
|
||||||
|
{
|
||||||
|
RootClassName = context.ParseResult.GetValueForOption(RootClassNameOption),
|
||||||
|
Namespace = context.ParseResult.GetValueForOption(NamespaceOption),
|
||||||
|
}.WithCommonBindings(context);
|
||||||
|
}
|
||||||
|
}
|
45
DevDisciples.Json.Tools.CLI/Json2CSharpCommand.cs
Normal file
45
DevDisciples.Json.Tools.CLI/Json2CSharpCommand.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using DevDisciples.Json.Tools.CLI.Extensions;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public partial class Json2CSharpCommand : Command
|
||||||
|
{
|
||||||
|
private static readonly Option<string> RootClassNameOption =
|
||||||
|
new(
|
||||||
|
aliases: ["--root-class", "--root", "-r"],
|
||||||
|
description: "The name of the root class",
|
||||||
|
getDefaultValue: () => Json2CSharpTranslator.Context.DefaultRootClassName
|
||||||
|
);
|
||||||
|
|
||||||
|
private static readonly Option<string> NamespaceOption = new(
|
||||||
|
aliases: ["--namespace", "--ns", "-n",],
|
||||||
|
description: "The namespace to use",
|
||||||
|
getDefaultValue: () => Json2CSharpTranslator.Context.DefaultNamespace
|
||||||
|
);
|
||||||
|
|
||||||
|
public Json2CSharpCommand() : base("2csharp", "Transform a JSON object into a C# class")
|
||||||
|
{
|
||||||
|
AddAlias("2cs");
|
||||||
|
AddAlias("2c#");
|
||||||
|
|
||||||
|
this.AddIOCommandOptions();
|
||||||
|
AddOption(RootClassNameOption);
|
||||||
|
AddOption(NamespaceOption);
|
||||||
|
|
||||||
|
this.SetHandler(ExecuteAsync, new CommandOptionsBinder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ExecuteAsync(CommandOptions options)
|
||||||
|
{
|
||||||
|
var json = await IOHandler.HandleInput(options.Input, options.InputArgument, options.InputFile);
|
||||||
|
|
||||||
|
var output = Json2CSharpTranslator.Translate(json, new()
|
||||||
|
{
|
||||||
|
RootClassName = options.RootClassName ?? Json2CSharpTranslator.Context.DefaultRootClassName,
|
||||||
|
Namespace = options.Namespace ?? Json2CSharpTranslator.Context.DefaultNamespace
|
||||||
|
});
|
||||||
|
|
||||||
|
await IOHandler.HandleOutput(options.Output, options.OutputFile, output);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public partial class JsonPrettifyCommand
|
||||||
|
{
|
||||||
|
public class CommandOptions : CLI.CommandOptions
|
||||||
|
{
|
||||||
|
public int IndentSize { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
using DevDisciples.Json.Tools.CLI.Extensions;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public partial class JsonPrettifyCommand
|
||||||
|
{
|
||||||
|
public class CommandOptionsBinder : BinderBase<CommandOptions>
|
||||||
|
{
|
||||||
|
protected override CommandOptions GetBoundValue(BindingContext context) =>
|
||||||
|
new CommandOptions
|
||||||
|
{
|
||||||
|
IndentSize = context.ParseResult.GetValueForOption(IndentSizeOption)
|
||||||
|
}.WithCommonBindings(context);
|
||||||
|
}
|
||||||
|
}
|
32
DevDisciples.Json.Tools.CLI/JsonPrettifyCommand.cs
Normal file
32
DevDisciples.Json.Tools.CLI/JsonPrettifyCommand.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using DevDisciples.Json.Tools.CLI.Extensions;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public partial class JsonPrettifyCommand : Command
|
||||||
|
{
|
||||||
|
private static readonly Option<int> IndentSizeOption =
|
||||||
|
new(aliases: ["--indent", "--is"], description: "The indent size", getDefaultValue: () => 2);
|
||||||
|
|
||||||
|
public JsonPrettifyCommand() : base("prettify", "Prettify JSON")
|
||||||
|
{
|
||||||
|
AddAlias("p");
|
||||||
|
this.AddIOCommandOptions();
|
||||||
|
AddOption(IndentSizeOption);
|
||||||
|
|
||||||
|
this.SetHandler(ExecuteAsync, new CommandOptionsBinder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ExecuteAsync(CommandOptions options)
|
||||||
|
{
|
||||||
|
var json = await IOHandler.HandleInput(options.Input, options.InputArgument, options.InputFile);
|
||||||
|
|
||||||
|
var output = JsonFormatter.Format(json, new()
|
||||||
|
{
|
||||||
|
Beautify = true,
|
||||||
|
IndentSize = options.IndentSize
|
||||||
|
});
|
||||||
|
|
||||||
|
await IOHandler.HandleOutput(options.Output, options.OutputFile, output);
|
||||||
|
}
|
||||||
|
}
|
23
DevDisciples.Json.Tools.CLI/JsonUglifyCommand.cs
Normal file
23
DevDisciples.Json.Tools.CLI/JsonUglifyCommand.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using DevDisciples.Json.Tools.CLI.Extensions;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public class JsonUglifyCommand : Command
|
||||||
|
{
|
||||||
|
public JsonUglifyCommand() : base("uglify", "Uglify JSON")
|
||||||
|
{
|
||||||
|
AddAlias("u");
|
||||||
|
this.AddIOCommandOptions();
|
||||||
|
this.SetHandler(ExecuteAsync, new CommandOptionsBinder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ExecuteAsync(CommandOptions options)
|
||||||
|
{
|
||||||
|
var json = await IOHandler.HandleInput(options.Input, options.InputArgument, options.InputFile);
|
||||||
|
|
||||||
|
var output = JsonFormatter.Format(json, new() { Beautify = false });
|
||||||
|
|
||||||
|
await IOHandler.HandleOutput(options.Output, options.OutputFile, output);
|
||||||
|
}
|
||||||
|
}
|
18
DevDisciples.Json.Tools.CLI/OutputOptions.cs
Normal file
18
DevDisciples.Json.Tools.CLI/OutputOptions.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public enum OutputOptions
|
||||||
|
{
|
||||||
|
// Standard output
|
||||||
|
stdout,
|
||||||
|
std,
|
||||||
|
s,
|
||||||
|
|
||||||
|
// Clipboard
|
||||||
|
clipboard,
|
||||||
|
clip,
|
||||||
|
c,
|
||||||
|
|
||||||
|
// File
|
||||||
|
file,
|
||||||
|
f,
|
||||||
|
}
|
21
DevDisciples.Json.Tools.CLI/Program.cs
Normal file
21
DevDisciples.Json.Tools.CLI/Program.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.CommandLine.Builder;
|
||||||
|
using System.CommandLine.Parsing;
|
||||||
|
using DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
var parser = new CommandLineBuilder(new RootCommand())
|
||||||
|
.UseDefaults()
|
||||||
|
.UseExceptionHandler((ex, ctx) =>
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Use a custom ExceptionHandler to prevent the System.CommandLine library from printing the StackTrace by default.
|
||||||
|
*/
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set the exit code to a non-zero value to indicate to the shell that the process has failed.
|
||||||
|
*/
|
||||||
|
Environment.ExitCode = -1;
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await parser.InvokeAsync(args);
|
13
DevDisciples.Json.Tools.CLI/RootCommand.cs
Normal file
13
DevDisciples.Json.Tools.CLI/RootCommand.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public class RootCommand : System.CommandLine.RootCommand
|
||||||
|
{
|
||||||
|
public override string Name => "jtr";
|
||||||
|
|
||||||
|
public RootCommand() : base("A JSON transform CLI tool by DevDisciples.")
|
||||||
|
{
|
||||||
|
AddCommand(new JsonUglifyCommand());
|
||||||
|
AddCommand(new JsonPrettifyCommand());
|
||||||
|
AddCommand(new Json2CSharpCommand());
|
||||||
|
}
|
||||||
|
}
|
25
DevDisciples.Json.Tools.CLI/SharedCommandOptions.cs
Normal file
25
DevDisciples.Json.Tools.CLI/SharedCommandOptions.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools.CLI;
|
||||||
|
|
||||||
|
public static class SharedCommandOptions
|
||||||
|
{
|
||||||
|
public static readonly Option<InputOptions> InputOption =
|
||||||
|
new(aliases: ["--input", "-i"], getDefaultValue: () => InputOptions.stdin) { IsRequired = false };
|
||||||
|
|
||||||
|
public static readonly Argument<string?> InputArgument =
|
||||||
|
new(name: "input", description: "The input argument.", getDefaultValue: () => default);
|
||||||
|
|
||||||
|
public static readonly Option<FileInfo> InputFileOption = new(
|
||||||
|
aliases: ["--input-file", "--if"],
|
||||||
|
description: "Read the input from a file. This option is required when using an input of type file."
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Option<FileInfo> OutputFileOption = new(
|
||||||
|
aliases: ["--output-file", "--of"],
|
||||||
|
description: "Write the output to a file. This option is required when using an output of type file."
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Option<OutputOptions> OutputOption =
|
||||||
|
new(aliases: ["--output", "-o"], getDefaultValue: () => OutputOptions.stdout) { IsRequired = false };
|
||||||
|
}
|
5
DevDisciples.Json.Tools.CLI/scripts/publish_as_tool.sh
Normal file
5
DevDisciples.Json.Tools.CLI/scripts/publish_as_tool.sh
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
dotnet pack -v d
|
||||||
|
|
||||||
|
dotnet tool update --global --add-source ./nupkg DevDisciples.Json.Tools.CLI -v d
|
14
DevDisciples.Json.Tools/DevDisciples.Json.Tools.csproj
Normal file
14
DevDisciples.Json.Tools/DevDisciples.Json.Tools.csproj
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DevDisciples.Json.Parser\DevDisciples.Json.Parser.csproj" />
|
||||||
|
<ProjectReference Include="..\DevDisciples.Parsing\DevDisciples.Parsing.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -0,0 +1,27 @@
|
|||||||
|
using Humanizer;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools;
|
||||||
|
|
||||||
|
public static partial class Json2CSharpTranslator
|
||||||
|
{
|
||||||
|
public struct ClassTranslation : ITranslation
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public List<PropertyTranslation> Properties { get; set; }
|
||||||
|
|
||||||
|
public void Translate(Context context)
|
||||||
|
{
|
||||||
|
context.Builder.Append($"public class {Name.Pascalize()}\n");
|
||||||
|
context.Builder.Append("{\n");
|
||||||
|
|
||||||
|
var last = Properties.Last();
|
||||||
|
foreach (var property in Properties)
|
||||||
|
{
|
||||||
|
property.Translate(context);
|
||||||
|
context.Builder.Append(property.Equals(last) ? string.Empty : "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Builder.Append("}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
DevDisciples.Json.Tools/Json2CSharpTranslator.Context.cs
Normal file
17
DevDisciples.Json.Tools/Json2CSharpTranslator.Context.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools;
|
||||||
|
|
||||||
|
public static partial class Json2CSharpTranslator
|
||||||
|
{
|
||||||
|
public class Context
|
||||||
|
{
|
||||||
|
public const string DefaultRootClassName = "Root";
|
||||||
|
public const string DefaultNamespace = "My.Namespace";
|
||||||
|
|
||||||
|
public string RootClassName { get; set; } = DefaultRootClassName;
|
||||||
|
public string Namespace { get; set; } = DefaultNamespace;
|
||||||
|
public List<ClassTranslation> Classes { get; set; } = new();
|
||||||
|
public readonly StringBuilder Builder = new();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
namespace DevDisciples.Json.Tools;
|
||||||
|
|
||||||
|
public static partial class Json2CSharpTranslator
|
||||||
|
{
|
||||||
|
public interface ITranslation
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public void Translate(Context context);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, List<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using Humanizer;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools;
|
||||||
|
|
||||||
|
public static partial class Json2CSharpTranslator
|
||||||
|
{
|
||||||
|
public struct PropertyTranslation : ITranslation
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public void Translate(Context context)
|
||||||
|
{
|
||||||
|
context.Builder.Append($"\tpublic {Type} {Name.Pascalize()} {{ get; set; }}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
DevDisciples.Json.Tools/Json2CSharpTranslator.cs
Normal file
44
DevDisciples.Json.Tools/Json2CSharpTranslator.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using DevDisciples.Json.Parser;
|
||||||
|
using DevDisciples.Parsing;
|
||||||
|
using Humanizer;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools;
|
||||||
|
|
||||||
|
public static partial class Json2CSharpTranslator
|
||||||
|
{
|
||||||
|
public static readonly VisitorContainer<ISyntaxNode, ITranslation> Visitors;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Translate(string source, Context? context = null)
|
||||||
|
{
|
||||||
|
if (JsonParser.Parse("<source>", source) is not JsonObject root)
|
||||||
|
throw new Exception("Expected a JSON object.");
|
||||||
|
|
||||||
|
context ??= new();
|
||||||
|
|
||||||
|
var visitor = Visitors[typeof(JsonObject)];
|
||||||
|
visitor(root, context, context.RootClassName);
|
||||||
|
|
||||||
|
context.Builder.Append("//using System;\n");
|
||||||
|
context.Builder.Append("//using System.Collections.Generic;\n");
|
||||||
|
context.Builder.Append('\n');
|
||||||
|
context.Builder.Append($"namespace {context.Namespace};\n\n");
|
||||||
|
context.Classes.ForEach(@class => @class.Translate(context));
|
||||||
|
|
||||||
|
return context.Builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Context ContextFromArgs(object[] args) => (args[0] as Context)!;
|
||||||
|
|
||||||
|
private static string NameFromArgs(object[] args) => ((string)args[1]).Camelize();
|
||||||
|
}
|
20
DevDisciples.Json.Tools/JsonFormatter.Context.cs
Normal file
20
DevDisciples.Json.Tools/JsonFormatter.Context.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools;
|
||||||
|
|
||||||
|
public static partial class JsonFormatter
|
||||||
|
{
|
||||||
|
public class Context
|
||||||
|
{
|
||||||
|
protected int Depth { get; set; } = 0;
|
||||||
|
public StringBuilder Builder { get; set; } = new();
|
||||||
|
public bool Beautify { get; set; } = false;
|
||||||
|
public string Indent => new(' ', Depth);
|
||||||
|
public int IndentSize { get; set; } = 2;
|
||||||
|
public string NewLine => Beautify ? "\n" : "";
|
||||||
|
public string Space => Beautify ? " " : "";
|
||||||
|
|
||||||
|
public void IncrementDepth() => Depth += Beautify ? IndentSize : 0;
|
||||||
|
public void DecrementDepth() => Depth -= Beautify ? IndentSize : 0;
|
||||||
|
}
|
||||||
|
}
|
106
DevDisciples.Json.Tools/JsonFormatter.cs
Normal file
106
DevDisciples.Json.Tools/JsonFormatter.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using DevDisciples.Json.Parser;
|
||||||
|
using DevDisciples.Parsing;
|
||||||
|
|
||||||
|
namespace DevDisciples.Json.Tools;
|
||||||
|
|
||||||
|
public static partial class JsonFormatter
|
||||||
|
{
|
||||||
|
public static VisitorContainer Visitors { get; }
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Format(string source, Context? context)
|
||||||
|
{
|
||||||
|
var nodes = JsonParser.Parse("<source>", source);
|
||||||
|
return Format(nodes, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Format(ISyntaxNode visitee, Context? context = null)
|
||||||
|
{
|
||||||
|
var ctx = context ?? new();
|
||||||
|
Visitors[visitee.GetType()](visitee, ctx);
|
||||||
|
return context!.Builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Context ContextFromArgs(object[] args)
|
||||||
|
{
|
||||||
|
return (args[0] as Context)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintArray(object visitee, object[] args)
|
||||||
|
{
|
||||||
|
var context = ContextFromArgs(args);
|
||||||
|
var array = (JsonArray)visitee;
|
||||||
|
|
||||||
|
context.Builder.Append($"[{context.NewLine}");
|
||||||
|
context.IncrementDepth();
|
||||||
|
|
||||||
|
for (var i = 0; i < array.Elements.Count; i++)
|
||||||
|
{
|
||||||
|
var node = array.Elements[i];
|
||||||
|
context.Builder.Append(context.Indent);
|
||||||
|
Visitors[node.GetType()](node, args);
|
||||||
|
if (i < array.Elements.Count - 1) context.Builder.Append($",{context.NewLine}");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.DecrementDepth();
|
||||||
|
context.Builder.Append($"{context.NewLine}{context.Indent}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintObject(object visitee, object[] args)
|
||||||
|
{
|
||||||
|
var context = ContextFromArgs(args);
|
||||||
|
var @object = (JsonObject)visitee;
|
||||||
|
|
||||||
|
context.Builder.Append($"{{{context.NewLine}");
|
||||||
|
context.IncrementDepth();
|
||||||
|
|
||||||
|
var count = @object.Properties.Count;
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var (key, node) = @object.Properties.ElementAt(i);
|
||||||
|
context.Builder.Append($"{context.Indent}\"{key}\":{context.Space}");
|
||||||
|
Visitors[node.GetType()](node, args);
|
||||||
|
if (i < count - 1) context.Builder.Append($",{context.NewLine}");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.DecrementDepth();
|
||||||
|
context.Builder.Append($"{context.NewLine}{context.Indent}}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintString(object visitee, object[] args)
|
||||||
|
{
|
||||||
|
var context = ContextFromArgs(args);
|
||||||
|
var @string = (JsonString)visitee;
|
||||||
|
context.Builder.Append($"\"{@string.Token.Lexeme}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintNumber(object visitee, object[] args)
|
||||||
|
{
|
||||||
|
var context = ContextFromArgs(args);
|
||||||
|
var number = (JsonNumber)visitee;
|
||||||
|
context.Builder.Append($"{number.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintBool(object visitee, object[] args)
|
||||||
|
{
|
||||||
|
var context = ContextFromArgs(args);
|
||||||
|
var @bool = (JsonBool)visitee;
|
||||||
|
context.Builder.Append($"{@bool.Value.ToString().ToLower()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintNull(object visitee, object[] args)
|
||||||
|
{
|
||||||
|
var context = ContextFromArgs(args);
|
||||||
|
context.Builder.Append("null");
|
||||||
|
}
|
||||||
|
}
|
46
DevDisciples.Parsing.sln
Normal file
46
DevDisciples.Parsing.sln
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevDisciples.Parsing", "DevDisciples.Parsing\DevDisciples.Parsing.csproj", "{DE9A4F6A-08EF-4DFE-99B6-1986BEDB4FA2}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevDisciples.Json.Parser", "DevDisciples.Json.Parser\DevDisciples.Json.Parser.csproj", "{50AB3E1D-B9E0-481D-936E-9ADE737EF593}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevDisciples.Json.Parser.Tests", "DevDisciples.Json.Parser.Tests\DevDisciples.Json.Parser.Tests.csproj", "{F89D0862-4DBD-4D71-8B1B-D6E9E684005D}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevDisciples.Json.Tools.CLI", "DevDisciples.Json.Tools.CLI\DevDisciples.Json.Tools.CLI.csproj", "{A3CFA61D-0EA5-4D31-B438-885035B51198}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevDisciples.Json.Tools", "DevDisciples.Json.Tools\DevDisciples.Json.Tools.csproj", "{44BBB630-B403-49C0-B973-9A2E9CAF93CD}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevDisciples.Json.Tools.API", "DevDisciples.Json.Tools.API\DevDisciples.Json.Tools.API.csproj", "{0E19CB86-C12D-4A6E-B53F-F36FD14BA200}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{DE9A4F6A-08EF-4DFE-99B6-1986BEDB4FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{DE9A4F6A-08EF-4DFE-99B6-1986BEDB4FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DE9A4F6A-08EF-4DFE-99B6-1986BEDB4FA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{DE9A4F6A-08EF-4DFE-99B6-1986BEDB4FA2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{50AB3E1D-B9E0-481D-936E-9ADE737EF593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{50AB3E1D-B9E0-481D-936E-9ADE737EF593}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{50AB3E1D-B9E0-481D-936E-9ADE737EF593}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{50AB3E1D-B9E0-481D-936E-9ADE737EF593}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F89D0862-4DBD-4D71-8B1B-D6E9E684005D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F89D0862-4DBD-4D71-8B1B-D6E9E684005D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F89D0862-4DBD-4D71-8B1B-D6E9E684005D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F89D0862-4DBD-4D71-8B1B-D6E9E684005D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A3CFA61D-0EA5-4D31-B438-885035B51198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A3CFA61D-0EA5-4D31-B438-885035B51198}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A3CFA61D-0EA5-4D31-B438-885035B51198}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A3CFA61D-0EA5-4D31-B438-885035B51198}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{44BBB630-B403-49C0-B973-9A2E9CAF93CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{44BBB630-B403-49C0-B973-9A2E9CAF93CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{44BBB630-B403-49C0-B973-9A2E9CAF93CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{44BBB630-B403-49C0-B973-9A2E9CAF93CD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0E19CB86-C12D-4A6E-B53F-F36FD14BA200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0E19CB86-C12D-4A6E-B53F-F36FD14BA200}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0E19CB86-C12D-4A6E-B53F-F36FD14BA200}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0E19CB86-C12D-4A6E-B53F-F36FD14BA200}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
9
DevDisciples.Parsing/DevDisciples.Parsing.csproj
Normal file
9
DevDisciples.Parsing/DevDisciples.Parsing.csproj
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
8
DevDisciples.Parsing/ISourceLocation.cs
Normal file
8
DevDisciples.Parsing/ISourceLocation.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public interface ISourceLocation
|
||||||
|
{
|
||||||
|
public string File { get; }
|
||||||
|
public int Line { get; }
|
||||||
|
public int Column { get; }
|
||||||
|
}
|
3
DevDisciples.Parsing/ISyntaxNode.cs
Normal file
3
DevDisciples.Parsing/ISyntaxNode.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public interface ISyntaxNode { }
|
29
DevDisciples.Parsing/Lexer.Context.cs
Normal file
29
DevDisciples.Parsing/Lexer.Context.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public abstract partial class Lexer<TToken> where TToken : Enum
|
||||||
|
{
|
||||||
|
public class Context
|
||||||
|
{
|
||||||
|
public string File { get; }
|
||||||
|
public Source Source { get; }
|
||||||
|
public List<Token> Tokens { get; }
|
||||||
|
|
||||||
|
public Context(string file, Source source, List<Token> tokens)
|
||||||
|
{
|
||||||
|
File = file;
|
||||||
|
Source = source;
|
||||||
|
Tokens = tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddToken(TToken type, string lexeme, int line = -1, int column = -1)
|
||||||
|
{
|
||||||
|
Tokens.Add(new Token(
|
||||||
|
File,
|
||||||
|
type,
|
||||||
|
lexeme,
|
||||||
|
line == -1 ? Source.Line : line,
|
||||||
|
column == -1 ? Source.Column : column
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
213
DevDisciples.Parsing/Lexer.Rule.cs
Normal file
213
DevDisciples.Parsing/Lexer.Rule.cs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public abstract partial class Lexer<TToken> where TToken : Enum
|
||||||
|
{
|
||||||
|
public delegate bool Rule(Context ctx);
|
||||||
|
|
||||||
|
protected static class DefaultRule
|
||||||
|
{
|
||||||
|
private const int DefaultSpaceSize = 1;
|
||||||
|
private const int DefaultTabSize = 4;
|
||||||
|
|
||||||
|
public static bool IgnoreWhitespace(Context context, int space = DefaultSpaceSize, int tab = DefaultTabSize)
|
||||||
|
{
|
||||||
|
space = space <= 0 ? DefaultSpaceSize : space;
|
||||||
|
tab = tab <= 0 ? DefaultTabSize : tab;
|
||||||
|
|
||||||
|
var src = context.Source;
|
||||||
|
|
||||||
|
if (src.Match('\r'))
|
||||||
|
{
|
||||||
|
src.Ignore();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.Match(' '))
|
||||||
|
{
|
||||||
|
src.Ignore();
|
||||||
|
src.Column += space;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.Match('\t'))
|
||||||
|
{
|
||||||
|
src.Ignore();
|
||||||
|
src.Column += tab;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool NewLine(Context context)
|
||||||
|
{
|
||||||
|
if (!context.Source.Match('\n')) return false;
|
||||||
|
|
||||||
|
context.Source.Line++;
|
||||||
|
context.Source.Column = 1;
|
||||||
|
context.Source.Ignore();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rule SingleQuoteString(TToken token)
|
||||||
|
{
|
||||||
|
return context =>
|
||||||
|
{
|
||||||
|
if (!context.Source.Match('\'')) return false;
|
||||||
|
|
||||||
|
var src = context.Source;
|
||||||
|
int line = src.Line, column = src.Column;
|
||||||
|
|
||||||
|
while (src.Peek() != '\'' && !src.Ended())
|
||||||
|
{
|
||||||
|
// Account for source lines and columns when dealing with a string spanning multiple lines.
|
||||||
|
if (src.Peek() == '\n')
|
||||||
|
{
|
||||||
|
src.Line++;
|
||||||
|
src.Column = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
src.Advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.Ended())
|
||||||
|
{
|
||||||
|
throw new ParsingException(
|
||||||
|
$"[line: {src.Line}, column: {src.Column}] Unterminated string near '{src.Last}'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include the closing quotation mark.
|
||||||
|
src.Advance();
|
||||||
|
|
||||||
|
var lexeme = src.Extract();
|
||||||
|
|
||||||
|
context.AddToken(
|
||||||
|
token,
|
||||||
|
lexeme.Trim('\''),
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rule DoubleQuoteString(TToken token)
|
||||||
|
{
|
||||||
|
return context =>
|
||||||
|
{
|
||||||
|
if (!context.Source.Match('"')) return false;
|
||||||
|
|
||||||
|
var src = context.Source;
|
||||||
|
int line = src.Line, column = src.Column;
|
||||||
|
|
||||||
|
while (src.Peek() != '"' && !src.Ended())
|
||||||
|
{
|
||||||
|
// Account for source lines and columns when dealing with a string spanning multiple lines.
|
||||||
|
if (src.Peek() == '\n')
|
||||||
|
{
|
||||||
|
src.Line++;
|
||||||
|
src.Column = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
src.Advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.Ended())
|
||||||
|
{
|
||||||
|
throw new ParsingException(
|
||||||
|
$"[line: {src.Line}, column: {src.Column}] Unterminated string near '{src.Last}'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include the closing quotation mark.
|
||||||
|
src.Advance();
|
||||||
|
|
||||||
|
var lexeme = src.Extract();
|
||||||
|
|
||||||
|
context.AddToken(
|
||||||
|
token,
|
||||||
|
lexeme.Trim('"'),
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rule Identifier(TToken token)
|
||||||
|
{
|
||||||
|
return context =>
|
||||||
|
{
|
||||||
|
var src = context.Source;
|
||||||
|
|
||||||
|
if (!IsAlpha(src.Peek())) return false;
|
||||||
|
|
||||||
|
int line = src.Line, column = src.Column;
|
||||||
|
|
||||||
|
while (IsAlphaNumeric(src.Peek())) src.Advance();
|
||||||
|
|
||||||
|
var lexeme = src.Extract();
|
||||||
|
|
||||||
|
context.AddToken(token, lexeme, line, column);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// public static bool DoubleSlashComment(Context context)
|
||||||
|
// {
|
||||||
|
// var src = context.Source;
|
||||||
|
//
|
||||||
|
// if (!src.Match("//")) return false;
|
||||||
|
//
|
||||||
|
// while (!src.Ended() && src.Peek() != '\n') src.Advance();
|
||||||
|
//
|
||||||
|
// src.Ignore();
|
||||||
|
//
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
private static bool IsAlpha(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or '_';
|
||||||
|
|
||||||
|
private static bool IsAlphaNumeric(char c) => IsAlpha(c) || IsDigit(c);
|
||||||
|
|
||||||
|
public static Rule Number(TToken token)
|
||||||
|
{
|
||||||
|
return context =>
|
||||||
|
{
|
||||||
|
var src = context.Source;
|
||||||
|
|
||||||
|
if (!IsDigit(src.Peek())) return false;
|
||||||
|
|
||||||
|
int line = src.Line, column = src.Column;
|
||||||
|
|
||||||
|
ProcessDigits(src);
|
||||||
|
|
||||||
|
if (src.Peek() == '.' && IsDigit(src.Peek(1)))
|
||||||
|
{
|
||||||
|
// Consume the "."
|
||||||
|
src.Advance();
|
||||||
|
ProcessDigits(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lexeme = src.Extract();
|
||||||
|
|
||||||
|
context.AddToken(token, lexeme, line, column);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDigit(char c) => c is >= '0' and <= '9';
|
||||||
|
|
||||||
|
private static void ProcessDigits(Source src)
|
||||||
|
{
|
||||||
|
while (IsDigit(src.Peek()))
|
||||||
|
src.Advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
DevDisciples.Parsing/Lexer.Token.cs
Normal file
23
DevDisciples.Parsing/Lexer.Token.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public abstract partial class Lexer<TToken> where TToken : Enum
|
||||||
|
{
|
||||||
|
public struct Token : ISourceLocation
|
||||||
|
{
|
||||||
|
public string File { get; }
|
||||||
|
public TToken Type { get; }
|
||||||
|
public string Lexeme { get; }
|
||||||
|
public int Line { get; }
|
||||||
|
public int Column { get; }
|
||||||
|
|
||||||
|
|
||||||
|
public Token(string file, TToken type, string lexeme, int line, int column)
|
||||||
|
{
|
||||||
|
File = file;
|
||||||
|
Type = type;
|
||||||
|
Lexeme = lexeme;
|
||||||
|
Line = line;
|
||||||
|
Column = column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
DevDisciples.Parsing/Lexer.cs
Normal file
68
DevDisciples.Parsing/Lexer.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public abstract partial class Lexer<TToken> where TToken : Enum
|
||||||
|
{
|
||||||
|
protected List<Rule> Rules { get; init; } = default!;
|
||||||
|
|
||||||
|
protected abstract TToken EndOfSource { get; }
|
||||||
|
|
||||||
|
public List<Token> Lex(string file, string source)
|
||||||
|
{
|
||||||
|
var ctx = new Context(file, new Source(file, source), new List<Token>());
|
||||||
|
|
||||||
|
while (!ctx.Source.Ended())
|
||||||
|
{
|
||||||
|
var matched = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < Rules.Count; i++)
|
||||||
|
{
|
||||||
|
if (Rules[i](ctx))
|
||||||
|
{
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matched)
|
||||||
|
{
|
||||||
|
Report.Halt(ctx.Source, $"Unexpected character '{ctx.Source.Current}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.AddToken(EndOfSource, "<EOF>", ctx.Source.Line, ctx.Source.Column);
|
||||||
|
|
||||||
|
return ctx.Tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static bool Match(Context ctx, TToken type, char @char)
|
||||||
|
{
|
||||||
|
if (!ctx.Source.Match(@char)) return false;
|
||||||
|
|
||||||
|
var line = ctx.Source.Line;
|
||||||
|
var column = ctx.Source.Column;
|
||||||
|
var lexeme = ctx.Source.Extract();
|
||||||
|
|
||||||
|
ctx.Source.Column += 1;
|
||||||
|
|
||||||
|
ctx.AddToken(type, lexeme, line, column);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Do not use this method for keywords!
|
||||||
|
* This will treat an identifier named 'ifelse' as separated 'if' and 'else' tokens.
|
||||||
|
*/
|
||||||
|
protected static bool Match(Context ctx, TToken token, string sequence)
|
||||||
|
{
|
||||||
|
if (!ctx.Source.Match(sequence)) return false;
|
||||||
|
|
||||||
|
var line = ctx.Source.Line;
|
||||||
|
var column = ctx.Source.Column;
|
||||||
|
var lexeme = ctx.Source.Extract();
|
||||||
|
ctx.Source.Column += sequence.Length;
|
||||||
|
ctx.Tokens.Add(new Token(ctx.File, token, lexeme, line, column));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
32
DevDisciples.Parsing/ParsableStream.cs
Normal file
32
DevDisciples.Parsing/ParsableStream.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public abstract class ParsableStream<T>
|
||||||
|
{
|
||||||
|
private readonly ReadOnlyMemory<T> _tokens;
|
||||||
|
|
||||||
|
protected ReadOnlySpan<T> Tokens => _tokens.Span;
|
||||||
|
|
||||||
|
public int Position { get; set; }
|
||||||
|
|
||||||
|
public T Current => Position < Tokens.Length ? Tokens[Position] : default!;
|
||||||
|
|
||||||
|
public ParsableStream(ReadOnlyMemory<T> tokens)
|
||||||
|
{
|
||||||
|
_tokens = tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual T Advance()
|
||||||
|
{
|
||||||
|
return Position + 1 <= Tokens.Length ? Tokens[++Position - 1] : default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual T Peek(int offset = 0)
|
||||||
|
{
|
||||||
|
return Position + offset < Tokens.Length ? Tokens[Position + offset] : default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool Ended()
|
||||||
|
{
|
||||||
|
return Position >= Tokens.Length;
|
||||||
|
}
|
||||||
|
}
|
106
DevDisciples.Parsing/ParserContext.cs
Normal file
106
DevDisciples.Parsing/ParserContext.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public class ParserContext<TToken> : ParsableStream<Lexer<TToken>.Token> where TToken : Enum
|
||||||
|
{
|
||||||
|
protected readonly TToken _endOfSource;
|
||||||
|
|
||||||
|
public ParserContext(Memory<Lexer<TToken>.Token> tokens, TToken endOfSource) : base(tokens)
|
||||||
|
{
|
||||||
|
_endOfSource = endOfSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Check(TToken type, int offset = 0)
|
||||||
|
{
|
||||||
|
if (Ended()) return false;
|
||||||
|
if (Equals(Peek(offset).Type, _endOfSource)) return false;
|
||||||
|
return Equals(Peek(offset).Type, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the passed sequence can be matched against the current parsing context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sequence"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool CheckSequence(params TToken[] sequence)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < sequence.Length; i++)
|
||||||
|
{
|
||||||
|
if (!Check(sequence[i], i))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Ended()
|
||||||
|
{
|
||||||
|
return base.Ended() || Equals(Current.Type, _endOfSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Match(TToken token)
|
||||||
|
{
|
||||||
|
var matched = Check(token);
|
||||||
|
if (matched) Advance();
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MatchAny(params TToken[] types)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < types.Length; i++)
|
||||||
|
{
|
||||||
|
if (Check(types[i]))
|
||||||
|
{
|
||||||
|
Advance();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MatchSequence(params TToken[] sequence)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < sequence.Length; i++)
|
||||||
|
{
|
||||||
|
if (!Check(sequence[i], i))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < sequence.Length; i++)
|
||||||
|
{
|
||||||
|
Advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Lexer<TToken>.Token Previous()
|
||||||
|
{
|
||||||
|
return Peek(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Lexer<TToken>.Token Consume(TToken type, string message)
|
||||||
|
{
|
||||||
|
if (Check(type)) return Advance();
|
||||||
|
throw Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Exception Error(string message)
|
||||||
|
{
|
||||||
|
return new ParsingException(Report.FormatMessage(Current, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Exception Error(Lexer<TToken>.Token token, string message)
|
||||||
|
{
|
||||||
|
return new ParsingException(Report.FormatMessage(token, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Halt(Lexer<TToken>.Token token, string message)
|
||||||
|
{
|
||||||
|
throw new ParsingException(Report.FormatMessage(token, message));
|
||||||
|
}
|
||||||
|
}
|
16
DevDisciples.Parsing/ParsingException.cs
Normal file
16
DevDisciples.Parsing/ParsingException.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public class ParsingException : Exception
|
||||||
|
{
|
||||||
|
public ParsingException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParsingException(string? message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParsingException(string? message, Exception? innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
19
DevDisciples.Parsing/Report.cs
Normal file
19
DevDisciples.Parsing/Report.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public static class Report
|
||||||
|
{
|
||||||
|
public static Exception Error(ISourceLocation token, string message)
|
||||||
|
{
|
||||||
|
return new(FormatMessage(token, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Halt(ISourceLocation token, string message)
|
||||||
|
{
|
||||||
|
throw new(FormatMessage(token, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatMessage(ISourceLocation token, string msg)
|
||||||
|
{
|
||||||
|
return $"{token.File}\n\t[line: {token.Line}, column: {token.Column}] {msg}";
|
||||||
|
}
|
||||||
|
}
|
64
DevDisciples.Parsing/Source.cs
Normal file
64
DevDisciples.Parsing/Source.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public class Source : ParsableStream<char>, ISourceLocation
|
||||||
|
{
|
||||||
|
private readonly string _source;
|
||||||
|
public string File { get; }
|
||||||
|
public int Start { get; set; }
|
||||||
|
public int Line { get; set; } = 1;
|
||||||
|
public int Column { get; set; } = 1;
|
||||||
|
public char Last => Tokens[^1];
|
||||||
|
|
||||||
|
public Source(string file, string source) : base(source.AsMemory())
|
||||||
|
{
|
||||||
|
File = file;
|
||||||
|
_source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override char Advance()
|
||||||
|
{
|
||||||
|
Column++;
|
||||||
|
return base.Advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Ended()
|
||||||
|
{
|
||||||
|
return Current == '\0' || base.Ended();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Extract()
|
||||||
|
{
|
||||||
|
var position = (Start, Length: Position - Start);
|
||||||
|
Start = Position;
|
||||||
|
return _source.Substring(position.Start, position.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Ignore()
|
||||||
|
{
|
||||||
|
Start = Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Match(char expected)
|
||||||
|
{
|
||||||
|
if (Tokens[Position] != expected) return false;
|
||||||
|
Position += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Match(ReadOnlySpan<char> expected)
|
||||||
|
{
|
||||||
|
if (Position + expected.Length > Tokens.Length) return false;
|
||||||
|
|
||||||
|
for (var index = 0; index < expected.Length; index++)
|
||||||
|
{
|
||||||
|
if (Tokens[Position + index] != expected[index])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Position += expected.Length;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
8
DevDisciples.Parsing/Visitor.cs
Normal file
8
DevDisciples.Parsing/Visitor.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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);
|
||||||
|
}
|
42
DevDisciples.Parsing/VisitorContainer.cs
Normal file
42
DevDisciples.Parsing/VisitorContainer.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
namespace DevDisciples.Parsing;
|
||||||
|
|
||||||
|
public class VisitorContainer
|
||||||
|
{
|
||||||
|
private Dictionary<Type, Visitor.Visit> Visitors { get; } = new();
|
||||||
|
private Visitor.Visit Default { get; set; } = default!;
|
||||||
|
|
||||||
|
public void Register<TVisitee>(Visitor.Visit visitor)
|
||||||
|
{
|
||||||
|
Visitors[typeof(TVisitee)] = visitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visitor.Visit this[Type type] => Visitors.GetValueOrDefault(type, Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VisitorContainer<T>
|
||||||
|
{
|
||||||
|
protected Dictionary<Type, Visitor.Visit<T>> Visitors { get; } = new();
|
||||||
|
public Visitor.Visit<T> Default { get; set; } = default!;
|
||||||
|
|
||||||
|
|
||||||
|
public VisitorContainer<T> Register<TVisitee>(Visitor.Visit<T> visitor)
|
||||||
|
{
|
||||||
|
Visitors[typeof(TVisitee)] = visitor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visitor.Visit<T> this[Type type] => Visitors.ContainsKey(type) ? Visitors[type] : Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VisitorContainer<TIn, TOut>
|
||||||
|
{
|
||||||
|
protected Dictionary<Type, Visitor.Visit<TIn, TOut>> Visitors { get; } = new();
|
||||||
|
public Visitor.Visit<TIn, TOut> Default { get; set; } = default!;
|
||||||
|
|
||||||
|
public void Register<TVisitee>(Visitor.Visit<TIn, TOut> visitor)
|
||||||
|
{
|
||||||
|
Visitors[typeof(TVisitee)] = visitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visitor.Visit<TIn, TOut> this[Type type] => Visitors.ContainsKey(type) ? Visitors[type] : Default;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user