Skip to content

Instantly share code, notes, and snippets.

@hhalex
Created December 15, 2023 14:40
Show Gist options
  • Save hhalex/aac4717c74b4de575602061f185b12e9 to your computer and use it in GitHub Desktop.
Save hhalex/aac4717c74b4de575602061f185b12e9 to your computer and use it in GitHub Desktop.
Handlebars rust template helper to execute xpath queries against xml documents
use handlebars::{
to_json, BlockContext, BlockParams, Context, Handlebars, Helper, HelperDef, HelperResult,
Output, PathAndJson, RenderContext, RenderError, RenderErrorReason, Renderable,
};
use serde_json::value::Value as Json;
use std::collections::HashMap;
use std::fmt::format;
use sxd_document::parser;
use sxd_xpath::evaluate_xpath;
use sxd_xpath::nodeset::Node;
fn create_block<'rc>(param: &PathAndJson<'rc>) -> BlockContext<'rc> {
let mut block = BlockContext::new();
if let Some(new_path) = param.context_path() {
*block.base_path_mut() = new_path.clone();
} else {
// use clone for now
block.set_base_value(param.value().clone());
}
block
}
#[inline]
fn copy_on_push_vec<T>(input: &[T], el: T) -> Vec<T>
where
T: Clone,
{
let mut new_vec = Vec::with_capacity(input.len() + 1);
new_vec.extend_from_slice(input);
new_vec.push(el);
new_vec
}
#[inline]
fn extend(base: &Vec<String>, slice: &[String]) -> Vec<String> {
let mut v: Vec<String> = base.clone();
for i in slice {
v.push(i.to_owned());
}
v
}
fn update_block_context(
block: &mut BlockContext<'_>,
base_path: &Option<Vec<String>>,
relative_path: String,
is_first: bool,
value: &Json,
) {
if let Some(p) = base_path {
if is_first {
*block.base_path_mut() = copy_on_push_vec(p, relative_path);
} else if let Some(ptr) = block.base_path_mut().last_mut() {
*ptr = relative_path;
}
} else {
block.set_base_value(value.clone());
}
}
fn set_block_param<'rc>(
block: &mut BlockContext<'rc>,
h: &Helper<'rc>,
base_path: &Option<Vec<String>>,
k: &Json,
v: &Json,
) -> Result<(), RenderError> {
if let Some(bp_val) = h.block_param() {
let mut params = BlockParams::new();
if base_path.is_some() {
params.add_path(bp_val, Vec::with_capacity(0))?;
} else {
params.add_value(bp_val, v.clone())?;
}
block.set_block_params(params);
} else if let Some((bp_val, bp_key)) = h.block_param_pair() {
let mut params = BlockParams::new();
if base_path.is_some() {
params.add_path(bp_val, Vec::with_capacity(0))?;
} else {
params.add_value(bp_val, v.clone())?;
}
params.add_value(bp_key, k.clone())?;
block.set_block_params(params);
}
Ok(())
}
#[derive(Clone, Copy)]
pub struct XpathHelper;
impl HelperDef for XpathHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'rc>,
r: &'reg Handlebars<'reg>,
ctx: &'rc Context,
rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let body = h
.param(0)
.ok_or(RenderErrorReason::ParamNotFoundForIndex("xpath", 0))?;
let query = h
.param(1)
.ok_or(RenderErrorReason::ParamNotFoundForIndex("xpath", 1))?;
let template = h.template();
match template {
Some(t) => match *body.value() {
Json::String(ref body_str)
if !body_str.is_empty() || (body_str.is_empty() && h.inverse().is_none()) =>
{
let block_context = create_block(body);
rc.push_block(block_context);
let package = parser::parse(body_str)
.ok()
.ok_or(RenderErrorReason::Other("problem parsing xml".to_string()))?;
let document = package.as_document();
let query_xpath = match *query.value() {
Json::String(ref query_str) => Ok(query_str),
_ => Err(RenderErrorReason::InvalidParamType(
"xpath query must be a string",
)),
}
.ok()
.ok_or(RenderErrorReason::Other(
"problem parsing xpath query".to_string(),
))?;
let value = evaluate_xpath(&document, query_xpath).ok().ok_or(
RenderErrorReason::Other("problem evaluating xpath query".to_string()),
)?;
match value {
sxd_xpath::Value::Nodeset(nodeset) => {
let len = nodeset.size();
let array_path = body
.context_path()
.map(move |v| extend(v, &[query_xpath.clone()]));
for (i, v) in nodeset.iter().enumerate().take(len) {
if let Some(ref mut block) = rc.block_mut() {
let is_first = i == 0usize;
let is_last = i == len - 1;
let el_json = to_json(
v.element()
.unwrap()
.attributes()
.iter()
.map(|attr| {
(attr.name().local_part().to_string(), attr.value())
})
.collect::<HashMap<String, &str>>(),
);
let index = to_json(i);
block.set_local_var("first", to_json(is_first));
block.set_local_var("last", to_json(is_last));
block.set_local_var("index", index.clone());
block.set_local_var("el", el_json.clone());
update_block_context(
block,
&array_path,
i.to_string(),
is_first,
&el_json,
);
set_block_param(block, h, &array_path, &index, &el_json)?;
dbg!(block.clone());
}
t.render(r, ctx, rc, out)?;
}
}
sxd_xpath::Value::String(v) => {
if let Some(ref mut block) = rc.block_mut() {
block.set_local_var("this", to_json(v));
}
t.render(r, ctx, rc, out)?;
}
sxd_xpath::Value::Number(v) => {
if let Some(ref mut block) = rc.block_mut() {
block.set_local_var("this", to_json(v));
}
t.render(r, ctx, rc, out)?;
}
sxd_xpath::Value::Boolean(v) => {
if let Some(ref mut block) = rc.block_mut() {
block.set_local_var("this", to_json(v));
}
t.render(r, ctx, rc, out)?;
}
}
rc.pop_block();
Ok(())
}
_ => {
if let Some(else_template) = h.inverse() {
else_template.render(r, ctx, rc, out)
} else if r.strict_mode() {
Err(RenderError::strict_error(body.relative_path()))
} else {
Ok(())
}
}
},
None => Ok(()),
}
}
}
pub static XPATH_HELPER: XpathHelper = XpathHelper;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment