From 95ed887097d82432df56b0a2489faa3da6dd5e3b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 15:46:06 +0000 Subject: [PATCH 1/9] Create draft PR for #52 [skip ci] From 910fc8a33e3b842fba56505b950f10fc4465a234 Mon Sep 17 00:00:00 2001 From: Matt Edwards Date: Fri, 18 Apr 2025 14:04:39 -0400 Subject: [PATCH 2/9] WIP: Adding Json and JsonPath Lab extensions --- Hyperbee.XS.sln | 6 + .../Hyperbee.Xs.Extensions.Lab.csproj | 56 +++++++ .../JsonParseExtension.cs | 101 ++++++++++++ src/Hyperbee.XS.Extensions.Lab/README.md | 20 +++ .../Hyperbee.Xs.Extensions.csproj | 2 +- .../Core/Parsers/RawStringParser.cs | 1 - src/Hyperbee.XS/Hyperbee.XS.csproj | 2 +- src/Hyperbee.XS/XsParser.Literals.cs | 4 +- .../Hyperbee.XS.Extensions.Tests.csproj | 4 +- .../JsonParseExtensionTests.cs | 151 ++++++++++++++++++ .../Hyperbee.XS.Interactive.Tests.csproj | 2 +- .../Hyperbee.XS.Tests.csproj | 3 +- 12 files changed, 344 insertions(+), 8 deletions(-) create mode 100644 src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj create mode 100644 src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs create mode 100644 src/Hyperbee.XS.Extensions.Lab/README.md create mode 100644 test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs diff --git a/Hyperbee.XS.sln b/Hyperbee.XS.sln index 0261e79..8e000d4 100644 --- a/Hyperbee.XS.sln +++ b/Hyperbee.XS.sln @@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.Xs.Interactive", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.XS.Interactive.Tests", "test\Hyperbee.XS.Interactive.Tests\Hyperbee.XS.Interactive.Tests.csproj", "{92F65113-015D-8683-5CD4-57D748DB3B5D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.Xs.Extensions.Lab", "src\Hyperbee.XS.Extensions.Lab\Hyperbee.Xs.Extensions.Lab.csproj", "{21C6B563-32FB-407A-82A9-E63F59A1AEFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +85,10 @@ Global {92F65113-015D-8683-5CD4-57D748DB3B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU {92F65113-015D-8683-5CD4-57D748DB3B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU {92F65113-015D-8683-5CD4-57D748DB3B5D}.Release|Any CPU.Build.0 = Release|Any CPU + {21C6B563-32FB-407A-82A9-E63F59A1AEFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21C6B563-32FB-407A-82A9-E63F59A1AEFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21C6B563-32FB-407A-82A9-E63F59A1AEFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21C6B563-32FB-407A-82A9-E63F59A1AEFB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj b/src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj new file mode 100644 index 0000000..fb56860 --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj @@ -0,0 +1,56 @@ + + + + net8.0;net9.0 + enable + disable + true + + Stillpoint Software, Inc. + Hyperbee.XS.Extensions.Lab + README.md + expressions;script + + icon.png + https://stillpoint-software.github.io/hyperbee.xs/ + LICENSE + Stillpoint Software, Inc. + Hyperbee Expression Script [XS] Language Extensions (lab) + Sample Expression Script [XS] language extensions. + https://github.com/Stillpoint-Software/Hyperbee.XS + git + https://github.com/Stillpoint-Software/Hyperbee.XS/releases/latest + $(MSBuildProjectName.Replace(" ", "_")) + + + + + + + + + <_Parameter1>$(AssemblyName).Tests + + + <_Parameter1>$(AssemblyName).Benchmark + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs new file mode 100644 index 0000000..b5ca88e --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs @@ -0,0 +1,101 @@ +using System.Linq.Expressions; +using System.Text.Json; +using Hyperbee.Expressions; +using Hyperbee.Expressions.Lab; +using Hyperbee.XS.Core; +using Hyperbee.XS.Core.Parsers; +using Hyperbee.XS.Core.Writer; +using Parlot.Fluent; + +using static System.Linq.Expressions.Expression; +using static Parlot.Fluent.Parsers; +using ExpressionExtensions = Hyperbee.Expressions.Lab.ExpressionExtensions; + +namespace Hyperbee.Xs.Extensions.Lab; + +public class JsonParseExtension : IParseExtension, IExpressionWriter, IXsWriter +{ + public ExtensionType Type => ExtensionType.Expression; + public string Key => "json"; + + public Parser CreateParser( ExtensionBinder binder ) + { + var (expression, _) = binder; + // var element = json """{ "first": 1, "second": 2 }""" + // var person = json """{ "name": "John", "age": 30 }""" + + var stringLiteral = Terms.String( StringLiteralQuotes.Double ) + .Then( static value => Constant( value.ToString() ) ); + + var selectLiteral = Terms.String() + .Then( static value => Constant( value.ToString() ) ); + + var rawStringLiteral = new RawStringParser() + .Then( static value => Constant( value.ToString() ) ); + + return + ZeroOrOne( + Between( + Terms.Char( '<' ), + XsParsers.TypeRuntime(), + Terms.Char( '>' ) + ) + ) + .AndSkip( new WhiteSpaceLiteral( true ) ) + .And( + OneOf( + rawStringLiteral, + stringLiteral, + expression + ) + ) + .Then( static parts => + { + var (type, value) = parts; + + return ExpressionExtensions.Json( value, type ); + } ) + .And( + ZeroOrOne( + Terms.Text( "->" ).SkipAnd( selectLiteral ) + ) + ).Then( static (ctx,parts) => + { + var (json, select) = parts; + + return select == null + ? json + : ExpressionExtensions.JsonPath( json, select ); + } + ) + .Named( "json" ); + } + + public bool CanWrite( Expression node ) + { + return node is JsonExpression; + } + + public void WriteExpression( Expression node, ExpressionWriterContext context ) + { + if ( node is not JsonExpression jsonExpression ) + return; + + using var writer = context.EnterExpression( "Hyperbee.Expressions.ExpressionExtensions.Lab.Json", true, false ); + + writer.WriteExpression( jsonExpression.InputExpression ); + writer.Write( ",\n" ); + writer.Write( jsonExpression.Type, indent: true ); + } + + public void WriteExpression( Expression node, XsWriterContext context ) + { + if ( node is not JsonExpression jsonExtension ) + return; + + using var writer = context.GetWriter(); + + writer.Write( "json " ); + writer.WriteExpression( jsonExtension.InputExpression ); + } +} diff --git a/src/Hyperbee.XS.Extensions.Lab/README.md b/src/Hyperbee.XS.Extensions.Lab/README.md new file mode 100644 index 0000000..941c7f2 --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/README.md @@ -0,0 +1,20 @@ +# XS.Extensions (Lab): Sample Extensions for Hyperbee.XS + +### **What is XS?** + +[XS](https://github.com/Stillpoint-Software/hyperbee.xs) is a lightweight scripting language designed to simplify and enhance the use of C# expression trees. +It provides a familiar C#-like syntax while offering advanced extensibility, making it a compelling choice for developers +building domain-specific languages (DSLs), rules engines, or dynamic runtime logic systems. + +XS.Extensions (Lab) is a collection of sample and proposed extensions for the XS language, including: + +- Fetch +- Json +- JsonPath +- Reduce +- Map + +## Contributing + +We welcome contributions! Please see our [Contributing Guide](https://github.com/Stillpoint-Software/.github/blob/main/.github/CONTRIBUTING.md) +for more details. diff --git a/src/Hyperbee.XS.Extensions/Hyperbee.Xs.Extensions.csproj b/src/Hyperbee.XS.Extensions/Hyperbee.Xs.Extensions.csproj index 9968dec..c5cad70 100644 --- a/src/Hyperbee.XS.Extensions/Hyperbee.Xs.Extensions.csproj +++ b/src/Hyperbee.XS.Extensions/Hyperbee.Xs.Extensions.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs b/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs index 74ad363..4b153c4 100644 --- a/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs +++ b/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs @@ -82,7 +82,6 @@ public override bool Parse( ParseContext context, ref ParseResult resu result.Set( start.Offset, end, decoded ); context.ExitParser( this ); - cursor.Advance(); return true; diff --git a/src/Hyperbee.XS/Hyperbee.XS.csproj b/src/Hyperbee.XS/Hyperbee.XS.csproj index 63a52a5..5482b89 100644 --- a/src/Hyperbee.XS/Hyperbee.XS.csproj +++ b/src/Hyperbee.XS/Hyperbee.XS.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/Hyperbee.XS/XsParser.Literals.cs b/src/Hyperbee.XS/XsParser.Literals.cs index 4893e4b..aab2f2e 100644 --- a/src/Hyperbee.XS/XsParser.Literals.cs +++ b/src/Hyperbee.XS/XsParser.Literals.cs @@ -35,8 +35,8 @@ private static Parser LiteralParser( XsConfig config, Deferred( static value => Constant( value.ToString() ) ); - var rawStringLiteral = new RawStringParser(). - Then( static value => Constant( value.ToString() ) ); + var rawStringLiteral = new RawStringParser() + .Then( static value => Constant( value.ToString() ) ); var nullLiteral = Terms.Text( "null" ) .Then( static _ => Constant( null ) ); diff --git a/test/Hyperbee.XS.Extensions.Tests/Hyperbee.XS.Extensions.Tests.csproj b/test/Hyperbee.XS.Extensions.Tests/Hyperbee.XS.Extensions.Tests.csproj index e70aa1b..6ceeaab 100644 --- a/test/Hyperbee.XS.Extensions.Tests/Hyperbee.XS.Extensions.Tests.csproj +++ b/test/Hyperbee.XS.Extensions.Tests/Hyperbee.XS.Extensions.Tests.csproj @@ -10,7 +10,8 @@ - + + @@ -19,6 +20,7 @@ + diff --git a/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs b/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs new file mode 100644 index 0000000..7879bd9 --- /dev/null +++ b/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs @@ -0,0 +1,151 @@ +using System.Text.Json; +using Hyperbee.Json.Extensions; +using Hyperbee.Xs.Extensions.Lab; +using static System.Linq.Expressions.Expression; + +namespace Hyperbee.XS.Extensions.Tests; + +[TestClass] +public class JsonParseExtensionTests +{ + public static XsParser Xs { get; set; } = new( GetXsConfig() ); + + private static XsConfig GetXsConfig() + { + var config = TestInitializer.XsConfig; + config.Extensions.Add( new JsonParseExtension() ); + return config; + } + + [TestMethod] + public void Parse_ShouldSucceed_WithJsonString() + { + var expression = Xs.Parse( + """" + var x = json """ + { + "First": "Joe", + "Last": "Jones" + } + """; + + x; + """" ); + + var lambda = Lambda>( expression ); + + var function = lambda.Compile(); + var result = function(); + + Assert.AreEqual( "Jones", result.Select( "$.Last" ).First().GetString() ); + } + + [TestMethod] + public void Parse_ShouldSucceed_WithExpression() + { + var expression = Xs.Parse( + """" + var s = """ + { + "First": "Joe", + "Last": "Jones" + } + """; + + var x = json s; + + x; + """" ); + + var lambda = Lambda>( expression ); + + var function = lambda.Compile(); + var result = function(); + + Assert.AreEqual( "Jones", result.Select( "$.Last" ).First().GetString() ); + } + + [TestMethod] + public void Parse_ShouldSucceed_WithExpressionStream() + { + var expression = Xs.Parse( + """" + using System.IO; + + var stream = new MemoryStream(); + var writer = new StreamWriter( stream ); + writer.Write( + """ + { + "First": "Joe", + "Last": "Jones" + } + """ ); + writer.Flush(); + stream.Position = 0L; + + var x = json stream as Stream; + + x.Last; + """" ); + + var lambda = Lambda>( expression ); + + var function = lambda.Compile(); + var result = function(); + + Assert.AreEqual( "Jones", result ); + } + + [TestMethod] + public void Parse_ShouldSucceed_WithType() + { + var expression = Xs.Parse( + """" + var x = json """ + { + "First": "Joe", + "Last": "Jones" + } + """; + + x.Last; + """" ); + + var lambda = Lambda>( expression ); + + var function = lambda.Compile(); + var result = function(); + + Assert.AreEqual( "Jones", result ); + } + + [TestMethod] + public void Parse_ShouldSucceed_WithJsonPath() + { + var expression = Xs.Parse( + """" + var x = json """ + { + "First": "Joe", + "Last": "Jones" + } + """->'$.Last'; + + x.First().GetString(); + """" ); + + var lambda = Lambda>( expression ); + + var function = lambda.Compile(); + var result = function(); + + Assert.AreEqual( "Jones", result ); + } + +} + +public record Person( string First, string Last ) +{ + public override string ToString() => $"{Last}, {First}"; +} diff --git a/test/Hyperbee.XS.Interactive.Tests/Hyperbee.XS.Interactive.Tests.csproj b/test/Hyperbee.XS.Interactive.Tests/Hyperbee.XS.Interactive.Tests.csproj index 319cd36..572219c 100644 --- a/test/Hyperbee.XS.Interactive.Tests/Hyperbee.XS.Interactive.Tests.csproj +++ b/test/Hyperbee.XS.Interactive.Tests/Hyperbee.XS.Interactive.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj b/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj index d0f2b44..3492f6b 100644 --- a/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj +++ b/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj @@ -11,13 +11,14 @@ - + + From d6ff17e60bbffc8ae6c0f062c42448f94230154e Mon Sep 17 00:00:00 2001 From: Matt Edwards Date: Tue, 22 Apr 2025 15:07:55 -0400 Subject: [PATCH 3/9] Adds fetch and JSON parsing extensions Adds extensions for fetching data from URLs and parsing JSON responses. Introduces a new `FetchParseExtension` to handle HTTP requests and integrates it with the existing XS parser. Includes tests. Adds a refinement to the `JsonParseExtension` to handle HTTP responses and await the result. --- .../FetchParseExtension.cs | 75 +++++++++++++ .../JsonParseExtension.cs | 19 +--- .../FetchParseExtensionTests.cs | 106 ++++++++++++++++++ .../JsonParseExtensionTests.cs | 16 +-- 4 files changed, 191 insertions(+), 25 deletions(-) create mode 100644 src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs create mode 100644 test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs diff --git a/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs new file mode 100644 index 0000000..e07b981 --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs @@ -0,0 +1,75 @@ +using System.Linq.Expressions; +using System.Text.Json; +using Hyperbee.Expressions.Lab; +using Hyperbee.XS.Core; +using Hyperbee.XS.Core.Parsers; +using Hyperbee.XS.Core.Writer; +using Parlot.Fluent; + +using static Parlot.Fluent.Parsers; +using ExpressionExtensions = Hyperbee.Expressions.Lab.ExpressionExtensions; + +namespace Hyperbee.Xs.Extensions.Lab; + +public class FetchParseExtension : IParseExtension, IExpressionWriter, IXsWriter +{ + public ExtensionType Type => ExtensionType.Expression; + public string Key => "fetch"; + + public Parser CreateParser( ExtensionBinder binder ) + { + var (expression, _) = binder; + // var response = fetch("name", "URL" ); + + return If( + ctx => ctx.StartsWith( "(" ), + Between( + Terms.Char( '(' ), + Separated( + Terms.Char( ',' ), + expression + ), + Terms.Char( ')' ) + ) + ) + .Then( static parts => parts.Count switch + { + 4 => ExpressionExtensions.Fetch( clientName: parts[0], url: parts[1], method: parts[2], content: parts[3], headers: parts[4] ), + 3 => ExpressionExtensions.Fetch( clientName: parts[0], url: parts[1], method: parts[2], content: parts[3] ), + _ => ExpressionExtensions.Fetch( clientName: parts[0], url: parts[1] ) + } ) + .Named( "fetch" ); + } + + public bool CanWrite( Expression node ) + { + return node is FetchExpression; + } + + public void WriteExpression( Expression node, ExpressionWriterContext context ) + { + if ( node is not FetchExpression fetchExpression ) + return; + + using var writer = context.EnterExpression( "Hyperbee.Expressions.ExpressionExtensions.Lab.Fetch", true, false ); + + writer.WriteExpression( fetchExpression.ClientName ); + writer.Write( ",\n" ); + writer.WriteExpression( fetchExpression.Url ); + writer.Write( ",\n" ); + writer.Write( fetchExpression.Type, indent: true ); + } + + public void WriteExpression( Expression node, XsWriterContext context ) + { + if ( node is not FetchExpression fetchExpression ) + return; + + using var writer = context.GetWriter(); + + writer.Write( "fetch(" ); + writer.WriteExpression( fetchExpression.ClientName ); + writer.WriteExpression( fetchExpression.Url ); + writer.Write( ")" ); + } +} diff --git a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs index b5ca88e..d3993ba 100644 --- a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs @@ -8,6 +8,7 @@ using Parlot.Fluent; using static System.Linq.Expressions.Expression; +using static Hyperbee.Expressions.ExpressionExtensions; using static Parlot.Fluent.Parsers; using ExpressionExtensions = Hyperbee.Expressions.Lab.ExpressionExtensions; @@ -24,15 +25,9 @@ public Parser CreateParser( ExtensionBinder binder ) // var element = json """{ "first": 1, "second": 2 }""" // var person = json """{ "name": "John", "age": 30 }""" - var stringLiteral = Terms.String( StringLiteralQuotes.Double ) - .Then( static value => Constant( value.ToString() ) ); - var selectLiteral = Terms.String() .Then( static value => Constant( value.ToString() ) ); - var rawStringLiteral = new RawStringParser() - .Then( static value => Constant( value.ToString() ) ); - return ZeroOrOne( Between( @@ -42,22 +37,18 @@ public Parser CreateParser( ExtensionBinder binder ) ) ) .AndSkip( new WhiteSpaceLiteral( true ) ) - .And( - OneOf( - rawStringLiteral, - stringLiteral, - expression - ) - ) + .And( expression ) .Then( static parts => { var (type, value) = parts; + if ( value.Type == typeof(HttpResponseMessage) ) + return Await( ExpressionExtensions.ReadJson( value, type ?? typeof(JsonElement) ) ); return ExpressionExtensions.Json( value, type ); } ) .And( ZeroOrOne( - Terms.Text( "->" ).SkipAnd( selectLiteral ) + Terms.Text( "::" ).SkipAnd( selectLiteral ) ) ).Then( static (ctx,parts) => { diff --git a/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs b/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs new file mode 100644 index 0000000..4db601b --- /dev/null +++ b/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs @@ -0,0 +1,106 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Hyperbee.Json.Extensions; +using Hyperbee.Xs.Extensions.Lab; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using static System.Linq.Expressions.Expression; +using static Hyperbee.Expressions.ExpressionExtensions; + +namespace Hyperbee.XS.Extensions.Tests; + +[TestClass] +public class FetchParseExtensionTests +{ + public static XsParser Xs { get; set; } = new( GetXsConfig() ); + + private static XsConfig GetXsConfig() + { + var config = TestInitializer.XsConfig; + config.Extensions.Add( new FetchParseExtension() ); + config.Extensions.Add( new JsonParseExtension() ); + return config; + } + + [TestMethod] + public async Task Parse_ShouldSucceed_WithFetch() + { + var serviceProvider = GetServiceProvider(); + + var expression = Xs.Parse( + """ + fetch( "Test", "/api" ) + """ ); + + var lambda = Lambda>>( expression ); + + var function = lambda.Compile( serviceProvider, preferInterpretation: false ); + var result = await function(); + + Assert.IsNotNull( result ); + Assert.AreEqual( HttpStatusCode.OK, result.StatusCode ); + } + + [TestMethod] + public async Task Parse_ShouldSucceed_WithFetchAndJsonBody() + { + var serviceProvider = GetServiceProvider(); + + var expression = Xs.Parse( + """ + async { + var response = await fetch( "Test", "/api" ); + json response::'$.mockKey'; + } + """ ); + + var lambda = Lambda>>>( expression ); + + var function = lambda.Compile( serviceProvider, preferInterpretation: false ); + var result = await function(); + + Assert.AreEqual( "mockValue", result.Single().GetString() ); + } + + private static IServiceProvider GetServiceProvider( HttpMessageHandler messageHandler = null ) + { + var host = Host.CreateDefaultBuilder() + .ConfigureServices( ( _, services ) => + { + services.AddSingleton( new JsonSerializerOptions() ); + + // Replace HttpClient with a mock or fake implementation for testing + services.AddHttpClient( "Test", ( client ) => + { + client.BaseAddress = new Uri( "https://example.com" ); + } ) + .ConfigurePrimaryHttpMessageHandler( () => messageHandler ?? new MockHttpMessageHandler() ); + } ) + .Build(); + + return host.Services; + } + + private class MockHttpMessageHandler : HttpMessageHandler + { + private readonly HttpStatusCode _statusCode; + + public MockHttpMessageHandler( HttpStatusCode statusCode = HttpStatusCode.OK ) + { + _statusCode = statusCode; + } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken ) + { + return Task.FromResult( new HttpResponseMessage( _statusCode ) + { + Content = new StringContent( "{\"mockKey\":\"mockValue\"}", Encoding.UTF8, "application/json" ) + } ); + } + } + +} + diff --git a/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs b/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs index 7879bd9..92b915f 100644 --- a/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs +++ b/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs @@ -22,14 +22,12 @@ public void Parse_ShouldSucceed_WithJsonString() { var expression = Xs.Parse( """" - var x = json """ + json """ { "First": "Joe", "Last": "Jones" } """; - - x; """" ); var lambda = Lambda>( expression ); @@ -84,9 +82,7 @@ public void Parse_ShouldSucceed_WithExpressionStream() writer.Flush(); stream.Position = 0L; - var x = json stream as Stream; - - x.Last; + (json stream as Stream).Last; """" ); var lambda = Lambda>( expression ); @@ -102,14 +98,12 @@ public void Parse_ShouldSucceed_WithType() { var expression = Xs.Parse( """" - var x = json """ + ( json """ { "First": "Joe", "Last": "Jones" } - """; - - x.Last; + """ ).Last; """" ); var lambda = Lambda>( expression ); @@ -130,7 +124,7 @@ public void Parse_ShouldSucceed_WithJsonPath() "First": "Joe", "Last": "Jones" } - """->'$.Last'; + """::'$.Last'; x.First().GetString(); """" ); From e720e338276ee26b6c249b1bec2de03a9d5a6551 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 22 Apr 2025 19:08:49 +0000 Subject: [PATCH 4/9] Updated code formatting to match rules in .editorconfig --- src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs index d3993ba..caa3bbf 100644 --- a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs @@ -41,8 +41,8 @@ public Parser CreateParser( ExtensionBinder binder ) .Then( static parts => { var (type, value) = parts; - if ( value.Type == typeof(HttpResponseMessage) ) - return Await( ExpressionExtensions.ReadJson( value, type ?? typeof(JsonElement) ) ); + if ( value.Type == typeof( HttpResponseMessage ) ) + return Await( ExpressionExtensions.ReadJson( value, type ?? typeof( JsonElement ) ) ); return ExpressionExtensions.Json( value, type ); } ) @@ -50,12 +50,12 @@ public Parser CreateParser( ExtensionBinder binder ) ZeroOrOne( Terms.Text( "::" ).SkipAnd( selectLiteral ) ) - ).Then( static (ctx,parts) => + ).Then( static ( ctx, parts ) => { var (json, select) = parts; - return select == null - ? json + return select == null + ? json : ExpressionExtensions.JsonPath( json, select ); } ) From 1a40addca5006fcb296933aebb6186fa3833248e Mon Sep 17 00:00:00 2001 From: Matt Edwards Date: Wed, 23 Apr 2025 13:37:55 -0400 Subject: [PATCH 5/9] Adds regex parsing extension Adds a new extension for parsing and extracting data using regular expressions. It includes a custom expression node for representing regex matches and integrates it into the XS parsing and writing pipeline. Simplifies fetch and json extensions by removing redundant ExpressionExtensions usage. Uses '/' as delimiters for string literals. --- .../FetchParseExtension.cs | 9 ++- .../JsonParseExtension.cs | 14 ++-- .../RegexMatchExpression.cs | 56 +++++++++++++++ .../RegexParseExtension.cs | 70 +++++++++++++++++++ .../FetchParseExtensionTests.cs | 3 +- .../JsonParseExtensionTests.cs | 18 ++++- .../RegexParseExtensionTests.cs | 33 +++++++++ 7 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs create mode 100644 src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs create mode 100644 test/Hyperbee.XS.Extensions.Tests/RegexParseExtensionTests.cs diff --git a/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs index e07b981..5478770 100644 --- a/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs @@ -1,13 +1,12 @@ using System.Linq.Expressions; -using System.Text.Json; using Hyperbee.Expressions.Lab; using Hyperbee.XS.Core; using Hyperbee.XS.Core.Parsers; using Hyperbee.XS.Core.Writer; using Parlot.Fluent; +using static Hyperbee.Expressions.Lab.ExpressionExtensions; using static Parlot.Fluent.Parsers; -using ExpressionExtensions = Hyperbee.Expressions.Lab.ExpressionExtensions; namespace Hyperbee.Xs.Extensions.Lab; @@ -34,9 +33,9 @@ public Parser CreateParser( ExtensionBinder binder ) ) .Then( static parts => parts.Count switch { - 4 => ExpressionExtensions.Fetch( clientName: parts[0], url: parts[1], method: parts[2], content: parts[3], headers: parts[4] ), - 3 => ExpressionExtensions.Fetch( clientName: parts[0], url: parts[1], method: parts[2], content: parts[3] ), - _ => ExpressionExtensions.Fetch( clientName: parts[0], url: parts[1] ) + 4 => Fetch( clientName: parts[0], url: parts[1], method: parts[2], content: parts[3], headers: parts[4] ), + 3 => Fetch( clientName: parts[0], url: parts[1], method: parts[2], content: parts[3] ), + _ => Fetch( clientName: parts[0], url: parts[1] ) } ) .Named( "fetch" ); } diff --git a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs index caa3bbf..f3c13e8 100644 --- a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs @@ -1,6 +1,5 @@ using System.Linq.Expressions; using System.Text.Json; -using Hyperbee.Expressions; using Hyperbee.Expressions.Lab; using Hyperbee.XS.Core; using Hyperbee.XS.Core.Parsers; @@ -8,9 +7,9 @@ using Parlot.Fluent; using static System.Linq.Expressions.Expression; +using static Hyperbee.Expressions.Lab.ExpressionExtensions; using static Hyperbee.Expressions.ExpressionExtensions; using static Parlot.Fluent.Parsers; -using ExpressionExtensions = Hyperbee.Expressions.Lab.ExpressionExtensions; namespace Hyperbee.Xs.Extensions.Lab; @@ -25,7 +24,7 @@ public Parser CreateParser( ExtensionBinder binder ) // var element = json """{ "first": 1, "second": 2 }""" // var person = json """{ "name": "John", "age": 30 }""" - var selectLiteral = Terms.String() + var jsonPathSelect = SkipWhiteSpace( new StringLiteral( '/' ) ) .Then( static value => Constant( value.ToString() ) ); return @@ -42,13 +41,13 @@ public Parser CreateParser( ExtensionBinder binder ) { var (type, value) = parts; if ( value.Type == typeof( HttpResponseMessage ) ) - return Await( ExpressionExtensions.ReadJson( value, type ?? typeof( JsonElement ) ) ); + return Await( ReadJson( value, type ?? typeof( JsonElement ) ) ); - return ExpressionExtensions.Json( value, type ); + return Expressions.Lab.ExpressionExtensions.Json( value, type ); } ) .And( ZeroOrOne( - Terms.Text( "::" ).SkipAnd( selectLiteral ) + Terms.Text( "::" ).SkipAnd( jsonPathSelect ) ) ).Then( static ( ctx, parts ) => { @@ -56,7 +55,7 @@ public Parser CreateParser( ExtensionBinder binder ) return select == null ? json - : ExpressionExtensions.JsonPath( json, select ); + : JsonPath( json, select ); } ) .Named( "json" ); @@ -90,3 +89,4 @@ public void WriteExpression( Expression node, XsWriterContext context ) writer.WriteExpression( jsonExtension.InputExpression ); } } + diff --git a/src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs b/src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs new file mode 100644 index 0000000..573f5d8 --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs @@ -0,0 +1,56 @@ +using System.Linq.Expressions; +using System.Text.RegularExpressions; + +namespace Hyperbee.Xs.Extensions.Lab; + +public class RegexMatchExpression : Expression +{ + public Expression InputExpression { get; } + public Expression Pattern { get; } + + public RegexMatchExpression( Expression inputExpression, Expression pattern ) + { + InputExpression = inputExpression ?? throw new ArgumentNullException( nameof(inputExpression) ); + Pattern = pattern ?? throw new ArgumentNullException( nameof(pattern) ); + } + + public override ExpressionType NodeType => ExpressionType.Extension; + public override Type Type => typeof( MatchCollection ); + public override bool CanReduce => true; + + protected override Expression VisitChildren( ExpressionVisitor visitor ) + { + var visitedInput = visitor.Visit( InputExpression ); + var visitedPattern = visitor.Visit( Pattern ); + + if ( visitedInput != InputExpression || visitedPattern != Pattern ) + { + return new RegexMatchExpression( visitedInput, visitedPattern ); + } + + return this; + } + + public override Expression Reduce() + { + var regexMatchesMethod = typeof(Regex) + .GetMethod(nameof(Regex.Matches), [typeof(string)] )!; + + // Use a constructor expression to create the Regex instance + var regexConstructor = typeof(Regex).GetConstructor( [typeof(string)] )!; + + return Call( + New( regexConstructor, Pattern ), + regexMatchesMethod, + InputExpression + ); + } +} + +public static partial class ExpressionExtensions +{ + public static RegexMatchExpression Regex( Expression inputExpression, Expression pattern ) + { + return new RegexMatchExpression( inputExpression, pattern ); + } +} diff --git a/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs new file mode 100644 index 0000000..d02a007 --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs @@ -0,0 +1,70 @@ +using System.Linq.Expressions; +using Hyperbee.XS.Core; +using Hyperbee.XS.Core.Writer; +using Parlot.Fluent; + +using static System.Linq.Expressions.Expression; +using static Parlot.Fluent.Parsers; + +namespace Hyperbee.Xs.Extensions.Lab; + +public class RegexParseExtension : IParseExtension, IExpressionWriter, IXsWriter +{ + public ExtensionType Type => ExtensionType.Expression; + public string Key => "regex"; + + public Parser CreateParser(ExtensionBinder binder) + { + var (expression, _) = binder; + + // Define the regex select parser + var regexPattern = SkipWhiteSpace(new StringLiteral('/')) + .Then(static value => Constant(value.ToString())); + + return + expression + .And( + Terms.Text("::").SkipAnd(regexPattern) + ).Then(static parts => + { + var (regex, pattern) = parts; + + return new RegexMatchExpression(regex, pattern); + }) + .Named("regex"); + } + + public bool CanWrite(Expression node) + { + return node is RegexMatchExpression; + } + + public void WriteExpression(Expression node, ExpressionWriterContext context) + { + if (node is not RegexMatchExpression regexExpression) + return; + + using var writer = context.EnterExpression( "Hyperbee.Xs.Extensions.Lab.ExpressionExtensions.Regex", true, false); + + writer.WriteExpression(regexExpression.InputExpression); + writer.Write(",\n"); + writer.Write(regexExpression.Pattern, indent: true); + } + + public void WriteExpression(Expression node, XsWriterContext context) + { + if (node is not RegexMatchExpression regexExpression) + return; + + using var writer = context.GetWriter(); + + writer.Write("regex "); + writer.WriteExpression(ExpressionExtensions.Regex(regexExpression.InputExpression, regexExpression.Pattern)); + + if (regexExpression.Pattern != null) + { + writer.Write("::"); + writer.Write(regexExpression.Pattern); + } + } +} diff --git a/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs b/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs index 4db601b..bfac92a 100644 --- a/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs +++ b/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs @@ -1,7 +1,6 @@ using System.Net; using System.Text; using System.Text.Json; -using Hyperbee.Json.Extensions; using Hyperbee.Xs.Extensions.Lab; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -51,7 +50,7 @@ public async Task Parse_ShouldSucceed_WithFetchAndJsonBody() """ async { var response = await fetch( "Test", "/api" ); - json response::'$.mockKey'; + json response::/$.mockKey/; } """ ); diff --git a/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs b/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs index 92b915f..20be409 100644 --- a/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs +++ b/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs @@ -124,7 +124,7 @@ public void Parse_ShouldSucceed_WithJsonPath() "First": "Joe", "Last": "Jones" } - """::'$.Last'; + """::/$.Last/; x.First().GetString(); """" ); @@ -137,6 +137,22 @@ public void Parse_ShouldSucceed_WithJsonPath() Assert.AreEqual( "Jones", result ); } + [TestMethod] + public void Parse_ShouldSucceed_WithRegex() + { + var expression = Xs.Parse( + """ + regex "Find world in string"::/world/[0].Value; + """ ); + + var lambda = Lambda>( expression ); + + var function = lambda.Compile(); + var result = function(); + + Assert.AreEqual( "world", result ); + } + } public record Person( string First, string Last ) diff --git a/test/Hyperbee.XS.Extensions.Tests/RegexParseExtensionTests.cs b/test/Hyperbee.XS.Extensions.Tests/RegexParseExtensionTests.cs new file mode 100644 index 0000000..821b6ee --- /dev/null +++ b/test/Hyperbee.XS.Extensions.Tests/RegexParseExtensionTests.cs @@ -0,0 +1,33 @@ +using Hyperbee.Xs.Extensions.Lab; +using static System.Linq.Expressions.Expression; + +namespace Hyperbee.XS.Extensions.Tests; + +[TestClass] +public class RegexParseExtensionTests +{ + public static XsParser Xs { get; set; } = new( GetXsConfig() ); + + private static XsConfig GetXsConfig() + { + var config = TestInitializer.XsConfig; + config.Extensions.Add( new RegexParseExtension() ); + return config; + } + + [TestMethod] + public void Parse_ShouldSucceed_WithRegex() + { + var expression = Xs.Parse( + """ + regex "Find world in string"::/world/[0].Value; + """ ); + + var lambda = Lambda>( expression ); + + var function = lambda.Compile(); + var result = function(); + + Assert.AreEqual( "world", result ); + } +} From 2510ad3c1290c2d1176a9a1845797763e8d078b6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 23 Apr 2025 17:38:50 +0000 Subject: [PATCH 6/9] Updated code formatting to match rules in .editorconfig --- .../JsonParseExtension.cs | 3 +- .../RegexMatchExpression.cs | 10 ++-- .../RegexParseExtension.cs | 46 +++++++++---------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs index f3c13e8..46f1b75 100644 --- a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs @@ -5,10 +5,9 @@ using Hyperbee.XS.Core.Parsers; using Hyperbee.XS.Core.Writer; using Parlot.Fluent; - using static System.Linq.Expressions.Expression; -using static Hyperbee.Expressions.Lab.ExpressionExtensions; using static Hyperbee.Expressions.ExpressionExtensions; +using static Hyperbee.Expressions.Lab.ExpressionExtensions; using static Parlot.Fluent.Parsers; namespace Hyperbee.Xs.Extensions.Lab; diff --git a/src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs b/src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs index 573f5d8..bf3302e 100644 --- a/src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs +++ b/src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs @@ -10,8 +10,8 @@ public class RegexMatchExpression : Expression public RegexMatchExpression( Expression inputExpression, Expression pattern ) { - InputExpression = inputExpression ?? throw new ArgumentNullException( nameof(inputExpression) ); - Pattern = pattern ?? throw new ArgumentNullException( nameof(pattern) ); + InputExpression = inputExpression ?? throw new ArgumentNullException( nameof( inputExpression ) ); + Pattern = pattern ?? throw new ArgumentNullException( nameof( pattern ) ); } public override ExpressionType NodeType => ExpressionType.Extension; @@ -33,11 +33,11 @@ protected override Expression VisitChildren( ExpressionVisitor visitor ) public override Expression Reduce() { - var regexMatchesMethod = typeof(Regex) - .GetMethod(nameof(Regex.Matches), [typeof(string)] )!; + var regexMatchesMethod = typeof( Regex ) + .GetMethod( nameof( Regex.Matches ), [typeof( string )] )!; // Use a constructor expression to create the Regex instance - var regexConstructor = typeof(Regex).GetConstructor( [typeof(string)] )!; + var regexConstructor = typeof( Regex ).GetConstructor( [typeof( string )] )!; return Call( New( regexConstructor, Pattern ), diff --git a/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs index d02a007..9122aa3 100644 --- a/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs @@ -13,58 +13,58 @@ public class RegexParseExtension : IParseExtension, IExpressionWriter, IXsWriter public ExtensionType Type => ExtensionType.Expression; public string Key => "regex"; - public Parser CreateParser(ExtensionBinder binder) + public Parser CreateParser( ExtensionBinder binder ) { var (expression, _) = binder; // Define the regex select parser - var regexPattern = SkipWhiteSpace(new StringLiteral('/')) - .Then(static value => Constant(value.ToString())); + var regexPattern = SkipWhiteSpace( new StringLiteral( '/' ) ) + .Then( static value => Constant( value.ToString() ) ); return expression - .And( - Terms.Text("::").SkipAnd(regexPattern) - ).Then(static parts => + .And( + Terms.Text( "::" ).SkipAnd( regexPattern ) + ).Then( static parts => { var (regex, pattern) = parts; - return new RegexMatchExpression(regex, pattern); - }) - .Named("regex"); + return new RegexMatchExpression( regex, pattern ); + } ) + .Named( "regex" ); } - public bool CanWrite(Expression node) + public bool CanWrite( Expression node ) { return node is RegexMatchExpression; } - public void WriteExpression(Expression node, ExpressionWriterContext context) + public void WriteExpression( Expression node, ExpressionWriterContext context ) { - if (node is not RegexMatchExpression regexExpression) + if ( node is not RegexMatchExpression regexExpression ) return; - using var writer = context.EnterExpression( "Hyperbee.Xs.Extensions.Lab.ExpressionExtensions.Regex", true, false); + using var writer = context.EnterExpression( "Hyperbee.Xs.Extensions.Lab.ExpressionExtensions.Regex", true, false ); - writer.WriteExpression(regexExpression.InputExpression); - writer.Write(",\n"); - writer.Write(regexExpression.Pattern, indent: true); + writer.WriteExpression( regexExpression.InputExpression ); + writer.Write( ",\n" ); + writer.Write( regexExpression.Pattern, indent: true ); } - public void WriteExpression(Expression node, XsWriterContext context) + public void WriteExpression( Expression node, XsWriterContext context ) { - if (node is not RegexMatchExpression regexExpression) + if ( node is not RegexMatchExpression regexExpression ) return; using var writer = context.GetWriter(); - writer.Write("regex "); - writer.WriteExpression(ExpressionExtensions.Regex(regexExpression.InputExpression, regexExpression.Pattern)); + writer.Write( "regex " ); + writer.WriteExpression( ExpressionExtensions.Regex( regexExpression.InputExpression, regexExpression.Pattern ) ); - if (regexExpression.Pattern != null) + if ( regexExpression.Pattern != null ) { - writer.Write("::"); - writer.Write(regexExpression.Pattern); + writer.Write( "::" ); + writer.Write( regexExpression.Pattern ); } } } From c3f25248c75afc8e8bf85b379bc986406e0aa2a0 Mon Sep 17 00:00:00 2001 From: Matt Edwards Date: Wed, 23 Apr 2025 14:38:57 -0400 Subject: [PATCH 7/9] Enhances expression and XS writing capabilities Improves expression and XS writing by adding support for usings, correctly handling string literals, and enabling JSON parsing. - Adds `Usings` to `ExpressionVisitorConfig` to include namespaces. - Correctly handles string literals in `ExpressionVisitor` and `XsVisitor`. - Introduces `JsonParseExtension` to parse JSON strings into expressions. --- .../JsonParseExtension.cs | 4 +- .../Core/Writer/ExpressionVisitor.cs | 4 +- .../Core/Writer/ExpressionVisitorConfig.cs | 1 + .../Core/Writer/ExpressionWriterContext.cs | 4 ++ src/Hyperbee.XS/Core/Writer/XsVisitor.cs | 4 +- .../ExpressionTreeStringTests.cs | 55 ++++++++++++++++++- .../PackageParseExtensionsTests.cs | 2 +- .../TestInitializer.cs | 3 +- 8 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs index 46f1b75..00b9895 100644 --- a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs @@ -70,11 +70,11 @@ public void WriteExpression( Expression node, ExpressionWriterContext context ) if ( node is not JsonExpression jsonExpression ) return; - using var writer = context.EnterExpression( "Hyperbee.Expressions.ExpressionExtensions.Lab.Json", true, false ); + using var writer = context.EnterExpression( "Hyperbee.Expressions.Lab.ExpressionExtensions.Json", true, false ); writer.WriteExpression( jsonExpression.InputExpression ); writer.Write( ",\n" ); - writer.Write( jsonExpression.Type, indent: true ); + writer.WriteType( jsonExpression.Type ); } public void WriteExpression( Expression node, XsWriterContext context ) diff --git a/src/Hyperbee.XS/Core/Writer/ExpressionVisitor.cs b/src/Hyperbee.XS/Core/Writer/ExpressionVisitor.cs index 8eaa0d4..2a191e3 100644 --- a/src/Hyperbee.XS/Core/Writer/ExpressionVisitor.cs +++ b/src/Hyperbee.XS/Core/Writer/ExpressionVisitor.cs @@ -78,7 +78,9 @@ protected override Expression VisitConstant( ConstantExpression node ) switch ( value ) { case string: - writer.Write( $"\"{value}\"" ); + writer.Write( $"""" + """{value}""" + """" ); break; case bool boolValue: diff --git a/src/Hyperbee.XS/Core/Writer/ExpressionVisitorConfig.cs b/src/Hyperbee.XS/Core/Writer/ExpressionVisitorConfig.cs index ae0c738..28937b9 100644 --- a/src/Hyperbee.XS/Core/Writer/ExpressionVisitorConfig.cs +++ b/src/Hyperbee.XS/Core/Writer/ExpressionVisitorConfig.cs @@ -4,4 +4,5 @@ public record ExpressionVisitorConfig( string Prefix = "Expression.", string Indentation = " ", string Variable = "expression", + string[] Usings = null, params IExpressionWriter[] Writers ); diff --git a/src/Hyperbee.XS/Core/Writer/ExpressionWriterContext.cs b/src/Hyperbee.XS/Core/Writer/ExpressionWriterContext.cs index c474928..9323331 100644 --- a/src/Hyperbee.XS/Core/Writer/ExpressionWriterContext.cs +++ b/src/Hyperbee.XS/Core/Writer/ExpressionWriterContext.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using NuGet.Packaging; namespace Hyperbee.XS.Core.Writer; @@ -31,6 +32,9 @@ public class ExpressionWriterContext internal ExpressionWriterContext( ExpressionVisitorConfig config = null ) { Config = config ?? new(); + if ( config?.Usings != null ) + Usings.AddRange( config.Usings ); + Visitor = new ExpressionVisitor( this ); } diff --git a/src/Hyperbee.XS/Core/Writer/XsVisitor.cs b/src/Hyperbee.XS/Core/Writer/XsVisitor.cs index 02f5824..876e93b 100644 --- a/src/Hyperbee.XS/Core/Writer/XsVisitor.cs +++ b/src/Hyperbee.XS/Core/Writer/XsVisitor.cs @@ -195,7 +195,9 @@ protected override Expression VisitConstant( ConstantExpression node ) break; case string: - writer.Write( $"\"{value}\"" ); + writer.Write( $"""" + """{value}""" + """" ); break; case bool boolValue: diff --git a/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs b/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs index 70cc8b1..a5fff63 100644 --- a/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs +++ b/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs @@ -1,7 +1,9 @@ using System.Linq.Expressions; +using System.Text.Json; using Hyperbee.Expressions; using Hyperbee.Xs.Extensions; using Hyperbee.XS.Core.Writer; +using Hyperbee.Xs.Extensions.Lab; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; @@ -12,11 +14,15 @@ public class ExpressionTreeStringTests { public static XsParser Xs { get; set; } = new( TestInitializer.XsConfig ); - public ExpressionVisitorConfig Config = new( "Expression.", "\t", "expression", - [.. XsExtensions.Extensions().OfType()] ); + public ExpressionVisitorConfig Config = new( + "Expression.", + "\t", + "expression", + ["Hyperbee.Expressions.Lab"], + [.. XsExtensions.Extensions().OfType(), new FetchParseExtension(), new JsonParseExtension(), new RegexParseExtension()] ); public XsVisitorConfig XsConfig = new( "\t", - [.. XsExtensions.Extensions().OfType()] ); + [.. XsExtensions.Extensions().OfType(), new FetchParseExtension(), new JsonParseExtension(), new RegexParseExtension()] ); [TestMethod] public async Task ToExpressionTreeString_ShouldCreate_ForLoop() @@ -230,6 +236,26 @@ public async Task ToExpressionTreeString_ShouldCreate_Config() await AssertScriptValueService( code, result ); } + [TestMethod] + public async Task ToExpressionTreeString_ShouldCreate_Json() + { + var script = """" + var person = json """{ "name": "John", "age": 30 }"""; + person.GetProperty( "name" ).GetString(); + """"; + + var expression = Xs.Parse( script ); + var code = expression.ToExpressionString( Config ); + + WriteResult( script, code ); + + var lambda = Expression.Lambda>( expression ); + var compiled = lambda.Compile(); + var result = compiled(); + + await AssertScriptValue( code, result ); + } + [TestMethod] public async Task ToXsString_ShouldCreate_AsyncAwait() { @@ -488,6 +514,28 @@ public async Task ToXsString_ShouldCreate_Config() await AssertScriptValueService( code, result ); } + [TestMethod] + public async Task ToXsString_ShouldCreate_Json() + { + var script = """" + var person = json """{ "name": "John", "age": 30 }"""; + person.GetProperty( "name" ).GetString(); + """"; + + var expression = Xs.Parse( script ); + var newScript = expression.ToXS( XsConfig ); + + WriteResult( script, newScript ); + + var newExpression = Xs.Parse( newScript ); + var lambda = Expression.Lambda>( newExpression ); + var compiled = lambda.Compile(); + var result = compiled(); + + var code = expression.ToExpressionString( Config ); + await AssertScriptValue( code, result ); + } + public static async Task AssertScriptValue( string code, T result ) { var scriptOptions = ScriptOptions.Default.WithReferences( @@ -593,4 +641,5 @@ private static void WriteResult( string script, string code ) Console.WriteLine( code ); #endif } + } diff --git a/test/Hyperbee.XS.Extensions.Tests/PackageParseExtensionsTests.cs b/test/Hyperbee.XS.Extensions.Tests/PackageParseExtensionsTests.cs index e1e78fc..b1ef1e7 100644 --- a/test/Hyperbee.XS.Extensions.Tests/PackageParseExtensionsTests.cs +++ b/test/Hyperbee.XS.Extensions.Tests/PackageParseExtensionsTests.cs @@ -7,7 +7,7 @@ namespace Hyperbee.XS.Extensions.Tests; [TestClass] public class PackageParseExtensionTests { - public ExpressionVisitorConfig Config = new( "Expression.", "\t", "expression", + public ExpressionVisitorConfig Config = new( "Expression.", "\t", "expression", null, XsExtensions.Extensions().OfType().ToArray() ); public XsVisitorConfig XsConfig = new( "\t", diff --git a/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs b/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs index 3649103..afb6bb5 100644 --- a/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs +++ b/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs @@ -1,6 +1,7 @@ using System.Reflection; using Hyperbee.Xs.Extensions; using Hyperbee.XS.Core; +using Hyperbee.Xs.Extensions.Lab; namespace Hyperbee.XS.Extensions.Tests; @@ -16,7 +17,7 @@ public static void Initialize( TestContext _ ) XsConfig = new XsConfig( typeResolver ) { - Extensions = [.. XsExtensions.Extensions()] + Extensions = [.. XsExtensions.Extensions(), new FetchParseExtension(), new JsonParseExtension(), new RegexParseExtension()] }; } } From f4c8625ba781cc112e93a3c9d45a5931ac82d1d3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 23 Apr 2025 18:39:54 +0000 Subject: [PATCH 8/9] Updated code formatting to match rules in .editorconfig --- .../ExpressionTreeStringTests.cs | 8 ++++---- test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs b/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs index a5fff63..35e8b61 100644 --- a/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs +++ b/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs @@ -2,8 +2,8 @@ using System.Text.Json; using Hyperbee.Expressions; using Hyperbee.Xs.Extensions; -using Hyperbee.XS.Core.Writer; using Hyperbee.Xs.Extensions.Lab; +using Hyperbee.XS.Core.Writer; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; @@ -14,9 +14,9 @@ public class ExpressionTreeStringTests { public static XsParser Xs { get; set; } = new( TestInitializer.XsConfig ); - public ExpressionVisitorConfig Config = new( - "Expression.", - "\t", + public ExpressionVisitorConfig Config = new( + "Expression.", + "\t", "expression", ["Hyperbee.Expressions.Lab"], [.. XsExtensions.Extensions().OfType(), new FetchParseExtension(), new JsonParseExtension(), new RegexParseExtension()] ); diff --git a/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs b/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs index afb6bb5..e5a4419 100644 --- a/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs +++ b/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs @@ -1,7 +1,7 @@ using System.Reflection; using Hyperbee.Xs.Extensions; -using Hyperbee.XS.Core; using Hyperbee.Xs.Extensions.Lab; +using Hyperbee.XS.Core; namespace Hyperbee.XS.Extensions.Tests; From 33624738b0af2ed900392131f08dfa1429f4e763 Mon Sep 17 00:00:00 2001 From: Matt Edwards Date: Mon, 12 May 2025 16:07:04 -0400 Subject: [PATCH 9/9] Updates dependencies and fixes parser Updates dependency versions, including FastExpressionCompiler, Hyperbee.Expressions, and Parlot. Fixes an issue in the RawStringParser to correctly handle raw string content parsing, preventing potential errors or unexpected behavior. Simplifies expression parser usage in extensions. --- src/Hyperbee.XS.Cli/Hyperbee.Xs.Cli.csproj | 2 +- .../FetchParseExtension.cs | 2 +- .../Hyperbee.Xs.Extensions.Lab.csproj | 6 ++--- .../JsonParseExtension.cs | 2 +- .../RegexParseExtension.cs | 7 +++--- .../Core/Parsers/RawStringParser.cs | 1 + src/Hyperbee.XS/Core/Writer/XsVisitor.cs | 2 +- .../Hyperbee.XS.Benchmark.csproj | 2 +- .../Hyperbee.XS.Tests.csproj | 2 +- test/Hyperbee.XS.Tests/TestInitializer.cs | 1 + .../XsParserTests.RawString.cs | 22 +++++++++++++++++-- 11 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/Hyperbee.XS.Cli/Hyperbee.Xs.Cli.csproj b/src/Hyperbee.XS.Cli/Hyperbee.Xs.Cli.csproj index be54443..fdfdb68 100644 --- a/src/Hyperbee.XS.Cli/Hyperbee.Xs.Cli.csproj +++ b/src/Hyperbee.XS.Cli/Hyperbee.Xs.Cli.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs index 5478770..2d98337 100644 --- a/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs @@ -17,7 +17,7 @@ public class FetchParseExtension : IParseExtension, IExpressionWriter, IXsWriter public Parser CreateParser( ExtensionBinder binder ) { - var (expression, _) = binder; + Parser expression = binder.ExpressionParser; // var response = fetch("name", "URL" ); return If( diff --git a/src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj b/src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj index fb56860..3c0a948 100644 --- a/src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj +++ b/src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj @@ -42,10 +42,10 @@ - - + + - + all diff --git a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs index 00b9895..386d65e 100644 --- a/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs @@ -19,7 +19,7 @@ public class JsonParseExtension : IParseExtension, IExpressionWriter, IXsWriter public Parser CreateParser( ExtensionBinder binder ) { - var (expression, _) = binder; + var expression = binder.ExpressionParser; // var element = json """{ "first": 1, "second": 2 }""" // var person = json """{ "name": "John", "age": 30 }""" diff --git a/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs b/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs index 9122aa3..9b3e7fb 100644 --- a/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs +++ b/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs @@ -15,7 +15,7 @@ public class RegexParseExtension : IParseExtension, IExpressionWriter, IXsWriter public Parser CreateParser( ExtensionBinder binder ) { - var (expression, _) = binder; + var expression = binder.ExpressionParser; // Define the regex select parser var regexPattern = SkipWhiteSpace( new StringLiteral( '/' ) ) @@ -23,9 +23,8 @@ public Parser CreateParser( ExtensionBinder binder ) return expression - .And( - Terms.Text( "::" ).SkipAnd( regexPattern ) - ).Then( static parts => + .And( Terms.Text( "::" ).SkipAnd( regexPattern ) ) + .Then( static parts => { var (regex, pattern) = parts; diff --git a/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs b/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs index 4b153c4..bb007f1 100644 --- a/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs +++ b/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs @@ -44,6 +44,7 @@ public override bool Parse( ParseContext context, ref ParseResult resu state = ParserState.BeginContent; requiredQuoteCount = quoteCount; begin = scanner.Cursor.Position; + continue; } else { diff --git a/src/Hyperbee.XS/Core/Writer/XsVisitor.cs b/src/Hyperbee.XS/Core/Writer/XsVisitor.cs index 876e93b..15a932f 100644 --- a/src/Hyperbee.XS/Core/Writer/XsVisitor.cs +++ b/src/Hyperbee.XS/Core/Writer/XsVisitor.cs @@ -196,7 +196,7 @@ protected override Expression VisitConstant( ConstantExpression node ) case string: writer.Write( $"""" - """{value}""" + """{value}""" """" ); break; diff --git a/test/Hyperbee.XS.Benchmark/Hyperbee.XS.Benchmark.csproj b/test/Hyperbee.XS.Benchmark/Hyperbee.XS.Benchmark.csproj index b5649d2..1d4ac54 100644 --- a/test/Hyperbee.XS.Benchmark/Hyperbee.XS.Benchmark.csproj +++ b/test/Hyperbee.XS.Benchmark/Hyperbee.XS.Benchmark.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj b/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj index 624bdc0..f47cfb4 100644 --- a/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj +++ b/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/Hyperbee.XS.Tests/TestInitializer.cs b/test/Hyperbee.XS.Tests/TestInitializer.cs index 85b2c0a..92dba3c 100644 --- a/test/Hyperbee.XS.Tests/TestInitializer.cs +++ b/test/Hyperbee.XS.Tests/TestInitializer.cs @@ -2,6 +2,7 @@ using System.Reflection; using FastExpressionCompiler; using Hyperbee.XS.Core; +using TestContext = Microsoft.VisualStudio.TestTools.UnitTesting.TestContext; namespace Hyperbee.XS.Tests; diff --git a/test/Hyperbee.XS.Tests/XsParserTests.RawString.cs b/test/Hyperbee.XS.Tests/XsParserTests.RawString.cs index 90a8a07..7aba29c 100644 --- a/test/Hyperbee.XS.Tests/XsParserTests.RawString.cs +++ b/test/Hyperbee.XS.Tests/XsParserTests.RawString.cs @@ -1,5 +1,4 @@ -using Hyperbee.XS.Core.Parsers; -using static System.Linq.Expressions.Expression; +using static System.Linq.Expressions.Expression; namespace Hyperbee.XS.Tests; @@ -28,6 +27,25 @@ public void Parse_ShouldSucceed_WithRawStringLiteral( CompilerType compiler ) Assert.AreEqual( "Raw string with \"With Quotes\".", result ); } + [DataTestMethod] + [DataRow( CompilerType.Fast )] + [DataRow( CompilerType.System )] + [DataRow( CompilerType.Interpret )] + public void Parse_ShouldSucceed_WithSingleRawString( CompilerType compiler ) + { + var expression = Xs.Parse( + """" + var x = """!"""; + x; + """" ); + + var lambda = Lambda>( expression ); + + var function = lambda.Compile( compiler ); + var result = function(); + + Assert.AreEqual( "!", result ); + } [DataTestMethod] [DataRow( CompilerType.Fast )]