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

johnandersen777.bsky.social/atlibs

0
0
main
122 lines · 6.6 KB Raw History
//! 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
}