Skip to main content

worldinterface_core/
descriptor.rs

1//! Connector descriptors for capability discovery.
2//!
3//! A `Descriptor` advertises what a connector does, what parameters it expects,
4//! and what output it produces. Descriptors power the discovery API
5//! (`list_capabilities`, `describe`).
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Self-description of a connector's capabilities.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Descriptor {
13    /// Unique registry name (e.g., "http.request", "fs.write").
14    pub name: String,
15    /// Human-readable display name.
16    pub display_name: String,
17    /// Description of what this connector does.
18    pub description: String,
19    /// Category for grouping/filtering.
20    pub category: ConnectorCategory,
21    /// JSON Schema describing expected input parameters.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub input_schema: Option<Value>,
24    /// JSON Schema describing the output shape.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub output_schema: Option<Value>,
27    /// Whether the connector is naturally idempotent (safe to retry).
28    pub idempotent: bool,
29    /// Whether the connector has external side effects.
30    pub side_effects: bool,
31}
32
33/// Category of a connector, used for grouping and filtering in discovery.
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ConnectorCategory {
37    Http,
38    FileSystem,
39    Delay,
40    Transform,
41    Custom(String),
42}
43
44#[cfg(test)]
45mod tests {
46    use serde_json::json;
47
48    use super::*;
49
50    fn sample_descriptor() -> Descriptor {
51        Descriptor {
52            name: "http.request".into(),
53            display_name: "HTTP Request".into(),
54            description: "Makes an HTTP request to an external URL.".into(),
55            category: ConnectorCategory::Http,
56            input_schema: Some(json!({
57                "type": "object",
58                "properties": {
59                    "url": { "type": "string" },
60                    "method": { "type": "string" }
61                }
62            })),
63            output_schema: Some(json!({
64                "type": "object",
65                "properties": {
66                    "status": { "type": "integer" },
67                    "body": { "type": "string" }
68                }
69            })),
70            idempotent: false,
71            side_effects: true,
72        }
73    }
74
75    #[test]
76    fn descriptor_json_roundtrip() {
77        let desc = sample_descriptor();
78        let json = serde_json::to_string(&desc).unwrap();
79        let back: Descriptor = serde_json::from_str(&json).unwrap();
80        assert_eq!(desc, back);
81    }
82
83    #[test]
84    fn category_variants_roundtrip() {
85        let categories = vec![
86            ConnectorCategory::Http,
87            ConnectorCategory::FileSystem,
88            ConnectorCategory::Delay,
89            ConnectorCategory::Transform,
90            ConnectorCategory::Custom("my_plugin".into()),
91        ];
92        for cat in categories {
93            let json = serde_json::to_string(&cat).unwrap();
94            let back: ConnectorCategory = serde_json::from_str(&json).unwrap();
95            assert_eq!(cat, back);
96        }
97    }
98
99    #[test]
100    fn descriptor_with_no_schemas() {
101        let desc = Descriptor {
102            name: "delay".into(),
103            display_name: "Delay".into(),
104            description: "Waits for a specified duration.".into(),
105            category: ConnectorCategory::Delay,
106            input_schema: None,
107            output_schema: None,
108            idempotent: true,
109            side_effects: false,
110        };
111        let json = serde_json::to_string(&desc).unwrap();
112        let back: Descriptor = serde_json::from_str(&json).unwrap();
113        assert_eq!(desc, back);
114        // Verify schemas are omitted from serialized form
115        assert!(!json.contains("input_schema"));
116        assert!(!json.contains("output_schema"));
117    }
118}