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
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ CLI tool for [Trax](https://www.nuget.org/packages/Trax.Effect/) — generate Tr

## What This Does

Takes an existing API schema and scaffolds a Trax project with two parts: an API project (from the `trax-api` template) and a shared trains library with trains, junctions, input/output records, and models. The trains library follows the same structure as the DistributedWorkers sample — it can be referenced by an API, scheduler, or standalone workers.
Takes an existing API schema and scaffolds a Trax project with two parts: a hub project (from the `trax-hub` template — API + Scheduler + Dashboard in one process) and a shared trains library with trains, junctions, input/output records, and models. The trains library follows the same structure as the DistributedWorkers sample — it can be referenced by an API, scheduler, or standalone workers.

Supports two schema formats:

Expand All @@ -18,7 +18,7 @@ Supports two schema formats:

## Prerequisites

The `trax-api` template must be installed:
The `trax-hub` template must be installed:

```bash
dotnet new install Trax.Samples
Expand Down Expand Up @@ -62,8 +62,8 @@ Given a schema with `createPlayer` and `getPlayer` operations:

```
MyProject/
├── MyProject.Api/ # From dotnet new trax-api
│ ├── MyProject.Api.csproj # + ProjectReference to trains library
├── MyProject.Hub/ # From dotnet new trax-hub
│ ├── MyProject.Hub.csproj # + ProjectReference to trains library
│ ├── Program.cs # Patched: AddMediator scans trains assembly
│ ├── appsettings.json
│ └── Trains/ # Template sample trains (kept as examples)
Expand Down Expand Up @@ -98,11 +98,12 @@ Each junction contains a `throw new NotImplementedException()` with a TODO comme
## After Generating

```bash
cd MyProject/MyProject.Api
cd MyProject/MyProject.Hub
dotnet restore
# Fill in junction implementations in MyProject.Trains/ (search for TODO)
dotnet run
# Open http://localhost:5002/trax/graphql
# Open http://localhost:5000/trax/graphql for GraphQL IDE
# Open http://localhost:5000/trax for Dashboard
```

## Part of Trax
Expand Down
Empty file removed projects/.gitkeep
Empty file.
4 changes: 2 additions & 2 deletions src/Trax.Cli/Commands/GenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ bool force
generator.Generate(apiSchema, output.FullName, name, force);

Console.WriteLine($"Generated Trax project at: {output.FullName}");
Console.WriteLine($" {name}.Api/ — API project (from trax-api template)");
Console.WriteLine($" {name}.Hub/ — Hub project (API + Scheduler + Dashboard)");
Console.WriteLine($" {name}.Trains/ — Trains library (generated from schema)");
Console.WriteLine();
Console.WriteLine("Next steps:");
Console.WriteLine($" cd {output.FullName}/{name}.Api");
Console.WriteLine($" cd {output.FullName}/{name}.Hub");
Console.WriteLine(" dotnet restore");
Console.WriteLine(" # Fill in junction implementations (search for TODO)");
Console.WriteLine(" dotnet run");
Expand Down
11 changes: 11 additions & 0 deletions src/Trax.Cli/Generator/CodeRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ namespace Trax.Cli.Generator;
public class CodeRenderer
{
private readonly Dictionary<string, Template> _templates = new();
private string? _modelsNamespace;

public CodeRenderer()
{
LoadTemplates();
}

/// <summary>
/// Sets the models namespace to include as a using directive in generated files.
/// Call this when the schema has shared model types (non-built-in types or enums).
/// </summary>
public void SetModelsNamespace(string modelsNamespace) => _modelsNamespace = modelsNamespace;

public string RenderTrainInterface(ApiOperation operation, string projectName)
{
var isUnit = IsUnitOutput(operation.OutputType);
Expand All @@ -30,6 +37,7 @@ public string RenderTrainInterface(ApiOperation operation, string projectName)
OutputTypeName = isUnit ? "Unit" : operation.OutputType.Name,
OutputIsUnit = isUnit,
InputIsUnit = operation.InputType.Fields.Count == 0,
ModelsUsing = _modelsNamespace,
}
);
}
Expand All @@ -53,6 +61,7 @@ public string RenderTrainImplementation(ApiOperation operation, string projectNa
InputIsUnit = operation.InputType.Fields.Count == 0,
Attribute = attribute,
Description = operation.Description ?? $"{operation.Name} operation",
ModelsUsing = _modelsNamespace,
}
);
}
Expand Down Expand Up @@ -83,6 +92,7 @@ public string RenderOutput(ApiOperation operation, string projectName)
TypeName = operation.OutputType.Name,
Fields = operation.OutputType.Fields.Select(MapField).ToList(),
HasFields = operation.OutputType.Fields.Count > 0,
ModelsUsing = _modelsNamespace,
}
);
}
Expand All @@ -105,6 +115,7 @@ public string RenderJunction(ApiOperation operation, string projectName)
InputIsUnit = operation.InputType.Fields.Count == 0,
HttpMethod = operation.HttpMethod,
HttpPath = operation.HttpPath,
ModelsUsing = _modelsNamespace,
}
);
}
Expand Down
35 changes: 21 additions & 14 deletions src/Trax.Cli/Generator/TraxProjectGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,35 @@ public void Generate(ApiSchema schema, string outputDir, string projectName, boo

Directory.CreateDirectory(outputDir);

// 1. Scaffold the API project via dotnet new
var apiProjectName = $"{projectName}.Api";
var apiDir = Path.Combine(outputDir, apiProjectName);
RunDotnetNew(apiProjectName, apiDir);
// 1. Scaffold the hub project via dotnet new
var hubProjectName = $"{projectName}.Hub";
var hubDir = Path.Combine(outputDir, hubProjectName);
RunDotnetNew(hubProjectName, hubDir);

// 2. Create the trains library
var trainsProjectName = $"{projectName}.Trains";
var trainsDir = Path.Combine(outputDir, trainsProjectName);
GenerateTrainsLibrary(schema, trainsDir, projectName);

// 3. Add ProjectReference from API to trains library
AddProjectReference(apiDir, apiProjectName, trainsProjectName);
// 3. Add ProjectReference from hub to trains library
AddProjectReference(hubDir, hubProjectName, trainsProjectName);

// 4. Patch API Program.cs to scan the trains assembly
PatchProgramCs(apiDir, projectName);
// 4. Patch hub Program.cs to scan the trains assembly
PatchProgramCs(hubDir, projectName);
}

internal void GenerateTrainsLibrary(ApiSchema schema, string trainsDir, string projectName)
{
var trainsProjectName = $"{projectName}.Trains";
Directory.CreateDirectory(trainsDir);

// If the schema has shared types or enums, set the models namespace
// so generated code includes the correct using directive
var hasSharedTypes =
schema.Types.Any(t => !t.IsBuiltIn && t.Fields.Count > 0) || schema.Enums.Count > 0;
if (hasSharedTypes)
_renderer.SetModelsNamespace($"{projectName}.Trains.Models");

// Write trains csproj
WriteFile(
Path.Combine(trainsDir, $"{trainsProjectName}.csproj"),
Expand Down Expand Up @@ -143,18 +150,18 @@ string trainsProjectName
File.WriteAllText(csprojPath, content);
}

internal static void PatchProgramCs(string apiDir, string projectName)
internal static void PatchProgramCs(string hubDir, string projectName)
{
var programPath = Path.Combine(apiDir, "Program.cs");
var programPath = Path.Combine(hubDir, "Program.cs");
if (!File.Exists(programPath))
return;

var content = File.ReadAllText(programPath);

// Replace typeof(Program).Assembly with typeof(ManifestNames).Assembly
// Add the trains assembly alongside Program's assembly so both get scanned
content = content.Replace(
"typeof(Program).Assembly",
$"typeof({projectName}.Trains.ManifestNames).Assembly"
$"typeof(Program).Assembly, typeof({projectName}.Trains.ManifestNames).Assembly"
);

// Add using for the trains namespace if not already present
Expand All @@ -180,7 +187,7 @@ private static void RunDotnetNew(string name, string outputDir)
var psi = new ProcessStartInfo
{
FileName = "dotnet",
ArgumentList = { "new", "trax-api", "-n", name, "-o", outputDir },
ArgumentList = { "new", "trax-hub", "-n", name, "-o", outputDir },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
Expand All @@ -200,7 +207,7 @@ private static void RunDotnetNew(string name, string outputDir)
"No templates or subcommands found",
StringComparison.Ordinal
)
? "The 'trax-api' template is not installed. Run: dotnet new install Trax.Samples"
? "The 'trax-hub' template is not installed. Run: dotnet new install Trax.Samples"
: $"dotnet new failed (exit code {process.ExitCode}): {stderr}";
throw new InvalidOperationException(message);
}
Expand Down
6 changes: 5 additions & 1 deletion src/Trax.Cli/Templates/Junction.sbn
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{{- if OutputIsUnit }}
using LanguageExt;
{{- end }}
using Trax.Core.Models;
{{- if ModelsUsing }}
using {{ ModelsUsing }};
{{- end }}
using Microsoft.Extensions.Logging;
using Trax.Core.Junction;

namespace {{ Namespace }}.Junctions;

Expand Down
4 changes: 4 additions & 0 deletions src/Trax.Cli/Templates/Output.sbn
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{{- if ModelsUsing }}
using {{ ModelsUsing }};
{{- end }}

namespace {{ Namespace }};

public record {{ TypeName }}
Expand Down
3 changes: 3 additions & 0 deletions src/Trax.Cli/Templates/TrainImplementation.sbn
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using LanguageExt;
using Trax.Effect.Attributes;
using Trax.Effect.Services.ServiceTrain;
{{- if ModelsUsing }}
using {{ ModelsUsing }};
{{- end }}
using {{ Namespace }}.Junctions;

namespace {{ Namespace }};
Expand Down
3 changes: 3 additions & 0 deletions src/Trax.Cli/Templates/TrainInterface.sbn
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{{- if InputIsUnit || OutputIsUnit }}
using LanguageExt;
{{- end }}
{{- if ModelsUsing }}
using {{ ModelsUsing }};
{{- end }}
using Trax.Effect.Services.ServiceTrain;

namespace {{ Namespace }};
Expand Down
3 changes: 2 additions & 1 deletion src/Trax.Cli/Templates/TrainsCsproj.sbn
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
</PropertyGroup>

<ItemGroup>
Expand All @@ -11,7 +12,7 @@

<ItemGroup>
<PackageReference Include="Trax.Effect" Version="1.*" />
<PackageReference Include="Trax.Effect.Data.Postgres" Version="1.*" />
<PackageReference Include="Trax.Effect.Data.InMemory" Version="1.*" />
<PackageReference Include="Trax.Mediator" Version="1.*" />
<PackageReference Include="Trax.Scheduler" Version="1.*" />
</ItemGroup>
Expand Down
9 changes: 5 additions & 4 deletions tests/Trax.Cli.Tests/UnitTests/TraxProjectGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ public void AddProjectReference_InsertsProjectReferenceIntoCsproj()
#region PatchProgramCs

[Test]
public void PatchProgramCs_ReplacesTypeoOfProgramWithManifestNames()
public void PatchProgramCs_AddTrainsAssemblyAlongsideProgram()
{
Directory.CreateDirectory(_tempDir);
var programPath = Path.Combine(_tempDir, "Program.cs");
Expand All @@ -306,7 +306,7 @@ public void PatchProgramCs_ReplacesTypeoOfProgramWithManifestNames()
using Trax.Mediator.Extensions;

builder.Services.AddTrax(trax =>
trax.AddEffects(effects => effects.UsePostgres(connectionString))
trax.AddEffects(effects => effects.UseInMemory())
.AddMediator(typeof(Program).Assembly)
);
"""
Expand All @@ -315,8 +315,9 @@ public void PatchProgramCs_ReplacesTypeoOfProgramWithManifestNames()
TraxProjectGenerator.PatchProgramCs(_tempDir, "TestProject");

var content = File.ReadAllText(programPath);
content.Should().Contain("typeof(TestProject.Trains.ManifestNames).Assembly");
content.Should().NotContain("typeof(Program).Assembly");
content
.Should()
.Contain("typeof(Program).Assembly, typeof(TestProject.Trains.ManifestNames).Assembly");
content.Should().Contain("using TestProject.Trains;");
}

Expand Down
Loading