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