distributed_cli/lib.rs
1//! The `dsvc` CLI for Distributed services — both a binary and a library.
2//!
3//! It bundles two things in one crate so there is no cross-repo coordination:
4//!
5//! - **Pure generation** (the [`generate`]/[`atlas`] modules): the rules for a
6//! Distributed service project — Cargo layout, Rust source templates, manifest
7//! wiring, GitOps/Knative inference, GitHub workflows, and Atlas schema
8//! resources. These perform no I/O — [`generate_service_scaffold`] takes a
9//! [`ServiceScaffoldSpec`] and returns a [`GeneratedProject`];
10//! [`render_atlas_schema`] wraps desired-state SQL into an `AtlasSchema`.
11//! - **The command surface** (the [`cli`] module): the clap types and [`run`]
12//! dispatcher that own the filesystem / process side effects (writing files,
13//! running `gh`, compiling the manifest harness).
14//!
15//! The `dsvc` binary parses [`ServiceArgs`] and calls [`run`]. Another CLI (e.g.
16//! `hops`) can depend on this crate, mount [`ServiceArgs`] under its own
17//! subcommand, and dispatch with [`run`] — re-exporting the commands rather than
18//! reimplementing them.
19
20mod atlas;
21mod cli;
22mod generate;
23
24pub use atlas::{render_atlas_schema, AtlasDatabaseUrl, AtlasSchemaSpec};
25pub use cli::{
26 run, Bus, DescribeArgs, Framework, GitopsPromote, ManifestFormat, ScaffoldArgs, SchemaArgs,
27 SchemaDialect, SchemaFormat, ServiceArgs, ServiceCommands, Store, Transport,
28};
29pub use generate::{generate_service_scaffold, package_name};
30
31/// What to scaffold. The pure input to [`generate_service_scaffold`].
32///
33/// `name` and the raw `models`/`commands`/`events` strings are normalized by the
34/// generator (kebab/pascal/ident casing, validation, dedup) — that normalization
35/// is part of the rules this crate owns.
36#[derive(Clone, Debug)]
37pub struct ServiceScaffoldSpec {
38 /// Service / package name (free-form; normalized to a kebab package name).
39 pub name: String,
40 /// Runtime transport to scaffold.
41 pub transport: ServiceTransport,
42 /// Read-model / schema storage target.
43 pub store: StoreTarget,
44 /// Optional message bus backend.
45 pub bus: Option<BusTarget>,
46 /// Aggregate model names to scaffold (raw; may be empty).
47 pub models: Vec<String>,
48 /// Generate placeholder read-model modules and register them in the manifest.
49 pub read_models: bool,
50 /// Command handler message names (raw; empty → a default command is derived).
51 pub commands: Vec<String>,
52 /// Event handler message names (raw; may be empty).
53 pub events: Vec<String>,
54 /// Relative path (from the generated project dir) to the local `distributed`
55 /// crate, used in the generated `Cargo.toml` dependency.
56 pub distributed_dependency_path: String,
57 /// Generate a Helm deploy chart under `.gitops/deploy`.
58 pub gitops: bool,
59 /// Generate a GitOps promotion chart for Argo CD or Flux.
60 pub gitops_promote: Option<GitopsPromoteTarget>,
61 /// The service's own GitHub repository: emits the version/release workflows
62 /// and an `EnsureGithubRepository` post-create action.
63 pub github: Option<GithubRepo>,
64 /// Preview-environment GitOps repository: emits the preview workflow and the
65 /// `.gitops/preview/helm` promotion chart. Independent of `github`.
66 pub github_preview: Option<GithubRepo>,
67 /// Permanent-environment GitOps repository: emits the promote workflow and the
68 /// `.gitops/promote/helm` promotion chart. Independent of `github`.
69 pub github_promote: Option<GithubRepo>,
70}
71
72/// Runtime transport for the scaffolded service.
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum ServiceTransport {
75 /// Axum HTTP transport (`microsvc::serve`).
76 Http,
77 /// Knative / CloudEvents HTTP ingress (`cloud_events_router`).
78 Knative,
79}
80
81/// Read-model / schema storage target.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum StoreTarget {
84 /// Postgres-backed persistence (`postgres` feature).
85 Postgres,
86 /// SQLite-backed persistence (`sqlite` feature).
87 Sqlite,
88 /// In-memory only (no persistence feature).
89 InMemory,
90}
91
92/// Message bus backend to scaffold.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum BusTarget {
95 /// RabbitMQ.
96 Rabbitmq,
97 /// Kafka.
98 Kafka,
99 /// Postgres-backed bus.
100 Psql,
101 /// NATS JetStream.
102 Nats,
103}
104
105impl BusTarget {
106 /// The lowercase kind string used in generated env/manifest values.
107 pub fn kind(self) -> &'static str {
108 match self {
109 BusTarget::Rabbitmq => "rabbitmq",
110 BusTarget::Kafka => "kafka",
111 BusTarget::Psql => "psql",
112 BusTarget::Nats => "nats",
113 }
114 }
115}
116
117/// GitOps promotion flavor.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum GitopsPromoteTarget {
120 /// Argo CD `Application`.
121 Argo,
122 /// Flux `HelmRelease`.
123 Flux,
124}
125
126/// An `owner/repo` GitHub identifier.
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct GithubRepo {
129 /// Repository owner (user or org).
130 pub owner: String,
131 /// Repository name.
132 pub repo: String,
133}
134
135impl GithubRepo {
136 /// Parse an `owner/repo` string, validating both halves.
137 pub fn parse(raw: &str) -> Result<Self, ScaffoldError> {
138 generate::parse_github_repo(raw)
139 }
140
141 /// `owner/repo`.
142 pub fn slug(&self) -> String {
143 format!("{}/{}", self.owner, self.repo)
144 }
145}
146
147/// The result of generating a scaffold: the files to write, advisory warnings,
148/// and side effects for the caller to perform. Filesystem-agnostic.
149#[derive(Clone, Debug, Default)]
150pub struct GeneratedProject {
151 /// Files to write, with paths relative to the project directory.
152 pub files: Vec<GeneratedFile>,
153 /// Non-fatal advisory messages (e.g. a requested feature not yet generated).
154 pub warnings: Vec<String>,
155 /// Side effects the caller should perform after writing files.
156 pub post_create_actions: Vec<PostCreateAction>,
157}
158
159/// A single generated file: a relative path and its contents.
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct GeneratedFile {
162 /// Path relative to the project directory (forward slashes).
163 pub path: String,
164 /// File contents.
165 pub contents: String,
166 /// Optional file mode hint (e.g. executable). `None` = default text file.
167 pub mode: Option<FileMode>,
168}
169
170/// File mode hint for a [`GeneratedFile`].
171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub enum FileMode {
173 /// The file should be marked executable.
174 Executable,
175}
176
177/// A side effect the caller should perform after writing the generated files.
178#[derive(Clone, Debug, PartialEq, Eq)]
179pub enum PostCreateAction {
180 /// Ensure the GitHub repository exists (e.g. `gh repo view` / `gh repo create`).
181 EnsureGithubRepository {
182 /// The repository to ensure.
183 repo: GithubRepo,
184 },
185}
186
187/// A scaffold generation error (bad spec value).
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct ScaffoldError(pub String);
190
191impl std::fmt::Display for ScaffoldError {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.write_str(&self.0)
194 }
195}
196
197impl std::error::Error for ScaffoldError {}
198
199impl ScaffoldError {
200 pub(crate) fn new(message: impl Into<String>) -> Self {
201 Self(message.into())
202 }
203}