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#'
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:dc="http://purl.org/dc/elements/1.1/"
         <dc:title>Black-breasted buzzard_AEB_IMG_7158</dc:title>
      </rdf:Description>
   </rdf:RDF>
'# | 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.
This commit is contained in:
Bruce Weirdan 2025-06-20 00:58:26 +02:00 committed by GitHub
parent 975a89269e
commit 1d032ce80c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -22,6 +22,12 @@ impl SimplePluginCommand for QueryXml {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build(self.name()) Signature::build(self.name())
.required("query", SyntaxShape::String, "xpath query") .required("query", SyntaxShape::String, "xpath query")
.named(
"namespaces",
SyntaxShape::Record(vec![]),
"map of prefixes to namespace URIs",
Some('n'),
)
.category(Category::Filters) .category(Category::Filters)
} }
@ -33,8 +39,9 @@ impl SimplePluginCommand for QueryXml {
input: &Value, input: &Value,
) -> Result<Value, LabeledError> { ) -> Result<Value, LabeledError> {
let query: Option<Spanned<String>> = call.opt(0)?; let query: Option<Spanned<String>> = call.opt(0)?;
let namespaces: Option<Record> = call.get_flag::<Record>("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, call: &EvaluatedCall,
input: &Value, input: &Value,
query: Option<Spanned<String>>, query: Option<Spanned<String>>,
namespaces: Option<Record>,
) -> Result<Value, LabeledError> { ) -> Result<Value, LabeledError> {
let (query_string, span) = match &query { let (query_string, span) = match &query {
Some(v) => (&v.item, v.span), Some(v) => (&v.item, v.span),
@ -65,7 +73,11 @@ pub fn execute_xpath_query(
let package = package.expect("invalid xml document"); let package = package.expect("invalid xml document");
let document = package.as_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 // leaving this here for augmentation at some point
// build_variables(&arguments, &mut context); // build_variables(&arguments, &mut context);
@ -152,7 +164,8 @@ mod tests {
span: Span::test_data(), 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( let expected = Value::list(
vec![Value::test_record(record! { vec![Value::test_record(record! {
"count(//a/*[posit..." => Value::test_float(1.0), "count(//a/*[posit..." => Value::test_float(1.0),
@ -181,7 +194,8 @@ mod tests {
span: Span::test_data(), 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( let expected = Value::list(
vec![Value::test_record(record! { vec![Value::test_record(record! {
"count(//*[contain..." => Value::test_float(1.0), "count(//*[contain..." => Value::test_float(1.0),
@ -191,4 +205,42 @@ mod tests {
assert_eq!(actual, expected); 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#"<?xml version="1.0" encoding="UTF-8"?><a xmlns:dp="http://example.com/ns"><dp:b>yay</dp:b></a>"#,
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<String> = 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);
}
} }