Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
JasperFx.Events, JasperFx.Events.SourceGenerator) set
<Version>$(JasperFxVersion)</Version> so they always release together. Bump this one
value to release the whole set. -->
<JasperFxVersion>2.4.0</JasperFxVersion>
<JasperFxVersion>2.4.1</JasperFxVersion>
<LangVersion>13</LangVersion>
<NoWarn>1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618</NoWarn>
<Authors>Jeremy D. Miller;Jaedyn Tonee</Authors>
Expand Down
4 changes: 4 additions & 0 deletions build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ partial class Build : NukeBuild
DotNet("run --framework net9.0 -- describe --environment Testing --applicationName Different --contentRoot /bin", Solution.TestHarnesses.CommandLineRunner.Directory);
DotNet("run --framework net9.0 -- describe --environment=Testing --applicationName=Different --contentRoot=/bin", Solution.TestHarnesses.CommandLineRunner.Directory);
DotNet("run --framework net9.0 -- codegen preview --start", Solution.TestHarnesses.GeneratorTarget.Directory);

// Validate the `--language fsharp` codegen flag is wired through the CLI and emits F#.
// (Compilable/runnable pre-generated F# is proven downstream against real handler chains.)
DotNet("run --framework net9.0 -- codegen preview --language fsharp --start", Solution.TestHarnesses.GeneratorTarget.Directory);
});

/// <summary>
Expand Down
105 changes: 105 additions & 0 deletions src/CodegenTests/CodegenLanguageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Model;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;

namespace CodegenTests;

public class CodegenLanguageTests
{
[Fact]
public void default_language_is_csharp()
{
new DynamicCodeBuilder(new ServiceCollection().BuildServiceProvider(), [])
.Language.ShouldBe(CodegenLanguage.csharp);
}

[Fact]
public void preview_emits_csharp_by_default()
{
var code = Build(CodegenLanguage.csharp).GenerateAllCode();

code.ShouldContain("namespace");
code.ShouldContain("class GreeterType"); // C# class syntax (F# would be "type GreeterType")
}

[Fact]
public void preview_emits_fsharp_when_language_is_fsharp()
{
var code = Build(CodegenLanguage.fsharp).GenerateAllCode();

code.ShouldContain("// <auto-generated/>");
code.ShouldContain("namespace");
code.ShouldContain("type GreeterType");
code.ShouldNotContain("public class");
}

[Fact]
public void write_uses_cs_extension_by_default()
{
var written = WriteToTempAndCollect(CodegenLanguage.csharp);

written.ShouldContain(f => f.EndsWith(".cs"));
written.ShouldNotContain(f => f.EndsWith(".fs"));
}

[Fact]
public void write_uses_fs_extension_and_fsharp_content_when_language_is_fsharp()
{
var written = WriteToTempAndCollect(CodegenLanguage.fsharp);

written.ShouldContain(f => f.EndsWith(".fs"));
written.ShouldNotContain(f => f.EndsWith(".cs"));

var fsFile = written.Single(f => f.EndsWith(".fs"));
File.ReadAllText(fsFile).ShouldContain("namespace");
}

private static DynamicCodeBuilder Build(CodegenLanguage language, GenerationRules? rules = null)
{
var collection = new SampleCollection(rules ?? new GenerationRules("Generated"), new SampleFile("Greeter"));
return new DynamicCodeBuilder(new ServiceCollection().BuildServiceProvider(), [collection])
{
Language = language
};
}

private static List<string> WriteToTempAndCollect(CodegenLanguage language)
{
var dir = Path.Combine(Path.GetTempPath(), "jasperfx-codegen-language-" + Guid.NewGuid().ToString("n"));
Directory.CreateDirectory(dir);
var rules = new GenerationRules("Generated") { GeneratedCodeOutputPath = dir };

var written = new List<string>();
Build(language, rules).WriteGeneratedCode(written.Add);
return written;
}

private sealed class SampleFile(string fileName) : ICodeFile
{
public string FileName { get; } = fileName;

public void AssembleTypes(GeneratedAssembly assembly) => assembly.AddType(FileName + "Type", typeof(object));

public Task<bool> AttachTypes(GenerationRules rules, System.Reflection.Assembly assembly,
IServiceProvider? services, string containingNamespace) => Task.FromResult(false);

public bool AttachTypesSynchronously(GenerationRules rules, System.Reflection.Assembly assembly,
IServiceProvider? services, string containingNamespace) => false;

public void AssertServiceLocationsAreAllowed(ServiceLocationReport[] reports, IServiceProvider? services) { }

public bool TryReplaceServiceProvider(out Variable serviceProvider)
{
serviceProvider = default!;
return false;
}
}

private sealed class SampleCollection(GenerationRules rules, params ICodeFile[] files) : ICodeFileCollection
{
public string ChildNamespace => "Sample";
public GenerationRules Rules { get; } = rules;
public IReadOnlyList<ICodeFile> BuildFiles() => files;
}
}
11 changes: 11 additions & 0 deletions src/JasperFx/CodeGeneration/CodegenLanguage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace JasperFx.CodeGeneration;

/// <summary>
/// Target language for generated code. C# is the default and supports both the dynamic (runtime
/// Roslyn) and pre-generated (static) models. F# targets the pre-generated/static model only.
/// </summary>
public enum CodegenLanguage
{
csharp,
fsharp
}
10 changes: 9 additions & 1 deletion src/JasperFx/CodeGeneration/Commands/GenerateCodeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,17 @@ public override bool Execute(GenerateCodeInput input)

var builder = new DynamicCodeBuilder(host.Services, collections)
{
ServiceVariableSource = host.Services.GetService<IServiceVariableSource>()
ServiceVariableSource = host.Services.GetService<IServiceVariableSource>(),
Language = input.LanguageFlag
};

if (input.LanguageFlag == CodegenLanguage.fsharp && input.Action == CodeAction.test)
{
AnsiConsole.MarkupLine(
"[red]'codegen test' does not support --language fsharp; the test action uses in-memory C# compilation. Use 'write' or 'preview' for F#.[/]");
return false;
}

switch (input.Action)
{
case CodeAction.preview:
Expand Down
3 changes: 3 additions & 0 deletions src/JasperFx/CodeGeneration/Commands/GenerateCodeInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public class GenerateCodeInput : NetCoreInput
[Description("Optionally limit the preview to only one type of code generation")]
public string? TypeFlag { get; set; }

[Description("Target language for the generated code (csharp or fsharp). Default is csharp")]
public CodegenLanguage LanguageFlag { get; set; } = CodegenLanguage.csharp;

[Description("Start the IHost instead of just building it. Use this when code generation participants are registered during host startup (e.g., ASP.NET Core Startup.Configure)")]
public bool StartFlag { get; set; }
}
21 changes: 18 additions & 3 deletions src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,23 @@ public DynamicCodeBuilder(IServiceProvider services, ICodeFileCollection[] colle

public IServiceVariableSource? ServiceVariableSource { get; set; }

/// <summary>
/// Target language for the generated source. F# (<see cref="CodegenLanguage.fsharp" />) emits the
/// pre-generated/static code model with the .fs extension; C# is the default.
/// </summary>
public CodegenLanguage Language { get; set; } = CodegenLanguage.csharp;

public string[] ChildNamespaces => Collections.Select(x => x.ChildNamespace).ToArray();

private string renderCode(GeneratedAssembly generatedAssembly, IServiceVariableSource? services)
{
return Language == CodegenLanguage.fsharp
? generatedAssembly.GenerateFSharpCode(services)
: generatedAssembly.GenerateCode(services);
}

private string fileExtension => Language == CodegenLanguage.fsharp ? ".fs" : ".cs";

public IServiceProvider Services { get; }

public ICodeFileCollection[] Collections { get; }
Expand Down Expand Up @@ -127,7 +142,7 @@ public void WriteGeneratedCode(Action<string> onFileWritten)
// `services` is shared across every file here, so reset the prior override first.
applyServiceProviderOverride(file, services);

var code = generatedAssembly.GenerateCode(services);
var code = renderCode(generatedAssembly, services);

// #227: enforce ServiceLocationPolicy per-file, matching the
// runtime compile path in DynamicTypeLoader.Initialize. The
Expand All @@ -138,7 +153,7 @@ public void WriteGeneratedCode(Action<string> onFileWritten)
// "all generated" result from the CLI.
assertServiceLocationsAllowed(file, services);

var fileName = Path.Combine(exportDirectory, file.FileName.Replace(" ", "_") + ".cs");
var fileName = Path.Combine(exportDirectory, file.FileName.Replace(" ", "_") + fileExtension);
File.WriteAllText(fileName, code);
onFileWritten(fileName);
}
Expand Down Expand Up @@ -185,7 +200,7 @@ private string generateCode(ICodeFileCollection collection)
// shared source.
applyServiceProviderOverride(file, services);

var code = generatedAssembly.GenerateCode(services);
var code = renderCode(generatedAssembly, services);
assertServiceLocationsAllowed(file, services);

writer.WriteLine(code);
Expand Down
Loading