Skip to main content

aion_package/codegen/awl_worker/
scaffold.rs

1//! The generator entry point: one compiled worker contract in, one Cargo
2//! crate's worth of source out.
3//!
4//! Nothing here reads or writes the filesystem. The whole file set is
5//! rendered before the caller touches a directory, so a refusal leaves the
6//! author's tree exactly as it was.
7
8use crate::contract::WorkerContract;
9
10use super::error::AwlScaffoldError;
11use super::plan::{ConnectionPlan, plan};
12use super::{declaration_rs, handlers_rs, main_rs, manifest};
13
14/// Where the generated crate takes the aion SDK crates from.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum AionDependency {
17    /// The published crates at one exact version.
18    ///
19    /// The version is the generating `aion` binary's own: it is the only
20    /// version whose SDK surface the emitted code was written against, so
21    /// pinning anything else would be a guess about an API the generator
22    /// never saw.
23    Version(String),
24    /// A local checkout's `crates/` directory, as the generated crate's
25    /// `Cargo.toml` should spell it (relative to the crate root, or
26    /// absolute).
27    ///
28    /// This is what an author working inside the aion tree needs, and what
29    /// makes a generated crate compile against an SDK that is not published
30    /// yet.
31    Path(String),
32}
33
34/// The directory the generated worker resolves the document's `schema("…")`
35/// imports against.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum DocumentRoot {
38    /// A path relative to the generated crate root, emitted as a
39    /// `CARGO_MANIFEST_DIR` concatenation so the crate finds its document
40    /// wherever the tree is checked out.
41    InCrateTree(String),
42    /// An absolute path, emitted verbatim — for a document in a tree the
43    /// generated crate shares nothing but the filesystem root with, where no
44    /// relative path is meaningful.
45    Absolute(String),
46}
47
48impl DocumentRoot {
49    /// The path text, whichever form it takes.
50    #[must_use]
51    pub fn text(&self) -> &str {
52        match self {
53            Self::InCrateTree(path) | Self::Absolute(path) => path,
54        }
55    }
56}
57
58/// Who owns a generated file once it exists.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum FileOwnership {
61    /// Regenerated from the document every time; a hand-edit is overwritten.
62    Generated,
63    /// Written ONCE and never rewritten — the author's file from then on.
64    Author,
65}
66
67/// One file of the scaffold: where it goes, what is in it, and who owns it.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct ScaffoldedFile {
70    /// Path relative to the generated crate's root.
71    pub relative: String,
72    /// The fully rendered contents.
73    pub contents: String,
74    /// Whether re-running the scaffold rewrites this file.
75    pub ownership: FileOwnership,
76}
77
78/// What to generate: one queue's compiled contract, plus how the emitted
79/// crate reaches the document and the SDK.
80#[derive(Debug, Clone)]
81pub struct AwlWorkerScaffold<'a> {
82    /// The compiled `worker` contract — the ONE source of the schemas the
83    /// generated worker advertises.
84    pub contract: &'a WorkerContract,
85    /// Cargo package (and binary) name for the generated crate.
86    pub crate_name: &'a str,
87    /// The document's path as `src/declaration.rs` must spell it for
88    /// `include_str!` — relative to that file, using `/` separators.
89    pub document_include: &'a str,
90    /// The document's DIRECTORY, which the emitted code uses to resolve the
91    /// document's `schema("…")` imports exactly as the deploy path does.
92    pub document_directory: &'a DocumentRoot,
93    /// The document's file name, for the generated documentation.
94    pub document_name: &'a str,
95    /// Where the generated `Cargo.toml` takes the aion crates from.
96    pub dependencies: &'a AionDependency,
97}
98
99/// A rendered scaffold: the connection plan it was derived from, and the
100/// files that realise it.
101#[derive(Debug, Clone)]
102pub struct WorkerScaffold {
103    /// The plan — one connection per node — the emitted crate implements.
104    pub plan: ConnectionPlan,
105    /// Every file to write, in a deterministic order.
106    pub files: Vec<ScaffoldedFile>,
107}
108
109/// Renders the worker scaffold for one compiled queue contract.
110///
111/// The emitted crate advertises a wire descriptor for every action it
112/// registers, opens one connection per node, omits every server-executed
113/// bodied action, and fails each un-implemented activity loudly. All four are
114/// the difference between a skeleton that serves a queue and one that is
115/// refused on the dial.
116///
117/// # Errors
118///
119/// Returns an [`AwlScaffoldError`] when the queue has no action for an
120/// out-of-band worker, when an action cannot name a Rust function, when the
121/// crate name or document paths are unusable, or when a declared schema
122/// cannot be rendered into the handler documentation.
123pub fn scaffold_awl_worker(
124    request: &AwlWorkerScaffold<'_>,
125) -> Result<WorkerScaffold, AwlScaffoldError> {
126    validate_crate_name(request.crate_name)?;
127    validate_document_path(request.document_include, "document")?;
128    validate_document_path(request.document_directory.text(), "directory")?;
129    let plan = plan(request.contract)?;
130    let files = vec![
131        ScaffoldedFile {
132            relative: "Cargo.toml".to_owned(),
133            contents: manifest::emit(request, &plan),
134            ownership: FileOwnership::Generated,
135        },
136        ScaffoldedFile {
137            relative: "src/main.rs".to_owned(),
138            contents: main_rs::emit(request, &plan),
139            ownership: FileOwnership::Generated,
140        },
141        ScaffoldedFile {
142            relative: "src/declaration.rs".to_owned(),
143            contents: declaration_rs::emit(request, &plan),
144            ownership: FileOwnership::Generated,
145        },
146        ScaffoldedFile {
147            relative: "src/handlers.rs".to_owned(),
148            contents: handlers_rs::emit(&plan)?,
149            ownership: FileOwnership::Author,
150        },
151    ];
152    Ok(WorkerScaffold { plan, files })
153}
154
155/// Refuses a package name Cargo would not accept, before it reaches a
156/// `Cargo.toml` that fails to parse.
157fn validate_crate_name(crate_name: &str) -> Result<(), AwlScaffoldError> {
158    let usable = !crate_name.is_empty()
159        && crate_name.chars().all(|character| {
160            character.is_ascii_alphanumeric() || character == '-' || character == '_'
161        });
162    if usable {
163        return Ok(());
164    }
165    Err(AwlScaffoldError::CrateNameInvalid {
166        crate_name: crate_name.to_owned(),
167    })
168}
169
170/// Refuses an empty document path: `include_str!("")` and a manifest dir with
171/// nothing appended both fail at build time, far from the cause.
172fn validate_document_path(path: &str, role: &'static str) -> Result<(), AwlScaffoldError> {
173    if path.is_empty() {
174        return Err(AwlScaffoldError::DocumentPathEmpty { role });
175    }
176    Ok(())
177}