rust_config_tree/config_schema/paths.rs
1//! Section and field path discovery from `confique` metadata and schema markers.
2
3use std::path::{Path, PathBuf};
4
5use confique::meta::{FieldKind, Meta};
6use serde_json::Value;
7
8use crate::config::ConfigSchema;
9
10use super::{
11 marker::{
12 DEFAULT_TREE_INNER_FIELD, ENV_ONLY_SCHEMA_EXTENSION, TREE_INNER_FIELD_EXTENSION,
13 TREE_SPLIT_SCHEMA_EXTENSION, TREE_TRANSPARENT_ARRAY_EXTENSION,
14 },
15 reference::resolve_schema_reference,
16};
17
18/// Resolves the output path for a split section schema.
19///
20/// # Arguments
21///
22/// - `root_schema_path`: Output path for the root schema.
23/// - `section_path`: Nested section field path.
24///
25/// # Returns
26///
27/// Returns the generated schema path for `section_path`.
28///
29/// # Examples
30///
31/// ```no_run
32/// let _ = ();
33/// ```
34pub fn schema_path_for_section(root_schema_path: &Path, section_path: &[&str]) -> PathBuf {
35 let Some((last, parents)) = section_path.split_last() else {
36 return root_schema_path.to_path_buf();
37 };
38
39 let mut path = root_schema_path
40 .parent()
41 .unwrap_or_else(|| Path::new("."))
42 .to_path_buf();
43
44 for parent in parents {
45 path.push(*parent);
46 }
47
48 path.push(format!("{}.schema.json", *last));
49 path
50}
51/// Collects every nested `confique` section path from schema metadata.
52///
53/// # Arguments
54///
55/// - `meta`: Root `confique` metadata to traverse.
56///
57/// # Returns
58///
59/// Returns nested section paths in metadata traversal order.
60///
61/// # Examples
62///
63/// ```no_run
64/// let _ = ();
65/// ```
66pub fn nested_section_paths(meta: &'static Meta) -> Vec<Vec<&'static str>> {
67 let mut paths = Vec::new();
68 collect_nested_section_paths(meta, &mut Vec::new(), &mut paths);
69 paths
70}
71
72/// Finds nested sections whose field schema opts into template/schema splitting.
73///
74/// # Type Parameters
75///
76/// - `S`: Config schema type whose metadata supplies nested section paths.
77///
78/// # Arguments
79///
80/// - `full_schema`: Full root schema containing `x-tree-split` markers.
81///
82/// # Returns
83///
84/// Returns nested section paths that should be split.
85///
86/// # Examples
87///
88/// ```no_run
89/// let _ = ();
90/// ```
91pub fn split_section_paths<S>(full_schema: &Value) -> Vec<Vec<&'static str>>
92where
93 S: ConfigSchema,
94{
95 nested_section_paths(&S::META)
96 .into_iter()
97 .filter(|section_path| section_has_tree_split_marker(full_schema, section_path))
98 .collect()
99}
100
101/// Finds nested sections whose field schema opts into transparent array serialization.
102///
103/// # Type Parameters
104///
105/// - `S`: Config schema type whose metadata supplies nested section paths.
106///
107/// # Arguments
108///
109/// - `full_schema`: Full root schema containing `x-tree-transparent-array` markers.
110///
111/// # Returns
112///
113/// Returns nested section paths that serialize as transparent arrays.
114pub fn transparent_array_section_paths<S>(full_schema: &Value) -> Vec<Vec<&'static str>>
115where
116 S: ConfigSchema,
117{
118 nested_section_paths(&S::META)
119 .into_iter()
120 .filter(|section_path| section_has_transparent_array_marker(full_schema, section_path))
121 .collect()
122}
123
124/// Returns the confique inner field name for one transparent array section.
125///
126/// # Arguments
127///
128/// - `full_schema`: Full root schema containing section markers.
129/// - `section_path`: Nested section field path to inspect.
130///
131/// # Returns
132///
133/// Returns the configured inner field name, or `"items"` when unset.
134pub fn inner_field_for_section(full_schema: &Value, section_path: &[&str]) -> String {
135 property_schema_for_path(full_schema, section_path)
136 .and_then(|schema| schema.get(TREE_INNER_FIELD_EXTENSION))
137 .and_then(Value::as_str)
138 .map(str::to_owned)
139 .unwrap_or_else(|| DEFAULT_TREE_INNER_FIELD.to_string())
140}
141
142/// Returns whether one section path uses transparent array serialization.
143///
144/// # Arguments
145///
146/// - `full_schema`: Full root schema to inspect.
147/// - `section_path`: Nested section field path to check.
148///
149/// # Returns
150///
151/// Returns `true` when the section schema carries `x-tree-transparent-array = true`.
152pub fn section_has_transparent_array_marker(root_schema: &Value, section_path: &[&str]) -> bool {
153 property_schema_for_path(root_schema, section_path)
154 .and_then(|schema| schema.get(TREE_TRANSPARENT_ARRAY_EXTENSION))
155 .and_then(Value::as_bool)
156 .unwrap_or(false)
157}
158
159/// Finds leaf fields whose schema opts out of template and schema output.
160///
161/// # Type Parameters
162///
163/// - `S`: Config schema type whose metadata supplies field paths.
164///
165/// # Arguments
166///
167/// - `full_schema`: Full root schema containing `x-env-only` markers.
168///
169/// # Returns
170///
171/// Returns leaf field paths marked with `x-env-only = true`.
172///
173/// # Examples
174///
175/// ```no_run
176/// let _ = ();
177/// ```
178pub fn env_only_field_paths<S>(full_schema: &Value) -> Vec<Vec<&'static str>>
179where
180 S: ConfigSchema,
181{
182 let mut paths = Vec::new();
183 collect_env_only_field_paths(&S::META, full_schema, &mut Vec::new(), &mut paths);
184 paths
185}
186
187/// Checks whether a section property carries the split marker extension.
188///
189/// # Arguments
190///
191/// - `root_schema`: Full root schema to inspect.
192/// - `section_path`: Nested section field path to check.
193///
194/// # Returns
195///
196/// Returns `true` when the section schema carries `x-tree-split = true`.
197///
198/// # Examples
199///
200/// ```no_run
201/// let _ = ();
202/// ```
203fn section_has_tree_split_marker(root_schema: &Value, section_path: &[&str]) -> bool {
204 property_schema_for_path(root_schema, section_path)
205 .and_then(|schema| schema.get(TREE_SPLIT_SCHEMA_EXTENSION))
206 .and_then(Value::as_bool)
207 .unwrap_or(false)
208}
209
210/// Checks whether a field property carries the env-only marker extension.
211///
212/// # Arguments
213///
214/// - `root_schema`: Full root schema to inspect.
215/// - `field_path`: Field path to check.
216///
217/// # Returns
218///
219/// Returns `true` when the field schema carries `x-env-only = true`.
220///
221/// # Examples
222///
223/// ```no_run
224/// let _ = ();
225/// ```
226fn field_has_env_only_marker(root_schema: &Value, field_path: &[&str]) -> bool {
227 property_schema_for_path(root_schema, field_path)
228 .and_then(|schema| schema.get(ENV_ONLY_SCHEMA_EXTENSION))
229 .and_then(Value::as_bool)
230 .unwrap_or(false)
231}
232
233/// Returns the original property schema for a field path.
234///
235/// # Arguments
236///
237/// - `root_schema`: Full root schema to traverse.
238/// - `path`: Field path to locate.
239///
240/// # Returns
241///
242/// Returns the original property schema when the section path exists.
243///
244/// # Examples
245///
246/// ```no_run
247/// let _ = ();
248/// ```
249fn property_schema_for_path<'a>(root_schema: &'a Value, path: &[&str]) -> Option<&'a Value> {
250 let mut current = root_schema;
251
252 for (index, section) in path.iter().enumerate() {
253 let property = current.get("properties")?.get(*section)?;
254 if index + 1 == path.len() {
255 return Some(property);
256 }
257
258 current = resolve_schema_reference(root_schema, property).unwrap_or(property);
259 }
260
261 None
262}
263
264/// Recursively appends nested section paths to `paths`.
265///
266/// # Arguments
267///
268/// - `meta`: Current `confique` metadata node.
269/// - `prefix`: Mutable section path prefix for `meta`.
270/// - `paths`: Output list receiving discovered nested section paths.
271///
272/// # Returns
273///
274/// Returns no value; `paths` and `prefix` are updated during traversal.
275///
276/// # Examples
277///
278/// ```no_run
279/// let _ = ();
280/// ```
281fn collect_nested_section_paths(
282 meta: &'static Meta,
283 prefix: &mut Vec<&'static str>,
284 paths: &mut Vec<Vec<&'static str>>,
285) {
286 for field in meta.fields {
287 if let FieldKind::Nested { meta } = field.kind {
288 prefix.push(field.name);
289 paths.push(prefix.clone());
290 collect_nested_section_paths(meta, prefix, paths);
291 prefix.pop();
292 }
293 }
294}
295
296/// Recursively appends env-only leaf field paths to `paths`.
297///
298/// # Arguments
299///
300/// - `meta`: Current `confique` metadata node.
301/// - `root_schema`: Full root schema containing marker extensions.
302/// - `prefix`: Mutable field path prefix for `meta`.
303/// - `paths`: Output list receiving discovered leaf paths.
304///
305/// # Returns
306///
307/// Returns no value; `paths` and `prefix` are updated during traversal.
308///
309/// # Examples
310///
311/// ```no_run
312/// let _ = ();
313/// ```
314fn collect_env_only_field_paths(
315 meta: &'static Meta,
316 root_schema: &Value,
317 prefix: &mut Vec<&'static str>,
318 paths: &mut Vec<Vec<&'static str>>,
319) {
320 for field in meta.fields {
321 prefix.push(field.name);
322
323 match field.kind {
324 FieldKind::Leaf { .. } => {
325 if field_has_env_only_marker(root_schema, prefix) {
326 paths.push(prefix.clone());
327 }
328 }
329 FieldKind::Nested { meta } => {
330 collect_env_only_field_paths(meta, root_schema, prefix, paths);
331 }
332 }
333
334 prefix.pop();
335 }
336}
337
338/// Returns split sections that are direct children of `section_path`.
339///
340/// # Arguments
341///
342/// - `section_path`: Parent section path to match.
343/// - `split_paths`: All split section paths.
344///
345/// # Returns
346///
347/// Returns split paths whose parent is exactly `section_path`.
348///
349/// # Examples
350///
351/// ```no_run
352/// let _ = ();
353/// ```
354pub fn direct_child_split_section_paths(
355 section_path: &[&'static str],
356 split_paths: &[Vec<&'static str>],
357) -> Vec<Vec<&'static str>> {
358 split_paths
359 .iter()
360 .filter(|path| path.len() == section_path.len() + 1 && path.starts_with(section_path))
361 .cloned()
362 .collect()
363}