1use dashmap::DashMap;
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::Arc;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use super::metadata::NodeId;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum ApiMethod {
21 Get,
23 Post,
25 Put,
27 Patch,
29 Delete,
31 Stream,
33 BiStream,
35 Subscribe,
37 Notify,
39}
40
41impl ApiMethod {
42 pub fn is_idempotent(&self) -> bool {
44 matches!(self, ApiMethod::Get | ApiMethod::Put | ApiMethod::Delete)
45 }
46
47 pub fn is_streaming(&self) -> bool {
49 matches!(
50 self,
51 ApiMethod::Stream | ApiMethod::BiStream | ApiMethod::Subscribe
52 )
53 }
54
55 pub fn is_safe(&self) -> bool {
57 matches!(self, ApiMethod::Get | ApiMethod::Subscribe)
58 }
59}
60
61pub const MAX_SCHEMA_DEPTH: usize = 128;
75
76fn check_json_nesting_depth(data: &[u8], max_depth: usize) -> Result<(), serde_json::Error> {
97 use serde::de::Error;
98 let mut depth: usize = 0;
99 let mut max_seen: usize = 0;
100 let mut i = 0;
101 let n = data.len();
102 while i < n {
103 let b = data[i];
104 match b {
105 b'{' | b'[' => {
106 depth = depth.saturating_add(1);
107 if depth > max_seen {
108 max_seen = depth;
109 }
110 if depth > max_depth {
111 return Err(serde_json::Error::custom(format!(
112 "max nesting depth exceeded ({} > {})",
113 depth, max_depth
114 )));
115 }
116 i += 1;
117 }
118 b'}' | b']' => {
119 depth = depth.saturating_sub(1);
120 i += 1;
121 }
122 b'"' => {
123 i += 1;
128 while i < n {
129 match data[i] {
130 b'\\' if i + 1 < n => i += 2,
131 b'"' => {
132 i += 1;
133 break;
134 }
135 _ => i += 1,
136 }
137 }
138 }
139 _ => i += 1,
140 }
141 }
142 Ok(())
143}
144
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[serde(tag = "type", rename_all = "lowercase")]
148pub enum SchemaType {
149 Null,
151 Boolean,
153 Integer {
155 #[serde(skip_serializing_if = "Option::is_none")]
157 minimum: Option<i64>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 maximum: Option<i64>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 multiple_of: Option<i64>,
164 },
165 Number {
167 #[serde(skip_serializing_if = "Option::is_none")]
169 minimum: Option<f64>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 maximum: Option<f64>,
173 },
174 String {
176 #[serde(skip_serializing_if = "Option::is_none")]
178 min_length: Option<usize>,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 max_length: Option<usize>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pattern: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 format: Option<StringFormat>,
188 },
189 Array {
191 items: Box<SchemaType>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 min_items: Option<usize>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 max_items: Option<usize>,
199 #[serde(default)]
201 unique_items: bool,
202 },
203 Object {
205 properties: HashMap<String, SchemaType>,
207 #[serde(default)]
209 required: Vec<String>,
210 #[serde(default)]
212 additional_properties: bool,
213 },
214 Enum {
216 values: Vec<serde_json::Value>,
218 },
219 AnyOf {
221 schemas: Vec<SchemaType>,
223 },
224 Ref {
226 schema_ref: String,
228 },
229 Any,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
235#[serde(rename_all = "kebab-case")]
236pub enum StringFormat {
237 DateTime,
239 Date,
241 Time,
243 Duration,
245 Email,
247 Uri,
249 Uuid,
251 Ipv4,
253 Ipv6,
255 Base64,
257 Hex,
259 Json,
261 Markdown,
263}
264
265impl SchemaType {
266 pub fn try_from_slice(data: &[u8]) -> Result<Self, serde_json::Error> {
287 check_json_nesting_depth(data, MAX_SCHEMA_DEPTH)?;
288 serde_json::from_slice(data)
289 }
290
291 pub fn try_from_str(s: &str) -> Result<Self, serde_json::Error> {
294 Self::try_from_slice(s.as_bytes())
295 }
296
297 pub fn string() -> Self {
299 SchemaType::String {
300 min_length: None,
301 max_length: None,
302 pattern: None,
303 format: None,
304 }
305 }
306
307 pub fn integer() -> Self {
309 SchemaType::Integer {
310 minimum: None,
311 maximum: None,
312 multiple_of: None,
313 }
314 }
315
316 pub fn number() -> Self {
318 SchemaType::Number {
319 minimum: None,
320 maximum: None,
321 }
322 }
323
324 pub fn boolean() -> Self {
326 SchemaType::Boolean
327 }
328
329 pub fn array(items: SchemaType) -> Self {
331 SchemaType::Array {
332 items: Box::new(items),
333 min_items: None,
334 max_items: None,
335 unique_items: false,
336 }
337 }
338
339 pub fn object() -> Self {
341 SchemaType::Object {
342 properties: HashMap::new(),
343 required: Vec::new(),
344 additional_properties: true,
345 }
346 }
347
348 pub fn with_property(mut self, name: impl Into<String>, schema: SchemaType) -> Self {
350 if let SchemaType::Object {
351 ref mut properties, ..
352 } = self
353 {
354 properties.insert(name.into(), schema);
355 }
356 self
357 }
358
359 pub fn with_required(mut self, name: impl Into<String>) -> Self {
361 if let SchemaType::Object {
362 ref mut required, ..
363 } = self
364 {
365 required.push(name.into());
366 }
367 self
368 }
369
370 pub fn with_minimum(mut self, min: i64) -> Self {
372 if let SchemaType::Integer {
373 ref mut minimum, ..
374 } = self
375 {
376 *minimum = Some(min);
377 }
378 self
379 }
380
381 pub fn with_maximum(mut self, max: i64) -> Self {
383 if let SchemaType::Integer {
384 ref mut maximum, ..
385 } = self
386 {
387 *maximum = Some(max);
388 }
389 self
390 }
391
392 pub fn with_max_length(mut self, len: usize) -> Self {
394 if let SchemaType::String {
395 ref mut max_length, ..
396 } = self
397 {
398 *max_length = Some(len);
399 }
400 self
401 }
402
403 pub fn with_format(mut self, fmt: StringFormat) -> Self {
405 if let SchemaType::String { ref mut format, .. } = self {
406 *format = Some(fmt);
407 }
408 self
409 }
410
411 pub fn validate(&self, value: &serde_json::Value) -> Result<(), ValidationError> {
424 self.validate_with_depth(value, 0)
425 }
426
427 fn validate_with_depth(
429 &self,
430 value: &serde_json::Value,
431 depth: usize,
432 ) -> Result<(), ValidationError> {
433 if depth >= MAX_SCHEMA_DEPTH {
434 return Err(ValidationError::RecursionLimitExceeded {
435 limit: MAX_SCHEMA_DEPTH,
436 });
437 }
438 match (self, value) {
439 (SchemaType::Null, serde_json::Value::Null) => Ok(()),
440 (SchemaType::Null, _) => Err(ValidationError::TypeMismatch {
441 expected: "null".into(),
442 got: value_type_name(value),
443 }),
444
445 (SchemaType::Boolean, serde_json::Value::Bool(_)) => Ok(()),
446 (SchemaType::Boolean, _) => Err(ValidationError::TypeMismatch {
447 expected: "boolean".into(),
448 got: value_type_name(value),
449 }),
450
451 (
452 SchemaType::Integer {
453 minimum,
454 maximum,
455 multiple_of,
456 },
457 serde_json::Value::Number(n),
458 ) => {
459 let i = n.as_i64().ok_or_else(|| ValidationError::TypeMismatch {
460 expected: "integer".into(),
461 got: "float".into(),
462 })?;
463
464 if let Some(min) = minimum {
465 if i < *min {
466 return Err(ValidationError::RangeError {
467 value: i as f64,
468 min: Some(*min as f64),
469 max: None,
470 });
471 }
472 }
473 if let Some(max) = maximum {
474 if i > *max {
475 return Err(ValidationError::RangeError {
476 value: i as f64,
477 min: None,
478 max: Some(*max as f64),
479 });
480 }
481 }
482 if let Some(mult) = multiple_of {
483 if i % mult != 0 {
484 return Err(ValidationError::MultipleOfError {
485 value: i,
486 multiple_of: *mult,
487 });
488 }
489 }
490 Ok(())
491 }
492 (SchemaType::Integer { .. }, _) => Err(ValidationError::TypeMismatch {
493 expected: "integer".into(),
494 got: value_type_name(value),
495 }),
496
497 (SchemaType::Number { minimum, maximum }, serde_json::Value::Number(n)) => {
498 let f = n.as_f64().unwrap_or(0.0);
499
500 if let Some(min) = minimum {
501 if f < *min {
502 return Err(ValidationError::RangeError {
503 value: f,
504 min: Some(*min),
505 max: None,
506 });
507 }
508 }
509 if let Some(max) = maximum {
510 if f > *max {
511 return Err(ValidationError::RangeError {
512 value: f,
513 min: None,
514 max: Some(*max),
515 });
516 }
517 }
518 Ok(())
519 }
520 (SchemaType::Number { .. }, _) => Err(ValidationError::TypeMismatch {
521 expected: "number".into(),
522 got: value_type_name(value),
523 }),
524
525 (
526 SchemaType::String {
527 min_length,
528 max_length,
529 pattern,
530 format: _,
531 },
532 serde_json::Value::String(s),
533 ) => {
534 if let Some(min) = min_length {
535 if s.len() < *min {
536 return Err(ValidationError::LengthError {
537 length: s.len(),
538 min: Some(*min),
539 max: None,
540 });
541 }
542 }
543 if let Some(max) = max_length {
544 if s.len() > *max {
545 return Err(ValidationError::LengthError {
546 length: s.len(),
547 min: None,
548 max: Some(*max),
549 });
550 }
551 }
552 if let Some(pat) = pattern {
553 if !s.contains(pat.as_str()) {
555 return Err(ValidationError::PatternMismatch {
556 value: s.clone(),
557 pattern: pat.clone(),
558 });
559 }
560 }
561 Ok(())
563 }
564 (SchemaType::String { .. }, _) => Err(ValidationError::TypeMismatch {
565 expected: "string".into(),
566 got: value_type_name(value),
567 }),
568
569 (
570 SchemaType::Array {
571 items,
572 min_items,
573 max_items,
574 unique_items,
575 },
576 serde_json::Value::Array(arr),
577 ) => {
578 if let Some(min) = min_items {
579 if arr.len() < *min {
580 return Err(ValidationError::LengthError {
581 length: arr.len(),
582 min: Some(*min),
583 max: None,
584 });
585 }
586 }
587 if let Some(max) = max_items {
588 if arr.len() > *max {
589 return Err(ValidationError::LengthError {
590 length: arr.len(),
591 min: None,
592 max: Some(*max),
593 });
594 }
595 }
596 if *unique_items {
597 let mut seen = HashSet::new();
598 for v in arr {
599 let s = serde_json::to_string(v).unwrap_or_default();
600 if !seen.insert(s) {
601 return Err(ValidationError::DuplicateItems);
602 }
603 }
604 }
605 for (i, v) in arr.iter().enumerate() {
606 if let Err(e) = items.validate_with_depth(v, depth + 1) {
607 if matches!(e, ValidationError::RecursionLimitExceeded { .. }) {
613 return Err(e);
614 }
615 return Err(ValidationError::ArrayItemError {
616 index: i,
617 error: Box::new(e),
618 });
619 }
620 }
621 Ok(())
622 }
623 (SchemaType::Array { .. }, _) => Err(ValidationError::TypeMismatch {
624 expected: "array".into(),
625 got: value_type_name(value),
626 }),
627
628 (
629 SchemaType::Object {
630 properties,
631 required,
632 additional_properties,
633 },
634 serde_json::Value::Object(obj),
635 ) => {
636 for req in required {
638 if !obj.contains_key(req) {
639 return Err(ValidationError::MissingRequired { field: req.clone() });
640 }
641 }
642
643 for (key, val) in obj {
645 if let Some(schema) = properties.get(key) {
646 if let Err(e) = schema.validate_with_depth(val, depth + 1) {
647 if matches!(e, ValidationError::RecursionLimitExceeded { .. }) {
650 return Err(e);
651 }
652 return Err(ValidationError::PropertyError {
653 property: key.clone(),
654 error: Box::new(e),
655 });
656 }
657 } else if !additional_properties {
658 return Err(ValidationError::UnknownProperty {
659 property: key.clone(),
660 });
661 }
662 }
663 Ok(())
664 }
665 (SchemaType::Object { .. }, _) => Err(ValidationError::TypeMismatch {
666 expected: "object".into(),
667 got: value_type_name(value),
668 }),
669
670 (SchemaType::Enum { values }, v) => {
671 if values.contains(v) {
672 Ok(())
673 } else {
674 Err(ValidationError::EnumMismatch {
675 value: v.clone(),
676 allowed: values.clone(),
677 })
678 }
679 }
680
681 (SchemaType::AnyOf { schemas }, v) => {
682 for schema in schemas {
683 match schema.validate_with_depth(v, depth + 1) {
684 Ok(()) => return Ok(()),
685 Err(ValidationError::RecursionLimitExceeded { limit }) => {
686 return Err(ValidationError::RecursionLimitExceeded { limit });
690 }
691 Err(_) => {}
692 }
693 }
694 Err(ValidationError::AnyOfFailed {
695 schema_count: schemas.len(),
696 })
697 }
698
699 (SchemaType::Ref { .. }, _) => {
700 Ok(())
702 }
703
704 (SchemaType::Any, _) => Ok(()),
705 }
706 }
707}
708
709fn value_type_name(v: &serde_json::Value) -> String {
710 match v {
711 serde_json::Value::Null => "null".into(),
712 serde_json::Value::Bool(_) => "boolean".into(),
713 serde_json::Value::Number(_) => "number".into(),
714 serde_json::Value::String(_) => "string".into(),
715 serde_json::Value::Array(_) => "array".into(),
716 serde_json::Value::Object(_) => "object".into(),
717 }
718}
719
720#[derive(Debug, Clone, PartialEq)]
722pub enum ValidationError {
723 TypeMismatch {
725 expected: String,
727 got: String,
729 },
730 RangeError {
732 value: f64,
734 min: Option<f64>,
736 max: Option<f64>,
738 },
739 MultipleOfError {
741 value: i64,
743 multiple_of: i64,
745 },
746 LengthError {
748 length: usize,
750 min: Option<usize>,
752 max: Option<usize>,
754 },
755 PatternMismatch {
757 value: String,
759 pattern: String,
761 },
762 DuplicateItems,
764 ArrayItemError {
766 index: usize,
768 error: Box<ValidationError>,
770 },
771 MissingRequired {
773 field: String,
775 },
776 UnknownProperty {
778 property: String,
780 },
781 PropertyError {
783 property: String,
785 error: Box<ValidationError>,
787 },
788 EnumMismatch {
790 value: serde_json::Value,
792 allowed: Vec<serde_json::Value>,
794 },
795 AnyOfFailed {
797 schema_count: usize,
799 },
800 RecursionLimitExceeded {
810 limit: usize,
812 },
813}
814
815impl std::fmt::Display for ValidationError {
816 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
817 match self {
818 ValidationError::TypeMismatch { expected, got } => {
819 write!(f, "expected {}, got {}", expected, got)
820 }
821 ValidationError::RangeError { value, min, max } => {
822 write!(f, "value {} out of range [{:?}, {:?}]", value, min, max)
823 }
824 ValidationError::MultipleOfError { value, multiple_of } => {
825 write!(f, "{} is not a multiple of {}", value, multiple_of)
826 }
827 ValidationError::LengthError { length, min, max } => {
828 write!(f, "length {} out of range [{:?}, {:?}]", length, min, max)
829 }
830 ValidationError::PatternMismatch { value, pattern } => {
831 write!(f, "'{}' does not match pattern '{}'", value, pattern)
832 }
833 ValidationError::DuplicateItems => write!(f, "duplicate items in array"),
834 ValidationError::ArrayItemError { index, error } => {
835 write!(f, "item [{}]: {}", index, error)
836 }
837 ValidationError::MissingRequired { field } => {
838 write!(f, "missing required field: {}", field)
839 }
840 ValidationError::UnknownProperty { property } => {
841 write!(f, "unknown property: {}", property)
842 }
843 ValidationError::PropertyError { property, error } => {
844 write!(f, "property '{}': {}", property, error)
845 }
846 ValidationError::EnumMismatch { value, .. } => {
847 write!(f, "{:?} is not a valid enum value", value)
848 }
849 ValidationError::AnyOfFailed { schema_count } => {
850 write!(f, "value did not match any of {} schemas", schema_count)
851 }
852 ValidationError::RecursionLimitExceeded { limit } => {
853 write!(f, "schema recursion depth exceeded {}", limit)
854 }
855 }
856 }
857}
858
859impl std::error::Error for ValidationError {}
860
861#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
863pub struct ApiParameter {
864 pub name: String,
866 pub description: Option<String>,
868 pub required: bool,
870 pub schema: SchemaType,
872 pub default: Option<serde_json::Value>,
874 pub example: Option<serde_json::Value>,
876}
877
878impl ApiParameter {
879 pub fn required(name: impl Into<String>, schema: SchemaType) -> Self {
881 Self {
882 name: name.into(),
883 description: None,
884 required: true,
885 schema,
886 default: None,
887 example: None,
888 }
889 }
890
891 pub fn optional(name: impl Into<String>, schema: SchemaType) -> Self {
893 Self {
894 name: name.into(),
895 description: None,
896 required: false,
897 schema,
898 default: None,
899 example: None,
900 }
901 }
902
903 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
905 self.description = Some(desc.into());
906 self
907 }
908
909 pub fn with_default(mut self, default: serde_json::Value) -> Self {
911 self.default = Some(default);
912 self
913 }
914
915 pub fn with_example(mut self, example: serde_json::Value) -> Self {
917 self.example = Some(example);
918 self
919 }
920}
921
922#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
924pub struct ApiEndpoint {
925 pub path: String,
927 pub method: ApiMethod,
929 pub description: Option<String>,
931 pub path_params: Vec<ApiParameter>,
933 pub query_params: Vec<ApiParameter>,
935 pub request_body: Option<SchemaType>,
937 pub response: Option<SchemaType>,
939 pub error_response: Option<SchemaType>,
941 pub required_capabilities: Vec<String>,
943 pub tags: Vec<String>,
945 pub deprecated: bool,
947 pub rate_limit: Option<u32>,
949 pub timeout_ms: Option<u64>,
951 pub auth_required: bool,
953}
954
955impl ApiEndpoint {
956 pub fn new(path: impl Into<String>, method: ApiMethod) -> Self {
958 Self {
959 path: path.into(),
960 method,
961 description: None,
962 path_params: Vec::new(),
963 query_params: Vec::new(),
964 request_body: None,
965 response: None,
966 error_response: None,
967 required_capabilities: Vec::new(),
968 tags: Vec::new(),
969 deprecated: false,
970 rate_limit: None,
971 timeout_ms: None,
972 auth_required: true,
973 }
974 }
975
976 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
978 self.description = Some(desc.into());
979 self
980 }
981
982 pub fn with_path_param(mut self, param: ApiParameter) -> Self {
984 self.path_params.push(param);
985 self
986 }
987
988 pub fn with_query_param(mut self, param: ApiParameter) -> Self {
990 self.query_params.push(param);
991 self
992 }
993
994 pub fn with_request_body(mut self, schema: SchemaType) -> Self {
996 self.request_body = Some(schema);
997 self
998 }
999
1000 pub fn with_response(mut self, schema: SchemaType) -> Self {
1002 self.response = Some(schema);
1003 self
1004 }
1005
1006 pub fn require_capability(mut self, cap: impl Into<String>) -> Self {
1008 self.required_capabilities.push(cap.into());
1009 self
1010 }
1011
1012 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1014 self.tags.push(tag.into());
1015 self
1016 }
1017
1018 pub fn with_rate_limit(mut self, requests_per_min: u32) -> Self {
1020 self.rate_limit = Some(requests_per_min);
1021 self
1022 }
1023
1024 pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
1026 self.timeout_ms = Some(timeout_ms);
1027 self
1028 }
1029
1030 pub fn no_auth(mut self) -> Self {
1032 self.auth_required = false;
1033 self
1034 }
1035
1036 pub fn deprecated(mut self) -> Self {
1038 self.deprecated = true;
1039 self
1040 }
1041
1042 pub fn validate_request(
1044 &self,
1045 path_params: &HashMap<String, serde_json::Value>,
1046 query_params: &HashMap<String, serde_json::Value>,
1047 body: Option<&serde_json::Value>,
1048 ) -> Result<(), ApiValidationError> {
1049 for param in &self.path_params {
1051 if let Some(value) = path_params.get(¶m.name) {
1052 param
1053 .schema
1054 .validate(value)
1055 .map_err(|e| ApiValidationError::PathParameter {
1056 name: param.name.clone(),
1057 error: e,
1058 })?;
1059 } else if param.required {
1060 return Err(ApiValidationError::MissingPathParameter {
1061 name: param.name.clone(),
1062 });
1063 }
1064 }
1065
1066 for param in &self.query_params {
1068 if let Some(value) = query_params.get(¶m.name) {
1069 param
1070 .schema
1071 .validate(value)
1072 .map_err(|e| ApiValidationError::QueryParameter {
1073 name: param.name.clone(),
1074 error: e,
1075 })?;
1076 } else if param.required {
1077 return Err(ApiValidationError::MissingQueryParameter {
1078 name: param.name.clone(),
1079 });
1080 }
1081 }
1082
1083 if let Some(body_schema) = &self.request_body {
1085 match body {
1086 Some(b) => {
1087 body_schema
1088 .validate(b)
1089 .map_err(|e| ApiValidationError::RequestBody { error: e })?;
1090 }
1091 None => {
1092 return Err(ApiValidationError::MissingRequestBody);
1093 }
1094 }
1095 }
1096
1097 Ok(())
1098 }
1099
1100 pub fn matches_path(&self, path: &str) -> Option<HashMap<String, String>> {
1102 let self_parts: Vec<&str> = self.path.split('/').collect();
1103 let path_parts: Vec<&str> = path.split('/').collect();
1104
1105 if self_parts.len() != path_parts.len() {
1106 return None;
1107 }
1108
1109 let mut params = HashMap::new();
1110
1111 for (self_part, path_part) in self_parts.iter().zip(path_parts.iter()) {
1112 if self_part.starts_with('{') && self_part.ends_with('}') {
1113 let param_name = &self_part[1..self_part.len() - 1];
1115 params.insert(param_name.to_string(), path_part.to_string());
1116 } else if self_part != path_part {
1117 return None;
1118 }
1119 }
1120
1121 Some(params)
1122 }
1123}
1124
1125#[derive(Debug, Clone, PartialEq)]
1127pub enum ApiValidationError {
1128 MissingPathParameter {
1130 name: String,
1132 },
1133 PathParameter {
1135 name: String,
1137 error: ValidationError,
1139 },
1140 MissingQueryParameter {
1142 name: String,
1144 },
1145 QueryParameter {
1147 name: String,
1149 error: ValidationError,
1151 },
1152 MissingRequestBody,
1154 RequestBody {
1156 error: ValidationError,
1158 },
1159}
1160
1161impl std::fmt::Display for ApiValidationError {
1162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1163 match self {
1164 ApiValidationError::MissingPathParameter { name } => {
1165 write!(f, "missing path parameter: {}", name)
1166 }
1167 ApiValidationError::PathParameter { name, error } => {
1168 write!(f, "path parameter '{}': {}", name, error)
1169 }
1170 ApiValidationError::MissingQueryParameter { name } => {
1171 write!(f, "missing query parameter: {}", name)
1172 }
1173 ApiValidationError::QueryParameter { name, error } => {
1174 write!(f, "query parameter '{}': {}", name, error)
1175 }
1176 ApiValidationError::MissingRequestBody => write!(f, "missing request body"),
1177 ApiValidationError::RequestBody { error } => {
1178 write!(f, "request body: {}", error)
1179 }
1180 }
1181 }
1182}
1183
1184impl std::error::Error for ApiValidationError {}
1185
1186#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1188pub struct ApiVersion {
1189 pub major: u32,
1191 pub minor: u32,
1193 pub patch: u32,
1195}
1196
1197impl ApiVersion {
1198 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
1200 Self {
1201 major,
1202 minor,
1203 patch,
1204 }
1205 }
1206
1207 pub fn is_compatible_with(&self, required: &ApiVersion) -> bool {
1209 if self.major != required.major {
1211 return false;
1212 }
1213 if self.minor < required.minor {
1215 return false;
1216 }
1217 if self.minor == required.minor && self.patch < required.patch {
1219 return false;
1220 }
1221 true
1222 }
1223
1224 pub fn parse(s: &str) -> Option<Self> {
1226 let parts: Vec<&str> = s.split('.').collect();
1227 if parts.len() != 3 {
1228 return None;
1229 }
1230 Some(Self {
1231 major: parts[0].parse().ok()?,
1232 minor: parts[1].parse().ok()?,
1233 patch: parts[2].parse().ok()?,
1234 })
1235 }
1236}
1237
1238impl std::fmt::Display for ApiVersion {
1239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1240 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
1241 }
1242}
1243
1244impl PartialOrd for ApiVersion {
1245 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1246 Some(self.cmp(other))
1247 }
1248}
1249
1250impl Ord for ApiVersion {
1251 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1252 match self.major.cmp(&other.major) {
1253 std::cmp::Ordering::Equal => match self.minor.cmp(&other.minor) {
1254 std::cmp::Ordering::Equal => self.patch.cmp(&other.patch),
1255 ord => ord,
1256 },
1257 ord => ord,
1258 }
1259 }
1260}
1261
1262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1264pub struct ApiSchema {
1265 pub name: String,
1267 pub description: Option<String>,
1269 pub version: ApiVersion,
1271 pub base_path: String,
1273 pub endpoints: Vec<ApiEndpoint>,
1275 pub definitions: HashMap<String, SchemaType>,
1277 pub tags: Vec<String>,
1279 pub contact: Option<String>,
1281 pub license: Option<String>,
1283}
1284
1285impl ApiSchema {
1286 pub fn new(name: impl Into<String>, version: ApiVersion) -> Self {
1288 Self {
1289 name: name.into(),
1290 description: None,
1291 version,
1292 base_path: "/".into(),
1293 endpoints: Vec::new(),
1294 definitions: HashMap::new(),
1295 tags: Vec::new(),
1296 contact: None,
1297 license: None,
1298 }
1299 }
1300
1301 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
1303 self.description = Some(desc.into());
1304 self
1305 }
1306
1307 pub fn with_base_path(mut self, path: impl Into<String>) -> Self {
1309 self.base_path = path.into();
1310 self
1311 }
1312
1313 pub fn add_endpoint(mut self, endpoint: ApiEndpoint) -> Self {
1315 self.endpoints.push(endpoint);
1316 self
1317 }
1318
1319 pub fn add_definition(mut self, name: impl Into<String>, schema: SchemaType) -> Self {
1321 self.definitions.insert(name.into(), schema);
1322 self
1323 }
1324
1325 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1327 self.tags.push(tag.into());
1328 self
1329 }
1330
1331 pub fn find_endpoint(&self, path: &str, method: ApiMethod) -> Option<&ApiEndpoint> {
1333 let full_path = if path.starts_with(&self.base_path) {
1334 path.to_string()
1335 } else {
1336 format!("{}{}", self.base_path.trim_end_matches('/'), path)
1337 };
1338
1339 self.endpoints
1340 .iter()
1341 .find(|e| e.method == method && e.matches_path(&full_path).is_some())
1342 }
1343
1344 pub fn endpoints_by_tag(&self, tag: &str) -> Vec<&ApiEndpoint> {
1346 self.endpoints
1347 .iter()
1348 .filter(|e| e.tags.contains(&tag.to_string()))
1349 .collect()
1350 }
1351
1352 pub fn to_bytes(&self) -> Vec<u8> {
1354 serde_json::to_vec(self).unwrap_or_default()
1355 }
1356
1357 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
1359 serde_json::from_slice(bytes).ok()
1360 }
1361}
1362
1363#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1365pub struct ApiAnnouncement {
1366 pub node_id: NodeId,
1368 pub schemas: Vec<ApiSchema>,
1370 pub version: u64,
1372 pub timestamp: u64,
1374 pub ttl_secs: u32,
1376}
1377
1378impl ApiAnnouncement {
1379 pub fn new(node_id: NodeId, schemas: Vec<ApiSchema>) -> Self {
1381 Self {
1382 node_id,
1383 schemas,
1384 version: 1,
1385 timestamp: SystemTime::now()
1386 .duration_since(UNIX_EPOCH)
1387 .unwrap_or_default()
1388 .as_millis() as u64,
1389 ttl_secs: 300,
1390 }
1391 }
1392
1393 pub fn with_version(mut self, version: u64) -> Self {
1395 self.version = version;
1396 self
1397 }
1398
1399 pub fn with_ttl(mut self, ttl_secs: u32) -> Self {
1401 self.ttl_secs = ttl_secs;
1402 self
1403 }
1404
1405 pub fn is_expired(&self) -> bool {
1407 let now = SystemTime::now()
1408 .duration_since(UNIX_EPOCH)
1409 .unwrap_or_default()
1410 .as_millis() as u64;
1411 let expiry = self.timestamp + (self.ttl_secs as u64 * 1000);
1412 now > expiry
1413 }
1414
1415 pub fn to_bytes(&self) -> Vec<u8> {
1417 serde_json::to_vec(self).unwrap_or_default()
1418 }
1419
1420 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
1422 serde_json::from_slice(bytes).ok()
1423 }
1424}
1425
1426#[derive(Debug, Clone, Default)]
1428pub struct ApiQuery {
1429 pub api_name: Option<String>,
1431 pub min_version: Option<ApiVersion>,
1433 pub endpoint_path: Option<String>,
1435 pub endpoint_method: Option<ApiMethod>,
1437 pub tag: Option<String>,
1439 pub capability: Option<String>,
1441}
1442
1443impl ApiQuery {
1444 pub fn new() -> Self {
1446 Self::default()
1447 }
1448
1449 pub fn with_api(mut self, name: impl Into<String>) -> Self {
1451 self.api_name = Some(name.into());
1452 self
1453 }
1454
1455 pub fn with_min_version(mut self, version: ApiVersion) -> Self {
1457 self.min_version = Some(version);
1458 self
1459 }
1460
1461 pub fn with_endpoint(mut self, path: impl Into<String>) -> Self {
1463 self.endpoint_path = Some(path.into());
1464 self
1465 }
1466
1467 pub fn with_method(mut self, method: ApiMethod) -> Self {
1469 self.endpoint_method = Some(method);
1470 self
1471 }
1472
1473 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1475 self.tag = Some(tag.into());
1476 self
1477 }
1478
1479 pub fn with_capability(mut self, cap: impl Into<String>) -> Self {
1481 self.capability = Some(cap.into());
1482 self
1483 }
1484
1485 pub fn matches_schema(&self, schema: &ApiSchema) -> bool {
1487 if let Some(ref name) = self.api_name {
1489 if &schema.name != name {
1490 return false;
1491 }
1492 }
1493
1494 if let Some(ref min_ver) = self.min_version {
1496 if !schema.version.is_compatible_with(min_ver) {
1497 return false;
1498 }
1499 }
1500
1501 if let Some(ref path) = self.endpoint_path {
1503 let method = self.endpoint_method;
1504 let found = schema.endpoints.iter().any(|e| {
1505 let path_matches = e.matches_path(path).is_some() || e.path.contains(path);
1506 let method_matches = method.is_none_or(|m| e.method == m);
1507 path_matches && method_matches
1508 });
1509 if !found {
1510 return false;
1511 }
1512 }
1513
1514 if let Some(ref tag) = self.tag {
1516 if !schema.tags.contains(tag) {
1517 return false;
1518 }
1519 }
1520
1521 if let Some(ref cap) = self.capability {
1523 let found = schema
1524 .endpoints
1525 .iter()
1526 .any(|e| e.required_capabilities.contains(cap));
1527 if !found {
1528 return false;
1529 }
1530 }
1531
1532 true
1533 }
1534}
1535
1536#[derive(Debug, Clone, PartialEq, Eq)]
1538pub enum RegistryError {
1539 NodeNotFound(NodeId),
1541 ApiNotFound(String),
1543 VersionConflict {
1545 expected: u64,
1547 actual: u64,
1549 },
1550 CapacityExceeded,
1552}
1553
1554impl std::fmt::Display for RegistryError {
1555 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1556 match self {
1557 RegistryError::NodeNotFound(_) => write!(f, "Node not found"),
1558 RegistryError::ApiNotFound(name) => write!(f, "API not found: {}", name),
1559 RegistryError::VersionConflict { expected, actual } => {
1560 write!(f, "Version conflict: expected {}, got {}", expected, actual)
1561 }
1562 RegistryError::CapacityExceeded => write!(f, "Registry capacity exceeded"),
1563 }
1564 }
1565}
1566
1567impl std::error::Error for RegistryError {}
1568
1569#[derive(Debug, Clone)]
1571pub struct IndexedApiNode {
1572 pub node_id: NodeId,
1574 pub announcement: Arc<ApiAnnouncement>,
1576}
1577
1578#[derive(Debug, Clone, Default)]
1580pub struct ApiRegistryStats {
1581 pub total_nodes: usize,
1583 pub total_schemas: usize,
1585 pub total_endpoints: usize,
1587 pub apis_by_name: HashMap<String, usize>,
1589 pub queries: u64,
1591 pub updates: u64,
1593}
1594
1595pub struct ApiRegistry {
1597 nodes: DashMap<NodeId, Arc<ApiAnnouncement>>,
1599 by_api_name: DashMap<String, HashSet<NodeId>>,
1601 by_tag: DashMap<String, HashSet<NodeId>>,
1603 by_endpoint: DashMap<String, HashSet<NodeId>>,
1605 query_count: AtomicU64,
1607 update_count: AtomicU64,
1609 max_capacity: Option<usize>,
1611}
1612
1613fn endpoint_prefix(path: &str) -> String {
1620 match path.match_indices('/').nth(1) {
1621 Some((idx, _)) => path[..idx].to_string(),
1622 None => path.to_string(),
1623 }
1624}
1625
1626impl ApiRegistry {
1627 pub fn new() -> Self {
1629 Self {
1630 nodes: DashMap::new(),
1631 by_api_name: DashMap::new(),
1632 by_tag: DashMap::new(),
1633 by_endpoint: DashMap::new(),
1634 query_count: AtomicU64::new(0),
1635 update_count: AtomicU64::new(0),
1636 max_capacity: None,
1637 }
1638 }
1639
1640 pub fn with_capacity(max: usize) -> Self {
1642 let mut reg = Self::new();
1643 reg.max_capacity = Some(max);
1644 reg
1645 }
1646
1647 pub fn register(&self, announcement: ApiAnnouncement) -> Result<(), RegistryError> {
1649 let node_id = announcement.node_id;
1650
1651 if let Some(max) = self.max_capacity {
1653 if !self.nodes.contains_key(&node_id) && self.nodes.len() >= max {
1654 return Err(RegistryError::CapacityExceeded);
1655 }
1656 }
1657
1658 if let Some(old) = self.nodes.get(&node_id) {
1660 self.remove_from_indexes(&old);
1661 }
1662
1663 let ann = Arc::new(announcement);
1664
1665 self.add_to_indexes(&ann);
1667
1668 self.nodes.insert(node_id, ann);
1670 self.update_count.fetch_add(1, Ordering::Relaxed);
1671
1672 Ok(())
1673 }
1674
1675 pub fn unregister(&self, node_id: &NodeId) -> Option<Arc<ApiAnnouncement>> {
1677 if let Some((_, ann)) = self.nodes.remove(node_id) {
1678 self.remove_from_indexes(&ann);
1679 Some(ann)
1680 } else {
1681 None
1682 }
1683 }
1684
1685 pub fn get(&self, node_id: &NodeId) -> Option<Arc<ApiAnnouncement>> {
1687 self.nodes.get(node_id).map(|r| Arc::clone(&r))
1688 }
1689
1690 pub fn query(&self, query: &ApiQuery) -> Vec<IndexedApiNode> {
1692 self.query_count.fetch_add(1, Ordering::Relaxed);
1693
1694 let candidates: Vec<NodeId> = if let Some(ref api_name) = query.api_name {
1696 self.by_api_name
1697 .get(api_name)
1698 .map(|s| s.iter().copied().collect())
1699 .unwrap_or_default()
1700 } else if let Some(ref tag) = query.tag {
1701 self.by_tag
1702 .get(tag)
1703 .map(|s| s.iter().copied().collect())
1704 .unwrap_or_default()
1705 } else {
1706 self.nodes.iter().map(|r| *r.key()).collect()
1708 };
1709
1710 candidates
1712 .into_iter()
1713 .filter_map(|id| {
1714 let ann = self.nodes.get(&id)?;
1715 let matches = ann.schemas.iter().any(|s| query.matches_schema(s));
1717 if matches && !ann.is_expired() {
1718 Some(IndexedApiNode {
1719 node_id: id,
1720 announcement: Arc::clone(&ann),
1721 })
1722 } else {
1723 None
1724 }
1725 })
1726 .collect()
1727 }
1728
1729 pub fn find_by_endpoint(&self, path: &str, method: ApiMethod) -> Vec<IndexedApiNode> {
1731 self.query_count.fetch_add(1, Ordering::Relaxed);
1732
1733 self.nodes
1734 .iter()
1735 .filter_map(|entry| {
1736 let ann = entry.value();
1737 if ann.is_expired() {
1738 return None;
1739 }
1740
1741 let has_endpoint = ann.schemas.iter().any(|schema| {
1743 schema
1744 .endpoints
1745 .iter()
1746 .any(|e| e.method == method && e.matches_path(path).is_some())
1747 });
1748
1749 if has_endpoint {
1750 Some(IndexedApiNode {
1751 node_id: *entry.key(),
1752 announcement: Arc::clone(ann),
1753 })
1754 } else {
1755 None
1756 }
1757 })
1758 .collect()
1759 }
1760
1761 pub fn find_compatible(&self, api_name: &str, min_version: &ApiVersion) -> Vec<IndexedApiNode> {
1763 self.query_count.fetch_add(1, Ordering::Relaxed);
1764
1765 let candidates = self
1766 .by_api_name
1767 .get(api_name)
1768 .map(|s| s.iter().copied().collect::<Vec<_>>())
1769 .unwrap_or_default();
1770
1771 candidates
1772 .into_iter()
1773 .filter_map(|id| {
1774 let ann = self.nodes.get(&id)?;
1775 if ann.is_expired() {
1776 return None;
1777 }
1778
1779 let compatible = ann.schemas.iter().any(|schema| {
1780 schema.name == api_name && schema.version.is_compatible_with(min_version)
1781 });
1782
1783 if compatible {
1784 Some(IndexedApiNode {
1785 node_id: id,
1786 announcement: Arc::clone(&ann),
1787 })
1788 } else {
1789 None
1790 }
1791 })
1792 .collect()
1793 }
1794
1795 pub fn stats(&self) -> ApiRegistryStats {
1797 let mut apis_by_name: HashMap<String, usize> = HashMap::new();
1798 let mut total_endpoints = 0;
1799
1800 for entry in self.nodes.iter() {
1801 for schema in &entry.value().schemas {
1802 *apis_by_name.entry(schema.name.clone()).or_default() += 1;
1803 total_endpoints += schema.endpoints.len();
1804 }
1805 }
1806
1807 ApiRegistryStats {
1808 total_nodes: self.nodes.len(),
1809 total_schemas: apis_by_name.values().sum(),
1810 total_endpoints,
1811 apis_by_name,
1812 queries: self.query_count.load(Ordering::Relaxed),
1813 updates: self.update_count.load(Ordering::Relaxed),
1814 }
1815 }
1816
1817 pub fn len(&self) -> usize {
1819 self.nodes.len()
1820 }
1821
1822 pub fn is_empty(&self) -> bool {
1824 self.nodes.is_empty()
1825 }
1826
1827 pub fn clear(&self) {
1829 self.nodes.clear();
1830 self.by_api_name.clear();
1831 self.by_tag.clear();
1832 self.by_endpoint.clear();
1833 }
1834
1835 pub fn cleanup_expired(&self) -> usize {
1837 let expired: Vec<NodeId> = self
1838 .nodes
1839 .iter()
1840 .filter(|e| e.value().is_expired())
1841 .map(|e| *e.key())
1842 .collect();
1843
1844 let count = expired.len();
1845 for id in expired {
1846 self.unregister(&id);
1847 }
1848 count
1849 }
1850
1851 fn add_to_indexes(&self, ann: &ApiAnnouncement) {
1853 let node_id = ann.node_id;
1854
1855 for schema in &ann.schemas {
1856 self.by_api_name
1858 .entry(schema.name.clone())
1859 .or_default()
1860 .insert(node_id);
1861
1862 for tag in &schema.tags {
1864 self.by_tag.entry(tag.clone()).or_default().insert(node_id);
1865 }
1866
1867 for endpoint in &schema.endpoints {
1869 let prefix = endpoint_prefix(&endpoint.path);
1870 self.by_endpoint.entry(prefix).or_default().insert(node_id);
1871 }
1872 }
1873 }
1874
1875 fn remove_from_indexes(&self, ann: &ApiAnnouncement) {
1877 let node_id = ann.node_id;
1878
1879 for schema in &ann.schemas {
1880 if let Some(mut set) = self.by_api_name.get_mut(&schema.name) {
1881 set.remove(&node_id);
1882 }
1883
1884 for tag in &schema.tags {
1885 if let Some(mut set) = self.by_tag.get_mut(tag) {
1886 set.remove(&node_id);
1887 }
1888 }
1889
1890 for endpoint in &schema.endpoints {
1891 let prefix = endpoint_prefix(&endpoint.path);
1892 if let Some(mut set) = self.by_endpoint.get_mut(&prefix) {
1893 set.remove(&node_id);
1894 }
1895 }
1896 }
1897 }
1898}
1899
1900impl Default for ApiRegistry {
1901 fn default() -> Self {
1902 Self::new()
1903 }
1904}
1905
1906#[cfg(test)]
1907mod tests {
1908 use super::*;
1909
1910 fn make_node_id(n: u8) -> NodeId {
1911 let mut id = [0u8; 32];
1912 id[0] = n;
1913 id
1914 }
1915
1916 #[test]
1917 fn test_schema_type_validation() {
1918 let schema = SchemaType::string().with_max_length(10);
1920 assert!(schema.validate(&serde_json::json!("hello")).is_ok());
1921 assert!(schema.validate(&serde_json::json!("hello world!")).is_err());
1922
1923 let schema = SchemaType::integer().with_minimum(0).with_maximum(100);
1925 assert!(schema.validate(&serde_json::json!(50)).is_ok());
1926 assert!(schema.validate(&serde_json::json!(-1)).is_err());
1927 assert!(schema.validate(&serde_json::json!(101)).is_err());
1928
1929 let schema = SchemaType::object()
1931 .with_property("name", SchemaType::string())
1932 .with_property("age", SchemaType::integer())
1933 .with_required("name");
1934
1935 assert!(schema
1936 .validate(&serde_json::json!({"name": "Alice", "age": 30}))
1937 .is_ok());
1938 assert!(schema.validate(&serde_json::json!({"age": 30})).is_err()); let schema = SchemaType::array(SchemaType::integer());
1942 assert!(schema.validate(&serde_json::json!([1, 2, 3])).is_ok());
1943 assert!(schema.validate(&serde_json::json!([1, "two", 3])).is_err());
1944 }
1945
1946 #[test]
1962 fn validate_returns_recursion_limit_error_on_deeply_nested_schema() {
1963 let mut schema = SchemaType::integer();
1966 for _ in 0..MAX_SCHEMA_DEPTH + 5 {
1967 schema = SchemaType::array(schema);
1968 }
1969
1970 let mut value = serde_json::json!(1);
1973 for _ in 0..MAX_SCHEMA_DEPTH + 5 {
1974 value = serde_json::json!([value]);
1975 }
1976
1977 let result = schema.validate(&value);
1979 match result {
1980 Err(ValidationError::RecursionLimitExceeded { limit }) => {
1981 assert_eq!(limit, MAX_SCHEMA_DEPTH);
1982 }
1983 other => panic!("expected RecursionLimitExceeded, got {:?}", other),
1984 }
1985 }
1986
1987 #[test]
1991 fn validate_accepts_schema_at_recursion_limit() {
1992 let mut schema = SchemaType::integer();
1993 for _ in 0..(MAX_SCHEMA_DEPTH - 1) {
1996 schema = SchemaType::array(schema);
1997 }
1998 let mut value = serde_json::json!(1);
1999 for _ in 0..(MAX_SCHEMA_DEPTH - 1) {
2000 value = serde_json::json!([value]);
2001 }
2002 assert!(
2003 schema.validate(&value).is_ok(),
2004 "schema right at the depth limit must still validate"
2005 );
2006 }
2007
2008 #[test]
2015 fn try_from_slice_rejects_input_over_max_schema_depth() {
2016 let depth = MAX_SCHEMA_DEPTH + 50;
2020 let mut s = String::new();
2021 for _ in 0..depth {
2022 s.push('[');
2023 }
2024 s.push_str("null");
2025 for _ in 0..depth {
2026 s.push(']');
2027 }
2028 let err = SchemaType::try_from_str(&s)
2029 .expect_err("deeply-nested JSON must be rejected by the depth pre-scan");
2030 let msg = format!("{}", err);
2031 assert!(
2032 msg.contains("max nesting depth exceeded"),
2033 "error message must name the depth cap; got: {}",
2034 msg
2035 );
2036 }
2037
2038 #[test]
2043 fn try_from_slice_handles_brackets_inside_strings_correctly() {
2044 let json = r#"{"type":"string","pattern":"[}{]\""}"#;
2048 let r = SchemaType::try_from_str(json);
2049 assert!(
2050 r.is_ok(),
2051 "valid schema with bracket-bearing string must parse: {:?}",
2052 r.err()
2053 );
2054 }
2055
2056 #[test]
2066 fn try_from_slice_accepts_normal_depth_schema() {
2067 let depth = 32usize;
2068 let mut s = String::new();
2069 for _ in 0..depth {
2070 s.push_str(r#"{"type":"array","items":"#);
2071 }
2072 s.push_str(r#"{"type":"null"}"#);
2073 for _ in 0..depth {
2074 s.push('}');
2075 }
2076 let r = SchemaType::try_from_str(&s);
2077 assert!(
2078 r.is_ok(),
2079 "moderately-nested schema (depth {}) must parse; got: {:?}",
2080 depth,
2081 r.err()
2082 );
2083 }
2084
2085 #[test]
2089 fn check_json_nesting_depth_unit() {
2090 assert!(check_json_nesting_depth(b"{}", 1).is_ok());
2091 assert!(check_json_nesting_depth(b"{}", 0).is_err()); assert!(check_json_nesting_depth(b"[[[[]]]]", 4).is_ok());
2093 assert!(check_json_nesting_depth(b"[[[[]]]]", 3).is_err());
2094 assert!(check_json_nesting_depth(b"\"[[[[\"", 0).is_ok());
2096 assert!(check_json_nesting_depth(b"\"[\\\"[[\"", 0).is_ok());
2098 assert!(check_json_nesting_depth(b"{\"a\":[1,2]}", 2).is_ok());
2100 assert!(check_json_nesting_depth(b"{\"a\":[1,2]}", 1).is_err());
2101 }
2102
2103 #[test]
2104 fn test_api_endpoint_path_matching() {
2105 let endpoint = ApiEndpoint::new("/models/{model_id}/infer", ApiMethod::Post)
2106 .with_path_param(ApiParameter::required("model_id", SchemaType::string()));
2107
2108 let params = endpoint.matches_path("/models/llama-7b/infer");
2110 assert!(params.is_some());
2111 let params = params.unwrap();
2112 assert_eq!(params.get("model_id"), Some(&"llama-7b".to_string()));
2113
2114 assert!(endpoint.matches_path("/models/llama-7b/train").is_none());
2116 assert!(endpoint.matches_path("/models/infer").is_none());
2117 }
2118
2119 #[test]
2120 fn test_api_version_compatibility() {
2121 let v1_0_0 = ApiVersion::new(1, 0, 0);
2122 let v1_1_0 = ApiVersion::new(1, 1, 0);
2123 let v1_1_1 = ApiVersion::new(1, 1, 1);
2124 let v2_0_0 = ApiVersion::new(2, 0, 0);
2125
2126 assert!(v1_0_0.is_compatible_with(&v1_0_0));
2128
2129 assert!(v1_1_0.is_compatible_with(&v1_0_0));
2131
2132 assert!(v1_1_1.is_compatible_with(&v1_1_0));
2134
2135 assert!(!v1_0_0.is_compatible_with(&v1_1_0));
2137
2138 assert!(!v2_0_0.is_compatible_with(&v1_0_0));
2140 assert!(!v1_0_0.is_compatible_with(&v2_0_0));
2141 }
2142
2143 #[test]
2144 fn test_api_schema() {
2145 let schema = ApiSchema::new("inference", ApiVersion::new(1, 0, 0))
2146 .with_description("Model inference API")
2147 .with_base_path("/api/v1")
2148 .with_tag("ai")
2149 .add_endpoint(
2150 ApiEndpoint::new("/models/{model_id}/infer", ApiMethod::Post)
2151 .with_description("Run inference on a model")
2152 .with_tag("inference"),
2153 )
2154 .add_endpoint(
2155 ApiEndpoint::new("/models", ApiMethod::Get)
2156 .with_description("List available models")
2157 .with_tag("models"),
2158 );
2159
2160 assert_eq!(schema.endpoints.len(), 2);
2161 assert!(schema.tags.contains(&"ai".to_string()));
2162
2163 let inference_endpoints = schema.endpoints_by_tag("inference");
2165 assert_eq!(inference_endpoints.len(), 1);
2166 }
2167
2168 #[test]
2169 fn test_api_registry_basic() {
2170 let registry = ApiRegistry::new();
2171
2172 let schema = ApiSchema::new("test-api", ApiVersion::new(1, 0, 0))
2173 .with_tag("test")
2174 .add_endpoint(ApiEndpoint::new("/test", ApiMethod::Get));
2175
2176 let ann = ApiAnnouncement::new(make_node_id(1), vec![schema]);
2177 registry.register(ann).unwrap();
2178
2179 assert_eq!(registry.len(), 1);
2180
2181 let result = registry.get(&make_node_id(1));
2182 assert!(result.is_some());
2183
2184 registry.unregister(&make_node_id(1));
2185 assert_eq!(registry.len(), 0);
2186 }
2187
2188 #[test]
2189 fn test_api_registry_query() {
2190 let registry = ApiRegistry::new();
2191
2192 for i in 0..10 {
2194 let api_name = if i < 5 { "inference" } else { "training" };
2195 let tag = if i % 2 == 0 { "gpu" } else { "cpu" };
2196
2197 let schema = ApiSchema::new(api_name, ApiVersion::new(1, i as u32, 0))
2198 .with_tag(tag)
2199 .add_endpoint(ApiEndpoint::new("/run", ApiMethod::Post));
2200
2201 let ann = ApiAnnouncement::new(make_node_id(i), vec![schema]);
2202 registry.register(ann).unwrap();
2203 }
2204
2205 let results = registry.query(&ApiQuery::new().with_api("inference"));
2207 assert_eq!(results.len(), 5);
2208
2209 let results = registry.query(&ApiQuery::new().with_tag("gpu"));
2211 assert_eq!(results.len(), 5);
2212
2213 let results = registry.query(&ApiQuery::new().with_api("inference").with_tag("gpu"));
2215 assert_eq!(results.len(), 3);
2217 }
2218
2219 #[test]
2220 fn test_api_registry_version_compatibility() {
2221 let registry = ApiRegistry::new();
2222
2223 for i in 0..5 {
2225 let schema = ApiSchema::new("my-api", ApiVersion::new(1, i as u32, 0));
2226 let ann = ApiAnnouncement::new(make_node_id(i), vec![schema]);
2227 registry.register(ann).unwrap();
2228 }
2229
2230 let results = registry.find_compatible("my-api", &ApiVersion::new(1, 2, 0));
2232 assert_eq!(results.len(), 3);
2234 }
2235
2236 #[test]
2237 fn test_request_validation() {
2238 let endpoint = ApiEndpoint::new("/users/{user_id}", ApiMethod::Get)
2239 .with_path_param(ApiParameter::required("user_id", SchemaType::string()))
2240 .with_query_param(ApiParameter::optional("limit", SchemaType::integer()));
2241
2242 let mut path_params = HashMap::new();
2244 path_params.insert("user_id".to_string(), serde_json::json!("123"));
2245
2246 let query_params = HashMap::new();
2247
2248 let result = endpoint.validate_request(&path_params, &query_params, None);
2249 assert!(result.is_ok());
2250
2251 let empty_path = HashMap::new();
2253 let result = endpoint.validate_request(&empty_path, &query_params, None);
2254 assert!(matches!(
2255 result,
2256 Err(ApiValidationError::MissingPathParameter { .. })
2257 ));
2258 }
2259
2260 #[test]
2261 fn test_api_method_properties() {
2262 assert!(ApiMethod::Get.is_idempotent());
2263 assert!(ApiMethod::Put.is_idempotent());
2264 assert!(!ApiMethod::Post.is_idempotent());
2265
2266 assert!(ApiMethod::Stream.is_streaming());
2267 assert!(ApiMethod::BiStream.is_streaming());
2268 assert!(!ApiMethod::Get.is_streaming());
2269
2270 assert!(ApiMethod::Get.is_safe());
2271 assert!(!ApiMethod::Post.is_safe());
2272 }
2273
2274 #[test]
2275 fn test_stats() {
2276 let registry = ApiRegistry::new();
2277
2278 for i in 0..5 {
2279 let schema = ApiSchema::new("api", ApiVersion::new(1, 0, 0))
2280 .add_endpoint(ApiEndpoint::new("/a", ApiMethod::Get))
2281 .add_endpoint(ApiEndpoint::new("/b", ApiMethod::Post));
2282
2283 let ann = ApiAnnouncement::new(make_node_id(i), vec![schema]);
2284 registry.register(ann).unwrap();
2285 }
2286
2287 registry.query(&ApiQuery::new());
2289 registry.query(&ApiQuery::new());
2290
2291 let stats = registry.stats();
2292 assert_eq!(stats.total_nodes, 5);
2293 assert_eq!(stats.total_schemas, 5);
2294 assert_eq!(stats.total_endpoints, 10);
2295 assert_eq!(stats.queries, 2);
2296 assert_eq!(stats.updates, 5);
2297 }
2298
2299 #[test]
2309 fn endpoint_prefix_matches_previous_split_join_behavior() {
2310 fn old(path: &str) -> String {
2312 path.split('/').take(2).collect::<Vec<_>>().join("/")
2313 }
2314
2315 let cases: &[&str] = &[
2316 "", "/", "//", "//a", "/a", "/a/", "a", "a/", "/api", "/api/users", "/api/users/123", "api/users/123", "/api/users/v2/list",
2329 "////",
2330 ];
2331
2332 for path in cases {
2333 assert_eq!(
2334 endpoint_prefix(path),
2335 old(path),
2336 "endpoint_prefix divergence for {path:?}",
2337 );
2338 }
2339 }
2340
2341 #[test]
2351 fn number_variant_range_and_type_errors() {
2352 let schema = SchemaType::Number {
2353 minimum: Some(0.0),
2354 maximum: Some(1.0),
2355 };
2356 assert!(schema.validate(&serde_json::json!(0.5)).is_ok());
2357 assert!(matches!(
2358 schema.validate(&serde_json::json!(-0.1)),
2359 Err(ValidationError::RangeError { .. })
2360 ));
2361 assert!(matches!(
2362 schema.validate(&serde_json::json!(1.5)),
2363 Err(ValidationError::RangeError { .. })
2364 ));
2365 assert!(matches!(
2366 schema.validate(&serde_json::json!("nope")),
2367 Err(ValidationError::TypeMismatch { .. })
2368 ));
2369 }
2370
2371 #[test]
2372 fn string_length_pattern_and_type_errors() {
2373 let schema = SchemaType::String {
2374 min_length: Some(2),
2375 max_length: Some(5),
2376 pattern: Some("ab".into()),
2377 format: None,
2378 };
2379 assert!(schema.validate(&serde_json::json!("xab")).is_ok());
2380 assert!(matches!(
2381 schema.validate(&serde_json::json!("a")),
2382 Err(ValidationError::LengthError { .. })
2383 ));
2384 assert!(matches!(
2385 schema.validate(&serde_json::json!("abcdef")),
2386 Err(ValidationError::LengthError { .. })
2387 ));
2388 assert!(matches!(
2389 schema.validate(&serde_json::json!("xyz")),
2390 Err(ValidationError::PatternMismatch { .. })
2391 ));
2392 assert!(matches!(
2393 schema.validate(&serde_json::json!(42)),
2394 Err(ValidationError::TypeMismatch { .. })
2395 ));
2396 }
2397
2398 #[test]
2399 fn array_length_uniqueness_and_type_errors() {
2400 let schema = SchemaType::Array {
2401 items: Box::new(SchemaType::integer()),
2402 min_items: Some(2),
2403 max_items: Some(3),
2404 unique_items: true,
2405 };
2406 assert!(schema.validate(&serde_json::json!([1, 2])).is_ok());
2407 assert!(matches!(
2408 schema.validate(&serde_json::json!([1])),
2409 Err(ValidationError::LengthError { .. })
2410 ));
2411 assert!(matches!(
2412 schema.validate(&serde_json::json!([1, 2, 3, 4])),
2413 Err(ValidationError::LengthError { .. })
2414 ));
2415 assert!(matches!(
2416 schema.validate(&serde_json::json!([1, 1, 2])),
2417 Err(ValidationError::DuplicateItems)
2418 ));
2419 assert!(matches!(
2420 schema.validate(&serde_json::json!([1, "two", 3])),
2421 Err(ValidationError::ArrayItemError { .. })
2422 ));
2423 assert!(matches!(
2424 schema.validate(&serde_json::json!("not-an-array")),
2425 Err(ValidationError::TypeMismatch { .. })
2426 ));
2427 }
2428
2429 #[test]
2430 fn object_property_unknown_and_type_errors() {
2431 let schema = SchemaType::object()
2432 .with_property("name", SchemaType::string())
2433 .with_property("age", SchemaType::integer())
2434 .with_required("name");
2435
2436 let err = schema
2438 .validate(&serde_json::json!({"name": "Alice", "age": "old"}))
2439 .unwrap_err();
2440 assert!(matches!(err, ValidationError::PropertyError { .. }));
2441
2442 let strict = SchemaType::Object {
2446 properties: {
2447 let mut m = HashMap::new();
2448 m.insert("name".into(), SchemaType::string());
2449 m
2450 },
2451 required: vec!["name".into()],
2452 additional_properties: false,
2453 };
2454 let err = strict
2455 .validate(&serde_json::json!({"name": "Alice", "extra": 1}))
2456 .unwrap_err();
2457 assert!(matches!(err, ValidationError::UnknownProperty { .. }));
2458
2459 assert!(matches!(
2461 schema.validate(&serde_json::json!([1, 2, 3])),
2462 Err(ValidationError::TypeMismatch { .. })
2463 ));
2464 }
2465
2466 #[test]
2467 fn enum_anyof_and_ref_arms() {
2468 let schema = SchemaType::Enum {
2470 values: vec![serde_json::json!("a"), serde_json::json!("b")],
2471 };
2472 assert!(schema.validate(&serde_json::json!("a")).is_ok());
2473 assert!(matches!(
2474 schema.validate(&serde_json::json!("c")),
2475 Err(ValidationError::EnumMismatch { .. })
2476 ));
2477
2478 let any = SchemaType::AnyOf {
2480 schemas: vec![SchemaType::integer(), SchemaType::string()],
2481 };
2482 assert!(any.validate(&serde_json::json!("ok")).is_ok());
2483 assert!(any.validate(&serde_json::json!(42)).is_ok());
2484 assert!(matches!(
2485 any.validate(&serde_json::json!(true)),
2486 Err(ValidationError::AnyOfFailed { .. })
2487 ));
2488
2489 let r = SchemaType::Ref {
2492 schema_ref: "#/definitions/X".into(),
2493 };
2494 assert!(r.validate(&serde_json::json!(null)).is_ok());
2495
2496 assert!(SchemaType::Any
2498 .validate(&serde_json::json!({"x":1}))
2499 .is_ok());
2500 }
2501
2502 #[test]
2505 fn query_matches_returns_false_on_each_filter_miss() {
2506 let schema = ApiSchema::new("svc", ApiVersion::new(1, 0, 0))
2507 .with_tag("gpu")
2508 .add_endpoint(ApiEndpoint::new("/run", ApiMethod::Post));
2509 let ann = ApiAnnouncement::new(make_node_id(1), vec![schema]);
2510
2511 let q = ApiQuery::new().with_api("other");
2513 assert_eq!(registry_match_count(&ann, &q), 0);
2514
2515 let q = ApiQuery::new().with_tag("cpu");
2517 assert_eq!(registry_match_count(&ann, &q), 0);
2518
2519 let q = ApiQuery::new().with_endpoint("/missing");
2521 assert_eq!(registry_match_count(&ann, &q), 0);
2522
2523 let q = ApiQuery::new()
2525 .with_endpoint("/run")
2526 .with_method(ApiMethod::Get);
2527 assert_eq!(registry_match_count(&ann, &q), 0);
2528 }
2529
2530 fn registry_match_count(ann: &ApiAnnouncement, q: &ApiQuery) -> usize {
2533 let r = ApiRegistry::new();
2534 r.register(ann.clone()).unwrap();
2535 r.query(q).len()
2536 }
2537
2538 #[test]
2541 fn find_by_endpoint_skips_expired_entries() {
2542 let registry = ApiRegistry::new();
2543 let schema = ApiSchema::new("svc", ApiVersion::new(1, 0, 0))
2544 .add_endpoint(ApiEndpoint::new("/run", ApiMethod::Post));
2545
2546 let mut ann = ApiAnnouncement::new(make_node_id(7), vec![schema]).with_ttl(1);
2554 ann.timestamp = 0;
2555 registry.register(ann).unwrap();
2556
2557 assert!(registry
2558 .find_by_endpoint("/run", ApiMethod::Post)
2559 .is_empty());
2560 }
2561}