postmortem/schema/
ref_schema.rs

1//! Schema reference type for registry-based validation.
2//!
3//! This module provides [`RefSchema`] which represents a reference to a named schema
4//! in a registry. References enable schema reuse and recursive structures.
5
6use serde_json::Value;
7use stillwater::Validation;
8
9use crate::error::{SchemaError, SchemaErrors};
10use crate::path::JsonPath;
11use crate::schema::SchemaLike;
12use crate::validation::ValidationContext;
13
14/// A schema that references another schema by name.
15///
16/// RefSchema enables schema reuse and recursive structures by referencing
17/// schemas stored in a registry. During validation, the reference is resolved
18/// to the actual schema.
19///
20/// References can only be validated through a registry using `SchemaRegistry::validate()`.
21/// Attempting to validate without a registry produces an error.
22///
23/// # Example
24///
25/// ```rust
26/// use postmortem::{Schema, SchemaRegistry};
27/// use serde_json::json;
28///
29/// let registry = SchemaRegistry::new();
30///
31/// // Register a base schema
32/// registry.register("UserId", Schema::integer().positive()).unwrap();
33///
34/// // Use a reference in another schema
35/// registry.register("User", Schema::object()
36///     .field("id", Schema::ref_("UserId"))
37///     .field("name", Schema::string())
38/// ).unwrap();
39///
40/// let result = registry.validate("User", &json!({
41///     "id": 42,
42///     "name": "Alice"
43/// })).unwrap();
44///
45/// assert!(result.is_success());
46/// ```
47pub struct RefSchema {
48    name: String,
49}
50
51impl RefSchema {
52    /// Creates a new schema reference.
53    ///
54    /// This is typically called via `Schema::ref_()` rather than directly.
55    pub fn new(name: impl Into<String>) -> Self {
56        Self { name: name.into() }
57    }
58
59    /// Returns the name of the referenced schema.
60    pub fn name(&self) -> &str {
61        &self.name
62    }
63}
64
65impl SchemaLike for RefSchema {
66    type Output = Value;
67
68    fn validate(&self, _value: &Value, path: &JsonPath) -> Validation<Value, SchemaErrors> {
69        // Cannot validate reference without registry
70        Validation::Failure(SchemaErrors::single(
71            SchemaError::new(
72                path.clone(),
73                format!(
74                    "reference to '{}' cannot be validated without a registry. \
75                     Use SchemaRegistry::validate() instead",
76                    self.name
77                ),
78            )
79            .with_code("missing_registry"),
80        ))
81    }
82
83    fn validate_to_value(&self, value: &Value, path: &JsonPath) -> Validation<Value, SchemaErrors> {
84        self.validate(value, path)
85    }
86
87    fn validate_with_context(
88        &self,
89        value: &Value,
90        path: &JsonPath,
91        context: &ValidationContext,
92    ) -> Validation<Value, SchemaErrors> {
93        // Check depth before resolving to prevent infinite loops
94        if context.depth() >= context.max_depth() {
95            return Validation::Failure(SchemaErrors::single(
96                SchemaError::new(
97                    path.clone(),
98                    format!(
99                        "maximum reference depth {} exceeded at path '{}'",
100                        context.max_depth(),
101                        path
102                    ),
103                )
104                .with_code("max_depth_exceeded"),
105            ));
106        }
107
108        // Resolve reference from registry
109        let schema = match context.registry().get_schema(&self.name) {
110            Some(s) => s,
111            None => {
112                return Validation::Failure(SchemaErrors::single(
113                    SchemaError::new(
114                        path.clone(),
115                        format!("schema '{}' not found in registry", self.name),
116                    )
117                    .with_code("missing_reference"),
118                ))
119            }
120        };
121
122        // Validate with incremented depth to track reference chain
123        schema.validate_value_with_context(value, path, &context.increment_depth())
124    }
125
126    fn validate_to_value_with_context(
127        &self,
128        value: &Value,
129        path: &JsonPath,
130        context: &ValidationContext,
131    ) -> Validation<Value, SchemaErrors> {
132        self.validate_with_context(value, path, context)
133    }
134
135    fn collect_refs(&self, refs: &mut Vec<String>) {
136        refs.push(self.name.clone());
137    }
138}