1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//! 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::<authority>::<name>`: `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<Vec<(String, Option<String>, Value)>, String> {
let json = |p: &Path| -> Result<Value, String> { 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<String, String> {
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<String, Tree>, 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<String>, 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<String, ::atlibs_compose::serde_json::Value>);\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<String, ::atlibs_compose::serde_json::Value> {{ 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
}