aion-package 0.9.1

Archive validation, content hashing, and namespacing for Aion workflow packages.
Documentation
//! Emission of the generated activity-wrapper Gleam module.
//!
//! `aion generate` emits `src/<pkg>_activity_wrappers.gleam` from a package's
//! declarations: one `activity.Activity` constructor per declaration, pairing
//! the engine activity name with its input/output codecs (from the generated
//! codecs module) and the author-written body (from the hand-written
//! activities module).
//!
//! Emission is a pure, deterministic function of the resolved declarations
//! (declaration order preserved), so a delete-and-regenerate round-trip is
//! byte-identical. Generated source is never `gleam format`-checked — it
//! carries the do-not-edit header and only has to compile — so emission
//! targets a fixed, simple layout rather than the formatter's line-wrapping
//! rules.

use std::fmt::Write as _;

use super::activity_model::ResolvedActivity;

/// Header line every declaration-derived generated module starts with (the
/// do-not-edit contract; `--check` regenerates and byte-compares against it).
pub(crate) const GENERATED_ACTIVITY_HEADER: &str =
    "//// Generated by aion generate — do not edit; regenerate from the activity declarations.";

/// Emits `src/<pkg>_activity_wrappers.gleam`: one typed `<name>_activity`
/// constructor per declaration, in declaration order. Each pairs the engine
/// activity name with its codecs (from the generated codecs module) and the
/// author's body (from the hand-written activities module).
pub(crate) fn emit_wrappers_module(package_name: &str, activities: &[ResolvedActivity]) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "{GENERATED_ACTIVITY_HEADER}");
    out.push_str("////\n");
    let _ = writeln!(
        out,
        "//// Typed activity wrappers for the `{package_name}` workflow project, derived"
    );
    out.push_str("//// from the declarations its `manifest()` returns.\n");
    out.push('\n');
    out.push_str("import aion/activity\n");
    let _ = writeln!(out, "import {package_name}_activities as activities");
    let _ = writeln!(out, "import {package_name}_codecs as codecs");
    let _ = writeln!(out, "import {package_name}_io as io");

    for activity in activities {
        let name = &activity.declaration.name;
        let input_type = &activity.input.gleam_type;
        let output_type = &activity.output.gleam_type;
        let input_prefix = &activity.input.fn_prefix;
        let output_prefix = &activity.output.fn_prefix;
        let _ = write!(
            out,
            "\npub fn {name}_activity(\n  input: io.{input_type},\n\
             ) -> activity.Activity(io.{input_type}, io.{output_type}) {{\n  \
             activity.new(\n    \"{name}\",\n    input,\n    \
             codecs.{input_prefix}_codec(),\n    codecs.{output_prefix}_codec(),\n    \
             activities.{name},\n  )\n}}\n"
        );
    }
    out
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::emit_wrappers_module;
    use crate::codegen::activity_model::{ResolvedActivity, ResolvedType};
    use crate::codegen::declaration::{ActivityDeclaration, Tier};
    use crate::codegen::model::{BoundaryType, GleamType};

    /// A throwaway boundary type; the wrapper emitter only reads the borrowed
    /// reference's address, never its contents.
    fn boundary() -> BoundaryType {
        BoundaryType {
            file: PathBuf::from("schemas/placeholder.json"),
            stem: "placeholder".to_owned(),
            root: GleamType::Named {
                type_name: "Placeholder".to_owned(),
                fn_prefix: "placeholder".to_owned(),
            },
            defs: Vec::new(),
        }
    }

    fn declaration(name: &str, tier: Tier, input: &str, output: &str) -> ActivityDeclaration {
        ActivityDeclaration {
            name: name.to_owned(),
            tier,
            input_type: input.to_owned(),
            output_type: output.to_owned(),
        }
    }

    fn resolved<'a>(
        declaration: &'a ActivityDeclaration,
        boundary: &'a BoundaryType,
    ) -> ResolvedActivity<'a> {
        let prefix = |ty: &str| ty.to_lowercase();
        ResolvedActivity {
            declaration,
            input: ResolvedType {
                gleam_type: declaration.input_type.clone(),
                fn_prefix: prefix(&declaration.input_type),
                boundary,
            },
            output: ResolvedType {
                gleam_type: declaration.output_type.clone(),
                fn_prefix: prefix(&declaration.output_type),
                boundary,
            },
        }
    }

    #[test]
    fn wrappers_module_emits_one_constructor_per_declaration_in_order() {
        let art = boundary();
        let d1 = declaration("reserve_inventory", Tier::RemotePython, "Order", "Receipt");
        let d2 = declaration("ship_order", Tier::InVm, "Order", "Shipment");
        let activities = [resolved(&d1, &art), resolved(&d2, &art)];

        let module = emit_wrappers_module("demo", &activities);

        assert!(module.starts_with(super::GENERATED_ACTIVITY_HEADER));
        assert!(module.contains("import demo_activities as activities\n"));
        assert!(module.contains("import demo_codecs as codecs\n"));
        assert!(module.contains("import demo_io as io\n"));
        let first = module.find("reserve_inventory_activity");
        let second = module.find("ship_order_activity");
        assert!(first.is_some() && second.is_some());
        assert!(first < second, "declaration order preserved");
        assert!(module.contains(
            "pub fn reserve_inventory_activity(\n  input: io.Order,\n\
             ) -> activity.Activity(io.Order, io.Receipt) {\n  activity.new(\n    \
             \"reserve_inventory\",\n    input,\n    codecs.order_codec(),\n    \
             codecs.receipt_codec(),\n    activities.reserve_inventory,\n  )\n}\n"
        ));
    }
}