diff --git a/Cargo.lock b/Cargo.lock index 98145dfd60..b9db5ef94f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -846,6 +847,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.3", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -2153,6 +2163,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hifijson" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18ae468bcb4dfecf0e4949ee28abbc99076b6a0077f51ddbc94dbfff8e6a870c" + [[package]] name = "hmac" version = "0.10.1" @@ -2624,6 +2640,68 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jaq-core" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d6a5713b8f33675abfac79d1db0022a3f28764b2a6b96a185c199ad8dab86d" +dependencies = [ + "aho-corasick", + "base64 0.21.7", + "hifijson", + "jaq-interpret", + "libm", + "log", + "regex", + "time", + "urlencoding", +] + +[[package]] +name = "jaq-interpret" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569e38e5fc677db8dfda89ee0b4c25b3f53e811b16434fd14bdc5b43fc362ac" +dependencies = [ + "ahash 0.8.11", + "dyn-clone", + "hifijson", + "indexmap 2.2.6", + "jaq-syn", + "once_cell", + "serde_json", +] + +[[package]] +name = "jaq-parse" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6f8beb9f9922546419e774e24199e8a968f54c63a5a2323c8f3ef3321ace14" +dependencies = [ + "chumsky", + "jaq-syn", +] + +[[package]] +name = "jaq-std" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7871c59297cbfdd18f6f1bbbafaad24e97fd555ee1e2a1be7a40a5a20f551a" +dependencies = [ + "bincode", + "jaq-parse", + "jaq-syn", +] + +[[package]] +name = "jaq-syn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d60101fb791b20c982731d848ed6e7d25363656497647c2093b68bd88398d6" +dependencies = [ + "serde", +] + [[package]] name = "jni" version = "0.21.1" @@ -2838,6 +2916,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libmimalloc-sys" version = "0.1.37" @@ -5197,6 +5281,11 @@ dependencies = [ "indexmap 2.2.6", "inquire", "insta", + "jaq-core", + "jaq-interpret", + "jaq-parse", + "jaq-std", + "jaq-syn", "jsonwebtoken", "lazy_static", "lru 0.12.3", diff --git a/Cargo.toml b/Cargo.toml index fb1130f6f1..c62b72f8b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,6 +150,11 @@ datatest-stable = "0.2.9" tokio-test = "0.4.4" base64 = "0.22.0" +jaq-core = "1.2.1" +jaq-parse = "1.0.2" +jaq-interpret = {version = "1.2.1",features = ["serde_json"]} +jaq-syn = "1.1.0" +jaq-std = "1.2.1" [dev-dependencies] tailcall-prettier = {path = "tailcall-prettier"} @@ -164,6 +169,7 @@ temp-env = "0.3.6" maplit = "1.0.2" tailcall-fixtures = { path = "./tailcall-fixtures" } + [features] # Feature Flag to enable V8. @@ -248,6 +254,10 @@ harness = false name = "protobuf_convert_output" harness = false +[[bench]] +name = "bench_jq_and_mustache" +harness = false + [[test]] name = "execution_spec" harness = false diff --git a/benches/bench_jq_and_mustache.rs b/benches/bench_jq_and_mustache.rs new file mode 100644 index 0000000000..8348dff513 --- /dev/null +++ b/benches/bench_jq_and_mustache.rs @@ -0,0 +1,29 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use serde_json::json; +use tailcall::blueprint::DynamicValue; +use tailcall::serde_value_ext::ValueExt; + +fn test_data() -> serde_json::Value { + json!({"foo": {"bar": {"baz": 1}}}) +} +fn bench_jq_and_mustache(c: &mut Criterion) { + let data = test_data(); + let value = json!({"a": "{{.foo.bar.baz}}"}); + let dynamic_value = DynamicValue::try_from(&value).unwrap(); + c.bench_function("mustache_bench", |b| { + b.iter(|| { + black_box(dynamic_value.render_value(&data)).unwrap(); + }) + }); + let data = test_data(); + let value = json!({"a": "{{jq: .foo.bar.baz}}"}); + let dynamic_value = DynamicValue::try_from(&value).unwrap(); + c.bench_function("jq_bench", |b| { + b.iter(|| { + black_box(dynamic_value.render_value(&data)).unwrap(); + }) + }); +} + +criterion_group!(benches, bench_jq_and_mustache); +criterion_main!(benches); diff --git a/benches/request_template_bench.rs b/benches/request_template_bench.rs index bc67c1486f..e6d20b118c 100644 --- a/benches/request_template_bench.rs +++ b/benches/request_template_bench.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use derive_setters::Setters; use hyper::HeaderMap; +use serde::{Serialize, Serializer}; use serde_json::json; use tailcall::endpoint::Endpoint; use tailcall::has_headers::HasHeaders; @@ -20,6 +21,16 @@ impl Default for Context { Self { value: serde_json::Value::Null, headers: HeaderMap::new() } } } + +impl Serialize for Context { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_none() + } +} + impl PathString for Context { fn path_string>(&self, parts: &[T]) -> Option> { self.value.path_string(parts) diff --git a/src/config/reader_context.rs b/src/config/reader_context.rs index 8c16f4783b..1f5212f4f8 100644 --- a/src/config/reader_context.rs +++ b/src/config/reader_context.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::collections::BTreeMap; use headers::HeaderMap; +use serde::{Serialize, Serializer}; use crate::has_headers::HasHeaders; use crate::path::PathString; @@ -13,6 +14,15 @@ pub struct ConfigReaderContext<'a> { pub headers: HeaderMap, } +impl<'a> Serialize for ConfigReaderContext<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_none() + } +} + impl<'a> PathString for ConfigReaderContext<'a> { fn path_string>(&self, path: &[T]) -> Option> { if path.is_empty() { diff --git a/src/grpc/request_template.rs b/src/grpc/request_template.rs index 6d53a5d7fe..3c80748a2a 100644 --- a/src/grpc/request_template.rs +++ b/src/grpc/request_template.rs @@ -124,6 +124,7 @@ mod tests { use hyper::header::{HeaderName, HeaderValue}; use hyper::{HeaderMap, Method}; use pretty_assertions::assert_eq; + use serde::{Serialize, Serializer}; use tailcall_fixtures::protobuf; use super::RequestTemplate; @@ -185,6 +186,15 @@ mod tests { } } + impl Serialize for Context { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_none() + } + } + impl crate::path::PathString for Context { fn path_string>(&self, parts: &[T]) -> Option> { self.value.path_string(parts) diff --git a/src/helpers/body.rs b/src/helpers/body.rs index 9940bbb338..aeb8ebb937 100644 --- a/src/helpers/body.rs +++ b/src/helpers/body.rs @@ -17,11 +17,11 @@ pub fn to_body(body: Option<&str>) -> Valid, String> { mod tests { use super::to_body; use crate::mustache::Mustache; - use crate::valid::Valid; + use crate::valid::{Valid, Validator}; #[test] fn no_body() { - let result = to_body(None); + let result = to_body(None).map(|v| v.map(|v| v.to_string())); assert_eq!(result, Valid::succeed(None)); } @@ -31,8 +31,8 @@ mod tests { let result = to_body(Some("content")); assert_eq!( - result, - Valid::succeed(Some(Mustache::parse("content").unwrap())) + result.map(|v| v.map(|v| v.to_string())), + Valid::succeed(Some(Mustache::parse("content").unwrap().to_string())) ); } } diff --git a/src/helpers/headers.rs b/src/helpers/headers.rs index 7559c31f98..8427811579 100644 --- a/src/helpers/headers.rs +++ b/src/helpers/headers.rs @@ -41,12 +41,22 @@ mod tests { )?; let headers = to_mustache_headers(&input).to_result()?; + let headers = headers + .into_iter() + .map(|(k, v)| (k, v.to_string())) + .collect::>(); assert_eq!( headers, vec![ - (HeaderName::from_bytes(b"a")?, Mustache::parse("str")?), - (HeaderName::from_bytes(b"b")?, Mustache::parse("123")?) + ( + HeaderName::from_bytes(b"a")?, + Mustache::parse("str")?.to_string() + ), + ( + HeaderName::from_bytes(b"b")?, + Mustache::parse("123")?.to_string() + ) ] ); diff --git a/src/helpers/url.rs b/src/helpers/url.rs index 996c3f1238..9460a69601 100644 --- a/src/helpers/url.rs +++ b/src/helpers/url.rs @@ -8,6 +8,7 @@ pub fn to_url(url: &str) -> Valid { #[cfg(test)] mod tests { use super::to_url; + use crate::valid::Validator; #[test] fn parse_url() { @@ -17,8 +18,9 @@ mod tests { let url = to_url("http://localhost:3000"); assert_eq!( - url, + url.map(|v| v.to_string()), Valid::succeed(Mustache::parse("http://localhost:3000").unwrap()) + .map(|v| v.to_string()) ); } } diff --git a/src/http/request_template.rs b/src/http/request_template.rs index 761c1a362e..cbe73726bc 100644 --- a/src/http/request_template.rs +++ b/src/http/request_template.rs @@ -263,6 +263,7 @@ mod tests { use hyper::header::HeaderName; use hyper::HeaderMap; use pretty_assertions::assert_eq; + use serde::{Serialize, Serializer}; use serde_json::json; use super::RequestTemplate; @@ -282,7 +283,16 @@ mod tests { } } - impl crate::path::PathString for Context { + impl Serialize for Context { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_none() + } + } + + impl PathString for Context { fn path_string>(&self, parts: &[T]) -> Option> { self.value.path_string(parts) } diff --git a/src/lib.rs b/src/lib.rs index 6ab74992b2..e3848e88dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ mod rest; pub mod runtime; pub mod scalar; mod schema_extension; -mod serde_value_ext; +pub mod serde_value_ext; pub mod tracing; pub mod try_fold; pub mod valid; diff --git a/src/mustache.rs b/src/mustache.rs index 689ce0b36f..1891ccd28b 100644 --- a/src/mustache.rs +++ b/src/mustache.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +use jaq_interpret::FilterT; use nom::branch::alt; use nom::bytes::complete::{tag, take_until}; use nom::character::complete::char; @@ -7,11 +8,15 @@ use nom::combinator::map; use nom::multi::many0; use nom::sequence::delimited; use nom::{Finish, IResult}; +use serde::Serialize; use crate::path::{PathGraphql, PathString}; -#[derive(Debug, Clone, PartialEq, Hash)] -pub struct Mustache(Vec); +#[derive(Debug, Clone)] +pub enum Mustache { + Segments(Vec), + Jaq(jaq_interpret::Filter), +} #[derive(Debug, Clone, PartialEq, Hash)] pub enum Segment { @@ -21,21 +26,22 @@ pub enum Segment { impl From> for Mustache { fn from(segments: Vec) -> Self { - Mustache(segments) + Mustache::Segments(segments) } } impl Mustache { pub fn is_const(&self) -> bool { match self { - Mustache(segments) => { - for s in segments { - if let Segment::Expression(_) = s { + Mustache::Segments(segs) => { + for seg in segs { + if let Segment::Expression(_) = seg { return false; } } true } + Mustache::Jaq(_) => false, } } @@ -50,46 +56,68 @@ impl Mustache { pub fn render(&self, value: &impl PathString) -> String { match self { - Mustache(segments) => segments + Mustache::Segments(segments) => segments .iter() .map(|segment| match segment { - Segment::Literal(text) => text.clone(), + Segment::Literal(text) => text.to_string(), Segment::Expression(parts) => value .path_string(parts) .map(|a| a.to_string()) .unwrap_or_default(), }) .collect(), + Mustache::Jaq(filter) => Self::evaluate_inner(filter, value) + .map(|v| v.to_string()) + .unwrap_or_default(), } } + // TODO: Null converts to "null" as string but it should be empty string + // fn evaluate(&self, value: &T) -> async_graphql::Value { + // self.evaluate_inner(value).unwrap_or_default() + // } + + fn evaluate_inner( + filter: &jaq_interpret::Filter, + value: &T, + ) -> Option { + let iter = jaq_interpret::RcIter::new(vec![].into_iter()); + let value = serde_json::to_value(value).ok()?; + if value.is_null() { + return None; + } + let mut result = filter.run(( + jaq_interpret::Ctx::new(vec![], &iter), + jaq_interpret::Val::from(value), + )); + let result = result.next()?; + let result = result.ok()?; + Some(result.into()) + } + pub fn render_graphql(&self, value: &impl PathGraphql) -> String { match self { - Mustache(segments) => segments + Mustache::Segments(segments) => segments .iter() .map(|segment| match segment { Segment::Literal(text) => text.to_string(), Segment::Expression(parts) => value.path_graphql(parts).unwrap_or_default(), }) .collect(), - } - } - - pub fn get_segments(&self) -> Vec<&Segment> { - match self { - Mustache(segments) => segments.iter().collect(), + Mustache::Jaq(_) => String::new(), } } pub fn expression_segments(&self) -> Vec<&Vec> { match self { - Mustache(segments) => segments + Mustache::Segments(segments) => segments .iter() .filter_map(|seg| match seg { Segment::Expression(parts) => Some(parts), _ => None, }) .collect(), + _ => vec![], } } } @@ -97,7 +125,7 @@ impl Mustache { impl Display for Mustache { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = match self { - Mustache(segments) => segments + Mustache::Segments(segments) => segments .iter() .map(|segment| match segment { Segment::Literal(text) => text.clone(), @@ -105,7 +133,9 @@ impl Display for Mustache { }) .collect::>() .join(""), + Mustache::Jaq(_) => String::new(), }; + write!(f, "{}", str) } } @@ -162,17 +192,58 @@ fn parse_segment(input: &str) -> IResult<&str, Vec> { } fn parse_mustache(input: &str) -> IResult<&str, Mustache> { - map(parse_segment, |segments| { - Mustache( - segments - .into_iter() - .filter(|seg| match seg { - Segment::Literal(s) => (!s.is_empty()) && s != "\"", - _ => true, - }) - .collect(), - ) - })(input) + let result = map(parse_segment, |segments| { + segments + .into_iter() + .filter(|seg| match seg { + Segment::Literal(s) => (!s.is_empty()) && s != "\"", + _ => true, + }) + .collect::>() + })(input); + let jq_result = parse_jq(input); + if jq_result.is_err() && result.is_err() { + return Err(nom::Err::Error(nom::error::Error::new( + "failed to parse mustache", + nom::error::ErrorKind::Tag, + ))); + } + + let (res, jacques) = jq_result.unwrap_or((input, None)); + if let Some(jq) = jacques { + Ok((res, Mustache::Jaq(jq))) + } else { + let (res, segments) = result.unwrap_or((input, vec![])); + Ok((res, Mustache::Segments(segments))) + } +} + +fn parse_jq(input: &str) -> IResult<&str, Option> { + let (input, _) = nom::character::complete::multispace0(input)?; + let (input, _) = tag("{{")(input)?; + let (input, _) = nom::character::complete::multispace0(input)?; + let (input, _) = tag("jq:")(input)?; + let (input, _) = nom::character::complete::multispace0(input)?; + let (input, filter) = take_until("}}")(input)?; + let filter = filter.trim(); + let mut defs = jaq_interpret::ParseCtx::new(vec![]); + defs.insert_natives(jaq_core::core()); + defs.insert_defs(jaq_std::std()); + + let (filter, errs) = jaq_parse::parse(filter, jaq_parse::main()); + let _err_str = errs + .iter() + .map(|v| v.to_string()) + .collect::>() + .join("\n"); + if !errs.is_empty() || filter.is_none() { + return Err(nom::Err::Error(nom::error::Error::new( + "failed to parse jq", // TODO: fix ownership issue and return _err_str + nom::error::ErrorKind::Tag, + ))); + } + let filter = defs.compile(filter.unwrap()); + Ok((input, Some(filter))) } #[cfg(test)] @@ -207,52 +278,67 @@ mod tests { fn test_single_literal() { let s = r"hello/world"; let mustache: Mustache = Mustache::parse(s).unwrap(); - assert_eq!( - mustache, + let Mustache::Segments(segments) = mustache else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = Mustache::from(vec![Segment::Literal("hello/world".to_string())]) - ); + else { + panic!("Mustache must be a segment") + }; + assert_eq!(segments, expected); } #[test] fn test_single_template() { let s = r"{{hello.world}}"; let mustache: Mustache = Mustache::parse(s).unwrap(); - assert_eq!( - mustache, - Mustache::from(vec![Segment::Expression(vec![ - "hello".to_string(), - "world".to_string(), - ])]) - ); + let Mustache::Segments(segments) = mustache else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = Mustache::from(vec![Segment::Expression(vec![ + "hello".to_string(), + "world".to_string(), + ])]) else { + panic!("Mustache must be a segment") + }; + assert_eq!(segments, expected); } #[test] fn test_mixed() { let s = r"http://localhost:8090/{{foo.bar}}/api/{{hello.world}}/end"; let mustache: Mustache = Mustache::parse(s).unwrap(); - assert_eq!( - mustache, - Mustache::from(vec![ - Segment::Literal("http://localhost:8090/".to_string()), - Segment::Expression(vec!["foo".to_string(), "bar".to_string()]), - Segment::Literal("/api/".to_string()), - Segment::Expression(vec!["hello".to_string(), "world".to_string()]), - Segment::Literal("/end".to_string()), - ]) - ); + + let Mustache::Segments(segments) = mustache else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = Mustache::from(vec![ + Segment::Literal("http://localhost:8090/".to_string()), + Segment::Expression(vec!["foo".to_string(), "bar".to_string()]), + Segment::Literal("/api/".to_string()), + Segment::Expression(vec!["hello".to_string(), "world".to_string()]), + Segment::Literal("/end".to_string()), + ]) else { + panic!("Mustache must be a segment") + }; + assert_eq!(segments, expected); } #[test] fn test_with_spaces() { let s = "{{ foo . bar }}"; let mustache: Mustache = Mustache::parse(s).unwrap(); - assert_eq!( - mustache, - Mustache::from(vec![Segment::Expression(vec![ - "foo".to_string(), - "bar".to_string(), - ])]) - ); + let Mustache::Segments(segments) = mustache else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = Mustache::from(vec![Segment::Expression(vec![ + "foo".to_string(), + "bar".to_string(), + ])]) else { + panic!("Mustache must be a segment") + }; + assert_eq!(segments, expected); } #[test] @@ -262,6 +348,12 @@ mod tests { Segment::Expression(vec!["foo".to_string(), "bar".to_string()]), Segment::Literal(" extra".to_string()), ]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; assert_eq!(result, expected); } @@ -269,6 +361,12 @@ mod tests { fn test_parse_expression_with_invalid_input() { let result = Mustache::parse("foo.bar }}").unwrap(); let expected = Mustache::from(vec![Segment::Literal("foo.bar }}".to_string())]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; assert_eq!(result, expected); } @@ -282,95 +380,137 @@ mod tests { Segment::Expression(vec!["baz".to_string(), "qux".to_string()]), Segment::Literal(" suffix".to_string()), ]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; assert_eq!(result, expected); } #[test] fn test_parse_segments_only_literal() { let result = Mustache::parse("just a string").unwrap(); - let expected = Mustache(vec![Segment::Literal("just a string".to_string())]); + let expected = Mustache::Segments(vec![Segment::Literal("just a string".to_string())]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; assert_eq!(result, expected); } #[test] fn test_parse_segments_only_expression() { let result = Mustache::parse("{{foo.bar}}").unwrap(); - let expected = Mustache(vec![Segment::Expression(vec![ + let expected = Mustache::Segments(vec![Segment::Expression(vec![ "foo".to_string(), "bar".to_string(), ])]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; assert_eq!(result, expected); } #[test] fn test_unfinished_expression() { let s = r"{{hello.world"; - let mustache: Mustache = Mustache::parse(s).unwrap(); - assert_eq!( - mustache, - Mustache::from(vec![Segment::Literal("{{hello.world".to_string())]) - ); + let result: Mustache = Mustache::parse(s).unwrap(); + let expected = Mustache::from(vec![Segment::Literal("{{hello.world".to_string())]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; + assert_eq!(result, expected); } #[test] fn test_new_number() { let mustache = Mustache::parse("123").unwrap(); - assert_eq!( - mustache, - Mustache::from(vec![Segment::Literal("123".to_string())]) - ); + let expected = Mustache::from(vec![Segment::Literal("123".to_string())]); + let Mustache::Segments(result) = mustache else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; + assert_eq!(result, expected); } #[test] fn parse_env_name() { let result = Mustache::parse("{{env.FOO}}").unwrap(); - assert_eq!( - result, - Mustache::from(vec![Segment::Expression(vec![ - "env".to_string(), - "FOO".to_string(), - ])]) - ); + let expected = Mustache::from(vec![Segment::Expression(vec![ + "env".to_string(), + "FOO".to_string(), + ])]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; + assert_eq!(result, expected); } #[test] fn parse_env_with_underscores() { let result = Mustache::parse("{{env.FOO_BAR}}").unwrap(); - assert_eq!( - result, - Mustache::from(vec![Segment::Expression(vec![ - "env".to_string(), - "FOO_BAR".to_string(), - ])]) - ); + let expected = Mustache::from(vec![Segment::Expression(vec![ + "env".to_string(), + "FOO_BAR".to_string(), + ])]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; + assert_eq!(result, expected); } #[test] fn single_curly_brackets() { let result = Mustache::parse("test:{SHA}string").unwrap(); - assert_eq!( - result, - Mustache::from(vec![Segment::Literal("test:{SHA}string".to_string())]) - ); + let expected = Mustache::from(vec![Segment::Literal("test:{SHA}string".to_string())]); + let Mustache::Segments(result) = result else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = expected else { + panic!("Mustache must be a segment") + }; + assert_eq!(result, expected); } #[test] fn test_optional_dot_expression() { let s = r"{{.foo.bar}}"; let mustache: Mustache = Mustache::parse(s).unwrap(); - assert_eq!( - mustache, - Mustache::from(vec![Segment::Expression(vec![ - "foo".to_string(), - "bar".to_string(), - ])]) - ); + let Mustache::Segments(segments) = mustache else { + panic!("Mustache must be a segment") + }; + let Mustache::Segments(expected) = Mustache::from(vec![Segment::Expression(vec![ + "foo".to_string(), + "bar".to_string(), + ])]) else { + panic!("Mustache must be a segment") + }; + assert_eq!(segments, expected); } } mod render { use std::borrow::Cow; + use serde::{Serialize, Serializer}; use serde_json::json; use crate::mustache::{Mustache, Segment}; @@ -389,6 +529,14 @@ mod tests { fn test_render_mixed() { struct DummyPath; + impl Serialize for DummyPath { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str("") + } + } impl PathString for DummyPath { fn path_string>(&self, parts: &[T]) -> Option> { let parts: Vec<&str> = parts.iter().map(AsRef::as_ref).collect(); @@ -421,6 +569,14 @@ mod tests { fn test_render_with_missing_path() { struct DummyPath; + impl Serialize for DummyPath { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str("") + } + } impl PathString for DummyPath { fn path_string>(&self, _: &[T]) -> Option> { None @@ -457,6 +613,14 @@ mod tests { fn test_render_preserves_spaces() { struct DummyPath; + impl Serialize for DummyPath { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str("") + } + } impl PathString for DummyPath { fn path_string>(&self, parts: &[T]) -> Option> { let parts: Vec<&str> = parts.iter().map(AsRef::as_ref).collect(); diff --git a/src/path.rs b/src/path.rs index f80ff9a0be..dd58895948 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use serde::{Serialize, Serializer}; use serde_json::json; use crate::json::JsonLike; @@ -13,7 +14,7 @@ use crate::lambda::{EvaluationContext, ResolverContextLike}; /// The PathString trait provides a method for accessing values from a JSON-like /// structure. The returned value is encoded as a plain string. /// This is typically used in evaluating mustache templates. -pub trait PathString { +pub trait PathString: Serialize { fn path_string>(&self, path: &[T]) -> Option>; } @@ -49,6 +50,18 @@ fn convert_value(value: Cow<'_, async_graphql::Value>) -> Option> { } } +impl<'a, Ctx: ResolverContextLike<'a>> Serialize for EvaluationContext<'a, Ctx> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.value() + .cloned() + .unwrap_or_default() + .serialize(serializer) + } +} + impl<'a, Ctx: ResolverContextLike<'a>> PathString for EvaluationContext<'a, Ctx> { fn path_string>(&self, path: &[T]) -> Option> { let ctx = self; diff --git a/src/serde_value_ext.rs b/src/serde_value_ext.rs index 1b0f46f6de..547bbe638f 100644 --- a/src/serde_value_ext.rs +++ b/src/serde_value_ext.rs @@ -44,120 +44,237 @@ impl ValueExt for DynamicValue { #[cfg(test)] mod tests { - use serde_json::json; - - use crate::blueprint::DynamicValue; - use crate::serde_value_ext::ValueExt; - - #[test] - fn test_render_value() { - let value = json!({"a": "{{foo}}"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": "baz"}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": {"bar": "baz"}})).unwrap(); - assert_eq!(result.unwrap(), expected); - } + mod render_value { + use serde_json::json; - #[test] - fn test_render_value_nested() { - let value = json!({"a": "{{foo.bar.baz}}"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": 1}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": 1})).unwrap(); - assert_eq!(result.unwrap(), expected); - } + use crate::blueprint::DynamicValue; + use crate::serde_value_ext::ValueExt; - #[test] - fn test_render_value_nested_str() { - let value = json!({"a": "{{foo.bar.baz}}"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": "foo"}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": "foo"})).unwrap(); - assert_eq!(result.unwrap(), expected); - } + #[test] + fn test_render_value() { + let value = json!({"a": "{{foo}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": "baz"}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": {"bar": "baz"}})).unwrap(); + assert_eq!(result.unwrap(), expected); + } - #[test] - fn test_render_value_null() { - let value = json!("{{foo.bar.baz}}"); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": null}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!(null)).unwrap(); - assert_eq!(result.unwrap(), expected); - } + #[test] + fn test_render_value_nested() { + let value = json!({"a": "{{foo.bar.baz}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": 1})).unwrap(); + assert_eq!(result.unwrap(), expected); + } - #[test] - fn test_render_value_nested_bool() { - let value = json!({"a": "{{foo.bar.baz}}"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": true}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": true})).unwrap(); - assert_eq!(result.unwrap(), expected); - } + #[test] + fn test_render_value_nested_str() { + let value = json!({"a": "{{foo.bar.baz}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": "foo"}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": "foo"})).unwrap(); + assert_eq!(result.unwrap(), expected); + } - #[test] - fn test_render_value_nested_float() { - let value = json!({"a": "{{foo.bar.baz}}"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": 1.1}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": 1.1})).unwrap(); - assert_eq!(result.unwrap(), expected); - } + #[test] + fn test_render_value_null() { + let value = json!("{{foo.bar.baz}}"); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": null}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!(null)).unwrap(); + assert_eq!(result.unwrap(), expected); + } - #[test] - fn test_render_value_arr() { - let value = json!({"a": "{{foo.bar.baz}}"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": [1,2,3]}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": [1, 2, 3]})).unwrap(); - assert_eq!(result.unwrap(), expected); - } + #[test] + fn test_render_value_nested_bool() { + let value = json!({"a": "{{foo.bar.baz}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": true}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": true})).unwrap(); + assert_eq!(result.unwrap(), expected); + } - #[test] - fn test_render_value_arr_template() { - let value = json!({"a": ["{{foo.bar.baz}}", "{{foo.bar.qux}}"]}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": [1, 2]})).unwrap(); - assert_eq!(result.unwrap(), expected); - } + #[test] + fn test_render_value_nested_float() { + let value = json!({"a": "{{foo.bar.baz}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1.1}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": 1.1})).unwrap(); + assert_eq!(result.unwrap(), expected); + } - #[test] - fn test_mustache_or_value_is_const() { - let value = json!("{{foo}}"); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": "bar"}); - let result = value.render_value(&ctx).unwrap(); - let expected = async_graphql::Value::String("bar".to_owned()); - assert_eq!(result, expected); - } + #[test] + fn test_render_value_arr() { + let value = json!({"a": "{{foo.bar.baz}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": [1,2,3]}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": [1, 2, 3]})).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_render_value_arr_template() { + let value = json!({"a": ["{{foo.bar.baz}}", "{{foo.bar.qux}}"]}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": [1, 2]})).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_mustache_or_value_is_const() { + let value = json!("{{foo}}"); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": "bar"}); + let result = value.render_value(&ctx).unwrap(); + let expected = async_graphql::Value::String("bar".to_owned()); + assert_eq!(result, expected); + } + + #[test] + fn test_mustache_arr_obj() { + let value = json!([{"a": "{{foo.bar.baz}}"}, {"a": "{{foo.bar.qux}}"}]); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!([{"a": 1}, {"a":2}])).unwrap(); + assert_eq!(result.unwrap(), expected); + } - #[test] - fn test_mustache_arr_obj() { - let value = json!([{"a": "{{foo.bar.baz}}"}, {"a": "{{foo.bar.qux}}"}]); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!([{"a": 1}, {"a":2}])).unwrap(); - assert_eq!(result.unwrap(), expected); + #[test] + fn test_mustache_arr_obj_arr() { + let value = + json!([{"a": [{"aa": "{{foo.bar.baz}}"}]}, {"a": [{"aa": "{{foo.bar.qux}}"}]}]); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); + let result = value.render_value(&ctx); + let expected = + async_graphql::Value::from_json(json!([{"a": [{"aa": 1}]}, {"a":[{"aa": 2}]}])) + .unwrap(); + assert_eq!(result.unwrap(), expected); + } } - #[test] - fn test_mustache_arr_obj_arr() { - let value = json!([{"a": [{"aa": "{{foo.bar.baz}}"}]}, {"a": [{"aa": "{{foo.bar.qux}}"}]}]); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); - let result = value.render_value(&ctx); - let expected = - async_graphql::Value::from_json(json!([{"a": [{"aa": 1}]}, {"a":[{"aa": 2}]}])) - .unwrap(); - assert_eq!(result.unwrap(), expected); + mod evaluate { + use serde_json::json; + + use crate::blueprint::DynamicValue; + use crate::serde_value_ext::ValueExt; + + #[test] + fn test_render_value() { + let value = json!({"a": "{{jq: .foo}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": "baz"}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": {"bar": "baz"}})).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_render_value_nested() { + let value = json!({"a": "{{jq: .foo.bar.baz}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": 1})).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_render_value_nested_str() { + let value = json!({"a": "{{ jq: .foo.bar.baz}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": "foo"}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": "foo"})).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_render_value_null() { + let value = json!("{{jq:.foo.bar.baz}}"); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": null}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!(null)).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_render_jq_sort() { + let value = json!({"a": "{{jq: sort_by(.age) | .[0].name }}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!([ + {"name": "Alice", "age": 25, "city": "New York"}, + {"name": "Bob", "age": 30, "city": "Chicago"}, + {"name": "Sandip", "age": 16, "city": "GoodQuestion"}, + {"name": "Charlie", "age": 22, "city": "San Francisco"} + ]); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": "Sandip"})).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_render_value_arr() { + let value = json!({"a": "{{ jq: .foo.bar.baz[0]}}"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": [1,2,3]}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": 1})).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_render_value_arr_template() { + let value = json!({"a": ["{{jq: .foo.bar.baz}}", "{{jq: .foo.bar.qux}}"]}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": [1, 2]})).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_mustache_or_value_is_const() { + let value = json!("{{jq: .foo}}"); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": "bar"}); + let result = value.render_value(&ctx).unwrap(); + let expected = async_graphql::Value::String("bar".to_owned()); + assert_eq!(result, expected); + } + + #[test] + fn test_mustache_arr_obj() { + let value = json!([{"a": "{{jq: .foo.bar.baz}}"}, {"a": "{{jq: .foo.bar.qux}}"}]); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!([{"a": 1}, {"a":2}])).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_mustache_arr_obj_arr() { + let value = json!([{"a": [{"aa": "{{jq: .foo.bar.baz}}"}]}, {"a": [{"aa": "{{jq: .foo.bar.qux}}"}]}]); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"foo": {"bar": {"baz": 1, "qux": 2}}}); + let result = value.render_value(&ctx); + let expected = + async_graphql::Value::from_json(json!([{"a": [{"aa": 1}]}, {"a":[{"aa": 2}]}])) + .unwrap(); + assert_eq!(result.unwrap(), expected); + } } }