trinja 0.7.1

HTML templating / SSG for RDF(S) resources
Documentation
//! Utilities for handling the [minijinja::Environment] for Trinja

pub(crate) mod functions;
use functions::*;

use std::sync::{Arc, RwLock};

use minijinja::{Environment, Value};
use taganak_core::{
    graphs::{Graph, GraphView},
    terms::Term,
};
use tokio::runtime::Handle;
use tracing::{debug, error, trace};

use crate::Params;

const MAX_TEMPLATE_RECURSION: usize = 3;

/// Load a template from an RDF graph
fn template_from_graph<G>(
    name: &str,
    graph: Arc<RwLock<G>>,
    default_template: Option<Term>,
) -> Result<Option<String>, minijinja::Error>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
{
    let handle = Handle::current();
    let name = match name {
        "default" => {
            if let Some(ref template) = default_template {
                debug!("Requested default template");
                template.to_string()
            } else {
                error!("Requested default template but none provided");
                return Ok(None);
            }
        }
        _ => name.to_string(),
    };

    handle.block_on(async {
        let graph_clone = graph.clone();
        let graph_read = graph_clone.read().unwrap();

        let mut name = name.to_string();
        let mut recursion = 0usize;

        debug!(?name, "Loading template for");

        while recursion < MAX_TEMPLATE_RECURSION {
            recursion += 1;

            match graph_read
                .view()
                .await
                .object(
                    Some(&name.clone().try_into().unwrap()),
                    Some(
                        &"<https://trinja.taganak.net/vocab/template>"
                            .try_into()
                            .expect("static IRI"),
                    ),
                )
                .await
                .ok()
                .unwrap()
                .as_deref()
            {
                Some(Term::Literal(literal)) => {
                    if literal.datatype() == "https://trinja.taganak.net/vocab/minijinja" {
                        debug!(?name, "specific template found for");
                        return Ok(Some(literal.lexical().to_string()));
                    } else {
                        error!(?name, "template with unsupported datatype for");
                        // FIXME map error instead
                        return Ok(None);
                    }
                }
                Some(node @ Term::NamedNode(_)) => {
                    name = node.to_string();
                    debug!(?name, "Recursing to new template");
                    continue;
                }
                None => {
                    if recursion > 1 {
                        error!(?name, "referenced template does not contain literal");
                        // FIXME map error instead
                        return Ok(None);
                    }

                    let rdf_type = graph_read
                        .view()
                        .await
                        .object(
                            Some(&name.clone().try_into().unwrap()),
                            Some(
                                &"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
                                    .try_into()
                                    .expect("static IRI"),
                            ),
                        )
                        .await
                        .unwrap()
                        .unwrap();
                    debug!(?rdf_type, "Finding template for RDF type of resource");
                    match graph_read
                        .view()
                        .await
                        .object(
                            Some(&rdf_type),
                            Some(
                                &"<https://trinja.taganak.net/vocab/genericTemplate>"
                                    .try_into()
                                    .expect("static IRI"),
                            ),
                        )
                        .await
                        .ok()
                        .unwrap()
                        .as_deref()
                    {
                        Some(Term::Literal(literal)) => {
                            if literal.datatype() == "https://trinja.taganak.net/vocab/minijinja" {
                                debug!(?name, ?rdf_type, "generic template for");
                                return Ok(Some(literal.lexical().to_string()));
                            } else {
                                error!(?rdf_type, "generic template has unsupported datatype");
                                // FIXME map error instead
                                return Ok(None);
                            }
                        }
                        Some(node @ Term::NamedNode(_)) => {
                            name = node.to_string();
                            debug!(?name, "Recursing to new template");
                            continue;
                        }
                        None => {
                            if let Some(ref term) = default_template {
                                debug!(?term, "No type; trying to load default template");
                                name = term.to_string();
                                continue;
                            }

                            error!(?name, "no template found and no default template set");
                            // FIXME map error instead
                            return Ok(None);
                        }
                        // FIXME maybe add subClassOf here
                        _ => {
                            error!(?name, "no template found for");
                            // FIXME map error instead
                            return Ok(None);
                        }
                    }
                }
                Some(_) => {
                    error!(
                        ?name,
                        "template is neither reference nor literal (schema error)"
                    );
                    return Ok(None);
                }
            };
        }

        // FIXME return error instead
        Ok(None)
    })
}

/// Create a new [minijinja::Environment] usable for the provided graph
///
/// The `default_template` points to a `trinja:Template` that will be made
/// available under the special name `default` in templates. It is used by
/// the static site generator to provide the base template for the entire
/// site.
pub(crate) fn environment<'e, G>(
    params: Params<'e, G>,
    default_template: Option<Term>,
) -> Arc<RwLock<Environment<'static>>>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
{
    trace!("Building new minijinja environment");
    let env: Environment<'static> = Environment::new();
    let g = params.graph.clone();
    let env = Arc::new(RwLock::new(env.clone()));

    env.write().unwrap().add_global(
        "rdf_get",
        Value::from_object(rdf_get::GraphFn::new(
            params
                .clone()
                .environment(Some(env.clone()))
                .to_with_env()
                .unwrap(),
        )),
    );

    env.write().unwrap().add_global(
        "rdf_subjects",
        Value::from_object(rdf_subjects::GraphFn::new(
            params
                .clone()
                .environment(Some(env.clone()))
                .to_with_env()
                .unwrap(),
        )),
    );
    env.write().unwrap().add_global(
        "rdf_objects",
        Value::from_object(rdf_objects::GraphFn::new(
            params
                .clone()
                .environment(Some(env.clone()))
                .to_with_env()
                .unwrap(),
        )),
    );
    env.write()
        .unwrap()
        .set_loader(move |name| template_from_graph(name, g.clone(), default_template.clone()));
    env
}