1use schemars::{JsonSchema, schema_for};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14use super::bidirectional::{StandardRequest, StandardResponse};
15
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
29pub struct PluginSchema {
30 pub namespace: String,
32
33 pub version: String,
35
36 pub description: String,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub long_description: Option<String>,
42
43 pub self_hash: String,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
50 pub children_hash: Option<String>,
51
52 pub hash: String,
56
57 pub methods: Vec<MethodSchema>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub children: Option<Vec<ChildSummary>>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
67#[serde(untagged)]
68pub enum SchemaResult {
69 Plugin(PluginSchema),
71 Method(MethodSchema),
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
77pub struct MethodSchema {
78 pub name: String,
80
81 pub description: String,
83
84 pub hash: String,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub params: Option<schemars::Schema>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub returns: Option<schemars::Schema>,
95
96 #[serde(default)]
104 pub streaming: bool,
105
106 #[serde(default)]
111 pub bidirectional: bool,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
118 pub request_type: Option<schemars::Schema>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
125 pub response_type: Option<schemars::Schema>,
126}
127
128impl PluginSchema {
129 fn compute_hashes(
131 methods: &[MethodSchema],
132 children: Option<&[ChildSummary]>,
133 ) -> (String, Option<String>, String) {
134 use std::collections::hash_map::DefaultHasher;
135 use std::hash::{Hash, Hasher};
136
137 let mut self_hasher = DefaultHasher::new();
139 for m in methods {
140 m.hash.hash(&mut self_hasher);
141 }
142 let self_hash = format!("{:016x}", self_hasher.finish());
143
144 let children_hash = children.map(|kids| {
146 let mut children_hasher = DefaultHasher::new();
147 for c in kids {
148 c.hash.hash(&mut children_hasher);
149 }
150 format!("{:016x}", children_hasher.finish())
151 });
152
153 let mut composite_hasher = DefaultHasher::new();
155 self_hash.hash(&mut composite_hasher);
156 if let Some(ref ch) = children_hash {
157 ch.hash(&mut composite_hasher);
158 }
159 let hash = format!("{:016x}", composite_hasher.finish());
160
161 (self_hash, children_hash, hash)
162 }
163
164 fn validate_no_collisions(
173 namespace: &str,
174 methods: &[MethodSchema],
175 children: Option<&[ChildSummary]>,
176 ) {
177 use std::collections::HashSet;
178
179 let mut seen: HashSet<&str> = HashSet::new();
180
181 for m in methods {
183 if !seen.insert(&m.name) {
184 panic!(
185 "Name collision in plugin '{}': duplicate method '{}'",
186 namespace, m.name
187 );
188 }
189 }
190
191 if let Some(kids) = children {
193 for c in kids {
194 if !seen.insert(&c.namespace) {
195 let collision_type = if methods.iter().any(|m| m.name == c.namespace) {
197 "method/child collision"
198 } else {
199 "duplicate child"
200 };
201 panic!(
202 "Name collision in plugin '{}': {} for '{}'",
203 namespace, collision_type, c.namespace
204 );
205 }
206 }
207 }
208 }
209
210 pub fn leaf(
212 namespace: impl Into<String>,
213 version: impl Into<String>,
214 description: impl Into<String>,
215 methods: Vec<MethodSchema>,
216 ) -> Self {
217 let namespace = namespace.into();
218 Self::validate_no_collisions(&namespace, &methods, None);
219 let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, None);
220 Self {
221 namespace,
222 version: version.into(),
223 description: description.into(),
224 long_description: None,
225 self_hash,
226 children_hash,
227 hash,
228 methods,
229 children: None,
230 }
231 }
232
233 pub fn leaf_with_long_description(
235 namespace: impl Into<String>,
236 version: impl Into<String>,
237 description: impl Into<String>,
238 long_description: impl Into<String>,
239 methods: Vec<MethodSchema>,
240 ) -> Self {
241 let namespace = namespace.into();
242 Self::validate_no_collisions(&namespace, &methods, None);
243 let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, None);
244 Self {
245 namespace,
246 version: version.into(),
247 description: description.into(),
248 long_description: Some(long_description.into()),
249 self_hash,
250 children_hash,
251 hash,
252 methods,
253 children: None,
254 }
255 }
256
257 pub fn hub(
259 namespace: impl Into<String>,
260 version: impl Into<String>,
261 description: impl Into<String>,
262 methods: Vec<MethodSchema>,
263 children: Vec<ChildSummary>,
264 ) -> Self {
265 let namespace = namespace.into();
266 Self::validate_no_collisions(&namespace, &methods, Some(&children));
267 let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, Some(&children));
268 Self {
269 namespace,
270 version: version.into(),
271 description: description.into(),
272 long_description: None,
273 self_hash,
274 children_hash,
275 hash,
276 methods,
277 children: Some(children),
278 }
279 }
280
281 pub fn hub_with_long_description(
283 namespace: impl Into<String>,
284 version: impl Into<String>,
285 description: impl Into<String>,
286 long_description: impl Into<String>,
287 methods: Vec<MethodSchema>,
288 children: Vec<ChildSummary>,
289 ) -> Self {
290 let namespace = namespace.into();
291 Self::validate_no_collisions(&namespace, &methods, Some(&children));
292 let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, Some(&children));
293 Self {
294 namespace,
295 version: version.into(),
296 description: description.into(),
297 long_description: Some(long_description.into()),
298 self_hash,
299 children_hash,
300 hash,
301 methods,
302 children: Some(children),
303 }
304 }
305
306 pub fn is_hub(&self) -> bool {
308 self.children.is_some()
309 }
310
311 pub fn is_leaf(&self) -> bool {
313 self.children.is_none()
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
319pub struct ChildSummary {
320 pub namespace: String,
322
323 pub description: String,
325
326 pub hash: String,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
332pub struct PluginHashes {
333 pub namespace: String,
334 pub self_hash: String,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub children_hash: Option<String>,
337 pub hash: String,
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub children: Option<Vec<ChildHashes>>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
344pub struct ChildHashes {
345 pub namespace: String,
346 pub hash: String,
347}
348
349impl MethodSchema {
350 pub fn new(
355 name: impl Into<String>,
356 description: impl Into<String>,
357 hash: impl Into<String>,
358 ) -> Self {
359 Self {
360 name: name.into(),
361 description: description.into(),
362 hash: hash.into(),
363 params: None,
364 returns: None,
365 streaming: false,
366 bidirectional: false,
367 request_type: None,
368 response_type: None,
369 }
370 }
371
372 pub fn with_params(mut self, params: schemars::Schema) -> Self {
374 self.params = Some(params);
375 self
376 }
377
378 pub fn with_returns(mut self, returns: schemars::Schema) -> Self {
380 self.returns = Some(returns);
381 self
382 }
383
384 pub fn with_streaming(mut self, streaming: bool) -> Self {
389 self.streaming = streaming;
390 self
391 }
392
393 pub fn with_bidirectional(mut self, bidirectional: bool) -> Self {
398 self.bidirectional = bidirectional;
399 self
400 }
401
402 pub fn with_request_type(mut self, schema: schemars::Schema) -> Self {
407 self.request_type = Some(schema);
408 self
409 }
410
411 pub fn with_response_type(mut self, schema: schemars::Schema) -> Self {
416 self.response_type = Some(schema);
417 self
418 }
419
420 pub fn with_standard_bidirectional(self) -> Self {
426 self.with_bidirectional(true)
427 .with_request_type(schema_for!(StandardRequest).into())
428 .with_response_type(schema_for!(StandardResponse).into())
429 }
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct Schema {
439 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none", default)]
441 pub schema_version: Option<String>,
442
443 #[serde(skip_serializing_if = "Option::is_none")]
445 pub title: Option<String>,
446
447 #[serde(skip_serializing_if = "Option::is_none")]
449 pub description: Option<String>,
450
451 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
453 pub schema_type: Option<serde_json::Value>,
454
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub properties: Option<HashMap<String, SchemaProperty>>,
458
459 #[serde(skip_serializing_if = "Option::is_none")]
461 pub required: Option<Vec<String>>,
462
463 #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
465 pub one_of: Option<Vec<Schema>>,
466
467 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
469 pub defs: Option<HashMap<String, serde_json::Value>>,
470
471 #[serde(flatten)]
473 pub additional: HashMap<String, serde_json::Value>,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
478#[serde(rename_all = "lowercase")]
479pub enum SchemaType {
480 Object,
481 Array,
482 String,
483 Number,
484 Integer,
485 Boolean,
486 Null,
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct SchemaProperty {
492 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
494 pub property_type: Option<serde_json::Value>,
495
496 #[serde(skip_serializing_if = "Option::is_none")]
498 pub description: Option<String>,
499
500 #[serde(skip_serializing_if = "Option::is_none")]
502 pub format: Option<String>,
503
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub items: Option<Box<SchemaProperty>>,
507
508 #[serde(skip_serializing_if = "Option::is_none")]
510 pub properties: Option<HashMap<String, SchemaProperty>>,
511
512 #[serde(skip_serializing_if = "Option::is_none")]
514 pub required: Option<Vec<String>>,
515
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub default: Option<serde_json::Value>,
519
520 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
522 pub enum_values: Option<Vec<serde_json::Value>>,
523
524 #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
526 pub reference: Option<String>,
527
528 #[serde(flatten)]
530 pub additional: HashMap<String, serde_json::Value>,
531}
532
533impl Schema {
534 pub fn new(title: impl Into<String>, description: impl Into<String>) -> Self {
536 Self {
537 schema_version: Some("http://json-schema.org/draft-07/schema#".to_string()),
538 title: Some(title.into()),
539 description: Some(description.into()),
540 schema_type: None,
541 properties: None,
542 required: None,
543 one_of: None,
544 defs: None,
545 additional: HashMap::new(),
546 }
547 }
548
549 pub fn object() -> Self {
551 Self {
552 schema_version: Some("http://json-schema.org/draft-07/schema#".to_string()),
553 title: None,
554 description: None,
555 schema_type: Some(serde_json::json!("object")),
556 properties: Some(HashMap::new()),
557 required: None,
558 one_of: None,
559 defs: None,
560 additional: HashMap::new(),
561 }
562 }
563
564 pub fn with_property(mut self, name: impl Into<String>, property: SchemaProperty) -> Self {
566 self.properties
567 .get_or_insert_with(HashMap::new)
568 .insert(name.into(), property);
569 self
570 }
571
572 pub fn with_required(mut self, name: impl Into<String>) -> Self {
574 self.required
575 .get_or_insert_with(Vec::new)
576 .push(name.into());
577 self
578 }
579
580 pub fn with_description(mut self, description: impl Into<String>) -> Self {
582 self.description = Some(description.into());
583 self
584 }
585
586 pub fn get_method_schema(&self, method_name: &str) -> Option<Schema> {
591 let variants = self.one_of.as_ref()?;
592
593 for variant in variants {
594 if let Some(props) = &variant.properties {
596 if let Some(method_prop) = props.get("method") {
597 if let Some(const_val) = method_prop.additional.get("const") {
599 if const_val.as_str() == Some(method_name) {
600 return Some(variant.clone());
601 }
602 }
603 if let Some(enum_vals) = &method_prop.enum_values {
605 if enum_vals.first().and_then(|v| v.as_str()) == Some(method_name) {
606 return Some(variant.clone());
607 }
608 }
609 }
610 }
611 }
612 None
613 }
614
615 pub fn list_methods(&self) -> Vec<String> {
617 let Some(variants) = &self.one_of else {
618 return Vec::new();
619 };
620
621 variants
622 .iter()
623 .filter_map(|variant| {
624 let props = variant.properties.as_ref()?;
625 let method_prop = props.get("method")?;
626
627 if let Some(const_val) = method_prop.additional.get("const") {
629 return const_val.as_str().map(String::from);
630 }
631 method_prop
633 .enum_values
634 .as_ref()?
635 .first()?
636 .as_str()
637 .map(String::from)
638 })
639 .collect()
640 }
641}
642
643impl SchemaProperty {
644 pub fn string() -> Self {
646 Self {
647 property_type: Some(serde_json::json!("string")),
648 description: None,
649 format: None,
650 items: None,
651 properties: None,
652 required: None,
653 default: None,
654 enum_values: None,
655 reference: None,
656 additional: HashMap::new(),
657 }
658 }
659
660 pub fn uuid() -> Self {
662 Self {
663 property_type: Some(serde_json::json!("string")),
664 description: None,
665 format: Some("uuid".to_string()),
666 items: None,
667 properties: None,
668 required: None,
669 default: None,
670 enum_values: None,
671 reference: None,
672 additional: HashMap::new(),
673 }
674 }
675
676 pub fn integer() -> Self {
678 Self {
679 property_type: Some(serde_json::json!("integer")),
680 description: None,
681 format: None,
682 items: None,
683 properties: None,
684 required: None,
685 default: None,
686 enum_values: None,
687 reference: None,
688 additional: HashMap::new(),
689 }
690 }
691
692 pub fn object() -> Self {
694 Self {
695 property_type: Some(serde_json::json!("object")),
696 description: None,
697 format: None,
698 items: None,
699 properties: Some(HashMap::new()),
700 required: None,
701 default: None,
702 enum_values: None,
703 reference: None,
704 additional: HashMap::new(),
705 }
706 }
707
708 pub fn array(items: SchemaProperty) -> Self {
710 Self {
711 property_type: Some(serde_json::json!("array")),
712 description: None,
713 format: None,
714 items: Some(Box::new(items)),
715 properties: None,
716 required: None,
717 default: None,
718 enum_values: None,
719 reference: None,
720 additional: HashMap::new(),
721 }
722 }
723
724 pub fn with_description(mut self, description: impl Into<String>) -> Self {
726 self.description = Some(description.into());
727 self
728 }
729
730 pub fn with_default(mut self, default: serde_json::Value) -> Self {
732 self.default = Some(default);
733 self
734 }
735
736 pub fn with_property(mut self, name: impl Into<String>, property: SchemaProperty) -> Self {
738 self.properties
739 .get_or_insert_with(HashMap::new)
740 .insert(name.into(), property);
741 self
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
750 fn test_schema_creation() {
751 let schema = Schema::object()
752 .with_property("id", SchemaProperty::uuid().with_description("The unique identifier"))
753 .with_property("name", SchemaProperty::string().with_description("The name"))
754 .with_required("id");
755
756 assert_eq!(schema.schema_type, Some(serde_json::json!("object")));
757 assert!(schema.properties.is_some());
758 assert_eq!(schema.required, Some(vec!["id".to_string()]));
759 }
760
761 #[test]
762 fn test_serialization() {
763 let schema = Schema::object()
764 .with_property("id", SchemaProperty::uuid());
765
766 let json = serde_json::to_string_pretty(&schema).unwrap();
767 assert!(json.contains("uuid"));
768 }
769
770 #[test]
771 fn test_self_hash_changes_on_method_change() {
772 let schema1 = PluginSchema::leaf(
773 "test",
774 "1.0",
775 "desc",
776 vec![MethodSchema::new("foo", "bar", "hash1")],
777 );
778
779 let schema2 = PluginSchema::leaf(
780 "test",
781 "1.0",
782 "desc",
783 vec![MethodSchema::new("foo", "baz", "hash2")], );
785
786 assert_ne!(schema1.self_hash, schema2.self_hash, "self_hash should change when methods change");
787 assert_eq!(schema1.children_hash, schema2.children_hash, "children_hash should stay same (both None)");
788 assert_ne!(schema1.hash, schema2.hash, "composite hash should change");
789 }
790
791 #[test]
792 fn test_children_hash_changes_on_child_change() {
793 let child1 = ChildSummary {
794 namespace: "child".into(),
795 description: "desc".into(),
796 hash: "old_hash".into(),
797 };
798
799 let child2 = ChildSummary {
800 namespace: "child".into(),
801 description: "desc".into(),
802 hash: "new_hash".into(),
803 };
804
805 let schema1 = PluginSchema::hub(
806 "parent",
807 "1.0",
808 "desc",
809 vec![],
810 vec![child1],
811 );
812
813 let schema2 = PluginSchema::hub(
814 "parent",
815 "1.0",
816 "desc",
817 vec![],
818 vec![child2],
819 );
820
821 assert_eq!(schema1.self_hash, schema2.self_hash, "self_hash should stay same (no methods changed)");
822 assert_ne!(schema1.children_hash, schema2.children_hash, "children_hash should change when child hash changes");
823 assert_ne!(schema1.hash, schema2.hash, "composite hash should change");
824 }
825
826 #[test]
827 fn test_leaf_has_no_children_hash() {
828 let schema = PluginSchema::leaf(
829 "leaf",
830 "1.0",
831 "desc",
832 vec![MethodSchema::new("method", "desc", "hash")],
833 );
834
835 assert!(schema.children_hash.is_none(), "leaf plugin should have None for children_hash");
836 assert_ne!(schema.self_hash, schema.hash, "leaf plugin's composite hash is hash(self_hash), not equal to self_hash");
837 }
838}