//! Module bindings from module.json, for a build.rs: //! //! ```no_run //! let out = std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("modules.rs"); //! std::fs::write(out, atlibs_compose::codegen::generate(&["atlibs.lock.json".as_ref()]).unwrap()).unwrap(); //! ``` //! //! A source is an atlibs.lock.json (every module it vendors, pinned) or a module directory (unpinned). //! Contracts go in `nsid::::`: `nsid::computer::socialweb::vm::Provision`. use std::collections::BTreeMap; use std::fmt::Write; use std::path::Path; use serde_json::Value; /// (key, pin, module.json) for each module in `sources`. pub fn read(sources: &[&Path]) -> Result, Value)>, String> { let json = |p: &Path| -> Result { serde_json::from_slice(&std::fs::read(p).map_err(|e| format!("{}: {e}", p.display()))?).map_err(|e| format!("{}: {e}", p.display())) }; let mut out = vec![]; for src in sources { if src.extension().is_some_and(|e| e == "json") { let lock = json(src)?; let base = src.parent().unwrap_or(Path::new(".")); for (key, e) in lock["modules"].as_object().into_iter().flatten() { let spec = json(&base.join(e["spec"].as_str().ok_or(format!("{key}: no spec in {}", src.display()))?))?; out.push((key.clone(), e["pin"].as_str().map(String::from), spec)); } } else { let key = src.canonicalize().map_err(|e| format!("{}: {e}", src.display()))?.file_name().and_then(|n| n.to_str()).unwrap_or_default().to_string(); out.push((key, None, json(&src.join("module.json"))?)); } } Ok(out) } /// The Rust bindings for `sources`. pub fn generate(sources: &[&Path]) -> Result { Ok(bindings(&read(sources)?)) } const KEYWORDS: &[&str] = &["as", "async", "await", "break", "const", "continue", "dyn", "else", "enum", "extern", "false", "fn", "for", "gen", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "static", "struct", "trait", "true", "type", "unsafe", "use", "where", "while"]; fn ident(s: &str) -> String { let s: String = s.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }).collect(); let s = if s.starts_with(|c: char| c.is_ascii_digit()) { format!("_{s}") } else { s }; if KEYWORDS.contains(&s.as_str()) { format!("r#{s}") } else { s } } fn camel(s: &str) -> String { s.split(|c: char| !c.is_ascii_alphanumeric()).filter(|w| !w.is_empty()).map(|w| w[..1].to_ascii_uppercase() + &w[1..]).collect() } /// `nsid::a::b::C` for NSID a.b.c. fn contract(nsid: &str) -> String { let (path, name) = nsid.rsplit_once('.').unwrap_or(("", nsid)); path.split('.').map(ident).chain([camel(name)]).fold("nsid".into(), |acc, s| format!("{acc}::{s}")) } #[derive(Default)] struct Tree(BTreeMap, Vec<(String, String)>); fn emit_tree(t: &Tree, depth: usize, out: &mut String) { let pad = " ".repeat(depth); for (name, nsid) in &t.1 { let _ = writeln!(out, "{pad}#[derive(Clone, Copy, Debug)]\n{pad}pub struct {name};\n{pad}impl ::atlibs_compose::Contract for {name} {{ const NSID: &'static str = {nsid:?}; }}"); } for (seg, sub) in &t.0 { let _ = writeln!(out, "{pad}pub mod {seg} {{"); emit_tree(sub, depth + 1, out); let _ = writeln!(out, "{pad}}}"); } } pub fn bindings(modules: &[(String, Option, Value)]) -> String { let list = |spec: &Value, k: &str| spec[k].as_array().cloned().unwrap_or_default(); let mut tree = Tree::default(); let mut seen = std::collections::BTreeSet::new(); for (_, _, spec) in modules { for h in list(spec, "hooks").iter().chain(list(spec, "implements").iter()) { let Some(nsid) = h["nsid"].as_str() else { continue }; if !seen.insert(nsid.to_string()) { continue; } let (path, name) = nsid.rsplit_once('.').unwrap_or(("", nsid)); let node = path.split('.').fold(&mut tree, |t, seg| t.0.entry(ident(seg)).or_default()); node.1.push((camel(name), nsid.to_string())); } } let mut out = String::from("// generated by atlibs-compose from module.json: do not edit\n#[allow(dead_code)]\npub mod nsid {\n"); emit_tree(&tree, 1, &mut out); out.push_str("}\n"); for (key, pin, spec) in modules { let ty = camel(key); let doc = spec["description"].as_str().unwrap_or_default(); let _ = writeln!(out, "/// {key}: {doc}\n#[derive(Debug)]\npub struct {ty};"); let _ = writeln!(out, "impl ::atlibs_compose::Module for {ty} {{ const KEY: &'static str = {key:?}; const PIN: Option<&'static str> = {pin:?}; }}"); let _ = writeln!(out, "#[allow(non_upper_case_globals, dead_code)]\nimpl {ty} {{"); for a in list(spec, "actions") { if let Some(e) = a["entry"].as_str() { let _ = writeln!(out, " pub const {}: ::atlibs_compose::Entry<{ty}> = ::atlibs_compose::Entry::new({e:?});", ident(e)); } } out.push_str("}\n"); let _ = writeln!(out, "/// {key}'s config: the input an instance may give it.\n#[derive(Default)]\npub struct {ty}Input(::atlibs_compose::serde_json::Map);\n#[allow(dead_code)]\nimpl {ty}Input {{"); for f in list(spec, "config") { let Some(k) = f["key"].as_str() else { continue }; let kind = match f["type"].as_str().unwrap_or("string") { "integer" => "Int", "number" => "Num", "boolean" => "Bool", "object" => "Obj", "array" => "Arr", _ => "Str" }; let _ = writeln!(out, " pub fn {}(mut self, v: impl Into<::atlibs_compose::Arg<::atlibs_compose::kind::{kind}>>) -> Self {{ self.0.insert({k:?}.into(), v.into().0); self }}", ident(k)); } let _ = writeln!(out, "}}\nimpl ::atlibs_compose::Input<{ty}> for {ty}Input {{ fn fields(self) -> ::atlibs_compose::serde_json::Map {{ self.0 }} }}"); for h in list(spec, "hooks") { let (Some(nsid), reason) = (h["nsid"].as_str(), h["reason"].as_str().unwrap_or_default()) else { continue }; let _ = writeln!(out, "impl ::atlibs_compose::Hooks<{}> for {ty} {{ const REASON: &'static str = {reason:?}; }}", contract(nsid)); } for i in list(spec, "implements") { let (Some(nsid), Some(entry)) = (i["nsid"].as_str(), i["entry"].as_str()) else { continue }; let _ = writeln!(out, "impl ::atlibs_compose::Implements<{}> for {ty} {{ const ENTRY: &'static str = {entry:?}; }}", contract(nsid)); } } out }