use clap::ValueEnum;
pub type ManifestFile = (&'static str, &'static str);
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum Template {
HelloWorld,
ApprovalFlow,
Saga,
DevPipeline,
}
const SHARED_FILES: &[ManifestFile] = &[
(
"gleam.toml",
include_str!("../../templates/shared/gleam.toml"),
),
(
".gitignore",
include_str!("../../templates/shared/gitignore"),
),
(
"aion.toml",
include_str!("../../templates/shared/aion.toml"),
),
];
const SHARED_FILES_WITHOUT_GLEAM_TOML: &[ManifestFile] = &[
(
".gitignore",
include_str!("../../templates/shared/gitignore"),
),
(
"aion.toml",
include_str!("../../templates/shared/aion.toml"),
),
];
const HELLO_WORLD_FILES: &[ManifestFile] = &[
(
"workflow.toml",
include_str!("../../templates/hello_world/workflow.toml"),
),
(
"schemas/input.json",
include_str!("../../templates/hello_world/schemas/input.json"),
),
(
"schemas/output.json",
include_str!("../../templates/hello_world/schemas/output.json"),
),
(
"src/{{name}}.gleam",
include_str!("../../templates/hello_world/project.gleam"),
),
(
"README.md",
include_str!("../../templates/hello_world/README.md"),
),
];
const APPROVAL_FLOW_FILES: &[ManifestFile] = &[
(
"workflow.toml",
include_str!("../../templates/approval_flow/workflow.toml"),
),
(
"schemas/input.json",
include_str!("../../templates/approval_flow/schemas/input.json"),
),
(
"schemas/output.json",
include_str!("../../templates/approval_flow/schemas/output.json"),
),
(
"src/{{name}}.gleam",
include_str!("../../templates/approval_flow/project.gleam"),
),
(
"README.md",
include_str!("../../templates/approval_flow/README.md"),
),
];
const SAGA_FILES: &[ManifestFile] = &[
(
"workflow.toml",
include_str!("../../templates/saga/workflow.toml"),
),
(
"schemas/input.json",
include_str!("../../templates/saga/schemas/input.json"),
),
(
"schemas/output.json",
include_str!("../../templates/saga/schemas/output.json"),
),
(
"src/{{name}}.gleam",
include_str!("../../templates/saga/project.gleam"),
),
("README.md", include_str!("../../templates/saga/README.md")),
];
const SAGA_WORKER_FILES: &[ManifestFile] = &[
(
"worker/Cargo.toml",
include_str!("../../templates/saga/worker/Cargo.toml.tmpl"),
),
(
"worker/src/main.rs",
include_str!("../../templates/saga/worker/main.rs"),
),
];
const DEV_PIPELINE_FILES: &[ManifestFile] = &[
(
"gleam.toml",
include_str!("../../templates/dev_pipeline/gleam.toml"),
),
(
"workflow.toml",
include_str!("../../templates/dev_pipeline/workflow.toml"),
),
(
"schemas/input.json",
include_str!("../../templates/dev_pipeline/schemas/input.json"),
),
(
"schemas/output.json",
include_str!("../../templates/dev_pipeline/schemas/output.json"),
),
(
"schemas/dev_input.json",
include_str!("../../templates/dev_pipeline/schemas/dev_input.json"),
),
(
"schemas/dev_output.json",
include_str!("../../templates/dev_pipeline/schemas/dev_output.json"),
),
(
"schemas/gate_input.json",
include_str!("../../templates/dev_pipeline/schemas/gate_input.json"),
),
(
"schemas/gate_output.json",
include_str!("../../templates/dev_pipeline/schemas/gate_output.json"),
),
(
"src/{{name}}.gleam",
include_str!("../../templates/dev_pipeline/project.gleam"),
),
(
"src/{{name}}_dev.gleam",
include_str!("../../templates/dev_pipeline/project_dev.gleam"),
),
(
"src/{{name}}_gate.gleam",
include_str!("../../templates/dev_pipeline/project_gate.gleam"),
),
(
"src/{{name}}_cli_ffi.erl",
include_str!("../../templates/dev_pipeline/cli_ffi.erl"),
),
(
"src/{{name}}/types.gleam",
include_str!("../../templates/dev_pipeline/support/types.gleam"),
),
(
"src/{{name}}/codecs_core.gleam",
include_str!("../../templates/dev_pipeline/support/codecs_core.gleam"),
),
(
"src/{{name}}/codecs_flow.gleam",
include_str!("../../templates/dev_pipeline/support/codecs_flow.gleam"),
),
(
"src/{{name}}/codecs_workflows.gleam",
include_str!("../../templates/dev_pipeline/support/codecs_workflows.gleam"),
),
(
"src/{{name}}/io_convert.gleam",
include_str!("../../templates/dev_pipeline/support/io_convert.gleam"),
),
(
"src/{{name}}/activities.gleam",
include_str!("../../templates/dev_pipeline/support/activities.gleam"),
),
(
"src/{{name}}/locals.gleam",
include_str!("../../templates/dev_pipeline/support/locals.gleam"),
),
(
"src/{{name}}/cli.gleam",
include_str!("../../templates/dev_pipeline/support/cli.gleam"),
),
(
"src/{{name}}/errors.gleam",
include_str!("../../templates/dev_pipeline/support/errors.gleam"),
),
(
"test/{{name}}_test.gleam",
include_str!("../../templates/dev_pipeline/test/project_test.gleam"),
),
(
"test/{{name}}_test_ffi.erl",
include_str!("../../templates/dev_pipeline/test/test_ffi.erl"),
),
(
"test/support/shims.gleam",
include_str!("../../templates/dev_pipeline/test/shims.gleam"),
),
(
"README.md",
include_str!("../../templates/dev_pipeline/README.md"),
),
];
const DEV_PIPELINE_WORKER_FILES: &[ManifestFile] = &[
(
"worker/Cargo.toml",
include_str!("../../templates/dev_pipeline/worker/Cargo.toml.tmpl"),
),
(
"worker/src/main.rs",
include_str!("../../templates/dev_pipeline/worker/main.rs"),
),
(
"worker/src/lib.rs",
include_str!("../../templates/dev_pipeline/worker/lib.rs"),
),
(
"worker/src/types.rs",
include_str!("../../templates/dev_pipeline/worker/types.rs"),
),
(
"worker/src/handlers.rs",
include_str!("../../templates/dev_pipeline/worker/handlers.rs"),
),
(
"worker/src/shell.rs",
include_str!("../../templates/dev_pipeline/worker/shell.rs"),
),
(
"worker/tests/wire_compat.rs",
include_str!("../../templates/dev_pipeline/worker/tests/wire_compat.rs"),
),
(
"worker/tests/handlers_shims.rs",
include_str!("../../templates/dev_pipeline/worker/tests/handlers_shims.rs"),
),
];
impl Template {
#[must_use]
pub fn id(self) -> &'static str {
match self {
Self::HelloWorld => "hello-world",
Self::ApprovalFlow => "approval-flow",
Self::Saga => "saga",
Self::DevPipeline => "dev-pipeline",
}
}
#[must_use]
pub fn files(self) -> Vec<ManifestFile> {
let (shared, own): (&[ManifestFile], &[ManifestFile]) = match self {
Self::HelloWorld => (SHARED_FILES, HELLO_WORLD_FILES),
Self::ApprovalFlow => (SHARED_FILES, APPROVAL_FLOW_FILES),
Self::Saga => (SHARED_FILES, SAGA_FILES),
Self::DevPipeline => (SHARED_FILES_WITHOUT_GLEAM_TOML, DEV_PIPELINE_FILES),
};
let mut files = shared.to_vec();
files.extend_from_slice(own);
files
}
#[must_use]
pub fn activities(self) -> &'static [&'static str] {
match self {
Self::HelloWorld | Self::ApprovalFlow => &[],
Self::Saga => &["charge_payment", "refund_payment"],
Self::DevPipeline => &[
"provision_workspace",
"warm_build",
"dev",
"scoped_checks",
"dev_resume",
"full_checks",
"request_review",
"land",
],
}
}
#[must_use]
pub fn worker_files(self) -> &'static [ManifestFile] {
match self {
Self::HelloWorld | Self::ApprovalFlow => &[],
Self::Saga => SAGA_WORKER_FILES,
Self::DevPipeline => DEV_PIPELINE_WORKER_FILES,
}
}
#[must_use]
pub fn requires_worker(self) -> bool {
match self {
Self::HelloWorld | Self::ApprovalFlow | Self::Saga => false,
Self::DevPipeline => true,
}
}
#[must_use]
pub fn generates_codecs(self) -> bool {
match self {
Self::HelloWorld | Self::ApprovalFlow | Self::Saga => false,
Self::DevPipeline => true,
}
}
#[cfg(test)]
pub fn all() -> &'static [Self] {
&[
Self::HelloWorld,
Self::ApprovalFlow,
Self::Saga,
Self::DevPipeline,
]
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::Template;
const REQUIRED_PATHS: &[&str] = &[
"gleam.toml",
".gitignore",
"aion.toml",
"workflow.toml",
"schemas/input.json",
"schemas/output.json",
"src/{{name}}.gleam",
"README.md",
];
#[test]
fn every_template_manifest_is_complete() {
for template in Template::all() {
let files = template.files();
let paths: Vec<&str> = files.iter().map(|(path, _)| *path).collect();
for required in REQUIRED_PATHS {
assert!(
paths.contains(required),
"template {} is missing {required}",
template.id()
);
}
let unique: HashSet<&str> = paths.iter().copied().collect();
assert_eq!(
unique.len(),
paths.len(),
"template {} declares duplicate paths",
template.id()
);
for (path, contents) in &files {
assert!(
!contents.trim().is_empty(),
"template {} embeds empty contents for {path}",
template.id()
);
}
}
}
#[test]
fn workflow_descriptors_declare_the_template_activities() {
for template in Template::all() {
let files = template.files();
let workflow_toml = files
.iter()
.find(|(path, _)| *path == "workflow.toml")
.map(|(_, contents)| *contents)
.unwrap_or_default();
for activity in template.activities() {
assert!(
workflow_toml.contains(&format!("\"{activity}\"")),
"template {} workflow.toml does not declare activity {activity}",
template.id()
);
}
assert!(
workflow_toml.contains("entry_module = \"{{name}}\""),
"template {} workflow.toml must name the project module as entry",
template.id()
);
}
}
#[test]
fn aion_toml_carries_every_required_server_key() {
for template in Template::all() {
let files = template.files();
let aion_toml = files
.iter()
.find(|(path, _)| *path == "aion.toml")
.map(|(_, contents)| *contents)
.unwrap_or_default();
for key in [
"listen_address",
"grpc_address",
"backend = \"libsql\"",
"query_timeout_ms",
"event_broadcast_capacity",
"enabled = true",
"max_archive_bytes",
"max_inflated_bytes",
] {
assert!(
aion_toml.contains(key),
"template {} aion.toml is missing {key}",
template.id()
);
}
}
}
#[test]
fn dev_pipeline_declares_three_workflow_entries_and_its_gates() {
let files = Template::DevPipeline.files();
let workflow_toml = files
.iter()
.find(|(path, _)| *path == "workflow.toml")
.map(|(_, contents)| *contents)
.unwrap_or_default();
for entry in [
"entry_module = \"{{name}}\"",
"entry_module = \"{{name}}_dev\"",
"entry_module = \"{{name}}_gate\"",
] {
assert!(
workflow_toml.contains(entry),
"dev-pipeline workflow.toml must declare {entry}"
);
}
for timeout in [
"timeout_seconds = 604800",
"timeout_seconds = 86400",
"timeout_seconds = 21600",
] {
assert!(
workflow_toml.contains(timeout),
"dev-pipeline workflow.toml must keep the documented {timeout}"
);
}
assert!(Template::DevPipeline.requires_worker());
assert!(Template::DevPipeline.generates_codecs());
}
#[test]
fn only_the_dev_pipeline_requires_a_worker_or_codegen() {
for template in [Template::HelloWorld, Template::ApprovalFlow, Template::Saga] {
assert!(
!template.requires_worker(),
"template {} must not require a worker",
template.id()
);
assert!(
!template.generates_codecs(),
"template {} must not run codegen",
template.id()
);
}
}
#[test]
fn dev_pipeline_schemas_avoid_codegen_rejected_constructs() {
for (path, contents) in Template::DevPipeline.files() {
if path.starts_with("schemas/") {
for forbidden in ["$ref", "$defs", "oneOf", "anyOf", "allOf"] {
assert!(
!contents.contains(forbidden),
"{path} must not use {forbidden}: aion codegen rejects it"
);
}
}
}
}
#[test]
fn worker_manifests_exist_exactly_for_templates_with_activities() {
for template in Template::all() {
let has_worker = !template.worker_files().is_empty();
let has_activities = !template.activities().is_empty();
assert_eq!(
has_worker,
has_activities,
"template {} worker manifest must match its activity surface",
template.id()
);
let worker_files = template.worker_files();
if !worker_files.is_empty() {
let paths: Vec<&str> = worker_files.iter().map(|(path, _)| *path).collect();
assert!(paths.contains(&"worker/Cargo.toml"));
assert!(paths.contains(&"worker/src/main.rs"));
let main = worker_files
.iter()
.find(|(path, _)| *path == "worker/src/main.rs")
.map(|(_, contents)| *contents)
.unwrap_or_default();
let condensed: String = main.split_whitespace().collect();
for activity in template.activities() {
assert!(
condensed.contains(&format!("register_activity(\"{activity}\"")),
"template {} worker must register {activity}",
template.id()
);
}
}
}
}
}