From 847a7dae3a6fc6181da8ffc3d84fd4260cf625b0 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Tue, 3 Dec 2024 16:57:21 +0200 Subject: [PATCH 01/28] feat(mustache): enhance mustache with JQ funtionality --- Cargo.lock | 77 +++++++++- Cargo.toml | 2 + src/core/blueprint/dynamic_value.rs | 4 +- src/core/mustache/jq_template.rs | 224 ++++++++++++++++++++++++++++ src/core/mustache/mod.rs | 2 + src/core/mustache/parse.rs | 7 + 6 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 src/core/mustache/jq_template.rs diff --git a/Cargo.lock b/Cargo.lock index 530a3206b2..08147de109 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1541,7 +1541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2158,6 +2158,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hifijson" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9958ab3ce3170c061a27679916bd9b969eceeb5e8b120438e6751d0987655c42" + [[package]] name = "hmac" version = "0.10.1" @@ -2784,6 +2790,47 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jaq-core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03ee1b714d63f0a5820131180056b592ecd400b32879c848e53e58707f19df0" +dependencies = [ + "dyn-clone", + "once_cell", + "typed-arena", +] + +[[package]] +name = "jaq-json" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf2b52304419d7bf5ec32891884c65274a3eedc0b5834b84627099901a1176" +dependencies = [ + "foldhash", + "hifijson", + "indexmap 2.6.0", + "jaq-core", + "jaq-std", + "serde_json", +] + +[[package]] +name = "jaq-std" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e2c65cceafd4c0019f15a0dac7c0dd659b0fcf5182fc3a10d15b89d89ac6e8" +dependencies = [ + "aho-corasick", + "base64 0.22.1", + "chrono", + "jaq-core", + "libm", + "log", + "regex-lite", + "urlencoding", +] + [[package]] name = "js-sys" version = "0.3.74" @@ -2990,9 +3037,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libmimalloc-sys" version = "0.1.39" @@ -4480,7 +4533,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4626,6 +4679,12 @@ dependencies = [ "regex-syntax 0.6.29", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -5641,6 +5700,8 @@ dependencies = [ "indexmap 2.6.0", "inquire", "insta", + "jaq-core", + "jaq-json", "jsonwebtoken", "lazy_static", "lru", @@ -5964,7 +6025,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6555,6 +6616,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.17.0" @@ -6931,7 +6998,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 199d15005d..1a8bf62e61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,6 +176,8 @@ tailcall-valid = { workspace = true } dashmap = "6.1.0" urlencoding = "2.1.3" tailcall-chunk = "0.3.0" +jaq-core = "2.0.0" +jaq-json = { version = "1.0.0", features = ["serde_json"]} # to build rquickjs bindings on systems without builtin bindings [target.'cfg(all(target_os = "windows", target_arch = "x86"))'.dependencies] diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index 1900b73566..4f7b2ef301 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -2,12 +2,12 @@ use async_graphql_value::{ConstValue, Name}; use indexmap::IndexMap; use serde_json::Value; -use crate::core::mustache::Mustache; +use crate::core::mustache::{JqTemplate, Mustache}; #[derive(Debug, Clone, PartialEq)] pub enum DynamicValue { Value(A), - Mustache(Mustache), + Mustache(JqTemplate), Object(IndexMap>), Array(Vec>), } diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs new file mode 100644 index 0000000000..89a5a1c1d5 --- /dev/null +++ b/src/core/mustache/jq_template.rs @@ -0,0 +1,224 @@ +use std::iter::Empty; + +use jaq_core::{ + load::{parse::Term, Arena, File, Loader}, + Compiler, Ctx, Filter, Native, RcIter, ValR, +}; +use jaq_json::Val; + +use crate::core::ir::{EvalContext, ResolverContextLike}; + +use super::Mustache; + +#[derive(Debug)] +pub enum JqTemplate { + Mustache(Mustache), + JqTemplate(JqTransformer) +} + +impl JqTemplate { + pub fn render(&self, value: &serde_json::Value) -> String { + match self { + JqTemplate::Mustache(mustache) => mustache.render(value), + JqTemplate::JqTemplate(jq_transformer) => todo!(), + } + } + + pub fn render_graphql<'a, Ctx: ResolverContextLike>(&self, value: &EvalContext<'a, Ctx>) -> String { + match self { + JqTemplate::Mustache(mustache) => mustache.render_graphql(value), + JqTemplate::JqTemplate(jq_transformer) => { + let Some(value) = value.value() else { + return String::default() + }; + jq_transformer.render_graphql(value) + }, + } + } +} + +pub struct JqTransformer { + filter: Filter>, + inputs: RcIter>>, + terms: Vec, +} + +impl JqTransformer { + /// Used to parse a `template` and try to convert it into a JqTemplate + pub fn try_new(template: &str) -> Result { + // the template is used to be parsed in to the IR AST + let template = File { code: template, path: () }; + // defs is used to extend the syntax with custom definitions of functions, like 'toString' + let defs = vec![]; + // the loader is used to load custom modules + let loader = Loader::new(defs); + // the arena is used to keep the loaded modules + let arena = Arena::default(); + // load the modules + let modules = loader.load(&arena, template).map_err(|errs| { + JqTemplateError::JqLoadError(errs.into_iter().map(|e| format!("{:?}", e.1)).collect()) + })?; + // the AST of the operation, used to transform the data + let filter = Compiler::<_, Native>::default() + .compile(modules) + .map_err(|errs| { + JqTemplateError::JqCompileError( + errs.into_iter().map(|e| format!("{:?}", e.1)).collect(), + ) + })?; + // the hardcoded inputs for the AST + let inputs = RcIter::new(core::iter::empty()); + + Ok(Self { filter, inputs }) + } + + /// Used to execute the transformation of the JQTemplate + pub fn run<'a>(&'a self, data: Val) -> impl Iterator> + 'a { + let ctx = Ctx::new([], &self.inputs); + self.filter.run((ctx, data)) + } + + pub fn render(&self, value: &serde_json::Value) -> String { + self.render_helper(Val::from(value.clone())) + } + + pub fn render_graphql(&self, value: &async_graphql_value::ConstValue) -> String { + let Ok(value) = value.clone().into_json() else { + return String::default() + }; + + self.render_helper(Val::from(value)) + } + + fn render_helper(&self, value: Val) -> String { + let res = self.run(value); + res.filter_map(|v| { + if let Ok(v) = v { + Some(v) + } else { + None + } + }).fold(String::new(), |acc, cur| { + let cur_string = cur.to_string(); + acc + &cur_string + }) + } + + /// Used to determine if the expression can be supported with current Mustache implementation + pub fn is_select_operation(template: &str) -> bool { + let lexer = jaq_core::load::Lexer::new(template); + let lex = lexer.lex().unwrap_or_default(); + let mut parser = jaq_core::load::parse::Parser::new(&lex); + let term = parser.term().unwrap_or_default(); + Self::recursive_is_select_operation(term) + } + + /// Used as a helper function to determine if the term can be supported with Mustache implementation + fn recursive_is_select_operation(term: jaq_core::load::parse::Term<&str>) -> bool { + match term { + jaq_core::load::parse::Term::Id => true, + jaq_core::load::parse::Term::Recurse => false, + jaq_core::load::parse::Term::Num(_) => false, + jaq_core::load::parse::Term::Str(formater, _) => formater.is_none(), + jaq_core::load::parse::Term::Arr(_) => false, + jaq_core::load::parse::Term::Obj(_) => false, + jaq_core::load::parse::Term::Neg(_) => false, + jaq_core::load::parse::Term::Pipe(local_term_1, pattern, local_term_2) => { + if pattern.is_some() { + false + } else { + Self::recursive_is_select_operation(*local_term_1) + && Self::recursive_is_select_operation(*local_term_2) + } + } + jaq_core::load::parse::Term::BinOp(_, _, _) => false, + jaq_core::load::parse::Term::Label(_, _) => false, + jaq_core::load::parse::Term::Break(_) => false, + jaq_core::load::parse::Term::Fold(_, _, _, _) => false, + jaq_core::load::parse::Term::TryCatch(_, _) => false, + jaq_core::load::parse::Term::IfThenElse(_, _) => false, + jaq_core::load::parse::Term::Def(_, _) => false, + jaq_core::load::parse::Term::Call(_, _) => false, + jaq_core::load::parse::Term::Var(_) => false, + jaq_core::load::parse::Term::Path(local_term, path) => { + Self::recursive_is_select_operation(*local_term) + && Self::is_path_select_operation(path) + } + } + } + + fn is_path_select_operation( + path: jaq_core::path::Path>, + ) -> bool { + path.0.into_iter().all(|part| match part { + (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Optional) => Self::recursive_is_select_operation(idx), + (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Essential) => Self::recursive_is_select_operation(idx), + (jaq_core::path::Part::Range(_, _), jaq_core::path::Opt::Optional) => false, + (jaq_core::path::Part::Range(_, _), jaq_core::path::Opt::Essential) => false, + }) + } +} + +impl Default for JqTransformer { + fn default() -> Self { + let inputs = RcIter::new(core::iter::empty()); + Self { filter: Default::default(), inputs } + } +} + +impl std::fmt::Debug for JqTransformer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JqTemplateData").finish() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum JqTemplateError { + #[error("{0}")] + Reason(String), + #[error("JQ Load Errors: {0:?}")] + JqLoadError(Vec), + #[error("JQ Compile Errors: {0:?}")] + JqCompileError(Vec), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_select_operation_simple_property() { + let template = ".fruit"; + assert!(JqTransformer::is_select_operation(template), "Should return true for simple property access"); + } + + #[test] + fn test_is_select_operation_nested_property() { + let template = ".fruit.name"; + assert!(JqTransformer::is_select_operation(template), "Should return true for nested property access"); + } + + #[test] + fn test_is_select_operation_array_index() { + let template = ".fruits[1]"; + assert!(!JqTransformer::is_select_operation(template), "Should return false for array index access"); + } + + #[test] + fn test_is_select_operation_pipe_operator() { + let template = ".fruits[] | .name"; + assert!(!JqTransformer::is_select_operation(template), "Should return false for pipe operator usage"); + } + + #[test] + fn test_is_select_operation_filter() { + let template = ".fruits[] | select(.price > 1)"; + assert!(!JqTransformer::is_select_operation(template), "Should return false for select filter usage"); + } + + #[test] + fn test_is_select_operation_function_call() { + let template = "map(.price)"; + assert!(!JqTransformer::is_select_operation(template), "Should return false for function call"); + } +} diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index 723765f76d..705e3b965b 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -1,5 +1,7 @@ mod eval; +mod jq_template; mod model; mod parse; pub use eval::{Eval, PathStringEval}; pub use model::*; +pub use jq_template::*; diff --git a/src/core/mustache/parse.rs b/src/core/mustache/parse.rs index 6035f7895b..c9b5bea483 100644 --- a/src/core/mustache/parse.rs +++ b/src/core/mustache/parse.rs @@ -16,6 +16,13 @@ impl Mustache { Err(_) => Mustache::from(vec![Segment::Literal(str.to_string())]), } } + + pub fn try_parse_optional(template: &str) -> Option { + match parse_mustache(template).finish() { + Ok((_, mustache)) => Some(mustache), + Err(_) => None, + } + } } fn parse_name(input: &str) -> IResult<&str, String> { From f35dc2aab377c014f4278d370eed5a3867ff408f Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Wed, 4 Dec 2024 13:40:52 +0200 Subject: [PATCH 02/28] wip: implement missing functions --- src/core/mustache/jq_template.rs | 117 ++++++++++++++++++++++++++----- src/core/mustache/parse.rs | 7 -- 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 89a5a1c1d5..5e3e80bdb7 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -1,4 +1,4 @@ -use std::iter::Empty; +use std::sync::Arc; use jaq_core::{ load::{parse::Term, Arena, File, Loader}, @@ -8,19 +8,19 @@ use jaq_json::Val; use crate::core::ir::{EvalContext, ResolverContextLike}; -use super::Mustache; +use super::{Mustache, Segment}; -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Hash)] pub enum JqTemplate { Mustache(Mustache), - JqTemplate(JqTransformer) + JqTemplate(Arc) } impl JqTemplate { pub fn render(&self, value: &serde_json::Value) -> String { match self { JqTemplate::Mustache(mustache) => mustache.render(value), - JqTemplate::JqTemplate(jq_transformer) => todo!(), + JqTemplate::JqTemplate(jq_transformer) => jq_transformer.render(value), } } @@ -35,17 +35,68 @@ impl JqTemplate { }, } } + + pub fn is_const(&self) -> bool { + match self { + JqTemplate::Mustache(mustache) => mustache.is_const(), + JqTemplate::JqTemplate(jq_transformer) => jq_transformer.is_const(), + } + } + + pub fn segments(&self) -> &Vec { + if let JqTemplate::Mustache(mustache) = self { + mustache.segments() + } else { + unimplemented!() + } + } + + pub fn segments_mut(&mut self) -> &mut Vec { + if let JqTemplate::Mustache(mustache) = self { + mustache.segments_mut() + } else { + unimplemented!() + } + } + + pub fn expression_segments(&self) -> Vec<&Vec> { + if let JqTemplate::Mustache(mustache) = self { + mustache.expression_segments() + } else { + unimplemented!() + } + } + + pub fn expression_contains(&self, expression: &str) -> bool { + if let JqTemplate::Mustache(mustache) = self { + mustache.expression_contains(expression) + } else { + unimplemented!() + } + } + + pub fn parse(template: &str) -> Self { + Self::Mustache(Mustache::parse(template)) + } +} + +impl Default for JqTemplate { + fn default() -> Self { + Self::Mustache(Mustache::default()) + } } pub struct JqTransformer { filter: Filter>, - inputs: RcIter>>, - terms: Vec, + term: Term<&'static str>, } impl JqTransformer { /// Used to parse a `template` and try to convert it into a JqTemplate - pub fn try_new(template: &str) -> Result { + pub fn try_new(template: &'static str) -> Result { + // the term is used because it can be easily serialized, deserialized and hashed + let term = Self::parse_template(template); + // the template is used to be parsed in to the IR AST let template = File { code: template, path: () }; // defs is used to extend the syntax with custom definitions of functions, like 'toString' @@ -66,15 +117,13 @@ impl JqTransformer { errs.into_iter().map(|e| format!("{:?}", e.1)).collect(), ) })?; - // the hardcoded inputs for the AST - let inputs = RcIter::new(core::iter::empty()); - Ok(Self { filter, inputs }) + Ok(Self { filter, term }) } /// Used to execute the transformation of the JQTemplate - pub fn run<'a>(&'a self, data: Val) -> impl Iterator> + 'a { - let ctx = Ctx::new([], &self.inputs); + pub fn run<'a, T: std::iter::Iterator>>(&'a self, inputs: &'a RcIter,data: Val) -> impl Iterator> + 'a { + let ctx = Ctx::new([], inputs); self.filter.run((ctx, data)) } @@ -91,7 +140,9 @@ impl JqTransformer { } fn render_helper(&self, value: Val) -> String { - let res = self.run(value); + // the hardcoded inputs for the AST + let inputs = RcIter::new(core::iter::empty()); + let res = self.run(&inputs, value); res.filter_map(|v| { if let Ok(v) = v { Some(v) @@ -106,11 +157,15 @@ impl JqTransformer { /// Used to determine if the expression can be supported with current Mustache implementation pub fn is_select_operation(template: &str) -> bool { + let term = Self::parse_template(template); + Self::recursive_is_select_operation(term) + } + + fn parse_template(template: &str) -> Term<&str> { let lexer = jaq_core::load::Lexer::new(template); let lex = lexer.lex().unwrap_or_default(); let mut parser = jaq_core::load::parse::Parser::new(&lex); - let term = parser.term().unwrap_or_default(); - Self::recursive_is_select_operation(term) + parser.term().unwrap_or_default() } /// Used as a helper function to determine if the term can be supported with Mustache implementation @@ -157,18 +212,42 @@ impl JqTransformer { (jaq_core::path::Part::Range(_, _), jaq_core::path::Opt::Essential) => false, }) } + + /// Used to determine if the transformer is a static value + pub fn is_const(&self) -> bool { + // TODO: parse terms to determine the value + false + } } impl Default for JqTransformer { fn default() -> Self { - let inputs = RcIter::new(core::iter::empty()); - Self { filter: Default::default(), inputs } + Self { filter: Default::default(), term: Term::default() } } } impl std::fmt::Debug for JqTransformer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("JqTemplateData").finish() + f.debug_struct("JqTransformer").field("term", &self.term).finish() + } +} + +impl ToString for JqTransformer { + fn to_string(&self) -> String { + format!("[JqTransformer]({:?})", self.term) + } +} + +impl std::cmp::PartialEq for JqTransformer { + fn eq(&self, other: &Self) -> bool { + // TODO: sorry for the quick hack + format!("{:?}", self).eq(&format!("{:?}", other)) + } +} + +impl std::hash::Hash for JqTransformer { + fn hash(&self, state: &mut H) { + self.to_string().hash(state); } } diff --git a/src/core/mustache/parse.rs b/src/core/mustache/parse.rs index c9b5bea483..6035f7895b 100644 --- a/src/core/mustache/parse.rs +++ b/src/core/mustache/parse.rs @@ -16,13 +16,6 @@ impl Mustache { Err(_) => Mustache::from(vec![Segment::Literal(str.to_string())]), } } - - pub fn try_parse_optional(template: &str) -> Option { - match parse_mustache(template).finish() { - Ok((_, mustache)) => Some(mustache), - Err(_) => None, - } - } } fn parse_name(input: &str) -> IResult<&str, String> { From a502e71bfcc32a21ca53823a21a77acc96c279fd Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Wed, 4 Dec 2024 16:08:15 +0200 Subject: [PATCH 03/28] feat: extend DynamicValue with jq templates * The resulting value is encoded to string --- src/core/blueprint/dynamic_value.rs | 23 +- src/core/mustache/jq_template.rs | 273 +++++++++--------- src/core/mustache/mod.rs | 2 +- src/core/serde_value_ext.rs | 30 ++ .../test-expr-scalar-as-string.md_0.snap | 14 +- 5 files changed, 190 insertions(+), 152 deletions(-) diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index 4f7b2ef301..587755482b 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -2,12 +2,13 @@ use async_graphql_value::{ConstValue, Name}; use indexmap::IndexMap; use serde_json::Value; -use crate::core::mustache::{JqTemplate, Mustache}; +use crate::core::mustache::{JqTransformer, Mustache}; #[derive(Debug, Clone, PartialEq)] pub enum DynamicValue { Value(A), - Mustache(JqTemplate), + Mustache(Mustache), + JqTemplate(JqTransformer), Object(IndexMap>), Array(Vec>), } @@ -38,6 +39,7 @@ impl DynamicValue { DynamicValue::Mustache(mustache) } } + DynamicValue::JqTemplate(_) => self, DynamicValue::Object(index_map) => { let index_map = index_map .into_iter() @@ -62,6 +64,9 @@ impl TryFrom<&DynamicValue> for ConstValue { DynamicValue::Mustache(_) => Err(anyhow::anyhow!( "mustache cannot be converted to const value" )), + DynamicValue::JqTemplate(_) => Err(anyhow::anyhow!( + "jq template cannot be converted to const value" + )), DynamicValue::Object(obj) => { let out: Result, anyhow::Error> = obj .into_iter() @@ -83,6 +88,7 @@ impl DynamicValue { pub fn is_const(&self) -> bool { match self { DynamicValue::Mustache(m) => m.is_const(), + DynamicValue::JqTemplate(t) => t.is_const(), DynamicValue::Object(obj) => obj.values().all(|v| v.is_const()), DynamicValue::Array(arr) => arr.iter().all(|v| v.is_const()), _ => true, @@ -110,10 +116,17 @@ impl TryFrom<&Value> for DynamicValue { } Value::String(s) => { let m = Mustache::parse(s.as_str()); - if m.is_const() { - Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) + if !m.is_const() { + return Ok(DynamicValue::Mustache(m)); + } + if let Ok(t) = JqTransformer::try_new(s.as_str()) { + if t.is_const() { + Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) + } else { + Ok(DynamicValue::JqTemplate(t)) + } } else { - Ok(DynamicValue::Mustache(m)) + Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) } } _ => Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)), diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 5e3e80bdb7..3095e03e51 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -1,105 +1,29 @@ +use std::fmt::Display; use std::sync::Arc; -use jaq_core::{ - load::{parse::Term, Arena, File, Loader}, - Compiler, Ctx, Filter, Native, RcIter, ValR, -}; +use jaq_core::load::parse::Term; +use jaq_core::load::{Arena, File, Loader}; +use jaq_core::{Compiler, Ctx, Filter, Native, RcIter, ValR}; use jaq_json::Val; -use crate::core::ir::{EvalContext, ResolverContextLike}; - -use super::{Mustache, Segment}; - -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum JqTemplate { - Mustache(Mustache), - JqTemplate(Arc) -} - -impl JqTemplate { - pub fn render(&self, value: &serde_json::Value) -> String { - match self { - JqTemplate::Mustache(mustache) => mustache.render(value), - JqTemplate::JqTemplate(jq_transformer) => jq_transformer.render(value), - } - } - - pub fn render_graphql<'a, Ctx: ResolverContextLike>(&self, value: &EvalContext<'a, Ctx>) -> String { - match self { - JqTemplate::Mustache(mustache) => mustache.render_graphql(value), - JqTemplate::JqTemplate(jq_transformer) => { - let Some(value) = value.value() else { - return String::default() - }; - jq_transformer.render_graphql(value) - }, - } - } - - pub fn is_const(&self) -> bool { - match self { - JqTemplate::Mustache(mustache) => mustache.is_const(), - JqTemplate::JqTemplate(jq_transformer) => jq_transformer.is_const(), - } - } - - pub fn segments(&self) -> &Vec { - if let JqTemplate::Mustache(mustache) = self { - mustache.segments() - } else { - unimplemented!() - } - } - - pub fn segments_mut(&mut self) -> &mut Vec { - if let JqTemplate::Mustache(mustache) = self { - mustache.segments_mut() - } else { - unimplemented!() - } - } - - pub fn expression_segments(&self) -> Vec<&Vec> { - if let JqTemplate::Mustache(mustache) = self { - mustache.expression_segments() - } else { - unimplemented!() - } - } - - pub fn expression_contains(&self, expression: &str) -> bool { - if let JqTemplate::Mustache(mustache) = self { - mustache.expression_contains(expression) - } else { - unimplemented!() - } - } - - pub fn parse(template: &str) -> Self { - Self::Mustache(Mustache::parse(template)) - } -} - -impl Default for JqTemplate { - fn default() -> Self { - Self::Mustache(Mustache::default()) - } -} - +#[derive(Clone)] pub struct JqTransformer { - filter: Filter>, - term: Term<&'static str>, + filter: Arc>>, + representation: String, + is_const: bool, } impl JqTransformer { /// Used to parse a `template` and try to convert it into a JqTemplate - pub fn try_new(template: &'static str) -> Result { + pub fn try_new(template: &str) -> Result { // the term is used because it can be easily serialized, deserialized and hashed let term = Self::parse_template(template); + let is_const = Self::calculate_is_const(&term); // the template is used to be parsed in to the IR AST let template = File { code: template, path: () }; - // defs is used to extend the syntax with custom definitions of functions, like 'toString' + // defs is used to extend the syntax with custom definitions of functions, like + // 'toString' let defs = vec![]; // the loader is used to load custom modules let loader = Loader::new(defs); @@ -118,22 +42,30 @@ impl JqTransformer { ) })?; - Ok(Self { filter, term }) + Ok(Self { + filter: Arc::new(filter), + representation: format!("{:?}", term), + is_const, + }) } /// Used to execute the transformation of the JQTemplate - pub fn run<'a, T: std::iter::Iterator>>(&'a self, inputs: &'a RcIter,data: Val) -> impl Iterator> + 'a { + pub fn run<'a, T: std::iter::Iterator>>( + &'a self, + inputs: &'a RcIter, + data: Val, + ) -> impl Iterator> + 'a { let ctx = Ctx::new([], inputs); self.filter.run((ctx, data)) } - pub fn render(&self, value: &serde_json::Value) -> String { - self.render_helper(Val::from(value.clone())) + pub fn render(&self, value: serde_json::Value) -> String { + self.render_helper(Val::from(value)) } pub fn render_graphql(&self, value: &async_graphql_value::ConstValue) -> String { let Ok(value) = value.clone().into_json() else { - return String::default() + return String::default(); }; self.render_helper(Val::from(value)) @@ -143,19 +75,15 @@ impl JqTransformer { // the hardcoded inputs for the AST let inputs = RcIter::new(core::iter::empty()); let res = self.run(&inputs, value); - res.filter_map(|v| { - if let Ok(v) = v { - Some(v) - } else { - None - } - }).fold(String::new(), |acc, cur| { - let cur_string = cur.to_string(); - acc + &cur_string - }) + res.filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) + .fold(String::new(), |acc, cur| { + let cur_string = cur.to_string(); + acc + &cur_string + }) } - /// Used to determine if the expression can be supported with current Mustache implementation + /// Used to determine if the expression can be supported with current + /// Mustache implementation pub fn is_select_operation(template: &str) -> bool { let term = Self::parse_template(template); Self::recursive_is_select_operation(term) @@ -168,17 +96,18 @@ impl JqTransformer { parser.term().unwrap_or_default() } - /// Used as a helper function to determine if the term can be supported with Mustache implementation - fn recursive_is_select_operation(term: jaq_core::load::parse::Term<&str>) -> bool { + /// Used as a helper function to determine if the term can be supported with + /// Mustache implementation + fn recursive_is_select_operation(term: Term<&str>) -> bool { match term { - jaq_core::load::parse::Term::Id => true, - jaq_core::load::parse::Term::Recurse => false, - jaq_core::load::parse::Term::Num(_) => false, - jaq_core::load::parse::Term::Str(formater, _) => formater.is_none(), - jaq_core::load::parse::Term::Arr(_) => false, - jaq_core::load::parse::Term::Obj(_) => false, - jaq_core::load::parse::Term::Neg(_) => false, - jaq_core::load::parse::Term::Pipe(local_term_1, pattern, local_term_2) => { + Term::Id => true, + Term::Recurse => false, + Term::Num(_) => false, + Term::Str(formater, _) => formater.is_none(), + Term::Arr(_) => false, + Term::Obj(_) => false, + Term::Neg(_) => false, + Term::Pipe(local_term_1, pattern, local_term_2) => { if pattern.is_some() { false } else { @@ -186,62 +115,102 @@ impl JqTransformer { && Self::recursive_is_select_operation(*local_term_2) } } - jaq_core::load::parse::Term::BinOp(_, _, _) => false, - jaq_core::load::parse::Term::Label(_, _) => false, - jaq_core::load::parse::Term::Break(_) => false, - jaq_core::load::parse::Term::Fold(_, _, _, _) => false, - jaq_core::load::parse::Term::TryCatch(_, _) => false, - jaq_core::load::parse::Term::IfThenElse(_, _) => false, - jaq_core::load::parse::Term::Def(_, _) => false, - jaq_core::load::parse::Term::Call(_, _) => false, - jaq_core::load::parse::Term::Var(_) => false, - jaq_core::load::parse::Term::Path(local_term, path) => { + Term::BinOp(_, _, _) => false, + Term::Label(_, _) => false, + Term::Break(_) => false, + Term::Fold(_, _, _, _) => false, + Term::TryCatch(_, _) => false, + Term::IfThenElse(_, _) => false, + Term::Def(_, _) => false, + Term::Call(_, _) => false, + Term::Var(_) => false, + Term::Path(local_term, path) => { Self::recursive_is_select_operation(*local_term) && Self::is_path_select_operation(path) } } } - fn is_path_select_operation( - path: jaq_core::path::Path>, - ) -> bool { + fn is_path_select_operation(path: jaq_core::path::Path>) -> bool { path.0.into_iter().all(|part| match part { - (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Optional) => Self::recursive_is_select_operation(idx), - (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Essential) => Self::recursive_is_select_operation(idx), + (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Optional) => { + Self::recursive_is_select_operation(idx) + } + (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Essential) => { + Self::recursive_is_select_operation(idx) + } (jaq_core::path::Part::Range(_, _), jaq_core::path::Opt::Optional) => false, (jaq_core::path::Part::Range(_, _), jaq_core::path::Opt::Essential) => false, }) } + fn calculate_is_const(term: &Term<&str>) -> bool { + match term { + Term::Id => false, + Term::Recurse => false, + Term::Num(_) => true, + Term::Str(formater, _) => formater.is_none(), + Term::Arr(_) => false, + Term::Obj(_) => false, + Term::Neg(_) => false, + Term::Pipe(local_term_1, pattern, local_term_2) => { + if pattern.is_some() { + false + } else { + Self::calculate_is_const(local_term_1) && Self::calculate_is_const(local_term_2) + } + } + Term::BinOp(_, _, _) => false, + Term::Label(_, _) => false, + Term::Break(_) => false, + Term::Fold(_, _, _, _) => false, + Term::TryCatch(_, _) => false, + Term::IfThenElse(_, _) => false, + Term::Def(_, _) => false, + Term::Call(_, _) => false, + Term::Var(_) => false, + Term::Path(_, _) => false, + } + } + /// Used to determine if the transformer is a static value pub fn is_const(&self) -> bool { - // TODO: parse terms to determine the value - false + self.is_const } } impl Default for JqTransformer { fn default() -> Self { - Self { filter: Default::default(), term: Term::default() } + Self { + filter: Default::default(), + representation: String::default(), + is_const: true, + } } } impl std::fmt::Debug for JqTransformer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("JqTransformer").field("term", &self.term).finish() + f.debug_struct("JqTransformer") + .field("representation", &self.representation) + .finish() } } -impl ToString for JqTransformer { - fn to_string(&self) -> String { - format!("[JqTransformer]({:?})", self.term) +impl Display for JqTransformer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + format!( + "[JqTransformer](is_const={})({})", + self.is_const, self.representation + ) + .fmt(f) } } impl std::cmp::PartialEq for JqTransformer { fn eq(&self, other: &Self) -> bool { // TODO: sorry for the quick hack - format!("{:?}", self).eq(&format!("{:?}", other)) + self.representation.eq(&other.representation) } } @@ -268,36 +237,54 @@ mod tests { #[test] fn test_is_select_operation_simple_property() { let template = ".fruit"; - assert!(JqTransformer::is_select_operation(template), "Should return true for simple property access"); + assert!( + JqTransformer::is_select_operation(template), + "Should return true for simple property access" + ); } #[test] fn test_is_select_operation_nested_property() { let template = ".fruit.name"; - assert!(JqTransformer::is_select_operation(template), "Should return true for nested property access"); + assert!( + JqTransformer::is_select_operation(template), + "Should return true for nested property access" + ); } #[test] fn test_is_select_operation_array_index() { let template = ".fruits[1]"; - assert!(!JqTransformer::is_select_operation(template), "Should return false for array index access"); + assert!( + !JqTransformer::is_select_operation(template), + "Should return false for array index access" + ); } #[test] fn test_is_select_operation_pipe_operator() { let template = ".fruits[] | .name"; - assert!(!JqTransformer::is_select_operation(template), "Should return false for pipe operator usage"); + assert!( + !JqTransformer::is_select_operation(template), + "Should return false for pipe operator usage" + ); } #[test] fn test_is_select_operation_filter() { let template = ".fruits[] | select(.price > 1)"; - assert!(!JqTransformer::is_select_operation(template), "Should return false for select filter usage"); + assert!( + !JqTransformer::is_select_operation(template), + "Should return false for select filter usage" + ); } #[test] fn test_is_select_operation_function_call() { let template = "map(.price)"; - assert!(!JqTransformer::is_select_operation(template), "Should return false for function call"); + assert!( + !JqTransformer::is_select_operation(template), + "Should return false for function call" + ); } } diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index 705e3b965b..0ff81de498 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -3,5 +3,5 @@ mod jq_template; mod model; mod parse; pub use eval::{Eval, PathStringEval}; -pub use model::*; pub use jq_template::*; +pub use model::*; diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index 37a095ac6c..78b489cb86 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -23,6 +23,36 @@ impl ValueExt for DynamicValue { // but, we can just use that string as is .unwrap_or_else(|_| GraphQLValue::String(rendered.into_owned())) } + DynamicValue::JqTemplate(_t) => { + let value = if let Some(value) = ctx.path_string(&["value"]) { + serde_json::from_str(&value).unwrap() + } else { + serde_json::Value::Object(serde_json::Map::new()) + }; + + let vars = if let Some(vars) = ctx.path_string(&["vars"]) { + serde_json::from_str(&vars).unwrap() + } else { + serde_json::Value::Object(serde_json::Map::new()) + }; + + let args = if let Some(args) = ctx.path_string(&["args"]) { + serde_json::from_str(&args).unwrap() + } else { + serde_json::Value::Object(serde_json::Map::new()) + }; + + let data = serde_json::json!({ + "value": value, + "vars": vars, + "args": args, + }); + println!("_t: {:?}", _t); + let rendered = _t.render(data); + + serde_json::from_str::(rendered.as_ref()) + .unwrap_or_else(|_| GraphQLValue::String(rendered)) + } DynamicValue::Object(obj) => { let out: IndexMap<_, _> = obj .iter() diff --git a/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap b/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap index c7349486ac..cfd37fd39c 100644 --- a/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap +++ b/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap @@ -11,9 +11,15 @@ expression: response "data": { "entry": { "num": "0", - "arr": "[1, 2, 3]", + "arr": [ + 1, + 2, + 3 + ], "str": "test", - "obj": "{e: 1}", + "obj": { + "e": 1 + }, "bool": "true", "nested": { "num": 0, @@ -23,7 +29,9 @@ expression: response 3 ], "str": "test", - "obj": "{e: 1}", + "obj": { + "e": 1 + }, "bool": true } } From 1fd76f0c4de6651edcd6577664be3b79d32f2d4e Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 5 Dec 2024 10:43:24 +0200 Subject: [PATCH 04/28] fix: jq template to work with current implementation --- Cargo.lock | 1 + Cargo.toml | 1 + src/core/blueprint/dynamic_value.rs | 14 +- src/core/mustache/jq_template.rs | 28 +++- src/core/serde_value_ext.rs | 141 +++++++++++++++++- .../test-expr-scalar-as-string.md_0.snap | 2 +- .../core/snapshots/test-jq-template.md_0.snap | 20 +++ .../core/snapshots/test-jq-template.md_1.snap | 20 +++ .../snapshots/test-jq-template.md_client.snap | 26 ++++ .../snapshots/test-jq-template.md_merged.snap | 26 ++++ tests/execution/test-jq-template.md | 63 ++++++++ 11 files changed, 331 insertions(+), 11 deletions(-) create mode 100644 tests/core/snapshots/test-jq-template.md_0.snap create mode 100644 tests/core/snapshots/test-jq-template.md_1.snap create mode 100644 tests/core/snapshots/test-jq-template.md_client.snap create mode 100644 tests/core/snapshots/test-jq-template.md_merged.snap create mode 100644 tests/execution/test-jq-template.md diff --git a/Cargo.lock b/Cargo.lock index 08147de109..b2227a940e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5702,6 +5702,7 @@ dependencies = [ "insta", "jaq-core", "jaq-json", + "jaq-std", "jsonwebtoken", "lazy_static", "lru", diff --git a/Cargo.toml b/Cargo.toml index 1a8bf62e61..c8d335c8a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,6 +177,7 @@ dashmap = "6.1.0" urlencoding = "2.1.3" tailcall-chunk = "0.3.0" jaq-core = "2.0.0" +jaq-std = "2.0.0" jaq-json = { version = "1.0.0", features = ["serde_json"]} # to build rquickjs bindings on systems without builtin bindings diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index 587755482b..55f358218f 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -117,16 +117,22 @@ impl TryFrom<&Value> for DynamicValue { Value::String(s) => { let m = Mustache::parse(s.as_str()); if !m.is_const() { + tracing::info!("Successfully loaded Mustache template: {}", s); return Ok(DynamicValue::Mustache(m)); } - if let Ok(t) = JqTransformer::try_new(s.as_str()) { - if t.is_const() { + match JqTransformer::try_new(s.as_str()) { + Ok(t) => if t.is_const() { + tracing::info!("Successfully loaded const value template: {}", s); Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) } else { + tracing::info!("Successfully loaded JQ template: {}", s); Ok(DynamicValue::JqTemplate(t)) + }, + Err(err) => { + tracing::info!("Defaulting to const value template: {}", s); + tracing::warn!("JQ template error: {:?}", err); + Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) } - } else { - Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) } } _ => Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)), diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 3095e03e51..355b4e4308 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -6,6 +6,8 @@ use jaq_core::load::{Arena, File, Loader}; use jaq_core::{Compiler, Ctx, Filter, Native, RcIter, ValR}; use jaq_json::Val; +use crate::core::json::JsonLike; + #[derive(Clone)] pub struct JqTransformer { filter: Arc>>, @@ -24,7 +26,7 @@ impl JqTransformer { let template = File { code: template, path: () }; // defs is used to extend the syntax with custom definitions of functions, like // 'toString' - let defs = vec![]; + let defs = jaq_std::defs(); // the loader is used to load custom modules let loader = Loader::new(defs); // the arena is used to keep the loaded modules @@ -35,6 +37,7 @@ impl JqTransformer { })?; // the AST of the operation, used to transform the data let filter = Compiler::<_, Native>::default() + .with_funs(jaq_std::funs()) .compile(modules) .map_err(|errs| { JqTemplateError::JqCompileError( @@ -63,6 +66,28 @@ impl JqTransformer { self.render_helper(Val::from(value)) } + pub fn render_value(&self, value: serde_json::Value) -> async_graphql_value::ConstValue { + let inputs = RcIter::new(core::iter::empty()); + let res = self.run(&inputs, Val::from(value)); + let res: Vec = res + .into_iter() + // TODO: handle error correct, now we ignore it + .filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) + .map(serde_json::Value::from) + .map(async_graphql_value::ConstValue::from_json) + // TODO: handle error correct, now we ignore it + .filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) + .collect(); + let res_len = res.len(); + if res_len == 0 { + async_graphql_value::ConstValue::Null + } else if res_len == 1 { + res.into_iter().next().unwrap() + } else { + async_graphql_value::ConstValue::array(res) + } + } + pub fn render_graphql(&self, value: &async_graphql_value::ConstValue) -> String { let Ok(value) = value.clone().into_json() else { return String::default(); @@ -75,6 +100,7 @@ impl JqTransformer { // the hardcoded inputs for the AST let inputs = RcIter::new(core::iter::empty()); let res = self.run(&inputs, value); + // TODO: handle error correct, now we ignore it res.filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) .fold(String::new(), |acc, cur| { let cur_string = cur.to_string(); diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index 78b489cb86..996b821032 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -23,7 +23,7 @@ impl ValueExt for DynamicValue { // but, we can just use that string as is .unwrap_or_else(|_| GraphQLValue::String(rendered.into_owned())) } - DynamicValue::JqTemplate(_t) => { + DynamicValue::JqTemplate(t) => { let value = if let Some(value) = ctx.path_string(&["value"]) { serde_json::from_str(&value).unwrap() } else { @@ -47,11 +47,8 @@ impl ValueExt for DynamicValue { "vars": vars, "args": args, }); - println!("_t: {:?}", _t); - let rendered = _t.render(data); - serde_json::from_str::(rendered.as_ref()) - .unwrap_or_else(|_| GraphQLValue::String(rendered)) + t.render_value(data) } DynamicValue::Object(obj) => { let out: IndexMap<_, _> = obj @@ -192,4 +189,138 @@ mod tests { .unwrap(); assert_eq!(result, expected); } + + #[test] + fn test_jq_render_value() { + let value = json!({"a": ".value.foo"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"foo": {"bar": "baz"}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": {"bar": "baz"}})).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_jq_render_value_nested() { + let value = json!({"a": ".value.foo.bar.baz"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"foo": {"bar": {"baz": 1}}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": 1})).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_jq_render_value_nested_str() { + let value = json!({"a": ".value.foo.bar.baz"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"foo": {"bar": {"baz": "foo"}}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": "foo"})).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_jq_render_value_null() { + let value = json!(".value.foo.bar.baz"); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"foo": {"bar": {"baz": null}}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!(null)).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_jq_render_value_nested_bool() { + let value = json!({"a": ".value.foo.bar.baz"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"foo": {"bar": {"baz": true}}}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!({"a": true})).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_jq_render_value_nested_float() { + let value = json!({"a": ".value.foo.bar.baz"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"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, expected); + } + + #[test] + fn test_jq_render_value_arr() { + let value = json!({"a": ".value.foo.bar.baz"}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"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, expected); + } + + #[test] + fn test_jq_render_value_arr_template() { + let value = json!({"a": [".value.foo.bar.baz", ".value.foo.bar.qux"]}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"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, expected); + } + + #[test] + fn test_jq_or_value_is_const() { + let value = json!(".value.foo"); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"foo": "bar"}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::String("bar".to_owned()); + assert_eq!(result, expected); + } + + #[test] + fn test_jq_arr_obj() { + let value = json!({"a": [".value.foo.bar.baz", ".value.foo.bar.qux"]}); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"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, expected); + } + + #[test] + fn test_jq_arr_obj_arr() { + let value = + json!([{"a": [{"aa": ".value.foo.bar.baz"}]}, {"a": [{"aa": ".value.foo.bar.qux"}]}]); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"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, expected); + } + + #[test] + fn test_jq_create_obj() { + let value = json!(".value.foo | split(\" \") | {fizz: .[0], buzz: .[1]}"); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"foo": "hello world"}}); + let result = value.render_value(&ctx); + let expected = + async_graphql::Value::from_json(json!({"fizz": "hello", "buzz": "world"})).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_jq_create_arr() { + let value = json!(".value.foo | split(\" \")"); + let value = DynamicValue::try_from(&value).unwrap(); + let ctx = json!({"value": {"foo": "hello world"}}); + let result = value.render_value(&ctx); + let expected = async_graphql::Value::from_json(json!(["hello", "world"])).unwrap(); + assert_eq!(result, expected); + } } diff --git a/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap b/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap index cfd37fd39c..653dabd4c9 100644 --- a/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap +++ b/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap @@ -20,7 +20,7 @@ expression: response "obj": { "e": 1 }, - "bool": "true", + "bool": true, "nested": { "num": 0, "arr": [ diff --git a/tests/core/snapshots/test-jq-template.md_0.snap b/tests/core/snapshots/test-jq-template.md_0.snap new file mode 100644 index 0000000000..8d2bc26369 --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_0.snap @@ -0,0 +1,20 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "foo": { + "bar": [ + "fizz", + "buzz" + ] + } + } + } +} diff --git a/tests/core/snapshots/test-jq-template.md_1.snap b/tests/core/snapshots/test-jq-template.md_1.snap new file mode 100644 index 0000000000..53037c1fd1 --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_1.snap @@ -0,0 +1,20 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "fizz": { + "buzz": { + "first": "fizz", + "second": "buzz" + } + } + } + } +} diff --git a/tests/core/snapshots/test-jq-template.md_client.snap b/tests/core/snapshots/test-jq-template.md_client.snap new file mode 100644 index 0000000000..21fc9c800c --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_client.snap @@ -0,0 +1,26 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +type Buzz { + first: String! + second: String! +} + +type Fizz { + bar: String! + buzz: Buzz! +} + +type Foo { + bar: [String!]! +} + +type Query { + fizz: Fizz! + foo: Foo! +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/test-jq-template.md_merged.snap b/tests/core/snapshots/test-jq-template.md_merged.snap new file mode 100644 index 0000000000..0aa2f8d979 --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_merged.snap @@ -0,0 +1,26 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema @server(hostname: "0.0.0.0", port: 8000) @upstream { + query: Query +} + +type Buzz { + first: String! + second: String! +} + +type Fizz { + bar: String! + buzz: Buzz! @expr(body: ".value.bar | split(\" \") | {first: .[0], second: .[1]}") +} + +type Foo { + bar: [String!]! @expr(body: ".value.bar | split(\" \")") +} + +type Query { + fizz: Fizz! @http(url: "http://upstream/foo") + foo: Foo! @http(url: "http://upstream/foo") +} diff --git a/tests/execution/test-jq-template.md b/tests/execution/test-jq-template.md new file mode 100644 index 0000000000..edd16bea22 --- /dev/null +++ b/tests/execution/test-jq-template.md @@ -0,0 +1,63 @@ +# Basic queries with field ordering check + +```graphql @config +schema @server(port: 8000, hostname: "0.0.0.0") { + query: Query +} + +type Query { + foo: Foo! @http(url: "http://upstream/foo") + fizz: Fizz! @http(url: "http://upstream/foo") +} + +type Foo { + bar: String! + bar: [String!]! @expr(body: ".value.bar | split(\" \")") +} + +type Fizz { + bar: String! + buzz: Buzz! @expr(body: ".value.bar | split(\" \") | {first: .[0], second: .[1]}") +} + +type Buzz { + first: String! + second: String! +} +``` + +```yml @mock +- request: + method: GET + url: http://upstream/foo + expectedHits: 2 + response: + status: 200 + body: + bar: "fizz buzz" +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + foo { + bar + } + } + +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + fizz { + buzz { + first + second + } + } + } +``` From ffa4e9cf95b0f55bba6301c418c1689ca0c0eafd Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 5 Dec 2024 10:48:34 +0200 Subject: [PATCH 05/28] fix: lint --- src/core/blueprint/dynamic_value.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index 55f358218f..f40fcce74f 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -121,13 +121,15 @@ impl TryFrom<&Value> for DynamicValue { return Ok(DynamicValue::Mustache(m)); } match JqTransformer::try_new(s.as_str()) { - Ok(t) => if t.is_const() { - tracing::info!("Successfully loaded const value template: {}", s); - Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) - } else { - tracing::info!("Successfully loaded JQ template: {}", s); - Ok(DynamicValue::JqTemplate(t)) - }, + Ok(t) => { + if t.is_const() { + tracing::info!("Successfully loaded const value template: {}", s); + Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) + } else { + tracing::info!("Successfully loaded JQ template: {}", s); + Ok(DynamicValue::JqTemplate(t)) + } + } Err(err) => { tracing::info!("Defaulting to const value template: {}", s); tracing::warn!("JQ template error: {:?}", err); From 9cb72ee26193ac230ecf86d39324b9c433c4f020 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 5 Dec 2024 12:38:23 +0200 Subject: [PATCH 06/28] fix: add tests --- src/core/blueprint/dynamic_value.rs | 6 +- src/core/mustache/jq_template.rs | 162 +++++++++++++++++++++------- 2 files changed, 128 insertions(+), 40 deletions(-) diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index f40fcce74f..e2f408b9a9 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -2,13 +2,13 @@ use async_graphql_value::{ConstValue, Name}; use indexmap::IndexMap; use serde_json::Value; -use crate::core::mustache::{JqTransformer, Mustache}; +use crate::core::mustache::{JqTemplate, Mustache}; #[derive(Debug, Clone, PartialEq)] pub enum DynamicValue { Value(A), Mustache(Mustache), - JqTemplate(JqTransformer), + JqTemplate(JqTemplate), Object(IndexMap>), Array(Vec>), } @@ -120,7 +120,7 @@ impl TryFrom<&Value> for DynamicValue { tracing::info!("Successfully loaded Mustache template: {}", s); return Ok(DynamicValue::Mustache(m)); } - match JqTransformer::try_new(s.as_str()) { + match JqTemplate::try_new(s.as_str()) { Ok(t) => { if t.is_const() { tracing::info!("Successfully loaded const value template: {}", s); diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 355b4e4308..1776a195e1 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -8,18 +8,23 @@ use jaq_json::Val; use crate::core::json::JsonLike; +/// Used to represent a JQ template. Currently used only on @expr directive. #[derive(Clone)] -pub struct JqTransformer { +pub struct JqTemplate { + /// The compiled filter filter: Arc>>, + /// The IR representation, used for debug purposes representation: String, + /// If the transformer returns a constant value is_const: bool, } -impl JqTransformer { +impl JqTemplate { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { // the term is used because it can be easily serialized, deserialized and hashed let term = Self::parse_template(template); + // calculate if the expression returns always a constant value let is_const = Self::calculate_is_const(&term); // the template is used to be parsed in to the IR AST @@ -52,7 +57,7 @@ impl JqTransformer { }) } - /// Used to execute the transformation of the JQTemplate + /// Used to execute the transformation of the JqTemplate pub fn run<'a, T: std::iter::Iterator>>( &'a self, inputs: &'a RcIter, @@ -62,10 +67,20 @@ impl JqTransformer { self.filter.run((ctx, data)) } + /// Used to calculate the result and format it to string. Could be used in place of Mustache::render pub fn render(&self, value: serde_json::Value) -> String { - self.render_helper(Val::from(value)) + // the hardcoded inputs for the AST + let inputs = RcIter::new(core::iter::empty()); + let res = self.run(&inputs, Val::from(value)); + // TODO: handle error correct, now we ignore it + res.filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) + .fold(String::new(), |acc, cur| { + let cur_string = cur.to_string(); + acc + &cur_string + }) } + /// Used to calculate the result and return it as json pub fn render_value(&self, value: serde_json::Value) -> async_graphql_value::ConstValue { let inputs = RcIter::new(core::iter::empty()); let res = self.run(&inputs, Val::from(value)); @@ -88,26 +103,6 @@ impl JqTransformer { } } - pub fn render_graphql(&self, value: &async_graphql_value::ConstValue) -> String { - let Ok(value) = value.clone().into_json() else { - return String::default(); - }; - - self.render_helper(Val::from(value)) - } - - fn render_helper(&self, value: Val) -> String { - // the hardcoded inputs for the AST - let inputs = RcIter::new(core::iter::empty()); - let res = self.run(&inputs, value); - // TODO: handle error correct, now we ignore it - res.filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) - .fold(String::new(), |acc, cur| { - let cur_string = cur.to_string(); - acc + &cur_string - }) - } - /// Used to determine if the expression can be supported with current /// Mustache implementation pub fn is_select_operation(template: &str) -> bool { @@ -115,6 +110,7 @@ impl JqTransformer { Self::recursive_is_select_operation(term) } + /// Used to parse the template string and return the IR representation fn parse_template(template: &str) -> Term<&str> { let lexer = jaq_core::load::Lexer::new(template); let lex = lexer.lex().unwrap_or_default(); @@ -157,6 +153,7 @@ impl JqTransformer { } } + /// Used to check if the path indicates a select operation or modify fn is_path_select_operation(path: jaq_core::path::Path>) -> bool { path.0.into_iter().all(|part| match part { (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Optional) => { @@ -170,6 +167,7 @@ impl JqTransformer { }) } + /// Used to calcuate if the template always returns a constant value fn calculate_is_const(term: &Term<&str>) -> bool { match term { Term::Id => false, @@ -205,7 +203,7 @@ impl JqTransformer { } } -impl Default for JqTransformer { +impl Default for JqTemplate { fn default() -> Self { Self { filter: Default::default(), @@ -215,32 +213,32 @@ impl Default for JqTransformer { } } -impl std::fmt::Debug for JqTransformer { +impl std::fmt::Debug for JqTemplate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("JqTransformer") + f.debug_struct("JqTemplate") .field("representation", &self.representation) .finish() } } -impl Display for JqTransformer { +impl Display for JqTemplate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { format!( - "[JqTransformer](is_const={})({})", + "[JqTemplate](is_const={})({})", self.is_const, self.representation ) .fmt(f) } } -impl std::cmp::PartialEq for JqTransformer { +impl std::cmp::PartialEq for JqTemplate { fn eq(&self, other: &Self) -> bool { // TODO: sorry for the quick hack self.representation.eq(&other.representation) } } -impl std::hash::Hash for JqTransformer { +impl std::hash::Hash for JqTemplate { fn hash(&self, state: &mut H) { self.to_string().hash(state); } @@ -259,12 +257,14 @@ pub enum JqTemplateError { #[cfg(test)] mod tests { use super::*; + use serde_json::json; + use jaq_core::load::parse::{BinaryOp, Pattern, Term}; #[test] fn test_is_select_operation_simple_property() { let template = ".fruit"; assert!( - JqTransformer::is_select_operation(template), + JqTemplate::is_select_operation(template), "Should return true for simple property access" ); } @@ -273,7 +273,7 @@ mod tests { fn test_is_select_operation_nested_property() { let template = ".fruit.name"; assert!( - JqTransformer::is_select_operation(template), + JqTemplate::is_select_operation(template), "Should return true for nested property access" ); } @@ -282,7 +282,7 @@ mod tests { fn test_is_select_operation_array_index() { let template = ".fruits[1]"; assert!( - !JqTransformer::is_select_operation(template), + !JqTemplate::is_select_operation(template), "Should return false for array index access" ); } @@ -291,7 +291,7 @@ mod tests { fn test_is_select_operation_pipe_operator() { let template = ".fruits[] | .name"; assert!( - !JqTransformer::is_select_operation(template), + !JqTemplate::is_select_operation(template), "Should return false for pipe operator usage" ); } @@ -300,7 +300,7 @@ mod tests { fn test_is_select_operation_filter() { let template = ".fruits[] | select(.price > 1)"; assert!( - !JqTransformer::is_select_operation(template), + !JqTemplate::is_select_operation(template), "Should return false for select filter usage" ); } @@ -309,8 +309,96 @@ mod tests { fn test_is_select_operation_function_call() { let template = "map(.price)"; assert!( - !JqTransformer::is_select_operation(template), + !JqTemplate::is_select_operation(template), "Should return false for function call" ); } + + #[test] + fn test_render() { + let template_str = ".[] | .foo"; + let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + + let input_json = json!([ + {"foo": 1}, + {"foo": 2} + ]); + + let expected_output = "12"; + let actual_output = jq_template.render(input_json); + + assert_eq!(actual_output, expected_output, "The rendered output did not match the expected output."); + } + + #[test] + fn test_calculate_is_const() { + // Test with a constant number + let term_num = Term::Num("42"); + assert!(JqTemplate::calculate_is_const(&term_num), "Expected true for a constant number"); + + // Test with a string without formatter + let term_str = Term::Str(None, vec![]); + assert!(JqTemplate::calculate_is_const(&term_str), "Expected true for a simple string"); + + // Test with a string with formatter + let term_str_fmt = Term::Str(Some("fmt"), vec![]); + assert!(!JqTemplate::calculate_is_const(&term_str_fmt), "Expected false for a formatted string"); + + // Test with an identity operation + let term_id = Term::Id; + assert!(!JqTemplate::calculate_is_const(&term_id), "Expected false for an identity operation"); + + // Test with a recursive operation + let term_recurse = Term::Recurse; + assert!(!JqTemplate::calculate_is_const(&term_recurse), "Expected false for a recursive operation"); + + // Test with a binary operation + let term_bin_op = Term::BinOp(Box::new(Term::Num("1")), BinaryOp::Math(jaq_core::ops::Math::Add), Box::new(Term::Num("2"))); + assert!(!JqTemplate::calculate_is_const(&term_bin_op), "Expected false for a binary operation"); + + // Test with a pipe operation without pattern + let term_pipe = Term::Pipe(Box::new(Term::Num("1")), None, Box::new(Term::Num("2"))); + assert!(JqTemplate::calculate_is_const(&term_pipe), "Expected true for a constant pipe operation"); + + // Test with a pipe operation with pattern + let pattern = Pattern::Var("x"); + let term_pipe_with_pattern = Term::Pipe(Box::new(Term::Num("1")), Some(pattern), Box::new(Term::Num("2"))); + assert!(!JqTemplate::calculate_is_const(&term_pipe_with_pattern), "Expected false for a pipe operation with pattern"); + } + + #[test] + fn test_recursive_is_select_operation() { + // Test with simple identity operation + let term_id = Term::Id; + assert!(JqTemplate::recursive_is_select_operation(term_id), "Expected true for identity operation"); + + // Test with a number + let term_num = Term::Num("42"); + assert!(!JqTemplate::recursive_is_select_operation(term_num), "Expected false for a number"); + + // Test with a string without formatter + let term_str = Term::Str(None, vec![]); + assert!(JqTemplate::recursive_is_select_operation(term_str), "Expected true for a simple string"); + + // Test with a string with formatter + let term_str_fmt = Term::Str(Some("fmt"), vec![]); + assert!(!JqTemplate::recursive_is_select_operation(term_str_fmt), "Expected false for a formatted string"); + + // Test with a recursive operation + let term_recurse = Term::Recurse; + assert!(!JqTemplate::recursive_is_select_operation(term_recurse), "Expected false for a recursive operation"); + + // Test with a binary operation + let term_bin_op = Term::BinOp(Box::new(Term::Num("1")), BinaryOp::Math(jaq_core::ops::Math::Add), Box::new(Term::Num("2"))); + assert!(!JqTemplate::recursive_is_select_operation(term_bin_op), "Expected false for a binary operation"); + + // Test with a pipe operation without pattern + let term_pipe = Term::Pipe(Box::new(Term::Num("1")), None, Box::new(Term::Num("2"))); + assert!(!JqTemplate::recursive_is_select_operation(term_pipe), "Expected false for a constant pipe operation"); + + // Test with a pipe operation with pattern + let pattern = Pattern::Var("x"); + let term_pipe_with_pattern = Term::Pipe(Box::new(Term::Num("1")), Some(pattern), Box::new(Term::Num("2"))); + assert!(!JqTemplate::recursive_is_select_operation(term_pipe_with_pattern), "Expected false for a pipe operation with pattern"); + } } From ef00eb850f4a3361ea7c9afe7425644365b824c6 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 5 Dec 2024 12:45:02 +0200 Subject: [PATCH 07/28] fix: lint --- src/core/mustache/jq_template.rs | 117 ++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 24 deletions(-) diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 1776a195e1..7a3f597b3f 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -67,7 +67,8 @@ impl JqTemplate { self.filter.run((ctx, data)) } - /// Used to calculate the result and format it to string. Could be used in place of Mustache::render + /// Used to calculate the result and format it to string. Could be used in + /// place of Mustache::render pub fn render(&self, value: serde_json::Value) -> String { // the hardcoded inputs for the AST let inputs = RcIter::new(core::iter::empty()); @@ -256,9 +257,10 @@ pub enum JqTemplateError { #[cfg(test)] mod tests { - use super::*; - use serde_json::json; use jaq_core::load::parse::{BinaryOp, Pattern, Term}; + use serde_json::json; + + use super::*; #[test] fn test_is_select_operation_simple_property() { @@ -327,78 +329,145 @@ mod tests { let expected_output = "12"; let actual_output = jq_template.render(input_json); - assert_eq!(actual_output, expected_output, "The rendered output did not match the expected output."); + assert_eq!( + actual_output, expected_output, + "The rendered output did not match the expected output." + ); } #[test] fn test_calculate_is_const() { // Test with a constant number let term_num = Term::Num("42"); - assert!(JqTemplate::calculate_is_const(&term_num), "Expected true for a constant number"); + assert!( + JqTemplate::calculate_is_const(&term_num), + "Expected true for a constant number" + ); // Test with a string without formatter let term_str = Term::Str(None, vec![]); - assert!(JqTemplate::calculate_is_const(&term_str), "Expected true for a simple string"); + assert!( + JqTemplate::calculate_is_const(&term_str), + "Expected true for a simple string" + ); // Test with a string with formatter let term_str_fmt = Term::Str(Some("fmt"), vec![]); - assert!(!JqTemplate::calculate_is_const(&term_str_fmt), "Expected false for a formatted string"); + assert!( + !JqTemplate::calculate_is_const(&term_str_fmt), + "Expected false for a formatted string" + ); // Test with an identity operation let term_id = Term::Id; - assert!(!JqTemplate::calculate_is_const(&term_id), "Expected false for an identity operation"); + assert!( + !JqTemplate::calculate_is_const(&term_id), + "Expected false for an identity operation" + ); // Test with a recursive operation let term_recurse = Term::Recurse; - assert!(!JqTemplate::calculate_is_const(&term_recurse), "Expected false for a recursive operation"); + assert!( + !JqTemplate::calculate_is_const(&term_recurse), + "Expected false for a recursive operation" + ); // Test with a binary operation - let term_bin_op = Term::BinOp(Box::new(Term::Num("1")), BinaryOp::Math(jaq_core::ops::Math::Add), Box::new(Term::Num("2"))); - assert!(!JqTemplate::calculate_is_const(&term_bin_op), "Expected false for a binary operation"); + let term_bin_op = Term::BinOp( + Box::new(Term::Num("1")), + BinaryOp::Math(jaq_core::ops::Math::Add), + Box::new(Term::Num("2")), + ); + assert!( + !JqTemplate::calculate_is_const(&term_bin_op), + "Expected false for a binary operation" + ); // Test with a pipe operation without pattern let term_pipe = Term::Pipe(Box::new(Term::Num("1")), None, Box::new(Term::Num("2"))); - assert!(JqTemplate::calculate_is_const(&term_pipe), "Expected true for a constant pipe operation"); + assert!( + JqTemplate::calculate_is_const(&term_pipe), + "Expected true for a constant pipe operation" + ); // Test with a pipe operation with pattern let pattern = Pattern::Var("x"); - let term_pipe_with_pattern = Term::Pipe(Box::new(Term::Num("1")), Some(pattern), Box::new(Term::Num("2"))); - assert!(!JqTemplate::calculate_is_const(&term_pipe_with_pattern), "Expected false for a pipe operation with pattern"); + let term_pipe_with_pattern = Term::Pipe( + Box::new(Term::Num("1")), + Some(pattern), + Box::new(Term::Num("2")), + ); + assert!( + !JqTemplate::calculate_is_const(&term_pipe_with_pattern), + "Expected false for a pipe operation with pattern" + ); } #[test] fn test_recursive_is_select_operation() { // Test with simple identity operation let term_id = Term::Id; - assert!(JqTemplate::recursive_is_select_operation(term_id), "Expected true for identity operation"); + assert!( + JqTemplate::recursive_is_select_operation(term_id), + "Expected true for identity operation" + ); // Test with a number let term_num = Term::Num("42"); - assert!(!JqTemplate::recursive_is_select_operation(term_num), "Expected false for a number"); + assert!( + !JqTemplate::recursive_is_select_operation(term_num), + "Expected false for a number" + ); // Test with a string without formatter let term_str = Term::Str(None, vec![]); - assert!(JqTemplate::recursive_is_select_operation(term_str), "Expected true for a simple string"); + assert!( + JqTemplate::recursive_is_select_operation(term_str), + "Expected true for a simple string" + ); // Test with a string with formatter let term_str_fmt = Term::Str(Some("fmt"), vec![]); - assert!(!JqTemplate::recursive_is_select_operation(term_str_fmt), "Expected false for a formatted string"); + assert!( + !JqTemplate::recursive_is_select_operation(term_str_fmt), + "Expected false for a formatted string" + ); // Test with a recursive operation let term_recurse = Term::Recurse; - assert!(!JqTemplate::recursive_is_select_operation(term_recurse), "Expected false for a recursive operation"); + assert!( + !JqTemplate::recursive_is_select_operation(term_recurse), + "Expected false for a recursive operation" + ); // Test with a binary operation - let term_bin_op = Term::BinOp(Box::new(Term::Num("1")), BinaryOp::Math(jaq_core::ops::Math::Add), Box::new(Term::Num("2"))); - assert!(!JqTemplate::recursive_is_select_operation(term_bin_op), "Expected false for a binary operation"); + let term_bin_op = Term::BinOp( + Box::new(Term::Num("1")), + BinaryOp::Math(jaq_core::ops::Math::Add), + Box::new(Term::Num("2")), + ); + assert!( + !JqTemplate::recursive_is_select_operation(term_bin_op), + "Expected false for a binary operation" + ); // Test with a pipe operation without pattern let term_pipe = Term::Pipe(Box::new(Term::Num("1")), None, Box::new(Term::Num("2"))); - assert!(!JqTemplate::recursive_is_select_operation(term_pipe), "Expected false for a constant pipe operation"); + assert!( + !JqTemplate::recursive_is_select_operation(term_pipe), + "Expected false for a constant pipe operation" + ); // Test with a pipe operation with pattern let pattern = Pattern::Var("x"); - let term_pipe_with_pattern = Term::Pipe(Box::new(Term::Num("1")), Some(pattern), Box::new(Term::Num("2"))); - assert!(!JqTemplate::recursive_is_select_operation(term_pipe_with_pattern), "Expected false for a pipe operation with pattern"); + let term_pipe_with_pattern = Term::Pipe( + Box::new(Term::Num("1")), + Some(pattern), + Box::new(Term::Num("2")), + ); + assert!( + !JqTemplate::recursive_is_select_operation(term_pipe_with_pattern), + "Expected false for a pipe operation with pattern" + ); } } From 1406a8d3d6d8672783e06fda570d19fa85052a26 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 5 Dec 2024 13:59:27 +0200 Subject: [PATCH 08/28] fix: add more tests --- src/core/mustache/jq_template.rs | 125 ++++++++++++++++++++++++------- 1 file changed, 98 insertions(+), 27 deletions(-) diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 7a3f597b3f..5b3fd609a0 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -67,20 +67,6 @@ impl JqTemplate { self.filter.run((ctx, data)) } - /// Used to calculate the result and format it to string. Could be used in - /// place of Mustache::render - pub fn render(&self, value: serde_json::Value) -> String { - // the hardcoded inputs for the AST - let inputs = RcIter::new(core::iter::empty()); - let res = self.run(&inputs, Val::from(value)); - // TODO: handle error correct, now we ignore it - res.filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) - .fold(String::new(), |acc, cur| { - let cur_string = cur.to_string(); - acc + &cur_string - }) - } - /// Used to calculate the result and return it as json pub fn render_value(&self, value: serde_json::Value) -> async_graphql_value::ConstValue { let inputs = RcIter::new(core::iter::empty()); @@ -234,7 +220,6 @@ impl Display for JqTemplate { impl std::cmp::PartialEq for JqTemplate { fn eq(&self, other: &Self) -> bool { - // TODO: sorry for the quick hack self.representation.eq(&other.representation) } } @@ -257,6 +242,8 @@ pub enum JqTemplateError { #[cfg(test)] mod tests { + use std::hash::{DefaultHasher, Hash, Hasher}; + use jaq_core::load::parse::{BinaryOp, Pattern, Term}; use serde_json::json; @@ -317,24 +304,44 @@ mod tests { } #[test] - fn test_render() { - let template_str = ".[] | .foo"; + fn test_render_value_no_results() { + let template_str = ".[] | select(.non_existent)"; let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(input_json); + assert_eq!( + result, + async_graphql_value::ConstValue::Null, + "Expected Null for no results" + ); + } - let input_json = json!([ - {"foo": 1}, - {"foo": 2} - ]); - - let expected_output = "12"; - let actual_output = jq_template.render(input_json); - + #[test] + fn test_render_value_single_result() { + let template_str = ".[0]"; + let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(input_json); assert_eq!( - actual_output, expected_output, - "The rendered output did not match the expected output." + result, + async_graphql_value::ConstValue::from_json(json!({"foo": 1})).unwrap(), + "Expected single result" ); } + #[test] + fn test_render_value_multiple_results() { + let template_str = ".[] | .foo"; + let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(input_json); + let expected = async_graphql_value::ConstValue::array(vec![ + async_graphql_value::ConstValue::from_json(json!(1)).unwrap(), + async_graphql_value::ConstValue::from_json(json!(2)).unwrap(), + ]); + assert_eq!(result, expected, "Expected array of results"); + } + #[test] fn test_calculate_is_const() { // Test with a constant number @@ -470,4 +477,68 @@ mod tests { "Expected false for a pipe operation with pattern" ); } + + #[test] + fn test_default() { + let jq_template = JqTemplate::default(); + assert_eq!(jq_template.representation, ""); + assert!(jq_template.is_const); + // Assuming `filter` has a sensible default implementation + } + + #[test] + fn test_debug() { + let jq_template = JqTemplate { + filter: Arc::new(Filter::default()), + representation: "test".to_string(), + is_const: false, + }; + let debug_string = format!("{:?}", jq_template); + assert_eq!(debug_string, "JqTemplate { representation: \"test\" }"); + } + + #[test] + fn test_display() { + let jq_template = JqTemplate { + filter: Arc::new(Filter::default()), + representation: "test".to_string(), + is_const: false, + }; + let display_string = format!("{}", jq_template); + assert_eq!(display_string, "[JqTemplate](is_const=false)(test)"); + } + + #[test] + fn test_partial_eq() { + let jq_template1 = JqTemplate { + filter: Arc::new(Filter::default()), + representation: "test".to_string(), + is_const: false, + }; + let jq_template2 = JqTemplate { + filter: Arc::new(Filter::default()), + representation: "test".to_string(), + is_const: true, // Different `is_const` value should not affect equality + }; + assert_eq!(jq_template1, jq_template2); + } + + #[test] + fn test_hash() { + let jq_template1 = JqTemplate { + filter: Arc::new(Filter::default()), + representation: "test".to_string(), + is_const: false, + }; + let jq_template2 = JqTemplate { + filter: Arc::new(Filter::default()), + representation: "test".to_string(), + is_const: false, + }; + let mut hasher1 = DefaultHasher::new(); + let mut hasher2 = DefaultHasher::new(); + jq_template1.hash(&mut hasher1); + jq_template2.hash(&mut hasher2); + assert_eq!(hasher1.finish(), hasher2.finish()); + } } From f3de14d655552c994fe1a65ff78a4dbe7d5e2def Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 5 Dec 2024 15:00:37 +0200 Subject: [PATCH 09/28] feat: jq with mustache --- src/core/blueprint/dynamic_value.rs | 1 + src/core/mustache/jq_template.rs | 84 ++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index e2f408b9a9..b0134e6dbb 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -120,6 +120,7 @@ impl TryFrom<&Value> for DynamicValue { tracing::info!("Successfully loaded Mustache template: {}", s); return Ok(DynamicValue::Mustache(m)); } + match JqTemplate::try_new(s.as_str()) { Ok(t) => { if t.is_const() { diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 5b3fd609a0..2e5c069325 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -5,6 +5,7 @@ use jaq_core::load::parse::Term; use jaq_core::load::{Arena, File, Loader}; use jaq_core::{Compiler, Ctx, Filter, Native, RcIter, ValR}; use jaq_json::Val; +use regex::Regex; use crate::core::json::JsonLike; @@ -22,13 +23,15 @@ pub struct JqTemplate { impl JqTemplate { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { + let template = transform_to_jq(template); + // the term is used because it can be easily serialized, deserialized and hashed - let term = Self::parse_template(template); + let term = Self::parse_template(&template); // calculate if the expression returns always a constant value let is_const = Self::calculate_is_const(&term); // the template is used to be parsed in to the IR AST - let template = File { code: template, path: () }; + let template = File { code: template.as_str(), path: () }; // defs is used to extend the syntax with custom definitions of functions, like // 'toString' let defs = jaq_std::defs(); @@ -240,6 +243,54 @@ pub enum JqTemplateError { JqCompileError(Vec), } +/// Used to convert mustache to jq +fn transform_to_jq(input: &str) -> String { + let re = Regex::new(r"\{\{\.([^}]*)\}\}").unwrap(); + let mut result = String::new(); + let mut last_end = 0; + let captures: Vec<_> = re.captures_iter(input).collect(); + + // when we do not have any mustache templates return the string + if captures.is_empty() { + return input.to_string(); + } + + for cap in captures { + let match_ = cap.get(0).unwrap(); + let var_name = cap.get(1).unwrap().as_str(); + + // Append the text before the match, then the transformed variable + if last_end != match_.start() { + if !result.is_empty() { + result.push_str(" + "); + } + result.push_str(&format!("\"{}\"", &input[last_end..match_.start()])); + } + + if !result.is_empty() { + result.push_str(" + "); + } + result.push_str(&format!(".{}", var_name)); + + last_end = match_.end(); + } + + // Append any remaining text after the last match + if last_end < input.len() { + if !result.is_empty() { + result.push_str(" + "); + } + result.push_str(&format!("\"{}\"", &input[last_end..])); + } + + // If no transformations were made, return the original input + if result.is_empty() { + return input.to_string(); + } + + result +} + #[cfg(test)] mod tests { use std::hash::{DefaultHasher, Hash, Hasher}; @@ -541,4 +592,33 @@ mod tests { jq_template2.hash(&mut hasher2); assert_eq!(hasher1.finish(), hasher2.finish()); } + + #[test] + fn test_transform_to_jq() { + assert_eq!( + transform_to_jq("Hello world: {{.foo.buzz | split(\" \")}}"), + "\"Hello world: \" + .foo.buzz | split(\" \")" + ); + assert_eq!( + transform_to_jq("Hello world: {{.foo.buzz | split(\" \")}} this is great"), + "\"Hello world: \" + .foo.buzz | split(\" \") + \" this is great\"" + ); + assert_eq!( + transform_to_jq("{{.foo.buzz | split(\" \")}} buzz"), + ".foo.buzz | split(\" \") + \" buzz\"" + ); + assert_eq!( + transform_to_jq("{{.foo.buzz | split(\" \")}} of type {{.bar}}"), + ".foo.buzz | split(\" \") + \" of type \" + .bar" + ); + } + + #[test] + fn test_transform_to_jq_identity() { + assert_eq!(transform_to_jq("Hello world"), "Hello world"); + assert_eq!( + transform_to_jq(".foo.buzz | split(\" \")"), + ".foo.buzz | split(\" \")" + ); + } } From ae9ebb062e66d383ca3645a7aecef0da6461c953 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Fri, 6 Dec 2024 00:38:41 +0200 Subject: [PATCH 10/28] wip: impl ValT --- src/core/mustache/jq_template.rs | 395 ++++++++++++++++++++++++++----- src/core/path.rs | 6 + src/core/serde_value_ext.rs | 32 +-- 3 files changed, 345 insertions(+), 88 deletions(-) diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 2e5c069325..4e504cceb1 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -3,24 +3,298 @@ use std::sync::Arc; use jaq_core::load::parse::Term; use jaq_core::load::{Arena, File, Loader}; -use jaq_core::{Compiler, Ctx, Filter, Native, RcIter, ValR}; +use jaq_core::{Compiler, Ctx, Error, Filter, Native, RcIter, ValR, ValT}; use jaq_json::Val; use regex::Regex; +use crate::core::ir::{EvalContext, ResolverContextLike}; use crate::core::json::JsonLike; +use crate::core::path::{PathString, ValueString}; /// Used to represent a JQ template. Currently used only on @expr directive. #[derive(Clone)] pub struct JqTemplate { /// The compiled filter - filter: Arc>>, + filter: Arc>>, /// The IR representation, used for debug purposes representation: String, /// If the transformer returns a constant value is_const: bool, } -impl JqTemplate { +#[derive(Clone)] +pub enum PathValueEnum { + PathValue(Arc), + Val(Val) +} + +pub trait PathJqValue { + fn get_value<'a>(&'a self, path: &str) -> Option>; +} + +impl PathJqValue for EvalContext<'_, Ctx> { + fn get_value<'a>(&'a self, path: &str) -> Option> { + todo!() + } +} + +impl PathJqValue for serde_json::Value { + fn get_value<'a>(&'a self, path: &str) -> Option> { + todo!() + } +} +pub trait PathJqValueString: PathString + PathJqValue {} + +impl PathJqValueString for EvalContext<'_, Ctx> {} + +impl PathJqValueString for serde_json::Value {} + + +impl ValT for PathValueEnum { + fn from_num(n: &str) -> ValR { + match Val::from_num(n) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + }, + } + } + + fn from_map>(iter: I) -> ValR { + let result: Result, String> = iter.into_iter().map(|(k, v)| { + match (k, v) { + (PathValueEnum::Val(key), PathValueEnum::Val(value)) => Ok((key, value)), + _ => Err("Invalid key or value type for map".into()) + } + }).collect(); + + match result { + Ok(pairs) => { + match Val::from_map(pairs.into_iter()) { + Ok(val) => ValR::Ok(PathValueEnum::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + }, + } + }, + Err(e) => Err(Error::new(Self::Val(Val::from(e)))) + } + } + + fn values(self) -> Box>> { + match self { + PathValueEnum::PathValue(_context) => { + // Create a new error message each time to avoid lifetime issues + let error_message = "Cannot iterate context.".to_string(); + let error_val = Val::from(error_message); + let error = Error::new(Self::Val(error_val)); + Box::new(std::iter::once(Err(error))) + }, + PathValueEnum::Val(val) => { + Box::new(std::iter::once(Ok(PathValueEnum::Val(val)))) + } + } + } + + fn index(self, index: &Self) -> ValR { + let PathValueEnum::Val(index) = index else { + return ValR::Err(Error::new(Self::Val(Val::from(format!("Could not convert index `{}` val.", index))))); + }; + + match self { + PathValueEnum::PathValue(pv) => { + let Some(index) = index.as_str() else { + return ValR::Err(Error::new(Self::Val(Val::from(format!("Could not convert index `{}` to string.", index))))); + }; + + let Some(v) = pv.get_value(index) else { + return ValR::Err(Error::new(Self::Val(Val::from(format!("Could not find key `{}` in context.", index))))); + }; + + match v { + crate::core::path::ValueString::Value(cow) => { + let cv = cow.as_ref().clone(); + match cv.into_json() { + Ok(js) => Ok(Self::Val(Val::from(js))), + Err(err) => ValR::Err(Error::new(Self::Val(Val::from(format!("Could not convert value to json: {:?}", err))))), + } + }, + crate::core::path::ValueString::String(cow) => { + let v = cow.to_string(); + Ok(Self::Val(Val::from(v))) + }, + } + }, + PathValueEnum::Val(val) => { + match val.index(index) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + }, + } + }, + } + } + + fn range(self, range: jaq_core::val::Range<&Self>) -> ValR { + todo!() + } + + fn map_values<'a, I: Iterator>>( + self, + opt: jaq_core::path::Opt, + f: impl Fn(Self) -> I, + ) -> jaq_core::ValX<'a, Self> { + todo!() + } + + fn map_index<'a, I: Iterator>>( + self, + index: &Self, + opt: jaq_core::path::Opt, + f: impl Fn(Self) -> I, + ) -> jaq_core::ValX<'a, Self> { + todo!() + } + + fn map_range<'a, I: Iterator>>( + self, + range: jaq_core::val::Range<&Self>, + opt: jaq_core::path::Opt, + f: impl Fn(Self) -> I, + ) -> jaq_core::ValX<'a, Self> { + todo!() + } + + fn as_bool(&self) -> bool { + todo!() + } + + fn as_str(&self) -> Option<&str> { + match self { + PathValueEnum::PathValue(_) => None, + PathValueEnum::Val(val) => val.as_str(), + } + } +} + +impl FromIterator for PathValueEnum { + fn from_iter>(iter: I) -> Self { + todo!() + } +} + +impl std::ops::Add for PathValueEnum { + type Output = ValR; + + fn add(self, rhs: Self) -> Self::Output { + todo!() + } +} + +impl std::ops::Sub for PathValueEnum { + type Output = ValR; + + fn sub(self, rhs: Self) -> Self::Output { + todo!() + } +} + +impl std::ops::Mul for PathValueEnum { + type Output = ValR; + + fn mul(self, rhs: Self) -> Self::Output { + todo!() + } +} + +impl std::ops::Div for PathValueEnum { + type Output = ValR; + + fn div(self, rhs: Self) -> Self::Output { + todo!() + } +} + +impl std::ops::Rem for PathValueEnum { + type Output = ValR; + + fn rem(self, rhs: Self) -> Self::Output { + todo!() + } +} + +impl std::ops::Neg for PathValueEnum { + type Output = ValR; + + fn neg(self) -> Self::Output { + todo!() + } +} + +impl Display for PathValueEnum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // match self { + // PathValueEnum::PathValue(_) => "[PathValue]".to_string().fmt(f), + // PathValueEnum::Val(val) => val.fmt(f), + // } + todo!() + } +} + +impl std::fmt::Debug for PathValueEnum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // match self { + // Self::PathValue(arg0) => f.debug_tuple("PathValue").field(arg0).finish(), + // Self::Val(arg0) => f.debug_tuple("Val").field(arg0).finish(), + // } + todo!() + } +} + +impl PartialEq for PathValueEnum { + fn eq(&self, other: &Self) -> bool { + todo!() + } +} + +impl PartialOrd for PathValueEnum { + fn partial_cmp(&self, other: &Self) -> Option { + todo!() + } +} + +impl Into for PathValueEnum { + fn into(self) -> serde_json::Value { + match self { + PathValueEnum::PathValue(_) => todo!(), + PathValueEnum::Val(val) => serde_json::Value::from(val), + } + } +} + +impl From for PathValueEnum { + fn from(value: String) -> Self { + todo!() + } +} + +impl From for PathValueEnum { + fn from(value: isize) -> Self { + todo!() + } +} + +impl From for PathValueEnum { + fn from(value: bool) -> Self { + todo!() + } +} + +impl JqTemplate { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { let template = transform_to_jq(template); @@ -43,9 +317,10 @@ impl JqTemplate { let modules = loader.load(&arena, template).map_err(|errs| { JqTemplateError::JqLoadError(errs.into_iter().map(|e| format!("{:?}", e.1)).collect()) })?; + // the AST of the operation, used to transform the data - let filter = Compiler::<_, Native>::default() - .with_funs(jaq_std::funs()) + let filter = Compiler::<_, Native>::default() + // .with_funs(jaq_std::funs()) .compile(modules) .map_err(|errs| { JqTemplateError::JqCompileError( @@ -61,24 +336,24 @@ impl JqTemplate { } /// Used to execute the transformation of the JqTemplate - pub fn run<'a, T: std::iter::Iterator>>( + pub fn run<'a, Y: std::iter::Iterator>>( &'a self, - inputs: &'a RcIter, - data: Val, - ) -> impl Iterator> + 'a { + inputs: &'a RcIter, + data: PathValueEnum, + ) -> impl Iterator> + 'a { let ctx = Ctx::new([], inputs); self.filter.run((ctx, data)) } /// Used to calculate the result and return it as json - pub fn render_value(&self, value: serde_json::Value) -> async_graphql_value::ConstValue { + pub fn render_value(&self, value: PathValueEnum) -> async_graphql_value::ConstValue { let inputs = RcIter::new(core::iter::empty()); - let res = self.run(&inputs, Val::from(value)); + let res = self.run(&inputs, value); let res: Vec = res .into_iter() // TODO: handle error correct, now we ignore it .filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) - .map(serde_json::Value::from) + .map(|v| std::convert::Into::into(v)) .map(async_graphql_value::ConstValue::from_json) // TODO: handle error correct, now we ignore it .filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) @@ -193,7 +468,7 @@ impl JqTemplate { } } -impl Default for JqTemplate { +impl Default for JqTemplate { fn default() -> Self { Self { filter: Default::default(), @@ -203,7 +478,7 @@ impl Default for JqTemplate { } } -impl std::fmt::Debug for JqTemplate { +impl std::fmt::Debug for JqTemplate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("JqTemplate") .field("representation", &self.representation) @@ -211,7 +486,7 @@ impl std::fmt::Debug for JqTemplate { } } -impl Display for JqTemplate { +impl Display for JqTemplate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { format!( "[JqTemplate](is_const={})({})", @@ -354,44 +629,44 @@ mod tests { ); } - #[test] - fn test_render_value_no_results() { - let template_str = ".[] | select(.non_existent)"; - let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); - let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(input_json); - assert_eq!( - result, - async_graphql_value::ConstValue::Null, - "Expected Null for no results" - ); - } - - #[test] - fn test_render_value_single_result() { - let template_str = ".[0]"; - let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); - let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(input_json); - assert_eq!( - result, - async_graphql_value::ConstValue::from_json(json!({"foo": 1})).unwrap(), - "Expected single result" - ); - } - - #[test] - fn test_render_value_multiple_results() { - let template_str = ".[] | .foo"; - let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); - let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(input_json); - let expected = async_graphql_value::ConstValue::array(vec![ - async_graphql_value::ConstValue::from_json(json!(1)).unwrap(), - async_graphql_value::ConstValue::from_json(json!(2)).unwrap(), - ]); - assert_eq!(result, expected, "Expected array of results"); - } + // #[test] + // fn test_render_value_no_results() { + // let template_str = ".[] | select(.non_existent)"; + // let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + // let input_json = json!([{"foo": 1}, {"foo": 2}]); + // let result = jq_template.render_value(input_json); + // assert_eq!( + // result, + // async_graphql_value::ConstValue::Null, + // "Expected Null for no results" + // ); + // } + + // #[test] + // fn test_render_value_single_result() { + // let template_str = ".[0]"; + // let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + // let input_json = json!([{"foo": 1}, {"foo": 2}]); + // let result = jq_template.render_value(input_json); + // assert_eq!( + // result, + // async_graphql_value::ConstValue::from_json(json!({"foo": 1})).unwrap(), + // "Expected single result" + // ); + // } + + // #[test] + // fn test_render_value_multiple_results() { + // let template_str = ".[] | .foo"; + // let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + // let input_json = json!([{"foo": 1}, {"foo": 2}]); + // let result = jq_template.render_value(input_json); + // let expected = async_graphql_value::ConstValue::array(vec![ + // async_graphql_value::ConstValue::from_json(json!(1)).unwrap(), + // async_graphql_value::ConstValue::from_json(json!(2)).unwrap(), + // ]); + // assert_eq!(result, expected, "Expected array of results"); + // } #[test] fn test_calculate_is_const() { @@ -531,7 +806,7 @@ mod tests { #[test] fn test_default() { - let jq_template = JqTemplate::default(); + let jq_template: JqTemplate = JqTemplate::default(); assert_eq!(jq_template.representation, ""); assert!(jq_template.is_const); // Assuming `filter` has a sensible default implementation @@ -539,7 +814,7 @@ mod tests { #[test] fn test_debug() { - let jq_template = JqTemplate { + let jq_template: JqTemplate = JqTemplate { filter: Arc::new(Filter::default()), representation: "test".to_string(), is_const: false, @@ -550,7 +825,7 @@ mod tests { #[test] fn test_display() { - let jq_template = JqTemplate { + let jq_template: JqTemplate = JqTemplate { filter: Arc::new(Filter::default()), representation: "test".to_string(), is_const: false, @@ -561,12 +836,12 @@ mod tests { #[test] fn test_partial_eq() { - let jq_template1 = JqTemplate { + let jq_template1: JqTemplate = JqTemplate { filter: Arc::new(Filter::default()), representation: "test".to_string(), is_const: false, }; - let jq_template2 = JqTemplate { + let jq_template2: JqTemplate = JqTemplate { filter: Arc::new(Filter::default()), representation: "test".to_string(), is_const: true, // Different `is_const` value should not affect equality @@ -576,12 +851,12 @@ mod tests { #[test] fn test_hash() { - let jq_template1 = JqTemplate { + let jq_template1: JqTemplate = JqTemplate { filter: Arc::new(Filter::default()), representation: "test".to_string(), is_const: false, }; - let jq_template2 = JqTemplate { + let jq_template2: JqTemplate = JqTemplate { filter: Arc::new(Filter::default()), representation: "test".to_string(), is_const: false, diff --git a/src/core/path.rs b/src/core/path.rs index 006450d19c..b03097d3bc 100644 --- a/src/core/path.rs +++ b/src/core/path.rs @@ -127,6 +127,12 @@ impl PathGraphql for EvalContext<'_, Ctx> { } } +impl PathValue for serde_json::Value { + fn raw_value<'a, T: AsRef>(&'a self, path: &[T]) -> Option> { + todo!() + } +} + #[cfg(test)] mod tests { diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index 996b821032..67ce98bfb4 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -4,14 +4,14 @@ use async_graphql::{Name, Value as GraphQLValue}; use indexmap::IndexMap; use crate::core::blueprint::DynamicValue; -use crate::core::path::PathString; +use super::mustache::PathJqValueString; pub trait ValueExt { - fn render_value(&self, ctx: &impl PathString) -> GraphQLValue; + fn render_value(&self, ctx: &impl PathJqValueString) -> GraphQLValue; } impl ValueExt for DynamicValue { - fn render_value<'a>(&self, ctx: &'a impl PathString) -> GraphQLValue { + fn render_value<'a>(&self, ctx: &'a impl PathJqValueString) -> GraphQLValue { match self { DynamicValue::Value(value) => value.to_owned(), DynamicValue::Mustache(m) => { @@ -24,31 +24,7 @@ impl ValueExt for DynamicValue { .unwrap_or_else(|_| GraphQLValue::String(rendered.into_owned())) } DynamicValue::JqTemplate(t) => { - let value = if let Some(value) = ctx.path_string(&["value"]) { - serde_json::from_str(&value).unwrap() - } else { - serde_json::Value::Object(serde_json::Map::new()) - }; - - let vars = if let Some(vars) = ctx.path_string(&["vars"]) { - serde_json::from_str(&vars).unwrap() - } else { - serde_json::Value::Object(serde_json::Map::new()) - }; - - let args = if let Some(args) = ctx.path_string(&["args"]) { - serde_json::from_str(&args).unwrap() - } else { - serde_json::Value::Object(serde_json::Map::new()) - }; - - let data = serde_json::json!({ - "value": value, - "vars": vars, - "args": args, - }); - - t.render_value(data) + todo!() } DynamicValue::Object(obj) => { let out: IndexMap<_, _> = obj From 531d5242dd496421bc907646b4cc24305d1fc2a1 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Fri, 6 Dec 2024 13:46:36 +0200 Subject: [PATCH 11/28] fix: compile errors --- src/core/mustache/jq_template.rs | 626 ++++++++++++++++++++++--------- src/core/path.rs | 6 - src/core/serde_value_ext.rs | 7 +- 3 files changed, 460 insertions(+), 179 deletions(-) diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 4e504cceb1..8d4695e905 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -3,19 +3,20 @@ use std::sync::Arc; use jaq_core::load::parse::Term; use jaq_core::load::{Arena, File, Loader}; -use jaq_core::{Compiler, Ctx, Error, Filter, Native, RcIter, ValR, ValT}; +use jaq_core::val::Range; +use jaq_core::{Compiler, Ctx, Error, Exn, Filter, Native, RcIter, ValR, ValT}; use jaq_json::Val; use regex::Regex; use crate::core::ir::{EvalContext, ResolverContextLike}; use crate::core::json::JsonLike; -use crate::core::path::{PathString, ValueString}; +use crate::core::path::{PathString, PathValue, ValueString}; /// Used to represent a JQ template. Currently used only on @expr directive. #[derive(Clone)] pub struct JqTemplate { /// The compiled filter - filter: Arc>>, + template: String, /// The IR representation, used for debug purposes representation: String, /// If the transformer returns a constant value @@ -23,24 +24,43 @@ pub struct JqTemplate { } #[derive(Clone)] -pub enum PathValueEnum { - PathValue(Arc), - Val(Val) +pub enum PathValueEnum<'a> { + PathValue(Arc<&'a dyn PathJqValue>), + Val(Val), } pub trait PathJqValue { - fn get_value<'a>(&'a self, path: &str) -> Option>; + fn get_value<'a>(&'a self, index: &Val) -> Option>; } impl PathJqValue for EvalContext<'_, Ctx> { - fn get_value<'a>(&'a self, path: &str) -> Option> { - todo!() + fn get_value<'a>(&'a self, index: &Val) -> Option> { + let Val::Str(index) = index else { return None }; + self.raw_value(&[index.as_str()]) } } impl PathJqValue for serde_json::Value { - fn get_value<'a>(&'a self, path: &str) -> Option> { - todo!() + fn get_value(&self, index: &Val) -> Option> { + match self { + serde_json::Value::Object(map) => { + let Val::Str(index) = index else { return None }; + map.get(index.as_str()).map(|v| { + ValueString::Value(std::borrow::Cow::Owned( + async_graphql_value::ConstValue::from_json(v.clone()).unwrap(), + )) + }) + } + serde_json::Value::Array(list) => { + let Val::Int(index) = index else { return None }; + list.get(*index as usize).map(|v| { + ValueString::Value(std::borrow::Cow::Owned( + async_graphql_value::ConstValue::from_json(v.clone()).unwrap(), + )) + }) + } + _ => None, + } } } pub trait PathJqValueString: PathString + PathJqValue {} @@ -49,68 +69,57 @@ impl PathJqValueString for EvalContext<'_, Ctx> {} impl PathJqValueString for serde_json::Value {} - -impl ValT for PathValueEnum { +impl ValT for PathValueEnum<'_> { fn from_num(n: &str) -> ValR { match Val::from_num(n) { Ok(val) => ValR::Ok(Self::Val(val)), Err(err) => { let val = err.into_val(); Err(Error::new(Self::Val(val))) - }, + } } } fn from_map>(iter: I) -> ValR { - let result: Result, String> = iter.into_iter().map(|(k, v)| { - match (k, v) { + let result: Result, String> = iter + .into_iter() + .map(|(k, v)| match (k, v) { (PathValueEnum::Val(key), PathValueEnum::Val(value)) => Ok((key, value)), - _ => Err("Invalid key or value type for map".into()) - } - }).collect(); + _ => Err("Invalid key or value type for map".into()), + }) + .collect(); match result { - Ok(pairs) => { - match Val::from_map(pairs.into_iter()) { - Ok(val) => ValR::Ok(PathValueEnum::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - }, + Ok(pairs) => match Val::from_map(pairs) { + Ok(val) => ValR::Ok(PathValueEnum::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) } }, - Err(e) => Err(Error::new(Self::Val(Val::from(e)))) + Err(e) => Err(Error::new(Self::Val(Val::from(e)))), } } fn values(self) -> Box>> { - match self { - PathValueEnum::PathValue(_context) => { - // Create a new error message each time to avoid lifetime issues - let error_message = "Cannot iterate context.".to_string(); - let error_val = Val::from(error_message); - let error = Error::new(Self::Val(error_val)); - Box::new(std::iter::once(Err(error))) - }, - PathValueEnum::Val(val) => { - Box::new(std::iter::once(Ok(PathValueEnum::Val(val)))) - } - } + todo!() } fn index(self, index: &Self) -> ValR { let PathValueEnum::Val(index) = index else { - return ValR::Err(Error::new(Self::Val(Val::from(format!("Could not convert index `{}` val.", index))))); + return ValR::Err(Error::new(Self::Val(Val::from(format!( + "Could not convert index `{}` val.", + index + ))))); }; match self { PathValueEnum::PathValue(pv) => { - let Some(index) = index.as_str() else { - return ValR::Err(Error::new(Self::Val(Val::from(format!("Could not convert index `{}` to string.", index))))); - }; - let Some(v) = pv.get_value(index) else { - return ValR::Err(Error::new(Self::Val(Val::from(format!("Could not find key `{}` in context.", index))))); + return ValR::Err(Error::new(Self::Val(Val::from(format!( + "Could not find key `{}` in context.", + index + ))))); }; match v { @@ -118,29 +127,81 @@ impl ValT for PathValueEnum { let cv = cow.as_ref().clone(); match cv.into_json() { Ok(js) => Ok(Self::Val(Val::from(js))), - Err(err) => ValR::Err(Error::new(Self::Val(Val::from(format!("Could not convert value to json: {:?}", err))))), + Err(err) => ValR::Err(Error::new(Self::Val(Val::from(format!( + "Could not convert value to json: {:?}", + err + ))))), } - }, + } crate::core::path::ValueString::String(cow) => { let v = cow.to_string(); Ok(Self::Val(Val::from(v))) - }, + } } - }, - PathValueEnum::Val(val) => { - match val.index(index) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - }, + } + PathValueEnum::Val(val) => match val.index(index) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) } }, } } fn range(self, range: jaq_core::val::Range<&Self>) -> ValR { - todo!() + let (start, end) = ( + range + .start + .map(|v| match v { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( + "Could not convert range start to val.".to_string(), + ))), + PathValueEnum::Val(val) => Ok(val.clone()), + }) + .transpose(), + range + .end + .map(|v| match v { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( + "Could not convert range end to val.".to_string(), + ))), + PathValueEnum::Val(val) => Ok(val.clone()), + }) + .transpose(), + ); + + let (start, end) = match (start, end) { + (Ok(start), Ok(end)) => (start, end), + (Ok(_), Err(err)) => { + let val = err.into_val(); + return Err(Error::new(Self::Val(val))); + } + (Err(err), Ok(_)) => { + let val = err.into_val(); + return Err(Error::new(Self::Val(val))); + } + (Err(_), Err(_)) => { + return ValR::Err(Error::new(Self::Val(Val::from( + "Could not convert range to val.".to_string(), + )))) + } + }; + + let range = Range { start: start.as_ref(), end: end.as_ref() }; + + match self { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Self::Val(Val::from( + "Cannot apply range operation at the context".to_string(), + )))), + PathValueEnum::Val(val) => match val.range(range) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + }, + } } fn map_values<'a, I: Iterator>>( @@ -148,7 +209,33 @@ impl ValT for PathValueEnum { opt: jaq_core::path::Opt, f: impl Fn(Self) -> I, ) -> jaq_core::ValX<'a, Self> { - todo!() + let f_new = move |x: Val| -> _ { + let iter = f(Self::Val(x)); + iter.map(|v| match v { + Ok(enum_val) => match enum_val { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( + Val::from("Cannot convert context to val.".to_string()), + ))), + PathValueEnum::Val(val) => Ok(val), + }, + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( + "Function execution failed with: {:?}", + err + ))))), + }) + }; + + match self { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( + Val::from("Cannot apply map_values operation at the context".to_string()), + )))), + PathValueEnum::Val(val) => match val.map_values(opt, f_new) { + Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( + format!("The map_values failed because: {:?}", err), + ))))), + }, + } } fn map_index<'a, I: Iterator>>( @@ -157,7 +244,40 @@ impl ValT for PathValueEnum { opt: jaq_core::path::Opt, f: impl Fn(Self) -> I, ) -> jaq_core::ValX<'a, Self> { - todo!() + let PathValueEnum::Val(index) = index else { + return jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from(format!( + "Could not convert index `{}` val.", + index + )))))); + }; + + let f_new = move |x: Val| -> _ { + let iter = f(Self::Val(x)); + iter.map(|v| match v { + Ok(enum_val) => match enum_val { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( + Val::from("Cannot convert context to val.".to_string()), + ))), + PathValueEnum::Val(val) => Ok(val), + }, + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( + "Function execution failed with: {:?}", + err + ))))), + }) + }; + + match self { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( + Val::from("Cannot apply map_index operation at the context".to_string()), + )))), + PathValueEnum::Val(val) => match val.map_index(index, opt, f_new) { + Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( + format!("The map_index failed because: {:?}", err), + ))))), + }, + } } fn map_range<'a, I: Iterator>>( @@ -166,146 +286,325 @@ impl ValT for PathValueEnum { opt: jaq_core::path::Opt, f: impl Fn(Self) -> I, ) -> jaq_core::ValX<'a, Self> { - todo!() + let (start, end) = ( + range + .start + .map(|v| match v { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( + "Could not convert range start to val.".to_string(), + ))), + PathValueEnum::Val(val) => Ok(val.clone()), + }) + .transpose(), + range + .end + .map(|v| match v { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( + "Could not convert range end to val.".to_string(), + ))), + PathValueEnum::Val(val) => Ok(val.clone()), + }) + .transpose(), + ); + + let (start, end) = match (start, end) { + (Ok(start), Ok(end)) => (start, end), + (Ok(_), Err(err)) => { + let val = err.into_val(); + return Err(Exn::from(Error::new(Self::Val(val)))); + } + (Err(err), Ok(_)) => { + let val = err.into_val(); + return Err(Exn::from(Error::new(Self::Val(val)))); + } + (Err(_), Err(_)) => { + return Err(Exn::from(Error::new(Self::Val(Val::from( + "Could not convert range to val.".to_string(), + ))))) + } + }; + + let range = Range { start: start.as_ref(), end: end.as_ref() }; + + let f_new = move |x: Val| -> _ { + let iter = f(Self::Val(x)); + iter.map(|v| match v { + Ok(enum_val) => match enum_val { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( + Val::from("Cannot convert context to val.".to_string()), + ))), + PathValueEnum::Val(val) => Ok(val), + }, + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( + "Function execution failed with: {:?}", + err + ))))), + }) + }; + + match self { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( + Val::from("Cannot apply map_range operation at the context".to_string()), + )))), + PathValueEnum::Val(val) => match val.map_range(range, opt, f_new) { + Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( + format!("The map_range failed because: {:?}", err), + ))))), + }, + } } fn as_bool(&self) -> bool { - todo!() + match self { + PathValueEnum::PathValue(_) => true, + PathValueEnum::Val(val) => val.as_bool(), + } } fn as_str(&self) -> Option<&str> { match self { - PathValueEnum::PathValue(_) => None, + PathValueEnum::PathValue(_) => Some("[Context]"), PathValueEnum::Val(val) => val.as_str(), } } } -impl FromIterator for PathValueEnum { - fn from_iter>(iter: I) -> Self { - todo!() +impl<'a> FromIterator> for PathValueEnum<'a> { + fn from_iter>>(iter: I) -> Self { + let iter = iter.into_iter().filter_map(|v| match v { + PathValueEnum::PathValue(_) => None, + PathValueEnum::Val(val) => Some(val), + }); + Self::Val(Val::from_iter(iter)) } } -impl std::ops::Add for PathValueEnum { +impl std::ops::Add for PathValueEnum<'_> { type Output = ValR; fn add(self, rhs: Self) -> Self::Output { - todo!() + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.add(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform add operation with context.".to_string(), + )))), + } } } -impl std::ops::Sub for PathValueEnum { +impl std::ops::Sub for PathValueEnum<'_> { type Output = ValR; fn sub(self, rhs: Self) -> Self::Output { - todo!() + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.sub(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform sub operation with context.".to_string(), + )))), + } } } -impl std::ops::Mul for PathValueEnum { +impl std::ops::Mul for PathValueEnum<'_> { type Output = ValR; fn mul(self, rhs: Self) -> Self::Output { - todo!() + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.mul(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform mul operation with context.".to_string(), + )))), + } } } -impl std::ops::Div for PathValueEnum { +impl std::ops::Div for PathValueEnum<'_> { type Output = ValR; fn div(self, rhs: Self) -> Self::Output { - todo!() + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.div(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform div operation with context.".to_string(), + )))), + } } } -impl std::ops::Rem for PathValueEnum { +impl std::ops::Rem for PathValueEnum<'_> { type Output = ValR; fn rem(self, rhs: Self) -> Self::Output { - todo!() + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.rem(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform rem operation with context.".to_string(), + )))), + } } } -impl std::ops::Neg for PathValueEnum { +impl std::ops::Neg for PathValueEnum<'_> { type Output = ValR; fn neg(self) -> Self::Output { - todo!() + match self { + PathValueEnum::Val(self_val) => match self_val.neg() { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + }, + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform neg operation at context.".to_string(), + )))), + } } } -impl Display for PathValueEnum { +impl Display for PathValueEnum<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // match self { - // PathValueEnum::PathValue(_) => "[PathValue]".to_string().fmt(f), - // PathValueEnum::Val(val) => val.fmt(f), - // } - todo!() + match self { + PathValueEnum::PathValue(_) => "[Context]".to_string().fmt(f), + PathValueEnum::Val(val) => val.fmt(f), + } } } -impl std::fmt::Debug for PathValueEnum { +impl std::fmt::Debug for PathValueEnum<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // match self { - // Self::PathValue(arg0) => f.debug_tuple("PathValue").field(arg0).finish(), - // Self::Val(arg0) => f.debug_tuple("Val").field(arg0).finish(), - // } - todo!() + match self { + PathValueEnum::PathValue(_) => derive_more::Debug::fmt(&"[Context]".to_string(), f), + PathValueEnum::Val(val) => derive_more::Debug::fmt(&val, f), + } } } -impl PartialEq for PathValueEnum { +impl PartialEq for PathValueEnum<'_> { fn eq(&self, other: &Self) -> bool { - todo!() + match (self, other) { + (PathValueEnum::PathValue(_), PathValueEnum::PathValue(_)) => true, + (PathValueEnum::PathValue(_), PathValueEnum::Val(_)) => false, + (PathValueEnum::Val(_), PathValueEnum::PathValue(_)) => false, + (PathValueEnum::Val(self_val), PathValueEnum::Val(other_val)) => self_val.eq(other_val), + } } } -impl PartialOrd for PathValueEnum { +impl PartialOrd for PathValueEnum<'_> { fn partial_cmp(&self, other: &Self) -> Option { - todo!() + match (self, other) { + (PathValueEnum::PathValue(_), PathValueEnum::PathValue(_)) => None, + (PathValueEnum::PathValue(_), PathValueEnum::Val(_)) => None, + (PathValueEnum::Val(_), PathValueEnum::PathValue(_)) => None, + (PathValueEnum::Val(self_val), PathValueEnum::Val(other_val)) => { + self_val.partial_cmp(other_val) + } + } } } -impl Into for PathValueEnum { - fn into(self) -> serde_json::Value { - match self { - PathValueEnum::PathValue(_) => todo!(), +impl From> for serde_json::Value { + fn from(value: PathValueEnum<'_>) -> Self { + match value { + PathValueEnum::PathValue(_) => serde_json::Value::String("[Context]".to_string()), PathValueEnum::Val(val) => serde_json::Value::from(val), } } } -impl From for PathValueEnum { +impl From for PathValueEnum<'_> { fn from(value: String) -> Self { - todo!() + Self::Val(Val::from(value)) } } -impl From for PathValueEnum { +impl From for PathValueEnum<'_> { fn from(value: isize) -> Self { - todo!() + Self::Val(Val::from(value)) } } -impl From for PathValueEnum { +impl From for PathValueEnum<'_> { fn from(value: bool) -> Self { - todo!() + Self::Val(Val::from(value)) } } -impl JqTemplate { +impl JqTemplate { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { let template = transform_to_jq(template); // the term is used because it can be easily serialized, deserialized and hashed let term = Self::parse_template(&template); + // calculate if the expression returns always a constant value let is_const = Self::calculate_is_const(&term); + Ok(Self { + template: template.to_string(), + representation: format!("{:?}", term), + is_const, + }) + } + + /// Used to execute the transformation of the JqTemplate + pub fn run<'obj>(&'obj self, data: PathValueEnum<'obj>) -> Vec>> { + let inputs = RcIter::new(core::iter::empty()); + let ctx = Ctx::new([], &inputs); + + match self.get_filter() { + Ok(filter) => filter.run((ctx, data)).collect::>(), + Err(_) => vec![], + } + } + + fn get_filter(&self) -> Result>>, JqTemplateError> { // the template is used to be parsed in to the IR AST - let template = File { code: template.as_str(), path: () }; + let template = File { code: self.template.as_str(), path: () }; // defs is used to extend the syntax with custom definitions of functions, like // 'toString' let defs = jaq_std::defs(); @@ -328,32 +627,17 @@ impl JqTemplate { ) })?; - Ok(Self { - filter: Arc::new(filter), - representation: format!("{:?}", term), - is_const, - }) - } - - /// Used to execute the transformation of the JqTemplate - pub fn run<'a, Y: std::iter::Iterator>>( - &'a self, - inputs: &'a RcIter, - data: PathValueEnum, - ) -> impl Iterator> + 'a { - let ctx = Ctx::new([], inputs); - self.filter.run((ctx, data)) + Ok(filter) } /// Used to calculate the result and return it as json pub fn render_value(&self, value: PathValueEnum) -> async_graphql_value::ConstValue { - let inputs = RcIter::new(core::iter::empty()); - let res = self.run(&inputs, value); + let res = self.run(value); let res: Vec = res .into_iter() // TODO: handle error correct, now we ignore it .filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) - .map(|v| std::convert::Into::into(v)) + .map(std::convert::Into::into) .map(async_graphql_value::ConstValue::from_json) // TODO: handle error correct, now we ignore it .filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) @@ -468,17 +752,17 @@ impl JqTemplate { } } -impl Default for JqTemplate { +impl Default for JqTemplate { fn default() -> Self { Self { - filter: Default::default(), + template: "".to_string(), representation: String::default(), is_const: true, } } } -impl std::fmt::Debug for JqTemplate { +impl std::fmt::Debug for JqTemplate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("JqTemplate") .field("representation", &self.representation) @@ -486,7 +770,7 @@ impl std::fmt::Debug for JqTemplate { } } -impl Display for JqTemplate { +impl Display for JqTemplate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { format!( "[JqTemplate](is_const={})({})", @@ -629,44 +913,44 @@ mod tests { ); } - // #[test] - // fn test_render_value_no_results() { - // let template_str = ".[] | select(.non_existent)"; - // let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); - // let input_json = json!([{"foo": 1}, {"foo": 2}]); - // let result = jq_template.render_value(input_json); - // assert_eq!( - // result, - // async_graphql_value::ConstValue::Null, - // "Expected Null for no results" - // ); - // } - - // #[test] - // fn test_render_value_single_result() { - // let template_str = ".[0]"; - // let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); - // let input_json = json!([{"foo": 1}, {"foo": 2}]); - // let result = jq_template.render_value(input_json); - // assert_eq!( - // result, - // async_graphql_value::ConstValue::from_json(json!({"foo": 1})).unwrap(), - // "Expected single result" - // ); - // } - - // #[test] - // fn test_render_value_multiple_results() { - // let template_str = ".[] | .foo"; - // let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); - // let input_json = json!([{"foo": 1}, {"foo": 2}]); - // let result = jq_template.render_value(input_json); - // let expected = async_graphql_value::ConstValue::array(vec![ - // async_graphql_value::ConstValue::from_json(json!(1)).unwrap(), - // async_graphql_value::ConstValue::from_json(json!(2)).unwrap(), - // ]); - // assert_eq!(result, expected, "Expected array of results"); - // } + #[test] + fn test_render_value_no_results() { + let template_str = ".[] | select(.non_existent)"; + let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + assert_eq!( + result, + async_graphql_value::ConstValue::Null, + "Expected Null for no results" + ); + } + + #[test] + fn test_render_value_single_result() { + let template_str = ".[0]"; + let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + assert_eq!( + result, + async_graphql_value::ConstValue::from_json(json!({"foo": 1})).unwrap(), + "Expected single result" + ); + } + + #[test] + fn test_render_value_multiple_results() { + let template_str = ".[] | .foo"; + let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + let expected = async_graphql_value::ConstValue::array(vec![ + async_graphql_value::ConstValue::from_json(json!(1)).unwrap(), + async_graphql_value::ConstValue::from_json(json!(2)).unwrap(), + ]); + assert_eq!(result, expected, "Expected array of results"); + } #[test] fn test_calculate_is_const() { @@ -815,7 +1099,7 @@ mod tests { #[test] fn test_debug() { let jq_template: JqTemplate = JqTemplate { - filter: Arc::new(Filter::default()), + template: "".to_string(), representation: "test".to_string(), is_const: false, }; @@ -826,7 +1110,7 @@ mod tests { #[test] fn test_display() { let jq_template: JqTemplate = JqTemplate { - filter: Arc::new(Filter::default()), + template: "".to_string(), representation: "test".to_string(), is_const: false, }; @@ -837,12 +1121,12 @@ mod tests { #[test] fn test_partial_eq() { let jq_template1: JqTemplate = JqTemplate { - filter: Arc::new(Filter::default()), + template: "".to_string(), representation: "test".to_string(), is_const: false, }; let jq_template2: JqTemplate = JqTemplate { - filter: Arc::new(Filter::default()), + template: "".to_string(), representation: "test".to_string(), is_const: true, // Different `is_const` value should not affect equality }; @@ -852,12 +1136,12 @@ mod tests { #[test] fn test_hash() { let jq_template1: JqTemplate = JqTemplate { - filter: Arc::new(Filter::default()), + template: "".to_string(), representation: "test".to_string(), is_const: false, }; let jq_template2: JqTemplate = JqTemplate { - filter: Arc::new(Filter::default()), + template: "".to_string(), representation: "test".to_string(), is_const: false, }; diff --git a/src/core/path.rs b/src/core/path.rs index b03097d3bc..006450d19c 100644 --- a/src/core/path.rs +++ b/src/core/path.rs @@ -127,12 +127,6 @@ impl PathGraphql for EvalContext<'_, Ctx> { } } -impl PathValue for serde_json::Value { - fn raw_value<'a, T: AsRef>(&'a self, path: &[T]) -> Option> { - todo!() - } -} - #[cfg(test)] mod tests { diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index 67ce98bfb4..e33c54e74e 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -1,10 +1,12 @@ use std::borrow::Cow; +use std::sync::Arc; use async_graphql::{Name, Value as GraphQLValue}; use indexmap::IndexMap; -use crate::core::blueprint::DynamicValue; use super::mustache::PathJqValueString; +use crate::core::blueprint::DynamicValue; +use crate::core::mustache::PathValueEnum; pub trait ValueExt { fn render_value(&self, ctx: &impl PathJqValueString) -> GraphQLValue; @@ -24,7 +26,8 @@ impl ValueExt for DynamicValue { .unwrap_or_else(|_| GraphQLValue::String(rendered.into_owned())) } DynamicValue::JqTemplate(t) => { - todo!() + let v = PathValueEnum::PathValue(Arc::new(ctx)); + t.render_value(v) } DynamicValue::Object(obj) => { let out: IndexMap<_, _> = obj From 533b766474164adffe954cd1b19764521fb14e0c Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Fri, 6 Dec 2024 17:57:46 +0200 Subject: [PATCH 12/28] fix: missing implementations --- src/core/mustache/jq_template.rs | 140 +++++++++++++++--- .../test-expr-scalar-as-string.md_0.snap | 16 +- .../snapshots/test-jq-template.md_merged.snap | 4 +- tests/execution/test-jq-template.md | 4 +- 4 files changed, 125 insertions(+), 39 deletions(-) diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 8d4695e905..f0da408265 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use jaq_core::load::parse::Term; use jaq_core::load::{Arena, File, Loader}; use jaq_core::val::Range; -use jaq_core::{Compiler, Ctx, Error, Exn, Filter, Native, RcIter, ValR, ValT}; +use jaq_core::{Compiler, Ctx, Error, Exn, Filter, Native, RcIter, ValR}; use jaq_json::Val; use regex::Regex; @@ -69,7 +69,33 @@ impl PathJqValueString for EvalContext<'_, Ctx> {} impl PathJqValueString for serde_json::Value {} -impl ValT for PathValueEnum<'_> { +impl jaq_std::ValT for PathValueEnum<'_> { + fn into_seq>(self) -> Result { + todo!() + } + + fn as_isize(&self) -> Option { + match self { + PathValueEnum::PathValue(_) => None, + PathValueEnum::Val(val) => val.as_isize(), + } + } + + fn as_f64(&self) -> Result> { + match self { + PathValueEnum::PathValue(_) => Err(Error::new(Self::Val(Val::from("Cannot convert context to f64".to_string())))), + PathValueEnum::Val(val) => match val.as_f64() { + Ok(val) => Ok(val), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + }, + } + } +} + +impl jaq_core::ValT for PathValueEnum<'_> { fn from_num(n: &str) -> ValR { match Val::from_num(n) { Ok(val) => ValR::Ok(Self::Val(val)), @@ -102,7 +128,15 @@ impl ValT for PathValueEnum<'_> { } fn values(self) -> Box>> { - todo!() + match self { + PathValueEnum::PathValue(_) => panic!("Cannot iterate context"), + PathValueEnum::Val(val) => Box::new(val.values().map(|v| { + v.map(PathValueEnum::Val).map_err(|err| { + let val = err.into_val(); + Error::new(PathValueEnum::Val(val)) + }) + })), + } } fn index(self, index: &Self) -> ValR { @@ -546,6 +580,27 @@ impl PartialOrd for PathValueEnum<'_> { } } +impl Eq for PathValueEnum<'_> {} + +impl Ord for PathValueEnum<'_> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (PathValueEnum::PathValue(_), PathValueEnum::PathValue(_)) => std::cmp::Ordering::Equal, + (PathValueEnum::PathValue(_), PathValueEnum::Val(_)) => std::cmp::Ordering::Greater, + (PathValueEnum::Val(_), PathValueEnum::PathValue(_)) => std::cmp::Ordering::Less, + (PathValueEnum::Val(self_val), PathValueEnum::Val(other_val)) => { + self_val.cmp(other_val) + } + } + } +} + +impl From for PathValueEnum<'_> { + fn from(value: f64) -> Self { + Self::Val(Val::from(value)) + } +} + impl From> for serde_json::Value { fn from(value: PathValueEnum<'_>) -> Self { match value { @@ -598,7 +653,10 @@ impl JqTemplate { match self.get_filter() { Ok(filter) => filter.run((ctx, data)).collect::>(), - Err(_) => vec![], + Err(err) => { + println!("JQ Create Err: {:?}", err); + vec![] + } } } @@ -619,7 +677,7 @@ impl JqTemplate { // the AST of the operation, used to transform the data let filter = Compiler::<_, Native>::default() - // .with_funs(jaq_std::funs()) + .with_funs(jaq_std::funs()) .compile(modules) .map_err(|errs| { JqTemplateError::JqCompileError( @@ -636,11 +694,23 @@ impl JqTemplate { let res: Vec = res .into_iter() // TODO: handle error correct, now we ignore it - .filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) + .filter_map(|v| match v { + Ok(v) => Some(v), + Err(err) => { + println!("ERR: {:?}", err); + None + } + }) .map(std::convert::Into::into) .map(async_graphql_value::ConstValue::from_json) // TODO: handle error correct, now we ignore it - .filter_map(|v| if let Ok(v) = v { Some(v) } else { None }) + .filter_map(|v| match v { + Ok(v) => Some(v), + Err(err) => { + println!("ERR: {:?}", err); + None + } + }) .collect(); let res_len = res.len(); if res_len == 0 { @@ -804,15 +874,16 @@ pub enum JqTemplateError { /// Used to convert mustache to jq fn transform_to_jq(input: &str) -> String { - let re = Regex::new(r"\{\{\.([^}]*)\}\}").unwrap(); + let input = input + .to_string() + .replace("{{{", "[-<-[{") + .replace("}}}", "}]->-]") + .replace("{{", "[-<-[") + .replace("}}", "]->]"); + let re = Regex::new(r"\[-<-\[(.*?)\]->\]").unwrap(); let mut result = String::new(); let mut last_end = 0; - let captures: Vec<_> = re.captures_iter(input).collect(); - - // when we do not have any mustache templates return the string - if captures.is_empty() { - return input.to_string(); - } + let captures: Vec<_> = re.captures_iter(&input).collect(); for cap in captures { let match_ = cap.get(0).unwrap(); @@ -829,7 +900,7 @@ fn transform_to_jq(input: &str) -> String { if !result.is_empty() { result.push_str(" + "); } - result.push_str(&format!(".{}", var_name)); + result.push_str(var_name); last_end = match_.end(); } @@ -842,12 +913,13 @@ fn transform_to_jq(input: &str) -> String { result.push_str(&format!("\"{}\"", &input[last_end..])); } - // If no transformations were made, return the original input + // If the result is empty, it means the input was a single mustache expression if result.is_empty() { return input.to_string(); } - result + // Remove unnecessary delimiters from the result + result.replace("\"[-<-[", "").replace("]->-]\"", "") } #[cfg(test)] @@ -1154,30 +1226,52 @@ mod tests { #[test] fn test_transform_to_jq() { + assert_eq!(transform_to_jq("Hello world"), "\"Hello world\""); + assert_eq!( transform_to_jq("Hello world: {{.foo.buzz | split(\" \")}}"), "\"Hello world: \" + .foo.buzz | split(\" \")" ); + + assert_eq!( + transform_to_jq("\"Hello world: \" + .foo.buzz | split(\" \")"), + "\"\"Hello world: \" + .foo.buzz | split(\" \")\"" + ); + assert_eq!( transform_to_jq("Hello world: {{.foo.buzz | split(\" \")}} this is great"), "\"Hello world: \" + .foo.buzz | split(\" \") + \" this is great\"" ); + assert_eq!( + transform_to_jq("\"Hello world: \" + .foo.buzz | split(\" \") + \" this is great\""), + "\"\"Hello world: \" + .foo.buzz | split(\" \") + \" this is great\"\"" + ); + assert_eq!( transform_to_jq("{{.foo.buzz | split(\" \")}} buzz"), ".foo.buzz | split(\" \") + \" buzz\"" ); + assert_eq!( + transform_to_jq(".foo.buzz | split(\" \") + \" buzz\""), + "\".foo.buzz | split(\" \") + \" buzz\"\"" + ); + assert_eq!( transform_to_jq("{{.foo.buzz | split(\" \")}} of type {{.bar}}"), ".foo.buzz | split(\" \") + \" of type \" + .bar" ); - } + assert_eq!( + transform_to_jq(".foo.buzz | split(\" \") + \" of type \" + .bar"), + "\".foo.buzz | split(\" \") + \" of type \" + .bar\"" + ); - #[test] - fn test_transform_to_jq_identity() { - assert_eq!(transform_to_jq("Hello world"), "Hello world"); assert_eq!( - transform_to_jq(".foo.buzz | split(\" \")"), - ".foo.buzz | split(\" \")" + transform_to_jq("{{.foo.buzz | split(\" \")}} of type {{.bar}}"), + ".foo.buzz | split(\" \") + \" of type \" + .bar" + ); + assert_eq!( + transform_to_jq("{{.value.body | split(\" \") | {first: .[0], second: .[1]}}}"), + ".value.body | split(\" \") | {first: .[0], second: .[1]}" ); } } diff --git a/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap b/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap index 653dabd4c9..c7349486ac 100644 --- a/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap +++ b/tests/core/snapshots/test-expr-scalar-as-string.md_0.snap @@ -11,16 +11,10 @@ expression: response "data": { "entry": { "num": "0", - "arr": [ - 1, - 2, - 3 - ], + "arr": "[1, 2, 3]", "str": "test", - "obj": { - "e": 1 - }, - "bool": true, + "obj": "{e: 1}", + "bool": "true", "nested": { "num": 0, "arr": [ @@ -29,9 +23,7 @@ expression: response 3 ], "str": "test", - "obj": { - "e": 1 - }, + "obj": "{e: 1}", "bool": true } } diff --git a/tests/core/snapshots/test-jq-template.md_merged.snap b/tests/core/snapshots/test-jq-template.md_merged.snap index 0aa2f8d979..f73644e97f 100644 --- a/tests/core/snapshots/test-jq-template.md_merged.snap +++ b/tests/core/snapshots/test-jq-template.md_merged.snap @@ -13,11 +13,11 @@ type Buzz { type Fizz { bar: String! - buzz: Buzz! @expr(body: ".value.bar | split(\" \") | {first: .[0], second: .[1]}") + buzz: Buzz! @expr(body: "{{.value.bar | split(\" \") | {first: .[0], second: .[1]}}}") } type Foo { - bar: [String!]! @expr(body: ".value.bar | split(\" \")") + bar: [String!]! @expr(body: "{{.value.bar | split(\" \")}}") } type Query { diff --git a/tests/execution/test-jq-template.md b/tests/execution/test-jq-template.md index edd16bea22..2174b398ae 100644 --- a/tests/execution/test-jq-template.md +++ b/tests/execution/test-jq-template.md @@ -12,12 +12,12 @@ type Query { type Foo { bar: String! - bar: [String!]! @expr(body: ".value.bar | split(\" \")") + bar: [String!]! @expr(body: "{{.value.bar | split(\" \")}}") } type Fizz { bar: String! - buzz: Buzz! @expr(body: ".value.bar | split(\" \") | {first: .[0], second: .[1]}") + buzz: Buzz! @expr(body: "{{.value.bar | split(\" \") | {first: .[0], second: .[1]}}}") } type Buzz { From d60d170b910d7bf62656fc29ff0b0034f2e530ca Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Mon, 9 Dec 2024 03:57:19 +0200 Subject: [PATCH 13/28] fix: compile jq only once --- src/core/mustache/jq_template.rs | 80 ++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index f0da408265..bd6fa77290 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -1,22 +1,28 @@ use std::fmt::Display; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use jaq_core::load::parse::Term; use jaq_core::load::{Arena, File, Loader}; use jaq_core::val::Range; use jaq_core::{Compiler, Ctx, Error, Exn, Filter, Native, RcIter, ValR}; use jaq_json::Val; +use lazy_static::lazy_static; use regex::Regex; use crate::core::ir::{EvalContext, ResolverContextLike}; use crate::core::json::JsonLike; use crate::core::path::{PathString, PathValue, ValueString}; +lazy_static! { + static ref JQ_TEMPLATE_STORAGE: RwLock>>>>> = + RwLock::new(Vec::new()); +} + /// Used to represent a JQ template. Currently used only on @expr directive. #[derive(Clone)] pub struct JqTemplate { - /// The compiled filter - template: String, + /// TODO + template_id: usize, /// The IR representation, used for debug purposes representation: String, /// If the transformer returns a constant value @@ -83,7 +89,9 @@ impl jaq_std::ValT for PathValueEnum<'_> { fn as_f64(&self) -> Result> { match self { - PathValueEnum::PathValue(_) => Err(Error::new(Self::Val(Val::from("Cannot convert context to f64".to_string())))), + PathValueEnum::PathValue(_) => Err(Error::new(Self::Val(Val::from( + "Cannot convert context to f64".to_string(), + )))), PathValueEnum::Val(val) => match val.as_f64() { Ok(val) => Ok(val), Err(err) => { @@ -639,30 +647,8 @@ impl JqTemplate { // calculate if the expression returns always a constant value let is_const = Self::calculate_is_const(&term); - Ok(Self { - template: template.to_string(), - representation: format!("{:?}", term), - is_const, - }) - } - - /// Used to execute the transformation of the JqTemplate - pub fn run<'obj>(&'obj self, data: PathValueEnum<'obj>) -> Vec>> { - let inputs = RcIter::new(core::iter::empty()); - let ctx = Ctx::new([], &inputs); - - match self.get_filter() { - Ok(filter) => filter.run((ctx, data)).collect::>(), - Err(err) => { - println!("JQ Create Err: {:?}", err); - vec![] - } - } - } - - fn get_filter(&self) -> Result>>, JqTemplateError> { // the template is used to be parsed in to the IR AST - let template = File { code: self.template.as_str(), path: () }; + let template = File { code: template.as_str(), path: () }; // defs is used to extend the syntax with custom definitions of functions, like // 'toString' let defs = jaq_std::defs(); @@ -685,11 +671,33 @@ impl JqTemplate { ) })?; - Ok(filter) + let mut write_lock = JQ_TEMPLATE_STORAGE.write().unwrap(); + + let template_id = write_lock.len(); + let filter = Box::new(filter); + write_lock.push(filter); + + Ok(Self { template_id, representation: format!("{:?}", term), is_const }) + } + + /// Used to execute the transformation of the JqTemplate + pub fn run<'input>(&self, data: PathValueEnum<'input>) -> Vec>> { + let inputs = RcIter::new(core::iter::empty()); + let ctx = Ctx::new([], &inputs); + + let read_guard = JQ_TEMPLATE_STORAGE.read().unwrap(); + + let filter: &Box>>> = + unsafe { std::mem::transmute(read_guard.get(self.template_id).unwrap()) }; + + filter.run((ctx, data)).collect::>() } /// Used to calculate the result and return it as json - pub fn render_value(&self, value: PathValueEnum) -> async_graphql_value::ConstValue { + pub fn render_value( + &self, + value: PathValueEnum<'_>, + ) -> async_graphql_value::ConstValue { let res = self.run(value); let res: Vec = res .into_iter() @@ -825,7 +833,7 @@ impl JqTemplate { impl Default for JqTemplate { fn default() -> Self { Self { - template: "".to_string(), + template_id: 0, representation: String::default(), is_const: true, } @@ -1171,7 +1179,7 @@ mod tests { #[test] fn test_debug() { let jq_template: JqTemplate = JqTemplate { - template: "".to_string(), + template_id: 0, representation: "test".to_string(), is_const: false, }; @@ -1182,7 +1190,7 @@ mod tests { #[test] fn test_display() { let jq_template: JqTemplate = JqTemplate { - template: "".to_string(), + template_id: 0, representation: "test".to_string(), is_const: false, }; @@ -1193,12 +1201,12 @@ mod tests { #[test] fn test_partial_eq() { let jq_template1: JqTemplate = JqTemplate { - template: "".to_string(), + template_id: 0, representation: "test".to_string(), is_const: false, }; let jq_template2: JqTemplate = JqTemplate { - template: "".to_string(), + template_id: 0, representation: "test".to_string(), is_const: true, // Different `is_const` value should not affect equality }; @@ -1208,12 +1216,12 @@ mod tests { #[test] fn test_hash() { let jq_template1: JqTemplate = JqTemplate { - template: "".to_string(), + template_id: 0, representation: "test".to_string(), is_const: false, }; let jq_template2: JqTemplate = JqTemplate { - template: "".to_string(), + template_id: 0, representation: "test".to_string(), is_const: false, }; From 1c082bee0268b48cfd7e6ef3891f18514272ae2e Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Mon, 9 Dec 2024 03:58:30 +0200 Subject: [PATCH 14/28] refactor: JqTemplate to JqTransform --- src/core/blueprint/dynamic_value.rs | 6 +- .../{jq_template.rs => jq_transform.rs} | 80 +++++++++---------- src/core/mustache/mod.rs | 4 +- 3 files changed, 45 insertions(+), 45 deletions(-) rename src/core/mustache/{jq_template.rs => jq_transform.rs} (94%) diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index b0134e6dbb..c95c804c63 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -2,13 +2,13 @@ use async_graphql_value::{ConstValue, Name}; use indexmap::IndexMap; use serde_json::Value; -use crate::core::mustache::{JqTemplate, Mustache}; +use crate::core::mustache::{JqTransform, Mustache}; #[derive(Debug, Clone, PartialEq)] pub enum DynamicValue { Value(A), Mustache(Mustache), - JqTemplate(JqTemplate), + JqTemplate(JqTransform), Object(IndexMap>), Array(Vec>), } @@ -121,7 +121,7 @@ impl TryFrom<&Value> for DynamicValue { return Ok(DynamicValue::Mustache(m)); } - match JqTemplate::try_new(s.as_str()) { + match JqTransform::try_new(s.as_str()) { Ok(t) => { if t.is_const() { tracing::info!("Successfully loaded const value template: {}", s); diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_transform.rs similarity index 94% rename from src/core/mustache/jq_template.rs rename to src/core/mustache/jq_transform.rs index bd6fa77290..65e669a864 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_transform.rs @@ -20,8 +20,8 @@ lazy_static! { /// Used to represent a JQ template. Currently used only on @expr directive. #[derive(Clone)] -pub struct JqTemplate { - /// TODO +pub struct JqTransform { + /// The compiled template transformation template_id: usize, /// The IR representation, used for debug purposes representation: String, @@ -636,7 +636,7 @@ impl From for PathValueEnum<'_> { } } -impl JqTemplate { +impl JqTransform { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { let template = transform_to_jq(template); @@ -830,7 +830,7 @@ impl JqTemplate { } } -impl Default for JqTemplate { +impl Default for JqTransform { fn default() -> Self { Self { template_id: 0, @@ -840,7 +840,7 @@ impl Default for JqTemplate { } } -impl std::fmt::Debug for JqTemplate { +impl std::fmt::Debug for JqTransform { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("JqTemplate") .field("representation", &self.representation) @@ -848,7 +848,7 @@ impl std::fmt::Debug for JqTemplate { } } -impl Display for JqTemplate { +impl Display for JqTransform { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { format!( "[JqTemplate](is_const={})({})", @@ -858,13 +858,13 @@ impl Display for JqTemplate { } } -impl std::cmp::PartialEq for JqTemplate { +impl std::cmp::PartialEq for JqTransform { fn eq(&self, other: &Self) -> bool { self.representation.eq(&other.representation) } } -impl std::hash::Hash for JqTemplate { +impl std::hash::Hash for JqTransform { fn hash(&self, state: &mut H) { self.to_string().hash(state); } @@ -943,7 +943,7 @@ mod tests { fn test_is_select_operation_simple_property() { let template = ".fruit"; assert!( - JqTemplate::is_select_operation(template), + JqTransform::is_select_operation(template), "Should return true for simple property access" ); } @@ -952,7 +952,7 @@ mod tests { fn test_is_select_operation_nested_property() { let template = ".fruit.name"; assert!( - JqTemplate::is_select_operation(template), + JqTransform::is_select_operation(template), "Should return true for nested property access" ); } @@ -961,7 +961,7 @@ mod tests { fn test_is_select_operation_array_index() { let template = ".fruits[1]"; assert!( - !JqTemplate::is_select_operation(template), + !JqTransform::is_select_operation(template), "Should return false for array index access" ); } @@ -970,7 +970,7 @@ mod tests { fn test_is_select_operation_pipe_operator() { let template = ".fruits[] | .name"; assert!( - !JqTemplate::is_select_operation(template), + !JqTransform::is_select_operation(template), "Should return false for pipe operator usage" ); } @@ -979,7 +979,7 @@ mod tests { fn test_is_select_operation_filter() { let template = ".fruits[] | select(.price > 1)"; assert!( - !JqTemplate::is_select_operation(template), + !JqTransform::is_select_operation(template), "Should return false for select filter usage" ); } @@ -988,7 +988,7 @@ mod tests { fn test_is_select_operation_function_call() { let template = "map(.price)"; assert!( - !JqTemplate::is_select_operation(template), + !JqTransform::is_select_operation(template), "Should return false for function call" ); } @@ -996,7 +996,7 @@ mod tests { #[test] fn test_render_value_no_results() { let template_str = ".[] | select(.non_existent)"; - let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); let input_json = json!([{"foo": 1}, {"foo": 2}]); let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); assert_eq!( @@ -1009,7 +1009,7 @@ mod tests { #[test] fn test_render_value_single_result() { let template_str = ".[0]"; - let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); let input_json = json!([{"foo": 1}, {"foo": 2}]); let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); assert_eq!( @@ -1022,7 +1022,7 @@ mod tests { #[test] fn test_render_value_multiple_results() { let template_str = ".[] | .foo"; - let jq_template = JqTemplate::try_new(template_str).expect("Failed to create JqTemplate"); + let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); let input_json = json!([{"foo": 1}, {"foo": 2}]); let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); let expected = async_graphql_value::ConstValue::array(vec![ @@ -1037,35 +1037,35 @@ mod tests { // Test with a constant number let term_num = Term::Num("42"); assert!( - JqTemplate::calculate_is_const(&term_num), + JqTransform::calculate_is_const(&term_num), "Expected true for a constant number" ); // Test with a string without formatter let term_str = Term::Str(None, vec![]); assert!( - JqTemplate::calculate_is_const(&term_str), + JqTransform::calculate_is_const(&term_str), "Expected true for a simple string" ); // Test with a string with formatter let term_str_fmt = Term::Str(Some("fmt"), vec![]); assert!( - !JqTemplate::calculate_is_const(&term_str_fmt), + !JqTransform::calculate_is_const(&term_str_fmt), "Expected false for a formatted string" ); // Test with an identity operation let term_id = Term::Id; assert!( - !JqTemplate::calculate_is_const(&term_id), + !JqTransform::calculate_is_const(&term_id), "Expected false for an identity operation" ); // Test with a recursive operation let term_recurse = Term::Recurse; assert!( - !JqTemplate::calculate_is_const(&term_recurse), + !JqTransform::calculate_is_const(&term_recurse), "Expected false for a recursive operation" ); @@ -1076,14 +1076,14 @@ mod tests { Box::new(Term::Num("2")), ); assert!( - !JqTemplate::calculate_is_const(&term_bin_op), + !JqTransform::calculate_is_const(&term_bin_op), "Expected false for a binary operation" ); // Test with a pipe operation without pattern let term_pipe = Term::Pipe(Box::new(Term::Num("1")), None, Box::new(Term::Num("2"))); assert!( - JqTemplate::calculate_is_const(&term_pipe), + JqTransform::calculate_is_const(&term_pipe), "Expected true for a constant pipe operation" ); @@ -1095,7 +1095,7 @@ mod tests { Box::new(Term::Num("2")), ); assert!( - !JqTemplate::calculate_is_const(&term_pipe_with_pattern), + !JqTransform::calculate_is_const(&term_pipe_with_pattern), "Expected false for a pipe operation with pattern" ); } @@ -1105,35 +1105,35 @@ mod tests { // Test with simple identity operation let term_id = Term::Id; assert!( - JqTemplate::recursive_is_select_operation(term_id), + JqTransform::recursive_is_select_operation(term_id), "Expected true for identity operation" ); // Test with a number let term_num = Term::Num("42"); assert!( - !JqTemplate::recursive_is_select_operation(term_num), + !JqTransform::recursive_is_select_operation(term_num), "Expected false for a number" ); // Test with a string without formatter let term_str = Term::Str(None, vec![]); assert!( - JqTemplate::recursive_is_select_operation(term_str), + JqTransform::recursive_is_select_operation(term_str), "Expected true for a simple string" ); // Test with a string with formatter let term_str_fmt = Term::Str(Some("fmt"), vec![]); assert!( - !JqTemplate::recursive_is_select_operation(term_str_fmt), + !JqTransform::recursive_is_select_operation(term_str_fmt), "Expected false for a formatted string" ); // Test with a recursive operation let term_recurse = Term::Recurse; assert!( - !JqTemplate::recursive_is_select_operation(term_recurse), + !JqTransform::recursive_is_select_operation(term_recurse), "Expected false for a recursive operation" ); @@ -1144,14 +1144,14 @@ mod tests { Box::new(Term::Num("2")), ); assert!( - !JqTemplate::recursive_is_select_operation(term_bin_op), + !JqTransform::recursive_is_select_operation(term_bin_op), "Expected false for a binary operation" ); // Test with a pipe operation without pattern let term_pipe = Term::Pipe(Box::new(Term::Num("1")), None, Box::new(Term::Num("2"))); assert!( - !JqTemplate::recursive_is_select_operation(term_pipe), + !JqTransform::recursive_is_select_operation(term_pipe), "Expected false for a constant pipe operation" ); @@ -1163,14 +1163,14 @@ mod tests { Box::new(Term::Num("2")), ); assert!( - !JqTemplate::recursive_is_select_operation(term_pipe_with_pattern), + !JqTransform::recursive_is_select_operation(term_pipe_with_pattern), "Expected false for a pipe operation with pattern" ); } #[test] fn test_default() { - let jq_template: JqTemplate = JqTemplate::default(); + let jq_template: JqTransform = JqTransform::default(); assert_eq!(jq_template.representation, ""); assert!(jq_template.is_const); // Assuming `filter` has a sensible default implementation @@ -1178,7 +1178,7 @@ mod tests { #[test] fn test_debug() { - let jq_template: JqTemplate = JqTemplate { + let jq_template: JqTransform = JqTransform { template_id: 0, representation: "test".to_string(), is_const: false, @@ -1189,7 +1189,7 @@ mod tests { #[test] fn test_display() { - let jq_template: JqTemplate = JqTemplate { + let jq_template: JqTransform = JqTransform { template_id: 0, representation: "test".to_string(), is_const: false, @@ -1200,12 +1200,12 @@ mod tests { #[test] fn test_partial_eq() { - let jq_template1: JqTemplate = JqTemplate { + let jq_template1: JqTransform = JqTransform { template_id: 0, representation: "test".to_string(), is_const: false, }; - let jq_template2: JqTemplate = JqTemplate { + let jq_template2: JqTransform = JqTransform { template_id: 0, representation: "test".to_string(), is_const: true, // Different `is_const` value should not affect equality @@ -1215,12 +1215,12 @@ mod tests { #[test] fn test_hash() { - let jq_template1: JqTemplate = JqTemplate { + let jq_template1: JqTransform = JqTransform { template_id: 0, representation: "test".to_string(), is_const: false, }; - let jq_template2: JqTemplate = JqTemplate { + let jq_template2: JqTransform = JqTransform { template_id: 0, representation: "test".to_string(), is_const: false, diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index 0ff81de498..4c939b5d1a 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -1,7 +1,7 @@ mod eval; -mod jq_template; +mod jq_transform; mod model; mod parse; pub use eval::{Eval, PathStringEval}; -pub use jq_template::*; +pub use jq_transform::*; pub use model::*; From 551c091c89d294cef0e70ec11da0943c88399f5b Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Mon, 9 Dec 2024 15:44:53 +0200 Subject: [PATCH 15/28] feat: add jq_template --- src/core/blueprint/dynamic_value.rs | 29 +- src/core/mustache/jq_template.rs | 145 +++++ src/core/mustache/jq_transform.rs | 583 ++++++++---------- src/core/mustache/mod.rs | 2 + src/core/path.rs | 22 + src/core/serde_value_ext.rs | 7 +- .../graphql-conformance-015.md_11.snap | 2 +- .../graphql-conformance-015.md_8.snap | 2 +- .../graphql-conformance-http-015.md_11.snap | 2 +- .../graphql-conformance-http-015.md_8.snap | 2 +- .../snapshots/test-jq-template.md_merged.snap | 2 +- tests/execution/http-select.md | 2 +- tests/execution/test-jq-template.md | 2 +- 13 files changed, 432 insertions(+), 370 deletions(-) create mode 100644 src/core/mustache/jq_template.rs diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index c95c804c63..58e871e6f9 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -2,13 +2,13 @@ use async_graphql_value::{ConstValue, Name}; use indexmap::IndexMap; use serde_json::Value; -use crate::core::mustache::{JqTransform, Mustache}; +use crate::core::mustache::{JqTemplate, Mustache}; #[derive(Debug, Clone, PartialEq)] pub enum DynamicValue { Value(A), Mustache(Mustache), - JqTemplate(JqTransform), + JqTemplate(JqTemplate), Object(IndexMap>), Array(Vec>), } @@ -115,27 +115,12 @@ impl TryFrom<&Value> for DynamicValue { Ok(DynamicValue::Array(out?)) } Value::String(s) => { - let m = Mustache::parse(s.as_str()); - if !m.is_const() { - tracing::info!("Successfully loaded Mustache template: {}", s); - return Ok(DynamicValue::Mustache(m)); - } + let jqt = JqTemplate::parse(s); - match JqTransform::try_new(s.as_str()) { - Ok(t) => { - if t.is_const() { - tracing::info!("Successfully loaded const value template: {}", s); - Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) - } else { - tracing::info!("Successfully loaded JQ template: {}", s); - Ok(DynamicValue::JqTemplate(t)) - } - } - Err(err) => { - tracing::info!("Defaulting to const value template: {}", s); - tracing::warn!("JQ template error: {:?}", err); - Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) - } + if jqt.is_const() { + Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) + } else { + Ok(DynamicValue::JqTemplate(jqt)) } } _ => Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)), diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs new file mode 100644 index 0000000000..dab030c1ee --- /dev/null +++ b/src/core/mustache/jq_template.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use nom::branch::alt; +use nom::bytes::complete::{tag, take_until}; +use nom::combinator::map; +use nom::multi::many0; +use nom::sequence::delimited; +use nom::{Finish, IResult}; + +use super::{JqTransform, Mustache, PathJqValueString}; +use crate::core::mustache::{JqTemplateError, Segment}; + +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum JqTemplateIR { + JqTransform(JqTransform), + Literal(String), + Mustache(Mustache), +} + +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct JqTemplate(pub Vec); + +impl JqTemplate { + pub fn is_const(&self) -> bool { + self.0.iter().all(|v| match v { + JqTemplateIR::JqTransform(jq) => jq.is_const(), + JqTemplateIR::Literal(_) => true, + JqTemplateIR::Mustache(m) => m.is_const(), + }) + } + + // TODO: return error + pub fn render_value(&self, ctx: &impl PathJqValueString) -> async_graphql_value::ConstValue { + let expressions_len = self.0.len(); + match expressions_len { + 0 => async_graphql_value::ConstValue::Null, + 1 => { + let expression = self.0.first().unwrap(); + self.execute_expression(ctx, expression) + } + _ => { + let result = self + .0 + .iter() + .map(|expr| self.execute_expression(ctx, expr)) + .fold(String::new(), |mut acc, cur| { + match &cur { + async_graphql::Value::String(s) => acc += s, + _ => acc += &cur.to_string(), + } + acc + }); + async_graphql_value::ConstValue::String(result) + } + } + } + + fn execute_expression( + &self, + ctx: &impl PathJqValueString, + expression: &JqTemplateIR, + ) -> async_graphql_value::ConstValue { + match expression { + JqTemplateIR::JqTransform(jq_transform) => { + jq_transform.render_value(super::PathValueEnum::PathValue(Arc::new(ctx))) + } + JqTemplateIR::Literal(value) => async_graphql_value::ConstValue::String(value.clone()), + JqTemplateIR::Mustache(mustache) => { + let mustache_result = mustache.render(ctx); + + serde_json::from_str::(&mustache_result) + .unwrap_or_else(|_| async_graphql_value::ConstValue::String(mustache_result)) + } + } + } + + pub fn parse(template: &str) -> Self { + let result = parse_jq_template(template).finish(); + match result { + Ok((_, jq_template)) => jq_template, + Err(_) => Self(vec![JqTemplateIR::Literal(template.to_string())]), + } + } +} + +fn parse_expression(input: &str) -> IResult<&str, JqTemplateIR> { + delimited( + tag("{{"), + map(take_until("}}"), |template| { + // TODO: use the error + match JqTransform::try_new(template) { + Ok(jq) => JqTemplateIR::JqTransform(jq), + Err(err) => match err { + JqTemplateError::JqIsMustache => { + let expression: Vec<_> = template + .trim() + .split('.') + .skip(1) + .map(String::from) + .collect(); + let segment = Segment::Expression(expression); + JqTemplateIR::Mustache(Mustache::from(vec![segment])) + } + _ => JqTemplateIR::Literal(template.to_string()), + }, + } + }), + tag("}}"), + )(input) +} + +fn parse_segment(input: &str) -> IResult<&str, Vec> { + let expression_result = many0(alt(( + parse_expression, + map(take_until("{{"), |txt: &str| { + JqTemplateIR::Literal(txt.to_string()) + }), + )))(input); + + if let Ok((remaining, segments)) = expression_result { + if remaining.is_empty() { + Ok((remaining, segments)) + } else { + let mut segments = segments; + segments.push(JqTemplateIR::Literal(remaining.to_string())); + Ok(("", segments)) + } + } else { + Ok(("", vec![JqTemplateIR::Literal(input.to_string())])) + } +} + +fn parse_jq_template(input: &str) -> IResult<&str, JqTemplate> { + map(parse_segment, |segments| { + JqTemplate( + segments + .into_iter() + .filter(|seg| match seg { + JqTemplateIR::Literal(s) => (!s.is_empty()) && s != "\"", + _ => true, + }) + .collect(), + ) + })(input) +} diff --git a/src/core/mustache/jq_transform.rs b/src/core/mustache/jq_transform.rs index 65e669a864..b946beb0b9 100644 --- a/src/core/mustache/jq_transform.rs +++ b/src/core/mustache/jq_transform.rs @@ -7,14 +7,13 @@ use jaq_core::val::Range; use jaq_core::{Compiler, Ctx, Error, Exn, Filter, Native, RcIter, ValR}; use jaq_json::Val; use lazy_static::lazy_static; -use regex::Regex; use crate::core::ir::{EvalContext, ResolverContextLike}; use crate::core::json::JsonLike; use crate::core::path::{PathString, PathValue, ValueString}; lazy_static! { - static ref JQ_TEMPLATE_STORAGE: RwLock>>>>> = + static ref JQ_TEMPLATE_STORAGE: RwLock>>>> = RwLock::new(Vec::new()); } @@ -25,8 +24,6 @@ pub struct JqTransform { template_id: usize, /// The IR representation, used for debug purposes representation: String, - /// If the transformer returns a constant value - is_const: bool, } #[derive(Clone)] @@ -639,16 +636,23 @@ impl From for PathValueEnum<'_> { impl JqTransform { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { - let template = transform_to_jq(template); - // the term is used because it can be easily serialized, deserialized and hashed - let term = Self::parse_template(&template); + let term = Self::parse_template(template); + + // calculate if the expression can be replaced with mustache + let is_mustache = Self::recursive_is_mustache(&term); + if is_mustache { + return Err(JqTemplateError::JqIsMustache); + } // calculate if the expression returns always a constant value let is_const = Self::calculate_is_const(&term); + if is_const { + return Err(JqTemplateError::JqIstConst); + } // the template is used to be parsed in to the IR AST - let template = File { code: template.as_str(), path: () }; + let template = File { code: template, path: () }; // defs is used to extend the syntax with custom definitions of functions, like // 'toString' let defs = jaq_std::defs(); @@ -674,10 +678,10 @@ impl JqTransform { let mut write_lock = JQ_TEMPLATE_STORAGE.write().unwrap(); let template_id = write_lock.len(); - let filter = Box::new(filter); + let filter = filter; write_lock.push(filter); - Ok(Self { template_id, representation: format!("{:?}", term), is_const }) + Ok(Self { template_id, representation: format!("{:?}", term) }) } /// Used to execute the transformation of the JqTemplate @@ -687,17 +691,14 @@ impl JqTransform { let read_guard = JQ_TEMPLATE_STORAGE.read().unwrap(); - let filter: &Box>>> = + let filter: &Filter>> = unsafe { std::mem::transmute(read_guard.get(self.template_id).unwrap()) }; filter.run((ctx, data)).collect::>() } /// Used to calculate the result and return it as json - pub fn render_value( - &self, - value: PathValueEnum<'_>, - ) -> async_graphql_value::ConstValue { + pub fn render_value(&self, value: PathValueEnum<'_>) -> async_graphql_value::ConstValue { let res = self.run(value); let res: Vec = res .into_iter() @@ -730,13 +731,6 @@ impl JqTransform { } } - /// Used to determine if the expression can be supported with current - /// Mustache implementation - pub fn is_select_operation(template: &str) -> bool { - let term = Self::parse_template(template); - Self::recursive_is_select_operation(term) - } - /// Used to parse the template string and return the IR representation fn parse_template(template: &str) -> Term<&str> { let lexer = jaq_core::load::Lexer::new(template); @@ -747,23 +741,48 @@ impl JqTransform { /// Used as a helper function to determine if the term can be supported with /// Mustache implementation - fn recursive_is_select_operation(term: Term<&str>) -> bool { + fn recursive_is_mustache(term: &Term<&str>) -> bool { match term { Term::Id => true, Term::Recurse => false, - Term::Num(_) => false, - Term::Str(formater, _) => formater.is_none(), + // const number values + Term::Num(_) => true, + // const string values + Term::Str(formater, inner) => formater.is_none() && (inner.len() == 1), Term::Arr(_) => false, Term::Obj(_) => false, Term::Neg(_) => false, - Term::Pipe(local_term_1, pattern, local_term_2) => { - if pattern.is_some() { - false - } else { - Self::recursive_is_select_operation(*local_term_1) - && Self::recursive_is_select_operation(*local_term_2) - } + Term::Pipe(_, _, _) => false, + Term::BinOp(_, _, _) => false, + Term::Label(_, _) => false, + Term::Break(_) => false, + Term::Fold(_, _, _, _) => false, + Term::TryCatch(_, _) => false, + Term::IfThenElse(_, _) => false, + Term::Def(_, _) => false, + // 'true' and 'false' values + Term::Call(name, args) => (*name == "true" || *name == "false") && args.is_empty(), + Term::Var(_) => false, + // paths .data.foo.bar + Term::Path(local_term, path) => { + Self::recursive_is_mustache(local_term) && Self::is_path_select_operation(path) } + } + } + + /// Used to check if a JQ path can be supported by mustache + fn is_path_mustache(term: &Term<&str>) -> bool { + match term { + Term::Id => true, + Term::Recurse => false, + // numbers, for example: .[1] + Term::Num(_) => false, + // string, for example: .data.user + Term::Str(formater, inner) => formater.is_none() && (inner.len() == 1), + Term::Arr(_) => false, + Term::Obj(_) => false, + Term::Neg(_) => false, + Term::Pipe(_, _, _) => false, Term::BinOp(_, _, _) => false, Term::Label(_, _) => false, Term::Break(_) => false, @@ -771,23 +790,22 @@ impl JqTransform { Term::TryCatch(_, _) => false, Term::IfThenElse(_, _) => false, Term::Def(_, _) => false, + // 'true' and 'false' values Term::Call(_, _) => false, Term::Var(_) => false, + // paths .data.foo.bar Term::Path(local_term, path) => { - Self::recursive_is_select_operation(*local_term) - && Self::is_path_select_operation(path) + Self::is_path_mustache(local_term) && Self::is_path_select_operation(path) } } } /// Used to check if the path indicates a select operation or modify - fn is_path_select_operation(path: jaq_core::path::Path>) -> bool { - path.0.into_iter().all(|part| match part { - (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Optional) => { - Self::recursive_is_select_operation(idx) - } + fn is_path_select_operation(path: &jaq_core::path::Path>) -> bool { + path.0.iter().all(|part| match part { + (jaq_core::path::Part::Index(_), jaq_core::path::Opt::Optional) => false, (jaq_core::path::Part::Index(idx), jaq_core::path::Opt::Essential) => { - Self::recursive_is_select_operation(idx) + Self::is_path_mustache(idx) } (jaq_core::path::Part::Range(_, _), jaq_core::path::Opt::Optional) => false, (jaq_core::path::Part::Range(_, _), jaq_core::path::Opt::Essential) => false, @@ -799,18 +817,14 @@ impl JqTransform { match term { Term::Id => false, Term::Recurse => false, + // const number values Term::Num(_) => true, - Term::Str(formater, _) => formater.is_none(), + // const string values + Term::Str(formater, inner) => formater.is_none() && (inner.len() == 1), Term::Arr(_) => false, Term::Obj(_) => false, Term::Neg(_) => false, - Term::Pipe(local_term_1, pattern, local_term_2) => { - if pattern.is_some() { - false - } else { - Self::calculate_is_const(local_term_1) && Self::calculate_is_const(local_term_2) - } - } + Term::Pipe(_, _, _) => false, Term::BinOp(_, _, _) => false, Term::Label(_, _) => false, Term::Break(_) => false, @@ -818,25 +832,17 @@ impl JqTransform { Term::TryCatch(_, _) => false, Term::IfThenElse(_, _) => false, Term::Def(_, _) => false, - Term::Call(_, _) => false, + // 'true' and 'false' values + Term::Call(name, args) => (*name == "true" || *name == "false") && args.is_empty(), Term::Var(_) => false, Term::Path(_, _) => false, } } - /// Used to determine if the transformer is a static value + /// Because we make checks when creating JqTeamplate to prevent the creation + /// of const JqTemplate we can safely return always false pub fn is_const(&self) -> bool { - self.is_const - } -} - -impl Default for JqTransform { - fn default() -> Self { - Self { - template_id: 0, - representation: String::default(), - is_const: true, - } + false } } @@ -850,11 +856,7 @@ impl std::fmt::Debug for JqTransform { impl Display for JqTransform { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - format!( - "[JqTemplate](is_const={})({})", - self.is_const, self.representation - ) - .fmt(f) + format!("[JqTemplate]({})", self.representation).fmt(f) } } @@ -878,408 +880,319 @@ pub enum JqTemplateError { JqLoadError(Vec), #[error("JQ Compile Errors: {0:?}")] JqCompileError(Vec), -} - -/// Used to convert mustache to jq -fn transform_to_jq(input: &str) -> String { - let input = input - .to_string() - .replace("{{{", "[-<-[{") - .replace("}}}", "}]->-]") - .replace("{{", "[-<-[") - .replace("}}", "]->]"); - let re = Regex::new(r"\[-<-\[(.*?)\]->\]").unwrap(); - let mut result = String::new(); - let mut last_end = 0; - let captures: Vec<_> = re.captures_iter(&input).collect(); - - for cap in captures { - let match_ = cap.get(0).unwrap(); - let var_name = cap.get(1).unwrap().as_str(); - - // Append the text before the match, then the transformed variable - if last_end != match_.start() { - if !result.is_empty() { - result.push_str(" + "); - } - result.push_str(&format!("\"{}\"", &input[last_end..match_.start()])); - } - - if !result.is_empty() { - result.push_str(" + "); - } - result.push_str(var_name); - - last_end = match_.end(); - } - - // Append any remaining text after the last match - if last_end < input.len() { - if !result.is_empty() { - result.push_str(" + "); - } - result.push_str(&format!("\"{}\"", &input[last_end..])); - } - - // If the result is empty, it means the input was a single mustache expression - if result.is_empty() { - return input.to_string(); - } - - // Remove unnecessary delimiters from the result - result.replace("\"[-<-[", "").replace("]->-]\"", "") + #[error("JQ Transform can be replaced with a Mustache")] + JqIsMustache, + #[error("JQ Transform can be replaced with a Literal")] + JqIstConst, } #[cfg(test)] mod tests { use std::hash::{DefaultHasher, Hash, Hasher}; - use jaq_core::load::parse::{BinaryOp, Pattern, Term}; use serde_json::json; use super::*; #[test] - fn test_is_select_operation_simple_property() { - let template = ".fruit"; + fn test_is_mustache_simple_property() { + let term = JqTransform::parse_template(".fruit"); assert!( - JqTransform::is_select_operation(template), + JqTransform::recursive_is_mustache(&term), "Should return true for simple property access" ); } #[test] - fn test_is_select_operation_nested_property() { - let template = ".fruit.name"; + fn test_is_mustache_nested_property() { + let term = JqTransform::parse_template(".fruit.name"); assert!( - JqTransform::is_select_operation(template), + JqTransform::recursive_is_mustache(&term), "Should return true for nested property access" ); } #[test] - fn test_is_select_operation_array_index() { - let template = ".fruits[1]"; + fn test_is_mustache_optional() { + let term = JqTransform::parse_template(".fruit.name?"); + assert!( + !JqTransform::recursive_is_mustache(&term), + "Should return false for optional operator" + ); + } + + #[test] + fn test_is_mustache_array_index() { + let term = JqTransform::parse_template(".fruits[1]"); assert!( - !JqTransform::is_select_operation(template), + !JqTransform::recursive_is_mustache(&term), "Should return false for array index access" ); } #[test] - fn test_is_select_operation_pipe_operator() { - let template = ".fruits[] | .name"; + fn test_is_mustache_pipe_operator() { + let term = JqTransform::parse_template(".fruits[] | .name"); assert!( - !JqTransform::is_select_operation(template), + !JqTransform::recursive_is_mustache(&term), "Should return false for pipe operator usage" ); } #[test] - fn test_is_select_operation_filter() { - let template = ".fruits[] | select(.price > 1)"; + fn test_is_mustache_filter() { + let term = JqTransform::parse_template(".fruits[] | select(.price > 1)"); assert!( - !JqTransform::is_select_operation(template), + !JqTransform::recursive_is_mustache(&term), "Should return false for select filter usage" ); } #[test] - fn test_is_select_operation_function_call() { - let template = "map(.price)"; + fn test_is_mustache_true_value() { + let term = JqTransform::parse_template("true"); assert!( - !JqTransform::is_select_operation(template), - "Should return false for function call" + JqTransform::recursive_is_mustache(&term), + "Should return true for const true value" ); } #[test] - fn test_render_value_no_results() { - let template_str = ".[] | select(.non_existent)"; - let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); - let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); - assert_eq!( - result, - async_graphql_value::ConstValue::Null, - "Expected Null for no results" + fn test_is_mustache_false_value() { + let term = JqTransform::parse_template("false"); + assert!( + JqTransform::recursive_is_mustache(&term), + "Should return true for const false value" ); } #[test] - fn test_render_value_single_result() { - let template_str = ".[0]"; - let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); - let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); - assert_eq!( - result, - async_graphql_value::ConstValue::from_json(json!({"foo": 1})).unwrap(), - "Expected single result" + fn test_is_mustache_number_value() { + let term = JqTransform::parse_template("1"); + assert!( + JqTransform::recursive_is_mustache(&term), + "Should return true for number value" ); } #[test] - fn test_render_value_multiple_results() { - let template_str = ".[] | .foo"; - let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); - let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); - let expected = async_graphql_value::ConstValue::array(vec![ - async_graphql_value::ConstValue::from_json(json!(1)).unwrap(), - async_graphql_value::ConstValue::from_json(json!(2)).unwrap(), - ]); - assert_eq!(result, expected, "Expected array of results"); + fn test_is_mustache_str_value() { + let term = JqTransform::parse_template("\"foobar\""); + assert!( + JqTransform::recursive_is_mustache(&term), + "Should return true for string value" + ); } #[test] - fn test_calculate_is_const() { - // Test with a constant number - let term_num = Term::Num("42"); + fn test_is_mustache_str_interpolate_value() { + let term = JqTransform::parse_template("\"Hello, \\(.name)!\""); assert!( - JqTransform::calculate_is_const(&term_num), - "Expected true for a constant number" + !JqTransform::recursive_is_mustache(&term), + "Should return false for string interpolated value" ); + } - // Test with a string without formatter - let term_str = Term::Str(None, vec![]); + #[test] + fn test_is_mustache_function_call() { + let term = JqTransform::parse_template("map(.price)"); assert!( - JqTransform::calculate_is_const(&term_str), - "Expected true for a simple string" + !JqTransform::recursive_is_mustache(&term), + "Should return false for function call" ); + } - // Test with a string with formatter - let term_str_fmt = Term::Str(Some("fmt"), vec![]); + #[test] + fn test_is_mustache_concat() { + let term = JqTransform::parse_template(".data.meat + .data.eggs"); assert!( - !JqTransform::calculate_is_const(&term_str_fmt), - "Expected false for a formatted string" + !JqTransform::recursive_is_mustache(&term), + "Should return false for concatenation" ); + } - // Test with an identity operation - let term_id = Term::Id; + #[test] + fn test_is_const_simple_property() { + let term = JqTransform::parse_template(".fruit"); assert!( - !JqTransform::calculate_is_const(&term_id), - "Expected false for an identity operation" + !JqTransform::calculate_is_const(&term), + "Should return false for simple property access" ); + } - // Test with a recursive operation - let term_recurse = Term::Recurse; + #[test] + fn test_is_const_nested_property() { + let term = JqTransform::parse_template(".fruit.name"); assert!( - !JqTransform::calculate_is_const(&term_recurse), - "Expected false for a recursive operation" + !JqTransform::calculate_is_const(&term), + "Should return false for nested property access" ); + } - // Test with a binary operation - let term_bin_op = Term::BinOp( - Box::new(Term::Num("1")), - BinaryOp::Math(jaq_core::ops::Math::Add), - Box::new(Term::Num("2")), - ); + #[test] + fn test_is_const_array_index() { + let term = JqTransform::parse_template(".fruits[1]"); assert!( - !JqTransform::calculate_is_const(&term_bin_op), - "Expected false for a binary operation" + !JqTransform::calculate_is_const(&term), + "Should return false for array index access" ); + } - // Test with a pipe operation without pattern - let term_pipe = Term::Pipe(Box::new(Term::Num("1")), None, Box::new(Term::Num("2"))); + #[test] + fn test_is_const_pipe_operator() { + let term = JqTransform::parse_template(".fruits[] | .name"); assert!( - JqTransform::calculate_is_const(&term_pipe), - "Expected true for a constant pipe operation" + !JqTransform::calculate_is_const(&term), + "Should return false for pipe operator usage" ); + } - // Test with a pipe operation with pattern - let pattern = Pattern::Var("x"); - let term_pipe_with_pattern = Term::Pipe( - Box::new(Term::Num("1")), - Some(pattern), - Box::new(Term::Num("2")), - ); + #[test] + fn test_is_const_filter() { + let term = JqTransform::parse_template(".fruits[] | select(.price > 1)"); assert!( - !JqTransform::calculate_is_const(&term_pipe_with_pattern), - "Expected false for a pipe operation with pattern" + !JqTransform::calculate_is_const(&term), + "Should return false for select filter usage" ); } #[test] - fn test_recursive_is_select_operation() { - // Test with simple identity operation - let term_id = Term::Id; + fn test_is_const_true_value() { + let term = JqTransform::parse_template("true"); assert!( - JqTransform::recursive_is_select_operation(term_id), - "Expected true for identity operation" + JqTransform::calculate_is_const(&term), + "Should return true for const true value" ); + } - // Test with a number - let term_num = Term::Num("42"); + #[test] + fn test_is_const_false_value() { + let term = JqTransform::parse_template("false"); assert!( - !JqTransform::recursive_is_select_operation(term_num), - "Expected false for a number" + JqTransform::calculate_is_const(&term), + "Should return true for const false value" ); + } - // Test with a string without formatter - let term_str = Term::Str(None, vec![]); + #[test] + fn test_is_const_number_value() { + let term = JqTransform::parse_template("1"); assert!( - JqTransform::recursive_is_select_operation(term_str), - "Expected true for a simple string" + JqTransform::calculate_is_const(&term), + "Should return true for number value" ); + } - // Test with a string with formatter - let term_str_fmt = Term::Str(Some("fmt"), vec![]); + #[test] + fn test_is_const_str_value() { + let term = JqTransform::parse_template("\"foobar\""); assert!( - !JqTransform::recursive_is_select_operation(term_str_fmt), - "Expected false for a formatted string" + JqTransform::calculate_is_const(&term), + "Should return true for string value" ); + } - // Test with a recursive operation - let term_recurse = Term::Recurse; + #[test] + fn test_is_const_str_interpolate_value() { + let term = JqTransform::parse_template("\"Hello, \\(.name)!\""); assert!( - !JqTransform::recursive_is_select_operation(term_recurse), - "Expected false for a recursive operation" + !JqTransform::calculate_is_const(&term), + "Should return false for string interpolated value" ); + } - // Test with a binary operation - let term_bin_op = Term::BinOp( - Box::new(Term::Num("1")), - BinaryOp::Math(jaq_core::ops::Math::Add), - Box::new(Term::Num("2")), - ); + #[test] + fn test_is_const_function_call() { + let term = JqTransform::parse_template("map(.price)"); assert!( - !JqTransform::recursive_is_select_operation(term_bin_op), - "Expected false for a binary operation" + !JqTransform::calculate_is_const(&term), + "Should return false for function call" ); + } - // Test with a pipe operation without pattern - let term_pipe = Term::Pipe(Box::new(Term::Num("1")), None, Box::new(Term::Num("2"))); + #[test] + fn test_is_const_concat() { + let term = JqTransform::parse_template(".data.meat + .data.eggs"); assert!( - !JqTransform::recursive_is_select_operation(term_pipe), - "Expected false for a constant pipe operation" + !JqTransform::calculate_is_const(&term), + "Should return false for concatenation" ); + } - // Test with a pipe operation with pattern - let pattern = Pattern::Var("x"); - let term_pipe_with_pattern = Term::Pipe( - Box::new(Term::Num("1")), - Some(pattern), - Box::new(Term::Num("2")), + #[test] + fn test_render_value_no_results() { + let template_str = ".[] | select(.non_existent)"; + let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + assert_eq!( + result, + async_graphql_value::ConstValue::Null, + "Expected Null for no results" ); - assert!( - !JqTransform::recursive_is_select_operation(term_pipe_with_pattern), - "Expected false for a pipe operation with pattern" + } + + #[test] + fn test_render_value_single_result() { + let template_str = ".[0]"; + let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + assert_eq!( + result, + async_graphql_value::ConstValue::from_json(json!({"foo": 1})).unwrap(), + "Expected single result" ); } #[test] - fn test_default() { - let jq_template: JqTransform = JqTransform::default(); - assert_eq!(jq_template.representation, ""); - assert!(jq_template.is_const); - // Assuming `filter` has a sensible default implementation + fn test_render_value_multiple_results() { + let template_str = ".[] | .foo"; + let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); + let input_json = json!([{"foo": 1}, {"foo": 2}]); + let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + let expected = async_graphql_value::ConstValue::array(vec![ + async_graphql_value::ConstValue::from_json(json!(1)).unwrap(), + async_graphql_value::ConstValue::from_json(json!(2)).unwrap(), + ]); + assert_eq!(result, expected, "Expected array of results"); } #[test] fn test_debug() { - let jq_template: JqTransform = JqTransform { - template_id: 0, - representation: "test".to_string(), - is_const: false, - }; + let jq_template: JqTransform = + JqTransform { template_id: 0, representation: "test".to_string() }; let debug_string = format!("{:?}", jq_template); assert_eq!(debug_string, "JqTemplate { representation: \"test\" }"); } #[test] fn test_display() { - let jq_template: JqTransform = JqTransform { - template_id: 0, - representation: "test".to_string(), - is_const: false, - }; + let jq_template: JqTransform = + JqTransform { template_id: 0, representation: "test".to_string() }; let display_string = format!("{}", jq_template); - assert_eq!(display_string, "[JqTemplate](is_const=false)(test)"); + assert_eq!(display_string, "[JqTemplate](test)"); } #[test] fn test_partial_eq() { - let jq_template1: JqTransform = JqTransform { - template_id: 0, - representation: "test".to_string(), - is_const: false, - }; - let jq_template2: JqTransform = JqTransform { - template_id: 0, - representation: "test".to_string(), - is_const: true, // Different `is_const` value should not affect equality - }; + let jq_template1: JqTransform = + JqTransform { template_id: 0, representation: "test".to_string() }; + let jq_template2: JqTransform = + JqTransform { template_id: 0, representation: "test".to_string() }; assert_eq!(jq_template1, jq_template2); } #[test] fn test_hash() { - let jq_template1: JqTransform = JqTransform { - template_id: 0, - representation: "test".to_string(), - is_const: false, - }; - let jq_template2: JqTransform = JqTransform { - template_id: 0, - representation: "test".to_string(), - is_const: false, - }; + let jq_template1: JqTransform = + JqTransform { template_id: 0, representation: "test".to_string() }; + let jq_template2: JqTransform = + JqTransform { template_id: 0, representation: "test".to_string() }; let mut hasher1 = DefaultHasher::new(); let mut hasher2 = DefaultHasher::new(); jq_template1.hash(&mut hasher1); jq_template2.hash(&mut hasher2); assert_eq!(hasher1.finish(), hasher2.finish()); } - - #[test] - fn test_transform_to_jq() { - assert_eq!(transform_to_jq("Hello world"), "\"Hello world\""); - - assert_eq!( - transform_to_jq("Hello world: {{.foo.buzz | split(\" \")}}"), - "\"Hello world: \" + .foo.buzz | split(\" \")" - ); - - assert_eq!( - transform_to_jq("\"Hello world: \" + .foo.buzz | split(\" \")"), - "\"\"Hello world: \" + .foo.buzz | split(\" \")\"" - ); - - assert_eq!( - transform_to_jq("Hello world: {{.foo.buzz | split(\" \")}} this is great"), - "\"Hello world: \" + .foo.buzz | split(\" \") + \" this is great\"" - ); - assert_eq!( - transform_to_jq("\"Hello world: \" + .foo.buzz | split(\" \") + \" this is great\""), - "\"\"Hello world: \" + .foo.buzz | split(\" \") + \" this is great\"\"" - ); - - assert_eq!( - transform_to_jq("{{.foo.buzz | split(\" \")}} buzz"), - ".foo.buzz | split(\" \") + \" buzz\"" - ); - assert_eq!( - transform_to_jq(".foo.buzz | split(\" \") + \" buzz\""), - "\".foo.buzz | split(\" \") + \" buzz\"\"" - ); - - assert_eq!( - transform_to_jq("{{.foo.buzz | split(\" \")}} of type {{.bar}}"), - ".foo.buzz | split(\" \") + \" of type \" + .bar" - ); - assert_eq!( - transform_to_jq(".foo.buzz | split(\" \") + \" of type \" + .bar"), - "\".foo.buzz | split(\" \") + \" of type \" + .bar\"" - ); - - assert_eq!( - transform_to_jq("{{.foo.buzz | split(\" \")}} of type {{.bar}}"), - ".foo.buzz | split(\" \") + \" of type \" + .bar" - ); - assert_eq!( - transform_to_jq("{{.value.body | split(\" \") | {first: .[0], second: .[1]}}}"), - ".value.body | split(\" \") | {first: .[0], second: .[1]}" - ); - } } diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index 4c939b5d1a..f9edb62b63 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -1,7 +1,9 @@ mod eval; +mod jq_template; mod jq_transform; mod model; mod parse; pub use eval::{Eval, PathStringEval}; +pub use jq_template::*; pub use jq_transform::*; pub use model::*; diff --git a/src/core/path.rs b/src/core/path.rs index 006450d19c..c6ca84860e 100644 --- a/src/core/path.rs +++ b/src/core/path.rs @@ -2,6 +2,7 @@ //! structure. use std::borrow::Cow; +use indexmap::IndexMap; use serde_json::json; use crate::core::ir::{EvalContext, ResolverContextLike}; @@ -79,6 +80,27 @@ impl EvalContext<'_, Ctx> { "vars" => Some(ValueString::String(Cow::Owned( json!(ctx.vars()).to_string(), ))), + "headers" => { + let arr = ctx + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str())) + .filter_map(|(k, v)| { + if let Ok(v) = v { + Some((async_graphql_value::Name::new(k), v)) + } else { + None + } + }) + .fold(IndexMap::new(), |mut acc, (k, v)| { + acc.insert(k, v.into()); + acc + }); + + Some(ValueString::Value(Cow::Owned( + async_graphql_value::ConstValue::object(arr), + ))) + } _ => None, }; } diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index e33c54e74e..a5fbd91de5 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -1,12 +1,10 @@ use std::borrow::Cow; -use std::sync::Arc; use async_graphql::{Name, Value as GraphQLValue}; use indexmap::IndexMap; use super::mustache::PathJqValueString; use crate::core::blueprint::DynamicValue; -use crate::core::mustache::PathValueEnum; pub trait ValueExt { fn render_value(&self, ctx: &impl PathJqValueString) -> GraphQLValue; @@ -25,10 +23,7 @@ impl ValueExt for DynamicValue { // but, we can just use that string as is .unwrap_or_else(|_| GraphQLValue::String(rendered.into_owned())) } - DynamicValue::JqTemplate(t) => { - let v = PathValueEnum::PathValue(Arc::new(ctx)); - t.render_value(v) - } + DynamicValue::JqTemplate(t) => t.render_value(ctx), DynamicValue::Object(obj) => { let out: IndexMap<_, _> = obj .iter() diff --git a/tests/core/snapshots/graphql-conformance-015.md_11.snap b/tests/core/snapshots/graphql-conformance-015.md_11.snap index a24ffcbc5d..c632b77700 100644 --- a/tests/core/snapshots/graphql-conformance-015.md_11.snap +++ b/tests/core/snapshots/graphql-conformance-015.md_11.snap @@ -12,7 +12,7 @@ expression: response "user": { "id": 4, "name": "User 4", - "spam": "FIZZ: [{\"bar\":\"BUZZ\"},{\"bar\":\"test\"}]" + "spam": "FIZZ: [{bar: \"BUZZ\"}, {bar: \"test\"}]" } } } diff --git a/tests/core/snapshots/graphql-conformance-015.md_8.snap b/tests/core/snapshots/graphql-conformance-015.md_8.snap index ada31c2959..9e59f910b9 100644 --- a/tests/core/snapshots/graphql-conformance-015.md_8.snap +++ b/tests/core/snapshots/graphql-conformance-015.md_8.snap @@ -12,7 +12,7 @@ expression: response "user": { "id": 4, "name": "User 4", - "searchComments": "video_4_[[\"test\",\"tost\"],[\"foo\"],[\"bar\"],[\"bizz\",\"buzz\"]]" + "searchComments": "video_4_[[\"test\", \"tost\"], [\"foo\"], [\"bar\"], [\"bizz\", \"buzz\"]]" } } } diff --git a/tests/core/snapshots/graphql-conformance-http-015.md_11.snap b/tests/core/snapshots/graphql-conformance-http-015.md_11.snap index a24ffcbc5d..c632b77700 100644 --- a/tests/core/snapshots/graphql-conformance-http-015.md_11.snap +++ b/tests/core/snapshots/graphql-conformance-http-015.md_11.snap @@ -12,7 +12,7 @@ expression: response "user": { "id": 4, "name": "User 4", - "spam": "FIZZ: [{\"bar\":\"BUZZ\"},{\"bar\":\"test\"}]" + "spam": "FIZZ: [{bar: \"BUZZ\"}, {bar: \"test\"}]" } } } diff --git a/tests/core/snapshots/graphql-conformance-http-015.md_8.snap b/tests/core/snapshots/graphql-conformance-http-015.md_8.snap index ada31c2959..9e59f910b9 100644 --- a/tests/core/snapshots/graphql-conformance-http-015.md_8.snap +++ b/tests/core/snapshots/graphql-conformance-http-015.md_8.snap @@ -12,7 +12,7 @@ expression: response "user": { "id": 4, "name": "User 4", - "searchComments": "video_4_[[\"test\",\"tost\"],[\"foo\"],[\"bar\"],[\"bizz\",\"buzz\"]]" + "searchComments": "video_4_[[\"test\", \"tost\"], [\"foo\"], [\"bar\"], [\"bizz\", \"buzz\"]]" } } } diff --git a/tests/core/snapshots/test-jq-template.md_merged.snap b/tests/core/snapshots/test-jq-template.md_merged.snap index f73644e97f..e7e8441797 100644 --- a/tests/core/snapshots/test-jq-template.md_merged.snap +++ b/tests/core/snapshots/test-jq-template.md_merged.snap @@ -13,7 +13,7 @@ type Buzz { type Fizz { bar: String! - buzz: Buzz! @expr(body: "{{.value.bar | split(\" \") | {first: .[0], second: .[1]}}}") + buzz: Buzz! @expr(body: "{{ .value.bar | split(\" \") | {first: .[0], second: .[1]} }}") } type Foo { diff --git a/tests/execution/http-select.md b/tests/execution/http-select.md index d36cbeb361..13a40a221a 100644 --- a/tests/execution/http-select.md +++ b/tests/execution/http-select.md @@ -10,7 +10,7 @@ type Query { userDetails(id: Int!): UserDetails @http( url: "http://upstream/users/{{.args.id}}" - select: {id: "{{.id}}", city: "{{.address.city}}", phone: "{{.phone}}"} + select: {id: "{{.args.id}}", city: "{{.args.address.city}}", phone: "{{.args.phone}}"} ) } diff --git a/tests/execution/test-jq-template.md b/tests/execution/test-jq-template.md index 2174b398ae..8e5415fd90 100644 --- a/tests/execution/test-jq-template.md +++ b/tests/execution/test-jq-template.md @@ -17,7 +17,7 @@ type Foo { type Fizz { bar: String! - buzz: Buzz! @expr(body: "{{.value.bar | split(\" \") | {first: .[0], second: .[1]}}}") + buzz: Buzz! @expr(body: "{{ .value.bar | split(\" \") | {first: .[0], second: .[1]} }}") } type Buzz { From 06b37b4d60a9309ae502ba2ac9d14e87a2cbec33 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Mon, 9 Dec 2024 16:02:36 +0200 Subject: [PATCH 16/28] fix: lint --- src/core/blueprint/dynamic_value.rs | 19 ++++++++++++++++++- src/core/mustache/jq_transform.rs | 10 +--------- tests/execution/http-select.md | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index 58e871e6f9..6ba54d43ab 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -39,7 +39,24 @@ impl DynamicValue { DynamicValue::Mustache(mustache) } } - DynamicValue::JqTemplate(_) => self, + DynamicValue::JqTemplate(jqt) => DynamicValue::JqTemplate(JqTemplate( + jqt.0 + .into_iter() + .map(|mut f| match &mut f { + crate::core::mustache::JqTemplateIR::JqTransform(_) => f, + crate::core::mustache::JqTemplateIR::Literal(_) => f, + crate::core::mustache::JqTemplateIR::Mustache(mustache) => { + let segments = mustache.segments_mut(); + if let Some(crate::core::mustache::Segment::Expression(vec)) = + segments.get_mut(0) + { + vec.insert(0, name.to_string()); + } + f + } + }) + .collect(), + )), DynamicValue::Object(index_map) => { let index_map = index_map .into_iter() diff --git a/src/core/mustache/jq_transform.rs b/src/core/mustache/jq_transform.rs index b946beb0b9..17a3b38d08 100644 --- a/src/core/mustache/jq_transform.rs +++ b/src/core/mustache/jq_transform.rs @@ -574,14 +574,7 @@ impl PartialEq for PathValueEnum<'_> { impl PartialOrd for PathValueEnum<'_> { fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (PathValueEnum::PathValue(_), PathValueEnum::PathValue(_)) => None, - (PathValueEnum::PathValue(_), PathValueEnum::Val(_)) => None, - (PathValueEnum::Val(_), PathValueEnum::PathValue(_)) => None, - (PathValueEnum::Val(self_val), PathValueEnum::Val(other_val)) => { - self_val.partial_cmp(other_val) - } - } + Some(self.cmp(other)) } } @@ -678,7 +671,6 @@ impl JqTransform { let mut write_lock = JQ_TEMPLATE_STORAGE.write().unwrap(); let template_id = write_lock.len(); - let filter = filter; write_lock.push(filter); Ok(Self { template_id, representation: format!("{:?}", term) }) diff --git a/tests/execution/http-select.md b/tests/execution/http-select.md index 13a40a221a..d36cbeb361 100644 --- a/tests/execution/http-select.md +++ b/tests/execution/http-select.md @@ -10,7 +10,7 @@ type Query { userDetails(id: Int!): UserDetails @http( url: "http://upstream/users/{{.args.id}}" - select: {id: "{{.args.id}}", city: "{{.args.address.city}}", phone: "{{.args.phone}}"} + select: {id: "{{.id}}", city: "{{.address.city}}", phone: "{{.phone}}"} ) } From dd7a9be6b923ec30d244a5ae58ad25ea3425fd2a Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Mon, 9 Dec 2024 16:59:34 +0200 Subject: [PATCH 17/28] fix: all tests --- src/core/mustache/jq_template.rs | 13 ++- src/core/serde_value_ext.rs | 134 ------------------------------- 2 files changed, 10 insertions(+), 137 deletions(-) diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index dab030c1ee..9ca6c16be1 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -94,14 +94,21 @@ fn parse_expression(input: &str) -> IResult<&str, JqTemplateIR> { JqTemplateError::JqIsMustache => { let expression: Vec<_> = template .trim() - .split('.') - .skip(1) + .trim_start_matches('.') + .split(".") .map(String::from) .collect(); let segment = Segment::Expression(expression); JqTemplateIR::Mustache(Mustache::from(vec![segment])) } - _ => JqTemplateIR::Literal(template.to_string()), + _ => { + let m = Mustache::parse(&format!("{{{{{}}}}}", template.trim())); + if !m.is_const() { + JqTemplateIR::Mustache(m) + } else { + JqTemplateIR::Literal(template.to_string()) + } + } }, } }), diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index a5fbd91de5..faa7b6ec85 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -163,138 +163,4 @@ mod tests { .unwrap(); assert_eq!(result, expected); } - - #[test] - fn test_jq_render_value() { - let value = json!({"a": ".value.foo"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"foo": {"bar": "baz"}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": {"bar": "baz"}})).unwrap(); - assert_eq!(result, expected); - } - - #[test] - fn test_jq_render_value_nested() { - let value = json!({"a": ".value.foo.bar.baz"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"foo": {"bar": {"baz": 1}}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": 1})).unwrap(); - assert_eq!(result, expected); - } - - #[test] - fn test_jq_render_value_nested_str() { - let value = json!({"a": ".value.foo.bar.baz"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"foo": {"bar": {"baz": "foo"}}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": "foo"})).unwrap(); - assert_eq!(result, expected); - } - - #[test] - fn test_jq_render_value_null() { - let value = json!(".value.foo.bar.baz"); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"foo": {"bar": {"baz": null}}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!(null)).unwrap(); - assert_eq!(result, expected); - } - - #[test] - fn test_jq_render_value_nested_bool() { - let value = json!({"a": ".value.foo.bar.baz"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"foo": {"bar": {"baz": true}}}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!({"a": true})).unwrap(); - assert_eq!(result, expected); - } - - #[test] - fn test_jq_render_value_nested_float() { - let value = json!({"a": ".value.foo.bar.baz"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"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, expected); - } - - #[test] - fn test_jq_render_value_arr() { - let value = json!({"a": ".value.foo.bar.baz"}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"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, expected); - } - - #[test] - fn test_jq_render_value_arr_template() { - let value = json!({"a": [".value.foo.bar.baz", ".value.foo.bar.qux"]}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"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, expected); - } - - #[test] - fn test_jq_or_value_is_const() { - let value = json!(".value.foo"); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"foo": "bar"}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::String("bar".to_owned()); - assert_eq!(result, expected); - } - - #[test] - fn test_jq_arr_obj() { - let value = json!({"a": [".value.foo.bar.baz", ".value.foo.bar.qux"]}); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"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, expected); - } - - #[test] - fn test_jq_arr_obj_arr() { - let value = - json!([{"a": [{"aa": ".value.foo.bar.baz"}]}, {"a": [{"aa": ".value.foo.bar.qux"}]}]); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"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, expected); - } - - #[test] - fn test_jq_create_obj() { - let value = json!(".value.foo | split(\" \") | {fizz: .[0], buzz: .[1]}"); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"foo": "hello world"}}); - let result = value.render_value(&ctx); - let expected = - async_graphql::Value::from_json(json!({"fizz": "hello", "buzz": "world"})).unwrap(); - assert_eq!(result, expected); - } - - #[test] - fn test_jq_create_arr() { - let value = json!(".value.foo | split(\" \")"); - let value = DynamicValue::try_from(&value).unwrap(); - let ctx = json!({"value": {"foo": "hello world"}}); - let result = value.render_value(&ctx); - let expected = async_graphql::Value::from_json(json!(["hello", "world"])).unwrap(); - assert_eq!(result, expected); - } } From 74ee6896ce41c9fef23e379a0451b5deb350c65d Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Tue, 10 Dec 2024 13:36:29 +0200 Subject: [PATCH 18/28] fix: error handling --- src/core/blueprint/dynamic_value.rs | 45 +- src/core/ir/error.rs | 6 +- src/core/ir/eval.rs | 2 +- src/core/mustache/jq_template.rs | 64 ++- src/core/mustache/jq_transform.rs | 800 +++++----------------------- src/core/mustache/jq_value.rs | 624 ++++++++++++++++++++++ src/core/mustache/mod.rs | 2 + src/core/serde_value_ext.rs | 54 +- tests/expression_spec.rs | 40 +- 9 files changed, 864 insertions(+), 773 deletions(-) create mode 100644 src/core/mustache/jq_value.rs diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index 6ba54d43ab..7e591f066a 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -2,12 +2,12 @@ use async_graphql_value::{ConstValue, Name}; use indexmap::IndexMap; use serde_json::Value; -use crate::core::mustache::{JqTemplate, Mustache}; +use crate::core::mustache::JqTemplate; #[derive(Debug, Clone, PartialEq)] +/// This is used to express dynamic value resolver engine. pub enum DynamicValue { Value(A), - Mustache(Mustache), JqTemplate(JqTemplate), Object(IndexMap>), Array(Vec>), @@ -26,25 +26,13 @@ impl DynamicValue { pub fn prepend(self, name: &str) -> Self { match self { DynamicValue::Value(value) => DynamicValue::Value(value), - DynamicValue::Mustache(mut mustache) => { - if mustache.is_const() { - DynamicValue::Mustache(mustache) - } else { - let segments = mustache.segments_mut(); - if let Some(crate::core::mustache::Segment::Expression(vec)) = - segments.get_mut(0) - { - vec.insert(0, name.to_string()); - } - DynamicValue::Mustache(mustache) - } - } DynamicValue::JqTemplate(jqt) => DynamicValue::JqTemplate(JqTemplate( jqt.0 .into_iter() .map(|mut f| match &mut f { crate::core::mustache::JqTemplateIR::JqTransform(_) => f, crate::core::mustache::JqTemplateIR::Literal(_) => f, + // this function can prepend a custom prefix to mustache only crate::core::mustache::JqTemplateIR::Mustache(mustache) => { let segments = mustache.segments_mut(); if let Some(crate::core::mustache::Segment::Expression(vec)) = @@ -78,9 +66,6 @@ impl TryFrom<&DynamicValue> for ConstValue { fn try_from(value: &DynamicValue) -> Result { match value { DynamicValue::Value(v) => Ok(v.to_owned()), - DynamicValue::Mustache(_) => Err(anyhow::anyhow!( - "mustache cannot be converted to const value" - )), DynamicValue::JqTemplate(_) => Err(anyhow::anyhow!( "jq template cannot be converted to const value" )), @@ -104,7 +89,6 @@ impl DynamicValue { // Helper method to determine if the value is constant (non-mustache). pub fn is_const(&self) -> bool { match self { - DynamicValue::Mustache(m) => m.is_const(), DynamicValue::JqTemplate(t) => t.is_const(), DynamicValue::Object(obj) => obj.values().all(|v| v.is_const()), DynamicValue::Array(arr) => arr.iter().all(|v| v.is_const()), @@ -116,6 +100,7 @@ impl DynamicValue { impl TryFrom<&Value> for DynamicValue { type Error = anyhow::Error; + /// Used to convert json notation to dynamic value fn try_from(value: &Value) -> Result { match value { Value::Object(obj) => { @@ -152,31 +137,33 @@ mod test { #[test] fn test_dynamic_value_inject() { let value: DynamicValue = - DynamicValue::Mustache(Mustache::parse("{{.foo}}")).prepend("args"); + DynamicValue::JqTemplate(JqTemplate::parse("{{.foo}}")).prepend("args"); let expected: DynamicValue = - DynamicValue::Mustache(Mustache::parse("{{.args.foo}}")); + DynamicValue::JqTemplate(JqTemplate::parse("{{.args.foo}}")); assert_eq!(value, expected); let mut value_map = IndexMap::new(); value_map.insert( Name::new("foo"), - DynamicValue::Mustache(Mustache::parse("{{.foo}}")), + DynamicValue::JqTemplate(JqTemplate::parse("{{.foo}}")), ); let value: DynamicValue = DynamicValue::Object(value_map).prepend("args"); let mut expected_map = IndexMap::new(); expected_map.insert( Name::new("foo"), - DynamicValue::Mustache(Mustache::parse("{{.args.foo}}")), + DynamicValue::JqTemplate(JqTemplate::parse("{{.args.foo}}")), ); let expected: DynamicValue = DynamicValue::Object(expected_map); assert_eq!(value, expected); - let value: DynamicValue = - DynamicValue::Array(vec![DynamicValue::Mustache(Mustache::parse("{{.foo}}"))]) - .prepend("args"); - let expected: DynamicValue = DynamicValue::Array(vec![DynamicValue::Mustache( - Mustache::parse("{{.args.foo}}"), - )]); + let value: DynamicValue = DynamicValue::Array(vec![DynamicValue::JqTemplate( + JqTemplate::parse("{{.foo}}"), + )]) + .prepend("args"); + let expected: DynamicValue = + DynamicValue::Array(vec![DynamicValue::JqTemplate(JqTemplate::parse( + "{{.args.foo}}", + ))]); assert_eq!(value, expected); let value: DynamicValue = DynamicValue::Value(ConstValue::Null).prepend("args"); diff --git a/src/core/ir/error.rs b/src/core/ir/error.rs index ac646429de..681f7333b9 100644 --- a/src/core/ir/error.rs +++ b/src/core/ir/error.rs @@ -6,6 +6,7 @@ use derive_more::From; use thiserror::Error; use crate::core::jit::graphql_error::{Error as ExtensionError, ErrorExtensions}; +use crate::core::mustache::JqRuntimeError; use crate::core::{auth, cache, worker, Errata}; #[derive(From, Debug, Error, Clone)] @@ -33,6 +34,8 @@ pub enum Error { Cache(cache::Error), + DynamicValue(JqRuntimeError), + #[from(ignore)] Entity(String), } @@ -67,7 +70,8 @@ impl From for Errata { } Error::Worker(err) => Errata::new("Worker Error").description(err.to_string()), Error::Cache(err) => Errata::new("Cache Error").description(err.to_string()), - Error::Entity(message) => Errata::new("Entity Resolver Error").description(message) + Error::Entity(message) => Errata::new("Entity Resolver Error").description(message), + Error::DynamicValue(err) => Errata::new("Dynamic Value Rendering Error").description(err.to_string()) } } } diff --git a/src/core/ir/eval.rs b/src/core/ir/eval.rs index e7b0a8c179..ab002ddabe 100644 --- a/src/core/ir/eval.rs +++ b/src/core/ir/eval.rs @@ -36,7 +36,7 @@ impl IR { .unwrap_or(&async_graphql::Value::Null) .clone()) } - IR::Dynamic(value) => Ok(value.render_value(ctx)), + IR::Dynamic(value) => Ok(value.render_value(ctx)?), IR::Protect(auth, expr) => { let verifier = AuthVerifier::from(auth.clone()); verifier.verify(ctx.request_ctx).await.to_result()?; diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs index 9ca6c16be1..f80731e8f5 100644 --- a/src/core/mustache/jq_template.rs +++ b/src/core/mustache/jq_template.rs @@ -7,20 +7,25 @@ use nom::multi::many0; use nom::sequence::delimited; use nom::{Finish, IResult}; -use super::{JqTransform, Mustache, PathJqValueString}; -use crate::core::mustache::{JqTemplateError, Segment}; +use super::{JqRuntimeError, JqTransform, Mustache, PathJqValueString}; +use crate::core::mustache::Segment; #[derive(Debug, Clone, PartialEq, Hash)] +/// Used to represent a mixture of getters mustache, jq transformations and +/// const values templates +pub struct JqTemplate(pub Vec); + +#[derive(Debug, Clone, PartialEq, Hash)] +/// The IR for each part of the template pub enum JqTemplateIR { JqTransform(JqTransform), Literal(String), Mustache(Mustache), } -#[derive(Debug, Clone, PartialEq, Hash)] -pub struct JqTemplate(pub Vec); - impl JqTemplate { + /// Used to check if the returned expression resolves to a constant value + /// always pub fn is_const(&self) -> bool { self.0.iter().all(|v| match v { JqTemplateIR::JqTransform(jq) => jq.is_const(), @@ -29,20 +34,42 @@ impl JqTemplate { }) } - // TODO: return error - pub fn render_value(&self, ctx: &impl PathJqValueString) -> async_graphql_value::ConstValue { + /// Used to render the template + pub fn render_value( + &self, + ctx: &impl PathJqValueString, + ) -> Result { let expressions_len = self.0.len(); match expressions_len { - 0 => async_graphql_value::ConstValue::Null, + 0 => Ok(async_graphql_value::ConstValue::Null), 1 => { let expression = self.0.first().unwrap(); self.execute_expression(ctx, expression) } _ => { - let result = self + let (errors, result): (Vec<_>, Vec<_>) = self .0 .iter() .map(|expr| self.execute_expression(ctx, expr)) + .partition(Result::is_err); + + let errors: Vec = errors + .into_iter() + .filter_map(|e| match e { + Ok(_) => None, + Err(err) => Some(err), + }) + .collect(); + if !errors.is_empty() { + return Err(JqRuntimeError::JqRuntimeErrors(errors)); + } + + let result = result + .into_iter() + .filter_map(|v| match v { + Ok(v) => Some(v), + Err(_) => None, + }) .fold(String::new(), |mut acc, cur| { match &cur { async_graphql::Value::String(s) => acc += s, @@ -50,7 +77,7 @@ impl JqTemplate { } acc }); - async_graphql_value::ConstValue::String(result) + Ok(async_graphql_value::ConstValue::String(result)) } } } @@ -59,17 +86,23 @@ impl JqTemplate { &self, ctx: &impl PathJqValueString, expression: &JqTemplateIR, - ) -> async_graphql_value::ConstValue { + ) -> Result { match expression { JqTemplateIR::JqTransform(jq_transform) => { jq_transform.render_value(super::PathValueEnum::PathValue(Arc::new(ctx))) } - JqTemplateIR::Literal(value) => async_graphql_value::ConstValue::String(value.clone()), + JqTemplateIR::Literal(value) => { + Ok(async_graphql_value::ConstValue::String(value.clone())) + } JqTemplateIR::Mustache(mustache) => { let mustache_result = mustache.render(ctx); - serde_json::from_str::(&mustache_result) - .unwrap_or_else(|_| async_graphql_value::ConstValue::String(mustache_result)) + Ok( + serde_json::from_str::(&mustache_result) + .unwrap_or_else(|_| { + async_graphql_value::ConstValue::String(mustache_result) + }), + ) } } } @@ -87,11 +120,10 @@ fn parse_expression(input: &str) -> IResult<&str, JqTemplateIR> { delimited( tag("{{"), map(take_until("}}"), |template| { - // TODO: use the error match JqTransform::try_new(template) { Ok(jq) => JqTemplateIR::JqTransform(jq), Err(err) => match err { - JqTemplateError::JqIsMustache => { + JqRuntimeError::JqIsMustache => { let expression: Vec<_> = template .trim() .trim_start_matches('.') diff --git a/src/core/mustache/jq_transform.rs b/src/core/mustache/jq_transform.rs index 17a3b38d08..0ae65b3726 100644 --- a/src/core/mustache/jq_transform.rs +++ b/src/core/mustache/jq_transform.rs @@ -1,18 +1,17 @@ use std::fmt::Display; -use std::sync::{Arc, RwLock}; +use std::sync::RwLock; use jaq_core::load::parse::Term; use jaq_core::load::{Arena, File, Loader}; -use jaq_core::val::Range; -use jaq_core::{Compiler, Ctx, Error, Exn, Filter, Native, RcIter, ValR}; -use jaq_json::Val; +use jaq_core::{Compiler, Ctx, Filter, Native, RcIter, ValR}; use lazy_static::lazy_static; -use crate::core::ir::{EvalContext, ResolverContextLike}; use crate::core::json::JsonLike; -use crate::core::path::{PathString, PathValue, ValueString}; + +use super::PathValueEnum; lazy_static! { + /// Used to store the compiled JQ templates static ref JQ_TEMPLATE_STORAGE: RwLock>>>> = RwLock::new(Vec::new()); } @@ -20,628 +19,29 @@ lazy_static! { /// Used to represent a JQ template. Currently used only on @expr directive. #[derive(Clone)] pub struct JqTransform { - /// The compiled template transformation + /// The compiled template index template_id: usize, /// The IR representation, used for debug purposes representation: String, } -#[derive(Clone)] -pub enum PathValueEnum<'a> { - PathValue(Arc<&'a dyn PathJqValue>), - Val(Val), -} - -pub trait PathJqValue { - fn get_value<'a>(&'a self, index: &Val) -> Option>; -} - -impl PathJqValue for EvalContext<'_, Ctx> { - fn get_value<'a>(&'a self, index: &Val) -> Option> { - let Val::Str(index) = index else { return None }; - self.raw_value(&[index.as_str()]) - } -} - -impl PathJqValue for serde_json::Value { - fn get_value(&self, index: &Val) -> Option> { - match self { - serde_json::Value::Object(map) => { - let Val::Str(index) = index else { return None }; - map.get(index.as_str()).map(|v| { - ValueString::Value(std::borrow::Cow::Owned( - async_graphql_value::ConstValue::from_json(v.clone()).unwrap(), - )) - }) - } - serde_json::Value::Array(list) => { - let Val::Int(index) = index else { return None }; - list.get(*index as usize).map(|v| { - ValueString::Value(std::borrow::Cow::Owned( - async_graphql_value::ConstValue::from_json(v.clone()).unwrap(), - )) - }) - } - _ => None, - } - } -} -pub trait PathJqValueString: PathString + PathJqValue {} - -impl PathJqValueString for EvalContext<'_, Ctx> {} - -impl PathJqValueString for serde_json::Value {} - -impl jaq_std::ValT for PathValueEnum<'_> { - fn into_seq>(self) -> Result { - todo!() - } - - fn as_isize(&self) -> Option { - match self { - PathValueEnum::PathValue(_) => None, - PathValueEnum::Val(val) => val.as_isize(), - } - } - - fn as_f64(&self) -> Result> { - match self { - PathValueEnum::PathValue(_) => Err(Error::new(Self::Val(Val::from( - "Cannot convert context to f64".to_string(), - )))), - PathValueEnum::Val(val) => match val.as_f64() { - Ok(val) => Ok(val), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - }, - } - } -} - -impl jaq_core::ValT for PathValueEnum<'_> { - fn from_num(n: &str) -> ValR { - match Val::from_num(n) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - } - } - - fn from_map>(iter: I) -> ValR { - let result: Result, String> = iter - .into_iter() - .map(|(k, v)| match (k, v) { - (PathValueEnum::Val(key), PathValueEnum::Val(value)) => Ok((key, value)), - _ => Err("Invalid key or value type for map".into()), - }) - .collect(); - - match result { - Ok(pairs) => match Val::from_map(pairs) { - Ok(val) => ValR::Ok(PathValueEnum::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - }, - Err(e) => Err(Error::new(Self::Val(Val::from(e)))), - } - } - - fn values(self) -> Box>> { - match self { - PathValueEnum::PathValue(_) => panic!("Cannot iterate context"), - PathValueEnum::Val(val) => Box::new(val.values().map(|v| { - v.map(PathValueEnum::Val).map_err(|err| { - let val = err.into_val(); - Error::new(PathValueEnum::Val(val)) - }) - })), - } - } - - fn index(self, index: &Self) -> ValR { - let PathValueEnum::Val(index) = index else { - return ValR::Err(Error::new(Self::Val(Val::from(format!( - "Could not convert index `{}` val.", - index - ))))); - }; - - match self { - PathValueEnum::PathValue(pv) => { - let Some(v) = pv.get_value(index) else { - return ValR::Err(Error::new(Self::Val(Val::from(format!( - "Could not find key `{}` in context.", - index - ))))); - }; - - match v { - crate::core::path::ValueString::Value(cow) => { - let cv = cow.as_ref().clone(); - match cv.into_json() { - Ok(js) => Ok(Self::Val(Val::from(js))), - Err(err) => ValR::Err(Error::new(Self::Val(Val::from(format!( - "Could not convert value to json: {:?}", - err - ))))), - } - } - crate::core::path::ValueString::String(cow) => { - let v = cow.to_string(); - Ok(Self::Val(Val::from(v))) - } - } - } - PathValueEnum::Val(val) => match val.index(index) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - }, - } - } - - fn range(self, range: jaq_core::val::Range<&Self>) -> ValR { - let (start, end) = ( - range - .start - .map(|v| match v { - PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( - "Could not convert range start to val.".to_string(), - ))), - PathValueEnum::Val(val) => Ok(val.clone()), - }) - .transpose(), - range - .end - .map(|v| match v { - PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( - "Could not convert range end to val.".to_string(), - ))), - PathValueEnum::Val(val) => Ok(val.clone()), - }) - .transpose(), - ); - - let (start, end) = match (start, end) { - (Ok(start), Ok(end)) => (start, end), - (Ok(_), Err(err)) => { - let val = err.into_val(); - return Err(Error::new(Self::Val(val))); - } - (Err(err), Ok(_)) => { - let val = err.into_val(); - return Err(Error::new(Self::Val(val))); - } - (Err(_), Err(_)) => { - return ValR::Err(Error::new(Self::Val(Val::from( - "Could not convert range to val.".to_string(), - )))) - } - }; - - let range = Range { start: start.as_ref(), end: end.as_ref() }; - - match self { - PathValueEnum::PathValue(_) => ValR::Err(Error::new(Self::Val(Val::from( - "Cannot apply range operation at the context".to_string(), - )))), - PathValueEnum::Val(val) => match val.range(range) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - }, - } - } - - fn map_values<'a, I: Iterator>>( - self, - opt: jaq_core::path::Opt, - f: impl Fn(Self) -> I, - ) -> jaq_core::ValX<'a, Self> { - let f_new = move |x: Val| -> _ { - let iter = f(Self::Val(x)); - iter.map(|v| match v { - Ok(enum_val) => match enum_val { - PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( - Val::from("Cannot convert context to val.".to_string()), - ))), - PathValueEnum::Val(val) => Ok(val), - }, - Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( - "Function execution failed with: {:?}", - err - ))))), - }) - }; - - match self { - PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( - Val::from("Cannot apply map_values operation at the context".to_string()), - )))), - PathValueEnum::Val(val) => match val.map_values(opt, f_new) { - Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), - Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( - format!("The map_values failed because: {:?}", err), - ))))), - }, - } - } - - fn map_index<'a, I: Iterator>>( - self, - index: &Self, - opt: jaq_core::path::Opt, - f: impl Fn(Self) -> I, - ) -> jaq_core::ValX<'a, Self> { - let PathValueEnum::Val(index) = index else { - return jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from(format!( - "Could not convert index `{}` val.", - index - )))))); - }; - - let f_new = move |x: Val| -> _ { - let iter = f(Self::Val(x)); - iter.map(|v| match v { - Ok(enum_val) => match enum_val { - PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( - Val::from("Cannot convert context to val.".to_string()), - ))), - PathValueEnum::Val(val) => Ok(val), - }, - Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( - "Function execution failed with: {:?}", - err - ))))), - }) - }; - - match self { - PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( - Val::from("Cannot apply map_index operation at the context".to_string()), - )))), - PathValueEnum::Val(val) => match val.map_index(index, opt, f_new) { - Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), - Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( - format!("The map_index failed because: {:?}", err), - ))))), - }, - } - } - - fn map_range<'a, I: Iterator>>( - self, - range: jaq_core::val::Range<&Self>, - opt: jaq_core::path::Opt, - f: impl Fn(Self) -> I, - ) -> jaq_core::ValX<'a, Self> { - let (start, end) = ( - range - .start - .map(|v| match v { - PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( - "Could not convert range start to val.".to_string(), - ))), - PathValueEnum::Val(val) => Ok(val.clone()), - }) - .transpose(), - range - .end - .map(|v| match v { - PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( - "Could not convert range end to val.".to_string(), - ))), - PathValueEnum::Val(val) => Ok(val.clone()), - }) - .transpose(), - ); - - let (start, end) = match (start, end) { - (Ok(start), Ok(end)) => (start, end), - (Ok(_), Err(err)) => { - let val = err.into_val(); - return Err(Exn::from(Error::new(Self::Val(val)))); - } - (Err(err), Ok(_)) => { - let val = err.into_val(); - return Err(Exn::from(Error::new(Self::Val(val)))); - } - (Err(_), Err(_)) => { - return Err(Exn::from(Error::new(Self::Val(Val::from( - "Could not convert range to val.".to_string(), - ))))) - } - }; - - let range = Range { start: start.as_ref(), end: end.as_ref() }; - - let f_new = move |x: Val| -> _ { - let iter = f(Self::Val(x)); - iter.map(|v| match v { - Ok(enum_val) => match enum_val { - PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( - Val::from("Cannot convert context to val.".to_string()), - ))), - PathValueEnum::Val(val) => Ok(val), - }, - Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( - "Function execution failed with: {:?}", - err - ))))), - }) - }; - - match self { - PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( - Val::from("Cannot apply map_range operation at the context".to_string()), - )))), - PathValueEnum::Val(val) => match val.map_range(range, opt, f_new) { - Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), - Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( - format!("The map_range failed because: {:?}", err), - ))))), - }, - } - } - - fn as_bool(&self) -> bool { - match self { - PathValueEnum::PathValue(_) => true, - PathValueEnum::Val(val) => val.as_bool(), - } - } - - fn as_str(&self) -> Option<&str> { - match self { - PathValueEnum::PathValue(_) => Some("[Context]"), - PathValueEnum::Val(val) => val.as_str(), - } - } -} - -impl<'a> FromIterator> for PathValueEnum<'a> { - fn from_iter>>(iter: I) -> Self { - let iter = iter.into_iter().filter_map(|v| match v { - PathValueEnum::PathValue(_) => None, - PathValueEnum::Val(val) => Some(val), - }); - Self::Val(Val::from_iter(iter)) - } -} - -impl std::ops::Add for PathValueEnum<'_> { - type Output = ValR; - - fn add(self, rhs: Self) -> Self::Output { - match (self, rhs) { - (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { - match self_val.add(rhs_val) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - } - } - _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( - "Cannot perform add operation with context.".to_string(), - )))), - } - } -} - -impl std::ops::Sub for PathValueEnum<'_> { - type Output = ValR; - - fn sub(self, rhs: Self) -> Self::Output { - match (self, rhs) { - (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { - match self_val.sub(rhs_val) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - } - } - _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( - "Cannot perform sub operation with context.".to_string(), - )))), - } - } -} - -impl std::ops::Mul for PathValueEnum<'_> { - type Output = ValR; - - fn mul(self, rhs: Self) -> Self::Output { - match (self, rhs) { - (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { - match self_val.mul(rhs_val) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - } - } - _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( - "Cannot perform mul operation with context.".to_string(), - )))), - } - } -} - -impl std::ops::Div for PathValueEnum<'_> { - type Output = ValR; - - fn div(self, rhs: Self) -> Self::Output { - match (self, rhs) { - (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { - match self_val.div(rhs_val) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - } - } - _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( - "Cannot perform div operation with context.".to_string(), - )))), - } - } -} - -impl std::ops::Rem for PathValueEnum<'_> { - type Output = ValR; - - fn rem(self, rhs: Self) -> Self::Output { - match (self, rhs) { - (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { - match self_val.rem(rhs_val) { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - } - } - _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( - "Cannot perform rem operation with context.".to_string(), - )))), - } - } -} - -impl std::ops::Neg for PathValueEnum<'_> { - type Output = ValR; - - fn neg(self) -> Self::Output { - match self { - PathValueEnum::Val(self_val) => match self_val.neg() { - Ok(val) => ValR::Ok(Self::Val(val)), - Err(err) => { - let val = err.into_val(); - Err(Error::new(Self::Val(val))) - } - }, - _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( - "Cannot perform neg operation at context.".to_string(), - )))), - } - } -} - -impl Display for PathValueEnum<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PathValueEnum::PathValue(_) => "[Context]".to_string().fmt(f), - PathValueEnum::Val(val) => val.fmt(f), - } - } -} - -impl std::fmt::Debug for PathValueEnum<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PathValueEnum::PathValue(_) => derive_more::Debug::fmt(&"[Context]".to_string(), f), - PathValueEnum::Val(val) => derive_more::Debug::fmt(&val, f), - } - } -} - -impl PartialEq for PathValueEnum<'_> { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (PathValueEnum::PathValue(_), PathValueEnum::PathValue(_)) => true, - (PathValueEnum::PathValue(_), PathValueEnum::Val(_)) => false, - (PathValueEnum::Val(_), PathValueEnum::PathValue(_)) => false, - (PathValueEnum::Val(self_val), PathValueEnum::Val(other_val)) => self_val.eq(other_val), - } - } -} - -impl PartialOrd for PathValueEnum<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Eq for PathValueEnum<'_> {} - -impl Ord for PathValueEnum<'_> { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match (self, other) { - (PathValueEnum::PathValue(_), PathValueEnum::PathValue(_)) => std::cmp::Ordering::Equal, - (PathValueEnum::PathValue(_), PathValueEnum::Val(_)) => std::cmp::Ordering::Greater, - (PathValueEnum::Val(_), PathValueEnum::PathValue(_)) => std::cmp::Ordering::Less, - (PathValueEnum::Val(self_val), PathValueEnum::Val(other_val)) => { - self_val.cmp(other_val) - } - } - } -} - -impl From for PathValueEnum<'_> { - fn from(value: f64) -> Self { - Self::Val(Val::from(value)) - } -} - -impl From> for serde_json::Value { - fn from(value: PathValueEnum<'_>) -> Self { - match value { - PathValueEnum::PathValue(_) => serde_json::Value::String("[Context]".to_string()), - PathValueEnum::Val(val) => serde_json::Value::from(val), - } - } -} - -impl From for PathValueEnum<'_> { - fn from(value: String) -> Self { - Self::Val(Val::from(value)) - } -} - -impl From for PathValueEnum<'_> { - fn from(value: isize) -> Self { - Self::Val(Val::from(value)) - } -} - -impl From for PathValueEnum<'_> { - fn from(value: bool) -> Self { - Self::Val(Val::from(value)) - } -} - impl JqTransform { /// Used to parse a `template` and try to convert it into a JqTemplate - pub fn try_new(template: &str) -> Result { + pub fn try_new(template: &str) -> Result { // the term is used because it can be easily serialized, deserialized and hashed - let term = Self::parse_template(template); + let term = + Self::parse_template(template).map_err(|err| JqRuntimeError::JqTemplateErrors(err))?; // calculate if the expression can be replaced with mustache let is_mustache = Self::recursive_is_mustache(&term); if is_mustache { - return Err(JqTemplateError::JqIsMustache); + return Err(JqRuntimeError::JqIsMustache); } // calculate if the expression returns always a constant value let is_const = Self::calculate_is_const(&term); if is_const { - return Err(JqTemplateError::JqIstConst); + return Err(JqRuntimeError::JqIstConst); } // the template is used to be parsed in to the IR AST @@ -655,7 +55,11 @@ impl JqTransform { let arena = Arena::default(); // load the modules let modules = loader.load(&arena, template).map_err(|errs| { - JqTemplateError::JqLoadError(errs.into_iter().map(|e| format!("{:?}", e.1)).collect()) + JqRuntimeError::JqTemplateErrors( + errs.into_iter() + .map(|err| JqTemplateError::JqLoadError(format!("{:?}", err))) + .collect::>(), + ) })?; // the AST of the operation, used to transform the data @@ -663,13 +67,15 @@ impl JqTransform { .with_funs(jaq_std::funs()) .compile(modules) .map_err(|errs| { - JqTemplateError::JqCompileError( - errs.into_iter().map(|e| format!("{:?}", e.1)).collect(), + JqRuntimeError::JqTemplateErrors( + errs.into_iter() + .map(|err| JqTemplateError::JqCompileError(format!("{:?}", err))) + .collect::>(), ) })?; + // store the compiled template let mut write_lock = JQ_TEMPLATE_STORAGE.write().unwrap(); - let template_id = write_lock.len(); write_lock.push(filter); @@ -690,45 +96,66 @@ impl JqTransform { } /// Used to calculate the result and return it as json - pub fn render_value(&self, value: PathValueEnum<'_>) -> async_graphql_value::ConstValue { - let res = self.run(value); - let res: Vec = res + pub fn render_value( + &self, + value: PathValueEnum<'_>, + ) -> Result { + let (errors, result): (Vec<_>, Vec<_>) = self + .run(value) .into_iter() - // TODO: handle error correct, now we ignore it - .filter_map(|v| match v { - Ok(v) => Some(v), - Err(err) => { - println!("ERR: {:?}", err); - None - } + .map(|v| match v { + Ok(v) => Ok(std::convert::Into::into(v)), + Err(err) => Err(err), }) - .map(std::convert::Into::into) - .map(async_graphql_value::ConstValue::from_json) - // TODO: handle error correct, now we ignore it + .map(|v| match v { + Ok(v) => async_graphql_value::ConstValue::from_json(v) + .map_err(|err| JqTemplateError::JsonParseError(err.to_string())), + Err(err) => Err(JqTemplateError::JqRuntimeError(err.to_string())), + }) + .partition(Result::is_err); + + let errors: Vec = errors + .into_iter() + .filter_map(|e| match e { + Ok(_) => None, + Err(err) => Some(err), + }) + .collect(); + if !errors.is_empty() { + return Err(JqRuntimeError::JqTemplateErrors(errors)); + } + + let result: Vec<_> = result + .into_iter() .filter_map(|v| match v { Ok(v) => Some(v), - Err(err) => { - println!("ERR: {:?}", err); - None - } + Err(_) => None, }) .collect(); - let res_len = res.len(); + + // convert results to graphql value + let res_len = result.len(); if res_len == 0 { - async_graphql_value::ConstValue::Null + Ok(async_graphql_value::ConstValue::Null) } else if res_len == 1 { - res.into_iter().next().unwrap() + Ok(result.into_iter().next().unwrap()) } else { - async_graphql_value::ConstValue::array(res) + Ok(async_graphql_value::ConstValue::array(result)) } } /// Used to parse the template string and return the IR representation - fn parse_template(template: &str) -> Term<&str> { + fn parse_template(template: &str) -> Result, Vec> { let lexer = jaq_core::load::Lexer::new(template); - let lex = lexer.lex().unwrap_or_default(); + let lex = lexer.lex().map_err(|err| { + err.into_iter() + .map(|err| JqTemplateError::JqLexError(format!("{:?}", err))) + .collect::>() + })?; let mut parser = jaq_core::load::parse::Parser::new(&lex); - parser.term().unwrap_or_default() + Ok(parser + .term() + .map_err(|err| vec![JqTemplateError::JqParseError(format!("{:?}", err))])?) } /// Used as a helper function to determine if the term can be supported with @@ -864,17 +291,31 @@ impl std::hash::Hash for JqTransform { } } -#[derive(Debug, thiserror::Error)] +#[derive(Clone, Debug, thiserror::Error)] pub enum JqTemplateError { - #[error("{0}")] - Reason(String), - #[error("JQ Load Errors: {0:?}")] - JqLoadError(Vec), - #[error("JQ Compile Errors: {0:?}")] - JqCompileError(Vec), - #[error("JQ Transform can be replaced with a Mustache")] + #[error("[JQ Load Errors] {0:?}\n")] + JqLoadError(String), + #[error("[JQ Compile Error] {0:?}\n")] + JqCompileError(String), + #[error("[JQ Parse Error] {0:?}\n")] + JqParseError(String), + #[error("[JQ Lex Error] {0:?}\n")] + JqLexError(String), + #[error("[JQ Runtime Error] {0:?}\n")] + JqRuntimeError(String), + #[error("[JQ Json Parse Error] {0:?}\n")] + JsonParseError(String), +} + +#[derive(Clone, Debug, thiserror::Error)] +pub enum JqRuntimeError { + #[error("{0:?}\n")] + JqTemplateErrors(Vec), + #[error("{0:?}\n")] + JqRuntimeErrors(Vec), + #[error("JQ Transform can be replaced with a Mustache.\n")] JqIsMustache, - #[error("JQ Transform can be replaced with a Literal")] + #[error("JQ Transform can be replaced with a Literal.\n")] JqIstConst, } @@ -882,13 +323,14 @@ pub enum JqTemplateError { mod tests { use std::hash::{DefaultHasher, Hash, Hasher}; + use jaq_json::Val; use serde_json::json; use super::*; #[test] fn test_is_mustache_simple_property() { - let term = JqTransform::parse_template(".fruit"); + let term = JqTransform::parse_template(".fruit").unwrap(); assert!( JqTransform::recursive_is_mustache(&term), "Should return true for simple property access" @@ -897,7 +339,7 @@ mod tests { #[test] fn test_is_mustache_nested_property() { - let term = JqTransform::parse_template(".fruit.name"); + let term = JqTransform::parse_template(".fruit.name").unwrap(); assert!( JqTransform::recursive_is_mustache(&term), "Should return true for nested property access" @@ -906,7 +348,7 @@ mod tests { #[test] fn test_is_mustache_optional() { - let term = JqTransform::parse_template(".fruit.name?"); + let term = JqTransform::parse_template(".fruit.name?").unwrap(); assert!( !JqTransform::recursive_is_mustache(&term), "Should return false for optional operator" @@ -915,7 +357,7 @@ mod tests { #[test] fn test_is_mustache_array_index() { - let term = JqTransform::parse_template(".fruits[1]"); + let term = JqTransform::parse_template(".fruits[1]").unwrap(); assert!( !JqTransform::recursive_is_mustache(&term), "Should return false for array index access" @@ -924,7 +366,7 @@ mod tests { #[test] fn test_is_mustache_pipe_operator() { - let term = JqTransform::parse_template(".fruits[] | .name"); + let term = JqTransform::parse_template(".fruits[] | .name").unwrap(); assert!( !JqTransform::recursive_is_mustache(&term), "Should return false for pipe operator usage" @@ -933,7 +375,7 @@ mod tests { #[test] fn test_is_mustache_filter() { - let term = JqTransform::parse_template(".fruits[] | select(.price > 1)"); + let term = JqTransform::parse_template(".fruits[] | select(.price > 1)").unwrap(); assert!( !JqTransform::recursive_is_mustache(&term), "Should return false for select filter usage" @@ -942,7 +384,7 @@ mod tests { #[test] fn test_is_mustache_true_value() { - let term = JqTransform::parse_template("true"); + let term = JqTransform::parse_template("true").unwrap(); assert!( JqTransform::recursive_is_mustache(&term), "Should return true for const true value" @@ -951,7 +393,7 @@ mod tests { #[test] fn test_is_mustache_false_value() { - let term = JqTransform::parse_template("false"); + let term = JqTransform::parse_template("false").unwrap(); assert!( JqTransform::recursive_is_mustache(&term), "Should return true for const false value" @@ -960,7 +402,7 @@ mod tests { #[test] fn test_is_mustache_number_value() { - let term = JqTransform::parse_template("1"); + let term = JqTransform::parse_template("1").unwrap(); assert!( JqTransform::recursive_is_mustache(&term), "Should return true for number value" @@ -969,7 +411,7 @@ mod tests { #[test] fn test_is_mustache_str_value() { - let term = JqTransform::parse_template("\"foobar\""); + let term = JqTransform::parse_template("\"foobar\"").unwrap(); assert!( JqTransform::recursive_is_mustache(&term), "Should return true for string value" @@ -978,7 +420,7 @@ mod tests { #[test] fn test_is_mustache_str_interpolate_value() { - let term = JqTransform::parse_template("\"Hello, \\(.name)!\""); + let term = JqTransform::parse_template("\"Hello, \\(.name)!\"").unwrap(); assert!( !JqTransform::recursive_is_mustache(&term), "Should return false for string interpolated value" @@ -987,7 +429,7 @@ mod tests { #[test] fn test_is_mustache_function_call() { - let term = JqTransform::parse_template("map(.price)"); + let term = JqTransform::parse_template("map(.price)").unwrap(); assert!( !JqTransform::recursive_is_mustache(&term), "Should return false for function call" @@ -996,7 +438,7 @@ mod tests { #[test] fn test_is_mustache_concat() { - let term = JqTransform::parse_template(".data.meat + .data.eggs"); + let term = JqTransform::parse_template(".data.meat + .data.eggs").unwrap(); assert!( !JqTransform::recursive_is_mustache(&term), "Should return false for concatenation" @@ -1005,7 +447,7 @@ mod tests { #[test] fn test_is_const_simple_property() { - let term = JqTransform::parse_template(".fruit"); + let term = JqTransform::parse_template(".fruit").unwrap(); assert!( !JqTransform::calculate_is_const(&term), "Should return false for simple property access" @@ -1014,7 +456,7 @@ mod tests { #[test] fn test_is_const_nested_property() { - let term = JqTransform::parse_template(".fruit.name"); + let term = JqTransform::parse_template(".fruit.name").unwrap(); assert!( !JqTransform::calculate_is_const(&term), "Should return false for nested property access" @@ -1023,7 +465,7 @@ mod tests { #[test] fn test_is_const_array_index() { - let term = JqTransform::parse_template(".fruits[1]"); + let term = JqTransform::parse_template(".fruits[1]").unwrap(); assert!( !JqTransform::calculate_is_const(&term), "Should return false for array index access" @@ -1032,7 +474,7 @@ mod tests { #[test] fn test_is_const_pipe_operator() { - let term = JqTransform::parse_template(".fruits[] | .name"); + let term = JqTransform::parse_template(".fruits[] | .name").unwrap(); assert!( !JqTransform::calculate_is_const(&term), "Should return false for pipe operator usage" @@ -1041,7 +483,7 @@ mod tests { #[test] fn test_is_const_filter() { - let term = JqTransform::parse_template(".fruits[] | select(.price > 1)"); + let term = JqTransform::parse_template(".fruits[] | select(.price > 1)").unwrap(); assert!( !JqTransform::calculate_is_const(&term), "Should return false for select filter usage" @@ -1050,7 +492,7 @@ mod tests { #[test] fn test_is_const_true_value() { - let term = JqTransform::parse_template("true"); + let term = JqTransform::parse_template("true").unwrap(); assert!( JqTransform::calculate_is_const(&term), "Should return true for const true value" @@ -1059,7 +501,7 @@ mod tests { #[test] fn test_is_const_false_value() { - let term = JqTransform::parse_template("false"); + let term = JqTransform::parse_template("false").unwrap(); assert!( JqTransform::calculate_is_const(&term), "Should return true for const false value" @@ -1068,7 +510,7 @@ mod tests { #[test] fn test_is_const_number_value() { - let term = JqTransform::parse_template("1"); + let term = JqTransform::parse_template("1").unwrap(); assert!( JqTransform::calculate_is_const(&term), "Should return true for number value" @@ -1077,7 +519,7 @@ mod tests { #[test] fn test_is_const_str_value() { - let term = JqTransform::parse_template("\"foobar\""); + let term = JqTransform::parse_template("\"foobar\"").unwrap(); assert!( JqTransform::calculate_is_const(&term), "Should return true for string value" @@ -1086,7 +528,7 @@ mod tests { #[test] fn test_is_const_str_interpolate_value() { - let term = JqTransform::parse_template("\"Hello, \\(.name)!\""); + let term = JqTransform::parse_template("\"Hello, \\(.name)!\"").unwrap(); assert!( !JqTransform::calculate_is_const(&term), "Should return false for string interpolated value" @@ -1095,7 +537,7 @@ mod tests { #[test] fn test_is_const_function_call() { - let term = JqTransform::parse_template("map(.price)"); + let term = JqTransform::parse_template("map(.price)").unwrap(); assert!( !JqTransform::calculate_is_const(&term), "Should return false for function call" @@ -1104,7 +546,7 @@ mod tests { #[test] fn test_is_const_concat() { - let term = JqTransform::parse_template(".data.meat + .data.eggs"); + let term = JqTransform::parse_template(".data.meat + .data.eggs").unwrap(); assert!( !JqTransform::calculate_is_const(&term), "Should return false for concatenation" @@ -1116,7 +558,9 @@ mod tests { let template_str = ".[] | select(.non_existent)"; let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + let result = jq_template + .render_value(PathValueEnum::Val(Val::from(input_json))) + .unwrap(); assert_eq!( result, async_graphql_value::ConstValue::Null, @@ -1129,7 +573,9 @@ mod tests { let template_str = ".[0]"; let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + let result = jq_template + .render_value(PathValueEnum::Val(Val::from(input_json))) + .unwrap(); assert_eq!( result, async_graphql_value::ConstValue::from_json(json!({"foo": 1})).unwrap(), @@ -1142,7 +588,9 @@ mod tests { let template_str = ".[] | .foo"; let jq_template = JqTransform::try_new(template_str).expect("Failed to create JqTemplate"); let input_json = json!([{"foo": 1}, {"foo": 2}]); - let result = jq_template.render_value(PathValueEnum::Val(Val::from(input_json))); + let result = jq_template + .render_value(PathValueEnum::Val(Val::from(input_json))) + .unwrap(); let expected = async_graphql_value::ConstValue::array(vec![ async_graphql_value::ConstValue::from_json(json!(1)).unwrap(), async_graphql_value::ConstValue::from_json(json!(2)).unwrap(), diff --git a/src/core/mustache/jq_value.rs b/src/core/mustache/jq_value.rs new file mode 100644 index 0000000000..66a7cba3c7 --- /dev/null +++ b/src/core/mustache/jq_value.rs @@ -0,0 +1,624 @@ +use std::fmt::Display; +use std::rc::Rc; +use std::sync::Arc; + +use jaq_core::val::Range; +use jaq_core::{Error, Exn, ValR}; +use jaq_json::Val; + +use crate::core::ir::{EvalContext, ResolverContextLike}; +use crate::core::path::{PathString, PathValue, ValueString}; + +#[derive(Clone)] +/// Used to hold a PathJqValue struct or the JQ Val struct +pub enum PathValueEnum<'a> { + PathValue(Arc<&'a dyn PathJqValue>), + Val(Val), +} + +impl jaq_std::ValT for PathValueEnum<'_> { + fn into_seq>(self) -> Result { + // convert an array into a sequence + match self { + PathValueEnum::PathValue(_) => Err(self), + PathValueEnum::Val(val) => match val { + Val::Arr(a) => match Rc::try_unwrap(a) { + Ok(a) => Ok(a.into_iter().map(Self::Val).collect()), + Err(a) => Ok(a.iter().cloned().map(Self::Val).collect()), + }, + _ => Err(Self::Val(val)), + }, + } + } + + fn as_isize(&self) -> Option { + match self { + PathValueEnum::PathValue(_) => None, + PathValueEnum::Val(val) => val.as_isize(), + } + } + + fn as_f64(&self) -> Result> { + match self { + PathValueEnum::PathValue(_) => Err(Error::new(Self::Val(Val::from( + "Cannot convert context to f64".to_string(), + )))), + PathValueEnum::Val(val) => match val.as_f64() { + Ok(val) => Ok(val), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + }, + } + } +} + +impl jaq_core::ValT for PathValueEnum<'_> { + fn from_num(n: &str) -> ValR { + match Val::from_num(n) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + + fn from_map>(iter: I) -> ValR { + let result: Result, String> = iter + .into_iter() + .map(|(k, v)| match (k, v) { + (PathValueEnum::Val(key), PathValueEnum::Val(value)) => Ok((key, value)), + _ => Err("Invalid key or value type for map".into()), + }) + .collect(); + + match result { + Ok(pairs) => match Val::from_map(pairs) { + Ok(val) => ValR::Ok(PathValueEnum::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + }, + Err(e) => Err(Error::new(Self::Val(Val::from(e)))), + } + } + + fn values(self) -> Box>> { + match self { + PathValueEnum::PathValue(_) => panic!("Cannot iterate context"), + PathValueEnum::Val(val) => Box::new(val.values().map(|v| { + v.map(PathValueEnum::Val).map_err(|err| { + let val = err.into_val(); + Error::new(PathValueEnum::Val(val)) + }) + })), + } + } + + fn index(self, index: &Self) -> ValR { + let PathValueEnum::Val(index) = index else { + return ValR::Err(Error::new(Self::Val(Val::from(format!( + "Could not convert index `{}` val.", + index + ))))); + }; + + match self { + PathValueEnum::PathValue(pv) => { + let Some(v) = pv.get_value(index) else { + return ValR::Err(Error::new(Self::Val(Val::from(format!( + "Could not find key `{}` in context.", + index + ))))); + }; + + match v { + crate::core::path::ValueString::Value(cow) => { + let cv = cow.as_ref().clone(); + match cv.into_json() { + Ok(js) => Ok(Self::Val(Val::from(js))), + Err(err) => ValR::Err(Error::new(Self::Val(Val::from(format!( + "Could not convert value to json: {:?}", + err + ))))), + } + } + crate::core::path::ValueString::String(cow) => { + let v = cow.to_string(); + Ok(Self::Val(Val::from(v))) + } + } + } + PathValueEnum::Val(val) => match val.index(index) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + }, + } + } + + fn range(self, range: jaq_core::val::Range<&Self>) -> ValR { + let (start, end) = ( + range + .start + .map(|v| match v { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( + "Could not convert range start to val.".to_string(), + ))), + PathValueEnum::Val(val) => Ok(val.clone()), + }) + .transpose(), + range + .end + .map(|v| match v { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( + "Could not convert range end to val.".to_string(), + ))), + PathValueEnum::Val(val) => Ok(val.clone()), + }) + .transpose(), + ); + + let (start, end) = match (start, end) { + (Ok(start), Ok(end)) => (start, end), + (Ok(_), Err(err)) => { + let val = err.into_val(); + return Err(Error::new(Self::Val(val))); + } + (Err(err), Ok(_)) => { + let val = err.into_val(); + return Err(Error::new(Self::Val(val))); + } + (Err(_), Err(_)) => { + return ValR::Err(Error::new(Self::Val(Val::from( + "Could not convert range to val.".to_string(), + )))) + } + }; + + let range = Range { start: start.as_ref(), end: end.as_ref() }; + + match self { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Self::Val(Val::from( + "Cannot apply range operation at the context".to_string(), + )))), + PathValueEnum::Val(val) => match val.range(range) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + }, + } + } + + fn map_values<'a, I: Iterator>>( + self, + opt: jaq_core::path::Opt, + f: impl Fn(Self) -> I, + ) -> jaq_core::ValX<'a, Self> { + let f_new = move |x: Val| -> _ { + let iter = f(Self::Val(x)); + iter.map(|v| match v { + Ok(enum_val) => match enum_val { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( + Val::from("Cannot convert context to val.".to_string()), + ))), + PathValueEnum::Val(val) => Ok(val), + }, + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( + "Function execution failed with: {:?}", + err + ))))), + }) + }; + + match self { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( + Val::from("Cannot apply map_values operation at the context".to_string()), + )))), + PathValueEnum::Val(val) => match val.map_values(opt, f_new) { + Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( + format!("The map_values failed because: {:?}", err), + ))))), + }, + } + } + + fn map_index<'a, I: Iterator>>( + self, + index: &Self, + opt: jaq_core::path::Opt, + f: impl Fn(Self) -> I, + ) -> jaq_core::ValX<'a, Self> { + let PathValueEnum::Val(index) = index else { + return jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from(format!( + "Could not convert index `{}` val.", + index + )))))); + }; + + let f_new = move |x: Val| -> _ { + let iter = f(Self::Val(x)); + iter.map(|v| match v { + Ok(enum_val) => match enum_val { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( + Val::from("Cannot convert context to val.".to_string()), + ))), + PathValueEnum::Val(val) => Ok(val), + }, + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( + "Function execution failed with: {:?}", + err + ))))), + }) + }; + + match self { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( + Val::from("Cannot apply map_index operation at the context".to_string()), + )))), + PathValueEnum::Val(val) => match val.map_index(index, opt, f_new) { + Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( + format!("The map_index failed because: {:?}", err), + ))))), + }, + } + } + + fn map_range<'a, I: Iterator>>( + self, + range: jaq_core::val::Range<&Self>, + opt: jaq_core::path::Opt, + f: impl Fn(Self) -> I, + ) -> jaq_core::ValX<'a, Self> { + let (start, end) = ( + range + .start + .map(|v| match v { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( + "Could not convert range start to val.".to_string(), + ))), + PathValueEnum::Val(val) => Ok(val.clone()), + }) + .transpose(), + range + .end + .map(|v| match v { + PathValueEnum::PathValue(_) => ValR::Err(Error::new(Val::from( + "Could not convert range end to val.".to_string(), + ))), + PathValueEnum::Val(val) => Ok(val.clone()), + }) + .transpose(), + ); + + let (start, end) = match (start, end) { + (Ok(start), Ok(end)) => (start, end), + (Ok(_), Err(err)) => { + let val = err.into_val(); + return Err(Exn::from(Error::new(Self::Val(val)))); + } + (Err(err), Ok(_)) => { + let val = err.into_val(); + return Err(Exn::from(Error::new(Self::Val(val)))); + } + (Err(_), Err(_)) => { + return Err(Exn::from(Error::new(Self::Val(Val::from( + "Could not convert range to val.".to_string(), + ))))) + } + }; + + let range = Range { start: start.as_ref(), end: end.as_ref() }; + + let f_new = move |x: Val| -> _ { + let iter = f(Self::Val(x)); + iter.map(|v| match v { + Ok(enum_val) => match enum_val { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new( + Val::from("Cannot convert context to val.".to_string()), + ))), + PathValueEnum::Val(val) => Ok(val), + }, + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Val::from(format!( + "Function execution failed with: {:?}", + err + ))))), + }) + }; + + match self { + PathValueEnum::PathValue(_) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val( + Val::from("Cannot apply map_range operation at the context".to_string()), + )))), + PathValueEnum::Val(val) => match val.map_range(range, opt, f_new) { + Ok(val) => jaq_core::ValX::Ok(Self::Val(val)), + Err(err) => jaq_core::ValX::Err(Exn::from(Error::new(Self::Val(Val::from( + format!("The map_range failed because: {:?}", err), + ))))), + }, + } + } + + fn as_bool(&self) -> bool { + match self { + PathValueEnum::PathValue(_) => true, + PathValueEnum::Val(val) => val.as_bool(), + } + } + + fn as_str(&self) -> Option<&str> { + match self { + PathValueEnum::PathValue(_) => Some("[Context]"), + PathValueEnum::Val(val) => val.as_str(), + } + } +} + +impl<'a> FromIterator> for PathValueEnum<'a> { + fn from_iter>>(iter: I) -> Self { + let iter = iter.into_iter().filter_map(|v| match v { + PathValueEnum::PathValue(_) => None, + PathValueEnum::Val(val) => Some(val), + }); + Self::Val(Val::from_iter(iter)) + } +} + +impl std::ops::Add for PathValueEnum<'_> { + type Output = ValR; + + fn add(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.add(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform add operation with context.".to_string(), + )))), + } + } +} + +impl std::ops::Sub for PathValueEnum<'_> { + type Output = ValR; + + fn sub(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.sub(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform sub operation with context.".to_string(), + )))), + } + } +} + +impl std::ops::Mul for PathValueEnum<'_> { + type Output = ValR; + + fn mul(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.mul(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform mul operation with context.".to_string(), + )))), + } + } +} + +impl std::ops::Div for PathValueEnum<'_> { + type Output = ValR; + + fn div(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.div(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform div operation with context.".to_string(), + )))), + } + } +} + +impl std::ops::Rem for PathValueEnum<'_> { + type Output = ValR; + + fn rem(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (PathValueEnum::Val(self_val), PathValueEnum::Val(rhs_val)) => { + match self_val.rem(rhs_val) { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + } + } + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform rem operation with context.".to_string(), + )))), + } + } +} + +impl std::ops::Neg for PathValueEnum<'_> { + type Output = ValR; + + fn neg(self) -> Self::Output { + match self { + PathValueEnum::Val(self_val) => match self_val.neg() { + Ok(val) => ValR::Ok(Self::Val(val)), + Err(err) => { + let val = err.into_val(); + Err(Error::new(Self::Val(val))) + } + }, + _ => ValR::Err(Error::new(PathValueEnum::Val(Val::from( + "Cannot perform neg operation at context.".to_string(), + )))), + } + } +} + +impl Display for PathValueEnum<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PathValueEnum::PathValue(_) => "[Context]".to_string().fmt(f), + PathValueEnum::Val(val) => val.fmt(f), + } + } +} + +impl std::fmt::Debug for PathValueEnum<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PathValueEnum::PathValue(_) => derive_more::Debug::fmt(&"[Context]".to_string(), f), + PathValueEnum::Val(val) => derive_more::Debug::fmt(&val, f), + } + } +} + +impl PartialEq for PathValueEnum<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (PathValueEnum::PathValue(_), PathValueEnum::PathValue(_)) => true, + (PathValueEnum::PathValue(_), PathValueEnum::Val(_)) => false, + (PathValueEnum::Val(_), PathValueEnum::PathValue(_)) => false, + (PathValueEnum::Val(self_val), PathValueEnum::Val(other_val)) => self_val.eq(other_val), + } + } +} + +impl PartialOrd for PathValueEnum<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for PathValueEnum<'_> {} + +impl Ord for PathValueEnum<'_> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (PathValueEnum::PathValue(_), PathValueEnum::PathValue(_)) => std::cmp::Ordering::Equal, + (PathValueEnum::PathValue(_), PathValueEnum::Val(_)) => std::cmp::Ordering::Greater, + (PathValueEnum::Val(_), PathValueEnum::PathValue(_)) => std::cmp::Ordering::Less, + (PathValueEnum::Val(self_val), PathValueEnum::Val(other_val)) => { + self_val.cmp(other_val) + } + } + } +} + +impl From for PathValueEnum<'_> { + fn from(value: f64) -> Self { + Self::Val(Val::from(value)) + } +} + +impl From> for serde_json::Value { + fn from(value: PathValueEnum<'_>) -> Self { + match value { + PathValueEnum::PathValue(_) => serde_json::Value::String("[Context]".to_string()), + PathValueEnum::Val(val) => serde_json::Value::from(val), + } + } +} + +impl From for PathValueEnum<'_> { + fn from(value: String) -> Self { + Self::Val(Val::from(value)) + } +} + +impl From for PathValueEnum<'_> { + fn from(value: isize) -> Self { + Self::Val(Val::from(value)) + } +} + +impl From for PathValueEnum<'_> { + fn from(value: bool) -> Self { + Self::Val(Val::from(value)) + } +} + +/// Used to get get keys/index out of json compatible objects like EvalContext +pub trait PathJqValue { + fn get_value<'a>(&'a self, index: &Val) -> Option>; +} + +impl PathJqValue for EvalContext<'_, Ctx> { + fn get_value<'a>(&'a self, index: &Val) -> Option> { + let Val::Str(index) = index else { return None }; + self.raw_value(&[index.as_str()]) + } +} + +impl PathJqValue for serde_json::Value { + fn get_value(&self, index: &Val) -> Option> { + match self { + serde_json::Value::Object(map) => { + let Val::Str(index) = index else { return None }; + map.get(index.as_str()).map(|v| { + ValueString::Value(std::borrow::Cow::Owned( + async_graphql_value::ConstValue::from_json(v.clone()).unwrap(), + )) + }) + } + serde_json::Value::Array(list) => { + let Val::Int(index) = index else { return None }; + list.get(*index as usize).map(|v| { + ValueString::Value(std::borrow::Cow::Owned( + async_graphql_value::ConstValue::from_json(v.clone()).unwrap(), + )) + }) + } + _ => None, + } + } +} + +/// Used as a type parameter to accept objects that implement both traits +pub trait PathJqValueString: PathString + PathJqValue {} + +impl PathJqValueString for EvalContext<'_, Ctx> {} + +impl PathJqValueString for serde_json::Value {} diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index f9edb62b63..6c2420e8ff 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -1,9 +1,11 @@ mod eval; mod jq_template; mod jq_transform; +mod jq_value; mod model; mod parse; pub use eval::{Eval, PathStringEval}; pub use jq_template::*; pub use jq_transform::*; +pub use jq_value::*; pub use model::*; diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index faa7b6ec85..6ca9c30300 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -3,43 +3,37 @@ use std::borrow::Cow; use async_graphql::{Name, Value as GraphQLValue}; use indexmap::IndexMap; -use super::mustache::PathJqValueString; +use super::mustache::{JqRuntimeError, PathJqValueString}; use crate::core::blueprint::DynamicValue; pub trait ValueExt { - fn render_value(&self, ctx: &impl PathJqValueString) -> GraphQLValue; + fn render_value(&self, ctx: &impl PathJqValueString) -> Result; } impl ValueExt for DynamicValue { - fn render_value<'a>(&self, ctx: &'a impl PathJqValueString) -> GraphQLValue { + fn render_value( + &self, + ctx: &impl PathJqValueString, + ) -> Result { match self { - DynamicValue::Value(value) => value.to_owned(), - DynamicValue::Mustache(m) => { - let rendered: Cow<'a, str> = Cow::Owned(m.render(ctx)); - - serde_json::from_str::(rendered.as_ref()) - // parsing can fail when Mustache::render returns bare string and since - // that string is not wrapped with quotes serde_json will fail to parse it - // but, we can just use that string as is - .unwrap_or_else(|_| GraphQLValue::String(rendered.into_owned())) - } + DynamicValue::Value(value) => Ok(value.to_owned()), DynamicValue::JqTemplate(t) => t.render_value(ctx), DynamicValue::Object(obj) => { - let out: IndexMap<_, _> = obj + let out: Result, _> = obj .iter() - .map(|(k, v)| { + .map(|(k, v)| -> Result<(_, _), _> { let key = Cow::Borrowed(k.as_str()); let val = v.render_value(ctx); - (Name::new(key), val) + Ok((Name::new(key), val?)) }) .collect(); - GraphQLValue::Object(out) + Ok(GraphQLValue::Object(out?)) } DynamicValue::Array(arr) => { - let out: Vec<_> = arr.iter().map(|v| v.render_value(ctx)).collect(); - GraphQLValue::List(out) + let out: Result, _> = arr.iter().map(|v| v.render_value(ctx)).collect(); + Ok(GraphQLValue::List(out?)) } } } @@ -57,7 +51,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!({"a": {"bar": "baz"}})).unwrap(); assert_eq!(result, expected); } @@ -67,7 +61,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!({"a": 1})).unwrap(); assert_eq!(result, expected); } @@ -77,7 +71,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!({"a": "foo"})).unwrap(); assert_eq!(result, expected); } @@ -87,7 +81,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!(null)).unwrap(); assert_eq!(result, expected); } @@ -97,7 +91,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!({"a": true})).unwrap(); assert_eq!(result, expected); } @@ -107,7 +101,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!({"a": 1.1})).unwrap(); assert_eq!(result, expected); } @@ -117,7 +111,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!({"a": [1, 2, 3]})).unwrap(); assert_eq!(result, expected); } @@ -127,7 +121,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!({"a": [1, 2]})).unwrap(); assert_eq!(result, expected); } @@ -137,7 +131,7 @@ mod tests { let value = json!("{{foo}}"); let value = DynamicValue::try_from(&value).unwrap(); let ctx = json!({"foo": "bar"}); - let result = value.render_value(&ctx); + let result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::String("bar".to_owned()); assert_eq!(result, expected); } @@ -147,7 +141,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!([{"a": 1}, {"a":2}])).unwrap(); assert_eq!(result, expected); } @@ -157,7 +151,7 @@ mod tests { 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 result = value.render_value(&ctx).unwrap(); let expected = async_graphql::Value::from_json(json!([{"a": [{"aa": 1}]}, {"a":[{"aa": 2}]}])) .unwrap(); diff --git a/tests/expression_spec.rs b/tests/expression_spec.rs index 564211dae1..8d4a73f16e 100644 --- a/tests/expression_spec.rs +++ b/tests/expression_spec.rs @@ -7,7 +7,7 @@ mod tests { use tailcall::core::http::RequestContext; use tailcall::core::ir::model::IR; use tailcall::core::ir::{EmptyResolverContext, Error, EvalContext}; - use tailcall::core::mustache::Mustache; + use tailcall::core::mustache::JqTemplate; async fn eval(expr: &IR) -> Result { let runtime = tailcall::cli::runtime::init(&Blueprint::default()); @@ -21,16 +21,16 @@ mod tests { async fn test_and_then() { let abcde = DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap(); let expr = IR::Dynamic(abcde) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.a}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.b}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.c}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.d}}", )))); @@ -44,7 +44,7 @@ mod tests { async fn test_with_args() { let expr = IR::Dynamic(DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap()) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.a.b.c.d}}", )))); @@ -58,16 +58,16 @@ mod tests { async fn test_with_args_piping() { let expr = IR::Dynamic(DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap()) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.a}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.b}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.c}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.d}}", )))); @@ -84,11 +84,11 @@ mod tests { let expr_with_dot = args.clone() - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{.args.a.b.c.d}}", )))); - let expr_without_dot = args.pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + let expr_without_dot = args.pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.a.b.c.d}}", )))); @@ -104,16 +104,16 @@ mod tests { async fn test_optional_dot_piping() { let expr = IR::Dynamic(DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap()) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{.args.a}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{.args.b}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{.args.c}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{.args.d}}", )))); @@ -127,16 +127,16 @@ mod tests { async fn test_mixed_dot_usages() { let expr = IR::Dynamic(DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap()) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{.args.a}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.b}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{.args.c}}", )))) - .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( + .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( "{{args.d}}", )))); From 98cc24d5a73af07bd40808b6c992f7caf9da8337 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Tue, 10 Dec 2024 14:40:36 +0200 Subject: [PATCH 19/28] fix: allow referencing .env for jq templates --- benches/data_loader_bench.rs | 4 ++++ .../impl_path_string_for_evaluation_context.rs | 4 ++++ src/cli/runtime/env.rs | 7 +++++++ src/core/ir/eval_context.rs | 16 ++++++++++++++++ src/core/mod.rs | 8 ++++++++ src/core/mustache/jq_transform.rs | 10 ++++------ src/core/path.rs | 8 ++++++++ src/core/runtime.rs | 7 +++++++ src/core/serde_value_ext.rs | 5 +---- tailcall-aws-lambda/src/runtime.rs | 6 ++++++ tailcall-cloudflare/src/env.rs | 4 ++++ tailcall-wasm/src/env.rs | 7 +++++++ tests/cli/gen.rs | 7 +++++++ tests/core/env.rs | 7 +++++++ tests/core/parse.rs | 7 +++++++ tests/server_spec.rs | 7 +++++++ 16 files changed, 104 insertions(+), 10 deletions(-) diff --git a/benches/data_loader_bench.rs b/benches/data_loader_bench.rs index d9213931f2..a68d25779d 100644 --- a/benches/data_loader_bench.rs +++ b/benches/data_loader_bench.rs @@ -33,6 +33,10 @@ impl EnvIO for Env { fn get(&self, _: &str) -> Option> { unimplemented!("Not needed for this bench") } + + fn get_raw(&self) -> Vec<(String, String)> { + unimplemented!("Not needed for this bench") + } } struct File; diff --git a/benches/impl_path_string_for_evaluation_context.rs b/benches/impl_path_string_for_evaluation_context.rs index e4eb603f67..6502208d34 100644 --- a/benches/impl_path_string_for_evaluation_context.rs +++ b/benches/impl_path_string_for_evaluation_context.rs @@ -84,6 +84,10 @@ impl EnvIO for Env { fn get(&self, _: &str) -> Option> { unimplemented!("Not needed for this bench") } + + fn get_raw(&self) -> Vec<(String, String)> { + unimplemented!("Not needed for this bench") + } } struct File; diff --git a/src/cli/runtime/env.rs b/src/cli/runtime/env.rs index 4c436ff998..057c071435 100644 --- a/src/cli/runtime/env.rs +++ b/src/cli/runtime/env.rs @@ -12,6 +12,13 @@ impl EnvIO for EnvNative { fn get(&self, key: &str) -> Option> { self.vars.get(key).map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.vars + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } impl EnvNative { diff --git a/src/core/ir/eval_context.rs b/src/core/ir/eval_context.rs index 89c03d80c6..a921673796 100644 --- a/src/core/ir/eval_context.rs +++ b/src/core/ir/eval_context.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use async_graphql::{ServerError, Value}; use http::header::HeaderMap; +use indexmap::IndexMap; use super::{GraphQLOperationContext, RelatedFields, ResolverContextLike, SelectionField}; use crate::core::document::print_directives; @@ -93,6 +94,21 @@ impl<'a, Ctx: ResolverContextLike> EvalContext<'a, Ctx> { self.request_ctx.runtime.env.get(key) } + pub fn env_vars(&self) -> async_graphql_value::ConstValue { + let env = self.request_ctx.runtime.env.get_raw(); + let env: Vec<_> = env + .into_iter() + .map(|(k, v)| { + ( + async_graphql_value::Name::new(&k), + async_graphql_value::ConstValue::String(v), + ) + }) + .collect(); + let map = IndexMap::from_iter(env); + async_graphql_value::ConstValue::Object(map) + } + pub fn var(&self, key: &str) -> Option<&str> { let vars = &self.request_ctx.server.vars; diff --git a/src/core/mod.rs b/src/core/mod.rs index 702d5a0a22..7210cdb862 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -67,6 +67,7 @@ pub const fn default_verify_ssl() -> Option { pub trait EnvIO: Send + Sync + 'static { fn get(&self, key: &str) -> Option>; + fn get_raw(&self) -> Vec<(String, String)>; } #[async_trait::async_trait] @@ -150,6 +151,13 @@ pub mod tests { fn get(&self, key: &str) -> Option> { self.0.get(key).map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.0 + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } impl FromIterator<(String, String)> for TestEnvIO { diff --git a/src/core/mustache/jq_transform.rs b/src/core/mustache/jq_transform.rs index 0ae65b3726..50bc62b276 100644 --- a/src/core/mustache/jq_transform.rs +++ b/src/core/mustache/jq_transform.rs @@ -6,9 +6,8 @@ use jaq_core::load::{Arena, File, Loader}; use jaq_core::{Compiler, Ctx, Filter, Native, RcIter, ValR}; use lazy_static::lazy_static; -use crate::core::json::JsonLike; - use super::PathValueEnum; +use crate::core::json::JsonLike; lazy_static! { /// Used to store the compiled JQ templates @@ -29,8 +28,7 @@ impl JqTransform { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { // the term is used because it can be easily serialized, deserialized and hashed - let term = - Self::parse_template(template).map_err(|err| JqRuntimeError::JqTemplateErrors(err))?; + let term = Self::parse_template(template).map_err(JqRuntimeError::JqTemplateErrors)?; // calculate if the expression can be replaced with mustache let is_mustache = Self::recursive_is_mustache(&term); @@ -153,9 +151,9 @@ impl JqTransform { .collect::>() })?; let mut parser = jaq_core::load::parse::Parser::new(&lex); - Ok(parser + parser .term() - .map_err(|err| vec![JqTemplateError::JqParseError(format!("{:?}", err))])?) + .map_err(|err| vec![JqTemplateError::JqParseError(format!("{:?}", err))]) } /// Used as a helper function to determine if the term can be supported with diff --git a/src/core/path.rs b/src/core/path.rs index c6ca84860e..1b4e9fe2c1 100644 --- a/src/core/path.rs +++ b/src/core/path.rs @@ -101,6 +101,7 @@ impl EvalContext<'_, Ctx> { async_graphql_value::ConstValue::object(arr), ))) } + "env" => Some(ValueString::Value(Cow::Owned(ctx.env_vars()))), _ => None, }; } @@ -175,6 +176,13 @@ mod tests { fn get(&self, key: &str) -> Option> { self.env.get(key).map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.env + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } impl Env { diff --git a/src/core/runtime.rs b/src/core/runtime.rs index 1caf1d5e3b..945bbf2ae6 100644 --- a/src/core/runtime.rs +++ b/src/core/runtime.rs @@ -166,6 +166,13 @@ pub mod test { fn get(&self, key: &str) -> Option> { self.vars.get(key).map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.vars + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } impl TestEnvIO { diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index 6ca9c30300..60485984b6 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -11,10 +11,7 @@ pub trait ValueExt { } impl ValueExt for DynamicValue { - fn render_value( - &self, - ctx: &impl PathJqValueString, - ) -> Result { + fn render_value(&self, ctx: &impl PathJqValueString) -> Result { match self { DynamicValue::Value(value) => Ok(value.to_owned()), DynamicValue::JqTemplate(t) => t.render_value(ctx), diff --git a/tailcall-aws-lambda/src/runtime.rs b/tailcall-aws-lambda/src/runtime.rs index 9fccef6d6f..c2f9719373 100644 --- a/tailcall-aws-lambda/src/runtime.rs +++ b/tailcall-aws-lambda/src/runtime.rs @@ -18,6 +18,12 @@ impl EnvIO for LambdaEnv { // as real env vars in the runtime. std::env::var(key).ok().map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + std::env::vars() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } pub fn init_env() -> Arc { diff --git a/tailcall-cloudflare/src/env.rs b/tailcall-cloudflare/src/env.rs index 51838efac2..71f6242a11 100644 --- a/tailcall-cloudflare/src/env.rs +++ b/tailcall-cloudflare/src/env.rs @@ -15,6 +15,10 @@ impl EnvIO for CloudflareEnv { fn get(&self, key: &str) -> Option> { self.env.var(key).ok().map(|v| Cow::from(v.to_string())) } + + fn get_raw(&self) -> Vec<(String, String)> { + unimplemented!() + } } impl CloudflareEnv { diff --git a/tailcall-wasm/src/env.rs b/tailcall-wasm/src/env.rs index 604041cae0..8846b2c805 100644 --- a/tailcall-wasm/src/env.rs +++ b/tailcall-wasm/src/env.rs @@ -20,4 +20,11 @@ impl EnvIO for WasmEnv { fn get(&self, key: &str) -> Option> { self.env.get(key).map(|v| Cow::Owned(v.value().clone())) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.env + .iter() + .map(|entry| (entry.key().clone(), entry.value().clone())) + .collect() + } } diff --git a/tests/cli/gen.rs b/tests/cli/gen.rs index 4fe29d8a3e..f84acd4ead 100644 --- a/tests/cli/gen.rs +++ b/tests/cli/gen.rs @@ -153,6 +153,13 @@ pub mod env { fn get(&self, key: &str) -> Option> { self.0.get(key).map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.0 + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } } diff --git a/tests/core/env.rs b/tests/core/env.rs index 7af2cd3907..bd41233c7b 100644 --- a/tests/core/env.rs +++ b/tests/core/env.rs @@ -14,6 +14,13 @@ impl EnvIO for Env { fn get(&self, key: &str) -> Option> { self.vars.get(key).map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.vars + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } impl Env { diff --git a/tests/core/parse.rs b/tests/core/parse.rs index a438a31e89..5c17c0cf2e 100644 --- a/tests/core/parse.rs +++ b/tests/core/parse.rs @@ -33,6 +33,13 @@ impl EnvIO for Env { fn get(&self, key: &str) -> Option> { self.env.get(key).map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.env + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } impl Env { diff --git a/tests/server_spec.rs b/tests/server_spec.rs index a024dad739..83fb9f48a9 100644 --- a/tests/server_spec.rs +++ b/tests/server_spec.rs @@ -123,6 +123,13 @@ pub mod test { fn get(&self, key: &str) -> Option> { self.vars.get(key).map(Cow::from) } + + fn get_raw(&self) -> Vec<(String, String)> { + self.vars + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } } impl TestEnvIO { From c13656d39c6ab5b2f78130f7890c23097fa0f084 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Tue, 10 Dec 2024 16:01:31 +0200 Subject: [PATCH 20/28] fix: add more tests * var tests * header tests * env tests * negative test --- src/cli/runtime/env.rs | 2 +- src/core/ir/eval_context.rs | 3 +- src/core/mod.rs | 5 +- src/core/mustache/jq_transform.rs | 20 +++---- src/core/path.rs | 6 +- src/core/runtime.rs | 2 +- tailcall-aws-lambda/src/runtime.rs | 4 +- tests/cli/gen.rs | 5 +- tests/core/env.rs | 2 +- tests/core/parse.rs | 2 +- .../core/snapshots/test-jq-template.md_2.snap | 24 ++++++++ .../core/snapshots/test-jq-template.md_3.snap | 18 ++++++ .../core/snapshots/test-jq-template.md_4.snap | 15 +++++ .../core/snapshots/test-jq-template.md_5.snap | 18 ++++++ .../snapshots/test-jq-template.md_client.snap | 8 +++ .../snapshots/test-jq-template.md_merged.snap | 12 +++- tests/execution/test-jq-template.md | 59 ++++++++++++++++++- tests/server_spec.rs | 2 +- 18 files changed, 173 insertions(+), 34 deletions(-) create mode 100644 tests/core/snapshots/test-jq-template.md_2.snap create mode 100644 tests/core/snapshots/test-jq-template.md_3.snap create mode 100644 tests/core/snapshots/test-jq-template.md_4.snap create mode 100644 tests/core/snapshots/test-jq-template.md_5.snap diff --git a/src/cli/runtime/env.rs b/src/cli/runtime/env.rs index 057c071435..dbf57261b4 100644 --- a/src/cli/runtime/env.rs +++ b/src/cli/runtime/env.rs @@ -16,7 +16,7 @@ impl EnvIO for EnvNative { fn get_raw(&self) -> Vec<(String, String)> { self.vars .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.clone(), v.clone())) .collect() } } diff --git a/src/core/ir/eval_context.rs b/src/core/ir/eval_context.rs index a921673796..b260b854f9 100644 --- a/src/core/ir/eval_context.rs +++ b/src/core/ir/eval_context.rs @@ -101,7 +101,8 @@ impl<'a, Ctx: ResolverContextLike> EvalContext<'a, Ctx> { .map(|(k, v)| { ( async_graphql_value::Name::new(&k), - async_graphql_value::ConstValue::String(v), + serde_json::from_str::(&v) + .unwrap_or_else(|_| async_graphql_value::ConstValue::String(v)), ) }) .collect(); diff --git a/src/core/mod.rs b/src/core/mod.rs index 7210cdb862..1b5b4d5823 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -153,10 +153,7 @@ pub mod tests { } fn get_raw(&self) -> Vec<(String, String)> { - self.0 - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect() + self.0.iter().map(|(k, v)| (k.clone(), v.clone())).collect() } } diff --git a/src/core/mustache/jq_transform.rs b/src/core/mustache/jq_transform.rs index 50bc62b276..2b48ead476 100644 --- a/src/core/mustache/jq_transform.rs +++ b/src/core/mustache/jq_transform.rs @@ -291,29 +291,29 @@ impl std::hash::Hash for JqTransform { #[derive(Clone, Debug, thiserror::Error)] pub enum JqTemplateError { - #[error("[JQ Load Errors] {0:?}\n")] + #[error("[JQ Load Errors] {0:?}")] JqLoadError(String), - #[error("[JQ Compile Error] {0:?}\n")] + #[error("[JQ Compile Error] {0:?}")] JqCompileError(String), - #[error("[JQ Parse Error] {0:?}\n")] + #[error("[JQ Parse Error] {0:?}")] JqParseError(String), - #[error("[JQ Lex Error] {0:?}\n")] + #[error("[JQ Lex Error] {0:?}")] JqLexError(String), - #[error("[JQ Runtime Error] {0:?}\n")] + #[error("[JQ Runtime Error] {0:?}")] JqRuntimeError(String), - #[error("[JQ Json Parse Error] {0:?}\n")] + #[error("[JQ Json Parse Error] {0:?}")] JsonParseError(String), } #[derive(Clone, Debug, thiserror::Error)] pub enum JqRuntimeError { - #[error("{0:?}\n")] + #[error("{0:?}")] JqTemplateErrors(Vec), - #[error("{0:?}\n")] + #[error("{0:?}")] JqRuntimeErrors(Vec), - #[error("JQ Transform can be replaced with a Mustache.\n")] + #[error("JQ Transform can be replaced with a Mustache.")] JqIsMustache, - #[error("JQ Transform can be replaced with a Literal.\n")] + #[error("JQ Transform can be replaced with a Literal.")] JqIstConst, } diff --git a/src/core/path.rs b/src/core/path.rs index 1b4e9fe2c1..2c90b7cb0e 100644 --- a/src/core/path.rs +++ b/src/core/path.rs @@ -77,8 +77,8 @@ impl EvalContext<'_, Ctx> { return match path[0].as_ref() { "value" => Some(ValueString::Value(ctx.path_value(&[] as &[T])?)), "args" => Some(ValueString::Value(ctx.path_arg::<&str>(&[])?)), - "vars" => Some(ValueString::String(Cow::Owned( - json!(ctx.vars()).to_string(), + "vars" => Some(ValueString::Value(Cow::Owned( + async_graphql_value::ConstValue::from_json(json!(ctx.vars())).unwrap(), ))), "headers" => { let arr = ctx @@ -180,7 +180,7 @@ mod tests { fn get_raw(&self) -> Vec<(String, String)> { self.env .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.clone(), v.clone())) .collect() } } diff --git a/src/core/runtime.rs b/src/core/runtime.rs index 945bbf2ae6..3ea193b9eb 100644 --- a/src/core/runtime.rs +++ b/src/core/runtime.rs @@ -170,7 +170,7 @@ pub mod test { fn get_raw(&self) -> Vec<(String, String)> { self.vars .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.clone(), v.clone())) .collect() } } diff --git a/tailcall-aws-lambda/src/runtime.rs b/tailcall-aws-lambda/src/runtime.rs index c2f9719373..b73a408611 100644 --- a/tailcall-aws-lambda/src/runtime.rs +++ b/tailcall-aws-lambda/src/runtime.rs @@ -20,9 +20,7 @@ impl EnvIO for LambdaEnv { } fn get_raw(&self) -> Vec<(String, String)> { - std::env::vars() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect() + std::env::vars().collect() } } diff --git a/tests/cli/gen.rs b/tests/cli/gen.rs index f84acd4ead..106c033cbb 100644 --- a/tests/cli/gen.rs +++ b/tests/cli/gen.rs @@ -155,10 +155,7 @@ pub mod env { } fn get_raw(&self) -> Vec<(String, String)> { - self.0 - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect() + self.0.iter().map(|(k, v)| (k.clone(), v.clone())).collect() } } } diff --git a/tests/core/env.rs b/tests/core/env.rs index bd41233c7b..7b6d93001a 100644 --- a/tests/core/env.rs +++ b/tests/core/env.rs @@ -18,7 +18,7 @@ impl EnvIO for Env { fn get_raw(&self) -> Vec<(String, String)> { self.vars .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.clone(), v.clone())) .collect() } } diff --git a/tests/core/parse.rs b/tests/core/parse.rs index 5c17c0cf2e..a81431a04d 100644 --- a/tests/core/parse.rs +++ b/tests/core/parse.rs @@ -37,7 +37,7 @@ impl EnvIO for Env { fn get_raw(&self) -> Vec<(String, String)> { self.env .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.clone(), v.clone())) .collect() } } diff --git a/tests/core/snapshots/test-jq-template.md_2.snap b/tests/core/snapshots/test-jq-template.md_2.snap new file mode 100644 index 0000000000..e6b49fdbac --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_2.snap @@ -0,0 +1,24 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": null, + "errors": [ + { + "message": "Dynamic Value Rendering Error: [JqRuntimeError(\"\\\"split input and separator must be strings\\\"\")]", + "locations": [ + { + "line": 3, + "column": 5 + } + ] + } + ] + } +} diff --git a/tests/core/snapshots/test-jq-template.md_3.snap b/tests/core/snapshots/test-jq-template.md_3.snap new file mode 100644 index 0000000000..fa5fc89c99 --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_3.snap @@ -0,0 +1,18 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "foobar": [ + "foo", + "bar" + ] + } + } +} diff --git a/tests/core/snapshots/test-jq-template.md_4.snap b/tests/core/snapshots/test-jq-template.md_4.snap new file mode 100644 index 0000000000..23dd1ff614 --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_4.snap @@ -0,0 +1,15 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "token": "JWT_TOKEN" + } + } +} diff --git a/tests/core/snapshots/test-jq-template.md_5.snap b/tests/core/snapshots/test-jq-template.md_5.snap new file mode 100644 index 0000000000..ef91216291 --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_5.snap @@ -0,0 +1,18 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "var": [ + "spam", + "eggs" + ] + } + } +} diff --git a/tests/core/snapshots/test-jq-template.md_client.snap b/tests/core/snapshots/test-jq-template.md_client.snap index 21fc9c800c..d0c8c1043e 100644 --- a/tests/core/snapshots/test-jq-template.md_client.snap +++ b/tests/core/snapshots/test-jq-template.md_client.snap @@ -2,6 +2,10 @@ source: tests/core/spec.rs expression: formatted --- +type Bar { + bar: [String!]! +} + type Buzz { first: String! second: String! @@ -17,8 +21,12 @@ type Foo { } type Query { + bar: Bar! fizz: Fizz! foo: Foo! + foobar: [String!]! + token: String! + var: [String!]! } schema { diff --git a/tests/core/snapshots/test-jq-template.md_merged.snap b/tests/core/snapshots/test-jq-template.md_merged.snap index e7e8441797..d813a7d624 100644 --- a/tests/core/snapshots/test-jq-template.md_merged.snap +++ b/tests/core/snapshots/test-jq-template.md_merged.snap @@ -2,10 +2,16 @@ source: tests/core/spec.rs expression: formatter --- -schema @server(hostname: "0.0.0.0", port: 8000) @upstream { +schema + @server(hostname: "0.0.0.0", port: 8000, vars: [{key: "id", value: "spam eggs"}]) + @upstream(allowedHeaders: ["Authorization"]) { query: Query } +type Bar { + bar: [String!]! @expr(body: "{{.value.foo | split(\" \")}}") +} + type Buzz { first: String! second: String! @@ -21,6 +27,10 @@ type Foo { } type Query { + bar: Bar! @http(url: "http://upstream/foo") fizz: Fizz! @http(url: "http://upstream/foo") foo: Foo! @http(url: "http://upstream/foo") + foobar: [String!]! @expr(body: "{{ .env.FOOBAR | split(\" \") }}") + token: String! @expr(body: "{{ .headers.authorization | split(\" \") | .[1] }}") + var: [String!]! @expr(body: "{{ .vars | .id | split(\" \") }}") } diff --git a/tests/execution/test-jq-template.md b/tests/execution/test-jq-template.md index 8e5415fd90..c4e3a7bf4d 100644 --- a/tests/execution/test-jq-template.md +++ b/tests/execution/test-jq-template.md @@ -1,20 +1,29 @@ # Basic queries with field ordering check ```graphql @config -schema @server(port: 8000, hostname: "0.0.0.0") { +schema + @server(port: 8000, hostname: "0.0.0.0", vars: [{key: "id", value: "spam eggs"}]) + @upstream(allowedHeaders: ["Authorization"]) { query: Query } type Query { foo: Foo! @http(url: "http://upstream/foo") + bar: Bar! @http(url: "http://upstream/foo") fizz: Fizz! @http(url: "http://upstream/foo") + foobar: [String!]! @expr(body: "{{ .env.FOOBAR | split(\" \") }}") + token: String! @expr(body: "{{ .headers.authorization | split(\" \") | .[1] }}") + var: [String!]! @expr(body: "{{ .vars | .id | split(\" \") }}") } type Foo { - bar: String! bar: [String!]! @expr(body: "{{.value.bar | split(\" \")}}") } +type Bar { + bar: [String!]! @expr(body: "{{.value.foo | split(\" \")}}") +} + type Fizz { bar: String! buzz: Buzz! @expr(body: "{{ .value.bar | split(\" \") | {first: .[0], second: .[1]} }}") @@ -26,11 +35,17 @@ type Buzz { } ``` +```json @env +{ + "FOOBAR": "foo bar" +} +``` + ```yml @mock - request: method: GET url: http://upstream/foo - expectedHits: 2 + expectedHits: 3 response: status: 200 body: @@ -60,4 +75,42 @@ type Buzz { } } } + +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + bar { + bar + } + } + +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + foobar + } + +- method: POST + url: http://localhost:8080/graphql + headers: + authorization: "Bearer JWT_TOKEN" + body: + query: | + { + token + } + +- method: POST + url: http://localhost:8080/graphql + headers: + authorization: "Bearer JWT_TOKEN" + body: + query: | + { + var + } ``` diff --git a/tests/server_spec.rs b/tests/server_spec.rs index 83fb9f48a9..b888a95992 100644 --- a/tests/server_spec.rs +++ b/tests/server_spec.rs @@ -127,7 +127,7 @@ pub mod test { fn get_raw(&self) -> Vec<(String, String)> { self.vars .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.clone(), v.clone())) .collect() } } From 85abf927fdba43a9357d53edf9ded247bac7d3d4 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Tue, 10 Dec 2024 16:04:04 +0200 Subject: [PATCH 21/28] fix: add args test --- tests/core/snapshots/test-jq-template.md_6.snap | 15 +++++++++++++++ .../snapshots/test-jq-template.md_client.snap | 1 + .../snapshots/test-jq-template.md_merged.snap | 1 + tests/execution/test-jq-template.md | 11 +++++++++-- 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 tests/core/snapshots/test-jq-template.md_6.snap diff --git a/tests/core/snapshots/test-jq-template.md_6.snap b/tests/core/snapshots/test-jq-template.md_6.snap new file mode 100644 index 0000000000..1fa6222f34 --- /dev/null +++ b/tests/core/snapshots/test-jq-template.md_6.snap @@ -0,0 +1,15 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "arg": "Hello" + } + } +} diff --git a/tests/core/snapshots/test-jq-template.md_client.snap b/tests/core/snapshots/test-jq-template.md_client.snap index d0c8c1043e..187d796748 100644 --- a/tests/core/snapshots/test-jq-template.md_client.snap +++ b/tests/core/snapshots/test-jq-template.md_client.snap @@ -21,6 +21,7 @@ type Foo { } type Query { + arg(text: String!): String! bar: Bar! fizz: Fizz! foo: Foo! diff --git a/tests/core/snapshots/test-jq-template.md_merged.snap b/tests/core/snapshots/test-jq-template.md_merged.snap index d813a7d624..eaf58537f7 100644 --- a/tests/core/snapshots/test-jq-template.md_merged.snap +++ b/tests/core/snapshots/test-jq-template.md_merged.snap @@ -27,6 +27,7 @@ type Foo { } type Query { + arg(text: String!): String! @expr(body: "{{ .args.text | split(\" \") | .[0] }}") bar: Bar! @http(url: "http://upstream/foo") fizz: Fizz! @http(url: "http://upstream/foo") foo: Foo! @http(url: "http://upstream/foo") diff --git a/tests/execution/test-jq-template.md b/tests/execution/test-jq-template.md index c4e3a7bf4d..33d16a650d 100644 --- a/tests/execution/test-jq-template.md +++ b/tests/execution/test-jq-template.md @@ -14,6 +14,7 @@ type Query { foobar: [String!]! @expr(body: "{{ .env.FOOBAR | split(\" \") }}") token: String! @expr(body: "{{ .headers.authorization | split(\" \") | .[1] }}") var: [String!]! @expr(body: "{{ .vars | .id | split(\" \") }}") + arg(text: String!): String! @expr(body: "{{ .args.text | split(\" \") | .[0] }}") } type Foo { @@ -106,11 +107,17 @@ type Buzz { - method: POST url: http://localhost:8080/graphql - headers: - authorization: "Bearer JWT_TOKEN" body: query: | { var } + +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + arg(text: "Hello World") + } ``` From d74853f80e6a579e1e9195a6e828d0e6294ddcb8 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Tue, 10 Dec 2024 16:56:25 +0200 Subject: [PATCH 22/28] fix: add jq select tests --- tests/core/snapshots/test-jq-select.md_0.snap | 26 +++++++++ .../snapshots/test-jq-select.md_client.snap | 17 ++++++ .../snapshots/test-jq-select.md_merged.snap | 25 +++++++++ tests/execution/test-jq-select.md | 56 +++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 tests/core/snapshots/test-jq-select.md_0.snap create mode 100644 tests/core/snapshots/test-jq-select.md_client.snap create mode 100644 tests/core/snapshots/test-jq-select.md_merged.snap create mode 100644 tests/execution/test-jq-select.md diff --git a/tests/core/snapshots/test-jq-select.md_0.snap b/tests/core/snapshots/test-jq-select.md_0.snap new file mode 100644 index 0000000000..18c2aa0baa --- /dev/null +++ b/tests/core/snapshots/test-jq-select.md_0.snap @@ -0,0 +1,26 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "userDetails": { + "id": [ + 49 + ], + "city": "FIZZ", + "phone": [ + 66, + 85, + 90, + 90 + ] + } + } + } +} diff --git a/tests/core/snapshots/test-jq-select.md_client.snap b/tests/core/snapshots/test-jq-select.md_client.snap new file mode 100644 index 0000000000..044c2ef776 --- /dev/null +++ b/tests/core/snapshots/test-jq-select.md_client.snap @@ -0,0 +1,17 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +type Query { + userDetails(id: Int!): UserDetails +} + +type UserDetails { + city: String! + id: [Int!]! + phone: [Int!]! +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/test-jq-select.md_merged.snap b/tests/core/snapshots/test-jq-select.md_merged.snap new file mode 100644 index 0000000000..e7a0711796 --- /dev/null +++ b/tests/core/snapshots/test-jq-select.md_merged.snap @@ -0,0 +1,25 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema @server(hostname: "0.0.0.0", port: 8001, queryValidation: false) @upstream(httpCache: 42) { + query: Query +} + +type Query { + userDetails(id: Int!): UserDetails + @http( + url: "http://upstream/users/{{.args.id}}" + select: { + id: "{{ .args.id | tostring | explode }}" + city: "{{ .args.address.city | ascii_upcase }}" + phone: "{{.args.phone | explode}}" + } + ) +} + +type UserDetails { + city: String! + id: [Int!]! + phone: [Int!]! +} diff --git a/tests/execution/test-jq-select.md b/tests/execution/test-jq-select.md new file mode 100644 index 0000000000..010e00c022 --- /dev/null +++ b/tests/execution/test-jq-select.md @@ -0,0 +1,56 @@ +# Basic queries with field ordering check + +```graphql @config +schema @server(port: 8001, queryValidation: false, hostname: "0.0.0.0") @upstream(httpCache: 42) { + query: Query +} + +type Query { + userDetails(id: Int!): UserDetails + @http( + url: "http://upstream/users/{{.args.id}}" + select: { + id: "{{ .args.id | tostring | explode }}" + city: "{{ .args.address.city | ascii_upcase }}" + phone: "{{.args.phone | explode}}" + } + ) +} + +type UserDetails { + id: [Int!]! + city: String! + phone: [Int!]! +} +``` + +```yml @mock +- request: + method: GET + url: http://upstream/users/1 + expectedHits: 1 + response: + status: 200 + body: + id: 1 + company: + name: FOO + catchPhrase: BAR + address: + city: FIZZ + phone: BUZZ +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + userDetails(id: 1) { + id + city + phone + } + } +``` From ef0b699e6a20d4669960e4be6ea5d99ad73d8c4a Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Tue, 10 Dec 2024 17:07:40 +0200 Subject: [PATCH 23/28] fix: update cargo lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 79e05620dc..bde42ac317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2814,7 +2814,7 @@ checksum = "8daf2b52304419d7bf5ec32891884c65274a3eedc0b5834b84627099901a1176" dependencies = [ "foldhash", "hifijson", - "indexmap 2.6.0", + "indexmap 2.7.0", "jaq-core", "jaq-std", "serde_json", From 4676300f90aff5e93bdb82ed8efa8d1b135bb106 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 12 Dec 2024 14:45:22 +0200 Subject: [PATCH 24/28] refactor: review changes * JqTransformer is part of Mustache * Mustache is a superset of JqTransform * Remove compiled filters storage --- src/core/blueprint/dynamic_value.rs | 66 ++++------ src/core/http/request_template.rs | 2 + src/core/mustache/eval.rs | 8 ++ src/core/mustache/jq_template.rs | 184 ---------------------------- src/core/mustache/jq_transform.rs | 61 ++++----- src/core/mustache/jq_value.rs | 2 +- src/core/mustache/mod.rs | 3 +- src/core/mustache/model.rs | 21 ++-- src/core/mustache/parse.rs | 66 ++++++---- src/core/mustache/render_value.rs | 79 ++++++++++++ src/core/path.rs | 20 +++ src/core/serde_value_ext.rs | 2 +- tests/expression_spec.rs | 40 +++--- 13 files changed, 243 insertions(+), 311 deletions(-) delete mode 100644 src/core/mustache/jq_template.rs create mode 100644 src/core/mustache/render_value.rs diff --git a/src/core/blueprint/dynamic_value.rs b/src/core/blueprint/dynamic_value.rs index 7e591f066a..9f5e8602ae 100644 --- a/src/core/blueprint/dynamic_value.rs +++ b/src/core/blueprint/dynamic_value.rs @@ -2,13 +2,13 @@ use async_graphql_value::{ConstValue, Name}; use indexmap::IndexMap; use serde_json::Value; -use crate::core::mustache::JqTemplate; +use crate::core::Mustache; #[derive(Debug, Clone, PartialEq)] /// This is used to express dynamic value resolver engine. pub enum DynamicValue { Value(A), - JqTemplate(JqTemplate), + Mustache(Mustache), Object(IndexMap>), Array(Vec>), } @@ -26,25 +26,15 @@ impl DynamicValue { pub fn prepend(self, name: &str) -> Self { match self { DynamicValue::Value(value) => DynamicValue::Value(value), - DynamicValue::JqTemplate(jqt) => DynamicValue::JqTemplate(JqTemplate( - jqt.0 - .into_iter() - .map(|mut f| match &mut f { - crate::core::mustache::JqTemplateIR::JqTransform(_) => f, - crate::core::mustache::JqTemplateIR::Literal(_) => f, - // this function can prepend a custom prefix to mustache only - crate::core::mustache::JqTemplateIR::Mustache(mustache) => { - let segments = mustache.segments_mut(); - if let Some(crate::core::mustache::Segment::Expression(vec)) = - segments.get_mut(0) - { - vec.insert(0, name.to_string()); - } - f - } - }) - .collect(), - )), + DynamicValue::Mustache(mut m) => { + for seg in m.segments_mut() { + if let crate::core::mustache::Segment::Expression(vec) = seg { + vec.insert(0, name.to_string()); + } + } + + DynamicValue::Mustache(m) + } DynamicValue::Object(index_map) => { let index_map = index_map .into_iter() @@ -66,8 +56,8 @@ impl TryFrom<&DynamicValue> for ConstValue { fn try_from(value: &DynamicValue) -> Result { match value { DynamicValue::Value(v) => Ok(v.to_owned()), - DynamicValue::JqTemplate(_) => Err(anyhow::anyhow!( - "jq template cannot be converted to const value" + DynamicValue::Mustache(_) => Err(anyhow::anyhow!( + "Mustache template cannot be converted to const value" )), DynamicValue::Object(obj) => { let out: Result, anyhow::Error> = obj @@ -89,7 +79,7 @@ impl DynamicValue { // Helper method to determine if the value is constant (non-mustache). pub fn is_const(&self) -> bool { match self { - DynamicValue::JqTemplate(t) => t.is_const(), + DynamicValue::Mustache(t) => t.is_const(), DynamicValue::Object(obj) => obj.values().all(|v| v.is_const()), DynamicValue::Array(arr) => arr.iter().all(|v| v.is_const()), _ => true, @@ -117,12 +107,12 @@ impl TryFrom<&Value> for DynamicValue { Ok(DynamicValue::Array(out?)) } Value::String(s) => { - let jqt = JqTemplate::parse(s); + let m = Mustache::parse(s); - if jqt.is_const() { + if m.is_const() { Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)) } else { - Ok(DynamicValue::JqTemplate(jqt)) + Ok(DynamicValue::Mustache(m)) } } _ => Ok(DynamicValue::Value(ConstValue::from_json(value.clone())?)), @@ -137,33 +127,31 @@ mod test { #[test] fn test_dynamic_value_inject() { let value: DynamicValue = - DynamicValue::JqTemplate(JqTemplate::parse("{{.foo}}")).prepend("args"); + DynamicValue::Mustache(Mustache::parse("{{.foo}}")).prepend("args"); let expected: DynamicValue = - DynamicValue::JqTemplate(JqTemplate::parse("{{.args.foo}}")); + DynamicValue::Mustache(Mustache::parse("{{.args.foo}}")); assert_eq!(value, expected); let mut value_map = IndexMap::new(); value_map.insert( Name::new("foo"), - DynamicValue::JqTemplate(JqTemplate::parse("{{.foo}}")), + DynamicValue::Mustache(Mustache::parse("{{.foo}}")), ); let value: DynamicValue = DynamicValue::Object(value_map).prepend("args"); let mut expected_map = IndexMap::new(); expected_map.insert( Name::new("foo"), - DynamicValue::JqTemplate(JqTemplate::parse("{{.args.foo}}")), + DynamicValue::Mustache(Mustache::parse("{{.args.foo}}")), ); let expected: DynamicValue = DynamicValue::Object(expected_map); assert_eq!(value, expected); - let value: DynamicValue = DynamicValue::Array(vec![DynamicValue::JqTemplate( - JqTemplate::parse("{{.foo}}"), - )]) - .prepend("args"); - let expected: DynamicValue = - DynamicValue::Array(vec![DynamicValue::JqTemplate(JqTemplate::parse( - "{{.args.foo}}", - ))]); + let value: DynamicValue = + DynamicValue::Array(vec![DynamicValue::Mustache(Mustache::parse("{{.foo}}"))]) + .prepend("args"); + let expected: DynamicValue = DynamicValue::Array(vec![DynamicValue::Mustache( + Mustache::parse("{{.args.foo}}"), + )]); assert_eq!(value, expected); let value: DynamicValue = DynamicValue::Value(ConstValue::Null).prepend("args"); diff --git a/src/core/http/request_template.rs b/src/core/http/request_template.rs index 1c57f140db..ed5a190728 100644 --- a/src/core/http/request_template.rs +++ b/src/core/http/request_template.rs @@ -301,6 +301,7 @@ impl<'a, A: PathValue> Eval<'a> for ValueStringEval { async_graphql::Value::String(text.to_owned()), ))), Segment::Expression(parts) => in_value.raw_value(parts), + Segment::JqTransform(_) => panic!("Cannot eval JQ for PathValue"), }) .next() // Return the first value that is found } @@ -344,6 +345,7 @@ impl<'a, A: PathString> Eval<'a> for ExpressionValueEval { } } } + Segment::JqTransform(_) => panic!("Cannot eval JQ for PathString"), } } (result, first_expression_value) diff --git a/src/core/mustache/eval.rs b/src/core/mustache/eval.rs index 5fd1a011dc..263eb036ad 100644 --- a/src/core/mustache/eval.rs +++ b/src/core/mustache/eval.rs @@ -38,6 +38,11 @@ impl PathStringEval { .unwrap_or( Mustache::from(vec![Segment::Expression(parts.to_vec())]).to_string(), ), + Segment::JqTransform(jqt) => { + Mustache::from(vec![Segment::JqTransform(jqt.clone())]) + .to_string() + .replace("\"", "\\\"") + } }) .collect() } @@ -57,6 +62,7 @@ impl Eval<'_> for PathStringEval { .path_string(parts) .map(|a| a.to_string()) .unwrap_or_default(), + Segment::JqTransform(_) => panic!("Cannot eval JQ for PathString"), }) .collect() } @@ -92,6 +98,7 @@ impl<'a, A: Path + 'a> Eval<'a> for PathEval<&'a A> { .filter_map(|segment| match segment { Segment::Literal(text) => Some(Exit::Text(text)), Segment::Expression(parts) => in_value.get_path(parts).map(Exit::Value), + Segment::JqTransform(_) => panic!("Cannot eval JQ for Path"), }) .collect::>() } @@ -116,6 +123,7 @@ impl Eval<'_> for PathGraphqlEval { .map(|segment| match segment { Segment::Literal(text) => text.to_string(), Segment::Expression(parts) => in_value.path_graphql(parts).unwrap_or_default(), + Segment::JqTransform(_) => panic!("Cannot eval JQ for PathGraphql"), }) .collect() } diff --git a/src/core/mustache/jq_template.rs b/src/core/mustache/jq_template.rs deleted file mode 100644 index f80731e8f5..0000000000 --- a/src/core/mustache/jq_template.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::sync::Arc; - -use nom::branch::alt; -use nom::bytes::complete::{tag, take_until}; -use nom::combinator::map; -use nom::multi::many0; -use nom::sequence::delimited; -use nom::{Finish, IResult}; - -use super::{JqRuntimeError, JqTransform, Mustache, PathJqValueString}; -use crate::core::mustache::Segment; - -#[derive(Debug, Clone, PartialEq, Hash)] -/// Used to represent a mixture of getters mustache, jq transformations and -/// const values templates -pub struct JqTemplate(pub Vec); - -#[derive(Debug, Clone, PartialEq, Hash)] -/// The IR for each part of the template -pub enum JqTemplateIR { - JqTransform(JqTransform), - Literal(String), - Mustache(Mustache), -} - -impl JqTemplate { - /// Used to check if the returned expression resolves to a constant value - /// always - pub fn is_const(&self) -> bool { - self.0.iter().all(|v| match v { - JqTemplateIR::JqTransform(jq) => jq.is_const(), - JqTemplateIR::Literal(_) => true, - JqTemplateIR::Mustache(m) => m.is_const(), - }) - } - - /// Used to render the template - pub fn render_value( - &self, - ctx: &impl PathJqValueString, - ) -> Result { - let expressions_len = self.0.len(); - match expressions_len { - 0 => Ok(async_graphql_value::ConstValue::Null), - 1 => { - let expression = self.0.first().unwrap(); - self.execute_expression(ctx, expression) - } - _ => { - let (errors, result): (Vec<_>, Vec<_>) = self - .0 - .iter() - .map(|expr| self.execute_expression(ctx, expr)) - .partition(Result::is_err); - - let errors: Vec = errors - .into_iter() - .filter_map(|e| match e { - Ok(_) => None, - Err(err) => Some(err), - }) - .collect(); - if !errors.is_empty() { - return Err(JqRuntimeError::JqRuntimeErrors(errors)); - } - - let result = result - .into_iter() - .filter_map(|v| match v { - Ok(v) => Some(v), - Err(_) => None, - }) - .fold(String::new(), |mut acc, cur| { - match &cur { - async_graphql::Value::String(s) => acc += s, - _ => acc += &cur.to_string(), - } - acc - }); - Ok(async_graphql_value::ConstValue::String(result)) - } - } - } - - fn execute_expression( - &self, - ctx: &impl PathJqValueString, - expression: &JqTemplateIR, - ) -> Result { - match expression { - JqTemplateIR::JqTransform(jq_transform) => { - jq_transform.render_value(super::PathValueEnum::PathValue(Arc::new(ctx))) - } - JqTemplateIR::Literal(value) => { - Ok(async_graphql_value::ConstValue::String(value.clone())) - } - JqTemplateIR::Mustache(mustache) => { - let mustache_result = mustache.render(ctx); - - Ok( - serde_json::from_str::(&mustache_result) - .unwrap_or_else(|_| { - async_graphql_value::ConstValue::String(mustache_result) - }), - ) - } - } - } - - pub fn parse(template: &str) -> Self { - let result = parse_jq_template(template).finish(); - match result { - Ok((_, jq_template)) => jq_template, - Err(_) => Self(vec![JqTemplateIR::Literal(template.to_string())]), - } - } -} - -fn parse_expression(input: &str) -> IResult<&str, JqTemplateIR> { - delimited( - tag("{{"), - map(take_until("}}"), |template| { - match JqTransform::try_new(template) { - Ok(jq) => JqTemplateIR::JqTransform(jq), - Err(err) => match err { - JqRuntimeError::JqIsMustache => { - let expression: Vec<_> = template - .trim() - .trim_start_matches('.') - .split(".") - .map(String::from) - .collect(); - let segment = Segment::Expression(expression); - JqTemplateIR::Mustache(Mustache::from(vec![segment])) - } - _ => { - let m = Mustache::parse(&format!("{{{{{}}}}}", template.trim())); - if !m.is_const() { - JqTemplateIR::Mustache(m) - } else { - JqTemplateIR::Literal(template.to_string()) - } - } - }, - } - }), - tag("}}"), - )(input) -} - -fn parse_segment(input: &str) -> IResult<&str, Vec> { - let expression_result = many0(alt(( - parse_expression, - map(take_until("{{"), |txt: &str| { - JqTemplateIR::Literal(txt.to_string()) - }), - )))(input); - - if let Ok((remaining, segments)) = expression_result { - if remaining.is_empty() { - Ok((remaining, segments)) - } else { - let mut segments = segments; - segments.push(JqTemplateIR::Literal(remaining.to_string())); - Ok(("", segments)) - } - } else { - Ok(("", vec![JqTemplateIR::Literal(input.to_string())])) - } -} - -fn parse_jq_template(input: &str) -> IResult<&str, JqTemplate> { - map(parse_segment, |segments| { - JqTemplate( - segments - .into_iter() - .filter(|seg| match seg { - JqTemplateIR::Literal(s) => (!s.is_empty()) && s != "\"", - _ => true, - }) - .collect(), - ) - })(input) -} diff --git a/src/core/mustache/jq_transform.rs b/src/core/mustache/jq_transform.rs index 2b48ead476..2e5480e228 100644 --- a/src/core/mustache/jq_transform.rs +++ b/src/core/mustache/jq_transform.rs @@ -1,25 +1,15 @@ use std::fmt::Display; -use std::sync::RwLock; use jaq_core::load::parse::Term; use jaq_core::load::{Arena, File, Loader}; use jaq_core::{Compiler, Ctx, Filter, Native, RcIter, ValR}; -use lazy_static::lazy_static; use super::PathValueEnum; use crate::core::json::JsonLike; - -lazy_static! { - /// Used to store the compiled JQ templates - static ref JQ_TEMPLATE_STORAGE: RwLock>>>> = - RwLock::new(Vec::new()); -} - /// Used to represent a JQ template. Currently used only on @expr directive. #[derive(Clone)] pub struct JqTransform { - /// The compiled template index - template_id: usize, + template: String, /// The IR representation, used for debug purposes representation: String, } @@ -27,8 +17,9 @@ pub struct JqTransform { impl JqTransform { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { + let template = template.replace("\\\"", "\""); // the term is used because it can be easily serialized, deserialized and hashed - let term = Self::parse_template(template).map_err(JqRuntimeError::JqTemplateErrors)?; + let term = Self::parse_template(&template).map_err(JqRuntimeError::JqTemplateErrors)?; // calculate if the expression can be replaced with mustache let is_mustache = Self::recursive_is_mustache(&term); @@ -42,6 +33,22 @@ impl JqTransform { return Err(JqRuntimeError::JqIstConst); } + Self::compile_template(&template)?; + + Ok(Self { + template: template.to_string(), + representation: format!("{:?}", term), + }) + } + + /// Used to get the template string + pub fn template(&self) -> &str { + &self.template + } + + fn compile_template( + template: &str, + ) -> Result>>, JqRuntimeError> { // the template is used to be parsed in to the IR AST let template = File { code: template, path: () }; // defs is used to extend the syntax with custom definitions of functions, like @@ -72,23 +79,19 @@ impl JqTransform { ) })?; - // store the compiled template - let mut write_lock = JQ_TEMPLATE_STORAGE.write().unwrap(); - let template_id = write_lock.len(); - write_lock.push(filter); - - Ok(Self { template_id, representation: format!("{:?}", term) }) + Ok(filter) } /// Used to execute the transformation of the JqTemplate - pub fn run<'input>(&self, data: PathValueEnum<'input>) -> Vec>> { + pub fn run<'input>( + &'input self, + data: PathValueEnum<'input>, + ) -> Vec>> { let inputs = RcIter::new(core::iter::empty()); let ctx = Ctx::new([], &inputs); - let read_guard = JQ_TEMPLATE_STORAGE.read().unwrap(); - - let filter: &Filter>> = - unsafe { std::mem::transmute(read_guard.get(self.template_id).unwrap()) }; + let filter: Filter>> = + Self::compile_template(&self.template).unwrap(); filter.run((ctx, data)).collect::>() } @@ -599,7 +602,7 @@ mod tests { #[test] fn test_debug() { let jq_template: JqTransform = - JqTransform { template_id: 0, representation: "test".to_string() }; + JqTransform { template: "".to_string(), representation: "test".to_string() }; let debug_string = format!("{:?}", jq_template); assert_eq!(debug_string, "JqTemplate { representation: \"test\" }"); } @@ -607,7 +610,7 @@ mod tests { #[test] fn test_display() { let jq_template: JqTransform = - JqTransform { template_id: 0, representation: "test".to_string() }; + JqTransform { template: "".to_string(), representation: "test".to_string() }; let display_string = format!("{}", jq_template); assert_eq!(display_string, "[JqTemplate](test)"); } @@ -615,18 +618,18 @@ mod tests { #[test] fn test_partial_eq() { let jq_template1: JqTransform = - JqTransform { template_id: 0, representation: "test".to_string() }; + JqTransform { template: "".to_string(), representation: "test".to_string() }; let jq_template2: JqTransform = - JqTransform { template_id: 0, representation: "test".to_string() }; + JqTransform { template: "".to_string(), representation: "test".to_string() }; assert_eq!(jq_template1, jq_template2); } #[test] fn test_hash() { let jq_template1: JqTransform = - JqTransform { template_id: 0, representation: "test".to_string() }; + JqTransform { template: "".to_string(), representation: "test".to_string() }; let jq_template2: JqTransform = - JqTransform { template_id: 0, representation: "test".to_string() }; + JqTransform { template: "".to_string(), representation: "test".to_string() }; let mut hasher1 = DefaultHasher::new(); let mut hasher2 = DefaultHasher::new(); jq_template1.hash(&mut hasher1); diff --git a/src/core/mustache/jq_value.rs b/src/core/mustache/jq_value.rs index 66a7cba3c7..26c921f65d 100644 --- a/src/core/mustache/jq_value.rs +++ b/src/core/mustache/jq_value.rs @@ -617,7 +617,7 @@ impl PathJqValue for serde_json::Value { } /// Used as a type parameter to accept objects that implement both traits -pub trait PathJqValueString: PathString + PathJqValue {} +pub trait PathJqValueString: PathString + PathJqValue + PathValue {} impl PathJqValueString for EvalContext<'_, Ctx> {} diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index 6c2420e8ff..c240d34626 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -1,11 +1,10 @@ mod eval; -mod jq_template; mod jq_transform; mod jq_value; mod model; mod parse; +mod render_value; pub use eval::{Eval, PathStringEval}; -pub use jq_template::*; pub use jq_transform::*; pub use jq_value::*; pub use model::*; diff --git a/src/core/mustache/model.rs b/src/core/mustache/model.rs index 6717533c48..eb6f218c99 100644 --- a/src/core/mustache/model.rs +++ b/src/core/mustache/model.rs @@ -1,5 +1,7 @@ use std::fmt::Display; +use super::JqTransform; + #[derive(Debug, Clone, PartialEq, Hash, Default)] pub struct Mustache(Vec); @@ -7,6 +9,7 @@ pub struct Mustache(Vec); pub enum Segment { Literal(String), Expression(Vec), + JqTransform(JqTransform), } impl> From for Mustache { @@ -16,17 +19,14 @@ impl> From for Mustache { } impl Mustache { + /// Used to check if the returned expression resolves to a constant value + /// always pub fn is_const(&self) -> bool { - match self { - Mustache(segments) => { - for s in segments { - if let Segment::Expression(_) = s { - return false; - } - } - true - } - } + self.0.iter().all(|v| match v { + Segment::Literal(_) => true, + Segment::Expression(_) => false, + Segment::JqTransform(jq_transform) => jq_transform.is_const(), + }) } pub fn segments(&self) -> &Vec { @@ -63,6 +63,7 @@ impl Display for Mustache { .map(|segment| match segment { Segment::Literal(text) => text.clone(), Segment::Expression(parts) => format!("{{{{.{}}}}}", parts.join(".")), + Segment::JqTransform(jq) => format!("{{{{{}}}}}", jq.template()), }) .collect::>() .join(""); diff --git a/src/core/mustache/parse.rs b/src/core/mustache/parse.rs index 6035f7895b..c6b66fb834 100644 --- a/src/core/mustache/parse.rs +++ b/src/core/mustache/parse.rs @@ -18,32 +18,18 @@ impl Mustache { } } -fn parse_name(input: &str) -> IResult<&str, String> { - let spaces = nom::character::complete::multispace0; - let alpha = nom::character::complete::alpha1; - let alphanumeric_or_underscore = nom::multi::many0(nom::branch::alt(( - nom::character::complete::alphanumeric1, - nom::bytes::complete::tag("_"), - ))); - - let parser = nom::sequence::tuple((spaces, alpha, alphanumeric_or_underscore, spaces)); - - nom::combinator::map(parser, |(_, a, b, _)| { - let b: String = b.into_iter().collect(); - format!("{}{}", a, b) - })(input) -} - fn parse_expression(input: &str) -> IResult<&str, Segment> { delimited( tag("{{"), - map( - nom::sequence::tuple(( - nom::combinator::opt(char('.')), // Optional leading dot - nom::multi::separated_list1(char('.'), parse_name), - )), - |(_, expr_parts)| Segment::Expression(expr_parts), - ), + map(take_until("}}"), |template| { + if let Ok(jq) = JqTransform::try_new(template) { + Segment::JqTransform(jq) + } else if let Ok((_, seg)) = parse_mustache_expression(input) { + seg + } else { + Segment::Literal(template.to_string()) + } + }), tag("}}"), )(input) } @@ -69,6 +55,36 @@ fn parse_segment(input: &str) -> IResult<&str, Vec> { } } +fn parse_mustache_expression(input: &str) -> IResult<&str, Segment> { + delimited( + tag("{{"), + map( + nom::sequence::tuple(( + nom::combinator::opt(char('.')), // Optional leading dot + nom::multi::separated_list1(char('.'), parse_name), + )), + |(_, expr_parts)| Segment::Expression(expr_parts), + ), + tag("}}"), + )(input) +} + +fn parse_name(input: &str) -> IResult<&str, String> { + let spaces = nom::character::complete::multispace0; + let alpha = nom::character::complete::alpha1; + let alphanumeric_or_underscore = nom::multi::many0(nom::branch::alt(( + nom::character::complete::alphanumeric1, + nom::bytes::complete::tag("_"), + ))); + + let parser = nom::sequence::tuple((spaces, alpha, alphanumeric_or_underscore, spaces)); + + nom::combinator::map(parser, |(_, a, b, _)| { + let b: String = b.into_iter().collect(); + format!("{}{}", a, b) + })(input) +} + fn parse_mustache(input: &str) -> IResult<&str, Mustache> { map(parse_segment, |segments| { Mustache::from(segments.into_iter().filter(|seg| match seg { @@ -226,7 +242,7 @@ mod tests { #[test] fn parse_env_name() { - let result = Mustache::parse("{{env.FOO}}"); + let result = Mustache::parse("{{.env.FOO}}"); assert_eq!( result, Mustache::from(vec![Segment::Expression(vec![ @@ -238,7 +254,7 @@ mod tests { #[test] fn parse_env_with_underscores() { - let result = Mustache::parse("{{env.FOO_BAR}}"); + let result = Mustache::parse("{{.env.FOO_BAR}}"); assert_eq!( result, Mustache::from(vec![Segment::Expression(vec![ diff --git a/src/core/mustache/render_value.rs b/src/core/mustache/render_value.rs new file mode 100644 index 0000000000..f0167bfc60 --- /dev/null +++ b/src/core/mustache/render_value.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use super::{JqRuntimeError, Mustache, PathJqValueString, Segment}; + +impl Mustache { + /// Used to render the template + pub fn render_value( + &self, + ctx: &impl PathJqValueString, + ) -> Result { + let expressions_len = self.segments().len(); + match expressions_len { + 0 => Ok(async_graphql_value::ConstValue::Null), + 1 => { + let expression = self.segments().first().unwrap(); + self.execute_expression(ctx, expression) + } + _ => { + let (errors, result): (Vec<_>, Vec<_>) = self + .segments() + .iter() + .map(|expr| self.execute_expression(ctx, expr)) + .partition(Result::is_err); + + let errors: Vec = errors + .into_iter() + .filter_map(|e| match e { + Ok(_) => None, + Err(err) => Some(err), + }) + .collect(); + if !errors.is_empty() { + return Err(JqRuntimeError::JqRuntimeErrors(errors)); + } + + let result = result + .into_iter() + .filter_map(|v| match v { + Ok(v) => Some(v), + Err(_) => None, + }) + .fold(String::new(), |mut acc, cur| { + match &cur { + async_graphql::Value::String(s) => acc += s, + _ => acc += &cur.to_string(), + } + acc + }); + Ok(async_graphql_value::ConstValue::String(result)) + } + } + } + + fn execute_expression( + &self, + ctx: &impl PathJqValueString, + expression: &Segment, + ) -> Result { + match expression { + Segment::JqTransform(jq_transform) => { + jq_transform.render_value(super::PathValueEnum::PathValue(Arc::new(ctx))) + } + Segment::Literal(value) => Ok(async_graphql_value::ConstValue::String(value.clone())), + Segment::Expression(parts) => { + let mustache_result = ctx + .path_string(parts) + .map(|a| a.to_string()) + .unwrap_or_default(); + + Ok( + serde_json::from_str::(&mustache_result) + .unwrap_or_else(|_| { + async_graphql_value::ConstValue::String(mustache_result) + }), + ) + } + } + } +} diff --git a/src/core/path.rs b/src/core/path.rs index 2c90b7cb0e..655e3986ee 100644 --- a/src/core/path.rs +++ b/src/core/path.rs @@ -128,6 +128,26 @@ impl PathValue for EvalContext<'_, Ctx> { } } +impl PathValue for serde_json::Value { + fn raw_value<'a, T: AsRef>(&'a self, path: &[T]) -> Option> { + let serde_json::Value::Object(map) = self else { + return None; + }; + + let (first, rest) = path.split_first()?; + + if rest.is_empty() { + map.get(first.as_ref()).map(|v| { + ValueString::Value(Cow::Owned( + async_graphql_value::ConstValue::from_json(v.clone()).unwrap(), + )) + }) + } else { + map.get(first.as_ref()).and_then(|v| v.raw_value(rest)) + } + } +} + impl PathString for EvalContext<'_, Ctx> { fn path_string>(&self, path: &[T]) -> Option> { self.to_raw_value(path).and_then(|value| match value { diff --git a/src/core/serde_value_ext.rs b/src/core/serde_value_ext.rs index 60485984b6..8a701b3bcc 100644 --- a/src/core/serde_value_ext.rs +++ b/src/core/serde_value_ext.rs @@ -14,7 +14,7 @@ impl ValueExt for DynamicValue { fn render_value(&self, ctx: &impl PathJqValueString) -> Result { match self { DynamicValue::Value(value) => Ok(value.to_owned()), - DynamicValue::JqTemplate(t) => t.render_value(ctx), + DynamicValue::Mustache(t) => t.render_value(ctx), DynamicValue::Object(obj) => { let out: Result, _> = obj .iter() diff --git a/tests/expression_spec.rs b/tests/expression_spec.rs index 8d4a73f16e..455ea38fe8 100644 --- a/tests/expression_spec.rs +++ b/tests/expression_spec.rs @@ -7,7 +7,7 @@ mod tests { use tailcall::core::http::RequestContext; use tailcall::core::ir::model::IR; use tailcall::core::ir::{EmptyResolverContext, Error, EvalContext}; - use tailcall::core::mustache::JqTemplate; + use tailcall::core::Mustache; async fn eval(expr: &IR) -> Result { let runtime = tailcall::cli::runtime::init(&Blueprint::default()); @@ -21,16 +21,16 @@ mod tests { async fn test_and_then() { let abcde = DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap(); let expr = IR::Dynamic(abcde) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.a}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.b}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.c}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.d}}", )))); @@ -44,7 +44,7 @@ mod tests { async fn test_with_args() { let expr = IR::Dynamic(DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap()) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.a.b.c.d}}", )))); @@ -58,16 +58,16 @@ mod tests { async fn test_with_args_piping() { let expr = IR::Dynamic(DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap()) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.a}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.b}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.c}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.d}}", )))); @@ -84,11 +84,11 @@ mod tests { let expr_with_dot = args.clone() - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{.args.a.b.c.d}}", )))); - let expr_without_dot = args.pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + let expr_without_dot = args.pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.a.b.c.d}}", )))); @@ -104,16 +104,16 @@ mod tests { async fn test_optional_dot_piping() { let expr = IR::Dynamic(DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap()) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{.args.a}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{.args.b}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{.args.c}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{.args.d}}", )))); @@ -127,16 +127,16 @@ mod tests { async fn test_mixed_dot_usages() { let expr = IR::Dynamic(DynamicValue::try_from(&json!({"a": {"b": {"c": {"d": "e"}}}})).unwrap()) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{.args.a}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.b}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{.args.c}}", )))) - .pipe(IR::Dynamic(DynamicValue::JqTemplate(JqTemplate::parse( + .pipe(IR::Dynamic(DynamicValue::Mustache(Mustache::parse( "{{args.d}}", )))); From 7571da7c8abf32003b07b99493cea245caba7284 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 12 Dec 2024 15:33:41 +0200 Subject: [PATCH 25/28] fix: remove headers and vars --- benches/data_loader_bench.rs | 4 --- ...impl_path_string_for_evaluation_context.rs | 4 --- src/cli/runtime/env.rs | 7 ----- src/core/ir/eval_context.rs | 17 ----------- src/core/mod.rs | 5 ---- src/core/mustache/jq_transform.rs | 1 + src/core/path.rs | 30 ------------------- src/core/runtime.rs | 7 ----- tailcall-aws-lambda/src/runtime.rs | 4 --- tailcall-cloudflare/src/env.rs | 4 --- tailcall-wasm/src/env.rs | 7 ----- tests/cli/gen.rs | 4 --- tests/core/env.rs | 7 ----- tests/core/parse.rs | 7 ----- tests/server_spec.rs | 7 ----- 15 files changed, 1 insertion(+), 114 deletions(-) diff --git a/benches/data_loader_bench.rs b/benches/data_loader_bench.rs index a68d25779d..d9213931f2 100644 --- a/benches/data_loader_bench.rs +++ b/benches/data_loader_bench.rs @@ -33,10 +33,6 @@ impl EnvIO for Env { fn get(&self, _: &str) -> Option> { unimplemented!("Not needed for this bench") } - - fn get_raw(&self) -> Vec<(String, String)> { - unimplemented!("Not needed for this bench") - } } struct File; diff --git a/benches/impl_path_string_for_evaluation_context.rs b/benches/impl_path_string_for_evaluation_context.rs index 6502208d34..e4eb603f67 100644 --- a/benches/impl_path_string_for_evaluation_context.rs +++ b/benches/impl_path_string_for_evaluation_context.rs @@ -84,10 +84,6 @@ impl EnvIO for Env { fn get(&self, _: &str) -> Option> { unimplemented!("Not needed for this bench") } - - fn get_raw(&self) -> Vec<(String, String)> { - unimplemented!("Not needed for this bench") - } } struct File; diff --git a/src/cli/runtime/env.rs b/src/cli/runtime/env.rs index dbf57261b4..4c436ff998 100644 --- a/src/cli/runtime/env.rs +++ b/src/cli/runtime/env.rs @@ -12,13 +12,6 @@ impl EnvIO for EnvNative { fn get(&self, key: &str) -> Option> { self.vars.get(key).map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.vars - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - } } impl EnvNative { diff --git a/src/core/ir/eval_context.rs b/src/core/ir/eval_context.rs index b260b854f9..89c03d80c6 100644 --- a/src/core/ir/eval_context.rs +++ b/src/core/ir/eval_context.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use async_graphql::{ServerError, Value}; use http::header::HeaderMap; -use indexmap::IndexMap; use super::{GraphQLOperationContext, RelatedFields, ResolverContextLike, SelectionField}; use crate::core::document::print_directives; @@ -94,22 +93,6 @@ impl<'a, Ctx: ResolverContextLike> EvalContext<'a, Ctx> { self.request_ctx.runtime.env.get(key) } - pub fn env_vars(&self) -> async_graphql_value::ConstValue { - let env = self.request_ctx.runtime.env.get_raw(); - let env: Vec<_> = env - .into_iter() - .map(|(k, v)| { - ( - async_graphql_value::Name::new(&k), - serde_json::from_str::(&v) - .unwrap_or_else(|_| async_graphql_value::ConstValue::String(v)), - ) - }) - .collect(); - let map = IndexMap::from_iter(env); - async_graphql_value::ConstValue::Object(map) - } - pub fn var(&self, key: &str) -> Option<&str> { let vars = &self.request_ctx.server.vars; diff --git a/src/core/mod.rs b/src/core/mod.rs index 1b5b4d5823..702d5a0a22 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -67,7 +67,6 @@ pub const fn default_verify_ssl() -> Option { pub trait EnvIO: Send + Sync + 'static { fn get(&self, key: &str) -> Option>; - fn get_raw(&self) -> Vec<(String, String)>; } #[async_trait::async_trait] @@ -151,10 +150,6 @@ pub mod tests { fn get(&self, key: &str) -> Option> { self.0.get(key).map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.0.iter().map(|(k, v)| (k.clone(), v.clone())).collect() - } } impl FromIterator<(String, String)> for TestEnvIO { diff --git a/src/core/mustache/jq_transform.rs b/src/core/mustache/jq_transform.rs index 2e5480e228..5e7576978b 100644 --- a/src/core/mustache/jq_transform.rs +++ b/src/core/mustache/jq_transform.rs @@ -18,6 +18,7 @@ impl JqTransform { /// Used to parse a `template` and try to convert it into a JqTemplate pub fn try_new(template: &str) -> Result { let template = template.replace("\\\"", "\""); + // the term is used because it can be easily serialized, deserialized and hashed let term = Self::parse_template(&template).map_err(JqRuntimeError::JqTemplateErrors)?; diff --git a/src/core/path.rs b/src/core/path.rs index 655e3986ee..4b86d0aa48 100644 --- a/src/core/path.rs +++ b/src/core/path.rs @@ -2,7 +2,6 @@ //! structure. use std::borrow::Cow; -use indexmap::IndexMap; use serde_json::json; use crate::core::ir::{EvalContext, ResolverContextLike}; @@ -80,28 +79,6 @@ impl EvalContext<'_, Ctx> { "vars" => Some(ValueString::Value(Cow::Owned( async_graphql_value::ConstValue::from_json(json!(ctx.vars())).unwrap(), ))), - "headers" => { - let arr = ctx - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str())) - .filter_map(|(k, v)| { - if let Ok(v) = v { - Some((async_graphql_value::Name::new(k), v)) - } else { - None - } - }) - .fold(IndexMap::new(), |mut acc, (k, v)| { - acc.insert(k, v.into()); - acc - }); - - Some(ValueString::Value(Cow::Owned( - async_graphql_value::ConstValue::object(arr), - ))) - } - "env" => Some(ValueString::Value(Cow::Owned(ctx.env_vars()))), _ => None, }; } @@ -196,13 +173,6 @@ mod tests { fn get(&self, key: &str) -> Option> { self.env.get(key).map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - } } impl Env { diff --git a/src/core/runtime.rs b/src/core/runtime.rs index 3ea193b9eb..1caf1d5e3b 100644 --- a/src/core/runtime.rs +++ b/src/core/runtime.rs @@ -166,13 +166,6 @@ pub mod test { fn get(&self, key: &str) -> Option> { self.vars.get(key).map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.vars - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - } } impl TestEnvIO { diff --git a/tailcall-aws-lambda/src/runtime.rs b/tailcall-aws-lambda/src/runtime.rs index b73a408611..9fccef6d6f 100644 --- a/tailcall-aws-lambda/src/runtime.rs +++ b/tailcall-aws-lambda/src/runtime.rs @@ -18,10 +18,6 @@ impl EnvIO for LambdaEnv { // as real env vars in the runtime. std::env::var(key).ok().map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - std::env::vars().collect() - } } pub fn init_env() -> Arc { diff --git a/tailcall-cloudflare/src/env.rs b/tailcall-cloudflare/src/env.rs index 71f6242a11..51838efac2 100644 --- a/tailcall-cloudflare/src/env.rs +++ b/tailcall-cloudflare/src/env.rs @@ -15,10 +15,6 @@ impl EnvIO for CloudflareEnv { fn get(&self, key: &str) -> Option> { self.env.var(key).ok().map(|v| Cow::from(v.to_string())) } - - fn get_raw(&self) -> Vec<(String, String)> { - unimplemented!() - } } impl CloudflareEnv { diff --git a/tailcall-wasm/src/env.rs b/tailcall-wasm/src/env.rs index 8846b2c805..604041cae0 100644 --- a/tailcall-wasm/src/env.rs +++ b/tailcall-wasm/src/env.rs @@ -20,11 +20,4 @@ impl EnvIO for WasmEnv { fn get(&self, key: &str) -> Option> { self.env.get(key).map(|v| Cow::Owned(v.value().clone())) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.env - .iter() - .map(|entry| (entry.key().clone(), entry.value().clone())) - .collect() - } } diff --git a/tests/cli/gen.rs b/tests/cli/gen.rs index 106c033cbb..4fe29d8a3e 100644 --- a/tests/cli/gen.rs +++ b/tests/cli/gen.rs @@ -153,10 +153,6 @@ pub mod env { fn get(&self, key: &str) -> Option> { self.0.get(key).map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.0.iter().map(|(k, v)| (k.clone(), v.clone())).collect() - } } } diff --git a/tests/core/env.rs b/tests/core/env.rs index 7b6d93001a..7af2cd3907 100644 --- a/tests/core/env.rs +++ b/tests/core/env.rs @@ -14,13 +14,6 @@ impl EnvIO for Env { fn get(&self, key: &str) -> Option> { self.vars.get(key).map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.vars - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - } } impl Env { diff --git a/tests/core/parse.rs b/tests/core/parse.rs index a81431a04d..a438a31e89 100644 --- a/tests/core/parse.rs +++ b/tests/core/parse.rs @@ -33,13 +33,6 @@ impl EnvIO for Env { fn get(&self, key: &str) -> Option> { self.env.get(key).map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - } } impl Env { diff --git a/tests/server_spec.rs b/tests/server_spec.rs index b888a95992..a024dad739 100644 --- a/tests/server_spec.rs +++ b/tests/server_spec.rs @@ -123,13 +123,6 @@ pub mod test { fn get(&self, key: &str) -> Option> { self.vars.get(key).map(Cow::from) } - - fn get_raw(&self) -> Vec<(String, String)> { - self.vars - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - } } impl TestEnvIO { From 2e3c8f9e38128f52149045328b3e37656e27c036 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 12 Dec 2024 16:56:20 +0200 Subject: [PATCH 26/28] fix: allow getting env and header from context --- ...ame__test__to_chat_request_conversion.snap | 2 +- src/core/mustache/eval.rs | 2 +- src/core/mustache/jq_transform.rs | 57 ++++++++++++++++++- src/core/mustache/jq_value.rs | 9 +++ src/core/path.rs | 4 +- .../snapshots/test-jq-template.md_merged.snap | 4 +- tests/execution/test-jq-template.md | 4 +- 7 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/cli/llm/snapshots/tailcall__cli__llm__infer_type_name__test__to_chat_request_conversion.snap b/src/cli/llm/snapshots/tailcall__cli__llm__infer_type_name__test__to_chat_request_conversion.snap index d50032444d..2250109952 100644 --- a/src/cli/llm/snapshots/tailcall__cli__llm__infer_type_name__test__to_chat_request_conversion.snap +++ b/src/cli/llm/snapshots/tailcall__cli__llm__infer_type_name__test__to_chat_request_conversion.snap @@ -8,7 +8,7 @@ ChatRequest { ChatMessage { role: System, content: Text( - "Given the sample schema of a GraphQL type, suggest 5 meaningful names for it.\nThe name should be concise, preferably a single word and must not be in the `ignore` list.\n\nExample Input:\n{\"ignore\":[\"User\"],\"fields\":[[\"id\",\"String\"],[\"name\",\"String\"],[\"age\",\"Int\"]]}\n\nExample Output:\n{\"suggestions\":[\"Person\",\"Profile\",\"Member\",\"Individual\",\"Contact\"]}\n\nEnsure the output is in valid JSON format.\n", + "Given the sample schema of a GraphQL type, suggest 5 meaningful names for it.\nThe name should be concise, preferably a single word and must not be in the `ignore` list.\n\nExample Input:\n\n\nExample Output:\n{\"suggestions\":[\"Person\",\"Profile\",\"Member\",\"Individual\",\"Contact\"]}\n\nEnsure the output is in valid JSON format.\n", ), extra: None, }, diff --git a/src/core/mustache/eval.rs b/src/core/mustache/eval.rs index 263eb036ad..26c56f4d0b 100644 --- a/src/core/mustache/eval.rs +++ b/src/core/mustache/eval.rs @@ -62,7 +62,7 @@ impl Eval<'_> for PathStringEval { .path_string(parts) .map(|a| a.to_string()) .unwrap_or_default(), - Segment::JqTransform(_) => panic!("Cannot eval JQ for PathString"), + Segment::JqTransform(_) => Default::default(), }) .collect() } diff --git a/src/core/mustache/jq_transform.rs b/src/core/mustache/jq_transform.rs index 5e7576978b..d49730e8d7 100644 --- a/src/core/mustache/jq_transform.rs +++ b/src/core/mustache/jq_transform.rs @@ -2,10 +2,11 @@ use std::fmt::Display; use jaq_core::load::parse::Term; use jaq_core::load::{Arena, File, Loader}; -use jaq_core::{Compiler, Ctx, Filter, Native, RcIter, ValR}; +use jaq_core::{Compiler, Ctx, Exn, Filter, Native, RcIter, ValR}; use super::PathValueEnum; use crate::core::json::JsonLike; +use crate::core::path::ValueString; /// Used to represent a JQ template. Currently used only on @expr directive. #[derive(Clone)] pub struct JqTransform { @@ -67,10 +68,62 @@ impl JqTransform { .collect::>(), ) })?; + type FnBind = Box<[jaq_core::Bind]>; + type NativeVal<'a> = Native>; + let custom_funs: [(&str, FnBind, NativeVal<'_>); 1] = [( + "from_context", + jaq_std::v(1), + Native::new(|_, mut cv: jaq_core::Cv<'_, PathValueEnum<'_>>| { + let path = match cv.0.pop_var() { + PathValueEnum::PathValue(_) => { + return Box::new(core::iter::once(Err(Exn::from(jaq_core::Error::new( + PathValueEnum::Val(jaq_json::Val::from( + "You cannot pass the context as a variable.".to_string(), + )), + ))))) + } + PathValueEnum::Val(val) => { + if let jaq_json::Val::Str(rc) = val { + rc.to_string() + } else { + return Box::new(core::iter::once(Err(Exn::from(jaq_core::Error::new(PathValueEnum::Val(jaq_json::Val::from( + "The function `from_context` receives a single string argument.".to_string(), + ))))))); + } + } + }; + + match cv.1 { + PathValueEnum::PathValue(arc) => { + let path: Vec<_> = path.split(".").collect(); + let res = arc.get_values(&path).unwrap_or_else(|| { + ValueString::Value(std::borrow::Cow::Owned( + async_graphql_value::ConstValue::Null, + )) + }); + let res = match res { + ValueString::Value(cow) => PathValueEnum::Val(jaq_json::Val::from( + cow.into_owned().into_json().unwrap(), + )), + ValueString::String(cow) => { + PathValueEnum::Val(jaq_json::Val::from(cow.to_string())) + } + }; + + Box::new(core::iter::once(Ok(res))) + } + PathValueEnum::Val(_) => Box::new(core::iter::once(Err(Exn::from( + jaq_core::Error::new(PathValueEnum::Val(jaq_json::Val::from( + "You can only use `from_context` on root level value.".to_string(), + ))), + )))), + } + }), + )]; // the AST of the operation, used to transform the data let filter = Compiler::<_, Native>::default() - .with_funs(jaq_std::funs()) + .with_funs(jaq_std::funs().chain(custom_funs)) .compile(modules) .map_err(|errs| { JqRuntimeError::JqTemplateErrors( diff --git a/src/core/mustache/jq_value.rs b/src/core/mustache/jq_value.rs index 26c921f65d..492ea29817 100644 --- a/src/core/mustache/jq_value.rs +++ b/src/core/mustache/jq_value.rs @@ -583,6 +583,7 @@ impl From for PathValueEnum<'_> { /// Used to get get keys/index out of json compatible objects like EvalContext pub trait PathJqValue { fn get_value<'a>(&'a self, index: &Val) -> Option>; + fn get_values<'a>(&'a self, index: &[&str]) -> Option>; } impl PathJqValue for EvalContext<'_, Ctx> { @@ -590,6 +591,10 @@ impl PathJqValue for EvalContext<'_, Ctx> { let Val::Str(index) = index else { return None }; self.raw_value(&[index.as_str()]) } + + fn get_values<'a>(&'a self, path: &[&str]) -> Option> { + self.raw_value(path) + } } impl PathJqValue for serde_json::Value { @@ -614,6 +619,10 @@ impl PathJqValue for serde_json::Value { _ => None, } } + + fn get_values<'a>(&'a self, _index: &[&str]) -> Option> { + todo!() + } } /// Used as a type parameter to accept objects that implement both traits diff --git a/src/core/path.rs b/src/core/path.rs index 4b86d0aa48..0d5d82786e 100644 --- a/src/core/path.rs +++ b/src/core/path.rs @@ -381,9 +381,11 @@ mod tests { Some(ValueString::String(Cow::Borrowed("var"))) ); assert_eq!(EVAL_CTX.raw_value(&["vars", "missing"]), None); + let mut map = IndexMap::new(); + map.insert(Name::new("existing"), Value::String("var".to_string())); assert_eq!( EVAL_CTX.raw_value(&["vars"]), - Some(ValueString::String(Cow::Borrowed(r#"{"existing":"var"}"#))) + Some(ValueString::Value(Cow::Owned(Value::Object(map)))) ); // envs diff --git a/tests/core/snapshots/test-jq-template.md_merged.snap b/tests/core/snapshots/test-jq-template.md_merged.snap index eaf58537f7..35e8501428 100644 --- a/tests/core/snapshots/test-jq-template.md_merged.snap +++ b/tests/core/snapshots/test-jq-template.md_merged.snap @@ -31,7 +31,7 @@ type Query { bar: Bar! @http(url: "http://upstream/foo") fizz: Fizz! @http(url: "http://upstream/foo") foo: Foo! @http(url: "http://upstream/foo") - foobar: [String!]! @expr(body: "{{ .env.FOOBAR | split(\" \") }}") - token: String! @expr(body: "{{ .headers.authorization | split(\" \") | .[1] }}") + foobar: [String!]! @expr(body: "{{ from_context(\"env.FOOBAR\") | split(\" \") }}") + token: String! @expr(body: "{{ from_context(\"headers.authorization\") | split(\" \") | .[1] }}") var: [String!]! @expr(body: "{{ .vars | .id | split(\" \") }}") } diff --git a/tests/execution/test-jq-template.md b/tests/execution/test-jq-template.md index 33d16a650d..b74a1dd435 100644 --- a/tests/execution/test-jq-template.md +++ b/tests/execution/test-jq-template.md @@ -11,8 +11,8 @@ type Query { foo: Foo! @http(url: "http://upstream/foo") bar: Bar! @http(url: "http://upstream/foo") fizz: Fizz! @http(url: "http://upstream/foo") - foobar: [String!]! @expr(body: "{{ .env.FOOBAR | split(\" \") }}") - token: String! @expr(body: "{{ .headers.authorization | split(\" \") | .[1] }}") + foobar: [String!]! @expr(body: "{{ from_context(\"env.FOOBAR\") | split(\" \") }}") + token: String! @expr(body: "{{ from_context(\"headers.authorization\") | split(\" \") | .[1] }}") var: [String!]! @expr(body: "{{ .vars | .id | split(\" \") }}") arg(text: String!): String! @expr(body: "{{ .args.text | split(\" \") | .[0] }}") } From 84cf7429ad2dfe96d9d4534b8962e66e3e7b566c Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 12 Dec 2024 16:58:47 +0200 Subject: [PATCH 27/28] fix: change priority of parsing --- ...infer_type_name__test__to_chat_request_conversion.snap | 2 +- src/core/mustache/parse.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli/llm/snapshots/tailcall__cli__llm__infer_type_name__test__to_chat_request_conversion.snap b/src/cli/llm/snapshots/tailcall__cli__llm__infer_type_name__test__to_chat_request_conversion.snap index 2250109952..d50032444d 100644 --- a/src/cli/llm/snapshots/tailcall__cli__llm__infer_type_name__test__to_chat_request_conversion.snap +++ b/src/cli/llm/snapshots/tailcall__cli__llm__infer_type_name__test__to_chat_request_conversion.snap @@ -8,7 +8,7 @@ ChatRequest { ChatMessage { role: System, content: Text( - "Given the sample schema of a GraphQL type, suggest 5 meaningful names for it.\nThe name should be concise, preferably a single word and must not be in the `ignore` list.\n\nExample Input:\n\n\nExample Output:\n{\"suggestions\":[\"Person\",\"Profile\",\"Member\",\"Individual\",\"Contact\"]}\n\nEnsure the output is in valid JSON format.\n", + "Given the sample schema of a GraphQL type, suggest 5 meaningful names for it.\nThe name should be concise, preferably a single word and must not be in the `ignore` list.\n\nExample Input:\n{\"ignore\":[\"User\"],\"fields\":[[\"id\",\"String\"],[\"name\",\"String\"],[\"age\",\"Int\"]]}\n\nExample Output:\n{\"suggestions\":[\"Person\",\"Profile\",\"Member\",\"Individual\",\"Contact\"]}\n\nEnsure the output is in valid JSON format.\n", ), extra: None, }, diff --git a/src/core/mustache/parse.rs b/src/core/mustache/parse.rs index c6b66fb834..52bcf5f8e6 100644 --- a/src/core/mustache/parse.rs +++ b/src/core/mustache/parse.rs @@ -22,11 +22,11 @@ fn parse_expression(input: &str) -> IResult<&str, Segment> { delimited( tag("{{"), map(take_until("}}"), |template| { - if let Ok(jq) = JqTransform::try_new(template) { - Segment::JqTransform(jq) - } else if let Ok((_, seg)) = parse_mustache_expression(input) { + if let Ok((_, seg)) = parse_mustache_expression(input) { seg - } else { + } else if let Ok(jq) = JqTransform::try_new(template) { + Segment::JqTransform(jq) + } else { Segment::Literal(template.to_string()) } }), From ed657051c4ced56deff82c80df2745a692f00d30 Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Thu, 12 Dec 2024 18:02:01 +0200 Subject: [PATCH 28/28] fix: lint --- src/core/mustache/parse.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/mustache/parse.rs b/src/core/mustache/parse.rs index 52bcf5f8e6..754639083e 100644 --- a/src/core/mustache/parse.rs +++ b/src/core/mustache/parse.rs @@ -26,7 +26,7 @@ fn parse_expression(input: &str) -> IResult<&str, Segment> { seg } else if let Ok(jq) = JqTransform::try_new(template) { Segment::JqTransform(jq) - } else { + } else { Segment::Literal(template.to_string()) } }),