Skip to main content

sim_lib_topology/
browse.rs

1//! Agent-facing topology browse, help, and example data.
2
3use sim_kernel::{Expr, LoadCx, Result, Symbol, Value};
4
5mod examples;
6mod specs;
7
8pub use examples::topology_example_specs;
9pub use specs::{topology_function_specs, topology_verb_specs};
10
11/// Public topology function metadata for Card generation.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct TopologyFunctionSpec {
14    /// The function symbol name.
15    pub symbol: &'static str,
16    /// One-line summary.
17    pub summary: &'static str,
18    /// Longer description.
19    pub detail: &'static str,
20    /// Argument description.
21    pub args: &'static str,
22    /// Result description.
23    pub result: &'static str,
24}
25
26/// Public topology node verb metadata for Card generation.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct TopologyVerbSpec {
29    /// The verb name.
30    pub name: &'static str,
31    /// One-line summary.
32    pub summary: &'static str,
33    /// Longer description.
34    pub detail: &'static str,
35}
36
37/// Generated topology package example.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct TopologyExampleSpec {
40    /// The example name.
41    pub name: &'static str,
42    /// One-line summary.
43    pub summary: &'static str,
44    /// The `.simtopo` package source for the example.
45    pub package: &'static str,
46}
47
48/// Returns the symbols of every browsable topology Card (functions, verbs,
49/// the package format, and examples).
50pub fn topology_browse_symbols() -> Vec<Symbol> {
51    let function_cards = topology_function_specs()
52        .into_iter()
53        .map(|spec| card_symbol("function", spec.symbol));
54    let verb_cards = topology_verb_specs()
55        .into_iter()
56        .map(|spec| Symbol::qualified("topology/verb", spec.name));
57    let examples = topology_example_specs()
58        .into_iter()
59        .map(|spec| Symbol::qualified("topology/example", spec.name));
60    function_cards
61        .chain(verb_cards)
62        .chain([Symbol::qualified("topology", "package-format")])
63        .chain(examples)
64        .collect()
65}
66
67/// Resolves a browse symbol to its Card as a runtime [`Value`].
68pub fn topology_browse_value(cx: &mut LoadCx, symbol: Symbol) -> Result<Value> {
69    cx.factory().expr(topology_card_expr(&symbol))
70}
71
72/// Builds the Card expression for a browse symbol (function, verb, package
73/// format, or example).
74pub fn topology_card_expr(symbol: &Symbol) -> Expr {
75    if let Some(name) = symbol.name.as_ref().strip_prefix("function-")
76        && let Some(spec) = topology_function_specs()
77            .into_iter()
78            .find(|spec| spec.symbol == name)
79    {
80        return function_card(spec);
81    }
82    if symbol.namespace.as_deref() == Some("topology/verb")
83        && let Some(spec) = topology_verb_specs()
84            .into_iter()
85            .find(|spec| spec.name == symbol.name.as_ref())
86    {
87        return verb_card(spec);
88    }
89    if symbol == &Symbol::qualified("topology", "package-format") {
90        return package_format_card();
91    }
92    if symbol.namespace.as_deref() == Some("topology/example")
93        && let Some(spec) = topology_example_specs()
94            .into_iter()
95            .find(|spec| spec.name == symbol.name.as_ref())
96    {
97        return example_card(spec);
98    }
99    card_v2(CardV2Spec {
100        subject: symbol.clone(),
101        kind: Symbol::qualified("topology", "unknown"),
102        summary: "unknown topology browse subject".to_owned(),
103        detail: String::new(),
104        args: Expr::Symbol(Symbol::qualified("core", "Any")),
105        result: Expr::Symbol(Symbol::qualified("core", "Any")),
106        tests: Vec::new(),
107        see_also: Vec::new(),
108    })
109}
110
111fn function_card(spec: TopologyFunctionSpec) -> Expr {
112    card_v2(CardV2Spec {
113        subject: Symbol::qualified("topology", spec.symbol),
114        kind: Symbol::qualified("core", "function"),
115        summary: spec.summary.to_owned(),
116        detail: spec.detail.to_owned(),
117        args: Expr::String(spec.args.to_owned()),
118        result: Expr::String(spec.result.to_owned()),
119        tests: Vec::new(),
120        see_also: vec![
121            Expr::Symbol(Symbol::qualified("topology", "package-format")),
122            Expr::Symbol(Symbol::qualified("topology/card", "function-test")),
123        ],
124    })
125}
126
127fn verb_card(spec: TopologyVerbSpec) -> Expr {
128    card_v2(CardV2Spec {
129        subject: Symbol::qualified("topology/verb", spec.name),
130        kind: Symbol::qualified("topology", "node-verb"),
131        summary: spec.summary.to_owned(),
132        detail: spec.detail.to_owned(),
133        args: Expr::String("node input ports".to_owned()),
134        result: Expr::String("node output ports".to_owned()),
135        tests: Vec::new(),
136        see_also: vec![Expr::Symbol(Symbol::qualified(
137            "topology",
138            "package-format",
139        ))],
140    })
141}
142
143fn package_format_card() -> Expr {
144    let tests = topology_example_specs()
145        .into_iter()
146        .map(example_test_card)
147        .collect::<Vec<_>>();
148    card_v2(CardV2Spec {
149        subject: Symbol::qualified("topology", "package-format"),
150        kind: Symbol::qualified("topology", "package-format"),
151        summary: "section-based .simtopo package format".to_owned(),
152        detail: "Packages contain graph, tests, metadata, and capabilities sections. Graph and tests reuse the topology text DSL.".to_owned(),
153        args: Expr::String("package source text".to_owned()),
154        result: Expr::String("TopologyPackage graph, tests, metadata, and capabilities".to_owned()),
155        tests,
156        see_also: topology_example_specs()
157            .into_iter()
158            .map(|spec| Expr::Symbol(Symbol::qualified("topology/example", spec.name)))
159            .collect(),
160    })
161}
162
163fn example_card(spec: TopologyExampleSpec) -> Expr {
164    card_v2(CardV2Spec {
165        subject: Symbol::qualified("topology/example", spec.name),
166        kind: Symbol::qualified("core", "test"),
167        summary: spec.summary.to_owned(),
168        detail: "Generated .simtopo package example that can be run with topology/test.".to_owned(),
169        args: Expr::String(spec.package.to_owned()),
170        result: Expr::String("topology test report".to_owned()),
171        tests: vec![example_test_card(spec)],
172        see_also: vec![Expr::Symbol(Symbol::qualified(
173            "topology",
174            "package-format",
175        ))],
176    })
177}
178
179struct CardV2Spec {
180    subject: Symbol,
181    kind: Symbol,
182    summary: String,
183    detail: String,
184    args: Expr,
185    result: Expr,
186    tests: Vec<Expr>,
187    see_also: Vec<Expr>,
188}
189
190fn card_v2(spec: CardV2Spec) -> Expr {
191    let CardV2Spec {
192        subject,
193        kind,
194        summary,
195        detail,
196        args,
197        result,
198        tests,
199        see_also,
200    } = spec;
201    let coverage = coverage_expr(&tests);
202    Expr::Map(vec![
203        entry("subject", Expr::Symbol(subject.clone())),
204        entry("kind", Expr::Symbol(kind)),
205        entry(
206            "help",
207            help_expr(subject, summary, detail, see_also.clone()),
208        ),
209        entry("args", args),
210        entry("result", result),
211        entry("tests", Expr::List(tests)),
212        entry("ops", Expr::List(Vec::new())),
213        entry("requires", Expr::List(Vec::new())),
214        entry("see-also", Expr::List(see_also)),
215        entry("shape-known", Expr::Bool(true)),
216        entry("facets", Expr::List(Vec::new())),
217        entry("coverage", coverage),
218        entry("provenance", Expr::List(Vec::new())),
219        entry("freshness", Expr::Symbol(Symbol::new("fresh"))),
220    ])
221}
222
223fn help_expr(subject: Symbol, summary: String, detail: String, see_also: Vec<Expr>) -> Expr {
224    Expr::Map(vec![
225        entry("subject", Expr::Symbol(subject)),
226        entry("kind", Expr::Symbol(Symbol::qualified("core", "function"))),
227        entry("summary", Expr::String(summary)),
228        entry("detail", Expr::String(detail)),
229        entry(
230            "exported-by",
231            Expr::Symbol(Symbol::qualified("sim", "topology")),
232        ),
233        entry("stability", Expr::Symbol(Symbol::new("experimental"))),
234        entry("capabilities", Expr::List(Vec::new())),
235        entry("demand", Expr::List(Vec::new())),
236        entry("see-also", Expr::List(see_also)),
237    ])
238}
239
240fn example_test_card(spec: TopologyExampleSpec) -> Expr {
241    Expr::Map(vec![
242        entry(
243            "name",
244            Expr::Symbol(Symbol::qualified("topology/example", spec.name)),
245        ),
246        entry(
247            "subjects",
248            Expr::List(vec![Expr::Symbol(Symbol::qualified(
249                "topology",
250                "package-format",
251            ))]),
252        ),
253        entry("lib", Expr::Symbol(Symbol::qualified("sim", "topology"))),
254        entry("mode", Expr::Symbol(Symbol::new("example"))),
255        entry(
256            "expr",
257            Expr::Call {
258                operator: Box::new(Expr::Symbol(Symbol::qualified("topology", "test"))),
259                args: vec![Expr::String(spec.package.to_owned())],
260            },
261        ),
262        entry(
263            "expr-codec",
264            Expr::Symbol(Symbol::qualified("codec", "lisp")),
265        ),
266        entry("expected", Expr::Nil),
267        entry("expected-codec", Expr::Nil),
268        entry("expected-error", Expr::Nil),
269        entry("codecs", Expr::List(Vec::new())),
270        entry("example", Expr::Bool(true)),
271        entry("capabilities", Expr::List(Vec::new())),
272    ])
273}
274
275fn coverage_expr(tests: &[Expr]) -> Expr {
276    Expr::Map(vec![
277        entry("tests", number_expr(tests.len() as u64)),
278        entry("examples", number_expr(example_count(tests) as u64)),
279        entry("runnable", Expr::Bool(!tests.is_empty())),
280        entry("passed", Expr::Nil),
281        entry("failed", Expr::Nil),
282        entry("skipped", Expr::Nil),
283        entry("last-run", Expr::Nil),
284        entry("stale", Expr::Bool(false)),
285    ])
286}
287
288fn example_count(tests: &[Expr]) -> usize {
289    tests
290        .iter()
291        .filter(|test| map_bool_field(test, "example") == Some(true))
292        .count()
293}
294
295fn map_bool_field(expr: &Expr, name: &str) -> Option<bool> {
296    let Expr::Map(entries) = expr else {
297        return None;
298    };
299    entries.iter().find_map(|(key, value)| match (key, value) {
300        (Expr::Symbol(symbol), Expr::Bool(value))
301            if symbol.namespace.is_none() && symbol.name.as_ref() == name =>
302        {
303            Some(*value)
304        }
305        _ => None,
306    })
307}
308
309fn card_symbol(kind: &str, name: &str) -> Symbol {
310    Symbol::qualified("topology/card", format!("{kind}-{name}"))
311}
312
313use sim_value::build::{entry, uint as number_expr};