Skip to content

Commit

Permalink
WIP: add draft implementation of map function
Browse files Browse the repository at this point in the history
  • Loading branch information
mitghi committed Mar 18, 2023
1 parent 6747f10 commit 37c41e6
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 10 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
[<img src="https://img.shields.io/badge/try-online%20repl-brightgreen"></img>](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",
Expand All @@ -34,12 +39,13 @@ struct Output {
let output: Option<Output> = 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.

Expand All @@ -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
{
Expand Down
39 changes: 38 additions & 1 deletion src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,28 @@ pub(crate) struct Context<'a> {
pub results: Rc<RefCell<Vec<Rc<Value>>>>,
}

#[derive(Debug, PartialEq, Clone)]
pub enum MapBody {
None,
Method { name: String, subpath: Vec<Filter> },
Subpath(Vec<Filter>),
}

#[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,
Expand All @@ -247,6 +269,7 @@ pub enum FuncArg {
Key(String),
Ord(Filter),
SubExpr(Vec<Filter>),
MapStmt(MapAST),
}

#[derive(Debug, PartialEq, Clone)]
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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()),
]))]
);
}
}
16 changes: 15 additions & 1 deletion src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value> = 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));
}
_ => {}
},
_ => {}
Expand Down
61 changes: 60 additions & 1 deletion src/func.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -263,6 +265,62 @@ impl Callable for AllOnBoolean {
}
}

struct MapFn;
impl MapFn {
fn eval(&mut self, value: &Value, subpath: &[Filter]) -> Result<Vec<Value>, Error> {
let mut output: Vec<Value> = 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<Value, Error> {
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();
Expand All @@ -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
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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 ~ ":]" }
Expand All @@ -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) ~
Expand Down
80 changes: 79 additions & 1 deletion src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -90,6 +90,55 @@ pub(crate) fn parse<'a>(input: &'a str) -> Result<Vec<Filter>, 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",);
}
Expand Down Expand Up @@ -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()),
],
},
})],
})
]
);
}
}

0 comments on commit 37c41e6

Please sign in to comment.