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