Skip to main content

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}