Skip to main content

jsonschema_schema/schema/
navigate.rs

1use super::{Schema, SchemaValue};
2
3/// Extract the trailing name from a `$ref` path (e.g. `"#/$defs/Foo"` -> `"Foo"`).
4pub fn ref_name(ref_str: &str) -> &str {
5    ref_str.rsplit('/').next().unwrap_or(ref_str)
6}
7
8/// Resolve a `$ref` within the same schema document.
9///
10/// If the given schema has a `$ref` that begins with `#/`, follow the path
11/// through the root schema. Otherwise return the schema unchanged.
12pub fn resolve_ref<'a>(schema: &'a Schema, root: &'a Schema) -> &'a Schema {
13    if let Some(ref ref_str) = schema.ref_
14        && let Some(path) = ref_str.strip_prefix("#/")
15    {
16        // Navigate the root using serde_json::Value for flexibility
17        let Ok(root_value) = serde_json::to_value(root) else {
18            return schema;
19        };
20        let mut current = &root_value;
21        for segment in path.split('/') {
22            let decoded = segment.replace("~1", "/").replace("~0", "~");
23            match current.get(&decoded) {
24                Some(next) => current = next,
25                None => return schema,
26            }
27        }
28        // Try to deserialize the resolved value back into a Schema.
29        // This is expensive, so we use a different approach for the explain crate.
30        // For now, just return the original schema — the explain crate has its own
31        // resolve_ref that works with SchemaValue trees directly.
32        let _ = current;
33        return schema;
34    }
35    schema
36}
37
38/// Walk a JSON Pointer path through a schema, resolving `$ref` at each step.
39///
40/// Segments are decoded per RFC 6901 (`~1` → `/`, `~0` → `~`).
41/// Returns the sub-`SchemaValue` at the given pointer, or an error.
42///
43/// # Errors
44///
45/// Returns an error if a segment in the pointer cannot be resolved.
46pub fn navigate_pointer<'a>(
47    schema: &'a SchemaValue,
48    root: &'a SchemaValue,
49    pointer: &str,
50) -> Result<&'a SchemaValue, String> {
51    let path = pointer.strip_prefix('/').unwrap_or(pointer);
52    if path.is_empty() {
53        return Ok(schema);
54    }
55
56    let mut current = resolve_schema_value_ref(schema, root);
57    let mut segments = path.split('/').peekable();
58
59    while let Some(segment) = segments.next() {
60        let decoded = segment.replace("~1", "/").replace("~0", "~");
61        current = resolve_schema_value_ref(current, root);
62
63        let Some(schema) = current.as_schema() else {
64            return Err(format!(
65                "cannot resolve segment '{decoded}' in pointer '{pointer}'"
66            ));
67        };
68
69        // Map-bearing keywords: consume this segment AND the next one.
70        if is_map_keyword(&decoded) {
71            let key_segment = segments
72                .next()
73                .ok_or_else(|| format!("expected key after '{decoded}' in pointer '{pointer}'"))?;
74            let key = key_segment.replace("~1", "/").replace("~0", "~");
75            if let Some(entry) = schema.get_map_entry(&decoded, &key) {
76                current = entry;
77                continue;
78            }
79            return Err(format!(
80                "cannot resolve segment '{key}' in '{decoded}' in pointer '{pointer}'"
81            ));
82        }
83
84        // Array-bearing keywords: consume this segment, then the next as an index.
85        if is_array_keyword(&decoded) {
86            let idx_segment = segments.next().ok_or_else(|| {
87                format!("expected index after '{decoded}' in pointer '{pointer}'")
88            })?;
89            let idx: usize = idx_segment.parse().map_err(|_| {
90                format!("expected numeric index after '{decoded}', got '{idx_segment}'")
91            })?;
92            if let Some(entry) = schema.get_array_entry(&decoded, idx) {
93                current = entry;
94                continue;
95            }
96            return Err(format!(
97                "index {idx} out of bounds in '{decoded}' in pointer '{pointer}'"
98            ));
99        }
100
101        // Single-value keywords (items, not, if, then, else, etc.)
102        if let Some(sv) = schema.get_keyword(&decoded) {
103            current = sv;
104            continue;
105        }
106
107        // Fall back: try as a key in the schema's maps (for when the
108        // pointer navigates directly into a map without naming the keyword).
109        if let Some(sv) = schema.get_map_entry_by_pointer_segment(&decoded) {
110            current = sv;
111            continue;
112        }
113
114        // Try as array index (for arrays embedded in composition keywords)
115        if let Ok(idx) = decoded.parse::<usize>() {
116            let found = ["allOf", "anyOf", "oneOf", "prefixItems"]
117                .iter()
118                .find_map(|kw| schema.get_array_entry(kw, idx));
119            if let Some(entry) = found {
120                current = entry;
121                continue;
122            }
123        }
124
125        return Err(format!(
126            "cannot resolve segment '{decoded}' in pointer '{pointer}'"
127        ));
128    }
129
130    Ok(resolve_schema_value_ref(current, root))
131}
132
133/// Whether a JSON pointer segment names a map-bearing keyword.
134fn is_map_keyword(segment: &str) -> bool {
135    matches!(
136        segment,
137        "properties" | "patternProperties" | "$defs" | "dependentSchemas"
138    )
139}
140
141/// Whether a JSON pointer segment names an array-bearing keyword.
142fn is_array_keyword(segment: &str) -> bool {
143    matches!(segment, "allOf" | "anyOf" | "oneOf" | "prefixItems")
144}
145
146/// Resolve `$ref` on a `SchemaValue`, returning the referenced `SchemaValue`.
147fn resolve_schema_value_ref<'a>(sv: &'a SchemaValue, root: &'a SchemaValue) -> &'a SchemaValue {
148    let Some(schema) = sv.as_schema() else {
149        return sv;
150    };
151    if let Some(ref ref_str) = schema.ref_
152        && let Some(path) = ref_str.strip_prefix("#/")
153    {
154        let mut current = root;
155        let mut segments = path.split('/').peekable();
156        while let Some(segment) = segments.next() {
157            let decoded = segment.replace("~1", "/").replace("~0", "~");
158            let Some(inner) = current.as_schema() else {
159                return sv;
160            };
161
162            // Map-bearing keywords: consume the next segment as a key
163            if is_map_keyword(&decoded) {
164                let Some(key_segment) = segments.next() else {
165                    return sv;
166                };
167                let key = key_segment.replace("~1", "/").replace("~0", "~");
168                match inner.get_map_entry(&decoded, &key) {
169                    Some(n) => current = n,
170                    None => return sv,
171                }
172                continue;
173            }
174
175            // Array-bearing keywords: consume the next segment as an index
176            if is_array_keyword(&decoded) {
177                let Some(idx_segment) = segments.next() else {
178                    return sv;
179                };
180                let Ok(idx) = idx_segment.parse::<usize>() else {
181                    return sv;
182                };
183                match inner.get_array_entry(&decoded, idx) {
184                    Some(n) => current = n,
185                    None => return sv,
186                }
187                continue;
188            }
189
190            // Single-value keywords
191            if let Some(n) = inner.get_keyword(&decoded) {
192                current = n;
193                continue;
194            }
195
196            // Fall back to map entry lookup
197            if let Some(n) = inner.get_map_entry_by_pointer_segment(&decoded) {
198                current = n;
199                continue;
200            }
201
202            return sv;
203        }
204        return current;
205    }
206    sv
207}