Skip to main content

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}