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}