Skip to main content

rust_config_tree/config_schema/
reference.rs

1//! Local `$ref` resolution and schema-map reference collection.
2
3use std::collections::BTreeSet;
4
5use serde_json::Value;
6
7/// Resolves the local schema reference shape emitted by `schemars`.
8///
9/// # Arguments
10///
11/// - `root_schema`: Full root schema that owns referenced definitions.
12/// - `schema`: Schema value that may contain a local `$ref`.
13///
14/// # Returns
15///
16/// Returns the referenced schema when `schema` contains a supported reference.
17///
18/// # Examples
19///
20/// ```no_run
21/// let _ = ();
22/// ```
23pub fn resolve_schema_reference<'a>(root_schema: &'a Value, schema: &'a Value) -> Option<&'a Value> {
24    if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
25        return resolve_json_pointer_ref(root_schema, reference);
26    }
27
28    schema
29        .get("allOf")
30        .and_then(Value::as_array)
31        .and_then(|schemas| schemas.first())
32        .and_then(|schema| schema.get("$ref"))
33        .and_then(Value::as_str)
34        .and_then(|reference| resolve_json_pointer_ref(root_schema, reference))
35}
36
37/// Resolves a local JSON Pointer `$ref` against the root schema.
38///
39/// # Arguments
40///
41/// - `root_schema`: Full schema to query with the JSON Pointer.
42/// - `reference`: `$ref` string that must start with `#`.
43///
44/// # Returns
45///
46/// Returns the referenced schema value when the pointer resolves.
47///
48/// # Examples
49///
50/// ```no_run
51/// let _ = ();
52/// ```
53fn resolve_json_pointer_ref<'a>(root_schema: &'a Value, reference: &str) -> Option<&'a Value> {
54    let pointer = reference.strip_prefix('#')?;
55    root_schema.pointer(pointer)
56}
57
58/// Expands the reference set with references used by already retained schemas.
59///
60/// # Arguments
61///
62/// - `schema`: Root schema containing schema maps to inspect.
63/// - `definitions`: Referenced `definitions` names to expand in place.
64/// - `defs`: Referenced `$defs` names to expand in place.
65///
66/// # Returns
67///
68/// Returns no value; `definitions` and `defs` are updated directly.
69///
70/// # Examples
71///
72/// ```no_run
73/// let _ = ();
74/// ```
75pub fn collect_transitive_schema_refs(
76    schema: &Value,
77    definitions: &mut BTreeSet<String>,
78    defs: &mut BTreeSet<String>,
79) {
80    let current_definitions = definitions.clone();
81    let current_defs = defs.clone();
82    let mut referenced_definitions = BTreeSet::new();
83    let mut referenced_defs = BTreeSet::new();
84
85    if let Some(schema_map) = schema.get("definitions").and_then(Value::as_object) {
86        for name in &current_definitions {
87            if let Some(schema) = schema_map.get(name) {
88                collect_schema_refs(
89                    schema,
90                    true,
91                    &mut referenced_definitions,
92                    &mut referenced_defs,
93                );
94            }
95        }
96    }
97
98    if let Some(schema_map) = schema.get("$defs").and_then(Value::as_object) {
99        for name in &current_defs {
100            if let Some(schema) = schema_map.get(name) {
101                collect_schema_refs(
102                    schema,
103                    true,
104                    &mut referenced_definitions,
105                    &mut referenced_defs,
106                );
107            }
108        }
109    }
110
111    definitions.extend(referenced_definitions);
112    defs.extend(referenced_defs);
113}
114
115/// Walks a schema value and collects local references to schema maps.
116///
117/// # Arguments
118///
119/// - `value`: Schema subtree to inspect.
120/// - `include_schema_maps`: Whether nested `definitions` and `$defs` maps
121///   should also be traversed.
122/// - `definitions`: Output set of referenced `definitions` names.
123/// - `defs`: Output set of referenced `$defs` names.
124///
125/// # Returns
126///
127/// Returns no value; output sets are updated directly.
128///
129/// # Examples
130///
131/// ```no_run
132/// let _ = ();
133/// ```
134pub fn collect_schema_refs(
135    value: &Value,
136    include_schema_maps: bool,
137    definitions: &mut BTreeSet<String>,
138    defs: &mut BTreeSet<String>,
139) {
140    match value {
141        Value::Object(object) => {
142            if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
143                collect_schema_ref(reference, definitions, defs);
144            }
145
146            for (key, child) in object {
147                if !include_schema_maps && matches!(key.as_str(), "definitions" | "$defs") {
148                    continue;
149                }
150
151                collect_schema_refs(child, include_schema_maps, definitions, defs);
152            }
153        }
154        Value::Array(items) => {
155            for item in items {
156                collect_schema_refs(item, include_schema_maps, definitions, defs);
157            }
158        }
159        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
160    }
161}
162
163/// Records one local `$ref` if it points at `definitions` or `$defs`.
164///
165/// # Arguments
166///
167/// - `reference`: `$ref` string to inspect.
168/// - `definitions`: Output set of referenced `definitions` names.
169/// - `defs`: Output set of referenced `$defs` names.
170///
171/// # Returns
172///
173/// Returns no value; matching references are inserted into the output sets.
174///
175/// # Examples
176///
177/// ```no_run
178/// let _ = ();
179/// ```
180fn collect_schema_ref(
181    reference: &str,
182    definitions: &mut BTreeSet<String>,
183    defs: &mut BTreeSet<String>,
184) {
185    if let Some(name) = schema_ref_name(reference, "#/definitions/") {
186        definitions.insert(name);
187    } else if let Some(name) = schema_ref_name(reference, "#/$defs/") {
188        defs.insert(name);
189    }
190}
191
192/// Extracts and JSON-Pointer-decodes a schema-map entry name from a `$ref`.
193///
194/// # Arguments
195///
196/// - `reference`: `$ref` string to parse.
197/// - `prefix`: Schema-map pointer prefix to strip.
198///
199/// # Returns
200///
201/// Returns the decoded schema-map entry name when the reference matches.
202///
203/// # Examples
204///
205/// ```no_run
206/// let _ = ();
207/// ```
208fn schema_ref_name(reference: &str, prefix: &str) -> Option<String> {
209    let name = reference.strip_prefix(prefix)?.split('/').next()?;
210    Some(decode_json_pointer_token(name))
211}
212
213/// Decodes the escaping used by one JSON Pointer path token.
214///
215/// # Arguments
216///
217/// - `token`: Encoded JSON Pointer path token.
218///
219/// # Returns
220///
221/// Returns `token` with `~1` and `~0` escapes decoded.
222///
223/// # Examples
224///
225/// ```no_run
226/// let _ = ();
227/// ```
228fn decode_json_pointer_token(token: &str) -> String {
229    token.replace("~1", "/").replace("~0", "~")
230}
231
232/// Retains only referenced entries in a root schema map.
233///
234/// # Arguments
235///
236/// - `schema`: Root schema containing the schema map.
237/// - `key`: Schema-map key, such as `definitions` or `$defs`.
238/// - `used_names`: Entry names that should remain in the map.
239///
240/// # Returns
241///
242/// Returns no value; the map is pruned in place.
243///
244/// # Examples
245///
246/// ```no_run
247/// let _ = ();
248/// ```
249pub fn retain_schema_map(schema: &mut Value, key: &str, used_names: &BTreeSet<String>) {
250    let Some(object) = schema.as_object_mut() else {
251        return;
252    };
253
254    let Some(schema_map) = object.get_mut(key).and_then(Value::as_object_mut) else {
255        return;
256    };
257
258    schema_map.retain(|name, _| used_names.contains(name));
259
260    if schema_map.is_empty() {
261        object.remove(key);
262    }
263}