Skip to main content

copperlace/
config.rs

1use std::fmt;
2use std::path::Path;
3use std::str::FromStr;
4
5use crate::render::config_loader;
6use crate::render::{
7    CopperlaceValue, ProcessorRegistry, RenderContext, RenderError, RenderOptions, RuleSet,
8};
9
10/// Error returned while loading, parsing, compiling, or rendering configuration.
11#[derive(Debug, PartialEq, Eq)]
12pub enum ConfigError {
13    /// The configuration document could not be loaded or parsed.
14    Parse(String),
15    /// The config parsed successfully, but compilation or rendering failed.
16    Render(RenderError),
17}
18
19impl fmt::Display for ConfigError {
20    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            ConfigError::Parse(error) => write!(formatter, "failed to parse config: {error}"),
23            ConfigError::Render(error) => write!(formatter, "{error}"),
24        }
25    }
26}
27
28impl std::error::Error for ConfigError {}
29
30impl From<RenderError> for ConfigError {
31    fn from(error: RenderError) -> Self {
32        ConfigError::Render(error)
33    }
34}
35
36/// Load-once renderer for repeated renders from one configuration.
37///
38/// `Copperlace` wraps a compiled [`RuleSet`]. Use it when rendering more than
39/// one rule, or rendering the same rule multiple times, so the config is not
40/// parsed and compiled for every render.
41pub struct Copperlace {
42    ruleset: RuleSet,
43}
44
45impl Copperlace {
46    /// Compiles a configuration string into a reusable renderer.
47    ///
48    /// Returns [`ConfigError::Parse`] when the string is not valid configuration, and
49    /// [`ConfigError::Render`] when the parsed config is not a valid Copperlace
50    /// rule set.
51    #[allow(clippy::should_implement_trait)]
52    pub fn from_str(config: &str) -> Result<Self, ConfigError> {
53        Ok(Self {
54            ruleset: ruleset_from_str(config)?,
55        })
56    }
57
58    /// Loads and compiles a configuration file into a reusable renderer.
59    ///
60    /// Returns [`ConfigError::Parse`] when the file cannot be loaded as configuration,
61    /// and [`ConfigError::Render`] when the parsed config is not a valid
62    /// Copperlace rule set.
63    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
64        Ok(Self {
65            ruleset: ruleset_from_file(path)?,
66        })
67    }
68
69    /// Renders a named rule from the compiled config.
70    ///
71    /// Each call starts with a fresh render context. Bindings are consistent
72    /// within one output but do not carry over to later renders.
73    pub fn render(&self, rule_name: &str) -> Result<String, RenderError> {
74        self.ruleset.render_rule(rule_name)
75    }
76
77    /// Renders a named rule from the compiled config with render options.
78    pub fn render_with_options(
79        &self,
80        rule_name: &str,
81        options: RenderOptions,
82    ) -> Result<String, RenderError> {
83        self.ruleset.render_rule_with_options(rule_name, options)
84    }
85
86    /// Renders a named rule from the compiled config with initial context.
87    ///
88    /// Initial context values are scoped to this render call. They resolve
89    /// before config-defined `context` defaults and named rules.
90    pub fn render_with_context(
91        &self,
92        rule_name: &str,
93        context: RenderContext,
94    ) -> Result<String, RenderError> {
95        self.ruleset.render_rule_with_context(rule_name, context)
96    }
97
98    /// Renders a named rule from the compiled config with initial context and render options.
99    pub fn render_with_context_and_options(
100        &self,
101        rule_name: &str,
102        context: RenderContext,
103        options: RenderOptions,
104    ) -> Result<String, RenderError> {
105        self.ruleset
106            .render_rule_with_context_and_options(rule_name, context, options)
107    }
108
109    /// Renders a rule as text, inferring formatted structured JSON for object-valued rules.
110    pub fn render_inferred(&self, rule_name: &str) -> Result<String, RenderError> {
111        self.ruleset.render_rule_inferred(rule_name)
112    }
113
114    /// Renders a rule with render options, inferring formatted structured JSON for object-valued rules.
115    pub fn render_inferred_with_options(
116        &self,
117        rule_name: &str,
118        options: RenderOptions,
119    ) -> Result<String, RenderError> {
120        self.ruleset
121            .render_rule_inferred_with_options(rule_name, options)
122    }
123
124    /// Renders a rule with initial context, inferring formatted structured JSON for object-valued rules.
125    pub fn render_inferred_with_context(
126        &self,
127        rule_name: &str,
128        context: RenderContext,
129    ) -> Result<String, RenderError> {
130        self.ruleset
131            .render_rule_inferred_with_context(rule_name, context)
132    }
133
134    /// Renders a rule with initial context and render options, inferring formatted structured JSON for object-valued rules.
135    pub fn render_inferred_with_context_and_options(
136        &self,
137        rule_name: &str,
138        context: RenderContext,
139        options: RenderOptions,
140    ) -> Result<String, RenderError> {
141        self.ruleset
142            .render_rule_inferred_with_context_and_options(rule_name, context, options)
143    }
144
145    /// Renders an object-valued rule from the compiled config as a structured value.
146    pub fn render_structured(&self, rule_name: &str) -> Result<CopperlaceValue, RenderError> {
147        self.ruleset.render_rule_structured(rule_name)
148    }
149
150    /// Renders an object-valued rule from the compiled config as a structured value with render options.
151    pub fn render_structured_with_options(
152        &self,
153        rule_name: &str,
154        options: RenderOptions,
155    ) -> Result<CopperlaceValue, RenderError> {
156        self.ruleset
157            .render_rule_structured_with_options(rule_name, options)
158    }
159
160    /// Renders an object-valued rule from the compiled config as a structured value with initial context.
161    pub fn render_structured_with_context(
162        &self,
163        rule_name: &str,
164        context: RenderContext,
165    ) -> Result<CopperlaceValue, RenderError> {
166        self.ruleset
167            .render_rule_structured_with_context(rule_name, context)
168    }
169
170    /// Renders an object-valued rule from the compiled config as a structured value with initial context and render options.
171    pub fn render_structured_with_context_and_options(
172        &self,
173        rule_name: &str,
174        context: RenderContext,
175        options: RenderOptions,
176    ) -> Result<CopperlaceValue, RenderError> {
177        self.ruleset
178            .render_rule_structured_with_context_and_options(rule_name, context, options)
179    }
180}
181
182impl FromStr for Copperlace {
183    type Err = ConfigError;
184
185    fn from_str(config: &str) -> Result<Self, Self::Err> {
186        Ok(Self {
187            ruleset: ruleset_from_str(config)?,
188        })
189    }
190}
191
192/// Parses a configuration string and compiles it into a reusable [`RuleSet`].
193pub fn ruleset_from_str(config: &str) -> Result<RuleSet, ConfigError> {
194    ruleset_from_str_with_processors(config, ProcessorRegistry::new())
195}
196
197pub(crate) fn ruleset_from_str_with_processors(
198    config: &str,
199    processors: ProcessorRegistry,
200) -> Result<RuleSet, ConfigError> {
201    let parsed = config_loader::load_str(config, None).map_err(ConfigError::Parse)?;
202    RuleSet::from_template_config(parsed, processors).map_err(ConfigError::Render)
203}
204
205fn config_options_for_file(path: &Path) -> hocon_rs::ConfigOptions {
206    let mut locations = Vec::new();
207    if let Some(parent) = path.parent() {
208        locations.push(parent.to_string_lossy().to_string());
209    }
210    if let Ok(current_dir) = std::env::current_dir() {
211        let current_dir = current_dir.to_string_lossy().to_string();
212        if !locations.iter().any(|location| location == &current_dir) {
213            locations.push(current_dir);
214        }
215    }
216    hocon_rs::ConfigOptions::new(false, locations)
217}
218
219/// Loads a configuration file and compiles it into a reusable [`RuleSet`].
220pub fn ruleset_from_file(path: impl AsRef<Path>) -> Result<RuleSet, ConfigError> {
221    ruleset_from_file_with_processors(path, ProcessorRegistry::new())
222}
223
224pub(crate) fn ruleset_from_file_with_processors(
225    path: impl AsRef<Path>,
226    processors: ProcessorRegistry,
227) -> Result<RuleSet, ConfigError> {
228    let path = path.as_ref();
229    let parsed = config_loader::load_file(path, config_options_for_file(path))
230        .map_err(ConfigError::Parse)?;
231    RuleSet::from_template_config(parsed, processors).map_err(ConfigError::Render)
232}
233
234/// Renders one rule from a configuration string.
235///
236/// This convenience helper parses and compiles the config, renders one rule,
237/// and drops the compiled ruleset. Use [`Copperlace::from_str`] or
238/// [`ruleset_from_str`] for repeated renders.
239pub fn render_str(config: &str, rule_name: &str) -> Result<String, ConfigError> {
240    render_str_with_context(config, rule_name, RenderContext::new())
241}
242
243/// Renders one rule from a configuration string with initial context.
244pub fn render_str_with_context(
245    config: &str,
246    rule_name: &str,
247    context: RenderContext,
248) -> Result<String, ConfigError> {
249    render_str_with_context_and_options(config, rule_name, context, RenderOptions::default())
250}
251
252/// Renders one rule from a configuration string with initial context and render options.
253pub fn render_str_with_context_and_options(
254    config: &str,
255    rule_name: &str,
256    context: RenderContext,
257    options: RenderOptions,
258) -> Result<String, ConfigError> {
259    ruleset_from_str(config)?
260        .render_rule_with_context_and_options(rule_name, context, options)
261        .map_err(ConfigError::Render)
262}
263
264/// Renders one rule from a configuration string, inferring formatted structured JSON for object-valued rules.
265pub fn render_str_inferred(config: &str, rule_name: &str) -> Result<String, ConfigError> {
266    render_str_inferred_with_context(config, rule_name, RenderContext::new())
267}
268
269/// Renders one rule from a configuration string with initial context, inferring formatted structured JSON for object-valued rules.
270pub fn render_str_inferred_with_context(
271    config: &str,
272    rule_name: &str,
273    context: RenderContext,
274) -> Result<String, ConfigError> {
275    render_str_inferred_with_context_and_options(
276        config,
277        rule_name,
278        context,
279        RenderOptions::default(),
280    )
281}
282
283/// Renders one rule from a configuration string with initial context and render options, inferring formatted structured JSON for object-valued rules.
284pub fn render_str_inferred_with_context_and_options(
285    config: &str,
286    rule_name: &str,
287    context: RenderContext,
288    options: RenderOptions,
289) -> Result<String, ConfigError> {
290    ruleset_from_str(config)?
291        .render_rule_inferred_with_context_and_options(rule_name, context, options)
292        .map_err(ConfigError::Render)
293}
294
295/// Renders one object-valued rule from a configuration string as a structured value.
296pub fn render_str_structured(
297    config: &str,
298    rule_name: &str,
299) -> Result<CopperlaceValue, ConfigError> {
300    render_str_structured_with_context(config, rule_name, RenderContext::new())
301}
302
303/// Renders one object-valued rule from a configuration string as a structured value with initial context.
304pub fn render_str_structured_with_context(
305    config: &str,
306    rule_name: &str,
307    context: RenderContext,
308) -> Result<CopperlaceValue, ConfigError> {
309    render_str_structured_with_context_and_options(
310        config,
311        rule_name,
312        context,
313        RenderOptions::default(),
314    )
315}
316
317/// Renders one object-valued rule from a configuration string as a structured value with initial context and render options.
318pub fn render_str_structured_with_context_and_options(
319    config: &str,
320    rule_name: &str,
321    context: RenderContext,
322    options: RenderOptions,
323) -> Result<CopperlaceValue, ConfigError> {
324    ruleset_from_str(config)?
325        .render_rule_structured_with_context_and_options(rule_name, context, options)
326        .map_err(ConfigError::Render)
327}
328
329/// Renders one rule from a configuration file.
330///
331/// This convenience helper loads and compiles the file, renders one rule, and
332/// drops the compiled ruleset. Use [`Copperlace::from_file`] or
333/// [`ruleset_from_file`] for repeated renders.
334pub fn render_file(path: impl AsRef<Path>, rule_name: &str) -> Result<String, ConfigError> {
335    render_file_with_context(path, rule_name, RenderContext::new())
336}
337
338/// Renders one rule from a configuration file with initial context.
339pub fn render_file_with_context(
340    path: impl AsRef<Path>,
341    rule_name: &str,
342    context: RenderContext,
343) -> Result<String, ConfigError> {
344    render_file_with_context_and_options(path, rule_name, context, RenderOptions::default())
345}
346
347/// Renders one rule from a configuration file with initial context and render options.
348pub fn render_file_with_context_and_options(
349    path: impl AsRef<Path>,
350    rule_name: &str,
351    context: RenderContext,
352    options: RenderOptions,
353) -> Result<String, ConfigError> {
354    ruleset_from_file(path)?
355        .render_rule_with_context_and_options(rule_name, context, options)
356        .map_err(ConfigError::Render)
357}
358
359/// Renders one rule from a configuration file, inferring formatted structured JSON for object-valued rules.
360pub fn render_file_inferred(
361    path: impl AsRef<Path>,
362    rule_name: &str,
363) -> Result<String, ConfigError> {
364    render_file_inferred_with_context(path, rule_name, RenderContext::new())
365}
366
367/// Renders one rule from a configuration file with initial context, inferring formatted structured JSON for object-valued rules.
368pub fn render_file_inferred_with_context(
369    path: impl AsRef<Path>,
370    rule_name: &str,
371    context: RenderContext,
372) -> Result<String, ConfigError> {
373    render_file_inferred_with_context_and_options(
374        path,
375        rule_name,
376        context,
377        RenderOptions::default(),
378    )
379}
380
381/// Renders one rule from a configuration file with initial context and render options, inferring formatted structured JSON for object-valued rules.
382pub fn render_file_inferred_with_context_and_options(
383    path: impl AsRef<Path>,
384    rule_name: &str,
385    context: RenderContext,
386    options: RenderOptions,
387) -> Result<String, ConfigError> {
388    ruleset_from_file(path)?
389        .render_rule_inferred_with_context_and_options(rule_name, context, options)
390        .map_err(ConfigError::Render)
391}
392
393/// Renders one object-valued rule from a configuration file as a structured value.
394pub fn render_file_structured(
395    path: impl AsRef<Path>,
396    rule_name: &str,
397) -> Result<CopperlaceValue, ConfigError> {
398    render_file_structured_with_context(path, rule_name, RenderContext::new())
399}
400
401/// Renders one object-valued rule from a configuration file as a structured value with initial context.
402pub fn render_file_structured_with_context(
403    path: impl AsRef<Path>,
404    rule_name: &str,
405    context: RenderContext,
406) -> Result<CopperlaceValue, ConfigError> {
407    render_file_structured_with_context_and_options(
408        path,
409        rule_name,
410        context,
411        RenderOptions::default(),
412    )
413}
414
415/// Renders one object-valued rule from a configuration file as a structured value with initial context and render options.
416pub fn render_file_structured_with_context_and_options(
417    path: impl AsRef<Path>,
418    rule_name: &str,
419    context: RenderContext,
420    options: RenderOptions,
421) -> Result<CopperlaceValue, ConfigError> {
422    ruleset_from_file(path)?
423        .render_rule_structured_with_context_and_options(rule_name, context, options)
424        .map_err(ConfigError::Render)
425}