use std::fmt::Write as _;
use super::plan::ConnectionPlan;
use super::scaffold::{AwlWorkerScaffold, DocumentRoot};
use super::text::string_literal;
pub(super) fn emit(request: &AwlWorkerScaffold<'_>, plan: &ConnectionPlan) -> String {
let mut out = String::new();
emit_header(&mut out, request);
emit_constants(&mut out, request, plan);
emit_error(&mut out);
emit_compile(&mut out);
emit_descriptor(&mut out);
emit_service_guard(&mut out);
emit_select_worker(&mut out);
out
}
fn emit_header(out: &mut String, request: &AwlWorkerScaffold<'_>) {
let _ = write!(
out,
"//! Generated by `aion awl scaffold` from `{document}` — do not edit; regenerate\n\
//! from the document.\n\
//!\n\
//! THE AWL DOCUMENT as this worker's one source of advertisement truth.\n\
//!\n\
//! WHAT THIS EXISTS FOR. A worker that registers a handler and no wire DESCRIPTOR\n\
//! advertises nothing, and a queue carrying any deployed contract refuses it: the\n\
//! server's admission gate (`aion_package::contract_diffs`) demands, for every\n\
//! bodyless action a dispatch could reach the connection with, an advertised\n\
//! descriptor whose input schema accepts everything the workflow may send and whose\n\
//! output schema is decodable by the workflow. An unadvertised action is reported as\n\
//! `<missing>` and the whole connection is rejected with `WORKER_CONTRACT_MISMATCH`.\n\
//!\n\
//! WHY THE DOCUMENT AND NOT THE RUST TYPES. The schemas the server admits against\n\
//! are the ones `aion_awl::compile` derived from `{document}` and committed into\n\
//! package identity. Deriving the advertisement from the handlers' Rust types\n\
//! instead (with `schemars`) would produce a SECOND, independent rendering of the\n\
//! same shapes — and the contract gate exists precisely to catch the drift between\n\
//! two such renderings. Sourcing the advertisement from the compiled contract makes\n\
//! the drift structurally impossible: the bytes advertised are the bytes deployed.\n\
//!\n\
//! The document is embedded with `include_str!`, so the binary carries the\n\
//! declaration it was built from and cannot be run against a document it never saw.\n\
\n",
document = request.document_name,
);
}
fn emit_constants(out: &mut String, request: &AwlWorkerScaffold<'_>, plan: &ConnectionPlan) {
out.push_str(
"use std::collections::BTreeSet;\n\
use std::path::Path;\n\
\n\
use aion_package::{ActivityDescriptor, PackageContract, WorkerContract};\n\
\n",
);
let _ = write!(
out,
"/// The document, embedded at compile time. This binary advertises what THIS text\n\
/// declares — nothing else.\n\
const DOCUMENT: &str = include_str!({include});\n\
\n\
/// The document's own directory, as it stood when the scaffold was generated.\n\
/// `aion_awl::compile` resolves a document's `schema(\"…\")` imports relative to it,\n\
/// so this must be the SAME root the deploy path uses or the compiled schemas would\n\
/// differ from the deployed ones.\n\
const DOCUMENT_DIR: &str = {directory};\n\
\n\
/// The one task queue every action of this worker is dispatched on — the document's\n\
/// `worker` block name, which the deploy path turns into the task queue.\n\
pub const TASK_QUEUE: &str = {queue};\n\
\n",
include = string_literal(request.document_include),
directory = document_root_expression(request.document_directory),
queue = string_literal(&plan.task_queue),
);
}
fn document_root_expression(root: &DocumentRoot) -> String {
match root {
DocumentRoot::InCrateTree(path) => format!(
"concat!(env!(\"CARGO_MANIFEST_DIR\"), {})",
string_literal(&format!("/{path}"))
),
DocumentRoot::Absolute(path) => string_literal(path),
}
}
fn emit_error(out: &mut String) {
out.push_str(
"/// Why this binary cannot derive what it advertises.\n\
///\n\
/// Every variant is a STARTUP failure. A worker that cannot prove what it serves\n\
/// must refuse to start rather than dial a connection the server will reject, or\n\
/// leave a declared dispatch parked forever.\n\
#[derive(Debug, thiserror::Error)]\n\
pub enum DeclarationError {\n\
\x20 /// The embedded document does not compile.\n\
\x20 #[error(\"the embedded AWL document does not compile: {reason}\")]\n\
\x20 Compile {\n\
\x20 /// The compiler's own diagnostics.\n\
\x20 reason: String,\n\
\x20 },\n\
\x20 /// The document declares no worker block of this queue's name.\n\
\x20 #[error(\n\
\x20 \"the compiled document declares no `worker {TASK_QUEUE}` block, so this binary \\\n\
\x20 has no declared action to advertise; it declares: [{declared}]\"\n\
\x20 )]\n\
\x20 NoSuchWorker {\n\
\x20 /// The worker blocks the document DOES declare.\n\
\x20 declared: String,\n\
\x20 },\n\
\x20 /// This binary registers a handler the document does not declare.\n\
\x20 #[error(\n\
\x20 \"this binary registers a handler for activity `{action}`, which the document's \\\n\
\x20 `worker {TASK_QUEUE}` block does not declare; it declares: [{declared}]\"\n\
\x20 )]\n\
\x20 UndeclaredAction {\n\
\x20 /// The activity registered with no declaration behind it.\n\
\x20 action: String,\n\
\x20 /// The actions the document DOES declare.\n\
\x20 declared: String,\n\
\x20 },\n\
\x20 /// This binary registers a handler on a node no dispatch can reach.\n\
\x20 #[error(\n\
\x20 \"this binary registers a handler for activity `{action}` on {registered}, but \\\n\
\x20 the document pins that action to node `{pin}`; the server routes by \\\n\
\x20 (namespace, task_queue, node) alone, so no dispatch could ever reach the \\\n\
\x20 handler\"\n\
\x20 )]\n\
\x20 Unreachable {\n\
\x20 /// The activity whose handler is unreachable.\n\
\x20 action: String,\n\
\x20 /// The locality the connection registers.\n\
\x20 registered: String,\n\
\x20 /// The node the document pins the action to.\n\
\x20 pin: String,\n\
\x20 },\n\
\x20 /// A declared action needing an out-of-band worker that this binary does not\n\
\x20 /// serve.\n\
\x20 #[error(\n\
\x20 \"the document declares {count} action(s) with no body — each REQUIRES an \\\n\
\x20 out-of-band worker — that this binary serves no handler for: {missing}. The \\\n\
\x20 queue's contract cannot be served by this build, so it refuses to start \\\n\
\x20 rather than leave those dispatches parked forever.\"\n\
\x20 )]\n\
\x20 Unserved {\n\
\x20 /// How many declared actions are unserved.\n\
\x20 count: usize,\n\
\x20 /// Each unserved action AND its declared node — the node is what tells the\n\
\x20 /// operator which connection owed it.\n\
\x20 missing: String,\n\
\x20 },\n\
}\n\
\n",
);
}
fn emit_compile(out: &mut String) {
out.push_str(
"/// The compiled worker contract: every action the document declares, with the\n\
/// schemas and the node pin the server admits against.\n\
#[derive(Clone, Debug)]\n\
pub struct Declaration {\n\
\x20 contract: WorkerContract,\n\
}\n\
\n\
impl Declaration {\n\
\x20 /// Compiles the embedded document and takes this queue's worker contract.\n\
\x20 ///\n\
\x20 /// This is the production path, run ONCE at startup: everything the worker\n\
\x20 /// advertises is derived from the value it returns.\n\
\x20 ///\n\
\x20 /// # Errors\n\
\x20 ///\n\
\x20 /// [`DeclarationError::Compile`] when the embedded document does not compile,\n\
\x20 /// or [`DeclarationError::NoSuchWorker`] when it declares no worker block of\n\
\x20 /// this queue's name.\n\
\x20 pub fn compile() -> Result<Self, DeclarationError> {\n\
\x20 let compiled = aion_awl::compile(DOCUMENT, Path::new(DOCUMENT_DIR)).map_err(|error| {\n\
\x20 DeclarationError::Compile {\n\
\x20 reason: error.to_string(),\n\
\x20 }\n\
\x20 })?;\n\
\x20 Ok(Self {\n\
\x20 contract: select_worker(compiled.contract)?,\n\
\x20 })\n\
\x20 }\n\
\n\
\x20 /// The compiled contract, exactly as the server admits against it.\n\
\x20 #[must_use]\n\
\x20 pub const fn contract(&self) -> &WorkerContract {\n\
\x20 &self.contract\n\
\x20 }\n\
\n",
);
}
fn emit_descriptor(out: &mut String) {
out.push_str(
"\x20 /// The DECLARED wire descriptor for one action a connection on `node`\n\
\x20 /// registers a handler for.\n\
\x20 ///\n\
\x20 /// # Errors\n\
\x20 ///\n\
\x20 /// [`DeclarationError::UndeclaredAction`] when the document declares no action\n\
\x20 /// of that name, or [`DeclarationError::Unreachable`] when it declares one\n\
\x20 /// pinned to a DIFFERENT node — a handler registered on a node the action is\n\
\x20 /// not pinned to can never be dispatched to, because the server routes by\n\
\x20 /// (namespace, `task_queue`, node) alone.\n\
\x20 pub fn descriptor(\n\
\x20 &self,\n\
\x20 action: &str,\n\
\x20 node: Option<&str>,\n\
\x20 ) -> Result<ActivityDescriptor, DeclarationError> {\n\
\x20 let declared = self\n\
\x20 .contract\n\
\x20 .actions\n\
\x20 .iter()\n\
\x20 .find(|declared| declared.name == action)\n\
\x20 .ok_or_else(|| DeclarationError::UndeclaredAction {\n\
\x20 action: action.to_owned(),\n\
\x20 declared: self.declared_action_names(),\n\
\x20 })?;\n\
\x20 if let Some(pin) = declared.node.as_deref()\n\
\x20 && node != Some(pin)\n\
\x20 {\n\
\x20 return Err(DeclarationError::Unreachable {\n\
\x20 action: action.to_owned(),\n\
\x20 registered: node.map_or_else(\n\
\x20 || \"a connection carrying no node\".to_owned(),\n\
\x20 |node| format!(\"node `{node}`\"),\n\
\x20 ),\n\
\x20 pin: pin.to_owned(),\n\
\x20 });\n\
\x20 }\n\
\x20 Ok(ActivityDescriptor {\n\
\x20 name: declared.name.clone(),\n\
\x20 input_schema: declared.input_schema.clone(),\n\
\x20 output_schema: declared.output_schema.clone(),\n\
\x20 })\n\
\x20 }\n\
\n",
);
}
fn emit_service_guard(out: &mut String) {
out.push_str(
"\x20 /// Requires every declared action that NEEDS an out-of-band worker to be\n\
\x20 /// served by this binary.\n\
\x20 ///\n\
\x20 /// An action whose declaration carries a `body` is executed by the SERVER\n\
\x20 /// itself, so no worker serves it and none is demanded here — the same\n\
\x20 /// `ActionContract::worker_owed` exclusion `aion_package::contract_diffs`\n\
\x20 /// applies before deciding what a connection owes.\n\
\x20 ///\n\
\x20 /// This is what catches a document that GREW an action after the scaffold was\n\
\x20 /// generated: the new action is unserved, and the worker says so at startup\n\
\x20 /// instead of leaving its dispatches parked.\n\
\x20 ///\n\
\x20 /// # Errors\n\
\x20 ///\n\
\x20 /// [`DeclarationError::Unserved`], naming every unserved bodyless action AND\n\
\x20 /// its declared node.\n\
\x20 pub fn require_every_bodyless_action_served(\n\
\x20 &self,\n\
\x20 served: &BTreeSet<String>,\n\
\x20 ) -> Result<(), DeclarationError> {\n\
\x20 let missing = self\n\
\x20 .contract\n\
\x20 .actions\n\
\x20 .iter()\n\
\x20 .filter(|action| action.worker_owed())\n\
\x20 .filter(|action| !served.contains(&action.name))\n\
\x20 .map(|action| {\n\
\x20 format!(\n\
\x20 \"`{}` (declared on node {})\",\n\
\x20 action.name,\n\
\x20 action.node.as_deref().map_or_else(\n\
\x20 || \"any — the action is unpinned\".to_owned(),\n\
\x20 |node| format!(\"`{node}`\"),\n\
\x20 )\n\
\x20 )\n\
\x20 })\n\
\x20 .collect::<Vec<_>>();\n\
\x20 if missing.is_empty() {\n\
\x20 return Ok(());\n\
\x20 }\n\
\x20 Err(DeclarationError::Unserved {\n\
\x20 count: missing.len(),\n\
\x20 missing: missing.join(\", \"),\n\
\x20 })\n\
\x20 }\n\
\n\
\x20 /// The declared action names, quoted, in declaration order.\n\
\x20 fn declared_action_names(&self) -> String {\n\
\x20 self.contract\n\
\x20 .actions\n\
\x20 .iter()\n\
\x20 .map(|action| format!(\"`{}`\", action.name))\n\
\x20 .collect::<Vec<_>>()\n\
\x20 .join(\", \")\n\
\x20 }\n\
}\n\
\n",
);
}
fn emit_select_worker(out: &mut String) {
out.push_str(
"/// Takes this queue's worker contract out of a compiled package contract.\n\
///\n\
/// # Errors\n\
///\n\
/// [`DeclarationError::NoSuchWorker`], naming every worker the contract DOES\n\
/// declare — a worker with nothing to advertise must refuse to start, not start\n\
/// advertising nothing.\n\
fn select_worker(contract: PackageContract) -> Result<WorkerContract, DeclarationError> {\n\
\x20 let declared = contract\n\
\x20 .workers\n\
\x20 .iter()\n\
\x20 .map(|worker| format!(\"`{}`\", worker.task_queue))\n\
\x20 .collect::<Vec<_>>()\n\
\x20 .join(\", \");\n\
\x20 contract\n\
\x20 .workers\n\
\x20 .into_iter()\n\
\x20 .find(|worker| worker.task_queue == TASK_QUEUE)\n\
\x20 .ok_or(DeclarationError::NoSuchWorker { declared })\n\
}\n",
);
}