use std::fmt;
use crate::document::XmlDocument;
use crate::error::Result;
use crate::xpath::{AsQuery, Expr, Query, XPathEvaluator, XPathResult, XPathSource, parse_xpath};
use super::error::{TransformError, TransformResult};
use super::xpath_analyze::{XPathAnalysis, analyze_xpath};
#[derive(Debug, Clone)]
pub struct StreamableQuery {
expr: Expr,
}
impl StreamableQuery {
pub fn compile(xpath: &str) -> TransformResult<Self> {
let expr = parse_xpath(xpath)?;
match analyze_xpath(&expr) {
XPathAnalysis::Streamable(_) => Ok(Self { expr }),
XPathAnalysis::NotStreamable(reason) => Err(TransformError::NotStreamable {
xpath: xpath.to_string(),
reason,
}),
}
}
}
impl From<StreamableQuery> for Query {
fn from(streamable: StreamableQuery) -> Self {
Query::from_expr(streamable.expr)
}
}
impl From<&StreamableQuery> for Query {
fn from(streamable: &StreamableQuery) -> Self {
Query::from_expr(streamable.expr.clone())
}
}
impl AsQuery for StreamableQuery {
fn eval_on(&self, doc: &XmlDocument) -> Result<XPathResult> {
XPathEvaluator::new(doc).evaluate_expr(&self.expr)
}
}
impl TryFrom<&Query> for StreamableQuery {
type Error = TransformError;
fn try_from(query: &Query) -> TransformResult<Self> {
let expr = query.expr();
match analyze_xpath(expr) {
XPathAnalysis::Streamable(_) => Ok(Self { expr: expr.clone() }),
XPathAnalysis::NotStreamable(reason) => Err(TransformError::NotStreamable {
xpath: query.to_string(),
reason,
}),
}
}
}
impl fmt::Display for StreamableQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.expr.fmt(f)
}
}
impl TryFrom<Query> for StreamableQuery {
type Error = TransformError;
fn try_from(query: Query) -> TransformResult<Self> {
StreamableQuery::try_from(&query)
}
}
pub trait IntoStreamable {
fn into_xpath_source(self) -> XPathSource;
}
impl IntoStreamable for &str {
fn into_xpath_source(self) -> XPathSource {
XPathSource::String(self.to_string())
}
}
impl IntoStreamable for String {
fn into_xpath_source(self) -> XPathSource {
XPathSource::String(self)
}
}
impl IntoStreamable for &String {
fn into_xpath_source(self) -> XPathSource {
XPathSource::String(self.clone())
}
}
impl IntoStreamable for StreamableQuery {
fn into_xpath_source(self) -> XPathSource {
XPathSource::Ast(self.expr)
}
}
impl IntoStreamable for &StreamableQuery {
fn into_xpath_source(self) -> XPathSource {
XPathSource::Ast(self.expr.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transform::Transformer;
#[test]
fn compile_accepts_streamable() {
assert!(StreamableQuery::compile("//item").is_ok());
assert!(StreamableQuery::compile("//item[@id='2']").is_ok());
}
#[test]
fn compile_rejects_non_streamable() {
assert!(StreamableQuery::compile("//item[last()]").is_err());
assert!(StreamableQuery::compile("//item/parent::*").is_err());
}
#[test]
fn compile_rejects_invalid_syntax() {
assert!(StreamableQuery::compile("///bad[").is_err());
}
#[test]
fn transformer_accepts_str_and_compiled() {
let xml = r#"<root><item/><item/></root>"#;
let a = Transformer::from(xml)
.on("//item", |n| n.set_attribute("a", "1"))
.to_string()
.unwrap();
assert_eq!(a.matches(r#"a="1""#).count(), 2);
let q = StreamableQuery::compile("//item").unwrap();
let b = Transformer::from(xml)
.on(&q, |n| n.set_attribute("b", "1"))
.to_string()
.unwrap();
assert_eq!(b.matches(r#"b="1""#).count(), 2);
}
#[test]
fn streamable_to_query_and_eval() {
use crate::{Parser, Query, QueryExt};
let sq = StreamableQuery::compile("//item").unwrap();
let doc = Parser::from("<root><item/><item/><item/></root>")
.parse()
.unwrap();
let q: Query = (&sq).into();
assert_eq!(q.find_nodes(&doc).unwrap().len(), 3);
assert_eq!(doc.query_nodes(&sq).unwrap().len(), 3);
}
#[test]
fn query_to_streamable_is_fallible() {
use crate::Query;
let ok = Query::compile("//item").unwrap();
assert!(StreamableQuery::try_from(&ok).is_ok());
let bad = Query::compile("//item[last()]").unwrap();
let err = StreamableQuery::try_from(&bad).unwrap_err();
assert!(format!("{err}").contains("//item[last()]"));
}
#[test]
fn streamable_to_string_roundtrips() {
let sq = StreamableQuery::compile("//item[@id='2']").unwrap();
assert_eq!(sq.to_string(), "//item[@id='2']");
assert!(StreamableQuery::compile(&sq.to_string()).is_ok());
}
#[test]
fn transformer_collect_accepts_compiled() {
let xml = r#"<root><item id="1"/><item id="2"/></root>"#;
let q = StreamableQuery::compile("//item").unwrap();
let ids: Vec<String> = Transformer::from(xml)
.collect(&q, |n| n.get_attribute("id").unwrap_or_default())
.unwrap();
assert_eq!(ids, vec!["1", "2"]);
}
}