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
7 changes: 7 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
<ItemGroup>
<!-- Cratis -->
<PackageVersion Include="Cratis" Version="21.13.0" />
<PackageVersion Include="Cratis.Arc" Version="21.13.0" />
<PackageVersion Include="Cratis.Arc.Chronicle" Version="21.13.0" />
<PackageVersion Include="Cratis.Arc.Chronicle.Testing" Version="21.13.0" />
<PackageVersion Include="Cratis.Arc.MongoDB" Version="21.13.0" />
<PackageVersion Include="Cratis.Arc.Swagger" Version="21.13.0" />
<PackageVersion Include="Cratis.Arc.Testing" Version="21.13.0" />
<PackageVersion Include="Cratis.Chronicle" Version="16.26.0" />
<PackageVersion Include="MongoDB.Driver" Version="3.10.0" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="16.28.0" />
<PackageVersion Include="Cratis.Screenplay" Version="2.1.0" />
<PackageVersion Include="Cratis.Templates" Version="1.1.1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Cratis.Stage.Rendering.Cratis.Scaffolding</AssemblyName>
<RootNamespace>Cratis.Stage.Rendering.Cratis.Scaffolding</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<Description>Scaffolds the Cratis project a rendered Screenplay application is placed into, from the Cratis templates.</Description>
<!-- New package: there is no released baseline to compare against yet. -->
<EnablePackageValidation>false</EnablePackageValidation>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cratis.Templates" />
<PackageReference Include="Microsoft.TemplateEngine.Abstractions" />
<PackageReference Include="Microsoft.TemplateEngine.Edge" />
<PackageReference Include="Microsoft.TemplateEngine.Orchestrator.RunnableProjects" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Rendering.Cratis/Rendering.Cratis.csproj" />
</ItemGroup>
</Project>
46 changes: 46 additions & 0 deletions Source/Rendering.Cratis/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Cratis.Arc.Generated.GeneratedMarker</Target>
<Left>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Left>
<Right>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Cratis.Stage.Rendering.Cratis.Scaffolding.SampleSlice</Target>
<Left>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Left>
<Right>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Cratis.Stage.Rendering.Cratis.Scaffolding.ScaffoldingFailed</Target>
<Left>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Left>
<Right>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Cratis.Stage.Rendering.Cratis.Scaffolding.TemplateEngineProjectScaffolder</Target>
<Left>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Left>
<Right>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Cratis.Stage.Rendering.Cratis.Scaffolding.TemplateNotFound</Target>
<Left>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Left>
<Right>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Cratis.Stage.Rendering.Cratis.Scaffolding.TemplatePackageNotFound</Target>
<Left>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Left>
<Right>lib/net10.0/Cratis.Stage.Rendering.Cratis.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
43 changes: 40 additions & 3 deletions Source/Rendering.Cratis/CratisRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Cratis.Stage.Rendering.Cratis.Naming;
using Cratis.Stage.Rendering.Cratis.Renderers;
using Cratis.Stage.Rendering.Cratis.Scaffolding;
using Cratis.Stage.Rendering.Cratis.Specifications;

namespace Cratis.Stage.Rendering.Cratis;

Expand All @@ -23,9 +24,14 @@ namespace Cratis.Stage.Rendering.Cratis;
public class CratisRenderer(IProjectScaffolder scaffolder, IReadOnlyDictionary<SliceType, ISliceRenderer> sliceRenderers, ICodeOutput codeOutput) : IRenderer
{
/// <summary>
/// Creates a <see cref="CratisRenderer"/> wired with the real scaffolder, slice renderers, and local file
/// system output.
/// Creates a <see cref="CratisRenderer"/> wired with the slice renderers and local file system output,
/// rendering into the target directory without scaffolding a project around it.
/// </summary>
/// <remarks>
/// Pass a <see cref="IProjectScaffolder"/> to the constructor to scaffold as well — the template-engine
/// one lives in <c>Cratis.Stage.Rendering.Cratis.Scaffolding</c>, kept out of this package because the
/// engine it needs cannot be hosted beside MSBuild.
/// </remarks>
/// <returns>The <see cref="CratisRenderer"/>.</returns>
public static CratisRenderer CreateDefault()
{
Expand All @@ -38,7 +44,7 @@ public static CratisRenderer CreateDefault()
[SliceType.Translate] = reactorRenderer,
};

return new CratisRenderer(new TemplateEngineProjectScaffolder(), sliceRenderers, new LocalFileSystemOutput());
return new CratisRenderer(new TargetDirectoryScaffolder(), sliceRenderers, new LocalFileSystemOutput());
}

/// <inheritdoc/>
Expand Down Expand Up @@ -196,13 +202,44 @@ async Task RenderSlice(LocatedSlice slice, ApplicationSet applicationSet, string
{
await output.WriteLineAsync($"Rendering slice '{slicePath}'...");
await WriteFile(renderer.Render(slice, applicationSet, rootNamespace), targetDirectory, output, error);
await RenderSpecifications(slice, applicationSet, rootNamespace, targetDirectory, output, error);
}
catch (Exception exception)
{
await error.WriteLineAsync($"Failed to render slice '{slicePath}': {exception.Message}");
}
}

/// <summary>
/// Renders the slice's specifications, one file each. A specification that cannot be rendered faithfully is
/// reported rather than emitted — a spec asserting something the document did not state is worse than none.
/// </summary>
/// <param name="slice">The located slice whose specifications to render.</param>
/// <param name="applicationSet">The <see cref="ApplicationSet"/> to resolve against.</param>
/// <param name="rootNamespace">The root namespace of the target application.</param>
/// <param name="targetDirectory">The directory to render into.</param>
/// <param name="output">The <see cref="TextWriter"/> progress is reported to.</param>
/// <param name="error">The <see cref="TextWriter"/> rendering problems are reported to.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
async Task RenderSpecifications(
LocatedSlice slice, ApplicationSet applicationSet, string rootNamespace, DirectoryInfo targetDirectory, TextWriter output, TextWriter error)
{
foreach (var specification in slice.Slice.Specifications)
{
if (SpecificationRenderer.Unrenderable(specification, slice.Slice) is { } reason)
{
await error.WriteLineAsync($"Specification '{specification.Name}' is not rendered — {reason}.");
continue;
}

await WriteFile(
SpecificationRenderer.Render(specification, slice.Slice.Commands.First(), slice, applicationSet, rootNamespace),
targetDirectory,
output,
error);
}
}

async Task WriteFile(RenderedFile file, DirectoryInfo targetDirectory, TextWriter output, TextWriter error)
{
try
Expand Down
8 changes: 8 additions & 0 deletions Source/Rendering.Cratis/Naming/Identifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ public static string ToCamelCase(string name)
/// <returns>The lowercase, space-separated words.</returns>
public static string ToWords(string name) => string.Join(' ', SplitWords(name).SelectMany(SplitOnCaseBoundary)).ToLowerInvariant();

/// <summary>
/// Converts a name into snake_case — used for the spec folder and class names the repository conventions
/// use, where <c>RegisteringADraftInvoice</c> reads as <c>registering_a_draft_invoice</c>.
/// </summary>
/// <param name="name">The name to convert.</param>
/// <returns>The snake_case name.</returns>
public static string ToSnakeCase(string name) => ToWords(name).Replace(' ', '_');

/// <summary>
/// Escapes an identifier with <c>@</c> when it is a reserved C# keyword.
/// </summary>
Expand Down
7 changes: 3 additions & 4 deletions Source/Rendering.Cratis/Renderers/UnrenderedConstructs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,9 @@ public static void Report(CSharpCodeBuilder builder, SliceSyntax slice, Rendered
slice.Captures.Count(),
"capture",
"no ingestion of the captured source is rendered.");
yield return (
slice.Specifications.Count(),
"specification",
"no specs are rendered for the generated application.");

// Specifications are rendered separately, one file each, and each one that cannot be says so on its own
// — so counting them here would report the same thing twice and count the rendered ones as dropped.
}

// Both collections are trailing optionals on SliceSyntax and are null on a slice that declares neither.
Expand Down
25 changes: 18 additions & 7 deletions Source/Rendering.Cratis/Rendering.Cratis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,35 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cratis.Screenplay" />
<PackageReference Include="Cratis.Templates" />
<PackageReference Include="Microsoft.TemplateEngine.Abstractions" />
<PackageReference Include="Microsoft.TemplateEngine.Edge" />
<PackageReference Include="Microsoft.TemplateEngine.Orchestrator.RunnableProjects" />
</ItemGroup>
<!--
Specs compile the rendered output in memory against the real Cratis assemblies — asserting on generated
strings is what let a whole class of non-compiling output ship unnoticed. These are private assets: the
renderer itself does not depend on Cratis at runtime, only its specs do.

Named individually rather than through the Cratis meta-package: that package contributes
GlobalUsings.cs through its Cratis.props, so every consumer of this one silently gains 28 global
usings and the type collisions that come with them. These carry no build assets. Arc.Chronicle and
Arc.Swagger are here only so the spec compilation can model the ambient namespaces a real Cratis
application has, not because rendered code names anything in them; MongoDB.Driver is, because a
rendered read model exposes its collection query as IMongoCollection&lt;T&gt;. The two Testing packages
are what a rendered specification compiles against - CommandScenario and the appended-event assertions.
-->
<ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<!--
Analyzers are excluded deliberately: Cratis' type-discovery source generator would bake a registration
into this assembly that binds the Cratis runtime at startup, which the renderer — a text generator —
has no reason to do.
-->
<PackageReference Include="Cratis" ExcludeAssets="analyzers" />
<PackageReference Include="Cratis.Arc.MongoDB" ExcludeAssets="analyzers" />
<PackageReference Include="Cratis.Arc" ExcludeAssets="analyzers" />
<PackageReference Include="Cratis.Arc.Chronicle" ExcludeAssets="analyzers" />
<PackageReference Include="Cratis.Arc.Chronicle.Testing" ExcludeAssets="analyzers" />
<PackageReference Include="Cratis.Arc.Swagger" ExcludeAssets="analyzers" />
<PackageReference Include="Cratis.Arc.Testing" ExcludeAssets="analyzers" />
<PackageReference Include="Cratis.Chronicle" ExcludeAssets="analyzers" />
<PackageReference Include="MongoDB.Driver" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
Expand Down
32 changes: 32 additions & 0 deletions Source/Rendering.Cratis/Scaffolding/TargetDirectoryScaffolder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Stage.Rendering.Cratis.Scaffolding;

/// <summary>
/// Makes sure the target directory exists, and scaffolds nothing into it.
/// </summary>
/// <remarks>
/// <para>
/// The default, because rendering into a project that already exists is the common case — regenerating from
/// a document that has moved on. Rendering needs somewhere to write and nothing more; a rendered application
/// is 33 files for a document the size of the language's own <c>invoicing.play</c>, and every one of them
/// lands here whether or not a project file surrounds them.
/// </para>
/// <para>
/// Scaffolding a project around them is a separate job, and an expensive one to carry: it needs the template
/// engine, which brings the whole NuGet client with it, which is what made this package impossible to host
/// beside anything that uses MSBuild (<see href="https://github.com/Cratis/Stage/issues/34">Cratis/Stage#34</see>).
/// A caller that wants it takes <c>Cratis.Stage.Rendering.Cratis.Scaffolding</c> and passes its
/// <c>TemplateEngineProjectScaffolder</c> in.
/// </para>
/// </remarks>
public class TargetDirectoryScaffolder : IProjectScaffolder
{
/// <inheritdoc/>
public Task<bool> EnsureScaffolded(DirectoryInfo targetDirectory, string projectName, TextWriter output)
{
targetDirectory.Create();
return Task.FromResult(false);
}
}
Loading
Loading