liminal-cli 0.2.0

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! `list` and `describe` (SDK-017 R2): the topology record read fresh, the
//! stations and their subjects; one subject's activities with the exact
//! schema strings the worker registered.

use serde_json::{Value, json};

use super::Rail;
use crate::mcp::tools::ToolResult;

/// The stations on the map right now.
pub fn list<R: Rail>(rail: &R) -> ToolResult {
    let record = match rail.topology() {
        Ok(record) => record,
        Err(error) => return ToolResult::failed(&error.to_string(), json!({})),
    };
    let stations: Vec<Value> = stations_of(&record)
        .iter()
        .map(|station| {
            let mut listed = json!({
                "id": station.get("id").cloned().unwrap_or(Value::Null),
                "name": station.get("name").cloned().unwrap_or(Value::Null),
                "device": station.get("device").cloned().unwrap_or(Value::Null),
                "kind": station.get("kind").cloned().unwrap_or(Value::Null),
                "state": station.get("state").cloned().unwrap_or(Value::Null),
            });
            if let Some(subjects) = station.get("subjects") {
                listed["subjects"] = subjects.clone();
            }
            if let Some(activities) = station.get("activities").and_then(Value::as_array) {
                listed["activities"] = activities
                    .iter()
                    .filter_map(|activity| activity.get("name").cloned())
                    .collect();
            }
            listed
        })
        .collect();
    ToolResult::ok(json!({
        "rev": record.get("rev").cloned().unwrap_or(Value::Null),
        "stations": stations,
    }))
}

/// One subject's station and its activities with their exact schemas.
pub fn describe<R: Rail>(rail: &R, subject: &str) -> ToolResult {
    let record = match rail.topology() {
        Ok(record) => record,
        Err(error) => return ToolResult::failed(&error.to_string(), json!({})),
    };
    let served = stations_of(&record).iter().find(|station| {
        station
            .get("subjects")
            .and_then(Value::as_array)
            .is_some_and(|subjects| subjects.iter().any(|s| s.as_str() == Some(subject)))
    });
    served.map_or_else(
        || ToolResult::failed(&format!("no station serves {subject}"), json!({})),
        |station| {
            ToolResult::ok(json!({
                "rev": record.get("rev").cloned().unwrap_or(Value::Null),
                "subject": subject,
                "station": station.get("id").cloned().unwrap_or(Value::Null),
                "name": station.get("name").cloned().unwrap_or(Value::Null),
                "device": station.get("device").cloned().unwrap_or(Value::Null),
                "activities": station.get("activities").cloned().unwrap_or_else(|| json!([])),
            }))
        },
    )
}

fn stations_of(record: &Value) -> &[Value] {
    record
        .get("stations")
        .and_then(Value::as_array)
        .map_or(&[], Vec::as_slice)
}

#[cfg(test)]
#[path = "directory_tests.rs"]
mod tests;