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}