use std::collections::{BTreeSet, HashMap};
use std::sync::Mutex;
use super::value::Value;
#[cfg(test)]
use super::value::NumericType;
use super::version::XPathVersion;
use crate::xml::{Document, NodeId};
#[derive(Debug, Default)]
pub struct Documents {
loaded: HashMap<Request, NodeId>,
origins: HashMap<NodeId, String>,
missing: Mutex<BTreeSet<Request>>,
}
pub type Request = (String, Option<String>);
impl Documents {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&mut self,
uri: impl Into<String>,
base: Option<String>,
origin: impl Into<String>,
root: NodeId,
) {
self.loaded.insert((uri.into(), base), root);
self.origins.insert(root, origin.into());
}
#[must_use]
pub fn lookup(&self, uri: &str, base: Option<&str>) -> Option<NodeId> {
let request = (uri.to_string(), base.map(ToString::to_string));
if let Some(root) = self.loaded.get(&request) {
return Some(*root);
}
self.record_missing(request);
None
}
#[must_use]
pub fn origin_of(&self, root: NodeId) -> Option<&str> {
self.origins.get(&root).map(String::as_str)
}
fn record_missing(&self, request: Request) {
if let Ok(mut missing) = self.missing.lock() {
missing.insert(request);
}
}
#[must_use]
pub fn missing(&self) -> Vec<Request> {
self.missing
.lock()
.map(|missing| missing.iter().cloned().collect())
.unwrap_or_default()
}
pub fn clear_missing(&self) {
if let Ok(mut missing) = self.missing.lock() {
missing.clear();
}
}
#[must_use]
pub fn len(&self) -> usize {
self.loaded.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.loaded.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct Namespaces {
bindings: HashMap<String, String>,
}
impl Namespaces {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, prefix: impl Into<String>, uri: impl Into<String>) {
self.bindings.insert(prefix.into(), uri.into());
}
#[must_use]
pub fn resolve(&self, prefix: &str) -> Option<&str> {
if prefix == "xml" {
return Some(crate::xml::XML_NAMESPACE);
}
self.bindings.get(prefix).map(String::as_str)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.bindings.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct Keys {
indexes: HashMap<String, HashMap<String, Vec<NodeId>>>,
}
impl Keys {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn declare(&mut self, name: impl Into<String>) {
self.indexes.entry(name.into()).or_default();
}
pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>, node: NodeId) {
self.indexes
.entry(name.into())
.or_default()
.entry(value.into())
.or_default()
.push(node);
}
#[must_use]
pub fn lookup(&self, name: &str, value: &str) -> Vec<NodeId> {
self.indexes
.get(name)
.and_then(|index| index.get(value))
.cloned()
.unwrap_or_default()
}
#[must_use]
pub fn is_declared(&self, name: &str) -> bool {
self.indexes.contains_key(name)
}
#[must_use]
pub fn len(&self) -> usize {
self.indexes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.indexes.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct Variables {
entries: Vec<(String, Value)>,
}
impl Variables {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn bind(&mut self, name: impl Into<String>, value: Value) {
self.entries.push((name.into(), value));
}
#[must_use]
pub fn lookup(&self, name: &str) -> Option<&Value> {
self.entries
.iter()
.rev()
.find(|(n, _)| n == name)
.map(|(_, v)| v)
}
#[must_use]
pub fn mark(&self) -> usize {
self.entries.len()
}
pub fn truncate(&mut self, mark: usize) {
self.entries.truncate(mark);
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.entries.iter().map(|(n, _)| n.as_str())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct EvalContext<'a> {
pub document: &'a Document,
pub node: NodeId,
pub position: usize,
pub size: usize,
pub variables: &'a Variables,
pub namespaces: &'a Namespaces,
pub current: NodeId,
pub version: XPathVersion,
pub current_time: Option<f64>,
pub implicit_timezone: i32,
pub keys: Option<&'a Keys>,
pub documents: Option<&'a Documents>,
}
impl<'a> EvalContext<'a> {
#[must_use]
pub fn new(
document: &'a Document,
node: NodeId,
variables: &'a Variables,
namespaces: &'a Namespaces,
) -> Self {
Self {
document,
node,
position: 1,
size: 1,
variables,
namespaces,
current: node,
version: XPathVersion::V1,
current_time: None,
implicit_timezone: 0,
keys: None,
documents: None,
}
}
#[must_use]
pub fn with_keys(mut self, keys: &'a Keys) -> Self {
self.keys = Some(keys);
self
}
#[must_use]
pub fn with_implicit_timezone(mut self, minutes: i32) -> Self {
self.implicit_timezone = minutes;
self
}
#[must_use]
pub fn with_current_time(mut self, seconds: f64) -> Self {
self.current_time = Some(seconds);
self
}
#[must_use]
pub fn with_version(mut self, version: XPathVersion) -> Self {
self.version = version;
self
}
#[must_use]
pub fn with_documents(mut self, documents: &'a Documents) -> Self {
self.documents = Some(documents);
self
}
#[must_use]
pub fn focus(&self, node: NodeId, position: usize, size: usize) -> Self {
Self {
node,
position,
size,
..*self
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inner_binding_shadows_outer() {
let mut vars = Variables::new();
vars.bind("x", Value::Number(1.0, NumericType::default()));
vars.bind("x", Value::Number(2.0, NumericType::default()));
assert_eq!(vars.lookup("x"), Some(&Value::Number(2.0, NumericType::default())));
}
#[test]
fn truncate_restores_the_outer_scope() {
let mut vars = Variables::new();
vars.bind("x", Value::Number(1.0, NumericType::default()));
let mark = vars.mark();
vars.bind("x", Value::Number(2.0, NumericType::default()));
vars.truncate(mark);
assert_eq!(vars.lookup("x"), Some(&Value::Number(1.0, NumericType::default())));
}
#[test]
fn unbound_lookup_returns_none() {
assert!(Variables::new().lookup("x").is_none());
}
#[test]
fn xml_prefix_is_bound_without_declaration() {
assert_eq!(
Namespaces::new().resolve("xml"),
Some(crate::xml::XML_NAMESPACE)
);
}
}