use crate::engine::error::Result;
use crate::engine::functions::template::{Template, TemplateCompiler};
use crate::engine::task_context::TaskContext;
use crate::engine::utils::compute_data_path;
use serde::{Deserialize, Deserializer};
use serde_json::Value;
use std::borrow::Cow;
use std::marker::PhantomData;
use std::sync::Arc;
#[derive(Clone, Copy)]
pub struct ParamCtx<'a> {
engine: &'a datalogic_rs::Engine,
context: datavalue::DataValue<'a>,
arena: &'a datalogic_rs::bumpalo::Bump,
}
impl<'a> ParamCtx<'a> {
pub(crate) fn new(
engine: &'a datalogic_rs::Engine,
context: datavalue::DataValue<'a>,
arena: &'a datalogic_rs::bumpalo::Bump,
) -> Self {
Self {
engine,
context,
arena,
}
}
pub(crate) fn from_arena(
engine: &'a datalogic_rs::Engine,
arena_ctx: &crate::engine::executor::ArenaContext<'a>,
) -> Self {
Self::new(engine, arena_ctx.as_data_value(), arena_ctx.arena())
}
pub(crate) fn engine(&self) -> &'a datalogic_rs::Engine {
self.engine
}
pub(crate) fn context(&self) -> &datavalue::DataValue<'a> {
&self.context
}
pub(crate) fn arena(&self) -> &'a datalogic_rs::bumpalo::Bump {
self.arena
}
}
pub type ResolvedPath = (Arc<str>, Arc<[Arc<str>]>);
pub trait PathRoot: Default + Clone + Copy + std::fmt::Debug {
fn compute(path: &str) -> ResolvedPath;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ContextRoot;
impl PathRoot for ContextRoot {
fn compute(path: &str) -> ResolvedPath {
(
Arc::from(path),
path.split('.').map(Arc::from).collect::<Vec<_>>().into(),
)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DataRoot;
impl PathRoot for DataRoot {
fn compute(path: &str) -> ResolvedPath {
compute_data_path(path)
}
}
#[derive(Debug, Clone)]
pub struct PathTemplate<R: PathRoot = ContextRoot> {
template: Template,
precomputed: Option<ResolvedPath>,
root: PhantomData<R>,
}
impl<'de, R: PathRoot> Deserialize<'de> for PathTemplate<R> {
fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
Ok(Self::from_template(Template::deserialize(d)?))
}
}
impl<R: PathRoot> Default for PathTemplate<R> {
fn default() -> Self {
Self::from_template(Template::from(Value::String(String::new())))
}
}
impl<R: PathRoot> From<Value> for PathTemplate<R> {
fn from(raw: Value) -> Self {
Self::from_template(Template::from(raw))
}
}
impl<R: PathRoot> From<&str> for PathTemplate<R> {
fn from(path: &str) -> Self {
Self::from(Value::String(path.to_string()))
}
}
impl<R: PathRoot> PathTemplate<R> {
fn from_template(template: Template) -> Self {
Self {
template,
precomputed: None,
root: PhantomData,
}
}
pub fn compile(&mut self, c: &TemplateCompiler, label: &str) -> Result<()> {
self.template.compile(c, label)?;
self.precomputed = self.template.constant_string().map(|s| R::compute(&s));
Ok(())
}
pub fn resolve(&self, ctx: &TaskContext<'_>) -> Result<ResolvedPath> {
if let Some((dotted, parts)) = &self.precomputed {
return Ok((Arc::clone(dotted), Arc::clone(parts)));
}
if !self.template.is_compiled() {
if let Value::String(s) = self.template.as_json() {
return Ok(R::compute(s));
}
}
Ok(R::compute(&self.template.resolve_string(ctx)?))
}
pub(crate) fn resolve_in_arena(&self, p: ParamCtx<'_>) -> Result<Cow<'_, ResolvedPath>> {
if let Some(pair) = &self.precomputed {
return Ok(Cow::Borrowed(pair));
}
if !self.template.is_compiled() {
if let Value::String(s) = self.template.as_json() {
return Ok(Cow::Owned(R::compute(s)));
}
}
Ok(Cow::Owned(R::compute(
&self.template.resolve_str_in_arena(p)?,
)))
}
pub fn as_json(&self) -> &Value {
self.template.as_json()
}
pub fn is_constant(&self) -> bool {
self.precomputed.is_some()
}
pub fn constant_path(&self) -> Option<&str> {
self.precomputed.as_ref().map(|(dotted, _)| &**dotted)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::compiler::datalogic_engine_builder;
use crate::engine::message::Message;
use crate::engine::utils::compute_path_parts;
use serde_json::json;
fn compiler() -> TemplateCompiler {
TemplateCompiler::new(Arc::new(datalogic_engine_builder().build()))
}
fn parts_of(parts: &Arc<[Arc<str>]>) -> Vec<String> {
parts.iter().map(|p| p.to_string()).collect()
}
#[test]
fn a_static_context_path_is_precomputed_and_split_as_before() {
let mut p: PathTemplate<ContextRoot> = PathTemplate::from("data.user.name");
p.compile(&compiler(), "lbl").unwrap();
assert!(p.is_constant(), "a literal path must fold to a constant");
assert_eq!(p.constant_path(), Some("data.user.name"));
let mut m = Message::from_value(&json!({}));
let dl = Arc::new(datalogic_engine_builder().build());
let ctx = TaskContext::new(&mut m, &dl);
let (dotted, parts) = p.resolve(&ctx).unwrap();
assert_eq!(&*dotted, "data.user.name");
assert_eq!(parts_of(&parts), ["data", "user", "name"]);
}
#[test]
fn a_static_data_rooted_target_gains_the_data_prefix() {
let mut p: PathTemplate<DataRoot> = PathTemplate::from("orders");
p.compile(&compiler(), "lbl").unwrap();
assert_eq!(p.constant_path(), Some("data.orders"));
let (dotted, parts) = compute_data_path("orders");
assert_eq!(p.constant_path(), Some(&*dotted));
assert_eq!(parts_of(&parts), ["data", "orders"]);
}
#[test]
fn a_dynamic_path_resolves_against_the_message() {
let dl = Arc::new(datalogic_engine_builder().build());
let mut p: PathTemplate<ContextRoot> =
PathTemplate::from(json!({"cat": ["data.accounts.", {"var": "data.id"}, ".balance"]}));
p.compile(&compiler(), "lbl").unwrap();
assert!(!p.is_constant());
assert_eq!(p.constant_path(), None, "a dynamic path names no one place");
let mut m = Message::from_value(&json!({}));
crate::engine::utils::set_nested_value(
&mut m.context,
"data.id",
datavalue::OwnedDataValue::String("ACC7".to_string()),
);
let ctx = TaskContext::new(&mut m, &dl);
let (dotted, parts) = p.resolve(&ctx).unwrap();
assert_eq!(&*dotted, "data.accounts.ACC7.balance");
assert_eq!(parts_of(&parts), ["data", "accounts", "ACC7", "balance"]);
}
#[test]
fn an_uncompiled_literal_still_resolves_for_directly_built_configs() {
let dl = Arc::new(datalogic_engine_builder().build());
let p: PathTemplate<ContextRoot> = PathTemplate::from("temp_data.x");
let mut m = Message::from_value(&json!({}));
let ctx = TaskContext::new(&mut m, &dl);
let (dotted, parts) = p.resolve(&ctx).unwrap();
assert_eq!(&*dotted, "temp_data.x");
assert_eq!(parts_of(&parts), ["temp_data", "x"]);
}
#[test]
fn a_hash_prefixed_segment_survives_the_split() {
let mut p: PathTemplate<ContextRoot> = PathTemplate::from("data.rows.#20.total");
p.compile(&compiler(), "lbl").unwrap();
let dl = Arc::new(datalogic_engine_builder().build());
let mut m = Message::from_value(&json!({}));
let ctx = TaskContext::new(&mut m, &dl);
let (_, parts) = p.resolve(&ctx).unwrap();
assert_eq!(parts_of(&parts), ["data", "rows", "#20", "total"]);
}
#[test]
fn compute_path_parts_is_the_shared_split_for_data_rooting() {
let (_, parts) = DataRoot::compute("a.b");
assert_eq!(
parts_of(&parts),
parts_of(&compute_path_parts("data", "a.b"))
);
}
}