use std::{collections::HashMap, str::FromStr};
use fasteval::{Compiler, Evaler};
use itertools::Itertools;
use serde_json_path::JsonPath;
use crate::plugin::output::{
default::eval::{config::ExpressionConfig, Operation},
OutputPluginError,
};
pub enum PathSegment {
Key(String),
Index(usize),
}
pub struct CompiledExpression {
pub inputs: Vec<(String, JsonPath)>,
pub expr: String,
pub output_segments: Vec<PathSegment>,
pub slab: fasteval::Slab,
pub compiled: fasteval::Instruction,
}
pub enum CompiledOnFailure {
Interrupt,
Record { segments: Vec<PathSegment> },
Ignore,
}
pub fn compile_expression(
conf: ExpressionConfig,
) -> Result<CompiledExpression, crate::plugin::PluginError> {
let inputs = conf
.inputs
.into_iter()
.map(|(name, path_str)| {
let path = JsonPath::parse(&path_str).map_err(|e| {
crate::plugin::PluginError::BuildFailed(format!(
"invalid JSONPath '{path_str}' for input '{name}': {e}"
))
})?;
Ok((name, path))
})
.collect::<Result<Vec<_>, crate::plugin::PluginError>>()?;
let output_segments = parse_path_segments(&conf.output).map_err(|e| {
crate::plugin::PluginError::BuildFailed(format!(
"invalid output path '{}': {e}",
conf.output
))
})?;
let mut slab = fasteval::Slab::new();
let parsed = fasteval::Parser::new()
.parse(&conf.expr, &mut slab.ps)
.map_err(|e| {
crate::plugin::PluginError::BuildFailed(format!(
"failed to parse expression '{}': {e}",
conf.expr
))
})?;
let compiled = parsed.from(&slab.ps).compile(&slab.ps, &mut slab.cs);
Ok(CompiledExpression {
inputs,
expr: conf.expr,
output_segments,
slab,
compiled,
})
}
pub fn parse_path_segments(path: &str) -> Result<Vec<PathSegment>, OutputPluginError> {
let stripped = path
.strip_prefix("$.")
.or_else(|| path.strip_prefix('$'))
.unwrap_or(path);
if stripped.is_empty() {
return Err(OutputPluginError::OutputPluginFailed(
"output path must not be empty after '$'".to_string(),
));
}
let mut segments = Vec::new();
let mut current_key = String::new();
let mut chars = stripped.chars().peekable();
while let Some(c) = chars.next() {
match c {
'.' => {
if !current_key.is_empty() {
segments.push(PathSegment::Key(std::mem::take(&mut current_key)));
}
}
'[' => {
if !current_key.is_empty() {
segments.push(PathSegment::Key(std::mem::take(&mut current_key)));
}
let mut bracket_content = String::new();
for ic in chars.by_ref() {
if ic == ']' {
break;
}
bracket_content.push(ic);
}
let segment = if (bracket_content.starts_with('\'')
&& bracket_content.ends_with('\''))
|| (bracket_content.starts_with('"') && bracket_content.ends_with('"'))
{
let key = bracket_content[1..bracket_content.len() - 1].to_string();
PathSegment::Key(key)
} else {
let idx: usize = bracket_content.parse().map_err(|_| {
OutputPluginError::OutputPluginFailed(format!(
"invalid bracket segment '[{bracket_content}]' in path '{path}': \
expected an integer index or a quoted string key"
))
})?;
PathSegment::Index(idx)
};
segments.push(segment);
}
other => current_key.push(other),
}
}
if !current_key.is_empty() {
segments.push(PathSegment::Key(current_key));
}
Ok(segments)
}
pub fn set_path(
root: &mut serde_json::Value,
segments: &[PathSegment],
value: serde_json::Value,
) -> Result<(), OutputPluginError> {
match segments {
[] => Err(OutputPluginError::OutputPluginFailed(
"empty output path".to_string(),
)),
[PathSegment::Key(k)] => {
root.as_object_mut()
.ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"cannot write key '{k}' into a non-object JSON value"
))
})?
.insert(k.clone(), value);
Ok(())
}
[PathSegment::Index(i)] => {
let arr = root.as_array_mut().ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"cannot index a non-array JSON value with [{i}]"
))
})?;
if *i < arr.len() {
arr[*i] = value;
Ok(())
} else {
Err(OutputPluginError::OutputPluginFailed(format!(
"array index {i} is out of bounds (length {})",
arr.len()
)))
}
}
[PathSegment::Key(k), rest @ ..] => {
if !root.is_object() {
*root = serde_json::Value::Object(serde_json::Map::new());
}
let child = root
.as_object_mut()
.unwrap()
.entry(k.clone())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
set_path(child, rest, value)
}
[PathSegment::Index(i), rest @ ..] => {
let arr = root.as_array_mut().ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"cannot index a non-array JSON value with [{i}]"
))
})?;
let child = arr.get_mut(*i).ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!("array index {i} is out of bounds"))
})?;
set_path(child, rest, value)
}
}
}
pub fn record_error(
root: &mut serde_json::Value,
segments: &[PathSegment],
expr_str: &str,
message: &str,
) -> Result<(), OutputPluginError> {
let entry = serde_json::json!({ "expr": expr_str, "error": message });
let (last, parent_segments) = match segments.split_last() {
Some(pair) => pair,
None => {
return Err(OutputPluginError::OutputPluginFailed(
"on_failure record path must not be empty".to_string(),
))
}
};
let parent = navigate_mut(root, parent_segments)?;
match last {
PathSegment::Key(k) => {
let obj = parent.as_object_mut().ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"cannot record error under key '{k}' in a non-object value"
))
})?;
let slot = obj
.entry(k.clone())
.or_insert_with(|| serde_json::Value::Array(vec![]));
if let Some(arr) = slot.as_array_mut() {
arr.push(entry);
} else {
*slot = serde_json::Value::Array(vec![entry]);
}
}
PathSegment::Index(i) => {
let arr = parent.as_array_mut().ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"cannot index a non-array JSON value with [{i}] while recording error"
))
})?;
if *i < arr.len() {
if let Some(slot_arr) = arr[*i].as_array_mut() {
slot_arr.push(entry);
} else {
arr[*i] = serde_json::Value::Array(vec![entry]);
}
} else {
return Err(OutputPluginError::OutputPluginFailed(format!(
"array index {i} is out of bounds while recording error"
)));
}
}
}
Ok(())
}
fn navigate_mut<'a>(
root: &'a mut serde_json::Value,
segments: &[PathSegment],
) -> Result<&'a mut serde_json::Value, OutputPluginError> {
let mut current = root;
for seg in segments {
match seg {
PathSegment::Key(k) => {
if !current.is_object() {
*current = serde_json::Value::Object(serde_json::Map::new());
}
current = current
.as_object_mut()
.unwrap()
.entry(k.clone())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
}
PathSegment::Index(i) => {
let arr = current.as_array_mut().ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"cannot index a non-array JSON value with [{i}]"
))
})?;
current = arr.get_mut(*i).ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"array index {i} is out of bounds"
))
})?;
}
}
}
Ok(current)
}
pub fn eval_and_write(
expr: &CompiledExpression,
output: &mut serde_json::Value,
) -> Result<(), OutputPluginError> {
let mut variables: HashMap<String, f64> = HashMap::new();
for (name, path) in &expr.inputs {
let node = path.query(output).exactly_one().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"JSONPath query for input '{name}' did not return exactly one result: {e}"
))
})?;
let f = node.as_f64().ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"input '{name}' is not a number (value: {node})"
))
})?;
variables.insert(name.clone(), f);
}
let mut callback_errors: Vec<String> = vec![];
let eval_result = expr
.compiled
.eval(&expr.slab, &mut |name: &str, args: Vec<f64>| {
if args.is_empty() {
return variables.get(name).copied();
}
let op = match Operation::from_str(name) {
Ok(operation) => Some(operation),
Err(e) => {
callback_errors.push(e);
None
}
}?;
let result = match op.apply(args.as_slice()) {
Ok(result) => Some(result),
Err(e) => {
callback_errors.push(e);
None
}
}?;
Some(result)
})
.map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"error evaluating expression '{}': {e}",
expr.expr
))
});
if !callback_errors.is_empty() {
let msg = callback_errors.into_iter().join("\n");
return Err(OutputPluginError::OutputPluginFailed(format!(
"failure evaluating expression operations: {msg}"
)));
}
let result = eval_result?;
let number = serde_json::Number::from_f64(result).ok_or_else(|| {
OutputPluginError::OutputPluginFailed(format!(
"expression '{}' produced a non-finite value: {result}",
expr.expr
))
})?;
set_path(
output,
&expr.output_segments,
serde_json::Value::Number(number),
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_single_key() {
let segs = parse_path_segments("$.foo").unwrap();
assert_eq!(segs.len(), 1);
assert!(matches!(&segs[0], PathSegment::Key(k) if k == "foo"));
}
#[test]
fn test_parse_nested_keys() {
let segs = parse_path_segments("$.a.b.c").unwrap();
assert_eq!(segs.len(), 3);
assert!(matches!(&segs[0], PathSegment::Key(k) if k == "a"));
assert!(matches!(&segs[1], PathSegment::Key(k) if k == "b"));
assert!(matches!(&segs[2], PathSegment::Key(k) if k == "c"));
}
#[test]
fn test_parse_bracket_single_quoted_string_key() {
let segs = parse_path_segments("$.a['10'].b").unwrap();
assert_eq!(segs.len(), 3);
assert!(matches!(&segs[0], PathSegment::Key(k) if k == "a"));
assert!(matches!(&segs[1], PathSegment::Key(k) if k == "10"));
assert!(matches!(&segs[2], PathSegment::Key(k) if k == "b"));
}
#[test]
fn test_parse_bracket_double_quoted_string_key() {
let segs = parse_path_segments("$.a[\"spaced key\"].b").unwrap();
assert_eq!(segs.len(), 3);
assert!(matches!(&segs[0], PathSegment::Key(k) if k == "a"));
assert!(matches!(&segs[1], PathSegment::Key(k) if k == "spaced key"));
assert!(matches!(&segs[2], PathSegment::Key(k) if k == "b"));
}
#[test]
fn test_parse_bracket_array_index() {
let segs = parse_path_segments("$.arr[2].val").unwrap();
assert_eq!(segs.len(), 3);
assert!(matches!(&segs[0], PathSegment::Key(k) if k == "arr"));
assert!(matches!(&segs[1], PathSegment::Index(2)));
assert!(matches!(&segs[2], PathSegment::Key(k) if k == "val"));
}
#[test]
fn test_parse_bare_dollar_fails() {
assert!(parse_path_segments("$").is_err());
}
#[test]
fn test_set_path_shallow_key() {
let mut root = json!({});
let segs = parse_path_segments("$.result").unwrap();
set_path(&mut root, &segs, json!(42.0)).unwrap();
assert_eq!(root["result"], json!(42.0));
}
#[test]
fn test_set_path_creates_intermediate_objects() {
let mut root = json!({});
let segs = parse_path_segments("$.a.b.c").unwrap();
set_path(&mut root, &segs, json!(99.0)).unwrap();
assert_eq!(root["a"]["b"]["c"], json!(99.0));
}
#[test]
fn test_set_path_overwrites_existing_key() {
let mut root = json!({ "x": 1.0 });
let segs = parse_path_segments("$.x").unwrap();
set_path(&mut root, &segs, json!(2.0)).unwrap();
assert_eq!(root["x"], json!(2.0));
}
#[test]
fn test_set_path_array_index() {
let mut root = json!({ "arr": [10.0, 20.0, 30.0] });
let segs = parse_path_segments("$.arr[1]").unwrap();
set_path(&mut root, &segs, json!(99.0)).unwrap();
assert_eq!(root["arr"][1], json!(99.0));
}
}