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}