1use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
27pub struct PluginSchema {
28 pub namespace: String,
30
31 pub version: String,
33
34 pub description: String,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub long_description: Option<String>,
40
41 pub hash: String,
44
45 pub methods: Vec<MethodSchema>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub children: Option<Vec<ChildSummary>>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
55#[serde(untagged)]
56pub enum SchemaResult {
57 Plugin(PluginSchema),
59 Method(MethodSchema),
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
65pub struct MethodSchema {
66 pub name: String,
68
69 pub description: String,
71
72 pub hash: String,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub params: Option<schemars::Schema>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub returns: Option<schemars::Schema>,
83
84 #[serde(default)]
92 pub streaming: bool,
93}
94
95impl PluginSchema {
96 fn compute_hash(methods: &[MethodSchema], children: Option<&[ChildSummary]>) -> String {
98 use std::collections::hash_map::DefaultHasher;
99 use std::hash::{Hash, Hasher};
100
101 let mut hasher = DefaultHasher::new();
102
103 for m in methods {
105 m.hash.hash(&mut hasher);
106 }
107
108 if let Some(kids) = children {
110 for c in kids {
111 c.hash.hash(&mut hasher);
112 }
113 }
114
115 format!("{:016x}", hasher.finish())
116 }
117
118 fn validate_no_collisions(
127 namespace: &str,
128 methods: &[MethodSchema],
129 children: Option<&[ChildSummary]>,
130 ) {
131 use std::collections::HashSet;
132
133 let mut seen: HashSet<&str> = HashSet::new();
134
135 for m in methods {
137 if !seen.insert(&m.name) {
138 panic!(
139 "Name collision in plugin '{}': duplicate method '{}'",
140 namespace, m.name
141 );
142 }
143 }
144
145 if let Some(kids) = children {
147 for c in kids {
148 if !seen.insert(&c.namespace) {
149 let collision_type = if methods.iter().any(|m| m.name == c.namespace) {
151 "method/child collision"
152 } else {
153 "duplicate child"
154 };
155 panic!(
156 "Name collision in plugin '{}': {} for '{}'",
157 namespace, collision_type, c.namespace
158 );
159 }
160 }
161 }
162 }
163
164 pub fn leaf(
166 namespace: impl Into<String>,
167 version: impl Into<String>,
168 description: impl Into<String>,
169 methods: Vec<MethodSchema>,
170 ) -> Self {
171 let namespace = namespace.into();
172 Self::validate_no_collisions(&namespace, &methods, None);
173 let hash = Self::compute_hash(&methods, None);
174 Self {
175 namespace,
176 version: version.into(),
177 description: description.into(),
178 long_description: None,
179 hash,
180 methods,
181 children: None,
182 }
183 }
184
185 pub fn leaf_with_long_description(
187 namespace: impl Into<String>,
188 version: impl Into<String>,
189 description: impl Into<String>,
190 long_description: impl Into<String>,
191 methods: Vec<MethodSchema>,
192 ) -> Self {
193 let namespace = namespace.into();
194 Self::validate_no_collisions(&namespace, &methods, None);
195 let hash = Self::compute_hash(&methods, None);
196 Self {
197 namespace,
198 version: version.into(),
199 description: description.into(),
200 long_description: Some(long_description.into()),
201 hash,
202 methods,
203 children: None,
204 }
205 }
206
207 pub fn hub(
209 namespace: impl Into<String>,
210 version: impl Into<String>,
211 description: impl Into<String>,
212 methods: Vec<MethodSchema>,
213 children: Vec<ChildSummary>,
214 ) -> Self {
215 let namespace = namespace.into();
216 Self::validate_no_collisions(&namespace, &methods, Some(&children));
217 let hash = Self::compute_hash(&methods, Some(&children));
218 Self {
219 namespace,
220 version: version.into(),
221 description: description.into(),
222 long_description: None,
223 hash,
224 methods,
225 children: Some(children),
226 }
227 }
228
229 pub fn hub_with_long_description(
231 namespace: impl Into<String>,
232 version: impl Into<String>,
233 description: impl Into<String>,
234 long_description: impl Into<String>,
235 methods: Vec<MethodSchema>,
236 children: Vec<ChildSummary>,
237 ) -> Self {
238 let namespace = namespace.into();
239 Self::validate_no_collisions(&namespace, &methods, Some(&children));
240 let hash = Self::compute_hash(&methods, Some(&children));
241 Self {
242 namespace,
243 version: version.into(),
244 description: description.into(),
245 long_description: Some(long_description.into()),
246 hash,
247 methods,
248 children: Some(children),
249 }
250 }
251
252 pub fn is_hub(&self) -> bool {
254 self.children.is_some()
255 }
256
257 pub fn is_leaf(&self) -> bool {
259 self.children.is_none()
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
265pub struct ChildSummary {
266 pub namespace: String,
268
269 pub description: String,
271
272 pub hash: String,
274}
275
276impl MethodSchema {
277 pub fn new(
282 name: impl Into<String>,
283 description: impl Into<String>,
284 hash: impl Into<String>,
285 ) -> Self {
286 Self {
287 name: name.into(),
288 description: description.into(),
289 hash: hash.into(),
290 params: None,
291 returns: None,
292 streaming: false,
293 }
294 }
295
296 pub fn with_params(mut self, params: schemars::Schema) -> Self {
298 self.params = Some(params);
299 self
300 }
301
302 pub fn with_returns(mut self, returns: schemars::Schema) -> Self {
304 self.returns = Some(returns);
305 self
306 }
307
308 pub fn with_streaming(mut self, streaming: bool) -> Self {
313 self.streaming = streaming;
314 self
315 }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct Schema {
325 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none", default)]
327 pub schema_version: Option<String>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub title: Option<String>,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub description: Option<String>,
336
337 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
339 pub schema_type: Option<serde_json::Value>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub properties: Option<HashMap<String, SchemaProperty>>,
344
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub required: Option<Vec<String>>,
348
349 #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
351 pub one_of: Option<Vec<Schema>>,
352
353 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
355 pub defs: Option<HashMap<String, serde_json::Value>>,
356
357 #[serde(flatten)]
359 pub additional: HashMap<String, serde_json::Value>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
364#[serde(rename_all = "lowercase")]
365pub enum SchemaType {
366 Object,
367 Array,
368 String,
369 Number,
370 Integer,
371 Boolean,
372 Null,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct SchemaProperty {
378 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
380 pub property_type: Option<serde_json::Value>,
381
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub description: Option<String>,
385
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub format: Option<String>,
389
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub items: Option<Box<SchemaProperty>>,
393
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub properties: Option<HashMap<String, SchemaProperty>>,
397
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub required: Option<Vec<String>>,
401
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub default: Option<serde_json::Value>,
405
406 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
408 pub enum_values: Option<Vec<serde_json::Value>>,
409
410 #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
412 pub reference: Option<String>,
413
414 #[serde(flatten)]
416 pub additional: HashMap<String, serde_json::Value>,
417}
418
419impl Schema {
420 pub fn new(title: impl Into<String>, description: impl Into<String>) -> Self {
422 Self {
423 schema_version: Some("http://json-schema.org/draft-07/schema#".to_string()),
424 title: Some(title.into()),
425 description: Some(description.into()),
426 schema_type: None,
427 properties: None,
428 required: None,
429 one_of: None,
430 defs: None,
431 additional: HashMap::new(),
432 }
433 }
434
435 pub fn object() -> Self {
437 Self {
438 schema_version: Some("http://json-schema.org/draft-07/schema#".to_string()),
439 title: None,
440 description: None,
441 schema_type: Some(serde_json::json!("object")),
442 properties: Some(HashMap::new()),
443 required: None,
444 one_of: None,
445 defs: None,
446 additional: HashMap::new(),
447 }
448 }
449
450 pub fn with_property(mut self, name: impl Into<String>, property: SchemaProperty) -> Self {
452 self.properties
453 .get_or_insert_with(HashMap::new)
454 .insert(name.into(), property);
455 self
456 }
457
458 pub fn with_required(mut self, name: impl Into<String>) -> Self {
460 self.required
461 .get_or_insert_with(Vec::new)
462 .push(name.into());
463 self
464 }
465
466 pub fn with_description(mut self, description: impl Into<String>) -> Self {
468 self.description = Some(description.into());
469 self
470 }
471
472 pub fn get_method_schema(&self, method_name: &str) -> Option<Schema> {
477 let variants = self.one_of.as_ref()?;
478
479 for variant in variants {
480 if let Some(props) = &variant.properties {
482 if let Some(method_prop) = props.get("method") {
483 if let Some(const_val) = method_prop.additional.get("const") {
485 if const_val.as_str() == Some(method_name) {
486 return Some(variant.clone());
487 }
488 }
489 if let Some(enum_vals) = &method_prop.enum_values {
491 if enum_vals.first().and_then(|v| v.as_str()) == Some(method_name) {
492 return Some(variant.clone());
493 }
494 }
495 }
496 }
497 }
498 None
499 }
500
501 pub fn list_methods(&self) -> Vec<String> {
503 let Some(variants) = &self.one_of else {
504 return Vec::new();
505 };
506
507 variants
508 .iter()
509 .filter_map(|variant| {
510 let props = variant.properties.as_ref()?;
511 let method_prop = props.get("method")?;
512
513 if let Some(const_val) = method_prop.additional.get("const") {
515 return const_val.as_str().map(String::from);
516 }
517 method_prop
519 .enum_values
520 .as_ref()?
521 .first()?
522 .as_str()
523 .map(String::from)
524 })
525 .collect()
526 }
527}
528
529impl SchemaProperty {
530 pub fn string() -> Self {
532 Self {
533 property_type: Some(serde_json::json!("string")),
534 description: None,
535 format: None,
536 items: None,
537 properties: None,
538 required: None,
539 default: None,
540 enum_values: None,
541 reference: None,
542 additional: HashMap::new(),
543 }
544 }
545
546 pub fn uuid() -> Self {
548 Self {
549 property_type: Some(serde_json::json!("string")),
550 description: None,
551 format: Some("uuid".to_string()),
552 items: None,
553 properties: None,
554 required: None,
555 default: None,
556 enum_values: None,
557 reference: None,
558 additional: HashMap::new(),
559 }
560 }
561
562 pub fn integer() -> Self {
564 Self {
565 property_type: Some(serde_json::json!("integer")),
566 description: None,
567 format: None,
568 items: None,
569 properties: None,
570 required: None,
571 default: None,
572 enum_values: None,
573 reference: None,
574 additional: HashMap::new(),
575 }
576 }
577
578 pub fn object() -> Self {
580 Self {
581 property_type: Some(serde_json::json!("object")),
582 description: None,
583 format: None,
584 items: None,
585 properties: Some(HashMap::new()),
586 required: None,
587 default: None,
588 enum_values: None,
589 reference: None,
590 additional: HashMap::new(),
591 }
592 }
593
594 pub fn array(items: SchemaProperty) -> Self {
596 Self {
597 property_type: Some(serde_json::json!("array")),
598 description: None,
599 format: None,
600 items: Some(Box::new(items)),
601 properties: None,
602 required: None,
603 default: None,
604 enum_values: None,
605 reference: None,
606 additional: HashMap::new(),
607 }
608 }
609
610 pub fn with_description(mut self, description: impl Into<String>) -> Self {
612 self.description = Some(description.into());
613 self
614 }
615
616 pub fn with_default(mut self, default: serde_json::Value) -> Self {
618 self.default = Some(default);
619 self
620 }
621
622 pub fn with_property(mut self, name: impl Into<String>, property: SchemaProperty) -> Self {
624 self.properties
625 .get_or_insert_with(HashMap::new)
626 .insert(name.into(), property);
627 self
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn test_schema_creation() {
637 let schema = Schema::object()
638 .with_property("id", SchemaProperty::uuid().with_description("The unique identifier"))
639 .with_property("name", SchemaProperty::string().with_description("The name"))
640 .with_required("id");
641
642 assert_eq!(schema.schema_type, Some(serde_json::json!("object")));
643 assert!(schema.properties.is_some());
644 assert_eq!(schema.required, Some(vec!["id".to_string()]));
645 }
646
647 #[test]
648 fn test_serialization() {
649 let schema = Schema::object()
650 .with_property("id", SchemaProperty::uuid());
651
652 let json = serde_json::to_string_pretty(&schema).unwrap();
653 assert!(json.contains("uuid"));
654 }
655}