Skip to main content

nextcloud_dav/xml/
element.rs

1//! An element located in a document.
2
3use super::escape::unescape_text;
4use super::response::{elements, first_element, has_element};
5
6/// An element located in a document: its qualified name and its content.
7///
8/// A handle, not a tree node: it borrows the document slice it was found in,
9/// so chained lookups cost no allocation and nothing is copied until
10/// [`text`](Self::text) is called.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct Element<'a> {
13    name: &'a str,
14    inner: &'a str,
15}
16
17impl<'a> Element<'a> {
18    /// Wraps a located element; callers obtain one through
19    /// [`elements`](super::response::elements) or
20    /// [`first_element`](super::response::first_element).
21    pub(crate) const fn new(name: &'a str, inner: &'a str) -> Self {
22        Self { name, inner }
23    }
24
25    /// Qualified name the element was found by, e.g. `d:propstat`.
26    pub const fn name(&self) -> &'a str {
27        self.name
28    }
29
30    /// Content between the tags, markup included.
31    pub const fn inner(&self) -> &'a str {
32        self.inner
33    }
34
35    /// Content as text: entities decoded and surrounding whitespace trimmed.
36    pub fn text(&self) -> String {
37        unescape_text(self.inner).trim().to_owned()
38    }
39
40    /// First child element with `name`.
41    pub fn child(&self, name: &'a str) -> Option<Self> {
42        first_element(self.inner, name)
43    }
44
45    /// Every child element with `name`, in document order.
46    ///
47    /// "Child" is scanning order, not strict nesting: a match inside another
48    /// element of a different name is still returned.
49    pub fn children(&self, name: &'a str) -> Vec<Self> {
50        elements(self.inner, name)
51    }
52
53    /// Whether an element with `name` appears anywhere in the content.
54    pub fn has(&self, name: &'a str) -> bool {
55        has_element(self.inner, name)
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use crate::xml::response::first_element;
62
63    const BODY: &str = "<d:response><d:href>/a</d:href><d:propstat><d:prop>\
64        <oc:fileid>7</oc:fileid></d:prop></d:propstat></d:response>";
65
66    fn response() -> super::Element<'static> {
67        first_element(BODY, "d:response").expect("response is present")
68    }
69
70    #[test]
71    fn chained_lookups_walk_into_nested_elements() {
72        let propstat = response().child("d:propstat").expect("propstat present");
73        let fileid = propstat
74            .child("d:prop")
75            .and_then(|prop| prop.child("oc:fileid"))
76            .map(|element| element.text());
77        assert_eq!(fileid.as_deref(), Some("7"));
78    }
79
80    #[test]
81    fn name_and_inner_expose_the_located_slice() {
82        let element = response();
83        assert_eq!(element.name(), "d:response");
84        assert!(element.inner().starts_with("<d:href>"));
85    }
86
87    #[test]
88    fn has_reports_absence_rather_than_guessing() {
89        assert!(response().has("d:propstat"));
90        assert!(!response().has("d:collection"));
91    }
92}