use crate::compiler::prelude::*;
use std::sync::LazyLock;
static DEFAULT_COMPACT: LazyLock<Value> = LazyLock::new(|| Value::Boolean(false));
static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
vec![
Parameter::required("target", kind::ANY, "The path of the field to delete"),
Parameter::optional(
"compact",
kind::BOOLEAN,
"After deletion, if `compact` is `true` and there is an empty object or array left,
the empty object or array is also removed, cascading up to the root. This only
applies to the path being deleted, and any parent paths.",
)
.default(&DEFAULT_COMPACT),
]
});
#[inline]
fn del(query: &expression::Query, compact: bool, ctx: &mut Context) -> Resolved {
let path = query.path();
if let Some(target_path) = query.external_path() {
Ok(ctx
.target_mut()
.target_remove(&target_path, compact)
.ok()
.flatten()
.unwrap_or(Value::Null))
} else if let Some(ident) = query.variable_ident() {
match ctx.state_mut().variable_mut(ident) {
Some(value) => {
let new_value = value.get(path).cloned();
value.remove(path, compact);
Ok(new_value.unwrap_or(Value::Null))
}
None => Ok(Value::Null),
}
} else if let Some(expr) = query.expression_target() {
let value = expr.resolve(ctx)?;
Ok(value.get(path).cloned().unwrap_or(Value::Null))
} else {
Ok(Value::Null)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Del;
impl Function for Del {
fn identifier(&self) -> &'static str {
"del"
}
fn usage(&self) -> &'static str {
indoc! {"
Removes the field specified by the static `path` from the target.
For dynamic path deletion, see the `remove` function.
"}
}
fn category(&self) -> &'static str {
Category::Path.as_ref()
}
fn return_kind(&self) -> u16 {
kind::ANY
}
fn return_rules(&self) -> &'static [&'static str] {
&[
"Returns the value of the field being deleted. Returns `null` if the field doesn't exist.",
]
}
fn notices(&self) -> &'static [&'static str] {
&[
"The `del` function _modifies the current event in place_ and returns the value of the deleted field.",
]
}
fn pure(&self) -> bool {
false
}
fn parameters(&self) -> &'static [Parameter] {
PARAMETERS.as_slice()
}
fn examples(&self) -> &'static [Example] {
&[
example! {
title: "Delete a field",
source: indoc! {r#"
. = { "foo": "bar" }
del(.foo)
"#},
result: Ok("bar"),
},
example! {
title: "Rename a field",
source: indoc! {r#"
. = { "old": "foo" }
.new = del(.old)
.
"#},
result: Ok(r#"{ "new": "foo" }"#),
},
example! {
title: "Returns null for unknown field",
source: r#"del({"foo": "bar"}.baz)"#,
result: Ok("null"),
},
example! {
title: "External target",
source: indoc! {r#"
. = { "foo": true, "bar": 10 }
del(.foo)
.
"#},
result: Ok(r#"{ "bar": 10 }"#),
},
example! {
title: "Delete field from variable",
source: indoc! {r#"
var = { "foo": true, "bar": 10 }
del(var.foo)
var
"#},
result: Ok(r#"{ "bar": 10 }"#),
},
example! {
title: "Delete object field",
source: indoc! {r#"
var = { "foo": {"nested": true}, "bar": 10 }
del(var.foo.nested, false)
var
"#},
result: Ok(r#"{ "foo": {}, "bar": 10 }"#),
},
example! {
title: "Compact object field",
source: indoc! {r#"
var = { "foo": {"nested": true}, "bar": 10 }
del(var.foo.nested, true)
var
"#},
result: Ok(r#"{ "bar": 10 }"#),
},
]
}
fn compile(
&self,
_state: &state::TypeState,
ctx: &mut FunctionCompileContext,
arguments: ArgumentList,
) -> Compiled {
let query = arguments.required_query("target")?;
let compact = arguments.optional("compact");
if let Some(target_path) = query.external_path()
&& ctx.is_read_only_path(&target_path)
{
return Err(function::Error::ReadOnlyMutation {
context: format!("{query} is read-only, and cannot be deleted"),
}
.into());
}
Ok(Box::new(DelFn { query, compact }))
}
}
#[derive(Debug, Clone)]
pub(crate) struct DelFn {
query: expression::Query,
compact: Option<Box<dyn Expression>>,
}
impl DelFn {
#[cfg(test)]
fn new(path: &str) -> Self {
use crate::path::{PathPrefix, parse_value_path};
Self {
query: expression::Query::new(
expression::Target::External(PathPrefix::Event),
parse_value_path(path).unwrap(),
),
compact: None,
}
}
}
impl Expression for DelFn {
fn resolve(&self, ctx: &mut Context) -> Resolved {
let compact = self
.compact
.map_resolve_with_default(ctx, || DEFAULT_COMPACT.clone())?
.try_boolean()?;
del(&self.query, compact, ctx)
}
fn type_info(&self, state: &state::TypeState) -> TypeInfo {
let mut state = state.clone();
let return_type = self.query.apply_type_info(&mut state).impure();
let compact: Option<bool> = self
.compact
.as_ref()
.and_then(|compact| compact.resolve_constant(&state))
.and_then(|compact| compact.as_boolean());
if let Some(compact) = compact {
self.query.delete_type_def(&mut state.external, compact);
} else {
let mut false_result = state.external.clone();
self.query.delete_type_def(&mut false_result, false);
let mut true_result = state.external.clone();
self.query.delete_type_def(&mut true_result, true);
state.external = false_result.merge(true_result);
}
TypeInfo::new(state, return_type)
}
}
impl fmt::Display for DelFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::btreemap;
use crate::value;
#[test]
fn del() {
let cases = vec![
(
btreemap! { "exists" => "value" },
Ok(value!("value")),
DelFn::new("exists"),
),
(
btreemap! { "exists" => "value" },
Ok(value!(null)),
DelFn::new("does_not_exist"),
),
(
btreemap! { "exists" => value!([1, 2, 3]) },
Ok(value!([1, 2, 3])),
DelFn::new("exists"),
),
(
btreemap! { "exists" => value!(null) },
Ok(value!(null)),
DelFn::new("exists"),
),
(
btreemap! {"exists" => btreemap! { "foo" => "bar" }},
Ok(value!(btreemap! {"foo" => "bar" })),
DelFn::new("exists"),
),
(
btreemap! { "exists" => 127 },
Ok(value!(127)),
DelFn::new("exists"),
),
(
btreemap! {"exists" => value!([1, 2, 3]) },
Ok(value!(2)),
DelFn::new(".exists[1]"),
),
];
let tz = TimeZone::default();
for (object, exp, func) in cases {
let mut object: Value = object.into();
let mut runtime_state = state::RuntimeState::default();
let mut ctx = Context::new(&mut object, &mut runtime_state, &tz);
let got = func
.resolve(&mut ctx)
.map_err(|e| format!("{:#}", anyhow::anyhow!(e)));
assert_eq!(got, exp);
}
}
}