From 1d032ce80c7bd3da78ea173ded35133b3f7ac4ae Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Fri, 20 Jun 2025 00:58:26 +0200 Subject: [PATCH] Support namespaces in `query xml` (#16008) Refs #15992 Refs #14457 # Description This PR introduces a new switch for `query xml`, `--namespaces`, and thus allows people to use namespace prefixes in the XPath query to query namespaced XML. Example: ```nushell r#' Black-breasted buzzard_AEB_IMG_7158 '# | query xml --namespaces {dublincore: "http://purl.org/dc/elements/1.1/"} "//dublincore:title/text()" ``` # User-Facing Changes New switch added to `query xml`: `query xml --namespaces {....}` # Tests + Formatting Pass. # After Submitting IIRC the commands docs on the website are automatically generated, so nothing to do here. --- crates/nu_plugin_query/src/query_xml.rs | 60 +++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/crates/nu_plugin_query/src/query_xml.rs b/crates/nu_plugin_query/src/query_xml.rs index ae45859ba4..11ed39299e 100644 --- a/crates/nu_plugin_query/src/query_xml.rs +++ b/crates/nu_plugin_query/src/query_xml.rs @@ -22,6 +22,12 @@ impl SimplePluginCommand for QueryXml { fn signature(&self) -> Signature { Signature::build(self.name()) .required("query", SyntaxShape::String, "xpath query") + .named( + "namespaces", + SyntaxShape::Record(vec![]), + "map of prefixes to namespace URIs", + Some('n'), + ) .category(Category::Filters) } @@ -33,8 +39,9 @@ impl SimplePluginCommand for QueryXml { input: &Value, ) -> Result { let query: Option> = call.opt(0)?; + let namespaces: Option = call.get_flag::("namespaces")?; - execute_xpath_query(call, input, query) + execute_xpath_query(call, input, query, namespaces) } } @@ -42,6 +49,7 @@ pub fn execute_xpath_query( call: &EvaluatedCall, input: &Value, query: Option>, + namespaces: Option, ) -> Result { let (query_string, span) = match &query { Some(v) => (&v.item, v.span), @@ -65,7 +73,11 @@ pub fn execute_xpath_query( let package = package.expect("invalid xml document"); let document = package.as_document(); - let context = Context::new(); + let mut context = Context::new(); + + for (prefix, uri) in namespaces.unwrap_or_default().into_iter() { + context.set_namespace(prefix.as_str(), uri.into_string()?.as_str()); + } // leaving this here for augmentation at some point // build_variables(&arguments, &mut context); @@ -152,7 +164,8 @@ mod tests { span: Span::test_data(), }; - let actual = query(&call, &text, Some(spanned_str)).expect("test should not fail"); + let actual = query(&call, &text, Some(spanned_str), None).expect("test should not fail"); + let expected = Value::list( vec![Value::test_record(record! { "count(//a/*[posit..." => Value::test_float(1.0), @@ -181,7 +194,8 @@ mod tests { span: Span::test_data(), }; - let actual = query(&call, &text, Some(spanned_str)).expect("test should not fail"); + let actual = query(&call, &text, Some(spanned_str), None).expect("test should not fail"); + let expected = Value::list( vec![Value::test_record(record! { "count(//*[contain..." => Value::test_float(1.0), @@ -191,4 +205,42 @@ mod tests { assert_eq!(actual, expected); } + + #[test] + fn namespaces_are_used() { + let call = EvaluatedCall { + head: Span::test_data(), + positional: vec![], + named: vec![], + }; + + // document uses `dp` ("document prefix") as a prefix + let text = Value::string( + r#"yay"#, + Span::test_data(), + ); + + // but query uses `qp` ("query prefix") as a prefix + let namespaces = record! { + "qp" => Value::string("http://example.com/ns", Span::test_data()), + }; + + let spanned_str: Spanned = Spanned { + item: "//qp:b/text()".to_string(), + span: Span::test_data(), + }; + + let actual = + query(&call, &text, Some(spanned_str), Some(namespaces)).expect("test should not fail"); + + let expected = Value::list( + vec![Value::test_record(record! { + "//qp:b/text()" => Value::string("yay", Span::test_data()), + })], + Span::test_data(), + ); + + // and yet it should work regardless + assert_eq!(actual, expected); + } }