1use dashmap::DashMap;
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12use std::sync::atomic::{AtomicU64, AtomicUsize, 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 pub fn path_matches(&self, path: &str) -> bool {
1130 let mut self_parts = self.path.split('/');
1131 let mut path_parts = path.split('/');
1132 loop {
1133 match (self_parts.next(), path_parts.next()) {
1134 (Some(sp), Some(pp)) => {
1135 let is_param = sp.starts_with('{') && sp.ends_with('}');
1136 if !is_param && sp != pp {
1137 return false;
1138 }
1139 }
1140 (None, None) => return true,
1142 _ => return false,
1144 }
1145 }
1146 }
1147}
1148
1149#[derive(Debug, Clone, PartialEq)]
1151pub enum ApiValidationError {
1152 MissingPathParameter {
1154 name: String,
1156 },
1157 PathParameter {
1159 name: String,
1161 error: ValidationError,
1163 },
1164 MissingQueryParameter {
1166 name: String,
1168 },
1169 QueryParameter {
1171 name: String,
1173 error: ValidationError,
1175 },
1176 MissingRequestBody,
1178 RequestBody {
1180 error: ValidationError,
1182 },
1183}
1184
1185impl std::fmt::Display for ApiValidationError {
1186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1187 match self {
1188 ApiValidationError::MissingPathParameter { name } => {
1189 write!(f, "missing path parameter: {}", name)
1190 }
1191 ApiValidationError::PathParameter { name, error } => {
1192 write!(f, "path parameter '{}': {}", name, error)
1193 }
1194 ApiValidationError::MissingQueryParameter { name } => {
1195 write!(f, "missing query parameter: {}", name)
1196 }
1197 ApiValidationError::QueryParameter { name, error } => {
1198 write!(f, "query parameter '{}': {}", name, error)
1199 }
1200 ApiValidationError::MissingRequestBody => write!(f, "missing request body"),
1201 ApiValidationError::RequestBody { error } => {
1202 write!(f, "request body: {}", error)
1203 }
1204 }
1205 }
1206}
1207
1208impl std::error::Error for ApiValidationError {}
1209
1210#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1212pub struct ApiVersion {
1213 pub major: u32,
1215 pub minor: u32,
1217 pub patch: u32,
1219}
1220
1221impl ApiVersion {
1222 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
1224 Self {
1225 major,
1226 minor,
1227 patch,
1228 }
1229 }
1230
1231 pub fn is_compatible_with(&self, required: &ApiVersion) -> bool {
1233 if self.major != required.major {
1235 return false;
1236 }
1237 if self.minor < required.minor {
1239 return false;
1240 }
1241 if self.minor == required.minor && self.patch < required.patch {
1243 return false;
1244 }
1245 true
1246 }
1247
1248 pub fn parse(s: &str) -> Option<Self> {
1250 let parts: Vec<&str> = s.split('.').collect();
1251 if parts.len() != 3 {
1252 return None;
1253 }
1254 Some(Self {
1255 major: parts[0].parse().ok()?,
1256 minor: parts[1].parse().ok()?,
1257 patch: parts[2].parse().ok()?,
1258 })
1259 }
1260}
1261
1262impl std::fmt::Display for ApiVersion {
1263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1264 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
1265 }
1266}
1267
1268impl PartialOrd for ApiVersion {
1269 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1270 Some(self.cmp(other))
1271 }
1272}
1273
1274impl Ord for ApiVersion {
1275 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1276 match self.major.cmp(&other.major) {
1277 std::cmp::Ordering::Equal => match self.minor.cmp(&other.minor) {
1278 std::cmp::Ordering::Equal => self.patch.cmp(&other.patch),
1279 ord => ord,
1280 },
1281 ord => ord,
1282 }
1283 }
1284}
1285
1286#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1288pub struct ApiSchema {
1289 pub name: String,
1291 pub description: Option<String>,
1293 pub version: ApiVersion,
1295 pub base_path: String,
1297 pub endpoints: Vec<ApiEndpoint>,
1299 pub definitions: HashMap<String, SchemaType>,
1301 pub tags: Vec<String>,
1303 pub contact: Option<String>,
1305 pub license: Option<String>,
1307}
1308
1309impl ApiSchema {
1310 pub fn new(name: impl Into<String>, version: ApiVersion) -> Self {
1312 Self {
1313 name: name.into(),
1314 description: None,
1315 version,
1316 base_path: "/".into(),
1317 endpoints: Vec::new(),
1318 definitions: HashMap::new(),
1319 tags: Vec::new(),
1320 contact: None,
1321 license: None,
1322 }
1323 }
1324
1325 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
1327 self.description = Some(desc.into());
1328 self
1329 }
1330
1331 pub fn with_base_path(mut self, path: impl Into<String>) -> Self {
1333 self.base_path = path.into();
1334 self
1335 }
1336
1337 pub fn add_endpoint(mut self, endpoint: ApiEndpoint) -> Self {
1339 self.endpoints.push(endpoint);
1340 self
1341 }
1342
1343 pub fn add_definition(mut self, name: impl Into<String>, schema: SchemaType) -> Self {
1345 self.definitions.insert(name.into(), schema);
1346 self
1347 }
1348
1349 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1351 self.tags.push(tag.into());
1352 self
1353 }
1354
1355 pub fn find_endpoint(&self, path: &str, method: ApiMethod) -> Option<&ApiEndpoint> {
1357 let full_path = if path.starts_with(&self.base_path) {
1358 path.to_string()
1359 } else {
1360 format!("{}{}", self.base_path.trim_end_matches('/'), path)
1361 };
1362
1363 self.endpoints
1364 .iter()
1365 .find(|e| e.method == method && e.path_matches(&full_path))
1366 }
1367
1368 pub fn endpoints_by_tag(&self, tag: &str) -> Vec<&ApiEndpoint> {
1370 self.endpoints
1371 .iter()
1372 .filter(|e| e.tags.contains(&tag.to_string()))
1373 .collect()
1374 }
1375
1376 pub fn to_bytes(&self) -> Vec<u8> {
1378 serde_json::to_vec(self).unwrap_or_default()
1379 }
1380
1381 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
1383 serde_json::from_slice(bytes).ok()
1384 }
1385}
1386
1387#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1389pub struct ApiAnnouncement {
1390 pub node_id: NodeId,
1392 pub schemas: Vec<ApiSchema>,
1394 pub version: u64,
1396 pub timestamp: u64,
1398 pub ttl_secs: u32,
1400}
1401
1402impl ApiAnnouncement {
1403 pub fn new(node_id: NodeId, schemas: Vec<ApiSchema>) -> Self {
1405 Self {
1406 node_id,
1407 schemas,
1408 version: 1,
1409 timestamp: SystemTime::now()
1410 .duration_since(UNIX_EPOCH)
1411 .unwrap_or_default()
1412 .as_millis() as u64,
1413 ttl_secs: 300,
1414 }
1415 }
1416
1417 pub fn with_version(mut self, version: u64) -> Self {
1419 self.version = version;
1420 self
1421 }
1422
1423 pub fn with_ttl(mut self, ttl_secs: u32) -> Self {
1425 self.ttl_secs = ttl_secs;
1426 self
1427 }
1428
1429 pub fn is_expired(&self) -> bool {
1431 let now = SystemTime::now()
1432 .duration_since(UNIX_EPOCH)
1433 .unwrap_or_default()
1434 .as_millis() as u64;
1435 let expiry = self.timestamp + (self.ttl_secs as u64 * 1000);
1436 now > expiry
1437 }
1438
1439 pub fn to_bytes(&self) -> Vec<u8> {
1441 serde_json::to_vec(self).unwrap_or_default()
1442 }
1443
1444 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
1446 serde_json::from_slice(bytes).ok()
1447 }
1448}
1449
1450#[derive(Debug, Clone, Default)]
1452pub struct ApiQuery {
1453 pub api_name: Option<String>,
1455 pub min_version: Option<ApiVersion>,
1457 pub endpoint_path: Option<String>,
1459 pub endpoint_method: Option<ApiMethod>,
1461 pub tag: Option<String>,
1463 pub capability: Option<String>,
1465}
1466
1467impl ApiQuery {
1468 pub fn new() -> Self {
1470 Self::default()
1471 }
1472
1473 pub fn with_api(mut self, name: impl Into<String>) -> Self {
1475 self.api_name = Some(name.into());
1476 self
1477 }
1478
1479 pub fn with_min_version(mut self, version: ApiVersion) -> Self {
1481 self.min_version = Some(version);
1482 self
1483 }
1484
1485 pub fn with_endpoint(mut self, path: impl Into<String>) -> Self {
1487 self.endpoint_path = Some(path.into());
1488 self
1489 }
1490
1491 pub fn with_method(mut self, method: ApiMethod) -> Self {
1493 self.endpoint_method = Some(method);
1494 self
1495 }
1496
1497 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1499 self.tag = Some(tag.into());
1500 self
1501 }
1502
1503 pub fn with_capability(mut self, cap: impl Into<String>) -> Self {
1505 self.capability = Some(cap.into());
1506 self
1507 }
1508
1509 pub fn matches_schema(&self, schema: &ApiSchema) -> bool {
1511 if let Some(ref name) = self.api_name {
1513 if &schema.name != name {
1514 return false;
1515 }
1516 }
1517
1518 if let Some(ref min_ver) = self.min_version {
1520 if !schema.version.is_compatible_with(min_ver) {
1521 return false;
1522 }
1523 }
1524
1525 if let Some(ref path) = self.endpoint_path {
1527 let method = self.endpoint_method;
1528 let found = schema.endpoints.iter().any(|e| {
1529 let path_matches = e.path_matches(path) || e.path.contains(path);
1530 let method_matches = method.is_none_or(|m| e.method == m);
1531 path_matches && method_matches
1532 });
1533 if !found {
1534 return false;
1535 }
1536 }
1537
1538 if let Some(ref tag) = self.tag {
1540 if !schema.tags.contains(tag) {
1541 return false;
1542 }
1543 }
1544
1545 if let Some(ref cap) = self.capability {
1547 let found = schema
1548 .endpoints
1549 .iter()
1550 .any(|e| e.required_capabilities.contains(cap));
1551 if !found {
1552 return false;
1553 }
1554 }
1555
1556 true
1557 }
1558}
1559
1560#[derive(Debug, Clone, PartialEq, Eq)]
1562pub enum RegistryError {
1563 NodeNotFound(NodeId),
1565 ApiNotFound(String),
1567 VersionConflict {
1569 expected: u64,
1571 actual: u64,
1573 },
1574 CapacityExceeded,
1576}
1577
1578impl std::fmt::Display for RegistryError {
1579 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1580 match self {
1581 RegistryError::NodeNotFound(_) => write!(f, "Node not found"),
1582 RegistryError::ApiNotFound(name) => write!(f, "API not found: {}", name),
1583 RegistryError::VersionConflict { expected, actual } => {
1584 write!(f, "Version conflict: expected {}, got {}", expected, actual)
1585 }
1586 RegistryError::CapacityExceeded => write!(f, "Registry capacity exceeded"),
1587 }
1588 }
1589}
1590
1591impl std::error::Error for RegistryError {}
1592
1593#[derive(Debug, Clone)]
1595pub struct IndexedApiNode {
1596 pub node_id: NodeId,
1598 pub announcement: Arc<ApiAnnouncement>,
1600}
1601
1602#[derive(Debug, Clone, Default)]
1604pub struct ApiRegistryStats {
1605 pub total_nodes: usize,
1607 pub total_schemas: usize,
1613 pub total_endpoints: usize,
1616 pub apis_by_name: HashMap<String, usize>,
1619 pub queries: u64,
1621 pub updates: u64,
1623}
1624
1625pub struct ApiRegistry {
1627 nodes: DashMap<NodeId, Arc<ApiAnnouncement>>,
1629 by_api_name: DashMap<String, HashSet<NodeId>>,
1631 by_tag: DashMap<String, HashSet<NodeId>>,
1633 by_endpoint: DashMap<String, HashSet<NodeId>>,
1635 query_count: AtomicU64,
1637 update_count: AtomicU64,
1639 node_count: AtomicUsize,
1644 total_endpoints: AtomicUsize,
1645 max_capacity: Option<usize>,
1647}
1648
1649fn endpoint_prefix(path: &str) -> String {
1656 match path.match_indices('/').nth(1) {
1657 Some((idx, _)) => path[..idx].to_string(),
1658 None => path.to_string(),
1659 }
1660}
1661
1662impl ApiRegistry {
1663 pub fn new() -> Self {
1665 Self {
1666 nodes: DashMap::new(),
1667 by_api_name: DashMap::new(),
1668 by_tag: DashMap::new(),
1669 by_endpoint: DashMap::new(),
1670 query_count: AtomicU64::new(0),
1671 update_count: AtomicU64::new(0),
1672 node_count: AtomicUsize::new(0),
1673 total_endpoints: AtomicUsize::new(0),
1674 max_capacity: None,
1675 }
1676 }
1677
1678 pub fn with_capacity(max: usize) -> Self {
1680 let mut reg = Self::new();
1681 reg.max_capacity = Some(max);
1682 reg
1683 }
1684
1685 pub fn register(&self, announcement: ApiAnnouncement) -> Result<(), RegistryError> {
1687 let node_id = announcement.node_id;
1688
1689 if let Some(max) = self.max_capacity {
1691 if !self.nodes.contains_key(&node_id) && self.node_count.load(Ordering::Relaxed) >= max
1692 {
1693 return Err(RegistryError::CapacityExceeded);
1694 }
1695 }
1696
1697 let ann = Arc::new(announcement);
1698
1699 use dashmap::mapref::entry::Entry;
1706 match self.nodes.entry(node_id) {
1707 Entry::Occupied(mut slot) => {
1708 let old = slot.get().clone();
1709 self.remove_from_indexes(&old);
1710 self.add_to_indexes(&ann);
1711 slot.insert(ann);
1712 }
1713 Entry::Vacant(slot) => {
1714 self.add_to_indexes(&ann);
1715 slot.insert(ann);
1716 self.node_count.fetch_add(1, Ordering::Relaxed);
1717 }
1718 }
1719 self.update_count.fetch_add(1, Ordering::Relaxed);
1720
1721 Ok(())
1722 }
1723
1724 pub fn unregister(&self, node_id: &NodeId) -> Option<Arc<ApiAnnouncement>> {
1726 if let Some((_, ann)) = self.nodes.remove(node_id) {
1727 self.remove_from_indexes(&ann);
1728 self.node_count.fetch_sub(1, Ordering::Relaxed);
1729 Some(ann)
1730 } else {
1731 None
1732 }
1733 }
1734
1735 pub fn get(&self, node_id: &NodeId) -> Option<Arc<ApiAnnouncement>> {
1737 self.nodes.get(node_id).map(|r| Arc::clone(&r))
1738 }
1739
1740 pub fn query(&self, query: &ApiQuery) -> Vec<IndexedApiNode> {
1742 self.query_count.fetch_add(1, Ordering::Relaxed);
1743
1744 let candidates: Vec<NodeId> = if let Some(ref api_name) = query.api_name {
1746 self.by_api_name
1747 .get(api_name)
1748 .map(|s| s.iter().copied().collect())
1749 .unwrap_or_default()
1750 } else if let Some(ref tag) = query.tag {
1751 self.by_tag
1752 .get(tag)
1753 .map(|s| s.iter().copied().collect())
1754 .unwrap_or_default()
1755 } else {
1756 self.nodes.iter().map(|r| *r.key()).collect()
1758 };
1759
1760 candidates
1762 .into_iter()
1763 .filter_map(|id| {
1764 let ann = self.nodes.get(&id)?;
1765 let matches = ann.schemas.iter().any(|s| query.matches_schema(s));
1767 if matches && !ann.is_expired() {
1768 Some(IndexedApiNode {
1769 node_id: id,
1770 announcement: Arc::clone(&ann),
1771 })
1772 } else {
1773 None
1774 }
1775 })
1776 .collect()
1777 }
1778
1779 pub fn find_by_endpoint(&self, path: &str, method: ApiMethod) -> Vec<IndexedApiNode> {
1781 self.query_count.fetch_add(1, Ordering::Relaxed);
1782
1783 self.nodes
1784 .iter()
1785 .filter_map(|entry| {
1786 let ann = entry.value();
1787 if ann.is_expired() {
1788 return None;
1789 }
1790
1791 let has_endpoint = ann.schemas.iter().any(|schema| {
1797 schema
1798 .endpoints
1799 .iter()
1800 .any(|e| e.method == method && e.path_matches(path))
1801 });
1802
1803 if has_endpoint {
1804 Some(IndexedApiNode {
1805 node_id: *entry.key(),
1806 announcement: Arc::clone(ann),
1807 })
1808 } else {
1809 None
1810 }
1811 })
1812 .collect()
1813 }
1814
1815 pub fn find_compatible(&self, api_name: &str, min_version: &ApiVersion) -> Vec<IndexedApiNode> {
1817 self.query_count.fetch_add(1, Ordering::Relaxed);
1818
1819 let candidates = self
1820 .by_api_name
1821 .get(api_name)
1822 .map(|s| s.iter().copied().collect::<Vec<_>>())
1823 .unwrap_or_default();
1824
1825 candidates
1826 .into_iter()
1827 .filter_map(|id| {
1828 let ann = self.nodes.get(&id)?;
1829 if ann.is_expired() {
1830 return None;
1831 }
1832
1833 let compatible = ann.schemas.iter().any(|schema| {
1834 schema.name == api_name && schema.version.is_compatible_with(min_version)
1835 });
1836
1837 if compatible {
1838 Some(IndexedApiNode {
1839 node_id: id,
1840 announcement: Arc::clone(&ann),
1841 })
1842 } else {
1843 None
1844 }
1845 })
1846 .collect()
1847 }
1848
1849 pub fn stats(&self) -> ApiRegistryStats {
1858 let apis_by_name: HashMap<String, usize> = self
1859 .by_api_name
1860 .iter()
1861 .filter(|e| !e.value().is_empty())
1862 .map(|e| (e.key().clone(), e.value().len()))
1863 .collect();
1864
1865 ApiRegistryStats {
1866 total_nodes: self.node_count.load(Ordering::Relaxed),
1867 total_schemas: apis_by_name.values().sum(),
1868 total_endpoints: self.total_endpoints.load(Ordering::Relaxed),
1869 apis_by_name,
1870 queries: self.query_count.load(Ordering::Relaxed),
1871 updates: self.update_count.load(Ordering::Relaxed),
1872 }
1873 }
1874
1875 pub fn len(&self) -> usize {
1877 self.node_count.load(Ordering::Relaxed)
1878 }
1879
1880 pub fn is_empty(&self) -> bool {
1882 self.node_count.load(Ordering::Relaxed) == 0
1883 }
1884
1885 pub fn clear(&self) {
1887 let keys: Vec<NodeId> = self.nodes.iter().map(|r| *r.key()).collect();
1896 for key in keys {
1897 if let Some((_, ann)) = self.nodes.remove(&key) {
1898 self.remove_from_indexes(&ann); self.node_count.fetch_sub(1, Ordering::Relaxed);
1900 }
1901 }
1902 self.by_api_name.clear();
1905 self.by_tag.clear();
1906 self.by_endpoint.clear();
1907 }
1908
1909 pub fn cleanup_expired(&self) -> usize {
1911 let expired: Vec<NodeId> = self
1912 .nodes
1913 .iter()
1914 .filter(|e| e.value().is_expired())
1915 .map(|e| *e.key())
1916 .collect();
1917
1918 let count = expired.len();
1919 for id in expired {
1920 self.unregister(&id);
1921 }
1922 count
1923 }
1924
1925 fn add_to_indexes(&self, ann: &ApiAnnouncement) {
1927 let node_id = ann.node_id;
1928 let mut added_endpoints = 0usize;
1929
1930 for schema in &ann.schemas {
1931 self.by_api_name
1933 .entry(schema.name.clone())
1934 .or_default()
1935 .insert(node_id);
1936
1937 for tag in &schema.tags {
1939 self.by_tag.entry(tag.clone()).or_default().insert(node_id);
1940 }
1941
1942 for endpoint in &schema.endpoints {
1944 let prefix = endpoint_prefix(&endpoint.path);
1945 self.by_endpoint.entry(prefix).or_default().insert(node_id);
1946 }
1947 added_endpoints += schema.endpoints.len();
1948 }
1949 self.total_endpoints
1950 .fetch_add(added_endpoints, Ordering::Relaxed);
1951 }
1952
1953 fn remove_from_indexes(&self, ann: &ApiAnnouncement) {
1955 let node_id = ann.node_id;
1956 let mut removed_endpoints = 0usize;
1957
1958 for schema in &ann.schemas {
1959 if let Some(mut set) = self.by_api_name.get_mut(&schema.name) {
1960 set.remove(&node_id);
1961 }
1962
1963 for tag in &schema.tags {
1964 if let Some(mut set) = self.by_tag.get_mut(tag) {
1965 set.remove(&node_id);
1966 }
1967 }
1968
1969 for endpoint in &schema.endpoints {
1970 let prefix = endpoint_prefix(&endpoint.path);
1971 if let Some(mut set) = self.by_endpoint.get_mut(&prefix) {
1972 set.remove(&node_id);
1973 }
1974 }
1975 removed_endpoints += schema.endpoints.len();
1976 }
1977 self.total_endpoints
1978 .fetch_sub(removed_endpoints, Ordering::Relaxed);
1979 }
1980}
1981
1982impl Default for ApiRegistry {
1983 fn default() -> Self {
1984 Self::new()
1985 }
1986}
1987
1988#[cfg(test)]
1989mod tests {
1990 use super::*;
1991
1992 fn make_node_id(n: u8) -> NodeId {
1993 let mut id = [0u8; 32];
1994 id[0] = n;
1995 id
1996 }
1997
1998 #[test]
1999 fn test_schema_type_validation() {
2000 let schema = SchemaType::string().with_max_length(10);
2002 assert!(schema.validate(&serde_json::json!("hello")).is_ok());
2003 assert!(schema.validate(&serde_json::json!("hello world!")).is_err());
2004
2005 let schema = SchemaType::integer().with_minimum(0).with_maximum(100);
2007 assert!(schema.validate(&serde_json::json!(50)).is_ok());
2008 assert!(schema.validate(&serde_json::json!(-1)).is_err());
2009 assert!(schema.validate(&serde_json::json!(101)).is_err());
2010
2011 let schema = SchemaType::object()
2013 .with_property("name", SchemaType::string())
2014 .with_property("age", SchemaType::integer())
2015 .with_required("name");
2016
2017 assert!(schema
2018 .validate(&serde_json::json!({"name": "Alice", "age": 30}))
2019 .is_ok());
2020 assert!(schema.validate(&serde_json::json!({"age": 30})).is_err()); let schema = SchemaType::array(SchemaType::integer());
2024 assert!(schema.validate(&serde_json::json!([1, 2, 3])).is_ok());
2025 assert!(schema.validate(&serde_json::json!([1, "two", 3])).is_err());
2026 }
2027
2028 #[test]
2044 fn validate_returns_recursion_limit_error_on_deeply_nested_schema() {
2045 let mut schema = SchemaType::integer();
2048 for _ in 0..MAX_SCHEMA_DEPTH + 5 {
2049 schema = SchemaType::array(schema);
2050 }
2051
2052 let mut value = serde_json::json!(1);
2055 for _ in 0..MAX_SCHEMA_DEPTH + 5 {
2056 value = serde_json::json!([value]);
2057 }
2058
2059 let result = schema.validate(&value);
2061 match result {
2062 Err(ValidationError::RecursionLimitExceeded { limit }) => {
2063 assert_eq!(limit, MAX_SCHEMA_DEPTH);
2064 }
2065 other => panic!("expected RecursionLimitExceeded, got {:?}", other),
2066 }
2067 }
2068
2069 #[test]
2073 fn validate_accepts_schema_at_recursion_limit() {
2074 let mut schema = SchemaType::integer();
2075 for _ in 0..(MAX_SCHEMA_DEPTH - 1) {
2078 schema = SchemaType::array(schema);
2079 }
2080 let mut value = serde_json::json!(1);
2081 for _ in 0..(MAX_SCHEMA_DEPTH - 1) {
2082 value = serde_json::json!([value]);
2083 }
2084 assert!(
2085 schema.validate(&value).is_ok(),
2086 "schema right at the depth limit must still validate"
2087 );
2088 }
2089
2090 #[test]
2097 fn try_from_slice_rejects_input_over_max_schema_depth() {
2098 let depth = MAX_SCHEMA_DEPTH + 50;
2102 let mut s = String::new();
2103 for _ in 0..depth {
2104 s.push('[');
2105 }
2106 s.push_str("null");
2107 for _ in 0..depth {
2108 s.push(']');
2109 }
2110 let err = SchemaType::try_from_str(&s)
2111 .expect_err("deeply-nested JSON must be rejected by the depth pre-scan");
2112 let msg = format!("{}", err);
2113 assert!(
2114 msg.contains("max nesting depth exceeded"),
2115 "error message must name the depth cap; got: {}",
2116 msg
2117 );
2118 }
2119
2120 #[test]
2125 fn try_from_slice_handles_brackets_inside_strings_correctly() {
2126 let json = r#"{"type":"string","pattern":"[}{]\""}"#;
2130 let r = SchemaType::try_from_str(json);
2131 assert!(
2132 r.is_ok(),
2133 "valid schema with bracket-bearing string must parse: {:?}",
2134 r.err()
2135 );
2136 }
2137
2138 #[test]
2148 fn try_from_slice_accepts_normal_depth_schema() {
2149 let depth = 32usize;
2150 let mut s = String::new();
2151 for _ in 0..depth {
2152 s.push_str(r#"{"type":"array","items":"#);
2153 }
2154 s.push_str(r#"{"type":"null"}"#);
2155 for _ in 0..depth {
2156 s.push('}');
2157 }
2158 let r = SchemaType::try_from_str(&s);
2159 assert!(
2160 r.is_ok(),
2161 "moderately-nested schema (depth {}) must parse; got: {:?}",
2162 depth,
2163 r.err()
2164 );
2165 }
2166
2167 #[test]
2171 fn check_json_nesting_depth_unit() {
2172 assert!(check_json_nesting_depth(b"{}", 1).is_ok());
2173 assert!(check_json_nesting_depth(b"{}", 0).is_err()); assert!(check_json_nesting_depth(b"[[[[]]]]", 4).is_ok());
2175 assert!(check_json_nesting_depth(b"[[[[]]]]", 3).is_err());
2176 assert!(check_json_nesting_depth(b"\"[[[[\"", 0).is_ok());
2178 assert!(check_json_nesting_depth(b"\"[\\\"[[\"", 0).is_ok());
2180 assert!(check_json_nesting_depth(b"{\"a\":[1,2]}", 2).is_ok());
2182 assert!(check_json_nesting_depth(b"{\"a\":[1,2]}", 1).is_err());
2183 }
2184
2185 #[test]
2186 fn test_api_endpoint_path_matching() {
2187 let endpoint = ApiEndpoint::new("/models/{model_id}/infer", ApiMethod::Post)
2188 .with_path_param(ApiParameter::required("model_id", SchemaType::string()));
2189
2190 let params = endpoint.matches_path("/models/llama-7b/infer");
2192 assert!(params.is_some());
2193 let params = params.unwrap();
2194 assert_eq!(params.get("model_id"), Some(&"llama-7b".to_string()));
2195
2196 assert!(endpoint.matches_path("/models/llama-7b/train").is_none());
2198 assert!(endpoint.matches_path("/models/infer").is_none());
2199 }
2200
2201 #[test]
2202 fn test_api_version_compatibility() {
2203 let v1_0_0 = ApiVersion::new(1, 0, 0);
2204 let v1_1_0 = ApiVersion::new(1, 1, 0);
2205 let v1_1_1 = ApiVersion::new(1, 1, 1);
2206 let v2_0_0 = ApiVersion::new(2, 0, 0);
2207
2208 assert!(v1_0_0.is_compatible_with(&v1_0_0));
2210
2211 assert!(v1_1_0.is_compatible_with(&v1_0_0));
2213
2214 assert!(v1_1_1.is_compatible_with(&v1_1_0));
2216
2217 assert!(!v1_0_0.is_compatible_with(&v1_1_0));
2219
2220 assert!(!v2_0_0.is_compatible_with(&v1_0_0));
2222 assert!(!v1_0_0.is_compatible_with(&v2_0_0));
2223 }
2224
2225 #[test]
2226 fn test_api_schema() {
2227 let schema = ApiSchema::new("inference", ApiVersion::new(1, 0, 0))
2228 .with_description("Model inference API")
2229 .with_base_path("/api/v1")
2230 .with_tag("ai")
2231 .add_endpoint(
2232 ApiEndpoint::new("/models/{model_id}/infer", ApiMethod::Post)
2233 .with_description("Run inference on a model")
2234 .with_tag("inference"),
2235 )
2236 .add_endpoint(
2237 ApiEndpoint::new("/models", ApiMethod::Get)
2238 .with_description("List available models")
2239 .with_tag("models"),
2240 );
2241
2242 assert_eq!(schema.endpoints.len(), 2);
2243 assert!(schema.tags.contains(&"ai".to_string()));
2244
2245 let inference_endpoints = schema.endpoints_by_tag("inference");
2247 assert_eq!(inference_endpoints.len(), 1);
2248 }
2249
2250 #[test]
2251 fn test_api_registry_basic() {
2252 let registry = ApiRegistry::new();
2253
2254 let schema = ApiSchema::new("test-api", ApiVersion::new(1, 0, 0))
2255 .with_tag("test")
2256 .add_endpoint(ApiEndpoint::new("/test", ApiMethod::Get));
2257
2258 let ann = ApiAnnouncement::new(make_node_id(1), vec![schema]);
2259 registry.register(ann).unwrap();
2260
2261 assert_eq!(registry.len(), 1);
2262
2263 let result = registry.get(&make_node_id(1));
2264 assert!(result.is_some());
2265
2266 registry.unregister(&make_node_id(1));
2267 assert_eq!(registry.len(), 0);
2268 }
2269
2270 #[test]
2271 fn test_api_registry_query() {
2272 let registry = ApiRegistry::new();
2273
2274 for i in 0..10 {
2276 let api_name = if i < 5 { "inference" } else { "training" };
2277 let tag = if i % 2 == 0 { "gpu" } else { "cpu" };
2278
2279 let schema = ApiSchema::new(api_name, ApiVersion::new(1, i as u32, 0))
2280 .with_tag(tag)
2281 .add_endpoint(ApiEndpoint::new("/run", ApiMethod::Post));
2282
2283 let ann = ApiAnnouncement::new(make_node_id(i), vec![schema]);
2284 registry.register(ann).unwrap();
2285 }
2286
2287 let results = registry.query(&ApiQuery::new().with_api("inference"));
2289 assert_eq!(results.len(), 5);
2290
2291 let results = registry.query(&ApiQuery::new().with_tag("gpu"));
2293 assert_eq!(results.len(), 5);
2294
2295 let results = registry.query(&ApiQuery::new().with_api("inference").with_tag("gpu"));
2297 assert_eq!(results.len(), 3);
2299 }
2300
2301 #[test]
2302 fn test_api_registry_version_compatibility() {
2303 let registry = ApiRegistry::new();
2304
2305 for i in 0..5 {
2307 let schema = ApiSchema::new("my-api", ApiVersion::new(1, i as u32, 0));
2308 let ann = ApiAnnouncement::new(make_node_id(i), vec![schema]);
2309 registry.register(ann).unwrap();
2310 }
2311
2312 let results = registry.find_compatible("my-api", &ApiVersion::new(1, 2, 0));
2314 assert_eq!(results.len(), 3);
2316 }
2317
2318 #[test]
2319 fn test_request_validation() {
2320 let endpoint = ApiEndpoint::new("/users/{user_id}", ApiMethod::Get)
2321 .with_path_param(ApiParameter::required("user_id", SchemaType::string()))
2322 .with_query_param(ApiParameter::optional("limit", SchemaType::integer()));
2323
2324 let mut path_params = HashMap::new();
2326 path_params.insert("user_id".to_string(), serde_json::json!("123"));
2327
2328 let query_params = HashMap::new();
2329
2330 let result = endpoint.validate_request(&path_params, &query_params, None);
2331 assert!(result.is_ok());
2332
2333 let empty_path = HashMap::new();
2335 let result = endpoint.validate_request(&empty_path, &query_params, None);
2336 assert!(matches!(
2337 result,
2338 Err(ApiValidationError::MissingPathParameter { .. })
2339 ));
2340 }
2341
2342 #[test]
2343 fn test_api_method_properties() {
2344 assert!(ApiMethod::Get.is_idempotent());
2345 assert!(ApiMethod::Put.is_idempotent());
2346 assert!(!ApiMethod::Post.is_idempotent());
2347
2348 assert!(ApiMethod::Stream.is_streaming());
2349 assert!(ApiMethod::BiStream.is_streaming());
2350 assert!(!ApiMethod::Get.is_streaming());
2351
2352 assert!(ApiMethod::Get.is_safe());
2353 assert!(!ApiMethod::Post.is_safe());
2354 }
2355
2356 #[test]
2357 fn test_stats() {
2358 let registry = ApiRegistry::new();
2359
2360 for i in 0..5 {
2361 let schema = ApiSchema::new("api", ApiVersion::new(1, 0, 0))
2362 .add_endpoint(ApiEndpoint::new("/a", ApiMethod::Get))
2363 .add_endpoint(ApiEndpoint::new("/b", ApiMethod::Post));
2364
2365 let ann = ApiAnnouncement::new(make_node_id(i), vec![schema]);
2366 registry.register(ann).unwrap();
2367 }
2368
2369 registry.query(&ApiQuery::new());
2371 registry.query(&ApiQuery::new());
2372
2373 let stats = registry.stats();
2374 assert_eq!(stats.total_nodes, 5);
2375 assert_eq!(stats.total_schemas, 5);
2376 assert_eq!(stats.total_endpoints, 10);
2377 assert_eq!(stats.queries, 2);
2378 assert_eq!(stats.updates, 5);
2379 }
2380
2381 #[test]
2385 fn stats_and_len_track_register_update_unregister_clear() {
2386 let registry = ApiRegistry::new();
2387 for i in 0..4u8 {
2388 let schema = ApiSchema::new("api", ApiVersion::new(1, 0, 0))
2389 .add_endpoint(ApiEndpoint::new("/a", ApiMethod::Get))
2390 .add_endpoint(ApiEndpoint::new("/b", ApiMethod::Post));
2391 registry
2392 .register(ApiAnnouncement::new(make_node_id(i), vec![schema]))
2393 .unwrap();
2394 }
2395 assert_eq!(registry.len(), 4);
2396 let s = registry.stats();
2397 assert_eq!(s.total_nodes, 4);
2398 assert_eq!(s.apis_by_name.get("api"), Some(&4));
2399 assert_eq!(s.total_schemas, 4);
2400 assert_eq!(s.total_endpoints, 8);
2401
2402 let schema = ApiSchema::new("api2", ApiVersion::new(1, 0, 0))
2404 .add_endpoint(ApiEndpoint::new("/c", ApiMethod::Get));
2405 registry
2406 .register(ApiAnnouncement::new(make_node_id(0), vec![schema]))
2407 .unwrap();
2408 assert_eq!(registry.len(), 4, "update must not grow node count");
2409 let s = registry.stats();
2410 assert_eq!(s.total_nodes, 4);
2411 assert_eq!(s.apis_by_name.get("api"), Some(&3));
2412 assert_eq!(s.apis_by_name.get("api2"), Some(&1));
2413 assert_eq!(s.total_endpoints, 7); assert!(registry.unregister(&make_node_id(1)).is_some());
2417 assert_eq!(registry.len(), 3);
2418 assert_eq!(registry.stats().total_nodes, 3);
2419 assert!(registry.unregister(&make_node_id(99)).is_none());
2420 assert_eq!(registry.len(), 3);
2421
2422 registry.clear();
2424 assert_eq!(registry.len(), 0);
2425 assert!(registry.is_empty());
2426 let s = registry.stats();
2427 assert_eq!(s.total_nodes, 0);
2428 assert_eq!(s.total_endpoints, 0);
2429 assert!(s.apis_by_name.is_empty());
2430 }
2431
2432 #[test]
2439 fn stats_dedupes_duplicate_api_names_within_a_node() {
2440 let registry = ApiRegistry::new();
2441 let dup_a = ApiSchema::new("dup", ApiVersion::new(1, 0, 0))
2442 .add_endpoint(ApiEndpoint::new("/a", ApiMethod::Get));
2443 let dup_b = ApiSchema::new("dup", ApiVersion::new(2, 0, 0))
2444 .add_endpoint(ApiEndpoint::new("/b", ApiMethod::Get))
2445 .add_endpoint(ApiEndpoint::new("/c", ApiMethod::Post));
2446 registry
2447 .register(ApiAnnouncement::new(make_node_id(0), vec![dup_a, dup_b]))
2448 .unwrap();
2449
2450 let s = registry.stats();
2451 assert_eq!(s.total_nodes, 1);
2452 assert_eq!(
2453 s.apis_by_name.get("dup"),
2454 Some(&1),
2455 "one node = one provider of 'dup', even with two same-named schemas"
2456 );
2457 assert_eq!(
2458 s.total_schemas, 1,
2459 "total_schemas counts (node, name) pairs"
2460 );
2461 assert_eq!(
2462 s.total_endpoints, 3,
2463 "every endpoint instance still counts (1 + 2)"
2464 );
2465
2466 let other = ApiSchema::new("dup", ApiVersion::new(1, 0, 0))
2468 .add_endpoint(ApiEndpoint::new("/a", ApiMethod::Get));
2469 registry
2470 .register(ApiAnnouncement::new(make_node_id(1), vec![other]))
2471 .unwrap();
2472 let s = registry.stats();
2473 assert_eq!(s.apis_by_name.get("dup"), Some(&2));
2474 assert_eq!(s.total_schemas, 2);
2475 assert_eq!(s.total_endpoints, 4);
2476 }
2477
2478 #[test]
2485 fn concurrent_same_node_register_keeps_counters_exact() {
2486 use std::sync::Arc as StdArc;
2487
2488 let registry = StdArc::new(ApiRegistry::new());
2489 let threads = 16;
2490 let iters = 200;
2491
2492 let mut handles = Vec::new();
2493 for _ in 0..threads {
2494 let registry = StdArc::clone(®istry);
2495 handles.push(std::thread::spawn(move || {
2496 for _ in 0..iters {
2497 let schema = ApiSchema::new("api", ApiVersion::new(1, 0, 0))
2498 .add_endpoint(ApiEndpoint::new("/a", ApiMethod::Get))
2499 .add_endpoint(ApiEndpoint::new("/b", ApiMethod::Post));
2500 registry
2501 .register(ApiAnnouncement::new(make_node_id(0), vec![schema]))
2502 .unwrap();
2503 }
2504 }));
2505 }
2506 for h in handles {
2507 h.join().unwrap();
2508 }
2509
2510 assert_eq!(registry.len(), 1);
2512 let s = registry.stats();
2513 assert_eq!(s.total_nodes, 1);
2514 assert_eq!(
2515 s.total_endpoints, 2,
2516 "total_endpoints must not drift or underflow under concurrent re-register"
2517 );
2518 assert_eq!(s.apis_by_name.get("api"), Some(&1));
2519 }
2520
2521 #[test]
2528 fn concurrent_clear_does_not_underflow_counters() {
2529 use std::sync::Arc as StdArc;
2530
2531 let registry = StdArc::new(ApiRegistry::new());
2532 let mut handles = Vec::new();
2533
2534 for t in 0..8u8 {
2536 let registry = StdArc::clone(®istry);
2537 handles.push(std::thread::spawn(move || {
2538 for _ in 0..500 {
2539 let schema = ApiSchema::new("api", ApiVersion::new(1, 0, 0))
2540 .add_endpoint(ApiEndpoint::new("/a", ApiMethod::Get));
2541 registry
2542 .register(ApiAnnouncement::new(make_node_id(t), vec![schema]))
2543 .unwrap();
2544 registry.unregister(&make_node_id(t));
2545 }
2546 }));
2547 }
2548 {
2550 let registry = StdArc::clone(®istry);
2551 handles.push(std::thread::spawn(move || {
2552 for _ in 0..500 {
2553 registry.clear();
2554 }
2555 }));
2556 }
2557 for h in handles {
2558 h.join().unwrap();
2559 }
2560
2561 assert_eq!(
2564 registry.len(),
2565 registry.nodes.len(),
2566 "node_count must track the map, not underflow"
2567 );
2568 let live_endpoints: usize = registry
2569 .nodes
2570 .iter()
2571 .map(|e| {
2572 e.value()
2573 .schemas
2574 .iter()
2575 .map(|s| s.endpoints.len())
2576 .sum::<usize>()
2577 })
2578 .sum();
2579 assert_eq!(
2580 registry.stats().total_endpoints,
2581 live_endpoints,
2582 "total_endpoints must track the map, not underflow"
2583 );
2584 }
2585
2586 #[test]
2589 fn path_matches_agrees_with_matches_path() {
2590 let e = ApiEndpoint::new("/models/{model_id}/infer", ApiMethod::Post);
2591 for p in [
2592 "/models/llama-7b/infer", "/models/llama-7b/train", "/models/infer", "/models/a/infer/x", ] {
2597 assert_eq!(
2598 e.path_matches(p),
2599 e.matches_path(p).is_some(),
2600 "disagreement on {p}"
2601 );
2602 }
2603 assert!(e.path_matches("/models/llama-7b/infer"));
2604
2605 let lit = ApiEndpoint::new("/health", ApiMethod::Get);
2606 assert!(lit.path_matches("/health"));
2607 assert!(!lit.path_matches("/healthz"));
2608 }
2609
2610 #[test]
2620 fn endpoint_prefix_matches_previous_split_join_behavior() {
2621 fn old(path: &str) -> String {
2623 path.split('/').take(2).collect::<Vec<_>>().join("/")
2624 }
2625
2626 let cases: &[&str] = &[
2627 "", "/", "//", "//a", "/a", "/a/", "a", "a/", "/api", "/api/users", "/api/users/123", "api/users/123", "/api/users/v2/list",
2640 "////",
2641 ];
2642
2643 for path in cases {
2644 assert_eq!(
2645 endpoint_prefix(path),
2646 old(path),
2647 "endpoint_prefix divergence for {path:?}",
2648 );
2649 }
2650 }
2651
2652 #[test]
2662 fn number_variant_range_and_type_errors() {
2663 let schema = SchemaType::Number {
2664 minimum: Some(0.0),
2665 maximum: Some(1.0),
2666 };
2667 assert!(schema.validate(&serde_json::json!(0.5)).is_ok());
2668 assert!(matches!(
2669 schema.validate(&serde_json::json!(-0.1)),
2670 Err(ValidationError::RangeError { .. })
2671 ));
2672 assert!(matches!(
2673 schema.validate(&serde_json::json!(1.5)),
2674 Err(ValidationError::RangeError { .. })
2675 ));
2676 assert!(matches!(
2677 schema.validate(&serde_json::json!("nope")),
2678 Err(ValidationError::TypeMismatch { .. })
2679 ));
2680 }
2681
2682 #[test]
2683 fn string_length_pattern_and_type_errors() {
2684 let schema = SchemaType::String {
2685 min_length: Some(2),
2686 max_length: Some(5),
2687 pattern: Some("ab".into()),
2688 format: None,
2689 };
2690 assert!(schema.validate(&serde_json::json!("xab")).is_ok());
2691 assert!(matches!(
2692 schema.validate(&serde_json::json!("a")),
2693 Err(ValidationError::LengthError { .. })
2694 ));
2695 assert!(matches!(
2696 schema.validate(&serde_json::json!("abcdef")),
2697 Err(ValidationError::LengthError { .. })
2698 ));
2699 assert!(matches!(
2700 schema.validate(&serde_json::json!("xyz")),
2701 Err(ValidationError::PatternMismatch { .. })
2702 ));
2703 assert!(matches!(
2704 schema.validate(&serde_json::json!(42)),
2705 Err(ValidationError::TypeMismatch { .. })
2706 ));
2707 }
2708
2709 #[test]
2710 fn array_length_uniqueness_and_type_errors() {
2711 let schema = SchemaType::Array {
2712 items: Box::new(SchemaType::integer()),
2713 min_items: Some(2),
2714 max_items: Some(3),
2715 unique_items: true,
2716 };
2717 assert!(schema.validate(&serde_json::json!([1, 2])).is_ok());
2718 assert!(matches!(
2719 schema.validate(&serde_json::json!([1])),
2720 Err(ValidationError::LengthError { .. })
2721 ));
2722 assert!(matches!(
2723 schema.validate(&serde_json::json!([1, 2, 3, 4])),
2724 Err(ValidationError::LengthError { .. })
2725 ));
2726 assert!(matches!(
2727 schema.validate(&serde_json::json!([1, 1, 2])),
2728 Err(ValidationError::DuplicateItems)
2729 ));
2730 assert!(matches!(
2731 schema.validate(&serde_json::json!([1, "two", 3])),
2732 Err(ValidationError::ArrayItemError { .. })
2733 ));
2734 assert!(matches!(
2735 schema.validate(&serde_json::json!("not-an-array")),
2736 Err(ValidationError::TypeMismatch { .. })
2737 ));
2738 }
2739
2740 #[test]
2741 fn object_property_unknown_and_type_errors() {
2742 let schema = SchemaType::object()
2743 .with_property("name", SchemaType::string())
2744 .with_property("age", SchemaType::integer())
2745 .with_required("name");
2746
2747 let err = schema
2749 .validate(&serde_json::json!({"name": "Alice", "age": "old"}))
2750 .unwrap_err();
2751 assert!(matches!(err, ValidationError::PropertyError { .. }));
2752
2753 let strict = SchemaType::Object {
2757 properties: {
2758 let mut m = HashMap::new();
2759 m.insert("name".into(), SchemaType::string());
2760 m
2761 },
2762 required: vec!["name".into()],
2763 additional_properties: false,
2764 };
2765 let err = strict
2766 .validate(&serde_json::json!({"name": "Alice", "extra": 1}))
2767 .unwrap_err();
2768 assert!(matches!(err, ValidationError::UnknownProperty { .. }));
2769
2770 assert!(matches!(
2772 schema.validate(&serde_json::json!([1, 2, 3])),
2773 Err(ValidationError::TypeMismatch { .. })
2774 ));
2775 }
2776
2777 #[test]
2778 fn enum_anyof_and_ref_arms() {
2779 let schema = SchemaType::Enum {
2781 values: vec![serde_json::json!("a"), serde_json::json!("b")],
2782 };
2783 assert!(schema.validate(&serde_json::json!("a")).is_ok());
2784 assert!(matches!(
2785 schema.validate(&serde_json::json!("c")),
2786 Err(ValidationError::EnumMismatch { .. })
2787 ));
2788
2789 let any = SchemaType::AnyOf {
2791 schemas: vec![SchemaType::integer(), SchemaType::string()],
2792 };
2793 assert!(any.validate(&serde_json::json!("ok")).is_ok());
2794 assert!(any.validate(&serde_json::json!(42)).is_ok());
2795 assert!(matches!(
2796 any.validate(&serde_json::json!(true)),
2797 Err(ValidationError::AnyOfFailed { .. })
2798 ));
2799
2800 let r = SchemaType::Ref {
2803 schema_ref: "#/definitions/X".into(),
2804 };
2805 assert!(r.validate(&serde_json::json!(null)).is_ok());
2806
2807 assert!(SchemaType::Any
2809 .validate(&serde_json::json!({"x":1}))
2810 .is_ok());
2811 }
2812
2813 #[test]
2816 fn query_matches_returns_false_on_each_filter_miss() {
2817 let schema = ApiSchema::new("svc", ApiVersion::new(1, 0, 0))
2818 .with_tag("gpu")
2819 .add_endpoint(ApiEndpoint::new("/run", ApiMethod::Post));
2820 let ann = ApiAnnouncement::new(make_node_id(1), vec![schema]);
2821
2822 let q = ApiQuery::new().with_api("other");
2824 assert_eq!(registry_match_count(&ann, &q), 0);
2825
2826 let q = ApiQuery::new().with_tag("cpu");
2828 assert_eq!(registry_match_count(&ann, &q), 0);
2829
2830 let q = ApiQuery::new().with_endpoint("/missing");
2832 assert_eq!(registry_match_count(&ann, &q), 0);
2833
2834 let q = ApiQuery::new()
2836 .with_endpoint("/run")
2837 .with_method(ApiMethod::Get);
2838 assert_eq!(registry_match_count(&ann, &q), 0);
2839 }
2840
2841 fn registry_match_count(ann: &ApiAnnouncement, q: &ApiQuery) -> usize {
2844 let r = ApiRegistry::new();
2845 r.register(ann.clone()).unwrap();
2846 r.query(q).len()
2847 }
2848
2849 #[test]
2852 fn find_by_endpoint_skips_expired_entries() {
2853 let registry = ApiRegistry::new();
2854 let schema = ApiSchema::new("svc", ApiVersion::new(1, 0, 0))
2855 .add_endpoint(ApiEndpoint::new("/run", ApiMethod::Post));
2856
2857 let mut ann = ApiAnnouncement::new(make_node_id(7), vec![schema]).with_ttl(1);
2865 ann.timestamp = 0;
2866 registry.register(ann).unwrap();
2867
2868 assert!(registry
2869 .find_by_endpoint("/run", ApiMethod::Post)
2870 .is_empty());
2871 }
2872}