Skip to content

Commit 1f0b3fc

Browse files
committed
no-mistakes(review): Fix C# eject review findings: engine guard, pin warning
1 parent 4255c2a commit 1f0b3fc

16 files changed

Lines changed: 329 additions & 163 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,10 @@ generator catalog and the Atlas evaluation are 1.2 work.
156156
- **`api-docs` is a generator you can own.** `dotnet meta eject api-docs` copies the page
157157
generator with its page model, builder, renderer and path math. `dotnet meta docs` hands
158158
off to `dotnet run --project codegen -- docs` once `codegen/generators/ApiDocsGenerator.cs`
159-
exists, so your copy writes the reference. What the reference documents comes from the
160-
generators in your `Program.cs` list: an owned `routes` whose `AppliesTo` you changed is
161-
documented as it behaves, not as the packaged one does (`GeneratorApplicability.From`).
159+
exists, so your copy writes the reference. What the reference documents is still what the
160+
packaged generators emit for (`GeneratorApplicability.Packaged`): an owned `routes` whose
161+
`AppliesTo` you changed is documented as the packaged one behaves, unless you edit the
162+
owned builder.
162163
`dotnet meta gen` skips a docs generator and says so, and `verify --codegen` leaves it out.
163164
New public API in `MetaObjects.Codegen`: `IDocsGenerator`, `DocsRunFacts`,
164165
`GeneratorApplicability`, `GenContext.Applicability` and `GenContext.Docs`,
@@ -168,16 +169,27 @@ generator catalog and the Atlas evaluation are 1.2 work.
168169
`[Obsolete]` forwarder (see **Deprecated**).
169170
- **A copy you ejected before this release** keeps compiling and running: it finds the
170171
packaged emitters as it always did. `dotnet meta gen --list` now marks it as missing the
171-
ones it was not handed (`Fr019SharedEnum.cs missing`). To own them, eject the same name
172+
ones it was not handed (`Fr019SharedEnum.cs missing`). To own them, first raise the
173+
`MetaObjects.Codegen` version in `codegen/Codegen.csproj` to 1.1.0 or later (the 1.1.0
174+
emitter files are written against the 1.1.0 package, and some call API 1.0.x does not
175+
have), then eject the same name
172176
into a scratch directory (`dotnet meta eject entity --root <scratch>`) and copy the emitter
173177
files from its `codegen/generators/` into yours; your copy and your edits are untouched,
174178
and it resolves the new files by namespace.
179+
- **Eject warns about an older package pin.** Eject writes the sources of the tool you run
180+
and never edits `codegen/Codegen.csproj`. When that file pins a `MetaObjects.Codegen` older
181+
than the tool, `dotnet meta eject` now prints a warning with the pinned version, the
182+
tool's version and the line to change; the owned project does not compile until the pin
183+
is raised. A 1.0.x project needs this before `dotnet meta eject api-docs`, and before
184+
re-ejecting `names`, `render-helper` or a prompt-tier generator.
175185
- **A gate for the rule.** For every ejectable generator the registry lists, a test ejects it
176186
alone, compiles it against the public API, fails on any reference to a packaged emitter,
177187
generator or docs writer outside a written engine surface, fails on a handed-over file
178188
nothing calls, and compares its output with the packaged generator's over the shared
179189
corpora. A registered generator with no eject source and no written exemption fails it, as
180-
does a generator class the package ships that no registry entry builds.
190+
does a generator class the package ships that no registry entry builds. A second test
191+
compiles the package without the handed-over emitters, so nothing that stays packaged
192+
calls one.
181193
The cross-port manifest lists `api-docs` for `csharp`.
182194

183195
- **The stock `entity`, `queries`, `routes` and `barrel` generators are importable again

‎docs/features/own-your-codegen.md‎

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,11 @@ writes its output through come with it
10211021
On first use eject also writes `codegen/Codegen.csproj`, a console project referencing
10221022
`MetaObjects.Codegen` at the tool's version, and `codegen/Program.cs`, which lists your
10231023
generators with `new` and calls the public runner. After that, both files are yours: eject
1024-
never touches them again, even with `--force`, and prints what to add instead. Once
1024+
never touches them again, even with `--force`, and prints what to add instead. That includes
1025+
the package version: eject writes the sources of the tool you run, so when
1026+
`codegen/Codegen.csproj` pins an older `MetaObjects.Codegen` than the tool, eject prints a
1027+
warning with the pinned version, the tool's version and the line to change. The owned
1028+
project does not compile until the pin is the tool's version or later. Once
10251029
`codegen/Codegen.csproj` exists, `dotnet meta gen` and `dotnet meta verify --codegen` hand
10261030
off to `dotnet run --project codegen` with the same arguments, so your owned generators
10271031
run everywhere the tool did.
@@ -1074,11 +1078,15 @@ asks of the model (`FindInbound`, `TphPlan`, `InstanceArtifacts`, `ReportRows`).
10741078
returns emitted text is handed over; a table or a model question is not. The line is written
10751079
down and checked: a test ejects each generator the registry lists into an empty project,
10761080
compiles it against the public API and fails on any reference to a packaged emitter, generator
1077-
or docs writer that is not on that list.
1081+
or docs writer that is not on that list. A second test compiles the package without the
1082+
handed-over emitters, so nothing that stays packaged calls one.
10781083
10791084
**A copy you ejected before the emitters were handed over** keeps compiling and running, with
1080-
the packaged emitters. To own them, eject the same name into a scratch directory and copy the
1081-
emitter files into your project; your copy and your edits are untouched:
1085+
the packaged emitters. To own them, first raise the `MetaObjects.Codegen` version in
1086+
`codegen/Codegen.csproj` to 1.1.0 or later: the 1.1.0 emitter files are written against the
1087+
1.1.0 package, and some call API that 1.0.x does not have. Then eject the same name
1088+
into a scratch directory and copy the emitter files into your project; your copy and your
1089+
edits are untouched:
10821090
10831091
```bash
10841092
scratch=$(mktemp -d)
@@ -1099,16 +1107,18 @@ Once `codegen/generators/ApiDocsGenerator.cs` exists, `dotnet meta docs` hands o
10991107
`dotnet run --project codegen -- docs` with the same arguments, and the reference is written
11001108
by your copy. A project that owns `codegen/` but not `api-docs` keeps the packaged reference:
11011109
the hand-off for `docs` is keyed to the generator, not to the project, because an owned
1102-
project pinned to an older `MetaObjects.Codegen` does not know the `docs` command.
1103-
1104-
**It describes the generators you run.** The reference documents a route, an allowlist, a
1105-
render helper only if the generator that emits it would. The packaged generators answer that
1106-
through their static `AppliesTo`; when `docs` runs, the answers come from the generators in
1107-
your `Program.cs` list instead, so an owned `routes` whose `AppliesTo` you changed is
1108-
documented as it behaves. A generator you left in the package keeps answering as the package
1109-
does. The builder asks through `GenContext.Applicability`
1110-
(`GeneratorApplicability.From(list)` builds it), never through a generator class, which is
1111-
why the copy depends on nothing but the engine.
1110+
project pinned to an older `MetaObjects.Codegen` does not know the `docs` command. The owned
1111+
copy needs `MetaObjects.Codegen` 1.1.0 or later for the same reason; eject warns when
1112+
`codegen/Codegen.csproj` pins less.
1113+
1114+
**It describes what the packaged generators emit.** The reference documents a route, an
1115+
allowlist, a render helper only if the packaged generator that emits it would, which each
1116+
answers through its static `AppliesTo`. That holds in a project that owns one of those
1117+
generators too: an owned `routes` whose `AppliesTo` you changed is still documented as the
1118+
packaged one behaves. To document your own surface, edit the owned builder
1119+
(`CSharpApiModelBuilder.cs`), which is where the question is asked. The builder asks through
1120+
`GenContext.Applicability` (`GeneratorApplicability.Packaged`), never through a generator
1121+
class, which is why the copy depends on nothing but the engine.
11121122
11131123
`Program.cs` lists `new ApiDocsGenerator()` beside the code generators. `dotnet meta gen` and
11141124
`verify --codegen` skip it, and `gen` says so (`"api-docs" writes the api reference, so

‎docs/ports/csharp.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ for a worked example.
8181

8282
## Generate
8383

84-
**Own a generator.** `dotnet meta eject <name>...` copies a reference generator into `codegen/generators/` and scaffolds an owned `codegen/` console project that `dotnet meta gen` then runs. Every generator `dotnet meta gen --list` shows is ejectable except `template`, the Mustache primitive, which has no emit logic of its own to own. An ejected generator comes with the emitters it writes its output through (the enum, navigation, doc-comment and prompt-tier writers), so the copy owns the code that writes the file and not a shell around the package's; the naming and type-mapping tables (`CSharpNaming`, `ValueObjectNames`) stay in the package as the engine. `dotnet meta eject api-docs` does the same for the api reference `dotnet meta docs` writes, and the reference then describes the generators you own. See [Own your codegen → C#](../features/own-your-codegen.md#c-dotnet-meta-eject).
84+
**Own a generator.** `dotnet meta eject <name>...` copies a reference generator into `codegen/generators/` and scaffolds an owned `codegen/` console project that `dotnet meta gen` then runs. Every generator `dotnet meta gen --list` shows is ejectable except `template`, the Mustache primitive, which has no emit logic of its own to own. An ejected generator comes with the emitters it writes its output through (the enum, navigation, doc-comment and prompt-tier writers), so the copy owns the code that writes the file and not a shell around the package's; the naming and type-mapping tables (`CSharpNaming`, `ValueObjectNames`) stay in the package as the engine. `dotnet meta eject api-docs` does the same for the api reference `dotnet meta docs` writes. Ejecting into a project whose `codegen/Codegen.csproj` pins an older `MetaObjects.Codegen` than the tool prints a warning naming the line to change. See [Own your codegen → C#](../features/own-your-codegen.md#c-dotnet-meta-eject).
8585

8686
```bash
8787
# Generate EF Core entities + AppDbContext + CRUD minimal-API routes

‎server/csharp/MetaObjects.Cli.Tests/EjectCommandTests.cs‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,93 @@ public void A_second_eject_of_a_different_generator_does_not_touch_the_existing_
129129
Assert.Contains("adopter's own edit", File.ReadAllText(Path.Combine(CodegenDir, "Program.cs")));
130130
}
131131

132+
private string CsprojPath => Path.Combine(CodegenDir, "Codegen.csproj");
133+
134+
/// <summary>Scaffold with this tool, then re-pin the owned project the way an older tool left it.</summary>
135+
private string ScaffoldPinnedAt(string? version)
136+
{
137+
Assert.Equal(0, EjectCommand.Run(["entity"], _root, force: false).ExitCode);
138+
var scaffolded = File.ReadAllText(CsprojPath);
139+
var pin = $"Version=\"{EjectCommand.ToolVersion()}\"";
140+
Assert.Contains(pin, scaffolded);
141+
if (version is not null) File.WriteAllText(CsprojPath, scaffolded.Replace(pin, $"Version=\"{version}\""));
142+
return File.ReadAllText(CsprojPath);
143+
}
144+
145+
// Eject writes this tool's sources and never edits the owned project's package pin. A project
146+
// scaffolded by an older tool therefore gets sources its pinned package cannot compile, and the
147+
// only place that can say so before `dotnet run --project codegen` fails is eject's own report.
148+
[Fact]
149+
public void Ejecting_into_a_project_pinned_to_an_older_package_warns_and_names_the_line_to_change()
150+
{
151+
var csproj = ScaffoldPinnedAt("0.9.0");
152+
var pinLine = Array.FindIndex(csproj.Split('\n'), l => l.Contains("PackageReference", StringComparison.Ordinal)) + 1;
153+
154+
var result = EjectCommand.Run(["api-docs"], _root, force: false);
155+
156+
Assert.Equal(0, result.ExitCode);
157+
var warning = Assert.Single(result.Out, l => l.StartsWith("warning:", StringComparison.Ordinal));
158+
Assert.Contains("MetaObjects.Codegen 0.9.0", warning);
159+
Assert.Contains(EjectCommand.ToolVersion(), warning);
160+
Assert.Contains("will not compile", warning);
161+
Assert.Contains($"line {pinLine} of codegen/Codegen.csproj", warning);
162+
Assert.Contains(
163+
$"<PackageReference Include=\"MetaObjects.Codegen\" Version=\"{EjectCommand.ToolVersion()}\" />",
164+
result.Out[result.Out.ToList().IndexOf(warning) + 1]);
165+
Assert.Equal(csproj, File.ReadAllText(CsprojPath));
166+
}
167+
168+
[Theory]
169+
[InlineData(null)] // the pin this tool wrote: equal
170+
[InlineData("99.0.0")] // newer
171+
[InlineData("$(MetaObjectsVersion)")] // not a version eject can read
172+
[InlineData("[1.0.0,2.0.0)")] // a range
173+
public void Ejecting_into_a_project_whose_pin_is_not_older_says_nothing_about_it(string? version)
174+
{
175+
ScaffoldPinnedAt(version);
176+
177+
var result = EjectCommand.Run(["api-docs"], _root, force: false);
178+
179+
Assert.Equal(0, result.ExitCode);
180+
Assert.DoesNotContain(result.Out, l => l.Contains("warning", StringComparison.OrdinalIgnoreCase));
181+
}
182+
183+
[Fact]
184+
public void Ejecting_into_a_project_with_no_package_pin_says_nothing_about_it()
185+
{
186+
ScaffoldPinnedAt(null);
187+
File.WriteAllText(CsprojPath, "<Project Sdk=\"Microsoft.NET.Sdk\"><ItemGroup><ProjectReference Include=\"../x.csproj\" /></ItemGroup></Project>");
188+
189+
var result = EjectCommand.Run(["api-docs"], _root, force: false);
190+
191+
Assert.Equal(0, result.ExitCode);
192+
Assert.DoesNotContain(result.Out, l => l.Contains("warning", StringComparison.OrdinalIgnoreCase));
193+
}
194+
195+
// The order is semver.org's (§11), whose own worked chain is
196+
// 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
197+
// A string or System.Version comparison gets the release candidate and the two-digit parts wrong.
198+
[Theory]
199+
[InlineData("1.0.13", "1.1.0", true)]
200+
[InlineData("1.1.0-rc.1", "1.1.0", true)]
201+
[InlineData("1.1.0", "1.1.0-rc.1", false)]
202+
[InlineData("1.1.0", "1.1.0", false)]
203+
[InlineData("1.1.0+8ef72c3", "1.1.0", false)]
204+
[InlineData("1.9.0", "1.10.0", true)]
205+
[InlineData("1.10.0", "1.9.0", false)]
206+
[InlineData("1.0.0-alpha", "1.0.0-alpha.1", true)]
207+
[InlineData("1.0.0-alpha.1", "1.0.0-alpha.beta", true)]
208+
[InlineData("1.0.0-alpha.beta", "1.0.0-beta.2", true)]
209+
[InlineData("1.0.0-beta.2", "1.0.0-beta.11", true)]
210+
[InlineData("1.0.0-beta.11", "1.0.0-rc.1", true)]
211+
[InlineData("1.0.0-rc.1", "1.0.0-beta.11", false)]
212+
[InlineData("1.*", "1.1.0", false)]
213+
[InlineData("$(MetaObjectsVersion)", "1.1.0", false)]
214+
public void A_pin_is_older_by_semantic_version_precedence(string pinned, string required, bool older)
215+
{
216+
Assert.Equal(older, EjectCommand.IsOlderVersion(pinned, required));
217+
}
218+
132219
[Fact]
133220
public void Ejected_copy_is_content_identical_to_the_rewritten_reference()
134221
{

‎server/csharp/MetaObjects.Cli.Tests/EjectEndToEndTests.cs‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,12 @@ private void PointScaffoldAtThisCheckout(string repoRoot)
196196

197197
/// <summary>
198198
/// An owned api-docs generator, run for real (ADR-0025 / ADR-0034 Amendment 3): eject `routes` and
199-
/// `api-docs`, change what the owned routes serves, and run the owned project's `docs` command. The
200-
/// reference is written by the OWNED api-docs copy and describes the OWNED routes, so the edit shows up
201-
/// in the pages; and the same owned project's `gen` skips the docs generator and says so.
199+
/// `api-docs`, add a page to the owned api-docs copy, and run the owned project's `docs` command. The
200+
/// reference is written by the OWNED api-docs copy, so the added page is on disk beside the model's;
201+
/// and the same owned project's `gen` skips the docs generator and says so.
202202
/// </summary>
203203
[Fact]
204-
public void Owned_api_docs_writes_the_reference_and_describes_the_owned_routes()
204+
public void Owned_api_docs_writes_the_reference()
205205
{
206206
var repoRoot = RepoRoot();
207207
Directory.CreateDirectory(MetadataDir);
@@ -221,24 +221,25 @@ public void Owned_api_docs_writes_the_reference_and_describes_the_owned_routes()
221221
Assert.True(CodegenHandoff.HasOwnedDocs(_root));
222222
PointScaffoldAtThisCheckout(repoRoot);
223223

224-
// The project's own decision: its routes serve no entity.
225-
var routesPath = Path.Combine(_root, "codegen", "generators", "RoutesGenerator.cs");
226-
var routes = File.ReadAllText(routesPath);
227-
const string signature = "public static bool AppliesTo(MetaObject entity, MetaRoot root) =>";
228-
Assert.Contains(signature, routes);
229-
File.WriteAllText(routesPath, routes.Replace(
230-
signature,
231-
"public static bool AppliesTo(MetaObject entity, MetaRoot root) => false;\n" +
232-
" private static bool PackagedAppliesTo(MetaObject entity, MetaRoot root) =>"));
224+
// The project's own edit: its reference carries a page the packaged generator does not write.
225+
var apiDocsPath = Path.Combine(_root, "codegen", "generators", "ApiDocsGenerator.cs");
226+
var apiDocs = File.ReadAllText(apiDocsPath);
227+
const string lastPage = "yield return Page(facts, \"AGENT-API.md\", renderer.RenderAgentApi(model));";
228+
Assert.Contains(lastPage, apiDocs);
229+
File.WriteAllText(apiDocsPath, apiDocs.Replace(
230+
lastPage,
231+
lastPage + "\n yield return Page(facts, \"OWNED.md\", \"written by the owned api-docs\");"));
233232

234233
var docsOut = Path.Combine(_root, "docs");
235234
var (docsExit, docsOutput) = RunCodegenProject("docs", MetadataDir, "--out", docsOut, "--namespace", "Acme.Generated");
236235
Assert.True(docsExit == 0, $"docs failed (exit {docsExit}):\n{docsOutput}");
237236
Assert.Contains("api page(s) written", docsOutput);
238237

239238
var widget = File.ReadAllText(Directory.GetFiles(docsOut, "Widget.md", SearchOption.AllDirectories).Single());
240-
Assert.Contains("DbSet<Widget>", widget); // the owned api-docs described the model…
241-
Assert.DoesNotContain("GET /api/", widget); // …and what the OWNED routes serve: nothing.
239+
Assert.Contains("DbSet<Widget>", widget);
240+
Assert.Equal(
241+
"written by the owned api-docs",
242+
File.ReadAllText(Directory.GetFiles(docsOut, "OWNED.md", SearchOption.AllDirectories).Single()));
242243

243244
// The same owned project's gen leaves the docs generator out and says why.
244245
var (genExit, genOutput) = RunCodegenProject(

‎server/csharp/MetaObjects.Cli/CodegenHandoff.cs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ public static bool HasOwnedCodegen(string projectRoot) =>
2020

2121
/// <summary>True iff <paramref name="projectRoot"/> owns the api-docs generator: the ejected
2222
/// codegen project exists AND carries <c>codegen/generators/ApiDocsGenerator.cs</c>. That file is
23-
/// what `dotnet meta docs` hands off for — the project's own copy writes the reference, and the
24-
/// owned project's package version is one that knows the <c>docs</c> command (a project
25-
/// scaffolded before api-docs could be ejected has no such file, and its package does not).</summary>
23+
/// what `dotnet meta docs` hands off for — the project's own copy writes the reference. Its
24+
/// presence says a tool that can eject api-docs wrote it, NOT that the package
25+
/// <c>codegen/Codegen.csproj</c> pins knows the <c>docs</c> command: eject never edits that pin, so
26+
/// a project scaffolded by an older tool keeps the older one until its owner raises it, and
27+
/// <see cref="EjectCommand.Run"/> warns when it writes into such a project. A project with no such
28+
/// file never ejected api-docs, and <c>docs</c> stays with the packaged generator whatever it pins.</summary>
2629
public static bool HasOwnedDocs(string projectRoot) =>
2730
HasOwnedCodegen(projectRoot) &&
2831
MetaObjects.Codegen.GeneratorRegistry.Get("api-docs")?.SourceFileName is { } file &&

0 commit comments

Comments
 (0)