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}