Extract out xpath to a plugin. (#2661)

This commit is contained in:
Andrés N. Robalino
2020-10-12 18:18:39 -05:00
committed by GitHub
parent 2573441e28
commit 4e931fa73f
14 changed files with 292 additions and 203 deletions

View File

@ -245,7 +245,6 @@ pub fn create_default_context(interactive: bool) -> Result<EvaluationContext, Bo
whole_stream_command(FromURL),
whole_stream_command(FromXLSX),
whole_stream_command(FromXML),
whole_stream_command(XPath),
whole_stream_command(FromYAML),
whole_stream_command(FromYML),
whole_stream_command(FromIcs),

View File

@ -126,7 +126,6 @@ pub(crate) mod where_;
pub(crate) mod which_;
pub(crate) mod with_env;
pub(crate) mod wrap;
pub(crate) mod xpath;
pub(crate) use autoview::Autoview;
pub(crate) use cd::Cd;
@ -271,7 +270,6 @@ pub(crate) use where_::Where;
pub(crate) use which_::Which;
pub(crate) use with_env::WithEnv;
pub(crate) use wrap::Wrap;
pub(crate) use xpath::XPath;
#[cfg(test)]
mod tests {

View File

@ -1,150 +0,0 @@
extern crate sxd_document;
extern crate sxd_xpath;
use crate::commands::WholeStreamCommand;
use crate::prelude::*;
use bigdecimal::FromPrimitive;
use nu_errors::ShellError;
use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value};
use nu_source::Tagged;
use sxd_document::parser;
use sxd_xpath::{Context, Factory};
pub struct XPath;
#[derive(Deserialize)]
struct XPathArgs {
query: Tagged<String>,
}
#[async_trait]
impl WholeStreamCommand for XPath {
fn name(&self) -> &str {
"xpath"
}
fn signature(&self) -> Signature {
Signature::build("xpath").required("query", SyntaxShape::String, "xpath query")
}
fn usage(&self) -> &str {
"execute xpath query on xml"
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "find items with name attribute",
example: r#"echo '<?xml version="1.0" encoding="UTF-8"?><main><nushell rocks="true"/></main>' | from xml | to xml | xpath '//nushell/@rocks'"#,
result: None,
}]
}
async fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let tag = args.call_info.name_tag.clone();
let (XPathArgs { query }, input) = args.process(&registry).await?;
let query_string = query.as_str();
let input_string = input.collect_string(tag.clone()).await?.item;
let result_string = execute_xpath_query(input_string, query_string.to_string());
match result_string {
Some(r) => Ok(
futures::stream::iter(r.into_iter().map(ReturnSuccess::value)).to_output_stream(),
),
None => Err(ShellError::labeled_error(
"xpath query error",
"xpath query error",
query.tag(),
)),
}
}
}
pub fn execute_xpath_query(input_string: String, query_string: String) -> Option<Vec<Value>> {
let xpath = build_xpath(&query_string);
let package = parser::parse(&input_string).expect("failed to parse xml");
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()]
}
};
if !rows.is_empty() {
Some(rows)
} else {
None
}
}
Err(_) => None,
}
}
fn build_xpath(xpath_str: &str) -> sxd_xpath::XPath {
let factory = Factory::new();
factory
.build(xpath_str)
.unwrap_or_else(|e| panic!("Unable to compile XPath {}: {}", xpath_str, e))
.expect("error with building the xpath factory")
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::XPath;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
Ok(test_examples(XPath {})?)
}
}

View File

@ -1,41 +0,0 @@
use nu_test_support::fs::Stub::FileWithContentToBeTrimmed;
use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline};
#[test]
fn position_function_in_predicate() {
let actual = nu!(
cwd: ".", pipeline(
r#"
echo "<?xml version="1.0" encoding="UTF-8"?><a><b/><b/></a>" | from xml | to xml | xpath "count(//a/*[position() = 2])"
"#
));
assert_eq!(actual.out, "1.0000");
}
#[test]
fn functions_implicitly_coerce_argument_types() {
let actual = nu!(
cwd: ".", pipeline(
r#"
echo "<?xml version="1.0" encoding="UTF-8"?><a>true</a>" | from xml | to xml | xpath "count(//*[contains(., true)])"
"#
));
assert_eq!(actual.out, "1.0000");
}
#[test]
fn find_guid_permilink_is_true() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
open jonathan.xml
| to xml
| xpath '//guid/@isPermaLink'
"#
));
assert_eq!(actual.out, "true");
}

View File

@ -0,0 +1,24 @@
[package]
authors = ["The Nu Project Contributors"]
description = "Traverses xml"
edition = "2018"
license = "MIT"
name = "nu_plugin_xpath"
version = "0.20.0"
[lib]
doctest = false
[dependencies]
nu-plugin = {path = "../nu-plugin", version = "0.20.0"}
nu-errors = {version = "0.20.0", path = "../nu-errors"}
nu-protocol = {version = "0.20.0", path = "../nu-protocol"}
nu-source = {version = "0.20.0", path = "../nu-source"}
sxd-xpath = "0.4.2"
sxd-document = "0.3.2"
bigdecimal = {version = "0.2.0", features = ["serde"]}
indexmap = {version = "1.6.0", features = ["serde-1"]}
[dev-dependencies]
nu-test-support = {path = "../nu-test-support", version = "0.20.0"}

View File

@ -0,0 +1,4 @@
mod nu;
mod xpath;
pub use xpath::Xpath;

View File

@ -0,0 +1,6 @@
use nu_plugin::serve_plugin;
use nu_plugin_xpath::Xpath;
fn main() {
serve_plugin(&mut Xpath::new());
}

View 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,
)),
}
}
}

View 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(())
}
}