rust_config_tree/config_templates/render.rs
1//! Format-specific template rendering and include block injection.
2
3use std::path::{Path, PathBuf};
4
5use super::yaml::render_yaml_template;
6use crate::{
7 config::{ConfigResult, ConfigSchema},
8 config_format::{ConfigFormat, json5_options, toml_options, yaml_options},
9};
10
11/// Renders the default template for one path.
12///
13/// The template format is inferred from the path extension.
14///
15/// # Type Parameters
16///
17/// - `S`: Config schema type used to render the template.
18///
19/// # Arguments
20///
21/// - `path`: Output path whose extension selects the template format.
22///
23/// # Returns
24///
25/// Returns the generated template content.
26///
27/// # Examples
28///
29/// ```
30/// use confique::Config;
31/// use rust_config_tree::{ConfigSchema, template_for_path};
32///
33/// #[derive(Config)]
34/// struct AppConfig {
35/// #[config(default = [])]
36/// include: Vec<std::path::PathBuf>,
37/// #[config(default = "demo")]
38/// mode: String,
39/// }
40///
41/// impl ConfigSchema for AppConfig {
42/// fn include_paths(layer: &<Self as Config>::Layer) -> Vec<std::path::PathBuf> {
43/// layer.include.clone().unwrap_or_default()
44/// }
45/// }
46///
47/// let template = template_for_path::<AppConfig>("config.yaml")?;
48///
49/// assert!(template.contains("mode"));
50/// # Ok::<(), rust_config_tree::ConfigError>(())
51/// ```
52pub fn template_for_path<S>(path: impl AsRef<Path>) -> ConfigResult<String>
53where
54 S: ConfigSchema,
55{
56 let template = match ConfigFormat::from_path(path.as_ref()) {
57 ConfigFormat::Yaml => confique::yaml::template::<S>(yaml_options()),
58 ConfigFormat::Toml => confique::toml::template::<S>(toml_options()),
59 ConfigFormat::Json => confique::json5::template::<S>(json5_options()),
60 };
61
62 Ok(template)
63}
64
65/// Renders the template content for one collected template target.
66///
67/// # Type Parameters
68///
69/// - `S`: Config schema type used to render fields.
70///
71/// # Arguments
72///
73/// - `path`: Target template path whose extension selects the renderer.
74/// - `include_paths`: Include paths to place in the generated template.
75/// - `section_path`: Section path represented by this target.
76/// - `split_paths`: Section paths split out of the root template.
77/// - `env_only_paths`: Leaf field paths omitted from generated config files.
78///
79/// # Returns
80///
81/// Returns rendered template content for the target.
82///
83/// # Examples
84///
85/// ```no_run
86/// let _ = ();
87/// ```
88pub(super) fn template_for_target<S>(
89 path: &Path,
90 include_paths: &[PathBuf],
91 section_path: &[&'static str],
92 split_paths: &[Vec<&'static str>],
93 env_only_paths: &[Vec<&'static str>],
94) -> ConfigResult<String>
95where
96 S: ConfigSchema,
97{
98 if ConfigFormat::from_path(path) != ConfigFormat::Yaml
99 || (split_paths.is_empty() && env_only_paths.is_empty())
100 {
101 return template_for_path_with_includes::<S>(path, include_paths);
102 }
103
104 Ok(render_yaml_template(
105 &S::META,
106 include_paths,
107 section_path,
108 split_paths,
109 env_only_paths,
110 ))
111}
112
113/// Renders a format-specific template and injects an explicit include block.
114///
115/// # Type Parameters
116///
117/// - `S`: Config schema type used to render fields.
118///
119/// # Arguments
120///
121/// - `path`: Template path whose extension selects the renderer.
122/// - `include_paths`: Include paths to inject.
123///
124/// # Returns
125///
126/// Returns rendered template content with include paths inserted when present.
127///
128/// # Examples
129///
130/// ```no_run
131/// let _ = ();
132/// ```
133fn template_for_path_with_includes<S>(
134 path: &Path,
135 include_paths: &[PathBuf],
136) -> ConfigResult<String>
137where
138 S: ConfigSchema,
139{
140 let template = template_for_path::<S>(path)?;
141 if include_paths.is_empty() {
142 return Ok(template);
143 }
144
145 let template = match ConfigFormat::from_path(path) {
146 ConfigFormat::Yaml => {
147 let template = strip_prefix_once(&template, "# Default value: []\n#include: []\n\n");
148 format!("{}\n{template}", render_yaml_include(include_paths))
149 }
150 ConfigFormat::Toml => {
151 let template = strip_prefix_once(&template, "# Default value: []\n#include = []\n\n");
152 format!("{}\n{template}", render_toml_include(include_paths))
153 }
154 ConfigFormat::Json => {
155 let body = template.strip_prefix("{\n").unwrap_or(&template);
156 let body = strip_prefix_once(body, " // Default value: []\n //include: [],\n\n");
157 format!("{{\n{}\n{body}", render_json5_include(include_paths))
158 }
159 };
160
161 Ok(template)
162}
163
164/// Renders a YAML top-level include list.
165///
166/// # Arguments
167///
168/// - `paths`: Include paths to render.
169///
170/// # Returns
171///
172/// Returns a YAML `include` block.
173///
174/// # Examples
175///
176/// ```no_run
177/// let _ = ();
178/// ```
179pub(super) fn render_yaml_include(paths: &[PathBuf]) -> String {
180 let mut out = String::from("include:\n");
181 for path in paths {
182 out.push_str(" - ");
183 out.push_str("e_path(path));
184 out.push('\n');
185 }
186 out
187}
188
189/// Renders a TOML top-level include list.
190///
191/// # Arguments
192///
193/// - `paths`: Include paths to render.
194///
195/// # Returns
196///
197/// Returns a TOML `include` assignment.
198///
199/// # Examples
200///
201/// ```no_run
202/// let _ = ();
203/// ```
204fn render_toml_include(paths: &[PathBuf]) -> String {
205 let entries = paths
206 .iter()
207 .map(|path| quote_path(path))
208 .collect::<Vec<_>>()
209 .join(", ");
210 format!("include = [{entries}]\n")
211}
212
213/// Renders a JSON5 top-level include list.
214///
215/// # Arguments
216///
217/// - `paths`: Include paths to render.
218///
219/// # Returns
220///
221/// Returns a JSON5 `include` property block.
222///
223/// # Examples
224///
225/// ```no_run
226/// let _ = ();
227/// ```
228fn render_json5_include(paths: &[PathBuf]) -> String {
229 let mut out = String::from(" include: [\n");
230 for path in paths {
231 out.push_str(" ");
232 out.push_str("e_path(path));
233 out.push_str(",\n");
234 }
235 out.push_str(" ],\n");
236 out
237}
238
239/// Quotes a path using JSON string escaping, which is valid for all outputs.
240///
241/// # Arguments
242///
243/// - `path`: Path to render as a quoted string.
244///
245/// # Returns
246///
247/// Returns a JSON-escaped string representation of `path`.
248///
249/// # Examples
250///
251/// ```no_run
252/// let _ = ();
253/// ```
254pub(super) fn quote_path(path: &Path) -> String {
255 serde_json::to_string(&path.to_string_lossy()).expect("path string serialization cannot fail")
256}
257
258/// Removes one generated default include block when present.
259///
260/// # Arguments
261///
262/// - `value`: Text that may start with `prefix`.
263/// - `prefix`: Prefix to remove at most once.
264///
265/// # Returns
266///
267/// Returns `value` without `prefix` when it was present.
268///
269/// # Examples
270///
271/// ```no_run
272/// let _ = ();
273/// ```
274fn strip_prefix_once<'a>(value: &'a str, prefix: &str) -> &'a str {
275 value.strip_prefix(prefix).unwrap_or(value)
276}