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