Skip to main content

copperlace/render/
nodes.rs

1use rand::distr::Distribution;
2use rand::distr::weighted::WeightedIndex;
3use rand::seq::IndexedRandom;
4
5use super::error::RenderError;
6use super::state::{IterationValue, LoopMetadata, RenderState};
7
8/// A renderable text-generating piece of a compiled rule.
9///
10/// Nodes are produced from config values, template expressions, and template
11/// statements. Text generation is driven by `RenderState`, which carries the
12/// rule table, bound variables, RNG, and rule call stack for cycle detection.
13pub trait TextGeneratorNode {
14    /// Generates text using the supplied render state.
15    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError>;
16
17    /// Generates text for a strict unique call to `rule_name`.
18    fn generate_unique_text(
19        &self,
20        rule_name: &str,
21        _state: &mut RenderState,
22    ) -> Result<String, RenderError> {
23        Err(RenderError::UnsupportedUniqueChoice(rule_name.to_string()))
24    }
25}
26
27/// Literal text node.
28///
29/// `String` is used for plain template spans such as `"Hello "` and for scalar
30/// config values that do not need further expansion. Rendering returns the
31/// string unchanged.
32impl TextGeneratorNode for String {
33    fn generate_text(&self, _state: &mut RenderState) -> Result<String, RenderError> {
34        Ok(self.clone())
35    }
36}
37
38/// Looks up a previously bound variable in the current render context.
39///
40/// This node is useful when a template should require a value that was already
41/// bound by a `BindNode`. In the current parser, normal `{name}` expressions use
42/// `RuleCallNode` instead, because they can mean either a bound variable or a
43/// named rule.
44pub struct VariableNode {
45    name: String,
46}
47
48impl VariableNode {
49    /// Creates a variable node that reads a bound value by name.
50    pub fn new(name: String) -> Self {
51        VariableNode { name }
52    }
53}
54
55impl TextGeneratorNode for VariableNode {
56    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError> {
57        state
58            .resolve_bound_text(&self.name)?
59            .ok_or_else(|| RenderError::UnknownRule(self.name.clone()))
60    }
61}
62
63/// Calls another named rule, or reuses a bound/context value with the same name.
64///
65/// This is the node generated for `{rule}` template expressions. Resolution
66/// order is:
67/// 1. return an existing bound value from the render context;
68/// 2. render and cache a lazy `context` default, if one exists;
69/// 3. render the named rule from `RuleSet`.
70pub struct RuleCallNode {
71    name: String,
72    unique: bool,
73}
74
75impl RuleCallNode {
76    /// Creates a rule call node for a template reference.
77    pub fn new(name: String) -> Self {
78        RuleCallNode {
79            name,
80            unique: false,
81        }
82    }
83
84    /// Creates a strict unique rule call node for a template reference.
85    pub fn new_unique(name: String) -> Self {
86        RuleCallNode { name, unique: true }
87    }
88}
89
90impl TextGeneratorNode for RuleCallNode {
91    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError> {
92        if let Some(value) = state.resolve_bound_text(&self.name)? {
93            return Ok(value);
94        }
95
96        if let Some(value) = state
97            .ruleset
98            .render_context_default_with_state(&self.name, state)?
99        {
100            state.cache_context_default(&self.name, value.clone());
101            return Ok(value);
102        }
103
104        if self.unique {
105            return state
106                .ruleset
107                .render_unique_rule_with_state(&self.name, state);
108        }
109
110        state.ruleset.render_rule_with_state(&self.name, state)
111    }
112}
113
114/// Controls whether a binding expression preserves or overwrites an existing
115/// value in the render context.
116pub enum BindMode {
117    /// Preserve an existing binding and bind only when the name is missing.
118    IfMissing,
119    /// Always render the source and replace any existing binding.
120    Overwrite,
121}
122
123/// Binds the output of a child node into the render context without emitting it.
124///
125/// This is the node generated for `{% alias:rule %}` statements. If `alias` is
126/// not already bound, it renders `rule` and stores the result under `alias`. It
127/// also supports `{% alias:=rule %}` statements, which always render `rule` and
128/// overwrite `alias`. Binding statements always return an empty string so later
129/// `{alias}` references reuse the generated value.
130pub struct BindNode {
131    name: String,
132    node: Box<dyn TextGeneratorNode>,
133    mode: BindMode,
134}
135
136impl BindNode {
137    /// Creates a binding node for a target name, source node, and binding mode.
138    pub fn new(name: String, node: Box<dyn TextGeneratorNode>, mode: BindMode) -> Self {
139        BindNode { name, node, mode }
140    }
141}
142
143impl TextGeneratorNode for BindNode {
144    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError> {
145        if matches!(self.mode, BindMode::IfMissing) && state.contains_bound_value(&self.name)? {
146            return Ok(String::new());
147        }
148        if matches!(self.mode, BindMode::Overwrite) {
149            state.ensure_mutable_binding(&self.name)?;
150        }
151
152        let value = self.node.generate_text(state)?;
153        state.bind(&self.name, value, matches!(self.mode, BindMode::Overwrite))?;
154        Ok(String::new())
155    }
156}
157
158pub(crate) trait IterationSource {
159    fn elements<'a>(&self, state: &RenderState<'a>)
160    -> Result<Vec<IterationValue<'a>>, RenderError>;
161}
162
163pub(crate) struct ArrayIterationSource {
164    path: String,
165}
166
167impl ArrayIterationSource {
168    pub(crate) fn new(path: String) -> Self {
169        ArrayIterationSource { path }
170    }
171}
172
173impl IterationSource for ArrayIterationSource {
174    fn elements<'a>(
175        &self,
176        state: &RenderState<'a>,
177    ) -> Result<Vec<IterationValue<'a>>, RenderError> {
178        state.iterable_elements(&self.path)
179    }
180}
181
182pub(crate) struct ForEachNode {
183    variable_name: String,
184    source: Box<dyn IterationSource>,
185    body: Box<dyn TextGeneratorNode>,
186}
187
188impl ForEachNode {
189    pub(crate) fn new(
190        variable_name: String,
191        source: Box<dyn IterationSource>,
192        body: Box<dyn TextGeneratorNode>,
193    ) -> Self {
194        ForEachNode {
195            variable_name,
196            source,
197            body,
198        }
199    }
200}
201
202impl TextGeneratorNode for ForEachNode {
203    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError> {
204        let elements = self.source.elements(state)?;
205        let length = elements.len();
206        let mut output = String::new();
207
208        for (index, element) in elements.into_iter().enumerate() {
209            state.push_iteration_scope(
210                &self.variable_name,
211                element,
212                LoopMetadata { index, length },
213            );
214            let rendered = self.body.generate_text(state);
215            state.pop_iteration_scope();
216            output.push_str(&rendered?);
217        }
218
219        Ok(output)
220    }
221}
222
223/// Applies named processors to a rendered child value from left to right.
224pub struct ProcessorPipelineNode {
225    node: Box<dyn TextGeneratorNode>,
226    processors: Vec<String>,
227}
228
229impl ProcessorPipelineNode {
230    /// Creates a pipeline node that applies processors to the rendered child.
231    pub fn new(node: Box<dyn TextGeneratorNode>, processors: Vec<String>) -> Self {
232        ProcessorPipelineNode { node, processors }
233    }
234}
235
236impl TextGeneratorNode for ProcessorPipelineNode {
237    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError> {
238        let mut value = self.node.generate_text(state)?;
239        for processor_name in &self.processors {
240            value = state.ruleset.process(processor_name, &value)?;
241        }
242        Ok(value)
243    }
244}
245
246/// Randomly renders one child node from a list of alternatives.
247///
248/// This is produced from text-rendered config arrays. For example,
249/// `mood = [happy, sad]` becomes a choice between two literal nodes. If the
250/// array is empty, rendering returns `RenderError::EmptyChoice`.
251pub struct ChoiceNode {
252    nodes: Vec<Box<dyn TextGeneratorNode>>,
253}
254
255impl ChoiceNode {
256    /// Creates a choice node from renderable alternatives.
257    pub fn new(nodes: Vec<Box<dyn TextGeneratorNode>>) -> Self {
258        ChoiceNode { nodes }
259    }
260}
261
262impl TextGeneratorNode for ChoiceNode {
263    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError> {
264        let random_node = self
265            .nodes
266            .choose(&mut state.rng)
267            .ok_or(RenderError::EmptyChoice)?;
268        random_node.generate_text(state)
269    }
270
271    fn generate_unique_text(
272        &self,
273        rule_name: &str,
274        state: &mut RenderState,
275    ) -> Result<String, RenderError> {
276        if self.nodes.is_empty() {
277            return Err(RenderError::EmptyChoice);
278        }
279
280        let used_indices = state.used_unique_choice_indices(rule_name);
281        let unused_indices = (0..self.nodes.len())
282            .filter(|index| used_indices.is_none_or(|used| !used.contains(index)))
283            .collect::<Vec<_>>();
284        let selected_index = *unused_indices
285            .choose(&mut state.rng)
286            .ok_or_else(|| RenderError::ExhaustedUniqueChoice(rule_name.to_string()))?;
287        state.mark_unique_choice_index(rule_name, selected_index);
288        self.nodes[selected_index].generate_text(state)
289    }
290}
291
292/// Randomly renders one child node using per-child weights.
293///
294/// Weighted choices are produced from arrays containing at least one weighted
295/// object entry, such as `{ value = "common", weight = 9 }`. Plain entries in
296/// the same array receive weight `1.0`.
297pub struct WeightedChoiceNode {
298    nodes: Vec<Box<dyn TextGeneratorNode>>,
299    weights: Vec<f64>,
300    distribution: WeightedIndex<f64>,
301}
302
303impl WeightedChoiceNode {
304    /// Creates a weighted choice node from renderable alternatives and weights.
305    pub fn new(entries: Vec<(Box<dyn TextGeneratorNode>, f64)>) -> Result<Self, RenderError> {
306        let (nodes, weights): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
307        let distribution = WeightedIndex::new(weights.clone())
308            .map_err(|error| RenderError::InvalidWeightedChoice(error.to_string()))?;
309        Ok(WeightedChoiceNode {
310            nodes,
311            weights,
312            distribution,
313        })
314    }
315}
316
317impl TextGeneratorNode for WeightedChoiceNode {
318    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError> {
319        let index = self.distribution.sample(&mut state.rng);
320        self.nodes[index].generate_text(state)
321    }
322
323    fn generate_unique_text(
324        &self,
325        rule_name: &str,
326        state: &mut RenderState,
327    ) -> Result<String, RenderError> {
328        if self.nodes.is_empty() {
329            return Err(RenderError::EmptyChoice);
330        }
331
332        let used_indices = state.used_unique_choice_indices(rule_name);
333        let unused_entries = self
334            .weights
335            .iter()
336            .enumerate()
337            .filter(|(index, _)| used_indices.is_none_or(|used| !used.contains(index)))
338            .map(|(index, weight)| (index, *weight))
339            .collect::<Vec<_>>();
340        if unused_entries.is_empty() {
341            return Err(RenderError::ExhaustedUniqueChoice(rule_name.to_string()));
342        }
343
344        let remaining_weights = unused_entries
345            .iter()
346            .map(|(_, weight)| *weight)
347            .collect::<Vec<_>>();
348        let distribution = WeightedIndex::new(remaining_weights)
349            .map_err(|_| RenderError::ExhaustedUniqueChoice(rule_name.to_string()))?;
350        let selected_entry_index = distribution.sample(&mut state.rng);
351        let selected_node_index = unused_entries[selected_entry_index].0;
352        state.mark_unique_choice_index(rule_name, selected_node_index);
353        self.nodes[selected_node_index].generate_text(state)
354    }
355}
356
357/// Renders a sequence of child nodes and concatenates their output.
358///
359/// This is produced from string templates after splitting literal text,
360/// `{...}` expressions, and `{% ... %}` statements. For example,
361/// `"Hello {name}"` becomes a `VecNode` containing a literal `"Hello "` and a
362/// `RuleCallNode` for `name`.
363pub struct VecNode {
364    nodes: Vec<Box<dyn TextGeneratorNode>>,
365}
366
367impl VecNode {
368    /// Creates a sequence node that renders children in order.
369    pub fn new(nodes: Vec<Box<dyn TextGeneratorNode>>) -> Self {
370        VecNode { nodes }
371    }
372}
373
374impl TextGeneratorNode for VecNode {
375    fn generate_text(&self, state: &mut RenderState) -> Result<String, RenderError> {
376        let mut output = String::new();
377
378        for node in &self.nodes {
379            output.push_str(&node.generate_text(state)?);
380        }
381
382        Ok(output)
383    }
384}
385
386/// Placeholder node for config value types that are not renderable yet.
387///
388/// Object values currently compile to this node unless they are the special
389/// top-level `context` object handled by `RuleSet::from_config`. Rendering this
390/// node returns `RenderError::UnsupportedValue`.
391pub struct UnsupportedValueNode {
392    value_type: String,
393}
394
395impl UnsupportedValueNode {
396    /// Creates a node that reports an unsupported config value type at render time.
397    pub fn new(value_type: String) -> Self {
398        UnsupportedValueNode { value_type }
399    }
400}
401
402impl TextGeneratorNode for UnsupportedValueNode {
403    fn generate_text(&self, _state: &mut RenderState) -> Result<String, RenderError> {
404        Err(RenderError::UnsupportedValue(self.value_type.clone()))
405    }
406}