Skip to main content

gestalt/
catalog.rs

1use std::collections::BTreeSet;
2use std::path::Path;
3
4use schemars::JsonSchema;
5use serde_json::{Value as JsonValue, json};
6
7use crate::error::{Error, Result};
8use crate::generated::v1;
9
10pub type Catalog = v1::Catalog;
11pub type CatalogOperation = v1::CatalogOperation;
12pub type CatalogParameter = v1::CatalogParameter;
13
14impl Catalog {
15    pub fn with_name(mut self, name: impl Into<String>) -> Self {
16        let name = name.into();
17        if !name.trim().is_empty() {
18            self.name = name;
19        }
20        self
21    }
22}
23
24pub fn write_catalog(catalog: &Catalog, path: impl AsRef<Path>) -> Result<()> {
25    let path = path.as_ref();
26    if let Some(parent) = path.parent()
27        && !parent.as_os_str().is_empty()
28    {
29        std::fs::create_dir_all(parent)?;
30    }
31    let json = serde_json::to_string_pretty(&catalog_to_json_value(catalog))?;
32    std::fs::write(path, json)?;
33    Ok(())
34}
35
36fn catalog_to_json_value(catalog: &Catalog) -> JsonValue {
37    let mut obj = serde_json::Map::new();
38    obj.insert("name".to_owned(), json!(catalog.name));
39    if !catalog.display_name.is_empty() {
40        obj.insert("displayName".to_owned(), json!(catalog.display_name));
41    }
42    if !catalog.description.is_empty() {
43        obj.insert("description".to_owned(), json!(catalog.description));
44    }
45    if !catalog.icon_svg.is_empty() {
46        obj.insert("iconSvg".to_owned(), json!(catalog.icon_svg));
47    }
48    let ops: Vec<JsonValue> = catalog
49        .operations
50        .iter()
51        .map(operation_to_json_value)
52        .collect();
53    obj.insert("operations".to_owned(), json!(ops));
54    JsonValue::Object(obj)
55}
56
57fn operation_to_json_value(op: &CatalogOperation) -> JsonValue {
58    let mut obj = serde_json::Map::new();
59    obj.insert("id".to_owned(), json!(op.id));
60    obj.insert("method".to_owned(), json!(op.method));
61    if !op.title.is_empty() {
62        obj.insert("title".to_owned(), json!(op.title));
63    }
64    if !op.description.is_empty() {
65        obj.insert("description".to_owned(), json!(op.description));
66    }
67    if !op.input_schema.is_empty() {
68        if let Ok(schema) = serde_json::from_str::<JsonValue>(&op.input_schema) {
69            obj.insert("inputSchema".to_owned(), schema);
70        }
71    }
72    if !op.output_schema.is_empty() {
73        if let Ok(schema) = serde_json::from_str::<JsonValue>(&op.output_schema) {
74            obj.insert("outputSchema".to_owned(), schema);
75        }
76    }
77    if !op.tags.is_empty() {
78        obj.insert("tags".to_owned(), json!(op.tags));
79    }
80    if !op.required_scopes.is_empty() {
81        obj.insert("requiredScopes".to_owned(), json!(op.required_scopes));
82    }
83    if op.read_only {
84        obj.insert("readOnly".to_owned(), json!(true));
85    }
86    if let Some(visible) = op.visible {
87        obj.insert("visible".to_owned(), json!(visible));
88    }
89    if !op.transport.is_empty() {
90        obj.insert("transport".to_owned(), json!(op.transport));
91    }
92    if !op.allowed_roles.is_empty() {
93        obj.insert("allowedRoles".to_owned(), json!(op.allowed_roles));
94    }
95    if !op.parameters.is_empty() {
96        let params: Vec<JsonValue> = op
97            .parameters
98            .iter()
99            .map(|p| {
100                let mut m = serde_json::Map::new();
101                m.insert("name".to_owned(), json!(p.name));
102                m.insert("type".to_owned(), json!(p.r#type));
103                if !p.description.is_empty() {
104                    m.insert("description".to_owned(), json!(p.description));
105                }
106                if p.required {
107                    m.insert("required".to_owned(), json!(true));
108                }
109                if let Some(ref default) = p.default {
110                    let val = proto_value_to_json(default.clone());
111                    m.insert("default".to_owned(), val);
112                }
113                JsonValue::Object(m)
114            })
115            .collect();
116        obj.insert("parameters".to_owned(), json!(params));
117    }
118    if let Some(ref ann) = op.annotations {
119        let mut a = serde_json::Map::new();
120        if let Some(v) = ann.read_only_hint {
121            a.insert("readOnlyHint".to_owned(), json!(v));
122        }
123        if let Some(v) = ann.idempotent_hint {
124            a.insert("idempotentHint".to_owned(), json!(v));
125        }
126        if let Some(v) = ann.destructive_hint {
127            a.insert("destructiveHint".to_owned(), json!(v));
128        }
129        if let Some(v) = ann.open_world_hint {
130            a.insert("openWorldHint".to_owned(), json!(v));
131        }
132        if !a.is_empty() {
133            obj.insert("annotations".to_owned(), JsonValue::Object(a));
134        }
135    }
136    JsonValue::Object(obj)
137}
138
139pub(crate) fn schema_json<T: JsonSchema>() -> Result<JsonValue> {
140    serde_json::to_value(schemars::schema_for!(T)).map_err(Error::from)
141}
142
143pub(crate) fn schema_parameters(schema: &JsonValue) -> Vec<CatalogParameter> {
144    let required = schema
145        .get("required")
146        .and_then(JsonValue::as_array)
147        .map(|items| {
148            items
149                .iter()
150                .filter_map(JsonValue::as_str)
151                .map(ToOwned::to_owned)
152                .collect::<BTreeSet<_>>()
153        })
154        .unwrap_or_default();
155
156    let Some(properties) = schema.get("properties").and_then(JsonValue::as_object) else {
157        return Vec::new();
158    };
159
160    properties
161        .iter()
162        .map(|(name, property)| CatalogParameter {
163            name: name.clone(),
164            r#type: schema_type(property),
165            description: property
166                .get("description")
167                .and_then(JsonValue::as_str)
168                .unwrap_or_default()
169                .trim()
170                .to_owned(),
171            required: required.contains(name),
172            default: property.get("default").map(json_value_to_proto_value),
173        })
174        .collect()
175}
176
177fn json_value_to_proto_value(value: &JsonValue) -> prost_types::Value {
178    match value {
179        JsonValue::Null => prost_types::Value {
180            kind: Some(prost_types::value::Kind::NullValue(0)),
181        },
182        JsonValue::Bool(b) => prost_types::Value {
183            kind: Some(prost_types::value::Kind::BoolValue(*b)),
184        },
185        JsonValue::Number(n) => prost_types::Value {
186            kind: Some(prost_types::value::Kind::NumberValue(
187                n.as_f64().unwrap_or(0.0),
188            )),
189        },
190        JsonValue::String(s) => prost_types::Value {
191            kind: Some(prost_types::value::Kind::StringValue(s.clone())),
192        },
193        JsonValue::Array(items) => prost_types::Value {
194            kind: Some(prost_types::value::Kind::ListValue(
195                prost_types::ListValue {
196                    values: items.iter().map(json_value_to_proto_value).collect(),
197                },
198            )),
199        },
200        JsonValue::Object(map) => prost_types::Value {
201            kind: Some(prost_types::value::Kind::StructValue(prost_types::Struct {
202                fields: map
203                    .iter()
204                    .map(|(k, v)| (k.clone(), json_value_to_proto_value(v)))
205                    .collect(),
206            })),
207        },
208    }
209}
210
211pub(crate) fn object_map(value: Option<prost_types::Struct>) -> serde_json::Map<String, JsonValue> {
212    value
213        .map(|structure| {
214            structure
215                .fields
216                .into_iter()
217                .map(|(key, value)| (key, proto_value_to_json(value)))
218                .collect::<serde_json::Map<_, _>>()
219        })
220        .unwrap_or_default()
221}
222
223pub(crate) fn proto_value_to_json(value: prost_types::Value) -> JsonValue {
224    match value.kind {
225        Some(prost_types::value::Kind::NullValue(_)) | None => JsonValue::Null,
226        Some(prost_types::value::Kind::NumberValue(number)) => json!(number),
227        Some(prost_types::value::Kind::StringValue(text)) => json!(text),
228        Some(prost_types::value::Kind::BoolValue(flag)) => json!(flag),
229        Some(prost_types::value::Kind::StructValue(structure)) => {
230            JsonValue::Object(object_map(Some(structure)))
231        }
232        Some(prost_types::value::Kind::ListValue(list)) => {
233            JsonValue::Array(list.values.into_iter().map(proto_value_to_json).collect())
234        }
235    }
236}
237
238fn schema_type(schema: &JsonValue) -> String {
239    if schema.get("properties").is_some() {
240        return "object".to_owned();
241    }
242    if schema.get("items").is_some() {
243        return "array".to_owned();
244    }
245    match schema.get("type") {
246        Some(JsonValue::String(value)) => normalize_type(value).to_owned(),
247        Some(JsonValue::Array(values)) => values
248            .iter()
249            .filter_map(JsonValue::as_str)
250            .find(|value| *value != "null")
251            .map(|value| normalize_type(value).to_owned())
252            .unwrap_or_else(|| "object".to_owned()),
253        _ => "object".to_owned(),
254    }
255}
256
257fn normalize_type(value: &str) -> &'static str {
258    match value {
259        "integer" => "integer",
260        "number" => "number",
261        "boolean" => "boolean",
262        "array" => "array",
263        "object" => "object",
264        _ => "string",
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[derive(serde::Deserialize, schemars::JsonSchema)]
273    struct SampleInput {
274        #[allow(dead_code)]
275        #[schemars(description = "Search query")]
276        query: String,
277        #[allow(dead_code)]
278        #[serde(default)]
279        max_items: Option<u32>,
280    }
281
282    #[test]
283    fn schema_parameters_derive_required_and_optional_fields() {
284        let schema = schema_json::<SampleInput>().expect("schema");
285        let mut params = schema_parameters(&schema);
286        params.sort_by(|left, right| left.name.cmp(&right.name));
287
288        assert_eq!(params.len(), 2);
289        assert_eq!(params[0].name, "max_items");
290        assert!(!params[0].required);
291        assert_eq!(params[1].name, "query");
292        assert!(params[1].required);
293        assert_eq!(params[1].description, "Search query");
294    }
295}