1use schemars::{JsonSchema, schema_for};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14use super::bidirectional::{StandardRequest, StandardResponse};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
22#[serde(rename_all = "UPPERCASE")]
23pub enum HttpMethod {
24 Get,
26 Post,
28 Put,
30 Delete,
32 Patch,
34}
35
36impl Default for HttpMethod {
37 fn default() -> Self {
38 HttpMethod::Post
39 }
40}
41
42impl HttpMethod {
43 pub fn from_str(s: &str) -> Option<Self> {
45 match s.to_uppercase().as_str() {
46 "GET" => Some(HttpMethod::Get),
47 "POST" => Some(HttpMethod::Post),
48 "PUT" => Some(HttpMethod::Put),
49 "DELETE" => Some(HttpMethod::Delete),
50 "PATCH" => Some(HttpMethod::Patch),
51 _ => None,
52 }
53 }
54
55 pub fn as_str(&self) -> &'static str {
57 match self {
58 HttpMethod::Get => "GET",
59 HttpMethod::Post => "POST",
60 HttpMethod::Put => "PUT",
61 HttpMethod::Delete => "DELETE",
62 HttpMethod::Patch => "PATCH",
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
80pub struct PluginSchema {
81 pub namespace: String,
83
84 pub version: String,
86
87 pub description: String,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub long_description: Option<String>,
93
94 pub self_hash: String,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
101 pub children_hash: Option<String>,
102
103 pub hash: String,
107
108 pub methods: Vec<MethodSchema>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub children: Option<Vec<ChildSummary>>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
124 pub request: Option<serde_json::Value>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
129#[serde(untagged)]
130pub enum SchemaResult {
131 Plugin(PluginSchema),
133 Method(MethodSchema),
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
139pub struct MethodSchema {
140 pub name: String,
142
143 pub description: String,
145
146 pub hash: String,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub params: Option<schemars::Schema>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub returns: Option<schemars::Schema>,
157
158 #[serde(default)]
166 pub streaming: bool,
167
168 #[serde(default)]
173 pub bidirectional: bool,
174
175 #[serde(default)]
187 pub http_method: HttpMethod,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
194 pub request_type: Option<schemars::Schema>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
201 pub response_type: Option<schemars::Schema>,
202}
203
204impl PluginSchema {
205 fn compute_hashes(
207 methods: &[MethodSchema],
208 children: Option<&[ChildSummary]>,
209 ) -> (String, Option<String>, String) {
210 use std::collections::hash_map::DefaultHasher;
211 use std::hash::{Hash, Hasher};
212
213 let mut self_hasher = DefaultHasher::new();
215 for m in methods {
216 m.hash.hash(&mut self_hasher);
217 }
218 let self_hash = format!("{:016x}", self_hasher.finish());
219
220 let children_hash = children.map(|kids| {
222 let mut children_hasher = DefaultHasher::new();
223 for c in kids {
224 c.hash.hash(&mut children_hasher);
225 }
226 format!("{:016x}", children_hasher.finish())
227 });
228
229 let mut composite_hasher = DefaultHasher::new();
231 self_hash.hash(&mut composite_hasher);
232 if let Some(ref ch) = children_hash {
233 ch.hash(&mut composite_hasher);
234 }
235 let hash = format!("{:016x}", composite_hasher.finish());
236
237 (self_hash, children_hash, hash)
238 }
239
240 fn validate_no_collisions(
249 namespace: &str,
250 methods: &[MethodSchema],
251 children: Option<&[ChildSummary]>,
252 ) {
253 use std::collections::HashSet;
254
255 let mut seen: HashSet<&str> = HashSet::new();
256
257 for m in methods {
259 if !seen.insert(&m.name) {
260 panic!(
261 "Name collision in plugin '{}': duplicate method '{}'",
262 namespace, m.name
263 );
264 }
265 }
266
267 if let Some(kids) = children {
269 for c in kids {
270 if !seen.insert(&c.namespace) {
271 let collision_type = if methods.iter().any(|m| m.name == c.namespace) {
273 "method/child collision"
274 } else {
275 "duplicate child"
276 };
277 panic!(
278 "Name collision in plugin '{}': {} for '{}'",
279 namespace, collision_type, c.namespace
280 );
281 }
282 }
283 }
284 }
285
286 pub fn leaf(
288 namespace: impl Into<String>,
289 version: impl Into<String>,
290 description: impl Into<String>,
291 methods: Vec<MethodSchema>,
292 ) -> Self {
293 let namespace = namespace.into();
294 Self::validate_no_collisions(&namespace, &methods, None);
295 let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, None);
296 Self {
297 namespace,
298 version: version.into(),
299 description: description.into(),
300 long_description: None,
301 self_hash,
302 children_hash,
303 hash,
304 methods,
305 children: None,
306 request: None,
307 }
308 }
309
310 pub fn leaf_with_long_description(
312 namespace: impl Into<String>,
313 version: impl Into<String>,
314 description: impl Into<String>,
315 long_description: impl Into<String>,
316 methods: Vec<MethodSchema>,
317 ) -> Self {
318 let namespace = namespace.into();
319 Self::validate_no_collisions(&namespace, &methods, None);
320 let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, None);
321 Self {
322 namespace,
323 version: version.into(),
324 description: description.into(),
325 long_description: Some(long_description.into()),
326 self_hash,
327 children_hash,
328 hash,
329 methods,
330 children: None,
331 request: None,
332 }
333 }
334
335 pub fn hub(
337 namespace: impl Into<String>,
338 version: impl Into<String>,
339 description: impl Into<String>,
340 methods: Vec<MethodSchema>,
341 children: Vec<ChildSummary>,
342 ) -> Self {
343 let namespace = namespace.into();
344 Self::validate_no_collisions(&namespace, &methods, Some(&children));
345 let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, Some(&children));
346 Self {
347 namespace,
348 version: version.into(),
349 description: description.into(),
350 long_description: None,
351 self_hash,
352 children_hash,
353 hash,
354 methods,
355 children: Some(children),
356 request: None,
357 }
358 }
359
360 pub fn hub_with_long_description(
362 namespace: impl Into<String>,
363 version: impl Into<String>,
364 description: impl Into<String>,
365 long_description: impl Into<String>,
366 methods: Vec<MethodSchema>,
367 children: Vec<ChildSummary>,
368 ) -> Self {
369 let namespace = namespace.into();
370 Self::validate_no_collisions(&namespace, &methods, Some(&children));
371 let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, Some(&children));
372 Self {
373 namespace,
374 version: version.into(),
375 description: description.into(),
376 long_description: Some(long_description.into()),
377 self_hash,
378 children_hash,
379 hash,
380 methods,
381 children: Some(children),
382 request: None,
383 }
384 }
385
386 pub fn is_hub(&self) -> bool {
388 self.children.is_some()
389 }
390
391 pub fn is_leaf(&self) -> bool {
393 self.children.is_none()
394 }
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
399pub struct ChildSummary {
400 pub namespace: String,
402
403 pub description: String,
405
406 pub hash: String,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
412pub struct PluginHashes {
413 pub namespace: String,
414 pub self_hash: String,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 pub children_hash: Option<String>,
417 pub hash: String,
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub children: Option<Vec<ChildHashes>>,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
424pub struct ChildHashes {
425 pub namespace: String,
426 pub hash: String,
427}
428
429impl MethodSchema {
430 pub fn new(
435 name: impl Into<String>,
436 description: impl Into<String>,
437 hash: impl Into<String>,
438 ) -> Self {
439 Self {
440 name: name.into(),
441 description: description.into(),
442 hash: hash.into(),
443 params: None,
444 returns: None,
445 streaming: false,
446 bidirectional: false,
447 http_method: HttpMethod::default(),
448 request_type: None,
449 response_type: None,
450 }
451 }
452
453 pub fn with_params(mut self, params: schemars::Schema) -> Self {
455 self.params = Some(params);
456 self
457 }
458
459 pub fn with_returns(mut self, returns: schemars::Schema) -> Self {
461 self.returns = Some(returns);
462 self
463 }
464
465 pub fn with_streaming(mut self, streaming: bool) -> Self {
470 self.streaming = streaming;
471 self
472 }
473
474 pub fn with_http_method(mut self, http_method: HttpMethod) -> Self {
485 self.http_method = http_method;
486 self
487 }
488
489 pub fn with_bidirectional(mut self, bidirectional: bool) -> Self {
494 self.bidirectional = bidirectional;
495 self
496 }
497
498 pub fn with_request_type(mut self, schema: schemars::Schema) -> Self {
503 self.request_type = Some(schema);
504 self
505 }
506
507 pub fn with_response_type(mut self, schema: schemars::Schema) -> Self {
512 self.response_type = Some(schema);
513 self
514 }
515
516 pub fn with_standard_bidirectional(self) -> Self {
522 self.with_bidirectional(true)
523 .with_request_type(schema_for!(StandardRequest).into())
524 .with_response_type(schema_for!(StandardResponse).into())
525 }
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct Schema {
535 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none", default)]
537 pub schema_version: Option<String>,
538
539 #[serde(skip_serializing_if = "Option::is_none")]
541 pub title: Option<String>,
542
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub description: Option<String>,
546
547 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
549 pub schema_type: Option<serde_json::Value>,
550
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub properties: Option<HashMap<String, SchemaProperty>>,
554
555 #[serde(skip_serializing_if = "Option::is_none")]
557 pub required: Option<Vec<String>>,
558
559 #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
561 pub one_of: Option<Vec<Schema>>,
562
563 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
565 pub defs: Option<HashMap<String, serde_json::Value>>,
566
567 #[serde(flatten)]
569 pub additional: HashMap<String, serde_json::Value>,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
574#[serde(rename_all = "lowercase")]
575pub enum SchemaType {
576 Object,
577 Array,
578 String,
579 Number,
580 Integer,
581 Boolean,
582 Null,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct SchemaProperty {
588 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
590 pub property_type: Option<serde_json::Value>,
591
592 #[serde(skip_serializing_if = "Option::is_none")]
594 pub description: Option<String>,
595
596 #[serde(skip_serializing_if = "Option::is_none")]
598 pub format: Option<String>,
599
600 #[serde(skip_serializing_if = "Option::is_none")]
602 pub items: Option<Box<SchemaProperty>>,
603
604 #[serde(skip_serializing_if = "Option::is_none")]
606 pub properties: Option<HashMap<String, SchemaProperty>>,
607
608 #[serde(skip_serializing_if = "Option::is_none")]
610 pub required: Option<Vec<String>>,
611
612 #[serde(skip_serializing_if = "Option::is_none")]
614 pub default: Option<serde_json::Value>,
615
616 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
618 pub enum_values: Option<Vec<serde_json::Value>>,
619
620 #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
622 pub reference: Option<String>,
623
624 #[serde(flatten)]
626 pub additional: HashMap<String, serde_json::Value>,
627}
628
629impl Schema {
630 pub fn new(title: impl Into<String>, description: impl Into<String>) -> Self {
632 Self {
633 schema_version: Some("http://json-schema.org/draft-07/schema#".to_string()),
634 title: Some(title.into()),
635 description: Some(description.into()),
636 schema_type: None,
637 properties: None,
638 required: None,
639 one_of: None,
640 defs: None,
641 additional: HashMap::new(),
642 }
643 }
644
645 pub fn object() -> Self {
647 Self {
648 schema_version: Some("http://json-schema.org/draft-07/schema#".to_string()),
649 title: None,
650 description: None,
651 schema_type: Some(serde_json::json!("object")),
652 properties: Some(HashMap::new()),
653 required: None,
654 one_of: None,
655 defs: None,
656 additional: HashMap::new(),
657 }
658 }
659
660 pub fn with_property(mut self, name: impl Into<String>, property: SchemaProperty) -> Self {
662 self.properties
663 .get_or_insert_with(HashMap::new)
664 .insert(name.into(), property);
665 self
666 }
667
668 pub fn with_required(mut self, name: impl Into<String>) -> Self {
670 self.required
671 .get_or_insert_with(Vec::new)
672 .push(name.into());
673 self
674 }
675
676 pub fn with_description(mut self, description: impl Into<String>) -> Self {
678 self.description = Some(description.into());
679 self
680 }
681
682 pub fn get_method_schema(&self, method_name: &str) -> Option<Schema> {
687 let variants = self.one_of.as_ref()?;
688
689 for variant in variants {
690 if let Some(props) = &variant.properties {
692 if let Some(method_prop) = props.get("method") {
693 if let Some(const_val) = method_prop.additional.get("const") {
695 if const_val.as_str() == Some(method_name) {
696 return Some(variant.clone());
697 }
698 }
699 if let Some(enum_vals) = &method_prop.enum_values {
701 if enum_vals.first().and_then(|v| v.as_str()) == Some(method_name) {
702 return Some(variant.clone());
703 }
704 }
705 }
706 }
707 }
708 None
709 }
710
711 pub fn list_methods(&self) -> Vec<String> {
713 let Some(variants) = &self.one_of else {
714 return Vec::new();
715 };
716
717 variants
718 .iter()
719 .filter_map(|variant| {
720 let props = variant.properties.as_ref()?;
721 let method_prop = props.get("method")?;
722
723 if let Some(const_val) = method_prop.additional.get("const") {
725 return const_val.as_str().map(String::from);
726 }
727 method_prop
729 .enum_values
730 .as_ref()?
731 .first()?
732 .as_str()
733 .map(String::from)
734 })
735 .collect()
736 }
737}
738
739impl SchemaProperty {
740 pub fn string() -> Self {
742 Self {
743 property_type: Some(serde_json::json!("string")),
744 description: None,
745 format: None,
746 items: None,
747 properties: None,
748 required: None,
749 default: None,
750 enum_values: None,
751 reference: None,
752 additional: HashMap::new(),
753 }
754 }
755
756 pub fn uuid() -> Self {
758 Self {
759 property_type: Some(serde_json::json!("string")),
760 description: None,
761 format: Some("uuid".to_string()),
762 items: None,
763 properties: None,
764 required: None,
765 default: None,
766 enum_values: None,
767 reference: None,
768 additional: HashMap::new(),
769 }
770 }
771
772 pub fn integer() -> Self {
774 Self {
775 property_type: Some(serde_json::json!("integer")),
776 description: None,
777 format: None,
778 items: None,
779 properties: None,
780 required: None,
781 default: None,
782 enum_values: None,
783 reference: None,
784 additional: HashMap::new(),
785 }
786 }
787
788 pub fn object() -> Self {
790 Self {
791 property_type: Some(serde_json::json!("object")),
792 description: None,
793 format: None,
794 items: None,
795 properties: Some(HashMap::new()),
796 required: None,
797 default: None,
798 enum_values: None,
799 reference: None,
800 additional: HashMap::new(),
801 }
802 }
803
804 pub fn array(items: SchemaProperty) -> Self {
806 Self {
807 property_type: Some(serde_json::json!("array")),
808 description: None,
809 format: None,
810 items: Some(Box::new(items)),
811 properties: None,
812 required: None,
813 default: None,
814 enum_values: None,
815 reference: None,
816 additional: HashMap::new(),
817 }
818 }
819
820 pub fn with_description(mut self, description: impl Into<String>) -> Self {
822 self.description = Some(description.into());
823 self
824 }
825
826 pub fn with_default(mut self, default: serde_json::Value) -> Self {
828 self.default = Some(default);
829 self
830 }
831
832 pub fn with_property(mut self, name: impl Into<String>, property: SchemaProperty) -> Self {
834 self.properties
835 .get_or_insert_with(HashMap::new)
836 .insert(name.into(), property);
837 self
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844
845 #[test]
846 fn test_schema_creation() {
847 let schema = Schema::object()
848 .with_property("id", SchemaProperty::uuid().with_description("The unique identifier"))
849 .with_property("name", SchemaProperty::string().with_description("The name"))
850 .with_required("id");
851
852 assert_eq!(schema.schema_type, Some(serde_json::json!("object")));
853 assert!(schema.properties.is_some());
854 assert_eq!(schema.required, Some(vec!["id".to_string()]));
855 }
856
857 #[test]
858 fn test_serialization() {
859 let schema = Schema::object()
860 .with_property("id", SchemaProperty::uuid());
861
862 let json = serde_json::to_string_pretty(&schema).unwrap();
863 assert!(json.contains("uuid"));
864 }
865
866 #[test]
867 fn test_self_hash_changes_on_method_change() {
868 let schema1 = PluginSchema::leaf(
869 "test",
870 "1.0",
871 "desc",
872 vec![MethodSchema::new("foo", "bar", "hash1")],
873 );
874
875 let schema2 = PluginSchema::leaf(
876 "test",
877 "1.0",
878 "desc",
879 vec![MethodSchema::new("foo", "baz", "hash2")], );
881
882 assert_ne!(schema1.self_hash, schema2.self_hash, "self_hash should change when methods change");
883 assert_eq!(schema1.children_hash, schema2.children_hash, "children_hash should stay same (both None)");
884 assert_ne!(schema1.hash, schema2.hash, "composite hash should change");
885 }
886
887 #[test]
888 fn test_children_hash_changes_on_child_change() {
889 let child1 = ChildSummary {
890 namespace: "child".into(),
891 description: "desc".into(),
892 hash: "old_hash".into(),
893 };
894
895 let child2 = ChildSummary {
896 namespace: "child".into(),
897 description: "desc".into(),
898 hash: "new_hash".into(),
899 };
900
901 let schema1 = PluginSchema::hub(
902 "parent",
903 "1.0",
904 "desc",
905 vec![],
906 vec![child1],
907 );
908
909 let schema2 = PluginSchema::hub(
910 "parent",
911 "1.0",
912 "desc",
913 vec![],
914 vec![child2],
915 );
916
917 assert_eq!(schema1.self_hash, schema2.self_hash, "self_hash should stay same (no methods changed)");
918 assert_ne!(schema1.children_hash, schema2.children_hash, "children_hash should change when child hash changes");
919 assert_ne!(schema1.hash, schema2.hash, "composite hash should change");
920 }
921
922 #[test]
923 fn test_leaf_has_no_children_hash() {
924 let schema = PluginSchema::leaf(
925 "leaf",
926 "1.0",
927 "desc",
928 vec![MethodSchema::new("method", "desc", "hash")],
929 );
930
931 assert!(schema.children_hash.is_none(), "leaf plugin should have None for children_hash");
932 assert_ne!(schema.self_hash, schema.hash, "leaf plugin's composite hash is hash(self_hash), not equal to self_hash");
933 }
934}