slicc-discovery 1.19.2

Discovery engine for agent-facing web standards
Documentation
use crate::classifier::{Classification, ClassifierRegistry, Validity};
use crate::probe::{Probe, ProbeRegistry};
use serde_json::Value;

pub fn known_link_rels() -> &'static [&'static str] {
    &[
        "ard",
        "ai-catalog",
        "llms-txt",
        "api-catalog",
        "ucp",
        "describedby",
        "service-desc",
    ]
}

pub fn default_probes() -> ProbeRegistry {
    let mut registry = ProbeRegistry::new();
    registry.register(Probe {
        standard: "ard".into(),
        path: "/.well-known/ard.json".into(),
        accept: "application/json".into(),
        content_type_prefix: "application/json".into(),
    });
    registry
}

pub fn default_classifiers() -> ClassifierRegistry {
    let mut registry = ClassifierRegistry::new();
    registry.register("ard", classify_ard);
    registry
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResponseSignal {
    pub standard: String,
    pub summary: Value,
}

pub fn classify_response(status: u16, headers: &[(String, String)]) -> Vec<ResponseSignal> {
    let mut signals = Vec::new();
    if status == 402 {
        let challenge = headers
            .iter()
            .find(|(name, _)| name.eq_ignore_ascii_case("x-payment") || name.eq_ignore_ascii_case("payment-required"))
            .map(|(_, value)| Value::String(value.clone()))
            .unwrap_or(Value::Null);
        signals.push(ResponseSignal {
            standard: "x402".into(),
            summary: serde_json::json!({ "status": 402, "challenge": challenge }),
        });
    }
    signals
}

fn classify_ard(body: &str) -> Classification {
    match serde_json::from_str::<Value>(body) {
        Ok(value) => {
            let name = value
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();
            Classification {
                standard: "ard".into(),
                status: Validity::Valid,
                summary: serde_json::json!({ "name": name }),
                reason: None,
            }
        }
        Err(e) => Classification {
            standard: "ard".into(),
            status: Validity::Invalid,
            summary: Value::Null,
            reason: Some(e.to_string()),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn known_link_rels_includes_ard() {
        assert!(known_link_rels().contains(&"ard"));
    }

    #[test]
    fn known_link_rels_includes_ai_catalog() {
        assert!(known_link_rels().contains(&"ai-catalog"));
    }

    #[test]
    fn default_probes_has_ard() {
        let registry = default_probes();
        let probes = registry.probes_for_origin("https://example.com");
        assert_eq!(probes.len(), 1);
        assert_eq!(probes[0].probe.standard, "ard");
        assert_eq!(probes[0].url, "https://example.com/.well-known/ard.json");
    }

    #[test]
    fn default_classifiers_has_ard() {
        let registry = default_classifiers();
        let result = registry.classify("ard", r#"{"name":"Test"}"#).unwrap();
        assert_eq!(result.status, Validity::Valid);
        assert_eq!(result.summary["name"], "Test");
    }

    #[test]
    fn classify_ard_invalid_json() {
        let registry = default_classifiers();
        let result = registry.classify("ard", "not json").unwrap();
        assert_eq!(result.status, Validity::Invalid);
        assert!(result.reason.is_some());
    }

    #[test]
    fn classify_response_402() {
        let headers = vec![
            ("Content-Type".into(), "text/html".into()),
            ("X-Payment".into(), "required".into()),
        ];
        let signals = classify_response(402, &headers);
        assert_eq!(signals.len(), 1);
        assert_eq!(signals[0].standard, "x402");
        assert_eq!(signals[0].summary["status"], 402);
        assert_eq!(signals[0].summary["challenge"], "required");
    }

    #[test]
    fn classify_response_402_no_header() {
        let signals = classify_response(402, &[]);
        assert_eq!(signals.len(), 1);
        assert_eq!(signals[0].summary["challenge"], Value::Null);
    }

    #[test]
    fn classify_response_200_no_signal() {
        let signals = classify_response(200, &[]);
        assert!(signals.is_empty());
    }

    #[test]
    fn classify_response_401_no_signal() {
        let signals = classify_response(401, &[]);
        assert!(signals.is_empty());
    }
}