//! 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: 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: Module { const ENTRY: &'static str; } /// One of module `M`'s entries (generated: `VmMcp::vm_create`). pub struct Entry(&'static str, PhantomData); impl Entry { pub const fn new(name: &'static str) -> Self { Entry(name, PhantomData) } } impl Clone for Entry { fn clone(&self) -> Self { *self } } impl Copy for Entry {} /// 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 ParamType for Vec { const TYPE: &'static str = "array"; } impl ParamType for Map { const TYPE: &'static str = "object"; } /// A parameter's declaration; `Composition::param` adds it and gives its placeholder. pub struct Param { key: String, label: Option, required: bool, description: Option, secret: bool, range: (Option, Option), default: Option, choices: Option>, } impl Param { 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) -> Self { self.choices = Some(v); self } } impl Param { /// From the host's secrets, never a record: no default. pub fn secret(mut self) -> Self { self.secret = true; self } } impl Param { 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(String, PhantomData); impl Serialize for Placeholder { fn serialize(&self, s: S) -> Result { 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: `Input`, one setter per config field). pub trait Input { fn fields(self) -> Map; } /// A config field's value: a literal of its type, a parameter of its type, or a host value. pub struct Arg(pub Value, PhantomData); fn arg(v: Value) -> Arg { 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 From for Arg { 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> 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; Map); impl From> for Arg { fn from(v: Vec) -> Self { arg(json!(v)) } } impl From>> for Arg { fn from(p: Placeholder>) -> Self { arg(json!(p)) } } /// An instance of module `M` in a composition. pub struct Inst<'c, M> { c: &'c Composition, i: usize, _m: PhantomData, } impl 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 `Input` (its config fields, typed). pub fn input(self, v: impl Input) -> 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(self, inst: &Inst, entry: Entry) -> 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, instances: Vec>, handlers: Vec, edges: Vec>, exports: Vec, limits: Option, sites: Vec<(String, &'static Location<'static>)>, } pub struct Composition { name: String, description: Option, m: RefCell, } 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(&self, p: Param) -> Placeholder { 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(&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, 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, T: Implements>(&self, from: &Inst, _: C, to: &Inst, 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>(&self, from: &Inst, _: 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>(&self, _: C, inst: &Inst) { self.export_entry(C::NSID, inst, Entry::::new(T::ENTRY)); } /// An entry serving `nsid` that its module does not declare in implements. pub fn export_entry(&self, nsid: &str, inst: &Inst, entry: Entry) { 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| (!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 ` 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, }) } }