Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add raw-node feature and RawNode type to capture subtrees from the source #5

Merged
merged 1 commit into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ jobs:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt -- --check
- run: cargo clippy --all-targets -- --deny warnings
- run: cargo clippy --all-targets --no-default-features -- --deny warnings
- run: cargo clippy --all-targets --all-features -- --deny warnings

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test
- run: cargo test --all-features
10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/adamreichold/serde-roxmltree"
documentation = "https://docs.rs/serde-roxmltree"
readme = "README.md"
version = "0.8.3"
version = "0.8.4"
edition = "2021"

[dependencies]
Expand All @@ -18,3 +18,11 @@ serde = "1.0"

[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }

[features]
default = []
# Capture subtrees from the source
raw-node = []

[package.metadata.docs.rs]
all-features = true
26 changes: 21 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! Subtrees can be captured from the source by enabling the `raw-node` feature and using the [`RawNode`] type.
//!
//! Fields of structures map to child elements and attributes:
//!
//! ```
Expand Down Expand Up @@ -175,12 +177,16 @@
//! ```
//!
//! [namespaces]: https://www.w3.org/TR/REC-xml-names/
#![forbid(unsafe_code)]
#![deny(
unsafe_code,
missing_docs,
missing_copy_implementations,
missing_debug_implementations
)]

#[cfg(feature = "raw-node")]
mod raw_node;

use std::char::ParseCharError;
use std::error::Error as StdError;
use std::fmt;
Expand All @@ -195,6 +201,9 @@ use serde::de;

pub use roxmltree;

#[cfg(feature = "raw-node")]
pub use raw_node::RawNode;

/// Deserialize an instance of type `T` directly from XML text
pub fn from_str<T>(text: &str) -> Result<T, Error>
where
Expand Down Expand Up @@ -704,14 +713,21 @@ where

fn deserialize_struct<V>(
self,
_name: &'static str,
#[allow(unused_variables)] name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: de::Visitor<'de>,
{
self.deserialize_map(visitor)
#[cfg(feature = "raw-node")]
let res =
raw_node::deserialize_struct(self, name, move |this| this.deserialize_map(visitor));

#[cfg(not(feature = "raw-node"))]
let res = self.deserialize_map(visitor);

res
}

fn deserialize_enum<V>(
Expand Down Expand Up @@ -1160,14 +1176,14 @@ mod tests {

#[test]
fn borrowed_str() {
let document = Document::parse("<root><child>foobar</child></root>").unwrap();
let doc = Document::parse("<root><child>foobar</child></root>").unwrap();

#[derive(Deserialize)]
struct Root<'a> {
child: &'a str,
}

let val = from_doc::<Root>(&document).unwrap();
let val = from_doc::<Root>(&doc).unwrap();
assert_eq!(val.child, "foobar");
}

Expand Down
155 changes: 155 additions & 0 deletions src/raw_node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use std::cell::Cell;
use std::fmt;
use std::marker::PhantomData;
use std::mem::transmute;
use std::ops::Deref;
use std::ptr;

use roxmltree::Node;
use serde::de;

use crate::{Deserializer, Source};

/// Captures subtrees from the source
///
/// This type must borrow from the source during serialization and therefore requires the use of the [`from_doc`][crate::from_doc] or [`from_node`][crate::from_node] entry points.
/// It will however recover only the source `document` or `node` lifetime and not the full `input` lifetime.
///
/// ```
/// use roxmltree::Document;
/// use serde::Deserialize;
/// use serde_roxmltree::{from_doc, RawNode};
///
/// #[derive(Deserialize)]
/// struct Record<'a> {
/// #[serde(borrow)]
/// subtree: RawNode<'a>,
/// }
///
/// let document = Document::parse(r#"<document><subtree><field attribute="bar">foo</field></subtree></document>"#)?;
///
/// let record = from_doc::<Record>(&document)?;
/// assert!(record.subtree.has_tag_name("subtree"));
/// #
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RawNode<'a>(pub Node<'a, 'a>);

impl<'a> Deref for RawNode<'a> {
type Target = Node<'a, 'a>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<'de, 'a> de::Deserialize<'de> for RawNode<'a>
where
'de: 'a,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor<'a>(PhantomData<&'a ()>);

impl<'de, 'a> de::Visitor<'de> for Visitor<'a>
where
'de: 'a,
{
type Value = RawNode<'a>;

fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str("struct RawNode")
}

fn visit_map<M>(self, _map: M) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
match CURR_NODE.get() {
#[allow(unsafe_code)]
// SAFETY: This is set only while `deserialize_struct` is active.
Some(curr_node) => Ok(RawNode(unsafe {
transmute::<Node<'static, 'static>, Node<'a, 'a>>(curr_node)
})),
None => Err(de::Error::custom("no current node")),
}
}
}

deserializer.deserialize_struct(RAW_NODE_NAME, &[], Visitor(PhantomData))
}
}

pub fn deserialize_struct<'de, 'input, 'temp, O, F, R>(
this: Deserializer<'de, 'input, 'temp, O>,
name: &'static str,
f: F,
) -> R
where
F: FnOnce(Deserializer<'de, 'input, 'temp, O>) -> R,
{
let _reset_curr_node = match &this.source {
Source::Node(node) if ptr::eq(name, RAW_NODE_NAME) => {
#[allow(unsafe_code)]
// SAFETY: The guard will reset this before `deserialize_struct` returns.
CURR_NODE.set(Some(unsafe {
transmute::<Node<'de, 'input>, Node<'static, 'static>>(*node)
}));

Some(ResetCurrNode)
}
_ => None,
};

f(this)
}

static RAW_NODE_NAME: &str = "RawNode";

thread_local! {
static CURR_NODE: Cell<Option<Node<'static, 'static>>> = const { Cell::new(None) };
}

struct ResetCurrNode;

impl Drop for ResetCurrNode {
fn drop(&mut self) {
CURR_NODE.set(None);
}
}

#[cfg(test)]
mod tests {
use super::*;

use roxmltree::Document;
use serde::Deserialize;

use crate::from_doc;

#[test]
fn raw_node_captures_subtree() {
#[derive(Debug, Deserialize)]
struct Root<'a> {
#[serde(borrow)]
foo: RawNode<'a>,
}

let doc = Document::parse(r#"<root><foo><bar qux="42">23</bar>baz</foo></root>"#).unwrap();
let val = from_doc::<Root>(&doc).unwrap();

assert!(val.foo.0.is_element());
assert!(val.foo.0.has_tag_name("foo"));

let children = val.foo.0.children().collect::<Vec<_>>();
assert_eq!(children.len(), 2);
assert!(children[0].is_element());
assert!(children[0].has_tag_name("bar"));
assert_eq!(children[0].attribute("qux").unwrap(), "42");
assert!(children[1].is_text());
assert_eq!(children[1].text().unwrap(), "baz");
}
}
Loading