~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
385 lines · 13.3 KB Raw History
//! Write a `com.fedfork.exec.composition` in Rust. `codegen` turns each module's module.json (vendored in
//! atlibs.lock.json, or a module directory) into types: a module, its entries, the contracts it hooks and
//! implements, and its config as a typed input. A route to a module that does not implement the contract,
//! or an input field of the wrong type, does not compile. `check` runs the hosts' load checks
//! (`atlibs compose check`) and points a failure at your line.

use std::cell::RefCell;
use std::marker::PhantomData;
use std::panic::Location;
use std::path::Path;

use serde::{Serialize, Serializer};
use serde_json::{Map, Value, json};

pub mod codegen;
pub use serde_json;

/// A module binding (generated).
pub trait Module {
    const KEY: &'static str;
    const PIN: Option<&'static str>;
}

/// A contract: a lexicon procedure one module hooks and another implements (generated, one per NSID).
pub trait Contract: Copy {
    const NSID: &'static str;
}

#[diagnostic::on_unimplemented(message = "module `{Self}` has no hook `{C}`", label = "not one of the caller's module.json hooks")]
pub trait Hooks<C: Contract>: Module {
    const REASON: &'static str;
}

#[diagnostic::on_unimplemented(
    message = "module `{Self}` does not implement `{C}`",
    label = "its module.json implements does not list this contract",
    note = "route it to an instance whose module implements it"
)]
pub trait Implements<C: Contract>: Module {
    const ENTRY: &'static str;
}

/// One of module `M`'s entries (generated: `VmMcp::vm_create`).
pub struct Entry<M>(&'static str, PhantomData<M>);
impl<M> Entry<M> {
    pub const fn new(name: &'static str) -> Self {
        Entry(name, PhantomData)
    }
}
impl<M> Clone for Entry<M> {
    fn clone(&self) -> Self {
        *self
    }
}
impl<M> Copy for Entry<M> {}

/// A parameter type: its module.json field type.
pub trait ParamType: Serialize {
    const TYPE: &'static str;
}
impl ParamType for String {
    const TYPE: &'static str = "string";
}
impl ParamType for i64 {
    const TYPE: &'static str = "integer";
}
impl ParamType for f64 {
    const TYPE: &'static str = "number";
}
impl ParamType for bool {
    const TYPE: &'static str = "boolean";
}
impl<T: Serialize> ParamType for Vec<T> {
    const TYPE: &'static str = "array";
}
impl ParamType for Map<String, Value> {
    const TYPE: &'static str = "object";
}

/// A parameter's declaration; `Composition::param` adds it and gives its placeholder.
pub struct Param<T> {
    key: String,
    label: Option<String>,
    required: bool,
    description: Option<String>,
    secret: bool,
    range: (Option<i64>, Option<i64>),
    default: Option<T>,
    choices: Option<Vec<T>>,
}

impl<T: ParamType> Param<T> {
    pub fn new(key: &str) -> Self {
        Param { key: key.into(), label: None, required: false, description: None, secret: false, range: (None, None), default: None, choices: None }
    }
    pub fn label(mut self, s: &str) -> Self {
        self.label = Some(s.into());
        self
    }
    pub fn description(mut self, s: &str) -> Self {
        self.description = Some(s.into());
        self
    }
    pub fn required(mut self) -> Self {
        self.required = true;
        self
    }
    pub fn default(mut self, v: T) -> Self {
        self.default = Some(v);
        self
    }
    pub fn choices(mut self, v: Vec<T>) -> Self {
        self.choices = Some(v);
        self
    }
}
impl Param<String> {
    /// From the host's secrets, never a record: no default.
    pub fn secret(mut self) -> Self {
        self.secret = true;
        self
    }
}
impl Param<i64> {
    pub fn range(mut self, min: i64, max: i64) -> Self {
        self.range = (Some(min), Some(max));
        self
    }
}

/// `{"$param": key}`: a parameter's value in an instance's input.
#[derive(Clone)]
pub struct Placeholder<T>(String, PhantomData<T>);
impl<T> Serialize for Placeholder<T> {
    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        json!({ "$param": self.0 }).serialize(s)
    }
}

/// `{"$host": name}`: a value only the host gives (any field's type).
pub struct Host(String);
pub fn host(name: &str) -> Host {
    Host(name.into())
}

/// A module's typed input (generated: `<Module>Input`, one setter per config field).
pub trait Input<M> {
    fn fields(self) -> Map<String, Value>;
}

/// A config field's value: a literal of its type, a parameter of its type, or a host value.
pub struct Arg<K>(pub Value, PhantomData<K>);
fn arg<K>(v: Value) -> Arg<K> {
    Arg(v, PhantomData)
}
/// The field kinds (module.json field types).
pub mod kind {
    pub struct Str;
    pub struct Int;
    pub struct Num;
    pub struct Bool;
    pub struct Obj;
    pub struct Arr;
}
impl<K> From<Host> for Arg<K> {
    fn from(h: Host) -> Self {
        arg(json!({ "$host": h.0 }))
    }
}
macro_rules! args {
    ($kind:ty: $($t:ty),*; $p:ty) => {
        $(impl From<$t> for Arg<$kind> { fn from(v: $t) -> Self { arg(json!(v)) } })*
        impl From<Placeholder<$p>> for Arg<$kind> { fn from(p: Placeholder<$p>) -> Self { arg(json!(p)) } }
    };
}
args!(kind::Str: &str, String; String);
args!(kind::Int: i64, i32, u32; i64);
args!(kind::Num: f64, i64; f64);
args!(kind::Bool: bool; bool);
args!(kind::Obj: Map<String, Value>; Map<String, Value>);
impl<T: Serialize> From<Vec<T>> for Arg<kind::Arr> {
    fn from(v: Vec<T>) -> Self {
        arg(json!(v))
    }
}
impl<T> From<Placeholder<Vec<T>>> for Arg<kind::Arr> {
    fn from(p: Placeholder<Vec<T>>) -> Self {
        arg(json!(p))
    }
}

/// An instance of module `M` in a composition.
pub struct Inst<'c, M> {
    c: &'c Composition,
    i: usize,
    _m: PhantomData<M>,
}
impl<M> Inst<'_, M> {
    fn set(self, k: &str, v: Value) -> Self {
        self.c.m.borrow_mut().instances[self.i].insert(k.into(), v);
        self
    }
    fn name(&self) -> Value {
        self.c.m.borrow().instances[self.i]["name"].clone()
    }
    pub fn session(self, s: &str) -> Self {
        self.set("session", json!(s))
    }
    /// Its input: the module's generated `<Module>Input` (its config fields, typed).
    pub fn input(self, v: impl Input<M>) -> Self {
        self.set("input", Value::Object(v.fields()))
    }
    /// Not pinned to the vendored module: it gets its hooks' defaults.
    pub fn unpinned(self) -> Self {
        self.c.m.borrow_mut().instances[self.i].shift_remove("pin");
        self
    }
}

/// A handler the composition's caller supplies.
pub struct HandlerRef(String);

/// An edge, to add policies to.
pub struct EdgeRef<'c> {
    c: &'c Composition,
    i: usize,
}
impl EdgeRef<'_> {
    pub fn policy<M>(self, inst: &Inst<M>, entry: Entry<M>) -> Self {
        let p = json!({ "instance": inst.name(), "entry": entry.0 });
        let mut m = self.c.m.borrow_mut();
        let e = &mut m.edges[self.i];
        e.entry("policies").or_insert_with(|| json!([])).as_array_mut().expect("a list").push(p);
        drop(m);
        self
    }
}

#[derive(Default)]
struct Manifest {
    parameters: Vec<Value>,
    instances: Vec<Map<String, Value>>,
    handlers: Vec<Value>,
    edges: Vec<Map<String, Value>>,
    exports: Vec<Value>,
    limits: Option<Value>,
    sites: Vec<(String, &'static Location<'static>)>,
}

pub struct Composition {
    name: String,
    description: Option<String>,
    m: RefCell<Manifest>,
}

impl Composition {
    pub fn new(name: &str) -> Self {
        Composition { name: name.into(), description: None, m: RefCell::default() }
    }
    pub fn description(mut self, s: &str) -> Self {
        self.description = Some(s.into());
        self
    }
    pub fn limits(self, depth: u32, calls: u32) -> Self {
        self.m.borrow_mut().limits = Some(json!({ "depth": depth, "calls": calls }));
        self
    }

    pub fn param<T: ParamType>(&self, p: Param<T>) -> Placeholder<T> {
        let mut o = Map::new();
        o.insert("key".into(), json!(p.key));
        p.label.map(|v| o.insert("label".into(), json!(v)));
        o.insert("type".into(), json!(T::TYPE));
        p.required.then(|| o.insert("required".into(), json!(true)));
        p.description.map(|v| o.insert("description".into(), json!(v)));
        p.secret.then(|| o.insert("secret".into(), json!(true)));
        p.range.0.map(|v| o.insert("minimum".into(), json!(v)));
        p.range.1.map(|v| o.insert("maximum".into(), json!(v)));
        let mut values = Map::new();
        p.default.map(|v| values.insert("default".into(), json!(v)));
        p.choices.map(|v| values.insert("enum".into(), json!(v)));
        if !values.is_empty() {
            o.insert("values".into(), Value::Object(values));
        }
        self.m.borrow_mut().parameters.push(Value::Object(o));
        Placeholder(p.key, PhantomData)
    }

    /// An instance of `M`, pinned as vendored.
    #[track_caller]
    pub fn instance<M: Module>(&self, name: &str) -> Inst<'_, M> {
        let mut o = Map::new();
        o.insert("name".into(), json!(name));
        o.insert("module".into(), json!(M::KEY));
        M::PIN.map(|p| o.insert("pin".into(), json!(p)));
        let mut m = self.m.borrow_mut();
        m.instances.push(o);
        m.sites.push((format!("instance {name}"), Location::caller()));
        Inst { c: self, i: m.instances.len() - 1, _m: PhantomData }
    }

    pub fn handler(&self, name: &str, description: &str) -> HandlerRef {
        self.m.borrow_mut().handlers.push(json!({ "name": name, "description": description }));
        HandlerRef(name.into())
    }

    #[track_caller]
    fn edge(&self, from: Value, nsid: &str, to: Map<String, Value>, reason: &str) -> EdgeRef<'_> {
        let mut o = Map::new();
        o.insert("from".into(), from.clone());
        o.insert("nsid".into(), json!(nsid));
        o.extend(to);
        o.insert("reason".into(), json!(reason));
        let mut m = self.m.borrow_mut();
        m.edges.push(o);
        m.sites.push((format!("edge {} {nsid}", from.as_str().unwrap_or_default()), Location::caller()));
        EdgeRef { c: self, i: m.edges.len() - 1 }
    }

    /// `from` calling contract `C` goes to `to` (at the entry its module implements). Empty `reason`: the hook's.
    #[track_caller]
    pub fn route<C: Contract, F: Hooks<C>, T: Implements<C>>(&self, from: &Inst<F>, _: C, to: &Inst<T>, reason: &str) -> EdgeRef<'_> {
        self.edge(from.name(), C::NSID, Map::from_iter([("to".to_string(), to.name())]), if reason.is_empty() { F::REASON } else { reason })
    }

    /// `from` calling contract `C` goes to a handler the caller supplies.
    #[track_caller]
    pub fn route_handler<C: Contract, F: Hooks<C>>(&self, from: &Inst<F>, _: C, to: &HandlerRef, reason: &str) -> EdgeRef<'_> {
        self.edge(from.name(), C::NSID, Map::from_iter([("handler".to_string(), json!(to.0))]), if reason.is_empty() { F::REASON } else { reason })
    }

    /// The entry serving contract `C`: the instance's implementing one.
    pub fn export<C: Contract, T: Implements<C>>(&self, _: C, inst: &Inst<T>) {
        self.export_entry(C::NSID, inst, Entry::<T>::new(T::ENTRY));
    }

    /// An entry serving `nsid` that its module does not declare in implements.
    pub fn export_entry<T>(&self, nsid: &str, inst: &Inst<T>, entry: Entry<T>) {
        self.m.borrow_mut().exports.push(json!({ "nsid": nsid, "instance": inst.name(), "entry": entry.0 }));
    }

    pub fn manifest(&self) -> Value {
        let m = self.m.borrow();
        let mut o = Map::new();
        o.insert("$type".into(), json!("com.fedfork.exec.composition"));
        o.insert("name".into(), json!(self.name));
        self.description.as_ref().map(|d| o.insert("description".into(), json!(d)));
        let mut list = |k: &str, v: Vec<Value>| (!v.is_empty()).then(|| o.insert(k.into(), Value::Array(v)));
        list("parameters", m.parameters.clone());
        list("instances", m.instances.iter().cloned().map(Value::Object).collect());
        list("handlers", m.handlers.clone());
        list("edges", m.edges.iter().cloned().map(Value::Object).collect());
        list("exports", m.exports.clone());
        m.limits.clone().map(|l| o.insert("limits".into(), l));
        Value::Object(o)
    }

    /// The file a host loads: tab-indented, as `atlibs` and the JS builder write it.
    pub fn to_json(&self) -> String {
        let mut out = Vec::new();
        let mut ser = serde_json::Serializer::with_formatter(&mut out, serde_json::ser::PrettyFormatter::with_indent(b"\t"));
        self.manifest().serialize(&mut ser).expect("json");
        format!("{}\n", String::from_utf8(out).expect("utf8"))
    }

    /// The hosts' load checks, no wasm: `atlibs compose check <file>` in `dir` (where atlibs.lock.json vendors
    /// the modules). A failure names the line of the instance or route it is about.
    pub fn check(&self, dir: &Path) -> Result<(), String> {
        let file = dir.join(".atlibs-compose-check.json");
        std::fs::write(&file, self.to_json()).map_err(|e| format!("{}: {e}", file.display()))?;
        let out = std::process::Command::new("atlibs").args(["compose", "check"]).arg(&file).output();
        let _ = std::fs::remove_file(&file);
        let out = out.map_err(|e| format!("atlibs: {e}"))?;
        if out.status.success() {
            return Ok(());
        }
        let msg = String::from_utf8_lossy(&out.stderr).trim().to_string();
        let m = self.m.borrow();
        let site = m.sites.iter().filter(|(what, _)| msg.contains(&format!("{what}:")) || msg.contains(&format!("{what} "))).last();
        Err(match site {
            Some((_, at)) => format!("{msg}\n    at {at}"),
            None => msg,
        })
    }
}