nextcloud_dav/propfind/
request_body.rs1use crate::xml::XmlWriter;
4
5use super::{DavNamespace, DavProperty};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct PropfindBody {
14 properties: Vec<DavProperty>,
15}
16
17impl PropfindBody {
18 pub fn new(properties: impl IntoIterator<Item = DavProperty>) -> Self {
20 Self {
21 properties: properties.into_iter().collect(),
22 }
23 }
24
25 pub fn all() -> Self {
27 Self::new([
28 DavProperty::Etag,
29 DavProperty::ContentLength,
30 DavProperty::ContentType,
31 DavProperty::ResourceType,
32 DavProperty::LastModified,
33 DavProperty::DisplayName,
34 DavProperty::FileId,
35 DavProperty::Permissions,
36 DavProperty::HasPreview,
37 ])
38 }
39
40 pub fn properties(&self) -> &[DavProperty] {
42 &self.properties
43 }
44
45 pub fn is_empty(&self) -> bool {
48 self.properties.is_empty()
49 }
50
51 pub fn to_xml(&self) -> String {
53 let mut writer = XmlWriter::new();
54 writer.prolog();
55 writer.open_with("d:propfind", &self.namespace_declarations());
56 writer.open("d:prop");
57 for property in &self.properties {
58 writer.empty(property.qualified_name());
59 }
60 writer.close("d:prop");
61 writer.close("d:propfind");
62 writer.into_string()
63 }
64
65 fn namespace_declarations(&self) -> Vec<(&'static str, &'static str)> {
68 let mut declarations: Vec<(&'static str, &'static str)> = Vec::new();
69 for namespace in self.properties.iter().map(|p| p.namespace()) {
70 if declarations
71 .iter()
72 .any(|(key, _)| *key == declaration_key(namespace))
73 {
74 continue;
75 }
76 declarations.push((declaration_key(namespace), namespace.uri()));
77 }
78 if declarations.is_empty() {
79 let namespace = DavNamespace::Dav;
80 declarations.push((declaration_key(namespace), namespace.uri()));
81 }
82 declarations
83 }
84}
85
86const fn declaration_key(namespace: DavNamespace) -> &'static str {
88 match namespace {
89 DavNamespace::Dav => "xmlns:d",
90 DavNamespace::OwnCloud => "xmlns:oc",
91 DavNamespace::Nextcloud => "xmlns:nc",
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::PropfindBody;
98 use crate::propfind::DavProperty;
99
100 #[test]
101 fn every_preset_contains_the_three_namespace_declarations() {
102 let xml = PropfindBody::all().to_xml();
103 assert!(xml.contains(r#"xmlns:d="DAV:""#), "{xml}");
104 assert!(
105 xml.contains(r#"xmlns:oc="http://owncloud.org/ns""#),
106 "{xml}"
107 );
108 assert!(
109 xml.contains(r#"xmlns:nc="http://nextcloud.org/ns""#),
110 "{xml}"
111 );
112 }
113
114 #[test]
115 fn preset_asks_for_every_property_once() {
116 let xml = PropfindBody::all().to_xml();
117 assert_eq!(xml.matches("<d:getetag/>").count(), 1);
118 assert!(xml.contains("<oc:fileid/>"));
119 assert!(xml.contains("<nc:has-preview/>"));
120 assert_eq!(xml.matches("xmlns:d=").count(), 1);
121 }
122
123 #[test]
124 fn body_is_well_formed_and_carries_the_requested_properties() {
125 let xml = PropfindBody::new([DavProperty::Etag, DavProperty::FileId]).to_xml();
126 assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"utf-8\"?>"));
127 assert!(xml.contains("<d:propfind"));
128 assert!(xml.contains("<d:prop><d:getetag/><oc:fileid/></d:prop>"));
129 assert!(xml.ends_with("</d:propfind>"));
130 assert_eq!(xml.matches("<d:prop>").count(), 1);
131 assert_eq!(xml.matches("</d:propfind>").count(), 1);
132 assert!(!xml.contains("xmlns:nc"));
133 }
134
135 #[test]
136 fn core_only_body_still_declares_the_dav_namespace() {
137 let xml = PropfindBody::new([DavProperty::Etag]).to_xml();
138 assert!(xml.contains(r#"xmlns:d="DAV:""#));
139 assert!(!xml.contains("xmlns:oc"));
140 }
141
142 #[test]
143 fn empty_body_is_reported_and_still_renders_a_document() {
144 let body = PropfindBody::new([]);
145 assert!(body.is_empty());
146 assert!(body.to_xml().contains("<d:prop></d:prop>"));
147 }
148}