rust_config_tree/config_templates/render.rs
1//! Format-specific template rendering and include block injection.
2
3use std::path::{Path, PathBuf};
4
5use confique::meta::Meta;
6use schemars::JsonSchema;
7
8use super::{
9 json5::render_json5_template,
10 toml::render_toml_template,
11 yaml::render_yaml_template,
12};
13use crate::{
14 config::{ConfigResult, ConfigSchema},
15 config_format::ConfigFormat,
16 config_schema::{env_only_field_paths, root_config_schema},
17};
18
19/// Renders the default template for one path.
20///
21/// The template format is inferred from the path extension.
22///
23/// # Type Parameters
24///
25/// - `S`: Config schema type used to render the template.
26///
27/// # Arguments
28///
29/// - `path`: Output path whose extension selects the template format.
30///
31/// # Returns
32///
33/// Returns the generated template content.
34///
35/// # Examples
36///
37/// ```
38/// use confique::Config;
39/// use rust_config_tree::{ConfigSchema, template_for_path};
40/// use schemars::JsonSchema;
41///
42/// #[derive(Config, JsonSchema)]
43/// struct AppConfig {
44/// #[config(default = [])]
45/// include: Vec<std::path::PathBuf>,
46/// #[config(default = "demo")]
47/// mode: String,
48/// }
49///
50/// impl ConfigSchema for AppConfig {
51/// fn include_paths(layer: &<Self as Config>::Layer) -> Vec<std::path::PathBuf> {
52/// layer.include.clone().unwrap_or_default()
53/// }
54/// }
55///
56/// let template = template_for_path::<AppConfig>("config.yaml")?;
57///
58/// assert!(template.contains("mode"));
59/// # Ok::<(), rust_config_tree::ConfigError>(())
60/// ```
61pub fn template_for_path<S>(path: impl AsRef<Path>) -> ConfigResult<String>
62where
63 S: ConfigSchema + JsonSchema,
64{
65 let full_schema = root_config_schema::<S>()?;
66 let env_only_paths = env_only_field_paths::<S>(&full_schema);
67
68 Ok(render_template(
69 ConfigFormat::from_path(path.as_ref()),
70 &S::META,
71 &[],
72 &[],
73 &[],
74 &env_only_paths,
75 ))
76}
77
78/// Renders the template content for one collected template target.
79///
80/// # Type Parameters
81///
82/// - `S`: Config schema type used to render fields.
83///
84/// # Arguments
85///
86/// - `path`: Target template path whose extension selects the renderer.
87/// - `include_paths`: Include paths to place in the generated template.
88/// - `section_path`: Section path represented by this target.
89/// - `split_paths`: Section paths split out of the root template.
90/// - `env_only_paths`: Leaf field paths omitted from generated config files.
91///
92/// # Returns
93///
94/// Returns rendered template content for the target.
95///
96/// # Examples
97///
98/// ```no_run
99/// let _ = ();
100/// ```
101pub(super) fn template_for_target<S>(
102 path: &Path,
103 include_paths: &[PathBuf],
104 section_path: &[&'static str],
105 split_paths: &[Vec<&'static str>],
106 env_only_paths: &[Vec<&'static str>],
107) -> ConfigResult<String>
108where
109 S: ConfigSchema,
110{
111 Ok(render_template(
112 ConfigFormat::from_path(path),
113 &S::META,
114 include_paths,
115 section_path,
116 split_paths,
117 env_only_paths,
118 ))
119}
120
121fn render_template(
122 format: ConfigFormat,
123 meta: &'static Meta,
124 include_paths: &[PathBuf],
125 section_path: &[&'static str],
126 split_paths: &[Vec<&'static str>],
127 env_only_paths: &[Vec<&'static str>],
128) -> String {
129 match format {
130 ConfigFormat::Yaml => render_yaml_template(
131 meta,
132 include_paths,
133 section_path,
134 split_paths,
135 env_only_paths,
136 ),
137 ConfigFormat::Toml => render_toml_template(
138 meta,
139 include_paths,
140 section_path,
141 split_paths,
142 env_only_paths,
143 ),
144 ConfigFormat::Json => render_json5_template(
145 meta,
146 include_paths,
147 section_path,
148 split_paths,
149 env_only_paths,
150 ),
151 }
152}
153
154/// Renders a YAML top-level include list.
155///
156/// # Arguments
157///
158/// - `paths`: Include paths to render.
159///
160/// # Returns
161///
162/// Returns a YAML `include` block.
163///
164/// # Examples
165///
166/// ```no_run
167/// let _ = ();
168/// ```
169pub(super) fn render_yaml_include(paths: &[PathBuf]) -> String {
170 let mut out = String::from("include:\n");
171 for path in paths {
172 out.push_str(" - ");
173 out.push_str("e_path(path));
174 out.push('\n');
175 }
176 out
177}
178
179/// Renders a TOML top-level include list.
180pub(super) fn render_toml_include(paths: &[PathBuf]) -> String {
181 let entries = paths
182 .iter()
183 .map(|path| quote_path(path))
184 .collect::<Vec<_>>()
185 .join(", ");
186 format!("include = [{entries}]\n")
187}
188
189/// Renders a JSON5 top-level include list.
190pub(super) fn render_json5_include(paths: &[PathBuf]) -> String {
191 let mut out = String::from(" include: [\n");
192 for path in paths {
193 out.push_str(" ");
194 out.push_str("e_path(path));
195 out.push_str(",\n");
196 }
197 out.push_str(" ],\n");
198 out
199}
200
201/// Quotes a path using JSON string escaping, which is valid for all outputs.
202///
203/// # Arguments
204///
205/// - `path`: Path to render as a quoted string.
206///
207/// # Returns
208///
209/// Returns a JSON-escaped string representation of `path`.
210///
211/// # Examples
212///
213/// ```no_run
214/// let _ = ();
215/// ```
216pub(super) fn quote_path(path: &Path) -> String {
217 serde_json::to_string(&path.to_string_lossy()).expect("path string serialization cannot fail")
218}