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 ¤t_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 ¤t_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}