aion-package 0.13.6

Archive validation, content hashing, and namespacing for Aion workflow packages.
Documentation
//! Emission of the generated crate's `Cargo.toml`.
//!
//! The crate is deliberately STANDALONE (`[workspace]` with nothing in it):
//! an author scaffolds beside their document, which is often inside another
//! Cargo workspace, and a generated crate that silently joined it would
//! inherit that workspace's dependency resolution and lint policy instead of
//! the ones written here.
//!
//! The lint table is the aion workspace's own, at `deny`: this crate compiles
//! under the same discipline as the engine it serves, so a handler body that
//! reaches for `unwrap` fails the build rather than reaching production.

use std::fmt::Write as _;

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

/// The aion crates the generated worker depends on, in the order they are
/// written. `aion-awl` is a PRODUCTION dependency, not a test one: the
/// embedded document is compiled at startup and every advertised schema comes
/// out of it.
const AION_CRATES: [(&str, &str); 3] = [
    (
        "aion-worker",
        "the worker SDK: the connection, the registry, and `register_activity_with_descriptor`",
    ),
    (
        "aion-package",
        "`ActivityDescriptor`/`WorkerContract` — the advertisement types the server admits against",
    ),
    (
        "aion-awl",
        "the AWL compiler. The embedded document is compiled ONCE at startup and every schema \
         this worker advertises comes from the contract it returns, so it is a production \
         dependency",
    ),
];

/// Emits the standalone `Cargo.toml` for the generated worker crate.
pub(super) fn emit(request: &AwlWorkerScaffold<'_>, plan: &ConnectionPlan) -> String {
    let mut out = String::new();
    let _ = writeln!(
        out,
        "# Generated by `aion awl scaffold` from `{}` — do not edit; regenerate from the\n\
         # document.\n\
         #\n\
         # Serves the `{}` queue's {} action(s) across {} connection(s), one per node: the\n\
         # server routes by (namespace, task_queue, node) and never by activity type, so a\n\
         # process serving several nodes must dial once per node.\n\
         #\n\
         # Standalone (NOT a workspace member) so it builds from a plain `cargo build` in\n\
         # this directory wherever it was scaffolded.",
        request.document_name,
        plan.task_queue,
        plan.servable.len(),
        plan.connections.len(),
    );
    let name = string_literal(request.crate_name);
    let _ = writeln!(
        out,
        "\n[package]\n\
         name = {name}\n\
         version = \"0.1.0\"\n\
         edition = \"2024\"\n\
         publish = false\n\
         \n\
         [[bin]]\n\
         name = {name}\n\
         path = \"src/main.rs\"\n\
         \n\
         # An empty workspace table keeps this crate out of any workspace it is\n\
         # scaffolded inside.\n\
         [workspace]"
    );
    emit_lints(&mut out);
    emit_dependencies(&mut out, request.dependencies);
    out
}

/// The aion workspace's lint policy, verbatim: the generated crate is held to
/// the standard of the engine it serves.
fn emit_lints(out: &mut String) {
    out.push_str(
        "\n[lints.rust]\n\
         unsafe_code = \"deny\"\n\
         missing_docs = \"warn\"\n\
         \n\
         [lints.clippy]\n\
         all = { level = \"deny\", priority = -1 }\n\
         pedantic = { level = \"deny\", priority = -1 }\n\
         unwrap_used = \"deny\"\n\
         expect_used = \"deny\"\n\
         panic = \"deny\"\n\
         todo = \"deny\"\n",
    );
}

/// The dependency table: the three aion crates from the requested source,
/// then the runtime crates the generated code names.
fn emit_dependencies(out: &mut String, dependency: &AionDependency) {
    out.push_str("\n[dependencies]\n");
    for (crate_name, why) in AION_CRATES {
        for line in wrapped_comment(why) {
            let _ = writeln!(out, "# {line}");
        }
        match dependency {
            AionDependency::Version(version) => {
                let _ = writeln!(out, "{crate_name} = {}", string_literal(version));
            }
            AionDependency::Path(crates_directory) => {
                let _ = writeln!(
                    out,
                    "{crate_name} = {{ path = {} }}",
                    string_literal(&format!("{crates_directory}/{crate_name}"))
                );
            }
        }
    }
    out.push_str(
        "# Top-level failure reporting for the binary; the declaration module keeps its own\n\
         # typed errors.\n\
         anyhow = \"1\"\n\
         # The activity payloads: the types live in the AWL document, so the handlers take\n\
         # and return JSON values rather than a second Rust rendering of the same shapes.\n\
         serde_json = \"1\"\n\
         thiserror = \"2\"\n\
         tokio = { version = \"1\", features = [\"macros\", \"rt-multi-thread\"] }\n\
         tracing = \"0.1\"\n\
         # Without a subscriber the SDK's registration and reconnect logs go nowhere, and an\n\
         # operator watching a worker that will not dial sees silence.\n\
         tracing-subscriber = { version = \"0.3\", features = [\"env-filter\"] }\n",
    );
}

/// Wraps a comment at a width that keeps the emitted manifest readable.
fn wrapped_comment(text: &str) -> Vec<String> {
    const WIDTH: usize = 84;
    let mut lines = Vec::new();
    let mut current = String::new();
    for word in text.split_whitespace() {
        if !current.is_empty() && current.len() + 1 + word.len() > WIDTH {
            lines.push(std::mem::take(&mut current));
        }
        if !current.is_empty() {
            current.push(' ');
        }
        current.push_str(word);
    }
    if !current.is_empty() {
        lines.push(current);
    }
    lines
}