hexroll3_scroll/
renderer.rs

1/*
2// Copyright (C) 2020-2025 Pen, Dice & Paper
3//
4// This program is dual-licensed under the following terms:
5//
6// Option 1: (Non-Commercial) GNU Affero General Public License (AGPL)
7// This program is free software: you can redistribute it and/or modify
8// it under the terms of the GNU Affero General Public License as
9// published by the Free Software Foundation, either version 3 of the
10// License, or (at your option) any later version.
11//
12// This program is distributed in the hope that it will be useful,
13// but WITHOUT ANY WARRANTY; without even the implied warranty of
14// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15// GNU Affero General Public License for more details.
16//
17// You should have received a copy of the GNU Affero General Public License
18// along with this program. If not, see <http://www.gnu.org/licenses/>.
19//
20// Option 2: Commercial License
21// For commercial use, you are required to obtain a separate commercial
22// license. Please contact ithai at pendicepaper.com
23// for more information about commercial licensing terms.
24*/
25use anyhow::anyhow;
26use minijinja::Environment;
27use std::collections::HashMap;
28
29use crate::instance::SandboxInstance;
30use crate::renderer_env::prepare_renderer;
31use crate::repository::{ReadOnlyLoader, ReadOnlyTransaction};
32
33struct RendererContext<'a> {
34    cache: HashMap<String, serde_json::value::Value>,
35    env: Environment<'a>,
36}
37
38/// Generates HTML for the given object using a specified template.
39///
40/// # Arguments
41///
42/// * `instance` - Reference to the CachedInstance containing class specifications.
43/// * `tx` - Read-only transaction for accessing repository data.
44/// * `obj` - JSON object representing the data to be rendered.
45///
46/// # Returns
47///
48/// A `String` containing the rendered HTML if the class has an HTML body; otherwise, returns an empty string.
49///
50/// The function sets up a rendering environment, prepares the renderer, and attempts to render the template with the provided data.
51pub fn render_entity_html(
52    instance: &SandboxInstance,
53    tx: &ReadOnlyTransaction,
54    obj: &serde_json::Value,
55) -> anyhow::Result<(String, String)> {
56    let mut env = Environment::new();
57    prepare_renderer(&mut env, instance);
58    if let Some(class_spec) = obj["class"]
59        .as_str()
60        .and_then(|name| instance.classes.get(name))
61    {
62        if let (Some(html_body), Some(html_header)) =
63            (&class_spec.html_body, &class_spec.html_header)
64        {
65            let rendered_header = env
66                .render_str(
67                    html_header.as_str(),
68                    render_entity(instance, tx, obj, true)?,
69                )
70                .map_err(anyhow::Error::new)?;
71            let rendered_body = env
72                .render_str(html_body.as_str(), render_entity(instance, tx, obj, true)?)
73                .map_err(anyhow::Error::new)?;
74            return Ok((rendered_header, rendered_body));
75        }
76    }
77    Ok((String::new(), String::new()))
78}
79
80pub fn render_entity<T: ReadOnlyLoader>(
81    instance: &SandboxInstance,
82    tx: &T,
83    obj: &serde_json::Value,
84    is_root: bool,
85) -> anyhow::Result<serde_json::Value> {
86    let env = {
87        let mut env = Environment::new();
88        env.set_undefined_behavior(minijinja::UndefinedBehavior::Chainable);
89        prepare_renderer(&mut env, instance);
90        env
91    };
92    recursive_entity_renderer(
93        &mut RendererContext {
94            cache: HashMap::new(),
95            env,
96        },
97        instance,
98        tx,
99        obj,
100        is_root,
101        None,
102    )
103}
104
105/// Renders attributes of a given object recursively within a specified context.
106///
107/// This function processes an object's attributes based on the class specification,
108/// handles caching, and renders templates or nested objects as needed.
109/// Supports hierarchical rendering with parent and pointer references.
110///
111/// # Note
112/// This function assumes a well-formed context entry from the point it processes specific attributes.
113/// If any of the `as_str().unwrap()` calls fail, it indicates a logical bug in the input structure,
114/// rather than a recoverable runtime error.
115///
116/// # Type Parameters
117/// - `T`: A loader implementing `ReadOnlyLoader` for data retrieval.
118///
119/// # Arguments
120/// - `context`: The rendering context for managing templates and caching.
121/// - `instance`: The cached instance with class specifications.
122/// - `tx`: Data loader for retrieving entities.
123/// - `obj`: JSON object to be rendered.
124/// - `is_root`: Indicates if the object is the root node.
125/// - `stopper`: Optional key to halt rendering at a specific point.
126///
127/// # Returns
128/// - `anyhow::Result<serde_json::Value>`: Rendered object or an error if rendering fails.
129fn recursive_entity_renderer<T: ReadOnlyLoader>(
130    context: &mut RendererContext,
131    instance: &SandboxInstance,
132    tx: &T,
133    obj: &serde_json::Value,
134    is_root: bool,
135    stopper: Option<&str>,
136) -> anyhow::Result<serde_json::Value> {
137    let uuid = obj["uid"].as_str().unwrap().to_string();
138    if context.cache.contains_key(&uuid) && !is_root {
139        return Ok(context.cache.get(&uuid).unwrap().clone());
140    }
141    let class_name = obj["class"].as_str().unwrap();
142    let class_spec = &instance.classes[class_name];
143
144    let mut ctx = serde_json::json!({
145        "uuid" : obj["uid"]
146    });
147    let mut ret = serde_json::json!({
148        "class": obj["class"],
149        "uuid": obj["uid"]
150    });
151    for spec in class_spec.collects.iter() {
152        if let Some(attr) = &spec.virtual_attribute {
153            if attr.is_optional && !is_root {
154                continue;
155            }
156            let frame = tx.retrieve(&format!("{}_frame", uuid))?;
157            let unused = &frame.value["$collections"]["$unused"][&spec.class_name];
158            ctx[&attr.attr_name] = serde_json::Value::from(
159                unused
160                    .as_array()
161                    .unwrap()
162                    .iter()
163                    .map(|unused_id| {
164                        let next = tx.retrieve(unused_id.as_str().unwrap())?;
165                        recursive_entity_renderer(context, instance, tx, &next.value, false, None)
166                    })
167                    .collect::<Result<Vec<serde_json::Value>, _>>()?,
168            );
169            if attr.is_public || is_root {
170                ret[&attr.attr_name] = ctx[&attr.attr_name].clone();
171            }
172        }
173    }
174    for (attr_name, raw_value) in obj.as_object().unwrap() {
175        if attr_name.starts_with('$') {
176            continue;
177        }
178        let (is_optional, is_public, is_array) =
179            if let Some(attr_spec) = &class_spec.attrs.get(attr_name) {
180                (
181                    attr_spec.is_optional,
182                    attr_spec.is_public,
183                    attr_spec.is_array,
184                )
185            } else {
186                (false, false, false)
187            };
188        if is_optional && !is_root {
189            continue;
190        }
191        ctx[attr_name] = match raw_value {
192            serde_json::Value::Bool(_) | serde_json::Value::Number(_) => obj[attr_name].clone(),
193            serde_json::Value::String(_) => serde_json::Value::String(
194                context
195                    .env
196                    .render_str(obj[attr_name].as_str().unwrap(), &ctx)
197                    .map_err(|e| {
198                        anyhow::anyhow!(
199                            "Failed to render string template {} for uid {} attr {} with error {:#}",
200                            obj[attr_name].as_str().unwrap(),
201                            uuid,
202                            attr_name,
203                            e
204                        )
205                    })?,
206            ),
207            serde_json::Value::Array(_) => {
208                if is_array {
209                    serde_json::Value::from(
210                        raw_value
211                            .as_array()
212                            .unwrap()
213                            .iter()
214                            .map(|child_uid| {
215                                let next = tx.retrieve(child_uid.as_str().unwrap())?;
216                                recursive_entity_renderer(
217                                    context,
218                                    instance,
219                                    tx,
220                                    &next.value,
221                                    false,
222                                    None,
223                                )
224                            })
225                            .collect::<Result<Vec<serde_json::Value>, _>>()?,
226                    )
227                } else if let Some(id) = raw_value.as_array().unwrap().iter().next() {
228                    let next = tx.retrieve(id.as_str().unwrap())?;
229                    recursive_entity_renderer(context, instance, tx, &next.value, false, None)?
230                } else {
231                    serde_json::json!({})
232                }
233            }
234            serde_json::Value::Object(_) => {
235                render_indirections(context, instance, tx, obj, attr_name)?
236            }
237            serde_json::Value::Null => {
238                let tmpl_str = class_spec.attrs[attr_name].cmd.value().unwrap();
239                serde_json::Value::String(context.env.render_str(&tmpl_str, &ctx).map_err(|e| {
240                        anyhow::anyhow!(
241                            "Failed to render string template {} for uid {} attr {} with error {:#}",
242                            tmpl_str,
243                            uuid,
244                            attr_name,
245                            e
246                        )
247                    })?,
248                )
249            }
250        };
251        if is_public || is_root {
252            ret[attr_name] = ctx[attr_name].clone();
253        }
254        if let Some(stopper) = stopper {
255            if stopper == attr_name {
256                break;
257            }
258        } else {
259            context.cache.insert(uuid.clone(), ret.clone());
260        }
261    }
262    Ok(ret)
263}
264
265/// Renders a specific attribute from a pointed-to entity using its unique identifier.
266///
267/// Retrieves the entity by its UID, processes it through `render_inner`, and extracts
268/// the specified attribute.
269///
270/// # Type Parameters
271/// - `T`: A `ReadOnlyLoader` for retrieving entity data.
272///
273/// # Arguments
274/// - `context`: The rendering context for template and caching operations.
275/// - `instance`: Cached instance containing relevant specifications.
276/// - `tx`: Data loader for retrieving the pointed-to entity.
277/// - `uid`: Unique identifier for the target entity.
278/// - `attr`: Attribute to be rendered from the pointed entity.
279///
280/// # Returns
281/// - `anyhow::Result<serde_json::Value>`: Rendered attribute or an error if retrieval/rendering fails.
282fn render_pointer_attribute<T: ReadOnlyLoader>(
283    context: &mut RendererContext,
284    instance: &SandboxInstance,
285    tx: &T,
286    uid: &str,
287    attr: &str,
288) -> anyhow::Result<serde_json::Value> {
289    let pointed_entity = tx.retrieve(uid)?.value;
290    let pointed_render =
291        recursive_entity_renderer(context, instance, tx, &pointed_entity, true, Some(attr))?;
292    Ok(pointed_render[attr].clone())
293}
294
295/// Renders or retrieves a specified parent attribute from a cached entity instance,
296/// navigating up the hierarchy if necessary.
297///
298/// # Type Parameters
299/// - `T`: A loader implementing `ReadOnlyLoader` for data retrieval.
300///
301/// # Arguments
302/// - `context`: Rendering context used for attribute rendering.
303/// - `instance`: Cached instance containing class hierarchy information.
304/// - `tx`: Data loader for retrieving entities.
305/// - `pid`: Parent entity identifier.
306/// - `parent_class`: Class name to match in the hierarchy.
307/// - `parent_attr`: Attribute to retrieve.
308///
309/// # Returns
310/// - `anyhow::Result<serde_json::Value>`: The rendered or retrieved attribute value.
311///
312/// # Errors
313/// - Returns an error if the parent entity is missing or lacks a valid `class` attribute,
314///   or if its class is not found in the cached instance.
315fn render_parent_attribute<T: ReadOnlyLoader>(
316    context: &mut RendererContext,
317    instance: &SandboxInstance,
318    tx: &T,
319    pid: &str,
320    parent_class: &str,
321    parent_attr: &str,
322) -> anyhow::Result<serde_json::Value> {
323    let parent = tx.retrieve(pid)?.value;
324    let class = parent["class"].as_str().unwrap();
325    let Some(class_spec) = &instance.classes.get(class) else {
326        return Err(anyhow!("Could not find class {} in entity {}", class, pid));
327    };
328    if class_spec.hierarchy.contains(&parent_class.to_string()) {
329        // Theoretically, we should have done:
330        // ```
331        // let v = recursive_entity_renderer(context, instance, tx, &parent, true, Some(parent_attr))?;
332        // return Ok(v[parent_attr].clone());
333        // ```
334        // But it is excessive and will result in poor performance, so we care for three cases
335        // only:
336        //   * child entities
337        //   * indirections (pointers and context references)
338        //   * value copies
339        //
340        let v = &parent[parent_attr];
341        return if v.is_array() && !v.as_array().unwrap().is_empty() {
342            let data_uid = v.as_array().unwrap().first().unwrap().as_str().unwrap();
343            let data = tx.retrieve(data_uid)?.value;
344            recursive_entity_renderer(context, instance, tx, &data, false, Some(parent_attr))
345        } else if v.is_object() {
346            render_indirections(context, instance, tx, &parent, parent_attr)
347        } else {
348            Ok(parent[parent_attr].clone())
349        };
350    } else {
351        let my_pid = &parent["parent_uid"];
352        if my_pid != "root" {
353            return render_parent_attribute(
354                context,
355                instance,
356                tx,
357                my_pid.as_str().unwrap(),
358                parent_class,
359                parent_attr,
360            );
361        }
362    }
363    Ok(serde_json::json!(false))
364}
365
366/// Render pointers and context references indicated using a json object
367/// containing a `type` entry of either `context` or `pointer` a `spec`
368/// entry containing the data needed to indirectly render the value.
369///
370/// # Arguments
371/// - `context`: Rendering context used for attribute rendering.
372/// - `instance`: Cached instance containing class hierarchy information.
373/// - `tx`: Data loader for retrieving entities.
374/// - `obj`: Entity object holding this indirection attribute.
375/// - `attr_name`: Attribute name holding this indirection attribute.
376///
377/// # Returns
378/// - `anyhow::Result<serde_json::Value>`: The rendered or retrieved attribute value.
379fn render_indirections<T: ReadOnlyLoader>(
380    context: &mut RendererContext,
381    instance: &SandboxInstance,
382    tx: &T,
383    obj: &serde_json::Value,
384    attr_name: &str,
385) -> Result<serde_json::Value, anyhow::Error> {
386    let indirection = &obj[attr_name];
387    if indirection["type"] == "context" {
388        let pid = obj["parent_uid"].as_str().unwrap();
389        let spec = &indirection["spec"];
390        let parent_attr = spec["attr"].as_str().unwrap();
391        return render_parent_attribute(
392            context,
393            instance,
394            tx,
395            pid,
396            spec["parent"].as_str().unwrap(),
397            parent_attr,
398        );
399    } else if indirection["type"] == "pointer" {
400        let spec = &indirection["spec"];
401        let attr = spec["attr"].as_str().unwrap();
402        return render_pointer_attribute(
403            context,
404            instance,
405            tx,
406            spec["uid"].as_str().unwrap(),
407            attr,
408        );
409    } else {
410        return Err(anyhow!(
411            "Unknown obj detected {}, {}",
412            indirection,
413            attr_name
414        ));
415    }
416}