Skip to main content

net/adapter/net/behavior/
api.rs

1//! Phase 4D: Node APIs & Schemas (API-SCHEMA)
2//!
3//! This module provides runtime-discoverable API definitions for nodes:
4//! - Structured API endpoint definitions
5//! - JSON Schema-based type validation
6//! - API versioning and compatibility checking
7//! - API registry with discovery and matching
8
9use 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/// HTTP-like method types for API endpoints
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum ApiMethod {
21    /// Query/read operation
22    Get,
23    /// Create operation
24    Post,
25    /// Full update operation
26    Put,
27    /// Partial update operation
28    Patch,
29    /// Delete operation
30    Delete,
31    /// Streaming request
32    Stream,
33    /// Bidirectional streaming
34    BiStream,
35    /// Subscribe to events
36    Subscribe,
37    /// One-way notification
38    Notify,
39}
40
41impl ApiMethod {
42    /// Whether this method is idempotent
43    pub fn is_idempotent(&self) -> bool {
44        matches!(self, ApiMethod::Get | ApiMethod::Put | ApiMethod::Delete)
45    }
46
47    /// Whether this method involves streaming
48    pub fn is_streaming(&self) -> bool {
49        matches!(
50            self,
51            ApiMethod::Stream | ApiMethod::BiStream | ApiMethod::Subscribe
52        )
53    }
54
55    /// Whether this method is safe (no side effects)
56    pub fn is_safe(&self) -> bool {
57        matches!(self, ApiMethod::Get | ApiMethod::Subscribe)
58    }
59}
60
61/// Maximum recursion depth permitted by [`SchemaType::validate`].
62///
63/// `SchemaType` is `#[derive(Deserialize)]` and contains
64/// recursive variants (`Array { items: Box<SchemaType> }`,
65/// `Object { properties: HashMap<_, SchemaType> }`,
66/// `AnyOf { schemas: Vec<SchemaType> }`). An attacker who can
67/// ship a schema (announcements broadcast over the mesh, or any
68/// caller that parses untrusted JSON into `SchemaType`) could
69/// otherwise submit a deeply-nested schema and crash the
70/// validator (and the whole process) via stack overflow on an
71/// unbounded recursive `validate`. 128 is generous for realistic
72/// schemas (typical JSON Schemas rarely exceed depth 10) and well
73/// clear of the typical default 8 MB Linux stack.
74pub const MAX_SCHEMA_DEPTH: usize = 128;
75
76/// Scan the byte stream of a JSON document and reject if
77/// nesting depth (the deepest stack of `{` and `[` after
78/// balancing) exceeds `max_depth`.
79///
80/// This is the deserialize-side defence for [`SchemaType`]: an
81/// adversarial schema with thousands of nested `{"type":"array",
82/// "items":...}` levels would otherwise either trip `serde_json`'s
83/// internal limit (currently 128 by default but tied to a
84/// transitive dependency) or stack-overflow the typed
85/// deserialize. Pre-scanning the bytes has a single linear-time
86/// cost regardless of which deserialize path follows.
87///
88/// String literals are handled correctly: bracket characters
89/// inside a `"..."` string don't change depth, and escapes
90/// (`\"`, `\\`) are skipped so a `}` inside a string can't fool
91/// the counter.
92///
93/// Returns `Err(serde_json::Error)` with a `Custom` kind so
94/// callers can match on `*::is_data` / `*::is_eof` etc. uniformly
95/// with the standard `serde_json::from_slice` error surface.
96fn 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                // Skip the rest of the string. Honor `\"` (don't
124                // exit) and `\\` (don't treat the following char
125                // as an escape). Anything else inside the string
126                // is opaque to the depth counter.
127                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/// JSON Schema type definitions
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[serde(tag = "type", rename_all = "lowercase")]
148pub enum SchemaType {
149    /// Null type
150    Null,
151    /// Boolean type
152    Boolean,
153    /// Integer type
154    Integer {
155        /// Inclusive minimum value
156        #[serde(skip_serializing_if = "Option::is_none")]
157        minimum: Option<i64>,
158        /// Inclusive maximum value
159        #[serde(skip_serializing_if = "Option::is_none")]
160        maximum: Option<i64>,
161        /// Value must be a multiple of this
162        #[serde(skip_serializing_if = "Option::is_none")]
163        multiple_of: Option<i64>,
164    },
165    /// Number (float) type
166    Number {
167        /// Inclusive minimum value
168        #[serde(skip_serializing_if = "Option::is_none")]
169        minimum: Option<f64>,
170        /// Inclusive maximum value
171        #[serde(skip_serializing_if = "Option::is_none")]
172        maximum: Option<f64>,
173    },
174    /// String type
175    String {
176        /// Minimum string length in characters
177        #[serde(skip_serializing_if = "Option::is_none")]
178        min_length: Option<usize>,
179        /// Maximum string length in characters
180        #[serde(skip_serializing_if = "Option::is_none")]
181        max_length: Option<usize>,
182        /// Regular expression pattern the string must match
183        #[serde(skip_serializing_if = "Option::is_none")]
184        pattern: Option<String>,
185        /// Semantic format of the string value
186        #[serde(skip_serializing_if = "Option::is_none")]
187        format: Option<StringFormat>,
188    },
189    /// Array type
190    Array {
191        /// Schema for each array element
192        items: Box<SchemaType>,
193        /// Minimum number of items
194        #[serde(skip_serializing_if = "Option::is_none")]
195        min_items: Option<usize>,
196        /// Maximum number of items
197        #[serde(skip_serializing_if = "Option::is_none")]
198        max_items: Option<usize>,
199        /// Whether all items must be unique
200        #[serde(default)]
201        unique_items: bool,
202    },
203    /// Object type
204    Object {
205        /// Named property schemas
206        properties: HashMap<String, SchemaType>,
207        /// Property names that must be present
208        #[serde(default)]
209        required: Vec<String>,
210        /// Whether properties not listed in `properties` are allowed
211        #[serde(default)]
212        additional_properties: bool,
213    },
214    /// Enum type (one of specific values)
215    Enum {
216        /// Allowed JSON values for this enum
217        values: Vec<serde_json::Value>,
218    },
219    /// Union type (anyOf)
220    AnyOf {
221        /// Candidate schemas, at least one of which must validate
222        schemas: Vec<SchemaType>,
223    },
224    /// Reference to another schema
225    Ref {
226        /// Name or path of the referenced schema
227        schema_ref: String,
228    },
229    /// Any type (no validation)
230    Any,
231}
232
233/// String format specifiers
234#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
235#[serde(rename_all = "kebab-case")]
236pub enum StringFormat {
237    /// Date-time format (ISO 8601)
238    DateTime,
239    /// Date format
240    Date,
241    /// Time format
242    Time,
243    /// Duration format
244    Duration,
245    /// Email address
246    Email,
247    /// URI format
248    Uri,
249    /// UUID format
250    Uuid,
251    /// IPv4 address
252    Ipv4,
253    /// IPv6 address
254    Ipv6,
255    /// Base64 encoded binary
256    Base64,
257    /// Hexadecimal string
258    Hex,
259    /// JSON string
260    Json,
261    /// Markdown text
262    Markdown,
263}
264
265impl SchemaType {
266    /// Deserialize a `SchemaType` from JSON bytes with an explicit
267    /// nesting-depth cap.
268    ///
269    /// Callers that deserialize peer-supplied / untrusted JSON
270    /// into `SchemaType` MUST use this entry point. The
271    /// derive-`Deserialize` path inherits `serde_json`'s built-in
272    /// 128-frame recursion limit, but that's tied to a transitive
273    /// dependency and may shift across versions; we pin a local
274    /// cap matching [`MAX_SCHEMA_DEPTH`] by **pre-scanning** the
275    /// input bytes for max nesting depth (cheap O(n) walk over
276    /// `{`/`[`/`}`/`]` outside of strings) and rejecting before
277    /// any deserialize work runs. This also guards against
278    /// `serde_json::from_slice::<SchemaType>(...)` callsites that
279    /// might bypass [`Self::validate`]'s post-parse cap entirely.
280    ///
281    /// Returns:
282    /// - `Err(serde_json::Error)` with kind `Custom("max nesting
283    ///   depth exceeded")` if depth > [`MAX_SCHEMA_DEPTH`].
284    /// - The standard `serde_json::Error` variants from the
285    ///   downstream `from_slice` call otherwise.
286    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    /// Deserialize a `SchemaType` from a JSON string with an
292    /// explicit nesting-depth cap. See [`Self::try_from_slice`].
293    pub fn try_from_str(s: &str) -> Result<Self, serde_json::Error> {
294        Self::try_from_slice(s.as_bytes())
295    }
296
297    /// Create a string schema
298    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    /// Create an integer schema
308    pub fn integer() -> Self {
309        SchemaType::Integer {
310            minimum: None,
311            maximum: None,
312            multiple_of: None,
313        }
314    }
315
316    /// Create a number schema
317    pub fn number() -> Self {
318        SchemaType::Number {
319            minimum: None,
320            maximum: None,
321        }
322    }
323
324    /// Create a boolean schema
325    pub fn boolean() -> Self {
326        SchemaType::Boolean
327    }
328
329    /// Create an array schema
330    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    /// Create an object schema
340    pub fn object() -> Self {
341        SchemaType::Object {
342            properties: HashMap::new(),
343            required: Vec::new(),
344            additional_properties: true,
345        }
346    }
347
348    /// Add a property to an object schema
349    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    /// Mark a property as required
360    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    /// Set minimum for integer
371    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    /// Set maximum for integer
382    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    /// Set max length for string
393    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    /// Set format for string
404    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    /// Validate a JSON value against this schema.
412    ///
413    /// The recursion is bounded by [`MAX_SCHEMA_DEPTH`]; exceeding
414    /// it returns [`ValidationError::RecursionLimitExceeded`]
415    /// instead of blowing the stack. Recursive variants (`Array`,
416    /// `Object`, `AnyOf`) call `validate` recursively, so an
417    /// attacker who could ship a `SchemaType` (announcements
418    /// broadcast over the mesh, or any caller that parses
419    /// untrusted JSON into `SchemaType`) could otherwise submit a
420    /// deeply-nested schema and crash the validator — and the
421    /// whole process — via stack overflow when a request got
422    /// validated against it.
423    pub fn validate(&self, value: &serde_json::Value) -> Result<(), ValidationError> {
424        self.validate_with_depth(value, 0)
425    }
426
427    /// Internal depth-bounded validate — see [`validate`].
428    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                    // Simple pattern check - in production would use regex
554                    if !s.contains(pat.as_str()) {
555                        return Err(ValidationError::PatternMismatch {
556                            value: s.clone(),
557                            pattern: pat.clone(),
558                        });
559                    }
560                }
561                // Format validation would go here
562                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                        // Surface the recursion-limit signal
608                        // unwrapped — wrapping it in
609                        // `ArrayItemError` would obscure the
610                        // anti-DoS check from callers walking
611                        // the error chain.
612                        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                // Check required fields
637                for req in required {
638                    if !obj.contains_key(req) {
639                        return Err(ValidationError::MissingRequired { field: req.clone() });
640                    }
641                }
642
643                // Validate properties
644                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                            // Same as Array — surface the
648                            // recursion-limit signal unwrapped.
649                            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                            // Don't swallow the recursion-limit
687                            // signal — surface it instead of
688                            // converting to AnyOfFailed.
689                            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                // Reference resolution would happen at registry level
701                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/// Validation errors
721#[derive(Debug, Clone, PartialEq)]
722pub enum ValidationError {
723    /// Type mismatch
724    TypeMismatch {
725        /// Expected type name
726        expected: String,
727        /// Actual type name received
728        got: String,
729    },
730    /// Value out of range
731    RangeError {
732        /// The value that failed the range check
733        value: f64,
734        /// Inclusive minimum bound, if any
735        min: Option<f64>,
736        /// Inclusive maximum bound, if any
737        max: Option<f64>,
738    },
739    /// Multiple-of constraint failed
740    MultipleOfError {
741        /// The value that failed the constraint
742        value: i64,
743        /// The required divisor
744        multiple_of: i64,
745    },
746    /// Length constraint failed
747    LengthError {
748        /// Actual length of the string or array
749        length: usize,
750        /// Minimum allowed length, if any
751        min: Option<usize>,
752        /// Maximum allowed length, if any
753        max: Option<usize>,
754    },
755    /// Pattern mismatch
756    PatternMismatch {
757        /// The string value that did not match
758        value: String,
759        /// The regex pattern that was required
760        pattern: String,
761    },
762    /// Duplicate items in array
763    DuplicateItems,
764    /// Array item validation failed
765    ArrayItemError {
766        /// Zero-based index of the failing item
767        index: usize,
768        /// Nested validation error for the item
769        error: Box<ValidationError>,
770    },
771    /// Missing required field
772    MissingRequired {
773        /// Name of the required field that was absent
774        field: String,
775    },
776    /// Unknown property
777    UnknownProperty {
778        /// Name of the disallowed additional property
779        property: String,
780    },
781    /// Property validation failed
782    PropertyError {
783        /// Name of the property that failed validation
784        property: String,
785        /// Nested validation error for the property value
786        error: Box<ValidationError>,
787    },
788    /// Enum value not in allowed list
789    EnumMismatch {
790        /// The value that was not in the allowed set
791        value: serde_json::Value,
792        /// The set of allowed values
793        allowed: Vec<serde_json::Value>,
794    },
795    /// AnyOf validation failed
796    AnyOfFailed {
797        /// Number of candidate schemas that were all tried and failed
798        schema_count: usize,
799    },
800    /// Schema recursion depth exceeded (anti-DoS guard).
801    ///
802    /// Returned by [`SchemaType::validate`] when the recursive
803    /// walk through nested `Array`/`Object`/`AnyOf` variants
804    /// exceeds [`MAX_SCHEMA_DEPTH`]. Without this cap, an
805    /// attacker who could ship a `SchemaType` (announcements
806    /// broadcast over the mesh, or any caller parsing untrusted
807    /// JSON) could submit a deeply nested schema and crash the
808    /// validator (and the process) via stack overflow.
809    RecursionLimitExceeded {
810        /// The depth limit that was exceeded.
811        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/// API parameter definition
862#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
863pub struct ApiParameter {
864    /// Parameter name
865    pub name: String,
866    /// Parameter description
867    pub description: Option<String>,
868    /// Whether parameter is required
869    pub required: bool,
870    /// Parameter schema
871    pub schema: SchemaType,
872    /// Default value (if not required)
873    pub default: Option<serde_json::Value>,
874    /// Example value
875    pub example: Option<serde_json::Value>,
876}
877
878impl ApiParameter {
879    /// Create a new required parameter
880    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    /// Create a new optional parameter
892    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    /// Set description
904    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
905        self.description = Some(desc.into());
906        self
907    }
908
909    /// Set default value
910    pub fn with_default(mut self, default: serde_json::Value) -> Self {
911        self.default = Some(default);
912        self
913    }
914
915    /// Set example
916    pub fn with_example(mut self, example: serde_json::Value) -> Self {
917        self.example = Some(example);
918        self
919    }
920}
921
922/// API endpoint definition
923#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
924pub struct ApiEndpoint {
925    /// Endpoint path (e.g., "/models/{model_id}/infer")
926    pub path: String,
927    /// HTTP-like method
928    pub method: ApiMethod,
929    /// Endpoint description
930    pub description: Option<String>,
931    /// Path parameters
932    pub path_params: Vec<ApiParameter>,
933    /// Query parameters
934    pub query_params: Vec<ApiParameter>,
935    /// Request body schema
936    pub request_body: Option<SchemaType>,
937    /// Response schema
938    pub response: Option<SchemaType>,
939    /// Error response schema
940    pub error_response: Option<SchemaType>,
941    /// Required capabilities to call this endpoint
942    pub required_capabilities: Vec<String>,
943    /// Tags for categorization
944    pub tags: Vec<String>,
945    /// Whether endpoint is deprecated
946    pub deprecated: bool,
947    /// Rate limit (requests per minute)
948    pub rate_limit: Option<u32>,
949    /// Timeout in milliseconds
950    pub timeout_ms: Option<u64>,
951    /// Whether authentication is required
952    pub auth_required: bool,
953}
954
955impl ApiEndpoint {
956    /// Create a new endpoint
957    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    /// Set description
977    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
978        self.description = Some(desc.into());
979        self
980    }
981
982    /// Add path parameter
983    pub fn with_path_param(mut self, param: ApiParameter) -> Self {
984        self.path_params.push(param);
985        self
986    }
987
988    /// Add query parameter
989    pub fn with_query_param(mut self, param: ApiParameter) -> Self {
990        self.query_params.push(param);
991        self
992    }
993
994    /// Set request body schema
995    pub fn with_request_body(mut self, schema: SchemaType) -> Self {
996        self.request_body = Some(schema);
997        self
998    }
999
1000    /// Set response schema
1001    pub fn with_response(mut self, schema: SchemaType) -> Self {
1002        self.response = Some(schema);
1003        self
1004    }
1005
1006    /// Add required capability
1007    pub fn require_capability(mut self, cap: impl Into<String>) -> Self {
1008        self.required_capabilities.push(cap.into());
1009        self
1010    }
1011
1012    /// Add tag
1013    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1014        self.tags.push(tag.into());
1015        self
1016    }
1017
1018    /// Set rate limit
1019    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    /// Set timeout
1025    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
1026        self.timeout_ms = Some(timeout_ms);
1027        self
1028    }
1029
1030    /// Mark as not requiring auth
1031    pub fn no_auth(mut self) -> Self {
1032        self.auth_required = false;
1033        self
1034    }
1035
1036    /// Mark as deprecated
1037    pub fn deprecated(mut self) -> Self {
1038        self.deprecated = true;
1039        self
1040    }
1041
1042    /// Validate request parameters
1043    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        // Validate path params
1050        for param in &self.path_params {
1051            if let Some(value) = path_params.get(&param.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        // Validate query params
1067        for param in &self.query_params {
1068            if let Some(value) = query_params.get(&param.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        // Validate body
1084        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    /// Check if endpoint matches a path
1101    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                // Extract parameter name
1114                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/// API validation errors
1126#[derive(Debug, Clone, PartialEq)]
1127pub enum ApiValidationError {
1128    /// Missing path parameter
1129    MissingPathParameter {
1130        /// Name of the missing path parameter
1131        name: String,
1132    },
1133    /// Path parameter validation failed
1134    PathParameter {
1135        /// Name of the path parameter that failed
1136        name: String,
1137        /// Underlying schema validation error
1138        error: ValidationError,
1139    },
1140    /// Missing query parameter
1141    MissingQueryParameter {
1142        /// Name of the missing query parameter
1143        name: String,
1144    },
1145    /// Query parameter validation failed
1146    QueryParameter {
1147        /// Name of the query parameter that failed
1148        name: String,
1149        /// Underlying schema validation error
1150        error: ValidationError,
1151    },
1152    /// Missing request body
1153    MissingRequestBody,
1154    /// Request body validation failed
1155    RequestBody {
1156        /// Underlying schema validation error for the request body
1157        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/// Semantic versioning for APIs
1187#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1188pub struct ApiVersion {
1189    /// Major version (breaking changes)
1190    pub major: u32,
1191    /// Minor version (new features, backwards compatible)
1192    pub minor: u32,
1193    /// Patch version (bug fixes)
1194    pub patch: u32,
1195}
1196
1197impl ApiVersion {
1198    /// Create a new version
1199    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
1200        Self {
1201            major,
1202            minor,
1203            patch,
1204        }
1205    }
1206
1207    /// Check if this version is compatible with a requirement
1208    pub fn is_compatible_with(&self, required: &ApiVersion) -> bool {
1209        // Major versions must match
1210        if self.major != required.major {
1211            return false;
1212        }
1213        // Our minor version must be >= required
1214        if self.minor < required.minor {
1215            return false;
1216        }
1217        // If minor versions match, patch must be >= required
1218        if self.minor == required.minor && self.patch < required.patch {
1219            return false;
1220        }
1221        true
1222    }
1223
1224    /// Parse from string "major.minor.patch"
1225    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/// Complete API schema for a node
1263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1264pub struct ApiSchema {
1265    /// Schema name
1266    pub name: String,
1267    /// Schema description
1268    pub description: Option<String>,
1269    /// API version
1270    pub version: ApiVersion,
1271    /// Base path prefix
1272    pub base_path: String,
1273    /// Available endpoints
1274    pub endpoints: Vec<ApiEndpoint>,
1275    /// Shared schema definitions (for $ref)
1276    pub definitions: HashMap<String, SchemaType>,
1277    /// Global tags
1278    pub tags: Vec<String>,
1279    /// Contact information
1280    pub contact: Option<String>,
1281    /// License
1282    pub license: Option<String>,
1283}
1284
1285impl ApiSchema {
1286    /// Create a new API schema
1287    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    /// Set description
1302    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
1303        self.description = Some(desc.into());
1304        self
1305    }
1306
1307    /// Set base path
1308    pub fn with_base_path(mut self, path: impl Into<String>) -> Self {
1309        self.base_path = path.into();
1310        self
1311    }
1312
1313    /// Add endpoint
1314    pub fn add_endpoint(mut self, endpoint: ApiEndpoint) -> Self {
1315        self.endpoints.push(endpoint);
1316        self
1317    }
1318
1319    /// Add schema definition
1320    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    /// Add tag
1326    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1327        self.tags.push(tag.into());
1328        self
1329    }
1330
1331    /// Find endpoint by path and method
1332    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    /// Get all endpoints with a specific tag
1345    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    /// Serialize to bytes
1353    pub fn to_bytes(&self) -> Vec<u8> {
1354        serde_json::to_vec(self).unwrap_or_default()
1355    }
1356
1357    /// Deserialize from bytes
1358    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
1359        serde_json::from_slice(bytes).ok()
1360    }
1361}
1362
1363/// Node API announcement
1364#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1365pub struct ApiAnnouncement {
1366    /// Node ID
1367    pub node_id: NodeId,
1368    /// API schemas provided by this node
1369    pub schemas: Vec<ApiSchema>,
1370    /// Announcement version (monotonic)
1371    pub version: u64,
1372    /// Timestamp (Unix millis)
1373    pub timestamp: u64,
1374    /// TTL in seconds
1375    pub ttl_secs: u32,
1376}
1377
1378impl ApiAnnouncement {
1379    /// Create a new announcement
1380    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    /// Set version
1394    pub fn with_version(mut self, version: u64) -> Self {
1395        self.version = version;
1396        self
1397    }
1398
1399    /// Set TTL
1400    pub fn with_ttl(mut self, ttl_secs: u32) -> Self {
1401        self.ttl_secs = ttl_secs;
1402        self
1403    }
1404
1405    /// Check if expired
1406    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    /// Serialize to bytes
1416    pub fn to_bytes(&self) -> Vec<u8> {
1417        serde_json::to_vec(self).unwrap_or_default()
1418    }
1419
1420    /// Deserialize from bytes
1421    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
1422        serde_json::from_slice(bytes).ok()
1423    }
1424}
1425
1426/// API query for finding nodes with specific APIs
1427#[derive(Debug, Clone, Default)]
1428pub struct ApiQuery {
1429    /// Required API name
1430    pub api_name: Option<String>,
1431    /// Minimum version required
1432    pub min_version: Option<ApiVersion>,
1433    /// Required endpoint path pattern
1434    pub endpoint_path: Option<String>,
1435    /// Required endpoint method
1436    pub endpoint_method: Option<ApiMethod>,
1437    /// Required tag
1438    pub tag: Option<String>,
1439    /// Must have specific capability
1440    pub capability: Option<String>,
1441}
1442
1443impl ApiQuery {
1444    /// Create a new query
1445    pub fn new() -> Self {
1446        Self::default()
1447    }
1448
1449    /// Filter by API name
1450    pub fn with_api(mut self, name: impl Into<String>) -> Self {
1451        self.api_name = Some(name.into());
1452        self
1453    }
1454
1455    /// Filter by minimum version
1456    pub fn with_min_version(mut self, version: ApiVersion) -> Self {
1457        self.min_version = Some(version);
1458        self
1459    }
1460
1461    /// Filter by endpoint path
1462    pub fn with_endpoint(mut self, path: impl Into<String>) -> Self {
1463        self.endpoint_path = Some(path.into());
1464        self
1465    }
1466
1467    /// Filter by method
1468    pub fn with_method(mut self, method: ApiMethod) -> Self {
1469        self.endpoint_method = Some(method);
1470        self
1471    }
1472
1473    /// Filter by tag
1474    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1475        self.tag = Some(tag.into());
1476        self
1477    }
1478
1479    /// Filter by capability
1480    pub fn with_capability(mut self, cap: impl Into<String>) -> Self {
1481        self.capability = Some(cap.into());
1482        self
1483    }
1484
1485    /// Check if a schema matches this query
1486    pub fn matches_schema(&self, schema: &ApiSchema) -> bool {
1487        // Check API name
1488        if let Some(ref name) = self.api_name {
1489            if &schema.name != name {
1490                return false;
1491            }
1492        }
1493
1494        // Check version
1495        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        // Check endpoint
1502        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        // Check tag
1515        if let Some(ref tag) = self.tag {
1516            if !schema.tags.contains(tag) {
1517                return false;
1518            }
1519        }
1520
1521        // Check capability
1522        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/// Registry errors
1537#[derive(Debug, Clone, PartialEq, Eq)]
1538pub enum RegistryError {
1539    /// Node not found
1540    NodeNotFound(NodeId),
1541    /// API not found
1542    ApiNotFound(String),
1543    /// Version conflict
1544    VersionConflict {
1545        /// Version that was required for the operation
1546        expected: u64,
1547        /// Version that was found in the registry
1548        actual: u64,
1549    },
1550    /// Capacity exceeded
1551    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/// Indexed node API information
1570#[derive(Debug, Clone)]
1571pub struct IndexedApiNode {
1572    /// Node ID
1573    pub node_id: NodeId,
1574    /// API announcement
1575    pub announcement: Arc<ApiAnnouncement>,
1576}
1577
1578/// Registry statistics
1579#[derive(Debug, Clone, Default)]
1580pub struct ApiRegistryStats {
1581    /// Total nodes registered
1582    pub total_nodes: usize,
1583    /// Total API schemas
1584    pub total_schemas: usize,
1585    /// Total endpoints
1586    pub total_endpoints: usize,
1587    /// APIs by name
1588    pub apis_by_name: HashMap<String, usize>,
1589    /// Query count
1590    pub queries: u64,
1591    /// Update count
1592    pub updates: u64,
1593}
1594
1595/// High-performance API registry with indexes
1596pub struct ApiRegistry {
1597    /// Primary storage: node_id -> announcement
1598    nodes: DashMap<NodeId, Arc<ApiAnnouncement>>,
1599    /// Index by API name
1600    by_api_name: DashMap<String, HashSet<NodeId>>,
1601    /// Index by tag
1602    by_tag: DashMap<String, HashSet<NodeId>>,
1603    /// Index by endpoint path pattern
1604    by_endpoint: DashMap<String, HashSet<NodeId>>,
1605    /// Query counter
1606    query_count: AtomicU64,
1607    /// Update counter
1608    update_count: AtomicU64,
1609    /// Maximum capacity
1610    max_capacity: Option<usize>,
1611}
1612
1613/// Extract the leading path prefix used as the `by_endpoint` index
1614/// key. Slices up to (but not including) the second `/` so the
1615/// computation matches between `add_to_indexes` and
1616/// `remove_from_indexes` without allocating an intermediate
1617/// `Vec<&str>` for the split + join. Equivalent to the previous
1618/// `path.split('/').take(2).collect::<Vec<_>>().join("/")`.
1619fn 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    /// Create a new registry
1628    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    /// Create with capacity limit
1641    pub fn with_capacity(max: usize) -> Self {
1642        let mut reg = Self::new();
1643        reg.max_capacity = Some(max);
1644        reg
1645    }
1646
1647    /// Register or update a node's APIs
1648    pub fn register(&self, announcement: ApiAnnouncement) -> Result<(), RegistryError> {
1649        let node_id = announcement.node_id;
1650
1651        // Check capacity
1652        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        // Remove old indexes if updating
1659        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        // Add to indexes
1666        self.add_to_indexes(&ann);
1667
1668        // Store
1669        self.nodes.insert(node_id, ann);
1670        self.update_count.fetch_add(1, Ordering::Relaxed);
1671
1672        Ok(())
1673    }
1674
1675    /// Unregister a node
1676    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    /// Get a node's API announcement
1686    pub fn get(&self, node_id: &NodeId) -> Option<Arc<ApiAnnouncement>> {
1687        self.nodes.get(node_id).map(|r| Arc::clone(&r))
1688    }
1689
1690    /// Query for nodes matching criteria
1691    pub fn query(&self, query: &ApiQuery) -> Vec<IndexedApiNode> {
1692        self.query_count.fetch_add(1, Ordering::Relaxed);
1693
1694        // Use indexes for initial filtering
1695        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            // Full scan
1707            self.nodes.iter().map(|r| *r.key()).collect()
1708        };
1709
1710        // Filter and collect
1711        candidates
1712            .into_iter()
1713            .filter_map(|id| {
1714                let ann = self.nodes.get(&id)?;
1715                // Check if any schema matches
1716                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    /// Find nodes that provide a specific API endpoint
1730    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                // Check if any schema has this endpoint
1742                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    /// Find nodes with compatible API version
1762    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    /// Get statistics
1796    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    /// Number of registered nodes
1818    pub fn len(&self) -> usize {
1819        self.nodes.len()
1820    }
1821
1822    /// Check if empty
1823    pub fn is_empty(&self) -> bool {
1824        self.nodes.is_empty()
1825    }
1826
1827    /// Clear all registrations
1828    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    /// Remove expired entries
1836    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    // Private helper to add indexes
1852    fn add_to_indexes(&self, ann: &ApiAnnouncement) {
1853        let node_id = ann.node_id;
1854
1855        for schema in &ann.schemas {
1856            // API name index
1857            self.by_api_name
1858                .entry(schema.name.clone())
1859                .or_default()
1860                .insert(node_id);
1861
1862            // Tag index
1863            for tag in &schema.tags {
1864                self.by_tag.entry(tag.clone()).or_default().insert(node_id);
1865            }
1866
1867            // Endpoint index (simplified - just uses path prefix).
1868            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    // Private helper to remove indexes
1876    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        // String validation
1919        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        // Integer validation
1924        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        // Object validation
1930        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()); // missing required
1939
1940        // Array validation
1941        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    /// Regression for BUG_AUDIT_2026_04_30_CORE.md #109: pre-fix
1947    /// `SchemaType::validate` recursed without bound through
1948    /// `Array { items }` / `Object { properties }` / `AnyOf { schemas }`.
1949    /// An attacker who could ship a `SchemaType` (announcements
1950    /// broadcast over the mesh, or any caller parsing untrusted
1951    /// JSON) could submit a deeply-nested schema and crash the
1952    /// process via stack overflow when a request was validated.
1953    /// Post-fix: depth is bounded by `MAX_SCHEMA_DEPTH`;
1954    /// exceeding it returns `RecursionLimitExceeded` instead.
1955    ///
1956    /// We pin the bound by constructing a schema deeper than the
1957    /// limit (chained Array variants — each `Array { items: ... }`
1958    /// adds one level of recursion). With a payload that walks
1959    /// through every level, validate must surface the
1960    /// recursion-limit error rather than blowing the stack.
1961    #[test]
1962    fn validate_returns_recursion_limit_error_on_deeply_nested_schema() {
1963        // Build an Array<Array<Array<...<Integer>...>>> with
1964        // depth = MAX_SCHEMA_DEPTH + 5 (well past the cap).
1965        let mut schema = SchemaType::integer();
1966        for _ in 0..MAX_SCHEMA_DEPTH + 5 {
1967            schema = SchemaType::array(schema);
1968        }
1969
1970        // Build a matching nested-array payload so each level
1971        // descends into the next.
1972        let mut value = serde_json::json!(1);
1973        for _ in 0..MAX_SCHEMA_DEPTH + 5 {
1974            value = serde_json::json!([value]);
1975        }
1976
1977        // Pre-fix: stack overflow. Post-fix: bounded error.
1978        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    /// Sanity: a schema at exactly `MAX_SCHEMA_DEPTH` levels of
1988    /// nesting must still validate successfully (no false-positive
1989    /// recursion error).
1990    #[test]
1991    fn validate_accepts_schema_at_recursion_limit() {
1992        let mut schema = SchemaType::integer();
1993        // depth = MAX_SCHEMA_DEPTH - 1 means the validator visits
1994        // depth values 0..MAX_SCHEMA_DEPTH, all under the cap.
1995        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    /// CR-9: deeply-nested input must be rejected at the deserialize
2009    /// boundary, BEFORE `validate` is called. Pre-fix the cap was
2010    /// only enforced post-parse — an adversarial schema could
2011    /// trigger the recursive `Deserialize` to allocate a deep
2012    /// `SchemaType` tree (or stack-overflow on a very deep input)
2013    /// before any validation ran.
2014    #[test]
2015    fn try_from_slice_rejects_input_over_max_schema_depth() {
2016        // Build a JSON string with MAX_SCHEMA_DEPTH + 50 nested
2017        // arrays. Even though valid JSON, it must trip the
2018        // depth-scan guard before serde_json runs.
2019        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    /// CR-9: the depth pre-scan must not be fooled by JSON strings
2039    /// containing brackets. A long string of `}`s inside `"..."`
2040    /// must NOT be counted as depth-out (which would let an
2041    /// attacker mask real depth).
2042    #[test]
2043    fn try_from_slice_handles_brackets_inside_strings_correctly() {
2044        // A schema with a `pattern` field containing brackets in
2045        // a string. The string brackets must be ignored by the
2046        // depth counter.
2047        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    /// CR-9: a moderately-deep schema (well under both the depth
2057    /// pre-scan cap AND serde_json's internal recursion limit)
2058    /// must parse cleanly. The internal serde_json limit (128) and
2059    /// our `MAX_SCHEMA_DEPTH` (128) are intentionally aligned, but
2060    /// each `Box<SchemaType>` adds a serde call frame on top of
2061    /// the byte-counter depth, so the effective serde-side ceiling
2062    /// is a bit below `MAX_SCHEMA_DEPTH`. We pin a depth of 32
2063    /// here — comfortably representative of any real-world nested
2064    /// schema and well within both caps.
2065    #[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    /// CR-9: direct unit test on the depth scanner — confirms
2086    /// it counts both `{`/`}` and `[`/`]` correctly and respects
2087    /// string-literal boundaries.
2088    #[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()); // depth 1 > 0
2092        assert!(check_json_nesting_depth(b"[[[[]]]]", 4).is_ok());
2093        assert!(check_json_nesting_depth(b"[[[[]]]]", 3).is_err());
2094        // Brackets inside a string are NOT counted.
2095        assert!(check_json_nesting_depth(b"\"[[[[\"", 0).is_ok());
2096        // Escaped quote keeps us inside the string.
2097        assert!(check_json_nesting_depth(b"\"[\\\"[[\"", 0).is_ok());
2098        // Mixed nesting.
2099        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        // Should match
2109        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        // Should not match (wrong path)
2115        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        // Same version is compatible
2127        assert!(v1_0_0.is_compatible_with(&v1_0_0));
2128
2129        // Higher minor version is compatible
2130        assert!(v1_1_0.is_compatible_with(&v1_0_0));
2131
2132        // Higher patch version is compatible
2133        assert!(v1_1_1.is_compatible_with(&v1_1_0));
2134
2135        // Lower minor version is not compatible
2136        assert!(!v1_0_0.is_compatible_with(&v1_1_0));
2137
2138        // Different major version is not compatible
2139        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        // Find by tag
2164        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        // Add multiple nodes with different APIs
2193        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        // Query by API name
2206        let results = registry.query(&ApiQuery::new().with_api("inference"));
2207        assert_eq!(results.len(), 5);
2208
2209        // Query by tag
2210        let results = registry.query(&ApiQuery::new().with_tag("gpu"));
2211        assert_eq!(results.len(), 5);
2212
2213        // Query by both
2214        let results = registry.query(&ApiQuery::new().with_api("inference").with_tag("gpu"));
2215        // inference (0-4), gpu (0,2,4,6,8) -> intersection is 0,2,4
2216        assert_eq!(results.len(), 3);
2217    }
2218
2219    #[test]
2220    fn test_api_registry_version_compatibility() {
2221        let registry = ApiRegistry::new();
2222
2223        // Add nodes with different versions
2224        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        // Find nodes compatible with v1.2.0
2231        let results = registry.find_compatible("my-api", &ApiVersion::new(1, 2, 0));
2232        // v1.2.0, v1.3.0, v1.4.0 are compatible
2233        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        // Valid request
2243        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        // Missing required path param
2252        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        // Run some queries
2288        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    /// `endpoint_prefix` replaces the previous
2300    /// `path.split('/').take(2).collect::<Vec<_>>().join("/")` with a
2301    /// `match_indices('/').nth(1)`-based slice. The replacement
2302    /// must be byte-identical for every shape we feed it — a single
2303    /// drift here would put `add_to_indexes` and
2304    /// `remove_from_indexes` out of sync and silently leak entries
2305    /// in `by_endpoint`. Each case below names the previous
2306    /// behavior explicitly so a future reviewer can see the
2307    /// equivalence at a glance.
2308    #[test]
2309    fn endpoint_prefix_matches_previous_split_join_behavior() {
2310        // Helper that runs the OLD logic for ground truth.
2311        fn old(path: &str) -> String {
2312            path.split('/').take(2).collect::<Vec<_>>().join("/")
2313        }
2314
2315        let cases: &[&str] = &[
2316            "",               // empty
2317            "/",              // a lone separator
2318            "//",             // two separators, nothing between
2319            "//a",            // empty leading segment, then content
2320            "/a",             // single leading-slash segment
2321            "/a/",            // trailing slash
2322            "a",              // no slashes at all
2323            "a/",             // single segment + trailing slash
2324            "/api",           // typical absolute root
2325            "/api/users",     // two-segment absolute
2326            "/api/users/123", // deep absolute
2327            "api/users/123",  // deep relative
2328            "/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    // ---------- Validation error-branch coverage ----------
2342    //
2343    // The existing happy-path tests cover the success arms of
2344    // `SchemaType::validate`. These exercise the negative branches
2345    // that codecov flagged as uncovered: `Number` min/max (distinct
2346    // from `Integer`), every type-mismatch arm, string length /
2347    // pattern errors, array length / uniqueness errors, object
2348    // property errors, and the `Enum` / `AnyOf` / `Ref` arms.
2349
2350    #[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        // PropertyError: known property fails its own schema.
2437        let err = schema
2438            .validate(&serde_json::json!({"name": "Alice", "age": "old"}))
2439            .unwrap_err();
2440        assert!(matches!(err, ValidationError::PropertyError { .. }));
2441
2442        // UnknownProperty: requires additional_properties=false, which
2443        // the `SchemaType::object()` builder doesn't expose — construct
2444        // directly to flip it.
2445        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        // TypeMismatch: object schema receives non-object.
2460        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        // Enum miss.
2469        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        // AnyOf success on second arm + AnyOfFailed when all reject.
2479        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        // Ref arm at validator level returns Ok — resolution
2490        // is a registry-level concern (see L699-702).
2491        let r = SchemaType::Ref {
2492            schema_ref: "#/definitions/X".into(),
2493        };
2494        assert!(r.validate(&serde_json::json!(null)).is_ok());
2495
2496        // Any matches anything.
2497        assert!(SchemaType::Any
2498            .validate(&serde_json::json!({"x":1}))
2499            .is_ok());
2500    }
2501
2502    // ---------- ApiQuery negative-branch coverage ----------
2503
2504    #[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        // Wrong api name.
2512        let q = ApiQuery::new().with_api("other");
2513        assert_eq!(registry_match_count(&ann, &q), 0);
2514
2515        // Wrong tag.
2516        let q = ApiQuery::new().with_tag("cpu");
2517        assert_eq!(registry_match_count(&ann, &q), 0);
2518
2519        // Wrong endpoint path.
2520        let q = ApiQuery::new().with_endpoint("/missing");
2521        assert_eq!(registry_match_count(&ann, &q), 0);
2522
2523        // Wrong method on existing path.
2524        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    /// Helper: register an announcement and count matches against a query.
2531    /// Keeps the test focused on the matcher, not registry plumbing.
2532    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    // ---------- Expired-entry filtering ----------
2539
2540    #[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        // Stamp `timestamp` at the Unix epoch with a short ttl so
2547        // `is_expired()` returns true regardless of wall-clock
2548        // resolution. A previous version slept 5ms past a
2549        // `with_ttl(0)` announcement — flaky on loaded CI boxes
2550        // where the wall clock can read backward between
2551        // `ApiAnnouncement::new`'s SystemTime call and the
2552        // `is_expired()` check.
2553        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}