mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 22:47:43 +02:00
Extract out xpath to a plugin. (#2661)
This commit is contained in:
committed by
GitHub
parent
2573441e28
commit
4e931fa73f
4
crates/nu_plugin_xpath/src/lib.rs
Normal file
4
crates/nu_plugin_xpath/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod nu;
|
||||
mod xpath;
|
||||
|
||||
pub use xpath::Xpath;
|
6
crates/nu_plugin_xpath/src/main.rs
Normal file
6
crates/nu_plugin_xpath/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_xpath::Xpath;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut Xpath::new());
|
||||
}
|
49
crates/nu_plugin_xpath/src/nu/mod.rs
Normal file
49
crates/nu_plugin_xpath/src/nu/mod.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{
|
||||
CallInfo, Primitive, ReturnSuccess, ReturnValue, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::TaggedItem;
|
||||
|
||||
use crate::{xpath::string_to_value, Xpath};
|
||||
|
||||
impl Plugin for Xpath {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("xpath")
|
||||
.desc("execute xpath query on xml")
|
||||
.required("query", SyntaxShape::String, "xpath query")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
let tag = call_info.name_tag;
|
||||
|
||||
let query = call_info.args.nth(0).ok_or_else(|| {
|
||||
ShellError::labeled_error("xpath query not passed", "xpath query not passed", &tag)
|
||||
})?;
|
||||
|
||||
self.query = query.as_string()?;
|
||||
self.tag = tag;
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
match input {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(s)),
|
||||
..
|
||||
} => Ok(string_to_value(s, (*self.query).tagged(&self.tag))?
|
||||
.into_iter()
|
||||
.map(ReturnSuccess::value)
|
||||
.collect()),
|
||||
Value { tag, .. } => Err(ShellError::labeled_error_with_secondary(
|
||||
"Expected text from pipeline",
|
||||
"requires text input",
|
||||
&self.tag,
|
||||
"value originates from here",
|
||||
tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
166
crates/nu_plugin_xpath/src/xpath.rs
Normal file
166
crates/nu_plugin_xpath/src/xpath.rs
Normal file
@ -0,0 +1,166 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value};
|
||||
use nu_source::{Tag, Tagged};
|
||||
|
||||
use bigdecimal::{BigDecimal, FromPrimitive};
|
||||
|
||||
use sxd_document::parser;
|
||||
use sxd_xpath::{Context, Factory};
|
||||
|
||||
pub struct Xpath {
|
||||
pub query: String,
|
||||
pub tag: Tag,
|
||||
}
|
||||
|
||||
impl Xpath {
|
||||
pub fn new() -> Xpath {
|
||||
Xpath {
|
||||
query: String::new(),
|
||||
tag: Tag::unknown(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Xpath {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string_to_value(raw: String, query: Tagged<&str>) -> Result<Vec<Value>, ShellError> {
|
||||
execute_xpath_query(raw, query.item.to_string(), query.tag())
|
||||
}
|
||||
|
||||
fn execute_xpath_query(
|
||||
input_string: String,
|
||||
query_string: String,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Vec<Value>, ShellError> {
|
||||
let tag = tag.into();
|
||||
let xpath = build_xpath(&query_string)?;
|
||||
|
||||
let package = parser::parse(&input_string);
|
||||
|
||||
if package.is_err() {
|
||||
return Err(ShellError::labeled_error(
|
||||
"invalid xml document",
|
||||
"invalid xml document",
|
||||
tag.span,
|
||||
));
|
||||
}
|
||||
|
||||
let package = package.expect("invalid xml document");
|
||||
|
||||
let document = package.as_document();
|
||||
let context = Context::new();
|
||||
|
||||
// leaving this here for augmentation at some point
|
||||
// build_variables(&arguments, &mut context);
|
||||
// build_namespaces(&arguments, &mut context);
|
||||
|
||||
let res = xpath.evaluate(&context, document.root());
|
||||
|
||||
// Some xpath statements can be long, so let's truncate it with ellipsis
|
||||
let mut key = query_string.clone();
|
||||
if query_string.len() >= 20 {
|
||||
key.truncate(17);
|
||||
key += "...";
|
||||
} else {
|
||||
key = query_string;
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(r) => {
|
||||
let rows: Vec<Value> = match r {
|
||||
sxd_xpath::Value::Nodeset(ns) => ns
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
let mut row = TaggedDictBuilder::new(Tag::unknown());
|
||||
row.insert_value(&key, UntaggedValue::string(a.string_value()));
|
||||
row.into_value()
|
||||
})
|
||||
.collect::<Vec<Value>>(),
|
||||
sxd_xpath::Value::Boolean(b) => {
|
||||
let mut row = TaggedDictBuilder::new(Tag::unknown());
|
||||
row.insert_value(&key, UntaggedValue::boolean(b));
|
||||
vec![row.into_value()]
|
||||
}
|
||||
sxd_xpath::Value::Number(n) => {
|
||||
let mut row = TaggedDictBuilder::new(Tag::unknown());
|
||||
row.insert_value(
|
||||
&key,
|
||||
UntaggedValue::decimal(BigDecimal::from_f64(n).expect("error with f64"))
|
||||
.into_untagged_value(),
|
||||
);
|
||||
|
||||
vec![row.into_value()]
|
||||
}
|
||||
sxd_xpath::Value::String(s) => {
|
||||
let mut row = TaggedDictBuilder::new(Tag::unknown());
|
||||
row.insert_value(&key, UntaggedValue::string(s));
|
||||
vec![row.into_value()]
|
||||
}
|
||||
};
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
Err(_) => Err(ShellError::labeled_error(
|
||||
"xpath query error",
|
||||
"xpath query error",
|
||||
tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_xpath(xpath_str: &str) -> Result<sxd_xpath::XPath, ShellError> {
|
||||
let factory = Factory::new();
|
||||
|
||||
match factory.build(xpath_str) {
|
||||
Ok(xpath) => xpath.ok_or_else(|| ShellError::untagged_runtime_error("invalid xpath query")),
|
||||
Err(_) => Err(ShellError::untagged_runtime_error(
|
||||
"expected valid xpath query",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::string_to_value as query;
|
||||
use nu_errors::ShellError;
|
||||
use nu_source::{Span, TaggedItem};
|
||||
use nu_test_support::value::{decimal_from_float, row};
|
||||
|
||||
use indexmap::indexmap;
|
||||
|
||||
#[test]
|
||||
fn position_function_in_predicate() -> Result<(), ShellError> {
|
||||
let text = String::from(r#"<?xml version="1.0" encoding="UTF-8"?><a><b/><b/></a>"#);
|
||||
|
||||
let actual = query(text, "count(//a/*[position() = 2])".tagged_unknown())?;
|
||||
|
||||
assert_eq!(
|
||||
actual[0],
|
||||
row(
|
||||
indexmap! { "count(//a/*[posit...".into() => decimal_from_float(1.0, Span::unknown()) }
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn functions_implicitly_coerce_argument_types() -> Result<(), ShellError> {
|
||||
let text = String::from(r#"<?xml version="1.0" encoding="UTF-8"?><a>true</a>"#);
|
||||
|
||||
let actual = query(text, "count(//*[contains(., true)])".tagged_unknown())?;
|
||||
|
||||
assert_eq!(
|
||||
actual[0],
|
||||
row(
|
||||
indexmap! { "count(//*[contain...".into() => decimal_from_float(1.0, Span::unknown()) }
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user