Skip to content

Commit 1d117c1

Browse files
committed
Add raw-node feature and RawNode type to capture subtrees from the source.
1 parent 2cd4ea7 commit 1d117c1

File tree

4 files changed

+188
-8
lines changed

4 files changed

+188
-8
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ jobs:
1111
components: rustfmt, clippy
1212
- uses: Swatinem/rust-cache@v2
1313
- run: cargo fmt -- --check
14-
- run: cargo clippy --all-targets -- --deny warnings
14+
- run: cargo clippy --all-targets --no-default-features -- --deny warnings
15+
- run: cargo clippy --all-targets --all-features -- --deny warnings
1516

1617
test:
1718
runs-on: ubuntu-latest
1819
steps:
1920
- uses: actions/checkout@v4
2021
- uses: dtolnay/rust-toolchain@stable
2122
- uses: Swatinem/rust-cache@v2
22-
- run: cargo test
23+
- run: cargo test --all-features

Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0"
88
repository = "https://github.com/adamreichold/serde-roxmltree"
99
documentation = "https://docs.rs/serde-roxmltree"
1010
readme = "README.md"
11-
version = "0.8.3"
11+
version = "0.8.4"
1212
edition = "2021"
1313

1414
[dependencies]
@@ -18,3 +18,11 @@ serde = "1.0"
1818

1919
[dev-dependencies]
2020
serde = { version = "1.0", features = ["derive"] }
21+
22+
[features]
23+
default = []
24+
# Capture subtrees from the source
25+
raw-node = []
26+
27+
[package.metadata.docs.rs]
28+
all-features = true

src/lib.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
//! # Ok::<(), Box<dyn std::error::Error>>(())
3838
//! ```
3939
//!
40+
//! Subtrees can be captured from the source by enabling the `raw-node` feature and using the [`RawNode`] type.
41+
//!
4042
//! Fields of structures map to child elements and attributes:
4143
//!
4244
//! ```
@@ -175,12 +177,16 @@
175177
//! ```
176178
//!
177179
//! [namespaces]: https://www.w3.org/TR/REC-xml-names/
178-
#![forbid(unsafe_code)]
179180
#![deny(
181+
unsafe_code,
180182
missing_docs,
181183
missing_copy_implementations,
182184
missing_debug_implementations
183185
)]
186+
187+
#[cfg(feature = "raw-node")]
188+
mod raw_node;
189+
184190
use std::char::ParseCharError;
185191
use std::error::Error as StdError;
186192
use std::fmt;
@@ -195,6 +201,9 @@ use serde::de;
195201

196202
pub use roxmltree;
197203

204+
#[cfg(feature = "raw-node")]
205+
pub use raw_node::RawNode;
206+
198207
/// Deserialize an instance of type `T` directly from XML text
199208
pub fn from_str<T>(text: &str) -> Result<T, Error>
200209
where
@@ -704,14 +713,21 @@ where
704713

705714
fn deserialize_struct<V>(
706715
self,
707-
_name: &'static str,
716+
#[allow(unused_variables)] name: &'static str,
708717
_fields: &'static [&'static str],
709718
visitor: V,
710719
) -> Result<V::Value, Self::Error>
711720
where
712721
V: de::Visitor<'de>,
713722
{
714-
self.deserialize_map(visitor)
723+
#[cfg(feature = "raw-node")]
724+
let res =
725+
raw_node::deserialize_struct(self, name, move |this| this.deserialize_map(visitor));
726+
727+
#[cfg(not(feature = "raw-node"))]
728+
let res = self.deserialize_map(visitor);
729+
730+
res
715731
}
716732

717733
fn deserialize_enum<V>(
@@ -1160,14 +1176,14 @@ mod tests {
11601176

11611177
#[test]
11621178
fn borrowed_str() {
1163-
let document = Document::parse("<root><child>foobar</child></root>").unwrap();
1179+
let doc = Document::parse("<root><child>foobar</child></root>").unwrap();
11641180

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

1170-
let val = from_doc::<Root>(&document).unwrap();
1186+
let val = from_doc::<Root>(&doc).unwrap();
11711187
assert_eq!(val.child, "foobar");
11721188
}
11731189

src/raw_node.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
use std::cell::Cell;
2+
use std::fmt;
3+
use std::marker::PhantomData;
4+
use std::mem::transmute;
5+
use std::ops::Deref;
6+
use std::ptr;
7+
8+
use roxmltree::Node;
9+
use serde::de;
10+
11+
use crate::{Deserializer, Source};
12+
13+
/// Captures subtrees from the source
14+
///
15+
/// 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.
16+
/// It will however recover only the source `document` or `node` lifetime and not the full `input` lifetime.
17+
///
18+
/// ```
19+
/// use roxmltree::Document;
20+
/// use serde::Deserialize;
21+
/// use serde_roxmltree::{from_doc, RawNode};
22+
///
23+
/// #[derive(Deserialize)]
24+
/// struct Record<'a> {
25+
/// #[serde(borrow)]
26+
/// subtree: RawNode<'a>,
27+
/// }
28+
///
29+
/// let document = Document::parse(r#"<document><subtree><field attribute="bar">foo</field></subtree></document>"#)?;
30+
///
31+
/// let record = from_doc::<Record>(&document)?;
32+
/// assert!(record.subtree.has_tag_name("subtree"));
33+
/// #
34+
/// # Ok::<(), Box<dyn std::error::Error>>(())
35+
/// ```
36+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37+
pub struct RawNode<'a>(pub Node<'a, 'a>);
38+
39+
impl<'a> Deref for RawNode<'a> {
40+
type Target = Node<'a, 'a>;
41+
42+
fn deref(&self) -> &Self::Target {
43+
&self.0
44+
}
45+
}
46+
47+
impl<'de, 'a> de::Deserialize<'de> for RawNode<'a>
48+
where
49+
'de: 'a,
50+
{
51+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52+
where
53+
D: de::Deserializer<'de>,
54+
{
55+
struct Visitor<'a>(PhantomData<&'a ()>);
56+
57+
impl<'de, 'a> de::Visitor<'de> for Visitor<'a>
58+
where
59+
'de: 'a,
60+
{
61+
type Value = RawNode<'a>;
62+
63+
fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
64+
fmt.write_str("struct RawNode")
65+
}
66+
67+
fn visit_map<M>(self, _map: M) -> Result<Self::Value, M::Error>
68+
where
69+
M: de::MapAccess<'de>,
70+
{
71+
match CURR_NODE.get() {
72+
#[allow(unsafe_code)]
73+
// SAFETY: This is set only while `deserialize_struct` is active.
74+
Some(curr_node) => Ok(RawNode(unsafe {
75+
transmute::<Node<'static, 'static>, Node<'a, 'a>>(curr_node)
76+
})),
77+
None => Err(de::Error::custom("no current node")),
78+
}
79+
}
80+
}
81+
82+
deserializer.deserialize_struct(RAW_NODE_NAME, &[], Visitor(PhantomData))
83+
}
84+
}
85+
86+
pub fn deserialize_struct<'de, 'input, 'temp, O, F, R>(
87+
this: Deserializer<'de, 'input, 'temp, O>,
88+
name: &'static str,
89+
f: F,
90+
) -> R
91+
where
92+
F: FnOnce(Deserializer<'de, 'input, 'temp, O>) -> R,
93+
{
94+
let _reset_curr_node = match &this.source {
95+
Source::Node(node) if ptr::eq(name, RAW_NODE_NAME) => {
96+
#[allow(unsafe_code)]
97+
// SAFETY: The guard will reset this before `deserialize_struct` returns.
98+
CURR_NODE.set(Some(unsafe {
99+
transmute::<Node<'de, 'input>, Node<'static, 'static>>(*node)
100+
}));
101+
102+
Some(ResetCurrNode)
103+
}
104+
_ => None,
105+
};
106+
107+
f(this)
108+
}
109+
110+
static RAW_NODE_NAME: &str = "RawNode";
111+
112+
thread_local! {
113+
static CURR_NODE: Cell<Option<Node<'static, 'static>>> = const { Cell::new(None) };
114+
}
115+
116+
struct ResetCurrNode;
117+
118+
impl Drop for ResetCurrNode {
119+
fn drop(&mut self) {
120+
CURR_NODE.set(None);
121+
}
122+
}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use super::*;
127+
128+
use roxmltree::Document;
129+
use serde::Deserialize;
130+
131+
use crate::from_doc;
132+
133+
#[test]
134+
fn raw_node_captures_subtree() {
135+
#[derive(Debug, Deserialize)]
136+
struct Root<'a> {
137+
#[serde(borrow)]
138+
foo: RawNode<'a>,
139+
}
140+
141+
let doc = Document::parse(r#"<root><foo><bar qux="42">23</bar>baz</foo></root>"#).unwrap();
142+
let val = from_doc::<Root>(&doc).unwrap();
143+
144+
assert!(val.foo.0.is_element());
145+
assert!(val.foo.0.has_tag_name("foo"));
146+
147+
let children = val.foo.0.children().collect::<Vec<_>>();
148+
assert_eq!(children.len(), 2);
149+
assert!(children[0].is_element());
150+
assert!(children[0].has_tag_name("bar"));
151+
assert_eq!(children[0].attribute("qux").unwrap(), "42");
152+
assert!(children[1].is_text());
153+
assert_eq!(children[1].text().unwrap(), "baz");
154+
}
155+
}

0 commit comments

Comments
 (0)