Skip to main content

copperlace/
config.rs

1use std::fmt;
2use std::path::Path;
3use std::str::FromStr;
4
5use crate::render::{CopperlaceValue, RenderContext, RenderError, RuleSet};
6
7/// Error returned while loading, parsing, compiling, or rendering configuration.
8#[derive(Debug, PartialEq, Eq)]
9pub enum ConfigError {
10    /// The configuration document could not be loaded or parsed.
11    Parse(String),
12    /// The config parsed successfully, but compilation or rendering failed.
13    Render(RenderError),
14}
15
16impl fmt::Display for ConfigError {
17    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            ConfigError::Parse(error) => write!(formatter, "failed to parse config: {error}"),
20            ConfigError::Render(error) => write!(formatter, "{error}"),
21        }
22    }
23}
24
25impl std::error::Error for ConfigError {}
26
27impl From<RenderError> for ConfigError {
28    fn from(error: RenderError) -> Self {
29        ConfigError::Render(error)
30    }
31}
32
33/// Load-once renderer for repeated renders from one configuration.
34///
35/// `Copperlace` wraps a compiled [`RuleSet`]. Use it when rendering more than
36/// one rule, or rendering the same rule multiple times, so the config is not
37/// parsed and compiled for every render.
38pub struct Copperlace {
39    ruleset: RuleSet,
40}
41
42impl Copperlace {
43    /// Compiles a configuration string into a reusable renderer.
44    ///
45    /// Returns [`ConfigError::Parse`] when the string is not valid configuration, and
46    /// [`ConfigError::Render`] when the parsed config is not a valid Copperlace
47    /// rule set.
48    #[allow(clippy::should_implement_trait)]
49    pub fn from_str(config: &str) -> Result<Self, ConfigError> {
50        Ok(Self {
51            ruleset: ruleset_from_str(config)?,
52        })
53    }
54
55    /// Loads and compiles a configuration file into a reusable renderer.
56    ///
57    /// Returns [`ConfigError::Parse`] when the file cannot be loaded as configuration,
58    /// and [`ConfigError::Render`] when the parsed config is not a valid
59    /// Copperlace rule set.
60    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
61        Ok(Self {
62            ruleset: ruleset_from_file(path)?,
63        })
64    }
65
66    /// Renders a named rule from the compiled config.
67    ///
68    /// Each call starts with a fresh render context. Bindings are consistent
69    /// within one output but do not carry over to later renders.
70    pub fn render(&self, rule_name: &str) -> Result<String, RenderError> {
71        self.ruleset.render_rule(rule_name)
72    }
73
74    /// Renders a named rule from the compiled config with initial context.
75    ///
76    /// Initial context values are scoped to this render call. They resolve
77    /// before config-defined `context` defaults and named rules.
78    pub fn render_with_context(
79        &self,
80        rule_name: &str,
81        context: RenderContext,
82    ) -> Result<String, RenderError> {
83        self.ruleset.render_rule_with_context(rule_name, context)
84    }
85
86    /// Renders a rule as text, inferring formatted structured JSON for object-valued rules.
87    pub fn render_inferred(&self, rule_name: &str) -> Result<String, RenderError> {
88        self.ruleset.render_rule_inferred(rule_name)
89    }
90
91    /// Renders a rule with initial context, inferring formatted structured JSON for object-valued rules.
92    pub fn render_inferred_with_context(
93        &self,
94        rule_name: &str,
95        context: RenderContext,
96    ) -> Result<String, RenderError> {
97        self.ruleset
98            .render_rule_inferred_with_context(rule_name, context)
99    }
100
101    /// Renders an object-valued rule from the compiled config as a structured value.
102    pub fn render_structured(&self, rule_name: &str) -> Result<CopperlaceValue, RenderError> {
103        self.ruleset.render_rule_structured(rule_name)
104    }
105
106    /// Renders an object-valued rule from the compiled config as a structured value with initial context.
107    pub fn render_structured_with_context(
108        &self,
109        rule_name: &str,
110        context: RenderContext,
111    ) -> Result<CopperlaceValue, RenderError> {
112        self.ruleset
113            .render_rule_structured_with_context(rule_name, context)
114    }
115}
116
117impl FromStr for Copperlace {
118    type Err = ConfigError;
119
120    fn from_str(config: &str) -> Result<Self, Self::Err> {
121        Ok(Self {
122            ruleset: ruleset_from_str(config)?,
123        })
124    }
125}
126
127/// Parses a configuration string and compiles it into a reusable [`RuleSet`].
128pub fn ruleset_from_str(config: &str) -> Result<RuleSet, ConfigError> {
129    let value = hocon_rs::Config::parse_str::<hocon_rs::Value>(config, None)
130        .map_err(|error| ConfigError::Parse(format!("{error:?}")))?;
131    RuleSet::from_config(value).map_err(ConfigError::Render)
132}
133
134/// Loads a configuration file and compiles it into a reusable [`RuleSet`].
135pub fn ruleset_from_file(path: impl AsRef<Path>) -> Result<RuleSet, ConfigError> {
136    let path = path.as_ref().to_string_lossy();
137    let value = hocon_rs::Config::load(path.as_ref(), None)
138        .map_err(|error| ConfigError::Parse(format!("{error:?}")))?;
139    RuleSet::from_config(value).map_err(ConfigError::Render)
140}
141
142/// Renders one rule from a configuration string.
143///
144/// This convenience helper parses and compiles the config, renders one rule,
145/// and drops the compiled ruleset. Use [`Copperlace::from_str`] or
146/// [`ruleset_from_str`] for repeated renders.
147pub fn render_str(config: &str, rule_name: &str) -> Result<String, ConfigError> {
148    render_str_with_context(config, rule_name, RenderContext::new())
149}
150
151/// Renders one rule from a configuration string with initial context.
152pub fn render_str_with_context(
153    config: &str,
154    rule_name: &str,
155    context: RenderContext,
156) -> Result<String, ConfigError> {
157    ruleset_from_str(config)?
158        .render_rule_with_context(rule_name, context)
159        .map_err(ConfigError::Render)
160}
161
162/// Renders one rule from a configuration string, inferring formatted structured JSON for object-valued rules.
163pub fn render_str_inferred(config: &str, rule_name: &str) -> Result<String, ConfigError> {
164    render_str_inferred_with_context(config, rule_name, RenderContext::new())
165}
166
167/// Renders one rule from a configuration string with initial context, inferring formatted structured JSON for object-valued rules.
168pub fn render_str_inferred_with_context(
169    config: &str,
170    rule_name: &str,
171    context: RenderContext,
172) -> Result<String, ConfigError> {
173    ruleset_from_str(config)?
174        .render_rule_inferred_with_context(rule_name, context)
175        .map_err(ConfigError::Render)
176}
177
178/// Renders one object-valued rule from a configuration string as a structured value.
179pub fn render_str_structured(
180    config: &str,
181    rule_name: &str,
182) -> Result<CopperlaceValue, ConfigError> {
183    render_str_structured_with_context(config, rule_name, RenderContext::new())
184}
185
186/// Renders one object-valued rule from a configuration string as a structured value with initial context.
187pub fn render_str_structured_with_context(
188    config: &str,
189    rule_name: &str,
190    context: RenderContext,
191) -> Result<CopperlaceValue, ConfigError> {
192    ruleset_from_str(config)?
193        .render_rule_structured_with_context(rule_name, context)
194        .map_err(ConfigError::Render)
195}
196
197/// Renders one rule from a configuration file.
198///
199/// This convenience helper loads and compiles the file, renders one rule, and
200/// drops the compiled ruleset. Use [`Copperlace::from_file`] or
201/// [`ruleset_from_file`] for repeated renders.
202pub fn render_file(path: impl AsRef<Path>, rule_name: &str) -> Result<String, ConfigError> {
203    render_file_with_context(path, rule_name, RenderContext::new())
204}
205
206/// Renders one rule from a configuration file with initial context.
207pub fn render_file_with_context(
208    path: impl AsRef<Path>,
209    rule_name: &str,
210    context: RenderContext,
211) -> Result<String, ConfigError> {
212    ruleset_from_file(path)?
213        .render_rule_with_context(rule_name, context)
214        .map_err(ConfigError::Render)
215}
216
217/// Renders one rule from a configuration file, inferring formatted structured JSON for object-valued rules.
218pub fn render_file_inferred(
219    path: impl AsRef<Path>,
220    rule_name: &str,
221) -> Result<String, ConfigError> {
222    render_file_inferred_with_context(path, rule_name, RenderContext::new())
223}
224
225/// Renders one rule from a configuration file with initial context, inferring formatted structured JSON for object-valued rules.
226pub fn render_file_inferred_with_context(
227    path: impl AsRef<Path>,
228    rule_name: &str,
229    context: RenderContext,
230) -> Result<String, ConfigError> {
231    ruleset_from_file(path)?
232        .render_rule_inferred_with_context(rule_name, context)
233        .map_err(ConfigError::Render)
234}
235
236/// Renders one object-valued rule from a configuration file as a structured value.
237pub fn render_file_structured(
238    path: impl AsRef<Path>,
239    rule_name: &str,
240) -> Result<CopperlaceValue, ConfigError> {
241    render_file_structured_with_context(path, rule_name, RenderContext::new())
242}
243
244/// Renders one object-valued rule from a configuration file as a structured value with initial context.
245pub fn render_file_structured_with_context(
246    path: impl AsRef<Path>,
247    rule_name: &str,
248    context: RenderContext,
249) -> Result<CopperlaceValue, ConfigError> {
250    ruleset_from_file(path)?
251        .render_rule_structured_with_context(rule_name, context)
252        .map_err(ConfigError::Render)
253}