Skip to main content

copperlace/render/
ruleset.rs

1use std::collections::{BTreeMap, HashMap};
2
3use crate::processors::builtin_processors;
4
5use super::compile::{
6    insert_context_text_nodes, insert_named_text_nodes, value_to_structured_node,
7};
8use super::error::RenderError;
9use super::nodes::TextGeneratorNode;
10use super::processor::ProcessorRegistry;
11use super::state::{RenderContext, RenderState};
12use super::value::{CopperlaceValue, StructuredNode};
13
14/// Compiled collection of named rules from the config.
15///
16/// Top-level config entries become startable rules, so callers can render
17/// `origin`, `story`, `name`, or any other named entry directly. A top-level
18/// `context` object is treated specially: its entries become lazy defaults for
19/// bound variables, so `{hero}` can generate and cache `context.hero` on first
20/// use within a render.
21pub struct RuleSet {
22    document: StructuredNode,
23    text_rules: HashMap<String, Box<dyn TextGeneratorNode>>,
24    context_defaults: HashMap<String, Box<dyn TextGeneratorNode>>,
25    processors: ProcessorRegistry,
26}
27
28impl RuleSet {
29    /// Compiles a parsed configuration root value using the builtin processor registry.
30    ///
31    /// The root value must be a configuration object. Top-level entries become named
32    /// rules, except a top-level object named `context`, whose entries become
33    /// lazy defaults available to template references.
34    pub fn from_config(config: hocon_rs::Value) -> Result<Self, RenderError> {
35        Self::from_config_with_processors(config, ProcessorRegistry::new())
36    }
37
38    /// Compiles a parsed configuration root value with additional custom processors.
39    ///
40    /// Custom processors are merged into the builtin registry before templates
41    /// are compiled, so unknown processor names fail during compilation. A
42    /// custom processor with the same name as a builtin overrides the builtin.
43    pub fn from_config_with_processors(
44        config: hocon_rs::Value,
45        custom_processors: ProcessorRegistry,
46    ) -> Result<Self, RenderError> {
47        let hocon_rs::Value::Object(values) = config else {
48            return Err(RenderError::InvalidConfigRoot);
49        };
50
51        let mut processors = builtin_processors();
52        processors.extend(custom_processors);
53
54        let mut document_values = BTreeMap::new();
55        let mut text_rules = HashMap::new();
56        let mut context_defaults = HashMap::new();
57
58        for (name, value) in values {
59            document_values.insert(
60                name.clone(),
61                value_to_structured_node(value.clone(), &processors)?,
62            );
63            if name == "context" {
64                if let hocon_rs::Value::Object(context_values) = value {
65                    for (context_name, context_value) in context_values {
66                        insert_context_text_nodes(
67                            &mut context_defaults,
68                            context_name,
69                            context_value,
70                            &processors,
71                        )?;
72                    }
73                } else {
74                    insert_named_text_nodes(&mut text_rules, name, value, &processors)?;
75                }
76            } else {
77                insert_named_text_nodes(&mut text_rules, name, value, &processors)?;
78            }
79        }
80
81        Ok(RuleSet {
82            document: StructuredNode::Object(document_values),
83            text_rules,
84            context_defaults,
85            processors,
86        })
87    }
88
89    /// Renders a named rule from this ruleset.
90    ///
91    /// Each call starts with a fresh render context. Bindings and lazy context
92    /// defaults are cached within one render, but not shared with later calls.
93    pub fn render_rule(&self, rule_name: &str) -> Result<String, RenderError> {
94        self.render_rule_with_context(rule_name, RenderContext::new())
95    }
96
97    /// Renders a named rule with initial render context values.
98    ///
99    /// Initial context values resolve before lazy `context` defaults and named
100    /// rules. They are scoped to this render call and are not stored on the
101    /// ruleset.
102    pub fn render_rule_with_context(
103        &self,
104        rule_name: &str,
105        context: RenderContext,
106    ) -> Result<String, RenderError> {
107        let mut state = RenderState::with_context(self, context);
108        self.render_rule_with_state(rule_name, &mut state)
109    }
110
111    /// Renders a named rule as text, inferring structured JSON for object-valued rules.
112    ///
113    /// String-valued and list-valued rules use existing text rendering. Object-valued
114    /// rules render as formatted JSON using tab indentation.
115    pub fn render_rule_inferred(&self, rule_name: &str) -> Result<String, RenderError> {
116        self.render_rule_inferred_with_context(rule_name, RenderContext::new())
117    }
118
119    /// Renders a named rule with initial context, inferring structured JSON for object-valued rules.
120    pub fn render_rule_inferred_with_context(
121        &self,
122        rule_name: &str,
123        context: RenderContext,
124    ) -> Result<String, RenderError> {
125        if matches!(
126            self.structured_node(rule_name),
127            Ok(StructuredNode::Object(_))
128        ) {
129            return self
130                .render_rule_structured_with_context(rule_name, context)
131                .and_then(|value| value.to_formatted_json());
132        }
133        self.render_rule_with_context(rule_name, context)
134    }
135
136    /// Renders an object-valued rule as a native structured value.
137    ///
138    /// Each call starts with a fresh render context. Text leaves within the
139    /// structured object share one render state, so bindings and lazy context
140    /// defaults are stable within the structured render.
141    pub fn render_rule_structured(&self, rule_name: &str) -> Result<CopperlaceValue, RenderError> {
142        self.render_rule_structured_with_context(rule_name, RenderContext::new())
143    }
144
145    /// Renders an object-valued rule as a native structured value with initial context.
146    pub fn render_rule_structured_with_context(
147        &self,
148        rule_name: &str,
149        context: RenderContext,
150    ) -> Result<CopperlaceValue, RenderError> {
151        let node = self.structured_node(rule_name)?;
152        if !matches!(node, StructuredNode::Object(_)) {
153            return Err(RenderError::UnsupportedStructuredTarget(
154                rule_name.to_string(),
155            ));
156        }
157        let mut state = RenderState::with_context(self, context);
158        node.generate_value(&mut state)
159    }
160
161    /// Returns the compiled structured document tree.
162    pub fn structured_document(&self) -> &StructuredNode {
163        &self.document
164    }
165
166    fn structured_node(&self, rule_name: &str) -> Result<&StructuredNode, RenderError> {
167        let mut node = &self.document;
168        for segment in rule_name.split('.') {
169            if segment.is_empty() {
170                return Err(RenderError::UnknownRule(rule_name.to_string()));
171            }
172            let StructuredNode::Object(values) = node else {
173                return Err(RenderError::UnknownRule(rule_name.to_string()));
174            };
175            let Some(next_node) = values.get(segment) else {
176                return Err(RenderError::UnknownRule(rule_name.to_string()));
177            };
178            node = next_node;
179        }
180        Ok(node)
181    }
182
183    pub(crate) fn render_rule_with_state(
184        &self,
185        rule_name: &str,
186        state: &mut RenderState,
187    ) -> Result<String, RenderError> {
188        let Some(rule) = self
189            .text_rules
190            .get(rule_name)
191            .or_else(|| self.context_defaults.get(rule_name))
192        else {
193            return Err(RenderError::UnknownRule(rule_name.to_string()));
194        };
195
196        if state.call_stack.iter().any(|name| name == rule_name) {
197            let mut cycle = state.call_stack.clone();
198            cycle.push(rule_name.to_string());
199            return Err(RenderError::CircularRuleReference(cycle));
200        }
201
202        state.call_stack.push(rule_name.to_string());
203        let result = rule.generate_text(state);
204        state.call_stack.pop();
205        result
206    }
207
208    pub(crate) fn render_context_default_with_state(
209        &self,
210        name: &str,
211        state: &mut RenderState,
212    ) -> Result<Option<String>, RenderError> {
213        let Some(rule) = self.context_defaults.get(name) else {
214            return Ok(None);
215        };
216
217        if state.call_stack.iter().any(|rule_name| rule_name == name) {
218            let mut cycle = state.call_stack.clone();
219            cycle.push(name.to_string());
220            return Err(RenderError::CircularRuleReference(cycle));
221        }
222
223        state.call_stack.push(name.to_string());
224        let result = rule.generate_text(state);
225        state.call_stack.pop();
226        result.map(Some)
227    }
228
229    pub(crate) fn process(&self, processor_name: &str, value: &str) -> Result<String, RenderError> {
230        let Some(processor) = self.processors.get(processor_name) else {
231            return Err(RenderError::UnknownProcessor(processor_name.to_string()));
232        };
233
234        processor
235            .process(value)
236            .map_err(|message| RenderError::ProcessorError {
237                processor: processor_name.to_string(),
238                message,
239            })
240    }
241}
242
243/// Compiles a parsed configuration root value and renders one rule.
244///
245/// This is a one-shot helper around [`RuleSet::from_config`] and
246/// [`RuleSet::render_rule`]. Use [`RuleSet`] directly for repeated renders.
247pub fn render_config_rule(config: hocon_rs::Value, rule_name: &str) -> Result<String, RenderError> {
248    render_config_rule_with_context(config, rule_name, RenderContext::new())
249}
250
251/// Compiles a parsed configuration root value and renders one rule with initial context.
252pub fn render_config_rule_with_context(
253    config: hocon_rs::Value,
254    rule_name: &str,
255    context: RenderContext,
256) -> Result<String, RenderError> {
257    let ruleset = RuleSet::from_config(config)?;
258    ruleset.render_rule_with_context(rule_name, context)
259}
260
261/// Compiles a parsed configuration root value and renders one object-valued rule.
262pub fn render_config_rule_structured(
263    config: hocon_rs::Value,
264    rule_name: &str,
265) -> Result<CopperlaceValue, RenderError> {
266    render_config_rule_structured_with_context(config, rule_name, RenderContext::new())
267}
268
269/// Compiles a parsed configuration root value and renders one object-valued rule with initial context.
270pub fn render_config_rule_structured_with_context(
271    config: hocon_rs::Value,
272    rule_name: &str,
273    context: RenderContext,
274) -> Result<CopperlaceValue, RenderError> {
275    let ruleset = RuleSet::from_config(config)?;
276    ruleset.render_rule_structured_with_context(rule_name, context)
277}