aion-package 0.13.1

Archive validation, content hashing, and namespacing for Aion workflow packages.
Documentation
//! Emission of `src/declaration.rs` — the generated worker's one source of
//! advertisement truth.
//!
//! The emitted module embeds the AWL document with `include_str!`, compiles it
//! once at startup, and derives every advertised descriptor from the resulting
//! contract. That is the whole point of generating it: the schemas the server
//! admits against are the ones `aion_awl::compile` derived from the document
//! and committed into package identity, so deriving the advertisement from the
//! handlers' Rust types instead (with `schemars`) would produce a SECOND,
//! independent rendering of the same shapes — and the contract gate exists
//! precisely to catch the drift between two such renderings.

use std::fmt::Write as _;

use super::plan::ConnectionPlan;
use super::scaffold::{AwlWorkerScaffold, DocumentRoot};
use super::text::string_literal;

/// Emits the `declaration` module.
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
}

/// The module documentation: WHY the document, and not the Rust types, is the
/// source of the advertisement.
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,
    );
}

/// The embedded document, its schema-import root, and the queue 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),
    );
}

/// The `DOCUMENT_DIR` initialiser.
///
/// A document inside the crate's own tree is reached from
/// `CARGO_MANIFEST_DIR`, so the crate finds it wherever the tree is checked
/// out. A document in an unrelated tree has no meaningful relative path and is
/// named outright.
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),
    }
}

/// The typed refusals. Every one of them is a startup failure by design: a
/// worker that cannot prove what it serves must not start advertising
/// nothing.
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",
    );
}

/// The `Declaration` type and its startup compile.
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",
    );
}

/// The descriptor lookup — the advertisement itself.
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",
    );
}

/// The unserved-action guard and the shared name rendering.
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",
    );
}

/// The worker selection out of the compiled package contract.
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",
    );
}