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.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 new file mode 100644 index 0000000..2d98337 --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/FetchParseExtension.cs @@ -0,0 +1,74 @@ +using System.Linq.Expressions; +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; + +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 ) + { + Parser expression = binder.ExpressionParser; + // 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 => 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" ); + } + + 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/Hyperbee.Xs.Extensions.Lab.csproj b/src/Hyperbee.XS.Extensions.Lab/Hyperbee.Xs.Extensions.Lab.csproj new file mode 100644 index 0000000..3c0a948 --- /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..386d65e --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/JsonParseExtension.cs @@ -0,0 +1,91 @@ +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 System.Linq.Expressions.Expression; +using static Hyperbee.Expressions.ExpressionExtensions; +using static Hyperbee.Expressions.Lab.ExpressionExtensions; +using static Parlot.Fluent.Parsers; + +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.ExpressionParser; + // var element = json """{ "first": 1, "second": 2 }""" + // var person = json """{ "name": "John", "age": 30 }""" + + var jsonPathSelect = SkipWhiteSpace( new StringLiteral( '/' ) ) + .Then( static value => Constant( value.ToString() ) ); + + return + ZeroOrOne( + Between( + Terms.Char( '<' ), + XsParsers.TypeRuntime(), + Terms.Char( '>' ) + ) + ) + .AndSkip( new WhiteSpaceLiteral( true ) ) + .And( expression ) + .Then( static parts => + { + var (type, value) = parts; + if ( value.Type == typeof( HttpResponseMessage ) ) + return Await( ReadJson( value, type ?? typeof( JsonElement ) ) ); + + return Expressions.Lab.ExpressionExtensions.Json( value, type ); + } ) + .And( + ZeroOrOne( + Terms.Text( "::" ).SkipAnd( jsonPathSelect ) + ) + ).Then( static ( ctx, parts ) => + { + var (json, select) = parts; + + return select == null + ? json + : 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.Lab.ExpressionExtensions.Json", true, false ); + + writer.WriteExpression( jsonExpression.InputExpression ); + writer.Write( ",\n" ); + writer.WriteType( jsonExpression.Type ); + } + + 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.Lab/RegexMatchExpression.cs b/src/Hyperbee.XS.Extensions.Lab/RegexMatchExpression.cs new file mode 100644 index 0000000..bf3302e --- /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..9b3e7fb --- /dev/null +++ b/src/Hyperbee.XS.Extensions.Lab/RegexParseExtension.cs @@ -0,0 +1,69 @@ +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.ExpressionParser; + + // 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/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs b/src/Hyperbee.XS/Core/Parsers/RawStringParser.cs index 74ad363..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 { @@ -82,7 +83,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/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..15a932f 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/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.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.Extensions.Tests/ExpressionTreeStringTests.cs b/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs index b516200..a97e0f9 100644 --- a/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs +++ b/test/Hyperbee.XS.Extensions.Tests/ExpressionTreeStringTests.cs @@ -1,6 +1,8 @@ using System.Linq.Expressions; +using System.Text.Json; using Hyperbee.Expressions; using Hyperbee.Xs.Extensions; +using Hyperbee.Xs.Extensions.Lab; using Hyperbee.XS.Core.Writer; 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/FetchParseExtensionTests.cs b/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs new file mode 100644 index 0000000..bfac92a --- /dev/null +++ b/test/Hyperbee.XS.Extensions.Tests/FetchParseExtensionTests.cs @@ -0,0 +1,105 @@ +using System.Net; +using System.Text; +using System.Text.Json; +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/Hyperbee.XS.Extensions.Tests.csproj b/test/Hyperbee.XS.Extensions.Tests/Hyperbee.XS.Extensions.Tests.csproj index 91a7058..505c198 100644 --- a/test/Hyperbee.XS.Extensions.Tests/Hyperbee.XS.Extensions.Tests.csproj +++ b/test/Hyperbee.XS.Extensions.Tests/Hyperbee.XS.Extensions.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0 @@ -11,6 +11,7 @@ + @@ -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..20be409 --- /dev/null +++ b/test/Hyperbee.XS.Extensions.Tests/JsonParseExtensionTests.cs @@ -0,0 +1,161 @@ +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( + """" + json """ + { + "First": "Joe", + "Last": "Jones" + } + """; + """" ); + + 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; + + (json stream as Stream).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( + """" + ( json """ + { + "First": "Joe", + "Last": "Jones" + } + """ ).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 ); + } + + [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 ) +{ + public override string ToString() => $"{Last}, {First}"; +} 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/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 ); + } +} diff --git a/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs b/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs index 3649103..e5a4419 100644 --- a/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs +++ b/test/Hyperbee.XS.Extensions.Tests/TestInitializer.cs @@ -1,5 +1,6 @@ using System.Reflection; using Hyperbee.Xs.Extensions; +using Hyperbee.Xs.Extensions.Lab; using Hyperbee.XS.Core; 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()] }; } } diff --git a/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj b/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj index bcaebbe..f47cfb4 100644 --- a/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj +++ b/test/Hyperbee.XS.Tests/Hyperbee.XS.Tests.csproj @@ -10,7 +10,7 @@ - + @@ -18,6 +18,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 )]