Skip to main content

copperlace/render/
ruleset.rs

1use std::collections::{BTreeMap, HashMap};
2
3use crate::processors::builtin_processors;
4
5use super::compile::{
6    contains_template_loop, insert_context_text_nodes, insert_named_text_nodes,
7    value_to_structured_template_node,
8};
9use super::error::RenderError;
10use super::nodes::TextGeneratorNode;
11use super::processor::ProcessorRegistry;
12use super::state::{RenderContext, RenderOptions, RenderState};
13use super::structured_template::ParsedTemplateConfig;
14use super::value::{CopperlaceValue, StructuredNode};
15
16/// Compiled collection of named rules from the config.
17///
18/// Top-level config entries become startable rules, so callers can render
19/// `origin`, `story`, `name`, or any other named entry directly. A top-level
20/// `context` object is treated specially: its entries become lazy defaults for
21/// bound variables, so `{hero}` can generate and cache `context.hero` on first
22/// use within a render.
23pub struct RuleSet {
24    document: StructuredNode,
25    text_rules: HashMap<String, Box<dyn TextGeneratorNode>>,
26    context_defaults: HashMap<String, Box<dyn TextGeneratorNode>>,
27    processors: ProcessorRegistry,
28}
29
30impl RuleSet {
31    /// Compiles a parsed configuration root value using the builtin processor registry.
32    ///
33    /// The root value must be a configuration object. Top-level entries become named
34    /// rules, except a top-level object named `context`, whose entries become
35    /// lazy defaults available to template references.
36    pub fn from_config(config: hocon_rs::Value) -> Result<Self, RenderError> {
37        Self::from_config_with_processors(config, ProcessorRegistry::new())
38    }
39
40    /// Compiles a parsed configuration root value with additional custom processors.
41    ///
42    /// Custom processors are merged into the builtin registry before templates
43    /// are compiled, so unknown processor names fail during compilation. A
44    /// custom processor with the same name as a builtin overrides the builtin.
45    pub fn from_config_with_processors(
46        config: hocon_rs::Value,
47        custom_processors: ProcessorRegistry,
48    ) -> Result<Self, RenderError> {
49        Self::from_template_config(
50            ParsedTemplateConfig {
51                value: config,
52                loops: HashMap::new(),
53            },
54            custom_processors,
55        )
56    }
57
58    pub(crate) fn from_template_config(
59        config: ParsedTemplateConfig,
60        custom_processors: ProcessorRegistry,
61    ) -> Result<Self, RenderError> {
62        let ParsedTemplateConfig { value, loops } = config;
63        let hocon_rs::Value::Object(values) = value else {
64            return Err(RenderError::InvalidConfigRoot);
65        };
66
67        let mut processors = builtin_processors();
68        processors.extend(custom_processors);
69
70        let mut document_values = BTreeMap::new();
71        let mut text_rules = HashMap::new();
72        let mut context_defaults = HashMap::new();
73
74        for (name, value) in values {
75            if contains_template_loop(&value, &loops)
76                && (name == "context" || !matches!(&value, hocon_rs::Value::Object(_)))
77            {
78                return Err(RenderError::InvalidExpression(
79                    "structured loop blocks must be inside arrays in object-valued rules"
80                        .to_string(),
81                ));
82            }
83            document_values.insert(
84                name.clone(),
85                value_to_structured_template_node(value.clone(), &processors, &loops)?,
86            );
87            if name == "context" {
88                if let hocon_rs::Value::Object(context_values) = value {
89                    for (context_name, context_value) in context_values {
90                        insert_context_text_nodes(
91                            &mut context_defaults,
92                            context_name,
93                            context_value,
94                            &processors,
95                        )?;
96                    }
97                } else {
98                    insert_named_text_nodes(&mut text_rules, name, value, &processors, &loops)?;
99                }
100            } else {
101                insert_named_text_nodes(&mut text_rules, name, value, &processors, &loops)?;
102            }
103        }
104
105        Ok(RuleSet {
106            document: StructuredNode::Object(document_values),
107            text_rules,
108            context_defaults,
109            processors,
110        })
111    }
112
113    /// Renders a named rule from this ruleset.
114    ///
115    /// Each call starts with a fresh render context. Bindings and lazy context
116    /// defaults are cached within one render, but not shared with later calls.
117    pub fn render_rule(&self, rule_name: &str) -> Result<String, RenderError> {
118        self.render_rule_with_context(rule_name, RenderContext::new())
119    }
120
121    /// Renders a named rule using render options.
122    pub fn render_rule_with_options(
123        &self,
124        rule_name: &str,
125        options: RenderOptions,
126    ) -> Result<String, RenderError> {
127        self.render_rule_with_context_and_options(rule_name, RenderContext::new(), options)
128    }
129
130    /// Renders a named rule with initial render context values.
131    ///
132    /// Initial context values resolve before lazy `context` defaults and named
133    /// rules. They are scoped to this render call and are not stored on the
134    /// ruleset.
135    pub fn render_rule_with_context(
136        &self,
137        rule_name: &str,
138        context: RenderContext,
139    ) -> Result<String, RenderError> {
140        self.render_rule_with_context_and_options(rule_name, context, RenderOptions::default())
141    }
142
143    /// Renders a named rule with initial render context values and render options.
144    pub fn render_rule_with_context_and_options(
145        &self,
146        rule_name: &str,
147        context: RenderContext,
148        options: RenderOptions,
149    ) -> Result<String, RenderError> {
150        let mut state = RenderState::with_context_and_options(self, context, options);
151        self.render_rule_with_state(rule_name, &mut state)
152    }
153
154    /// Renders a named rule as text, inferring structured JSON for object-valued rules.
155    ///
156    /// String-valued and list-valued rules use existing text rendering. Object-valued
157    /// rules render as formatted JSON using tab indentation.
158    pub fn render_rule_inferred(&self, rule_name: &str) -> Result<String, RenderError> {
159        self.render_rule_inferred_with_context(rule_name, RenderContext::new())
160    }
161
162    /// Renders a named rule with render options, inferring structured JSON for object-valued rules.
163    pub fn render_rule_inferred_with_options(
164        &self,
165        rule_name: &str,
166        options: RenderOptions,
167    ) -> Result<String, RenderError> {
168        self.render_rule_inferred_with_context_and_options(rule_name, RenderContext::new(), options)
169    }
170
171    /// Renders a named rule with initial context, inferring structured JSON for object-valued rules.
172    pub fn render_rule_inferred_with_context(
173        &self,
174        rule_name: &str,
175        context: RenderContext,
176    ) -> Result<String, RenderError> {
177        self.render_rule_inferred_with_context_and_options(
178            rule_name,
179            context,
180            RenderOptions::default(),
181        )
182    }
183
184    /// Renders a named rule with initial context and render options, inferring structured JSON for object-valued rules.
185    pub fn render_rule_inferred_with_context_and_options(
186        &self,
187        rule_name: &str,
188        context: RenderContext,
189        options: RenderOptions,
190    ) -> Result<String, RenderError> {
191        if matches!(
192            self.structured_node(rule_name),
193            Ok(StructuredNode::Object(_))
194        ) {
195            return self
196                .render_rule_structured_with_context_and_options(rule_name, context, options)
197                .and_then(|value| value.to_formatted_json());
198        }
199        self.render_rule_with_context_and_options(rule_name, context, options)
200    }
201
202    /// Renders an object-valued rule as a native structured value.
203    ///
204    /// Each call starts with a fresh render context. Text leaves within the
205    /// structured object share one render state, so bindings and lazy context
206    /// defaults are stable within the structured render.
207    pub fn render_rule_structured(&self, rule_name: &str) -> Result<CopperlaceValue, RenderError> {
208        self.render_rule_structured_with_context(rule_name, RenderContext::new())
209    }
210
211    /// Renders an object-valued rule as a native structured value using render options.
212    pub fn render_rule_structured_with_options(
213        &self,
214        rule_name: &str,
215        options: RenderOptions,
216    ) -> Result<CopperlaceValue, RenderError> {
217        self.render_rule_structured_with_context_and_options(
218            rule_name,
219            RenderContext::new(),
220            options,
221        )
222    }
223
224    /// Renders an object-valued rule as a native structured value with initial context.
225    pub fn render_rule_structured_with_context(
226        &self,
227        rule_name: &str,
228        context: RenderContext,
229    ) -> Result<CopperlaceValue, RenderError> {
230        self.render_rule_structured_with_context_and_options(
231            rule_name,
232            context,
233            RenderOptions::default(),
234        )
235    }
236
237    /// Renders an object-valued rule as a native structured value with initial context and render options.
238    pub fn render_rule_structured_with_context_and_options(
239        &self,
240        rule_name: &str,
241        context: RenderContext,
242        options: RenderOptions,
243    ) -> Result<CopperlaceValue, RenderError> {
244        let node = self.structured_node(rule_name)?;
245        if !matches!(node, StructuredNode::Object(_)) {
246            return Err(RenderError::UnsupportedStructuredTarget(
247                rule_name.to_string(),
248            ));
249        }
250        let mut state = RenderState::with_context_and_options(self, context, options);
251        node.generate_value(&mut state)
252    }
253
254    /// Returns the compiled structured document tree.
255    pub fn structured_document(&self) -> &StructuredNode {
256        &self.document
257    }
258
259    pub(crate) fn structured_node(&self, rule_name: &str) -> Result<&StructuredNode, RenderError> {
260        let mut node = &self.document;
261        for segment in rule_name.split('.') {
262            if segment.is_empty() {
263                return Err(RenderError::UnknownRule(rule_name.to_string()));
264            }
265            let StructuredNode::Object(values) = node else {
266                return Err(RenderError::UnknownRule(rule_name.to_string()));
267            };
268            let Some(next_node) = values.get(segment) else {
269                return Err(RenderError::UnknownRule(rule_name.to_string()));
270            };
271            node = next_node;
272        }
273        Ok(node)
274    }
275
276    pub(crate) fn structured_context_node(&self, name: &str) -> Option<&StructuredNode> {
277        let StructuredNode::Object(document) = &self.document else {
278            return None;
279        };
280        let StructuredNode::Object(context) = document.get("context")? else {
281            return None;
282        };
283
284        let mut node = context.get(name.split('.').next()?)?;
285        for segment in name.split('.').skip(1) {
286            let StructuredNode::Object(values) = node else {
287                return None;
288            };
289            node = values.get(segment)?;
290        }
291        Some(node)
292    }
293
294    pub(crate) fn render_rule_with_state(
295        &self,
296        rule_name: &str,
297        state: &mut RenderState,
298    ) -> Result<String, RenderError> {
299        let Some(rule) = self
300            .text_rules
301            .get(rule_name)
302            .or_else(|| self.context_defaults.get(rule_name))
303        else {
304            return Err(RenderError::UnknownRule(rule_name.to_string()));
305        };
306
307        self.render_node_with_state(rule_name, rule.as_ref(), state, false)
308    }
309
310    pub(crate) fn render_unique_rule_with_state(
311        &self,
312        rule_name: &str,
313        state: &mut RenderState,
314    ) -> Result<String, RenderError> {
315        let Some(rule) = self.text_rules.get(rule_name) else {
316            return Err(RenderError::UnknownRule(rule_name.to_string()));
317        };
318
319        self.render_node_with_state(rule_name, rule.as_ref(), state, true)
320    }
321
322    fn render_node_with_state(
323        &self,
324        rule_name: &str,
325        rule: &dyn TextGeneratorNode,
326        state: &mut RenderState,
327        unique: bool,
328    ) -> Result<String, RenderError> {
329        let existing_calls = state
330            .call_stack
331            .iter()
332            .filter(|name| name.as_str() == rule_name)
333            .count();
334        if state.options.max_recursion_depth == 0 && existing_calls > 0 {
335            let mut cycle = state.call_stack.clone();
336            cycle.push(rule_name.to_string());
337            return Err(RenderError::CircularRuleReference(cycle));
338        }
339        if existing_calls > state.options.max_recursion_depth {
340            return Ok(String::new());
341        }
342
343        state.call_stack.push(rule_name.to_string());
344        let result = if unique {
345            rule.generate_unique_text(rule_name, state)
346        } else {
347            rule.generate_text(state)
348        };
349        state.call_stack.pop();
350        result
351    }
352
353    pub(crate) fn render_context_default_with_state(
354        &self,
355        name: &str,
356        state: &mut RenderState,
357    ) -> Result<Option<String>, RenderError> {
358        let Some(rule) = self.context_defaults.get(name) else {
359            return Ok(None);
360        };
361
362        let existing_calls = state
363            .call_stack
364            .iter()
365            .filter(|rule_name| rule_name.as_str() == name)
366            .count();
367        if state.options.max_recursion_depth == 0 && existing_calls > 0 {
368            let mut cycle = state.call_stack.clone();
369            cycle.push(name.to_string());
370            return Err(RenderError::CircularRuleReference(cycle));
371        }
372        if existing_calls > state.options.max_recursion_depth {
373            return Ok(Some(String::new()));
374        }
375
376        state.call_stack.push(name.to_string());
377        let result = rule.generate_text(state);
378        state.call_stack.pop();
379        result.map(Some)
380    }
381
382    pub(crate) fn process(&self, processor_name: &str, value: &str) -> Result<String, RenderError> {
383        let Some(processor) = self.processors.get(processor_name) else {
384            return Err(RenderError::UnknownProcessor(processor_name.to_string()));
385        };
386
387        processor
388            .process(value)
389            .map_err(|message| RenderError::ProcessorError {
390                processor: processor_name.to_string(),
391                message,
392            })
393    }
394}
395
396/// Compiles a parsed configuration root value and renders one rule.
397///
398/// This is a one-shot helper around [`RuleSet::from_config`] and
399/// [`RuleSet::render_rule`]. Use [`RuleSet`] directly for repeated renders.
400pub fn render_config_rule(config: hocon_rs::Value, rule_name: &str) -> Result<String, RenderError> {
401    render_config_rule_with_context(config, rule_name, RenderContext::new())
402}
403
404/// Compiles a parsed configuration root value and renders one rule with initial context.
405pub fn render_config_rule_with_context(
406    config: hocon_rs::Value,
407    rule_name: &str,
408    context: RenderContext,
409) -> Result<String, RenderError> {
410    render_config_rule_with_context_and_options(
411        config,
412        rule_name,
413        context,
414        RenderOptions::default(),
415    )
416}
417
418/// Compiles a parsed configuration root value and renders one rule with initial context and render options.
419pub fn render_config_rule_with_context_and_options(
420    config: hocon_rs::Value,
421    rule_name: &str,
422    context: RenderContext,
423    options: RenderOptions,
424) -> Result<String, RenderError> {
425    let ruleset = RuleSet::from_config(config)?;
426    ruleset.render_rule_with_context_and_options(rule_name, context, options)
427}
428
429/// Compiles a parsed configuration root value and renders one object-valued rule.
430pub fn render_config_rule_structured(
431    config: hocon_rs::Value,
432    rule_name: &str,
433) -> Result<CopperlaceValue, RenderError> {
434    render_config_rule_structured_with_context(config, rule_name, RenderContext::new())
435}
436
437/// Compiles a parsed configuration root value and renders one object-valued rule with initial context.
438pub fn render_config_rule_structured_with_context(
439    config: hocon_rs::Value,
440    rule_name: &str,
441    context: RenderContext,
442) -> Result<CopperlaceValue, RenderError> {
443    render_config_rule_structured_with_context_and_options(
444        config,
445        rule_name,
446        context,
447        RenderOptions::default(),
448    )
449}
450
451/// Compiles a parsed configuration root value and renders one object-valued rule with initial context and render options.
452pub fn render_config_rule_structured_with_context_and_options(
453    config: hocon_rs::Value,
454    rule_name: &str,
455    context: RenderContext,
456    options: RenderOptions,
457) -> Result<CopperlaceValue, RenderError> {
458    let ruleset = RuleSet::from_config(config)?;
459    ruleset.render_rule_structured_with_context_and_options(rule_name, context, options)
460}