Skip to main content

liminal_cli/network/
directory.rs

1//! `list` and `describe` (SDK-017 R2): the topology record read fresh, the
2//! stations and their subjects; one subject's activities with the exact
3//! schema strings the worker registered.
4
5use serde_json::{Value, json};
6
7use super::Rail;
8use crate::mcp::tools::ToolResult;
9
10/// The stations on the map right now.
11pub fn list<R: Rail>(rail: &R) -> ToolResult {
12    let record = match rail.topology() {
13        Ok(record) => record,
14        Err(error) => return ToolResult::failed(&error.to_string(), json!({})),
15    };
16    let stations: Vec<Value> = stations_of(&record)
17        .iter()
18        .map(|station| {
19            let mut listed = json!({
20                "id": station.get("id").cloned().unwrap_or(Value::Null),
21                "name": station.get("name").cloned().unwrap_or(Value::Null),
22                "device": station.get("device").cloned().unwrap_or(Value::Null),
23                "kind": station.get("kind").cloned().unwrap_or(Value::Null),
24                "state": station.get("state").cloned().unwrap_or(Value::Null),
25            });
26            if let Some(subjects) = station.get("subjects") {
27                listed["subjects"] = subjects.clone();
28            }
29            if let Some(activities) = station.get("activities").and_then(Value::as_array) {
30                listed["activities"] = activities
31                    .iter()
32                    .filter_map(|activity| activity.get("name").cloned())
33                    .collect();
34            }
35            listed
36        })
37        .collect();
38    ToolResult::ok(json!({
39        "rev": record.get("rev").cloned().unwrap_or(Value::Null),
40        "stations": stations,
41    }))
42}
43
44/// One subject's station and its activities with their exact schemas.
45pub fn describe<R: Rail>(rail: &R, subject: &str) -> ToolResult {
46    let record = match rail.topology() {
47        Ok(record) => record,
48        Err(error) => return ToolResult::failed(&error.to_string(), json!({})),
49    };
50    let served = stations_of(&record).iter().find(|station| {
51        station
52            .get("subjects")
53            .and_then(Value::as_array)
54            .is_some_and(|subjects| subjects.iter().any(|s| s.as_str() == Some(subject)))
55    });
56    served.map_or_else(
57        || ToolResult::failed(&format!("no station serves {subject}"), json!({})),
58        |station| {
59            ToolResult::ok(json!({
60                "rev": record.get("rev").cloned().unwrap_or(Value::Null),
61                "subject": subject,
62                "station": station.get("id").cloned().unwrap_or(Value::Null),
63                "name": station.get("name").cloned().unwrap_or(Value::Null),
64                "device": station.get("device").cloned().unwrap_or(Value::Null),
65                "activities": station.get("activities").cloned().unwrap_or_else(|| json!([])),
66            }))
67        },
68    )
69}
70
71fn stations_of(record: &Value) -> &[Value] {
72    record
73        .get("stations")
74        .and_then(Value::as_array)
75        .map_or(&[], Vec::as_slice)
76}
77
78#[cfg(test)]
79#[path = "directory_tests.rs"]
80mod tests;