Skip to main content

lattice_plugin_api/
json.rs

1//! AD.2: the catalog as JSON — the plugin API for a reader that wants
2//! structure rather than prose (an agent, a code generator, an editor
3//! integration).
4//!
5//! Hand-written rather than `serde`: this crate's contract is ZERO runtime
6//! dependencies (`lattice-host` links it, and whatever it depends on the host
7//! inherits), and the shape is small and fixed. Output is pretty-printed with
8//! stable key order so the checked-in file diffs line by line when the WIT
9//! moves.
10//!
11//! Shape (every field that can be absent is `null`, never omitted, so a
12//! consumer can rely on the keys):
13//!
14//! ```text
15//! { "package": "lattice:plugin-host@0.1.0",
16//!   "interfaces": [ { "name", "doc", "direction", "capability",
17//!       "examples": [ EXAMPLE ],
18//!       "functions": [ { "name", "display_name", "kind", "resource",
19//!                        "async", "params": [ {"name","type"} ],
20//!                        "result", "signature", "doc",
21//!                        "examples": [ EXAMPLE ] } ],
22//!       "types": [ { "name", "kind", "doc", "definition",
23//!                    "members": [ {"name","type","doc"} ],
24//!                    "examples": [ EXAMPLE ] } ],
25//!       "uses": [ { "name", "from", "original" } ] } ],
26//!   "worlds": [ { "name", "doc", "imports", "exports",
27//!                 "export_functions": [ FUNCTION ],
28//!                 "import_functions": [ FUNCTION ] } ] }
29//!
30//! FUNCTION = { "name", "display_name", "kind", "resource", "async",
31//!              "params", "result", "signature", "doc", "examples" }
32//!
33//! EXAMPLE = { "id", "caption", "source", "language", "code" }
34//! ```
35//!
36//! Examples sit on the item they illustrate (a method's on the method, a
37//! resource-level one on the resource's type entry), so a consumer reading a
38//! function has its examples without a second lookup.
39
40use crate::examples::Examples;
41use crate::render::{capability_short, direction_short, wit_definition};
42use crate::{ApiFunctionKind, ApiTypeKind, PluginApiCatalog};
43
44/// The whole catalog as pretty-printed JSON, newline-terminated. `examples`
45/// is the guest scan, or `None` to emit empty `examples` arrays.
46pub fn to_json(cat: &PluginApiCatalog, examples: Option<&Examples>) -> String {
47    let interfaces = cat
48        .interfaces
49        .iter()
50        .map(|i| {
51            let examples_for = |item: Option<&str>| {
52                let target = match item {
53                    Some(item) => format!("{}.{item}", i.name),
54                    None => i.name.clone(),
55                };
56                Json::Arr(
57                    examples
58                        .map(|ex| {
59                            ex.for_target(&target)
60                                .into_iter()
61                                .map(|e| {
62                                    Json::obj([
63                                        ("id", s(&e.id)),
64                                        ("caption", s(&e.caption)),
65                                        ("source", s(&e.source)),
66                                        ("language", s("rust")),
67                                        ("code", s(&e.code)),
68                                    ])
69                                })
70                                .collect()
71                        })
72                        .unwrap_or_default(),
73                )
74            };
75            let functions = i
76                .functions
77                .iter()
78                .map(|f| function_json(f, examples_for(Some(&f.display_name()))))
79                .collect();
80            let types = i
81                .types
82                .iter()
83                .map(|t| {
84                    let members = t
85                        .kind
86                        .members()
87                        .iter()
88                        .map(|m| {
89                            Json::obj([
90                                ("name", s(&m.name)),
91                                ("type", opt(m.ty.as_deref())),
92                                ("doc", opt(m.doc.as_deref())),
93                            ])
94                        })
95                        .collect();
96                    let kind = match t.kind {
97                        ApiTypeKind::Alias(_) => "alias",
98                        _ => t.kind.keyword(),
99                    };
100                    Json::obj([
101                        ("name", s(&t.name)),
102                        ("kind", s(kind)),
103                        ("doc", opt(t.doc.as_deref())),
104                        ("definition", s(&wit_definition(t))),
105                        ("members", Json::Arr(members)),
106                        ("examples", examples_for(Some(&t.name))),
107                    ])
108                })
109                .collect();
110            let uses = i
111                .uses
112                .iter()
113                .map(|u| {
114                    Json::obj([
115                        ("name", s(&u.name)),
116                        ("from", s(&u.from)),
117                        ("original", s(&u.original)),
118                    ])
119                })
120                .collect();
121            Json::obj([
122                ("name", s(&i.name)),
123                ("doc", opt(i.doc.as_deref())),
124                ("direction", s(direction_short(i.direction))),
125                ("capability", s(capability_json(i.capability))),
126                ("examples", examples_for(None)),
127                ("functions", Json::Arr(functions)),
128                ("types", Json::Arr(types)),
129                ("uses", Json::Arr(uses)),
130            ])
131        })
132        .collect();
133    let worlds = cat
134        .worlds
135        .iter()
136        .map(|w| {
137            Json::obj([
138                ("name", s(&w.name)),
139                ("doc", opt(w.doc.as_deref())),
140                (
141                    "imports",
142                    Json::Arr(w.imports.iter().map(|n| s(n)).collect()),
143                ),
144                (
145                    "exports",
146                    Json::Arr(w.exports.iter().map(|n| s(n)).collect()),
147                ),
148                (
149                    "export_functions",
150                    Json::Arr(
151                        w.export_functions
152                            .iter()
153                            .map(|f| function_json(f, Json::Arr(Vec::new())))
154                            .collect(),
155                    ),
156                ),
157                (
158                    "import_functions",
159                    Json::Arr(
160                        w.import_functions
161                            .iter()
162                            .map(|f| function_json(f, Json::Arr(Vec::new())))
163                            .collect(),
164                    ),
165                ),
166            ])
167        })
168        .collect();
169    let root = Json::obj([
170        ("package", s(crate::PACKAGE)),
171        ("interfaces", Json::Arr(interfaces)),
172        ("worlds", Json::Arr(worlds)),
173    ]);
174    let mut out = String::new();
175    root.write(&mut out, 0);
176    out.push('\n');
177    out
178}
179
180/// One function as JSON; `examples` is the already-built array.
181fn function_json(f: &crate::ApiFunction, examples: Json) -> Json {
182    let (kind, resource) = match &f.kind {
183        ApiFunctionKind::Freestanding => ("freestanding", None),
184        ApiFunctionKind::Method(r) => ("method", Some(r.as_str())),
185        ApiFunctionKind::Static(r) => ("static", Some(r.as_str())),
186        ApiFunctionKind::Constructor(r) => ("constructor", Some(r.as_str())),
187    };
188    let params = f
189        .params
190        .iter()
191        .map(|p| Json::obj([("name", s(&p.name)), ("type", s(&p.ty))]))
192        .collect();
193    Json::obj([
194        ("name", s(&f.name)),
195        ("display_name", s(&f.display_name())),
196        ("kind", s(kind)),
197        ("resource", opt(resource)),
198        ("async", Json::Bool(f.is_async)),
199        ("params", Json::Arr(params)),
200        ("result", opt(f.result.as_deref())),
201        ("signature", s(&f.signature())),
202        ("doc", opt(f.doc.as_deref())),
203        ("examples", examples),
204    ])
205}
206
207/// `none` rather than `-`: a JSON consumer should not have to know the
208/// table-cell convention of the markdown renderer.
209fn capability_json(c: crate::Capability) -> &'static str {
210    match capability_short(c) {
211        "-" => "none",
212        other => other,
213    }
214}
215
216/// The minimal JSON value this module needs.
217enum Json {
218    Str(String),
219    Bool(bool),
220    Null,
221    Arr(Vec<Json>),
222    /// Key order is insertion order — the order the shape above documents.
223    Obj(Vec<(&'static str, Json)>),
224}
225
226fn s(v: &str) -> Json {
227    Json::Str(v.to_string())
228}
229
230fn opt(v: Option<&str>) -> Json {
231    v.map(s).unwrap_or(Json::Null)
232}
233
234impl Json {
235    fn obj<const N: usize>(fields: [(&'static str, Json); N]) -> Json {
236        Json::Obj(fields.into_iter().collect())
237    }
238
239    fn write(&self, out: &mut String, depth: usize) {
240        let pad = |d: usize| "  ".repeat(d);
241        match self {
242            Json::Str(v) => write_str(out, v),
243            Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
244            Json::Null => out.push_str("null"),
245            Json::Arr(items) if items.is_empty() => out.push_str("[]"),
246            Json::Arr(items) => {
247                out.push_str("[\n");
248                for (n, item) in items.iter().enumerate() {
249                    out.push_str(&pad(depth + 1));
250                    item.write(out, depth + 1);
251                    out.push_str(if n + 1 < items.len() { ",\n" } else { "\n" });
252                }
253                out.push_str(&pad(depth));
254                out.push(']');
255            }
256            Json::Obj(fields) => {
257                out.push_str("{\n");
258                for (n, (key, value)) in fields.iter().enumerate() {
259                    out.push_str(&pad(depth + 1));
260                    write_str(out, key);
261                    out.push_str(": ");
262                    value.write(out, depth + 1);
263                    out.push_str(if n + 1 < fields.len() { ",\n" } else { "\n" });
264                }
265                out.push_str(&pad(depth));
266                out.push('}');
267            }
268        }
269    }
270}
271
272/// A JSON string literal: quotes, backslashes and control characters escaped
273/// (RFC 8259 §7); everything else, including non-ASCII, passes through as
274/// UTF-8.
275fn write_str(out: &mut String, v: &str) {
276    out.push('"');
277    for c in v.chars() {
278        match c {
279            '"' => out.push_str("\\\""),
280            '\\' => out.push_str("\\\\"),
281            '\n' => out.push_str("\\n"),
282            '\r' => out.push_str("\\r"),
283            '\t' => out.push_str("\\t"),
284            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
285            c => out.push(c),
286        }
287    }
288    out.push('"');
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn strings_are_escaped_per_rfc_8259() {
297        let mut out = String::new();
298        write_str(&mut out, "a\"b\\c\nd\u{1}é");
299        assert_eq!(out, r#""a\"b\\c\nd\u0001é""#);
300    }
301
302    #[test]
303    fn nesting_is_indented_and_empty_arrays_are_inline() {
304        let mut out = String::new();
305        Json::obj([("k", Json::Arr(vec![])), ("v", Json::Null)]).write(&mut out, 0);
306        assert_eq!(out, "{\n  \"k\": [],\n  \"v\": null\n}");
307    }
308}