use crate::base::{IssueCode, Result};
use crate::expr::{CompileCx, ConfigField, Expr, FieldKind, Path, Resolver, ValueKind, Visit};
use crate::function::SyncCtx;
use datavalue::OwnedDataValue;
use serde_json::Value;
use std::borrow::Cow;
use std::str::FromStr;
fn compile_path<T>(
raw: &Value,
cx: &mut CompileCx<'_>,
resolve: fn(&str) -> Result<T>,
) -> Option<(Expr<String>, Option<T>)> {
if matches!(raw, Value::Null | Value::Bool(_) | Value::Number(_)) {
cx.issue(
IssueCode::InputParse,
format!("`{raw}` must be a path string, or JSONLogic computing one"),
);
return None;
}
let expr = <Expr<String>>::compile(raw, cx)?;
let Some(text) = expr.constant() else {
return Some((expr, None));
};
if !raw.is_string() && !(raw.is_object() && folds_to_string(expr.logic(), cx)) {
cx.issue(
IssueCode::InputParse,
format!("`{raw}` must be a path string, or JSONLogic computing one"),
);
return None;
}
match resolve(text) {
Ok(resolved) => Some((expr, Some(resolved))),
Err(error) => {
let code = if raw.is_string() {
IssueCode::InvalidPath
} else {
IssueCode::InputParse
};
cx.issue(code, error.message().to_owned());
None
}
}
}
fn folds_to_string(logic: &datalogic_rs::Logic, cx: &CompileCx<'_>) -> bool {
crate::expr::arena::with_arena(|arena| {
cx.exprs()
.logic()
.evaluate(logic, datavalue::DataValue::Object(&[]), arena)
.is_ok_and(|v| matches!(v, datavalue::DataValue::String(_)))
})
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum ParseFrom {
Payload,
PayloadPath(Path),
Context(Path),
}
impl ParseFrom {
pub(crate) fn parse(s: &str) -> Result<Self> {
if s == "payload" || s == "payload." {
return Ok(Self::Payload);
}
if let Some(root) = s.strip_suffix('.')
&& crate::base::ROOTS.contains(&root)
{
return Path::from_str(root).map(Self::Context);
}
if let Some(rest) = s.strip_prefix("payload.") {
return Path::from_str(&format!("data.{rest}")).map(Self::PayloadPath);
}
Path::from_str(s).map(Self::Context)
}
pub(crate) fn text(&self) -> String {
match self {
Self::Payload => "payload".to_owned(),
Self::PayloadPath(path) => {
let dotted = path.dotted();
format!("payload.{}", dotted.strip_prefix("data.").unwrap_or(dotted))
}
Self::Context(path) => path.dotted().to_string(),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ParseSource {
Literal(ParseFrom),
Folded {
raw: Value,
from: ParseFrom,
},
Computed(Expr<String>),
}
impl ParseSource {
pub(crate) fn resolve<'p>(
&'p self,
cx: &mut SyncCtx<'_, '_, 'p>,
) -> Result<Cow<'p, ParseFrom>> {
match self {
Self::Literal(from) | Self::Folded { from, .. } => Ok(Cow::Borrowed(from)),
Self::Computed(expr) => ParseFrom::parse(&cx.eval(expr)?).map(Cow::Owned),
}
}
}
impl ConfigField for ParseSource {
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
let (expr, resolved) = compile_path(raw, cx, ParseFrom::parse)?;
Some(match resolved {
Some(from) if raw.is_string() => Self::Literal(from),
Some(from) => Self::Folded {
raw: raw.clone(),
from,
},
None => Self::Computed(expr),
})
}
fn visit(&self, v: &mut Visit<'_>) {
match self {
Self::Literal(from) => v.expr(&Value::from(from.text())),
Self::Folded { raw, .. } => v.expr(raw),
Self::Computed(expr) => v.expr(expr.raw()),
}
}
fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
match self {
Self::Literal(from) | Self::Folded { from, .. } => {
Ok(Some(OwnedDataValue::String(from.text())))
}
Self::Computed(expr) => ConfigField::resolve(expr, r),
}
}
fn kind() -> FieldKind {
FieldKind::Expr(ValueKind::String)
}
}
#[derive(Debug)]
pub struct DataPath {
expr: Expr<String>,
constant: Option<Path>,
}
impl DataPath {
#[cfg(test)]
pub fn constant(&self) -> Option<&Path> {
self.constant.as_ref()
}
#[cfg(test)]
pub(crate) fn expr(&self) -> &Expr<String> {
&self.expr
}
pub(crate) fn path<'p>(&'p self, cx: &mut SyncCtx<'_, '_, 'p>) -> Result<Cow<'p, Path>> {
match &self.constant {
Some(path) => Ok(Cow::Borrowed(path)),
None => Self::resolve(&cx.eval(&self.expr)?).map(Cow::Owned),
}
}
pub(crate) fn resolve(s: &str) -> Result<Path> {
Path::from_str(&format!("data.{s}"))
}
}
impl ConfigField for DataPath {
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
let (expr, constant) = compile_path(raw, cx, Self::resolve)?;
Some(Self { expr, constant })
}
fn visit(&self, v: &mut Visit<'_>) {
v.expr(self.expr.raw());
}
fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
ConfigField::resolve(&self.expr, r)
}
fn kind() -> FieldKind {
FieldKind::Expr(ValueKind::String)
}
}
#[derive(Debug)]
pub struct PublishSource {
path: DataPath,
fallback: Option<Path>,
}
impl PublishSource {
#[cfg(test)]
pub fn constant(&self) -> Option<&Path> {
self.path.constant()
}
#[cfg(test)]
pub fn fallback(&self) -> Option<&Path> {
self.fallback.as_ref()
}
#[cfg(test)]
pub(crate) fn expr(&self) -> &Expr<String> {
self.path.expr()
}
pub(crate) fn paths<'p>(
&'p self,
cx: &mut SyncCtx<'_, '_, 'p>,
) -> Result<(Cow<'p, Path>, Option<Cow<'p, Path>>)> {
match &self.path.constant {
Some(path) => Ok((
Cow::Borrowed(path),
self.fallback.as_ref().map(Cow::Borrowed),
)),
None => {
let (path, fallback) = Self::resolve(&cx.eval(&self.path.expr)?)?;
Ok((Cow::Owned(path), fallback.map(Cow::Owned)))
}
}
}
pub(crate) fn resolve(s: &str) -> Result<(Path, Option<Path>)> {
let primary = DataPath::resolve(s)?;
let fallback = match s.strip_prefix("data.") {
_ if s.is_empty() => Some(primary.prefix(1)),
Some("") => Some(primary.prefix(1)),
Some(rest) => DataPath::resolve(rest).ok(),
None => None,
};
Ok((primary, fallback))
}
}
impl ConfigField for PublishSource {
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
let (expr, resolved) = compile_path(raw, cx, Self::resolve)?;
let (constant, fallback) =
resolved.map_or((None, None), |(path, fallback)| (Some(path), fallback));
Some(Self {
path: DataPath { expr, constant },
fallback,
})
}
fn visit(&self, v: &mut Visit<'_>) {
self.path.visit(v);
}
fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
ConfigField::resolve(&self.path, r)
}
fn kind() -> FieldKind {
FieldKind::Expr(ValueKind::String)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr::ExprEngine;
use serde_json::json;
fn field<T: ConfigField>(raw: &Value) -> T {
let engine = ExprEngine::default();
let mut issues = Vec::new();
let value = {
let mut cx = CompileCx::new(&engine, &mut issues);
T::compile(raw, &mut cx)
};
assert!(issues.is_empty(), "{raw}: {issues:?}");
value.expect("compiles")
}
fn reported(field: &impl ConfigField) -> Vec<Value> {
struct Seen(Vec<Value>);
impl crate::expr::ExprVisitor for Seen {
fn expr(&mut self, _: &crate::base::AuthoredPath, raw: &Value, _: bool) {
self.0.push(raw.clone());
}
}
let mut seen = Seen(Vec::new());
field.visit(&mut Visit::new(
&mut seen,
crate::base::AuthoredPath::root(),
));
seen.0
}
#[test]
fn every_context_roots_trailing_dot_names_the_root() {
for (text, root) in [
("data.", "data"),
("metadata.", "metadata"),
("temp_data.", "temp_data"),
] {
assert!(
matches!(
ParseFrom::parse(text).unwrap(),
ParseFrom::Context(path) if path.to_string() == root
),
"{text}"
);
}
assert!(matches!(
ParseFrom::parse("payload.").unwrap(),
ParseFrom::Payload
));
}
#[test]
fn a_folded_computed_path_resolves_at_compile() {
let source = json!({"cat": ["payload.", "x.y"]});
let parse = field::<ParseSource>(&source);
assert!(
matches!(
&parse,
ParseSource::Folded { raw, from: ParseFrom::PayloadPath(p) }
if raw == &source && p.to_string() == "data.x.y"
),
"{parse:?}"
);
assert_eq!(reported(&parse), [source]);
let target = json!({"cat": ["or", "ders"]});
let data = field::<DataPath>(&target);
assert_eq!(
data.constant().map(ToString::to_string).as_deref(),
Some("data.orders")
);
assert_eq!(reported(&data), [target]);
let origin = json!({"cat": ["data.", "obj"]});
let publish = field::<PublishSource>(&origin);
assert_eq!(
publish.constant().map(ToString::to_string).as_deref(),
Some("data.data.obj")
);
assert_eq!(
publish.fallback().map(ToString::to_string).as_deref(),
Some("data.obj")
);
assert_eq!(reported(&publish), [origin]);
let computed = json!({"cat": ["payload.", {"var": "data.k"}]});
let parse = field::<ParseSource>(&computed);
assert!(matches!(parse, ParseSource::Computed(_)), "{parse:?}");
assert_eq!(reported(&parse), std::slice::from_ref(&computed));
assert!(field::<DataPath>(&computed).constant().is_none());
assert!(field::<PublishSource>(&computed).constant().is_none());
}
}