diff --git a/crates/weaver_forge/src/jq.rs b/crates/weaver_forge/src/jq.rs index 4f607202..c152edc8 100644 --- a/crates/weaver_forge/src/jq.rs +++ b/crates/weaver_forge/src/jq.rs @@ -11,6 +11,8 @@ use jaq_core::{ }; use jaq_json::Val; +type JqFileType = (); + fn semconv_prelude() -> impl Iterator> { jaq_core::load::parse(crate::SEMCONV_JQ, |p| p.defs()) .expect("BAD WEAVER BUILD - default JQ library failed to compile") @@ -25,6 +27,7 @@ fn prepare_jq_context(params: &BTreeMap) -> (Vec = File { code: filter_expr, path: (), // ToDo - give this the weaver-config location. }; @@ -48,9 +51,10 @@ pub fn execute_jq( // parse the filter let modules = loader .load(&arena, program) - .map_err(|errors| Error::FilterError { + .map_err(load_errors) + .map_err(|e| Error::FilterError { filter: filter_expr.to_owned(), - error: format!("{errors:#?}"), + error: e, })?; let (names, values) = prepare_jq_context(params); @@ -62,15 +66,10 @@ pub fn execute_jq( // This is *NOT* a simple identity function, but a lifetime inference workaround. .with_funs(funs.map(|x| x)) .compile(modules) - .map_err(|e| { - Error::CompoundError( - e.into_iter() - .map(|(_, errors)| Error::FilterError { - filter: filter_expr.to_owned(), - error: format!("{:?}", errors), - }) - .collect(), - ) + .map_err(compile_errors) + .map_err(|e| Error::FilterError { + filter: filter_expr.to_owned(), + error: e, })?; let inputs = RcIter::new(core::iter::empty()); let ctx = Ctx::new(values, &inputs); @@ -93,6 +92,62 @@ pub fn execute_jq( Ok(serde_json::Value::Array(values)) } +// JAQ errors must be parsed and synthesized. All of this code is adapted from `jaq/src/main.rs`. + +/// Converts all errors from jaq into a single string. +fn errors_to_string>(reports: Reports) -> String { + reports.into_iter().collect() +} + +/// Turns loading errors from jaq into raw strings. +fn load_errors(errs: jaq_core::load::Errors<&str, JqFileType>) -> String { + use jaq_core::load::Error; + let errs = errs.into_iter().flat_map(|(_, err)| { + let result: Vec = match err { + Error::Io(errs) => errs.into_iter().map(|e| report_io(e)).collect(), + Error::Lex(errs) => errs.into_iter().map(|e| report_lex(e)).collect(), + Error::Parse(errs) => errs.into_iter().map(|e| report_parse(e)).collect(), + }; + result + }); + errors_to_string(errs) +} + +/// Turns compile errors from jaq into raw strings. +fn compile_errors(errs: jaq_core::compile::Errors<&str, JqFileType>) -> String { + let errs = errs + .into_iter() + .flat_map(|(_, errs)| errs.into_iter().map(|e| report_compile(e))); + errors_to_string(errs) +} + +/// Turns IO errors from JQ into raw strings. +fn report_io((path, error): (&str, String)) -> String { + format!("could not load file {}: {}", path, error) +} + +/// Turns lexing errors from JQ into raw strings. +fn report_lex((expected, _): jaq_core::load::lex::Error<&str>) -> String { + format!("expected {}", expected.as_str()) +} + +/// Turns parsing errors from JQ into raw strings. +fn report_parse((expected, _): jaq_core::load::parse::Error<&str>) -> String { + format!("expected {}", expected.as_str()) +} + +/// Turns erros coming from JAQ compile phase into raw strings. +fn report_compile((found, undefined): jaq_core::compile::Error<&str>) -> String { + use jaq_core::compile::Undefined::Filter; + let wnoa = |exp, got| format!("wrong number of arguments (expected {exp}, found {got})"); + let message = match (found, undefined) { + ("reduce", Filter(arity)) => wnoa("2", arity), + ("foreach", Filter(arity)) => wnoa("2 or 3", arity), + (_, undefined) => format!("undefined {}", undefined.as_str()), + }; + message +} + #[cfg(test)] mod tests { use serde_json::json; @@ -126,4 +181,46 @@ mod tests { let result = execute_jq(&input, "$ctx1", &values).unwrap(); assert_eq!(result, values["ctx1"]); } + + #[test] + fn test_lex_error() { + let input = json!({}); + let values = BTreeMap::new(); + let error = execute_jq(&input, "(", &values) + .err() + .expect("Should have failed to lex"); + let msg = format!("{error}"); + assert!( + msg.contains("expected closing parenthesis"), + "Expected lex error {msg}" + ); + } + + #[test] + fn test_parse_error() { + let input = json!({}); + let values = BTreeMap::new(); + let error = execute_jq(&input, "if false then .", &values) + .err() + .expect("Should have failed to parse"); + let msg = format!("{error}"); + assert!( + msg.contains("expected else or end"), + "Expected parse error {msg}" + ); + } + + #[test] + fn test_compile_error() { + let input = json!({}); + let values = BTreeMap::new(); + let error = execute_jq(&input, ".x | de", &values) + .err() + .expect("Should have failed to parse"); + let msg = format!("{error}"); + assert!( + msg.contains("undefined filter"), + "Expected compile error {msg}" + ); + } } diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index 9b1fd413..919432a5 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -739,7 +739,7 @@ mod tests { let loader = FileSystemFileLoader::try_new("templates".into(), target) .expect("Failed to create file system loader"); let config = WeaverConfig::try_from_path(format!("templates/{}", target)).unwrap(); - let mut engine = TemplateEngine::new(config, loader, cli_params); + let engine = TemplateEngine::new(config, loader, cli_params); let schema = SchemaResolver::resolve_semantic_convention_registry(&mut registry) .expect("Failed to resolve registry");