trinja 0.7.1

HTML templating / SSG for RDF(S) resources
Documentation
//! Handle objects (RDF resources)

use std::sync::Arc;

use babel47::LangString;
use language_tags::LanguageTag;
use minijinja::{
    State, Value, context,
    value::{Enumerator, Object, ObjectRepr},
};
use taganak_core::{
    graphs::{Graph, GraphView},
    terms::Term,
};
use taganak_framework::prelude::{StreamExt, TryStreamExt};
use taganak_orm::re::GraphError;
use tokio::runtime::Handle;
use tracing::{error, trace};

use crate::{
    Params, ParamsWithEnv,
    env::environment,
    res::{str_to_term, term_to_value},
};

/// A [minijinja] object fulfilling attribute access from RDF predicates
///
/// This object, used as a [minijinja::Value], resolves attributes from an
/// RDF graph by interpreting attribute keys as RDF predicates.
#[derive(Debug, Clone)]
pub struct GraphObject<'e, G> {
    params: ParamsWithEnv<'e, G>,
}

impl<G> GraphObject<'static, G>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
{
    /// Instantiate a new [GraphObject] from an existing (`static`) [taganak_core::graphs::Graph]
    ///
    /// Takes a [taganak_core::graphs::Graph] wrapped in an [Arc] and [RwLock], because [minijinja]
    /// requires shared access in several locations.
    ///
    /// The `subject` is stored as a term, because predicates are resolved lazily on access.
    pub fn new(mut params: Params<'static, G>) -> Self {
        if params.environment.is_none() {
            let param_clone = params.clone().environment(None);
            let env = environment(param_clone, None);
            params = params.environment(Some(env));
        }

        trace!(?params.base, "GraphObject for");

        let params = params.to_with_env().unwrap();
        Self { params }
    }

    /// Render the resource into a [String]
    pub fn into_rendered(self) -> Result<String, minijinja::Error>
    where
        Self: 'static,
    {
        let env = self.params.environment.clone();
        let guard = env.read().unwrap();
        let tmpl = guard
            .get_template(&self.params.base.as_ref().unwrap().to_string())
            .unwrap();
        tmpl.render(context!(
            this => Value::from_object(self),
        ))
    }
}

impl<G> Object for GraphObject<'_, G>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
    Self: 'static,
{
    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
        trace!(
            "Getting attribute {} on {}",
            key,
            self.params.base.as_ref().unwrap()
        );

        let predicate: Option<Term> =
            str_to_term(self.params.clone().to_params(), &key.to_string()).ok();

        if predicate.is_none() {
            trace!(?key, "Requested attribute is not a term");
            match key.as_str() {
                Some("subject") => {
                    return Some(Value::from(self.params.base.as_ref().unwrap().to_string()));
                }
                Some("trans") => {
                    return Some(Value::from_object(TransFn::new(self.params.clone())));
                }
                Some("objects") => {
                    return Some(Value::from_object(ObjectsFn::new(self.params.clone())));
                }
                _ => return None,
            }
        };

        let predicate = predicate.expect("we just checked");

        trace!(?self.params.base, ?predicate, "resolving predicate");

        let handle = Handle::current();
        handle
            .block_on(async {
                let graph = self.params.graph.read().unwrap();
                graph
                    .view()
                    .await
                    .object(Some(self.params.base.as_ref().unwrap()), Some(&predicate))
                    .await
            })
            .ok()?
            .and_then(|t| super::term_to_value(self.params.clone(), &t))
    }

    fn enumerate(self: &Arc<Self>) -> minijinja::value::Enumerator {
        trace!(?self.params.base, "Enumerating predicates");

        let handle = Handle::current();
        let predicates: Vec<Result<String, GraphError>> = handle.block_on(async {
            let graph = self.params.graph.read().unwrap();
            graph
                .view()
                .await
                .predicates(Some(self.params.base.as_ref().unwrap()), None, None)
                .await
                .unwrap()
                .map_ok(|t| t.to_string())
                .collect()
                .await
        });
        trace!(?self.params.base, ?predicates, "found predicates");
        let mut values = Vec::new();
        for res in predicates {
            if let Ok(predicate) = res {
                values.push(Value::from(predicate));
            } else {
                error!(?self.params.base, "Could not iterate ovee");
                return Enumerator::NonEnumerable;
            }
        }

        Enumerator::Values(values)
    }

    #[track_caller]
    fn render(self: &Arc<Self>, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
    where
        Self: Sized + 'static,
    {
        tracing::trace!(caller = %core::panic::Location::caller(),?self.params.base,"render called");
        let globals = self
            .params
            .environment
            .read()
            .unwrap()
            .globals()
            .map(|(n, _)| n)
            .fold(String::new(), |mut acc, n| {
                acc.push_str(n);
                acc.push_str(", ");
                acc
            });

        let read_lock = self.params.environment.read().unwrap();
        let tmpl = read_lock
            .get_template(&self.params.base.as_ref().unwrap().to_string())
            .unwrap();
        let self_clone = GraphObject {
            params: self.params.clone(),
        };
        let rendered = tmpl
            .render(context!(
                this => Value::from_object(self_clone),
            ))
            .unwrap();
        write!(f, "{}", rendered)
    }
}

#[derive(Debug, Clone)]
pub struct TransFn<'e, G> {
    params: ParamsWithEnv<'e, G>,
}

impl<'e, G> TransFn<'e, G>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
{
    pub fn new(params: ParamsWithEnv<'e, G>) -> Self {
        Self { params }
    }
}

impl<G> Object for TransFn<'_, G>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
    Self: 'static,
{
    fn call(
        self: &Arc<Self>,
        _state: &State<'_, '_>,
        args: &[Value],
    ) -> Result<Value, minijinja::Error> {
        if args.is_empty() {
            return Err(minijinja::Error::new(
                minijinja::ErrorKind::MissingArgument,
                "needs predicate",
            ));
        }

        if args.len() > 2 {
            return Err(minijinja::Error::new(
                minijinja::ErrorKind::TooManyArguments,
                "too many arguments",
            ));
        }

        let language: Option<LanguageTag> = if args.len() == 2 {
            Some(args[1].as_str().unwrap().parse().unwrap())
        } else {
            self.params.language.clone()
        };

        let handle = Handle::current();
        handle.block_on(async {
            let predicate =
                str_to_term(self.params.clone().to_params(), &args[0].to_string()).unwrap();

            trace!(
                "Called trans on {} for predicate {}",
                self.params.base.as_ref().unwrap(),
                predicate
            );

            let lang_string = {
                let graph = self.params.graph.read().unwrap();
                let view = graph.view().await;
                LangString::from_graph(view, self.params.base.as_ref().unwrap(), &predicate)
                    .await
                    .unwrap()
            };

            Ok(Value::from(lang_string.get(language.as_ref())))
        })
    }
}

#[derive(Debug, Clone)]
pub struct ObjectsFn<'e, G> {
    params: ParamsWithEnv<'e, G>,
}

impl<'a, G> ObjectsFn<'a, G> {
    pub fn new(params: ParamsWithEnv<'a, G>) -> Self {
        Self { params }
    }
}

impl<G> Object for ObjectsFn<'_, G>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
    Self: 'static,
{
    fn call(
        self: &Arc<Self>,
        state: &State<'_, '_>,
        args: &[Value],
    ) -> Result<Value, minijinja::Error> {
        if args.is_empty() {
            return Err(minijinja::Error::new(
                minijinja::ErrorKind::MissingArgument,
                "needs a predicate",
            ));
        }

        if args.len() > 1 {
            return Err(minijinja::Error::new(
                minijinja::ErrorKind::TooManyArguments,
                "too many arguments",
            ));
        }

        trace!(
            "called objects on {} for predicate {}",
            self.params.base.as_ref().unwrap(),
            &args[0].to_string()
        );

        let handle = Handle::current();

        handle.block_on(async {
            let predicate =
                str_to_term(self.params.clone().to_params(), &args[0].to_string()).unwrap();
            let graph = self.params.graph.read().unwrap();
            const LIMIT: Option<usize> = Some(256);
            let terms: Vec<Arc<Term>> = graph
                .view()
                .await
                .objects(
                    Some(self.params.base.as_ref().unwrap()),
                    Some(&predicate),
                    LIMIT,
                )
                .await
                .unwrap()
                .try_collect()
                .await
                .unwrap();

            tracing::trace!(?terms, "found terms");
            Ok(Value::from_object(TermList::new(
                self.params.clone(),
                terms,
            )))
        })
    }
}

#[derive(Debug, Clone)]
pub(crate) struct TermList<'e, G> {
    params: ParamsWithEnv<'e, G>,
    terms: Vec<Arc<Term>>,
}

impl<'e, G> TermList<'e, G>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
{
    pub(crate) fn new(params: ParamsWithEnv<'e, G>, terms: Vec<Arc<Term>>) -> Self {
        trace!(?params.graph, ?params.base, ?terms,  "TermList for, containing terms");
        Self { params, terms }
    }
}

impl<G> Object for TermList<'static, G>
where
    G: Graph + std::fmt::Debug + Sync + 'static,
    Self: 'static,
{
    fn repr(self: &Arc<Self>) -> ObjectRepr {
        ObjectRepr::Iterable
    }

    fn enumerate(self: &Arc<Self>) -> Enumerator {
        trace!(?self.params.base, "starting iteration from");
        let self_clone = self.clone();
        let terms = self_clone.terms.clone();
        let params = self_clone.params.clone();
        Enumerator::Iter(Box::new(
            terms
                .into_iter()
                .map(move |t| term_to_value(params.clone(), &t).unwrap()),
        ))
    }
}