diff --git a/GenHTTP.slnx b/GenHTTP.slnx index 97c04037a..cc91cde7a 100644 --- a/GenHTTP.slnx +++ b/GenHTTP.slnx @@ -11,6 +11,8 @@ + + diff --git a/Modules/CodeGen/GenHTTP.Modules.CodeGen.csproj b/Modules/CodeGen/GenHTTP.Modules.CodeGen.csproj new file mode 100644 index 000000000..1d4c13c87 --- /dev/null +++ b/Modules/CodeGen/GenHTTP.Modules.CodeGen.csproj @@ -0,0 +1,16 @@ + + + + + netstandard2.0 + netstandard2.0 + + false + + + + + + + + diff --git a/Modules/Functional.CodeGen/EntryPoint.cs b/Modules/Functional.CodeGen/EntryPoint.cs new file mode 100644 index 000000000..986d245de --- /dev/null +++ b/Modules/Functional.CodeGen/EntryPoint.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using GenHTTP.Modules.Functional.CodeGen.Model; + +namespace GenHTTP.Modules.Functional.CodeGen; + +public static class EntryPoint +{ + + public static string Emit(GeneratedSource source) + { + using var template = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("GenHTTP.Modules.Functional.CodeGen.Templates.EntryPoint.scriban"); + + if (template == null) + { + throw new InvalidOperationException("Entry point template not found in assembly."); + } + + using var reader = new StreamReader(template); + + var parsed = Scriban.Template.Parse(reader.ReadToEnd()); + + if (parsed == null) + { + throw new InvalidOperationException("Failed to parse entry point template."); + } + + if (parsed.HasErrors) + { + throw new InvalidOperationException($"Failed to parse entry point template: {parsed.Messages}"); + } + + return parsed.Render(source); + } + +} diff --git a/Modules/Functional.CodeGen/GenHTTP.Modules.Functional.CodeGen.csproj b/Modules/Functional.CodeGen/GenHTTP.Modules.Functional.CodeGen.csproj new file mode 100644 index 000000000..2ae8cde73 --- /dev/null +++ b/Modules/Functional.CodeGen/GenHTTP.Modules.Functional.CodeGen.csproj @@ -0,0 +1,33 @@ + + + + + netstandard2.0 + netstandard2.0 + + false + false + Analyzer + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/Functional.CodeGen/InlineGenerator.cs b/Modules/Functional.CodeGen/InlineGenerator.cs new file mode 100644 index 000000000..8a12b6d07 --- /dev/null +++ b/Modules/Functional.CodeGen/InlineGenerator.cs @@ -0,0 +1,84 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Text; +using GenHTTP.Modules.Functional.CodeGen.Model; + +namespace GenHTTP.Modules.Functional.CodeGen; + +[Generator] +public class InlineHandlerGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // 1. Compilation provider + var compilationProvider = context.CompilationProvider; + + // 2. Syntax provider: suche nur InvocationExpressions + var invocationProvider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (node, _) => node is InvocationExpressionSyntax, + transform: static (ctx, _) => TransformIfBuildAs(ctx) + ) + .Where(m => m != null); + + // 3. Combine Compilation + Matches + var combined = compilationProvider.Combine(invocationProvider.Collect()); + + // 4. SourceOutput + context.RegisterSourceOutput(combined, (spc, pair) => + { + var (compilation, matches) = pair; + if (matches.IsDefaultOrEmpty) return; + + var handlers = new List(); + + foreach (var match in matches.OfType()) + { + var identifier = match.Identifier; + if (!string.IsNullOrEmpty(identifier)) + { + var typeName = $"GenHTTP_Inline_{Guid.NewGuid():N}"; + handlers.Add(new GeneratedHandler(typeName, identifier)); + } + } + + if (handlers.Count == 0) return; + + var source = new GeneratedSource("GenHTTP.Modules.Functional.CodeGen", "10.3.0", handlers); + var code = EntryPoint.Emit(source); + spc.AddSource("GenHTTP.InlineBuilder.Generated.g.cs", SourceText.From(code, Encoding.UTF8)); + }); + } + + private static InlineBuildAsMatch? TransformIfBuildAs(GeneratorSyntaxContext ctx) + { + if (ctx.Node is not InvocationExpressionSyntax invocation) return null; + + if (invocation.Expression is not MemberAccessExpressionSyntax ma) return null; + if (ma.Name.Identifier.Text != "BuildAs") return null; + + var args = invocation.ArgumentList.Arguments; + if (args.Count != 1) return null; + + if (args[0].Expression is not LiteralExpressionSyntax lit) return null; + if (!lit.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.StringLiteralExpression)) return null; + + var identifier = lit.Token.ValueText; + + return new InlineBuildAsMatch(invocation, identifier); + } +} + +// Modellklasse +public class InlineBuildAsMatch +{ + public InvocationExpressionSyntax Invocation; + public string Identifier; + + public InlineBuildAsMatch(InvocationExpressionSyntax invocation, string identifier) + { + Invocation = invocation; + Identifier = identifier; + } +} diff --git a/Modules/Functional.CodeGen/Model/GeneratedHandler.cs b/Modules/Functional.CodeGen/Model/GeneratedHandler.cs new file mode 100644 index 000000000..c38ba99e0 --- /dev/null +++ b/Modules/Functional.CodeGen/Model/GeneratedHandler.cs @@ -0,0 +1,16 @@ +namespace GenHTTP.Modules.Functional.CodeGen.Model; + +public class GeneratedHandler +{ + + public string TypeName { get; } + + public string Identifier { get; } + + public GeneratedHandler(string typeName, string identifier) + { + TypeName = typeName; + Identifier = identifier; + } + +} diff --git a/Modules/Functional.CodeGen/Model/GeneratedSource.cs b/Modules/Functional.CodeGen/Model/GeneratedSource.cs new file mode 100644 index 000000000..312af7a6d --- /dev/null +++ b/Modules/Functional.CodeGen/Model/GeneratedSource.cs @@ -0,0 +1,19 @@ +namespace GenHTTP.Modules.Functional.CodeGen.Model; + +public class GeneratedSource +{ + + public string Generator { get; } + + public string GeneratorVersion { get; } + + public List Handlers { get; } + + public GeneratedSource(string generator, string generatorVersion, List handlers) + { + Generator = generator; + GeneratorVersion = generatorVersion; + Handlers = handlers; + } + +} diff --git a/Modules/Functional.CodeGen/Models.cs b/Modules/Functional.CodeGen/Models.cs new file mode 100644 index 000000000..b45c0ad48 --- /dev/null +++ b/Modules/Functional.CodeGen/Models.cs @@ -0,0 +1,69 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace GenHTTP.Modules.Functional.CodeGen; + +// Equivalent to InlineFactoryMatch record +internal sealed class InlineFactoryMatch +{ + public InvocationExpressionSyntax FactoryCall { get; } + + public ITypeSymbol BuilderType { get; } + + public InlineFactoryMatch(InvocationExpressionSyntax factoryCall, ITypeSymbol builderType) + { + FactoryCall = factoryCall; + BuilderType = builderType; + } +} + +// Equivalent to InlineRoute record +internal sealed class InlineRoute +{ + public string Verb { get; } + + public string? Path { get; } + + public ExpressionSyntax DelegateExpression { get; } + + public bool IsSafe { get; } + + public string DelegateSource { get; } + + public InlineRoute(string verb, string? path, ExpressionSyntax delegateExpression, bool isSafe, string delegateSource) + { + Verb = verb; + Path = path; + DelegateExpression = delegateExpression; + IsSafe = isSafe; + DelegateSource = delegateSource; + } +} + +// Equivalent to InlineBuilderGroup record +internal sealed class InlineBuilderGroup +{ + public string Key { get; } + + public InvocationExpressionSyntax FactoryCall { get; } + + public ITypeSymbol BuilderType { get; } + + public List Routes { get; } + + // Primary constructor + public InlineBuilderGroup(string key, InvocationExpressionSyntax factoryCall, ITypeSymbol builderType, List routes) + { + Key = key; + FactoryCall = factoryCall; + BuilderType = builderType; + Routes = routes; + } + + // Auxiliary constructor to mimic original record constructor + public InlineBuilderGroup(string key, InvocationExpressionSyntax factoryCall, ITypeSymbol builderType, List routes, bool dummy) + : this(key, factoryCall, builderType, routes) + { + } + +} diff --git a/Modules/Functional.CodeGen/Templates/EntryPoint.scriban b/Modules/Functional.CodeGen/Templates/EntryPoint.scriban new file mode 100644 index 000000000..804e0ebf9 --- /dev/null +++ b/Modules/Functional.CodeGen/Templates/EntryPoint.scriban @@ -0,0 +1,34 @@ +// + +#nullable enable + +using System.CodeDom.Compiler; +using System.Runtime.CompilerServices; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Functional.CodeGen; +using GenHTTP.Modules.Reflection; + +namespace GenHTTP.Modules.Functional; + +{{ for handler in handlers }} +[GeneratedCode("{{ generator }}", "{{ generator_version }}")] +public sealed class {{ handler.type_name }}(MethodRegistry registry) : IHandler +{ + [ModuleInitializer] + public static void Register() + { + HandlerRegistry.Add("{{ handler.identifier }}", (registry) => new {{ handler.type_name }}(registry)); + } + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public ValueTask HandleAsync(IRequest request) + { + var x = registry.Formatting; + return default; + } +} +{{ end }} \ No newline at end of file diff --git a/Modules/Functional/CodeGen/HandlerRegistry.cs b/Modules/Functional/CodeGen/HandlerRegistry.cs new file mode 100644 index 000000000..a854797d9 --- /dev/null +++ b/Modules/Functional/CodeGen/HandlerRegistry.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using GenHTTP.Api.Content; + +using GenHTTP.Modules.Reflection; + +namespace GenHTTP.Modules.Functional.CodeGen; + +public static class HandlerRegistry +{ + private static readonly Dictionary> Factories = []; + + public static void Add(string identifier, Func factory) + { + Factories.Add(identifier, factory); + } + + public static bool TryGet(string identifier, MethodRegistry registry, [MaybeNullWhen(returnValue: false)] out IHandler handler) + { + if (Factories.TryGetValue(identifier, out var factory)) + { + handler = factory(registry); + return true; + } + + handler = null; + return false; + } + +} diff --git a/Modules/Functional/GenHTTP.Modules.Functional.csproj b/Modules/Functional/GenHTTP.Modules.Functional.csproj index 1c5b471d8..786fc1faf 100644 --- a/Modules/Functional/GenHTTP.Modules.Functional.csproj +++ b/Modules/Functional/GenHTTP.Modules.Functional.csproj @@ -7,7 +7,7 @@ README.md - + @@ -15,7 +15,7 @@ - + diff --git a/Modules/Functional/Inline.cs b/Modules/Functional/Inline.cs index 3b52052c2..900a41c19 100644 --- a/Modules/Functional/Inline.cs +++ b/Modules/Functional/Inline.cs @@ -1,4 +1,5 @@ -using GenHTTP.Modules.Functional.Provider; +using System.Runtime.CompilerServices; +using GenHTTP.Modules.Functional.Provider; namespace GenHTTP.Modules.Functional; @@ -10,4 +11,5 @@ public static class Inline /// which are executed to respond to incoming requests. /// public static InlineBuilder Create() => new(); + } diff --git a/Modules/Functional/Provider/InlineBuilder.cs b/Modules/Functional/Provider/InlineBuilder.cs index b2cf9d92b..e14361974 100644 --- a/Modules/Functional/Provider/InlineBuilder.cs +++ b/Modules/Functional/Provider/InlineBuilder.cs @@ -5,12 +5,13 @@ using GenHTTP.Modules.Conversion; using GenHTTP.Modules.Conversion.Formatters; using GenHTTP.Modules.Conversion.Serializers; +using GenHTTP.Modules.Functional.CodeGen; using GenHTTP.Modules.Reflection; using GenHTTP.Modules.Reflection.Injectors; namespace GenHTTP.Modules.Functional.Provider; -public class InlineBuilder : IHandlerBuilder, IRegistryBuilder +public class InlineBuilder: IHandlerBuilder, IRegistryBuilder { private static readonly HashSet AllMethods = [..Enum.GetValues().Select(FlexibleRequestMethod.Get)]; @@ -24,6 +25,8 @@ public class InlineBuilder : IHandlerBuilder, IRegistryBuilder? _serializers; + private string? _identifier; + #region Functionality /// @@ -158,6 +161,12 @@ public InlineBuilder On(Delegate function, HashSet? metho return this; } + public InlineBuilder Identifier(string identifier) + { + _identifier = identifier; + return this; + } + public InlineBuilder Add(IConcernBuilder concern) { _concerns.Add(concern); @@ -174,9 +183,23 @@ public IHandler Build() var extensions = new MethodRegistry(serializers, injectors, formatters); + if (_identifier != null) + { + if (HandlerRegistry.TryGet(_identifier, extensions, out var handler)) + { + return Concerns.Chain(_concerns, handler); + } + } + return Concerns.Chain(_concerns, new InlineHandler(_functions, extensions)); } + public IHandler BuildAs(string identifier) + { + Identifier(identifier); + return Build(); + } + #endregion } diff --git a/Playground/GenHTTP.Playground.csproj b/Playground/GenHTTP.Playground.csproj index 61342ed8b..6cee440a0 100644 --- a/Playground/GenHTTP.Playground.csproj +++ b/Playground/GenHTTP.Playground.csproj @@ -7,6 +7,9 @@ false + true + $(BaseIntermediateOutputPath)\Generated + @@ -47,6 +50,13 @@ + + + + diff --git a/Playground/Generated.cs b/Playground/Generated.cs new file mode 100644 index 000000000..5d724c490 --- /dev/null +++ b/Playground/Generated.cs @@ -0,0 +1,30 @@ +using System.CodeDom.Compiler; +using System.Runtime.CompilerServices; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Functional.CodeGen; +using GenHTTP.Modules.Reflection; + +namespace GenHTTP.Modules.Functional; + +[GeneratedCode("GenHTTP.Modules.Functional.CodeGen", "10.3.0")] +public sealed class MyHandler1(MethodRegistry registry) : IHandler +{ + + [ModuleInitializer] + public static void Register() + { + HandlerRegistry.Add("...", (registry) => new MyHandler1(registry)); + } + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public ValueTask HandleAsync(IRequest request) + { + var x = registry.Formatting; + return default; + } + +} diff --git a/Playground/Program.cs b/Playground/Program.cs index 7fe31c4f0..85d171da6 100644 --- a/Playground/Program.cs +++ b/Playground/Program.cs @@ -1,9 +1,10 @@ using GenHTTP.Engine.Internal; - -using GenHTTP.Modules.IO; +using GenHTTP.Modules.Functional; using GenHTTP.Modules.Practices; -var content = Content.From(Resource.FromString("Hello World!")); +var content = Inline.Create() + .Get(() => "Hello World!") + .BuildAs("identifier"); await Host.Create() .Handler(content)