mod builtins;
pub mod eval;
pub mod lexer;
pub mod module;
pub mod parser;
mod value_ops;
pub use value_ops::{arith_values, compare_values, values_order};
use crate::value::Value;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq)]
pub enum Pattern {
Var(String),
Array(Vec<Pattern>),
Object(Vec<(PatternKey, Pattern)>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatternKey {
Name(String),
Var(String),
Expr(Box<Filter>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Filter {
Identity,
Field(String),
Index(Box<Filter>),
Pipe(Box<Filter>, Box<Filter>),
Iterate,
Select(Box<Filter>),
ObjectConstruct(Vec<(ObjKey, Box<Filter>)>),
ArrayConstruct(Box<Filter>),
Literal(Value),
Compare(Box<Filter>, CmpOp, Box<Filter>),
Arith(Box<Filter>, ArithOp, Box<Filter>),
Comma(Vec<Filter>),
Recurse,
Builtin(String, Vec<Filter>),
Not(Box<Filter>),
BoolOp(Box<Filter>, BoolOp, Box<Filter>),
IfThenElse(Box<Filter>, Box<Filter>, Option<Box<Filter>>),
Alternative(Box<Filter>, Box<Filter>),
Try(Box<Filter>),
StringInterp(Vec<StringPart>),
Neg(Box<Filter>),
TryCatch(Box<Filter>, Box<Filter>),
Slice(Option<Box<Filter>>, Option<Box<Filter>>),
Var(String),
Bind(Box<Filter>, Pattern, Box<Filter>),
Reduce(Box<Filter>, Pattern, Box<Filter>, Box<Filter>),
Foreach(
Box<Filter>,
Pattern,
Box<Filter>,
Box<Filter>,
Option<Box<Filter>>,
),
Assign(Box<Filter>, AssignOp, Box<Filter>),
Def {
name: String,
params: Vec<String>,
body: Box<Filter>,
rest: Box<Filter>,
},
AltBind(Box<Filter>, Vec<Pattern>, Box<Filter>),
Label(String, Box<Filter>),
Break(String),
PostfixIndex(Box<Filter>, Box<Filter>),
PostfixSlice(Box<Filter>, Option<Box<Filter>>, Option<Box<Filter>>),
Import {
path: String,
alias: String,
is_data: bool,
metadata: Option<Value>,
rest: Box<Filter>,
},
Include {
path: String,
metadata: Option<Value>,
rest: Box<Filter>,
},
ModuleDecl { metadata: Value, rest: Box<Filter> },
}
#[derive(Debug, Clone, PartialEq)]
pub enum ObjKey {
Name(String),
Expr(Box<Filter>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CmpOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithOp {
Add,
Sub,
Mul,
Div,
Mod,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssignOp {
Update, Set, Add, Sub, Mul, Div, Mod, Alt, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoolOp {
And,
Or,
}
#[derive(Debug, Clone, PartialEq)]
pub enum StringPart {
Lit(String),
Expr(Filter),
}
#[derive(Debug, Clone)]
pub struct UserFunc {
pub params: Vec<String>,
pub body: Filter,
pub closure_env: Env,
pub is_def: bool,
}
#[derive(Debug, Clone)]
enum VarScope {
Empty,
Cons {
name: String,
value: Value,
parent: Rc<VarScope>,
},
}
impl VarScope {
fn get(&self, target: &str) -> Option<&Value> {
match self {
VarScope::Empty => None,
VarScope::Cons {
name,
value,
parent,
} => {
if name == target {
Some(value)
} else {
parent.get(target)
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct Env {
vars: Rc<VarScope>,
funcs: Rc<HashMap<(String, usize), UserFunc>>,
}
impl Env {
pub fn empty() -> Self {
Env {
vars: Rc::new(VarScope::Empty),
funcs: Rc::new(HashMap::new()),
}
}
pub fn is_empty(&self) -> bool {
matches!(*self.vars, VarScope::Empty)
}
pub fn get_var(&self, name: &str) -> Option<&Value> {
self.vars.get(name)
}
pub fn bind_var(&self, name: String, value: Value) -> Env {
Env {
vars: Rc::new(VarScope::Cons {
name,
value,
parent: self.vars.clone(),
}),
funcs: self.funcs.clone(),
}
}
pub fn bind_func(&self, name: String, arity: usize, func: UserFunc) -> Env {
let mut new_funcs = (*self.funcs).clone();
new_funcs.insert((name, arity), func);
Env {
vars: self.vars.clone(),
funcs: Rc::new(new_funcs),
}
}
pub fn get_func(&self, name: &str, arity: usize) -> Option<&UserFunc> {
self.funcs.get(&(name.to_string(), arity))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PassthroughBuiltin {
Length,
Keys,
KeysUnsorted,
Type,
Has(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PassthroughPath {
Identity,
FieldLength(Vec<String>),
FieldKeys { fields: Vec<String>, sorted: bool },
FieldType(Vec<String>),
FieldHas { fields: Vec<String>, key: String },
ArrayMapField {
prefix: Vec<String>,
fields: Vec<String>,
wrap_array: bool,
},
ArrayMapFieldsObj {
prefix: Vec<String>,
entries: Vec<String>,
wrap_array: bool,
},
ArrayMapBuiltin {
prefix: Vec<String>,
op: PassthroughBuiltin,
wrap_array: bool,
},
}
impl PassthroughPath {
pub fn requires_compact(&self) -> bool {
match self {
PassthroughPath::Identity => true,
PassthroughPath::FieldLength(_) => false,
PassthroughPath::FieldKeys { .. } => false,
PassthroughPath::FieldType(_) => false,
PassthroughPath::FieldHas { .. } => false,
PassthroughPath::ArrayMapField { .. } => true,
PassthroughPath::ArrayMapFieldsObj { .. } => true,
PassthroughPath::ArrayMapBuiltin { .. } => true,
}
}
}
pub fn collect_field_chain(filter: &Filter, fields: &mut Vec<String>) -> bool {
match filter {
Filter::Field(name) => {
fields.push(name.clone());
true
}
Filter::Pipe(a, b) => collect_field_chain(a, fields) && collect_field_chain(b, fields),
_ => false,
}
}
pub(crate) fn decompose_field_builtin(filter: &Filter) -> Option<(Vec<String>, &str)> {
match filter {
Filter::Pipe(lhs, rhs) => {
if let Filter::Builtin(name, args) = rhs.as_ref() {
if !args.is_empty() {
return None;
}
let mut fields = Vec::new();
if collect_field_chain(lhs, &mut fields) {
return Some((fields, name.as_str()));
}
}
None
}
_ => None,
}
}
fn try_simple_obj_shorthand(filter: &Filter) -> Option<Vec<String>> {
let pairs = match filter {
Filter::ObjectConstruct(pairs) => pairs,
_ => return None,
};
if pairs.is_empty() {
return None;
}
let mut entries = Vec::with_capacity(pairs.len());
for (key, val) in pairs {
match (key, val.as_ref()) {
(ObjKey::Name(k), Filter::Field(f)) if k == f => {
entries.push(k.clone());
}
_ => return None,
}
}
Some(entries)
}
fn try_passthrough_builtin(filter: &Filter) -> Option<PassthroughBuiltin> {
match filter {
Filter::Builtin(name, args) => match (name.as_str(), args.as_slice()) {
("length", []) => Some(PassthroughBuiltin::Length),
("keys", []) => Some(PassthroughBuiltin::Keys),
("keys_unsorted", []) => Some(PassthroughBuiltin::KeysUnsorted),
("type", []) => Some(PassthroughBuiltin::Type),
("has", [Filter::Literal(crate::value::Value::String(s))]) => {
Some(PassthroughBuiltin::Has(s.clone()))
}
_ => None,
},
_ => None,
}
}
pub fn passthrough_path(filter: &Filter) -> Option<PassthroughPath> {
match filter {
Filter::Identity => Some(PassthroughPath::Identity),
Filter::Builtin(name, args) => {
match (name.as_str(), args.as_slice()) {
("length", []) => Some(PassthroughPath::FieldLength(vec![])),
("keys", []) => Some(PassthroughPath::FieldKeys {
fields: vec![],
sorted: true,
}),
("keys_unsorted", []) => Some(PassthroughPath::FieldKeys {
fields: vec![],
sorted: false,
}),
("type", []) => Some(PassthroughPath::FieldType(vec![])),
("has", [Filter::Literal(crate::value::Value::String(s))]) => {
Some(PassthroughPath::FieldHas {
fields: vec![],
key: s.clone(),
})
}
("map", [inner]) => {
let mut fields = Vec::new();
if collect_field_chain(inner, &mut fields) && !fields.is_empty() {
return Some(PassthroughPath::ArrayMapField {
prefix: vec![],
fields,
wrap_array: true,
});
}
if let Some(entries) = try_simple_obj_shorthand(inner) {
return Some(PassthroughPath::ArrayMapFieldsObj {
prefix: vec![],
entries,
wrap_array: true,
});
}
if let Some(op) = try_passthrough_builtin(inner) {
return Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec![],
op,
wrap_array: true,
});
}
None
}
_ => None,
}
}
Filter::ArrayConstruct(inner) => {
if let Filter::Pipe(lhs, rhs) = inner.as_ref() {
if matches!(lhs.as_ref(), Filter::Iterate) {
let mut fields = Vec::new();
if collect_field_chain(rhs, &mut fields) && !fields.is_empty() {
return Some(PassthroughPath::ArrayMapField {
prefix: vec![],
fields,
wrap_array: true,
});
}
if let Some(entries) = try_simple_obj_shorthand(rhs) {
return Some(PassthroughPath::ArrayMapFieldsObj {
prefix: vec![],
entries,
wrap_array: true,
});
}
if let Some(op) = try_passthrough_builtin(rhs) {
return Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec![],
op,
wrap_array: true,
});
}
}
if let Filter::Pipe(inner_lhs, inner_rhs) = lhs.as_ref()
&& matches!(inner_rhs.as_ref(), Filter::Iterate)
{
let mut prefix = Vec::new();
if collect_field_chain(inner_lhs, &mut prefix) && !prefix.is_empty() {
let mut fields = Vec::new();
if collect_field_chain(rhs, &mut fields) && !fields.is_empty() {
return Some(PassthroughPath::ArrayMapField {
prefix,
fields,
wrap_array: true,
});
}
if let Some(entries) = try_simple_obj_shorthand(rhs) {
return Some(PassthroughPath::ArrayMapFieldsObj {
prefix,
entries,
wrap_array: true,
});
}
if let Some(op) = try_passthrough_builtin(rhs) {
return Some(PassthroughPath::ArrayMapBuiltin {
prefix,
op,
wrap_array: true,
});
}
}
}
}
None
}
Filter::Pipe(lhs, rhs) => {
if let Some((fields, builtin)) = decompose_field_builtin(filter) {
match builtin {
"length" => return Some(PassthroughPath::FieldLength(fields)),
"keys" => {
return Some(PassthroughPath::FieldKeys {
fields,
sorted: true,
});
}
"keys_unsorted" => {
return Some(PassthroughPath::FieldKeys {
fields,
sorted: false,
});
}
"type" => return Some(PassthroughPath::FieldType(fields)),
_ => {}
}
}
if let Filter::Builtin(name, args) = rhs.as_ref()
&& name == "has"
&& args.len() == 1
&& let Filter::Literal(crate::value::Value::String(key)) = &args[0]
{
let mut fields = Vec::new();
if collect_field_chain(lhs, &mut fields) {
return Some(PassthroughPath::FieldHas {
fields,
key: key.clone(),
});
}
}
if matches!(lhs.as_ref(), Filter::Iterate) {
let mut fields = Vec::new();
if collect_field_chain(rhs, &mut fields) && !fields.is_empty() {
return Some(PassthroughPath::ArrayMapField {
prefix: vec![],
fields,
wrap_array: false,
});
}
if let Some(entries) = try_simple_obj_shorthand(rhs) {
return Some(PassthroughPath::ArrayMapFieldsObj {
prefix: vec![],
entries,
wrap_array: false,
});
}
if let Some(op) = try_passthrough_builtin(rhs) {
return Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec![],
op,
wrap_array: false,
});
}
}
if let Filter::Builtin(name, args) = rhs.as_ref()
&& name == "map"
&& args.len() == 1
{
let mut prefix = Vec::new();
if collect_field_chain(lhs, &mut prefix) && !prefix.is_empty() {
let mut fields = Vec::new();
if collect_field_chain(&args[0], &mut fields) && !fields.is_empty() {
return Some(PassthroughPath::ArrayMapField {
prefix,
fields,
wrap_array: true,
});
}
if let Some(entries) = try_simple_obj_shorthand(&args[0]) {
return Some(PassthroughPath::ArrayMapFieldsObj {
prefix,
entries,
wrap_array: true,
});
}
if let Some(op) = try_passthrough_builtin(&args[0]) {
return Some(PassthroughPath::ArrayMapBuiltin {
prefix,
op,
wrap_array: true,
});
}
}
}
if let Filter::Pipe(inner_lhs, inner_rhs) = lhs.as_ref()
&& matches!(inner_rhs.as_ref(), Filter::Iterate)
{
let mut prefix = Vec::new();
if collect_field_chain(inner_lhs, &mut prefix) && !prefix.is_empty() {
let mut fields = Vec::new();
if collect_field_chain(rhs, &mut fields) && !fields.is_empty() {
return Some(PassthroughPath::ArrayMapField {
prefix,
fields,
wrap_array: false,
});
}
if let Some(entries) = try_simple_obj_shorthand(rhs) {
return Some(PassthroughPath::ArrayMapFieldsObj {
prefix,
entries,
wrap_array: false,
});
}
if let Some(op) = try_passthrough_builtin(rhs) {
return Some(PassthroughPath::ArrayMapBuiltin {
prefix,
op,
wrap_array: false,
});
}
}
}
None
}
_ => None,
}
}
pub fn parse(input: &str) -> anyhow::Result<Filter> {
let tokens = lexer::lex(input)?;
parser::parse(&tokens)
}
impl Filter {
pub fn is_parallel_safe(&self) -> bool {
true
}
}
impl Filter {
pub fn uses_input_builtins(&self) -> bool {
match self {
Filter::Builtin(name, args) => {
if name == "input" || name == "inputs" {
return true;
}
args.iter().any(|f| f.uses_input_builtins())
}
Filter::Identity
| Filter::Iterate
| Filter::Recurse
| Filter::Field(_)
| Filter::Var(_)
| Filter::Literal(_)
| Filter::Break(_) => false,
Filter::Index(f)
| Filter::Select(f)
| Filter::ArrayConstruct(f)
| Filter::Not(f)
| Filter::Try(f)
| Filter::Neg(f) => f.uses_input_builtins(),
Filter::Pipe(a, b)
| Filter::Compare(a, _, b)
| Filter::Arith(a, _, b)
| Filter::BoolOp(a, _, b)
| Filter::Alternative(a, b)
| Filter::TryCatch(a, b)
| Filter::Assign(a, _, b) => a.uses_input_builtins() || b.uses_input_builtins(),
Filter::Bind(a, _, b) | Filter::AltBind(a, _, b) => {
a.uses_input_builtins() || b.uses_input_builtins()
}
Filter::Comma(filters) => filters.iter().any(|f| f.uses_input_builtins()),
Filter::ObjectConstruct(pairs) => pairs.iter().any(|(k, v)| {
(match k {
ObjKey::Name(_) => false,
ObjKey::Expr(f) => f.uses_input_builtins(),
}) || v.uses_input_builtins()
}),
Filter::Slice(s, e) => {
s.as_ref().is_some_and(|f| f.uses_input_builtins())
|| e.as_ref().is_some_and(|f| f.uses_input_builtins())
}
Filter::IfThenElse(c, t, e) => {
c.uses_input_builtins()
|| t.uses_input_builtins()
|| e.as_ref().is_some_and(|f| f.uses_input_builtins())
}
Filter::Reduce(src, _, init, update) => {
src.uses_input_builtins()
|| init.uses_input_builtins()
|| update.uses_input_builtins()
}
Filter::Foreach(src, _, init, update, extract) => {
src.uses_input_builtins()
|| init.uses_input_builtins()
|| update.uses_input_builtins()
|| extract.as_ref().is_some_and(|f| f.uses_input_builtins())
}
Filter::Def { body, rest, .. } => {
body.uses_input_builtins() || rest.uses_input_builtins()
}
Filter::Import { rest, .. }
| Filter::Include { rest, .. }
| Filter::ModuleDecl { rest, .. } => rest.uses_input_builtins(),
Filter::Label(_, body) => body.uses_input_builtins(),
Filter::PostfixIndex(base, idx) => {
base.uses_input_builtins() || idx.uses_input_builtins()
}
Filter::PostfixSlice(base, s, e) => {
base.uses_input_builtins()
|| s.as_ref().is_some_and(|f| f.uses_input_builtins())
|| e.as_ref().is_some_and(|f| f.uses_input_builtins())
}
Filter::StringInterp(parts) => parts.iter().any(|p| match p {
StringPart::Lit(_) => false,
StringPart::Expr(f) => f.uses_input_builtins(),
}),
}
}
}
impl Filter {
pub fn collect_var_refs(&self, out: &mut HashSet<String>) {
match self {
Filter::Var(name) => {
out.insert(name.clone());
}
Filter::Identity
| Filter::Iterate
| Filter::Recurse
| Filter::Field(_)
| Filter::Literal(_)
| Filter::Break(_) => {}
Filter::Index(f)
| Filter::Select(f)
| Filter::ArrayConstruct(f)
| Filter::Not(f)
| Filter::Try(f)
| Filter::Neg(f) => f.collect_var_refs(out),
Filter::Pipe(a, b)
| Filter::Compare(a, _, b)
| Filter::Arith(a, _, b)
| Filter::BoolOp(a, _, b)
| Filter::Alternative(a, b)
| Filter::TryCatch(a, b)
| Filter::Assign(a, _, b) => {
a.collect_var_refs(out);
b.collect_var_refs(out);
}
Filter::Bind(a, pat, b) => {
a.collect_var_refs(out);
collect_pattern_var_refs(pat, out);
b.collect_var_refs(out);
}
Filter::AltBind(expr, pats, body) => {
expr.collect_var_refs(out);
for pat in pats {
collect_pattern_var_refs(pat, out);
}
body.collect_var_refs(out);
}
Filter::Comma(filters) | Filter::Builtin(_, filters) => {
for f in filters {
f.collect_var_refs(out);
}
}
Filter::ObjectConstruct(pairs) => {
for (k, v) in pairs {
if let ObjKey::Expr(f) = k {
f.collect_var_refs(out);
}
v.collect_var_refs(out);
}
}
Filter::Slice(s, e) => {
if let Some(f) = s {
f.collect_var_refs(out);
}
if let Some(f) = e {
f.collect_var_refs(out);
}
}
Filter::IfThenElse(c, t, e) => {
c.collect_var_refs(out);
t.collect_var_refs(out);
if let Some(f) = e {
f.collect_var_refs(out);
}
}
Filter::Reduce(src, pat, init, update) => {
src.collect_var_refs(out);
collect_pattern_var_refs(pat, out);
init.collect_var_refs(out);
update.collect_var_refs(out);
}
Filter::Foreach(src, pat, init, update, extract) => {
src.collect_var_refs(out);
collect_pattern_var_refs(pat, out);
init.collect_var_refs(out);
update.collect_var_refs(out);
if let Some(f) = extract {
f.collect_var_refs(out);
}
}
Filter::Def { body, rest, .. } => {
body.collect_var_refs(out);
rest.collect_var_refs(out);
}
Filter::Import { rest, .. }
| Filter::Include { rest, .. }
| Filter::ModuleDecl { rest, .. } => rest.collect_var_refs(out),
Filter::Label(_, body) => body.collect_var_refs(out),
Filter::PostfixIndex(base, idx) => {
base.collect_var_refs(out);
idx.collect_var_refs(out);
}
Filter::PostfixSlice(base, s, e) => {
base.collect_var_refs(out);
if let Some(f) = s {
f.collect_var_refs(out);
}
if let Some(f) = e {
f.collect_var_refs(out);
}
}
Filter::StringInterp(parts) => {
for p in parts {
if let StringPart::Expr(f) = p {
f.collect_var_refs(out);
}
}
}
}
}
}
pub(crate) fn collect_pattern_var_refs(pat: &Pattern, out: &mut HashSet<String>) {
match pat {
Pattern::Var(name) => {
out.insert(name.clone());
}
Pattern::Array(pats) => {
for p in pats {
collect_pattern_var_refs(p, out);
}
}
Pattern::Object(pairs) => {
for (key, p) in pairs {
if let PatternKey::Var(name) = key {
out.insert(name.clone());
}
if let PatternKey::Expr(f) = key {
f.collect_var_refs(out);
}
collect_pattern_var_refs(p, out);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn passthrough_map_field() {
let f = parse("map(.name)").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapField {
prefix: vec![],
fields: vec!["name".into()],
wrap_array: true,
})
);
}
#[test]
fn passthrough_map_nested_field() {
let f = parse("map(.a.b)").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapField {
prefix: vec![],
fields: vec!["a".into(), "b".into()],
wrap_array: true,
})
);
}
#[test]
fn passthrough_iterate_field() {
let f = parse(".[] | .name").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapField {
prefix: vec![],
fields: vec!["name".into()],
wrap_array: false,
})
);
}
#[test]
fn passthrough_prefix_map_field() {
let f = parse(".statuses | map(.user)").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapField {
prefix: vec!["statuses".into()],
fields: vec!["user".into()],
wrap_array: true,
})
);
}
#[test]
fn passthrough_prefix_iterate_field() {
let f = parse(".statuses[] | .user").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapField {
prefix: vec!["statuses".into()],
fields: vec!["user".into()],
wrap_array: false,
})
);
}
#[test]
fn passthrough_map_complex_not_detected() {
let f = parse("map(.a + .b)").unwrap();
assert_eq!(passthrough_path(&f), None);
}
#[test]
fn passthrough_map_fields_obj() {
let f = parse("map({name, age})").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapFieldsObj {
prefix: vec![],
entries: vec!["name".into(), "age".into()],
wrap_array: true,
})
);
}
#[test]
fn passthrough_iterate_fields_obj() {
let f = parse(".[] | {name, age}").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapFieldsObj {
prefix: vec![],
entries: vec!["name".into(), "age".into()],
wrap_array: false,
})
);
}
#[test]
fn passthrough_prefix_map_fields_obj() {
let f = parse(".data | map({x, y})").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapFieldsObj {
prefix: vec!["data".into()],
entries: vec!["x".into(), "y".into()],
wrap_array: true,
})
);
}
#[test]
fn passthrough_prefix_iterate_fields_obj() {
let f = parse(".data[] | {x, y}").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapFieldsObj {
prefix: vec!["data".into()],
entries: vec!["x".into(), "y".into()],
wrap_array: false,
})
);
}
#[test]
fn passthrough_obj_with_expr_key_not_detected() {
let f = parse("map({(.k): .v})").unwrap();
assert_eq!(passthrough_path(&f), None);
}
#[test]
fn passthrough_obj_renamed_not_detected() {
let f = parse("map({name: .user.name})").unwrap();
assert_eq!(passthrough_path(&f), None);
}
#[test]
fn passthrough_bare_keys_unsorted() {
let f = parse("keys_unsorted").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::FieldKeys {
fields: vec![],
sorted: false,
})
);
}
#[test]
fn passthrough_field_keys_unsorted() {
let f = parse(".data | keys_unsorted").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::FieldKeys {
fields: vec!["data".into()],
sorted: false,
})
);
}
#[test]
fn passthrough_bare_keys_sorted() {
let f = parse("keys").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::FieldKeys {
fields: vec![],
sorted: true,
})
);
}
#[test]
fn passthrough_bare_type() {
let f = parse("type").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::FieldType(vec![]))
);
}
#[test]
fn passthrough_field_type() {
let f = parse(".data | type").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::FieldType(vec!["data".into()]))
);
}
#[test]
fn passthrough_bare_has() {
let f = parse(r#"has("name")"#).unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::FieldHas {
fields: vec![],
key: "name".into(),
})
);
}
#[test]
fn passthrough_field_has() {
let f = parse(r#".data | has("name")"#).unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::FieldHas {
fields: vec!["data".into()],
key: "name".into(),
})
);
}
#[test]
fn passthrough_map_length() {
let f = parse("map(length)").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec![],
op: PassthroughBuiltin::Length,
wrap_array: true,
})
);
}
#[test]
fn passthrough_iterate_type() {
let f = parse(".[] | type").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec![],
op: PassthroughBuiltin::Type,
wrap_array: false,
})
);
}
#[test]
fn passthrough_map_keys() {
let f = parse("map(keys)").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec![],
op: PassthroughBuiltin::Keys,
wrap_array: true,
})
);
}
#[test]
fn passthrough_map_has() {
let f = parse(r#"map(has("x"))"#).unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec![],
op: PassthroughBuiltin::Has("x".into()),
wrap_array: true,
})
);
}
#[test]
fn passthrough_prefix_map_length() {
let f = parse(".items | map(length)").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec!["items".into()],
op: PassthroughBuiltin::Length,
wrap_array: true,
})
);
}
#[test]
fn passthrough_prefix_iterate_type() {
let f = parse(".items[] | type").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec!["items".into()],
op: PassthroughBuiltin::Type,
wrap_array: false,
})
);
}
#[test]
fn passthrough_array_construct_field() {
let f = parse("[.[] | .name]").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapField {
prefix: vec![],
fields: vec!["name".into()],
wrap_array: true,
})
);
}
#[test]
fn passthrough_array_construct_fields_obj() {
let f = parse("[.[] | {name, age}]").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapFieldsObj {
prefix: vec![],
entries: vec!["name".into(), "age".into()],
wrap_array: true,
})
);
}
#[test]
fn passthrough_array_construct_builtin() {
let f = parse("[.[] | length]").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec![],
op: PassthroughBuiltin::Length,
wrap_array: true,
})
);
}
#[test]
fn passthrough_array_construct_prefix_field() {
let f = parse("[.items[] | .name]").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapField {
prefix: vec!["items".into()],
fields: vec!["name".into()],
wrap_array: true,
})
);
}
#[test]
fn passthrough_array_construct_prefix_builtin() {
let f = parse("[.items[] | length]").unwrap();
assert_eq!(
passthrough_path(&f),
Some(PassthroughPath::ArrayMapBuiltin {
prefix: vec!["items".into()],
op: PassthroughBuiltin::Length,
wrap_array: true,
})
);
}
#[test]
fn filter_safety_check() {
assert!(Filter::Identity.is_parallel_safe());
assert!(Filter::Field("name".into()).is_parallel_safe());
assert!(Filter::Literal(Value::Int(42)).is_parallel_safe());
assert!(Filter::Literal(Value::String("hello".into())).is_parallel_safe());
assert!(Filter::Literal(Value::Array(Arc::new(vec![]))).is_parallel_safe());
assert!(Filter::Literal(Value::Object(Arc::new(vec![]))).is_parallel_safe());
assert!(
Filter::Pipe(
Box::new(Filter::Identity),
Box::new(Filter::Literal(Value::Array(Arc::new(vec![])))),
)
.is_parallel_safe()
);
}
}