diff --git a/README.md b/README.md index 67bd707..5af2e5e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,15 @@ [](https://jetro.io) ![GitHub](https://img.shields.io/github/license/mitghi/jetro) -Jetro is a tool with custom DSL for transforming, querying and comparing data in JSON format. +Jetro is a library which provides a custom DSL for transforming, querying and comparing data in JSON format. It is simple to use and extend. Jetro has minimal dependency, the traversal and eval algorithm is implemented on top of [serde_json](https://serde.rs). +Jetro can be used inside Web Browser by compiling down to WASM. Visit [Jetro Web](https://jetro.io) +to try it online, or [clone it](https://github.com/mitghi/jetroweb) and give it a shot. + +Jetro can be used in command line using [Jetrocli](https://github.com/mitghi/jetrocli). + ```rust let data = serde_json::json!({ "name": "mr snuggle", @@ -34,12 +39,13 @@ struct Output { let output: Option = values.from_index(0); ``` -Jetro can be used in Web Browser by compiling down to WASM. Visit [Jetro Web](https://jetro.io) -to try it online. +# architecture + +Jetro consists of a parser, context wrapper which manages traversal and evaluation of each step of user input and a runtime for dynamic functions. The future version will support user-defined functions. # example -Jetro combines access path with functions which operate on those values matched within the pipeline. +Jetro combines access path with functions which operate on values matched within the pipeline. By convention, functions are denoted using # operator. Functions can be composed. @@ -53,6 +59,7 @@ By convention, functions are denoted using # operator. Functions can be composed | #sum | Sum of numbers | | #formats('format with placeholder {} {}', 'key_a', 'key_b') [ -> \| ->* 'binding_value' ] | Insert formatted key:value into object or return it as single key:value | | #filter('target_key' (>, <, >=, <=, ==, ~=, !=) (string, boolean, number)) | Perform Filter on list | +| #map(x: x.y.z \| x.y.z.some_method())| Map each item in a list with the given lambda | ```json { diff --git a/src/context.rs b/src/context.rs index 7f9fd38..a49566a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -232,6 +232,28 @@ pub(crate) struct Context<'a> { pub results: Rc>>>, } +#[derive(Debug, PartialEq, Clone)] +pub enum MapBody { + None, + Method { name: String, subpath: Vec }, + Subpath(Vec), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct MapAST { + pub arg: String, + pub body: MapBody, +} + +impl Default for MapAST { + fn default() -> Self { + Self { + arg: String::from(""), + body: MapBody::None, + } + } +} + #[derive(Debug)] pub enum Error { EmptyQuery, @@ -247,6 +269,7 @@ pub enum FuncArg { Key(String), Ord(Filter), SubExpr(Vec), + MapStmt(MapAST), } #[derive(Debug, PartialEq, Clone)] @@ -613,7 +636,7 @@ impl Path { } } - fn collect_with_filter(v: Value, filters: &[Filter]) -> PathResult { + pub(crate) fn collect_with_filter(v: Value, filters: &[Filter]) -> PathResult { // TODO(): handle errors similar to collect method let mut ctx = Context::new(v, filters); ctx.collect(); @@ -1338,4 +1361,18 @@ mod test { ]))] ); } + + #[test] + fn test_map_on_path() { + let data = + serde_json::json!({"entry": {"values": [{"name": "gearbox"}, {"name": "steam"}]}}); + let result = Path::collect(data, ">/..values/#map(x: x.name)").unwrap(); + assert_eq!( + *result.0.borrow(), + vec![Rc::new(Value::Array(vec![ + Value::String("gearbox".to_string()), + Value::String("steam".to_string()), + ]))] + ); + } } diff --git a/src/fmt.rs b/src/fmt.rs index b454242..8d0fae2 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -39,9 +39,23 @@ impl FormatImpl { Some(output) => match &value { Value::Object(ref obj) => { let mut result = obj.clone(); - result.insert(alias.to_string(), Value::String(output)); + result.insert(alias.to_string(), Value::String(output.clone())); return Some(Value::Object(result)); } + Value::Array(ref array) => { + let mut ret: Vec = vec![]; + for item in array { + match &item { + Value::Object(ref obj) => { + let mut result = obj.clone(); + result.insert(alias.to_string(), Value::String(output.clone())); + ret.push(Value::Object(result)); + } + _ => {} + }; + } + return Some(Value::Array(ret)); + } _ => {} }, _ => {} diff --git a/src/func.rs b/src/func.rs index 8f8e431..b708b1e 100644 --- a/src/func.rs +++ b/src/func.rs @@ -1,9 +1,11 @@ //! Module func provides abstraction for jetro functions. -use crate::context::{Context, Error, Func, FuncArg}; +use crate::context::{Context, Error, Filter, Func, FuncArg, MapBody, Path}; use serde_json::Value; use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; +use super::context::MapAST; + pub(crate) trait Callable { fn call( &mut self, @@ -263,6 +265,62 @@ impl Callable for AllOnBoolean { } } +struct MapFn; +impl MapFn { + fn eval(&mut self, value: &Value, subpath: &[Filter]) -> Result, Error> { + let mut output: Vec = vec![]; + match &value { + Value::Array(ref array) => { + for item in array { + let result = Path::collect_with_filter(item.clone(), &subpath); + if result.0.borrow().len() == 0 { + return Err(Error::FuncEval( + "map statement do not evaluates to anything".to_owned(), + )); + } + let head = result.0.borrow()[0].clone(); + output.push((*head).clone()); + } + } + _ => {} + }; + return Ok(output); + } +} + +impl Callable for MapFn { + fn call( + &mut self, + func: &Func, + value: &Value, + _ctx: Option<&mut Context<'_>>, + ) -> Result { + match func.args.get(0) { + Some(&FuncArg::MapStmt(MapAST { arg: _, ref body })) => match &body { + MapBody::Method { + name: _, + subpath: _, + } => { + todo!("not implemented"); + return Err(Error::FuncEval("WIP: not implemented".to_owned())); + } + MapBody::Subpath(ref subpath) => { + let output = self.eval(&value, &subpath.as_slice())?; + return Ok(Value::Array(output)); + } + _ => { + return Err(Error::FuncEval("expetcted method call on path".to_owned())); + } + }, + _ => { + return Err(Error::FuncEval( + "expected first argument to be map statement".to_owned(), + )); + } + }; + } +} + impl Default for FuncRegistry { fn default() -> Self { let mut output = FuncRegistry::new(); @@ -273,6 +331,7 @@ impl Default for FuncRegistry { output.register("head", Box::new(Head)); output.register("tail", Box::new(Tail)); output.register("all", Box::new(AllOnBoolean)); + output.register("map", Box::new(MapFn)); output } } diff --git a/src/grammar.pest b/src/grammar.pest index beab118..9e94e7b 100644 --- a/src/grammar.pest +++ b/src/grammar.pest @@ -4,15 +4,19 @@ number = { digit+ } float = { digit+ ~ "." ~ digit+ } special_charaters = _{ "_" | "-" | "\\" } whitespace = _{ (" "| "\n") * } +colon = _{":"} +lbracket = _{"["} +rbracket = _{"]"} lparen = _{"("} rparen = _{")"} +parenPair = _{ lparen ~ rparen} at = _{ "@" } path = { ">" } reverse_path = { "<" } asterisk = _{ "*" } slash = { "/" } double_dot = _{ ".." } -ident = { (alpha | digit | special_charaters)+ } +ident = { (alpha | digit | special_charaters | "_")+ } _as = _{ "as" } _arrow = _{ "->" } as = { (" ")* ~ _as ~ (" ")* ~ literal } @@ -46,6 +50,7 @@ any_child = { slash ~ asterisk } descendant_child = { slash ~ double_dot ~ ident } grouped_any = { slash ~ grouped_literal } array_index = { slash ~ "[" ~ number ~ "]" } +pure_index = {"[" ~ number ~ "]"} slice = { slash ~ "[" ~ number ~ ":" ~ number ~ "]" } array_to = { slash ~ "[:" ~ number ~ "]" } array_from = { slash ~ "[" ~ number ~ ":]" } @@ -62,13 +67,16 @@ sub_expression_keyed_reversed = { sub_expression_reversed ~ as? } pickFn = { slash ~ pick } fnLit = { literal } fnExpr = { sub_expression } -fnCall = { sharp ~ ident ~ (whitespace ~ lparen ~ ((filterStmtCollection|fnLit|fnExpr ) ~ whitespace ~ ((",")* ~ whitespace ~ (filterStmtCollection|fnLit|fnExpr))*)? ~ whitespace ~ rparen ~ whitespace)? ~ (arrow | arrowDeref)? } +fnCall = { sharp ~ ident ~ (whitespace ~ lparen ~ ((mapStmt|filterStmtCollection|fnLit|fnExpr) ~ whitespace ~ ((",")* ~ whitespace ~ (mapStmt|filterStmtCollection|fnLit|fnExpr))*)? ~ whitespace ~ rparen ~ whitespace)? ~ (arrow | arrowDeref)? } fn = {slash ~ fnCall} filterStmt = { ( filter_elem ~ whitespace ~ cmp ~ whitespace ~ (float | truthy | literal | number ) ) } filterStmtCollection = { filterStmt ~ whitespace ~ (logical_cmp ~ whitespace ~ filterStmt~ whitespace)* } filter = { sharp ~ "filter" ~ lparen ~ whitespace ~ ( filterStmt ~ whitespace ~ (logical_cmp ~ whitespace ~ filterStmt~ whitespace)* ) ~ whitespace ~ rparen } filter_elem = { literal } filterFn = { slash ~ filter } +methodCall = {ident ~ "()"} +pathExpr = { ident ~ (dot ~ (pure_index|methodCall|ident))* } +mapStmt = { ident ~ whitespace ~ colon ~ whitespace ~ pathExpr } expression = { (path|reverse_path) ~ diff --git a/src/parser.rs b/src/parser.rs index 82f6f3e..96ccb55 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,7 @@ use pest::iterators::Pair; use crate::context::{ Filter, FilterAST, FilterInner, FilterInnerRighthand, FilterLogicalOp, FilterOp, Func, FuncArg, - PickFilterInner, + MapAST, MapBody, PickFilterInner, }; use crate::*; @@ -90,6 +90,55 @@ pub(crate) fn parse<'a>(input: &'a str) -> Result, pest::error::Erro func.should_deref = true; should_deref = true; } + Rule::mapStmt => { + let mut ast = MapAST::default(); + ast.arg = value + .clone() + .into_inner() + .nth(0) + .unwrap() + .as_str() + .to_string(); + let stmt = value.clone().into_inner().nth(1).unwrap().into_inner(); + for value in stmt { + match &value.as_rule() { + Rule::ident => match &mut ast.body { + MapBody::None => { + let v = value.as_str(); + if v != ast.arg { + todo!("handle wrong map arg"); + } + ast.body = MapBody::Subpath(vec![Filter::Root]); + } + MapBody::Subpath(ref mut subpath) => { + let v = value.as_str(); + subpath.push(Filter::Child(v.to_string())); + } + _ => { + todo!("handle unmatched arm"); + } + }, + Rule::methodCall => { + let v = &value.clone().into_inner().as_str(); + match &ast.body { + MapBody::Subpath(subpath) => { + ast.body = MapBody::Method { + name: v.to_string(), + subpath: subpath.to_vec(), + }; + } + _ => { + todo!("handle invalid type"); + } + } + } + _ => { + todo!("in unmatched arm"); + } + } + } + func.args.push(FuncArg::MapStmt(ast)); + } _ => { todo!("handle unmatched arm of function generalization",); } @@ -869,4 +918,33 @@ mod test { ] ); } + + #[test] + fn parse_map_statement() { + let actions = parse(">/..values/#map(x: x.a.b.c.to_string())").unwrap(); + assert_eq!( + actions, + vec![ + Filter::Root, + Filter::Descendant("values".to_string()), + Filter::Function(Func { + name: "map".to_string(), + alias: None, + should_deref: false, + args: vec![FuncArg::MapStmt(MapAST { + arg: "x".to_string(), + body: MapBody::Method { + name: "to_string".to_string(), + subpath: vec![ + Filter::Root, + Filter::Child("a".to_string()), + Filter::Child("b".to_string()), + Filter::Child("c".to_string()), + ], + }, + })], + }) + ] + ); + } }