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
16 changes: 14 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,20 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

# Bootstrap the first source-provider packages while their new package IDs await nuget.org trusted-publisher
# policies. Remove this step once both 0.1.0 releases are available from nuget.org.
- name: Download Screenplay source-provider packages
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download v0.1.0 --repo Cratis/Screenplay.Generation --pattern '*.nupkg' --dir ./Artifacts/Dependencies
gh release download v0.1.0 --repo Cratis/Screenplay.CritterStack --pattern '*.nupkg' --dir ./Artifacts/Dependencies

- name: Restore
run: dotnet restore --property:Configuration=Release --source ./Artifacts/Dependencies --source https://api.nuget.org/v3/index.json

- name: Build
run: dotnet build --configuration Release
run: dotnet build --no-restore --configuration Release

# The integration fixture starts a Chronicle container whose image is named after the Chronicle
# package version, so a version bump always faces a cold ~400MB pull. Testcontainers times the first
Expand All @@ -45,4 +57,4 @@ jobs:
docker pull "cratis/chronicle:$VERSION-development"

- name: Test
run: dotnet test --configuration Release
run: dotnet test --no-restore --configuration Release
48 changes: 44 additions & 4 deletions .github/workflows/publish-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,18 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Download Screenplay source-provider packages
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download v0.1.0 --repo Cratis/Screenplay.Generation --pattern '*.nupkg' --dir ./Artifacts/Dependencies
gh release download v0.1.0 --repo Cratis/Screenplay.CritterStack --pattern '*.nupkg' --dir ./Artifacts/Dependencies

- name: Restore
run: dotnet restore Source/Cli/Cli.csproj -r osx-arm64 --source ./Artifacts/Dependencies --source https://api.nuget.org/v3/index.json

- name: Publish
run: dotnet publish Source/Cli/Cli.csproj -c Release -r osx-arm64 -p:SelfContained=true -p:PublishSingleFile=true -p:Version=${{ inputs.version }} -o ./publish/osx-arm64
run: dotnet publish Source/Cli/Cli.csproj --no-restore -c Release -r osx-arm64 -p:SelfContained=true -p:PublishSingleFile=true -p:Version=${{ inputs.version }} -o ./publish/osx-arm64

- name: Package
run: |
Expand All @@ -55,8 +65,18 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Download Screenplay source-provider packages
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download v0.1.0 --repo Cratis/Screenplay.Generation --pattern '*.nupkg' --dir ./Artifacts/Dependencies
gh release download v0.1.0 --repo Cratis/Screenplay.CritterStack --pattern '*.nupkg' --dir ./Artifacts/Dependencies

- name: Restore
run: dotnet restore Source/Cli/Cli.csproj -r osx-x64 --source ./Artifacts/Dependencies --source https://api.nuget.org/v3/index.json

- name: Publish
run: dotnet publish Source/Cli/Cli.csproj -c Release -r osx-x64 -p:SelfContained=true -p:PublishSingleFile=true -p:Version=${{ inputs.version }} -o ./publish/osx-x64
run: dotnet publish Source/Cli/Cli.csproj --no-restore -c Release -r osx-x64 -p:SelfContained=true -p:PublishSingleFile=true -p:Version=${{ inputs.version }} -o ./publish/osx-x64

- name: Package
run: |
Expand All @@ -82,8 +102,18 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Download Screenplay source-provider packages
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download v0.1.0 --repo Cratis/Screenplay.Generation --pattern '*.nupkg' --dir ./Artifacts/Dependencies
gh release download v0.1.0 --repo Cratis/Screenplay.CritterStack --pattern '*.nupkg' --dir ./Artifacts/Dependencies

- name: Restore
run: dotnet restore Source/Cli/Cli.csproj -r linux-x64 --source ./Artifacts/Dependencies --source https://api.nuget.org/v3/index.json

- name: Publish
run: dotnet publish Source/Cli/Cli.csproj -c Release -r linux-x64 -p:SelfContained=true -p:PublishSingleFile=true -p:Version=${{ inputs.version }} -o ./publish/linux-x64
run: dotnet publish Source/Cli/Cli.csproj --no-restore -c Release -r linux-x64 -p:SelfContained=true -p:PublishSingleFile=true -p:Version=${{ inputs.version }} -o ./publish/linux-x64

- name: Package
run: |
Expand All @@ -109,8 +139,18 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Download Screenplay source-provider packages
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download v0.1.0 --repo Cratis/Screenplay.Generation --pattern '*.nupkg' --dir ./Artifacts/Dependencies
gh release download v0.1.0 --repo Cratis/Screenplay.CritterStack --pattern '*.nupkg' --dir ./Artifacts/Dependencies

- name: Restore
run: dotnet restore Source/Cli/Cli.csproj -r linux-arm64 --source ./Artifacts/Dependencies --source https://api.nuget.org/v3/index.json

- name: Publish
run: dotnet publish Source/Cli/Cli.csproj -c Release -r linux-arm64 -p:SelfContained=true -p:PublishSingleFile=true -p:Version=${{ inputs.version }} -o ./publish/linux-arm64
run: dotnet publish Source/Cli/Cli.csproj --no-restore -c Release -r linux-arm64 -p:SelfContained=true -p:PublishSingleFile=true -p:Version=${{ inputs.version }} -o ./publish/linux-arm64

- name: Package
run: |
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,19 @@ jobs:
- name: Remove any existing artifacts
run: rm -rf ${{ env.NUGET_OUTPUT }}

# Bootstrap until the new package IDs are available from nuget.org.
- name: Download Screenplay source-provider packages
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download v0.1.0 --repo Cratis/Screenplay.Generation --pattern '*.nupkg' --dir ./Artifacts/Dependencies
gh release download v0.1.0 --repo Cratis/Screenplay.CritterStack --pattern '*.nupkg' --dir ./Artifacts/Dependencies

- name: Restore
run: dotnet restore --property:Configuration=Release --source ./Artifacts/Dependencies --source https://api.nuget.org/v3/index.json

- name: Build
run: dotnet build --configuration Release -p:Version=${{ needs.release.outputs.version }}
run: dotnet build --no-restore --configuration Release -p:Version=${{ needs.release.outputs.version }}

- name: Create NuGet packages
run: dotnet pack Source/Cli/Cli.csproj --no-build --configuration Release -o ${{ env.NUGET_OUTPUT }} -p:PackageVersion=${{ needs.release.outputs.version }}
Expand Down
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<ItemGroup>
<!-- Cratis -->
<PackageVersion Include="Cratis.Arc.Screenplay" Version="22.0.0" />
<PackageVersion Include="Cratis.CritterStack.Screenplay" Version="0.1.0" />
<PackageVersion Include="Cratis.Screenplay.Generation.Contracts" Version="0.1.0" />
<PackageVersion Include="Cratis.Screenplay.Generation.DotNet" Version="0.1.0" />
<PackageVersion Include="Cratis.Chronicle.Connections" Version="16.37.0" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="16.37.0" />
<PackageVersion Include="Cratis.Fundamentals" Version="7.18.1" />
Expand Down
16 changes: 10 additions & 6 deletions Documentation/reference/screenplay.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Screenplay

`cratis screenplay` works with Cratis Screenplay (`.play`) documents. It generates one from the source code of a Cratis Arc application — so the event model your team reads is derived from the code that actually runs rather than maintained alongside it — and it compiles the documents you already have.
`cratis screenplay` works with Cratis Screenplay (`.play`) documents. It generates one from Arc, Marten, or Marten + Wolverine application source — so the event model your team reads is derived from the code that actually runs rather than maintained alongside it — and it compiles the documents you already have.

```bash
cratis screenplay generate [PATH]
Expand All @@ -9,11 +9,11 @@ cratis screenplay validate [PATH]

**Nothing needs to be running.** This is what separates `cratis screenplay` from [`cratis arc`](../arc/index.md): every `arc` command talks to a *running* application over HTTP, while `screenplay` only ever reads files. The result is reproducible from a checkout — commit it, diff it, and run it in CI, on a machine where the application was never started.

Fetching a `.play` document from a running Arc application over its introspection endpoint is a separate, complementary route: it trades the SDK requirement for the requirement that the application be running. That route does not exist yet — neither the Arc endpoint nor a CLI command for it — so generating from source is today the only way to derive a Screenplay from a Cratis Arc application.
Fetching a `.play` document from a running application over an introspection endpoint is a separate, complementary route: it trades the SDK requirement for the requirement that the application be running. Source generation remains reproducible from a restored checkout and does not execute application startup or connect to Chronicle/PostgreSQL.

## `cratis screenplay generate [PATH]`

Reads a solution or project, derives the event model from the Arc artifacts it finds — commands, events, read models, projections, reactors, constraints, and the concepts they are built from — and writes a Screenplay document.
Reads a solution or project, selects the Arc, Marten, or Critter Stack source provider, derives the event model from the framework artifacts and conventions it finds, and writes a Screenplay document.

By default the document goes to standard output, so it composes with the shell:

Expand All @@ -34,6 +34,7 @@ Pass `--file` to write it directly instead. The output is written as raw UTF-8,
| Option | Description |
|---|---|
| `--file <FILE>` | File to write the generated Screenplay to. Writes to standard output when not given. |
| `--provider <PROVIDER>` | Source provider: `auto`, `arc`, `marten`, or `critter-stack`. Defaults to auto detection. |
| `--domain <NAME>` | Name of the domain the generated document belongs to. Defaults to the assembly or root namespace of the project, and to the solution name when several projects are read. |
| `--module <NAME>` | Name of the module every discovered feature is placed within. Defaults to the domain. |
| `--skip-segments <COUNT>` | Number of leading namespace segments to skip when inferring features and slices. |
Expand All @@ -45,6 +46,8 @@ The output file uses `--file` rather than `-o`, because `-o/--output` is the glo
cratis screenplay generate
cratis screenplay generate ./MyApp.slnx --file MyApp.play
cratis screenplay generate ./Source/MyApp/MyApp.csproj
cratis screenplay generate ./Banking.csproj --provider marten --file Banking.play
cratis screenplay generate ./Helpdesk.csproj --provider critter-stack --file Helpdesk.play
cratis screenplay generate --domain Library --module Lending --file Library.play
```

Expand Down Expand Up @@ -76,7 +79,7 @@ A solution filter (`.slnf`) is read as the solution it filters, which is how a r

A Screenplay describes one application, and an application is regularly split across several projects — an executable alongside the libraries holding its slices. Every project of a solution therefore takes part in the same document, except:

- **Projects that cannot declare anything the document is made of.** Every artifact is declared with an attribute the framework ships, so a project resolving neither the Arc nor the Chronicle one — a Roslyn analyzer, a build-time tool, a code-generation project — is left out. This is asked of what the project can *see*, not of what it is called.
- **With the Arc provider, projects that cannot declare an Arc/Chronicle artifact.** A Roslyn analyzer, build-time tool, or code-generation project resolving neither framework is left out. Marten/Wolverine contracts are frequently markerless and may live in referenced projects without a direct package reference, so Critter Stack analysis retains non-spec C# projects and lets the provider contribute only evidence it recognizes.
- **Spec projects**, by name: the ones called, or ending in, `.Specs`, `.Specifications`, `.Tests`, `.Test`, `.IntegrationTests`, or `.Specs.AppHost`. Nothing about what a spec project can see tells it apart — it references the same framework the application does — so the name is what decides. `.Specs.AppHost` covers the host integration specs start the application in.

A project that targets several frameworks is read once. The workspace opens it once per target framework and names the results `MyApp(net10.0)`, `MyApp(net9.0)`; they all hold the same application, so one of them takes part.
Expand Down Expand Up @@ -131,7 +134,8 @@ The project does **not** have to have been built first. Sources MSBuild generate
| No solution or project found in `PATH` or any parent folder | Not-found error. |
| The solution holds no project that is not specs | Validation error (`CLI0001`). |
| A project has not been restored | Validation error (`CLI0005`) naming it; nothing is generated. |
| No project of the solution can declare a command or an event type | Validation error (`CLI0006`). |
| No Arc project of the solution can declare a command or event type | Validation error (`CLI0006`). |
| `--provider` is not `auto`, `arc`, `marten`, or `critter-stack` | Validation error (`CLI0007`). |
| A project cannot be read into a compilation | Validation error (`CLI0004`) naming it; the remaining projects are still described. |
| Generation reports one or more errors, with `--file` | Validation error; the document is written anyway. |
| Generation reports one or more errors, writing to standard output | Validation error; nothing is written. |
Expand Down Expand Up @@ -181,7 +185,7 @@ With `-o json` or `-o json-compact` the same diagnostics are written to standard

Generating from source is one of three ways to arrive at a `.play` file, and they meet in the same place:

- **From source** — `screenplay generate`, for an application that already exists in Cratis Arc. Needs the .NET SDK and a checkout; needs nothing running.
- **From source** — `screenplay generate`, for an Arc, Marten, or Critter Stack application. Needs the .NET SDK and a restored checkout; needs nothing running.
- **From a running system** — [`cratis prologue`](prologue.md) captures what a system does and interprets it into a Screenplay, for systems built without Cratis.
- **By hand** — write the `.play` file as the design, before any code exists.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Cratis.Cli.for_CritterStackScreenplayGeneration.given;

public class a_marten_application_built_from_source : Specification
{
protected const string ProjectName = "Banking";

static readonly string Source = string.Join(
'\n',
[
"namespace Marten",
"{",
" public interface IDocumentStore;",
" public class StoreOptions",
" {",
" public Marten.Events.Projections.ProjectionOptions Projections { get; } = new();",
" }",
"}",
"namespace Marten.Events.Projections",
"{",
" public enum SnapshotLifecycle { Inline }",
" public class ProjectionOptions",
" {",
" public void Snapshot<T>(SnapshotLifecycle lifecycle) { }",
" }",
"}",
"namespace Banking",
"{",
" public record AccountOpened(System.Guid AccountId);",
" public class Account",
" {",
" public System.Guid Id { get; set; }",
" public void Apply(AccountOpened opened) { }",
" }",
" public static class Configuration",
" {",
" public static void Configure(Marten.StoreOptions options) =>",
" options.Projections.Snapshot<Account>(Marten.Events.Projections.SnapshotLifecycle.Inline);",
" }",
"}"
]);

protected LoadedCompilation Loaded { get; private set; } = null!;

void Establish() => Loaded = new(
[
CSharpCompilation.Create(
ProjectName,
[CSharpSyntaxTree.ParseText(Source, path: "/workspace/Banking/Account.cs")],
References(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
],
[ProjectName],
[]);

static IEnumerable<MetadataReference> References() =>
((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
.Split(Path.PathSeparator)
.Select(_ => MetadataReference.CreateFromFile(_));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.CodeAnalysis.CSharp;

namespace Cratis.Cli.for_CritterStackScreenplayGeneration.when_generating;

public class and_source_does_not_compile : given.a_marten_application_built_from_source
{
GeneratedScreenplay _result = null!;

void Because()
{
var broken = Loaded.Compilations[0].AddSyntaxTrees(CSharpSyntaxTree.ParseText("public class Broken { MissingType Value; }"));
var loaded = Loaded with { Compilations = [broken] };
_result = CritterStackScreenplayGeneration.GenerateFrom(
loaded,
"/workspace/Banking/Banking.csproj",
ScreenplayGenerationOptions.Default with { Provider = ScreenplayProviders.Marten });
}

[Fact] void should_generate_no_source() => _result.Source.ShouldBeEmpty();
[Fact] void should_report_the_compilation_error() => _result.Diagnostics.Select(_ => _.Code).ShouldContainOnly(ScreenplayDiagnosticCodes.SourceDidNotCompile);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_CritterStackScreenplayGeneration.when_generating;

public class from_marten_source : given.a_marten_application_built_from_source
{
GeneratedScreenplay _result = null!;

void Because() => _result = CritterStackScreenplayGeneration.GenerateFrom(
Loaded,
"/workspace/Banking/Banking.csproj",
ScreenplayGenerationOptions.Default with { Provider = ScreenplayProviders.Marten });

[Fact] void should_report_the_project() => _result.Projects.ShouldContainOnly(ProjectName);
[Fact] void should_generate_the_read_model() => _result.Source.ShouldContain("readmodel Account");
[Fact] void should_generate_the_event() => _result.Source.ShouldContain("event AccountOpened");
[Fact] void should_generate_the_reducer() => _result.Source.ShouldContain("reducer AccountSnapshot => Account");
[Fact] void should_report_no_diagnostics() => _result.Diagnostics.ShouldBeEmpty();
}
Loading
Loading