Skip to main content

braid_http/types/
patch.rs

1//! Patch representing a partial update to a resource.
2
3use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5
6/// A patch representing a partial update to a resource.
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Patch {
9    /// The addressing unit type (e.g., `"json"`, `"bytes"`)
10    pub unit: String,
11    /// The range specification
12    pub range: String,
13    /// The patch content
14    pub content: Bytes,
15    /// Content length in bytes
16    pub content_length: Option<usize>,
17}
18
19#[cfg(feature = "fuzzing")]
20impl<'a> arbitrary::Arbitrary<'a> for Patch {
21    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
22        Ok(Patch {
23            unit: u.arbitrary()?,
24            range: u.arbitrary()?,
25            content: bytes::Bytes::from(u.arbitrary::<Vec<u8>>()?),
26            content_length: u.arbitrary()?,
27        })
28    }
29}
30
31impl Patch {
32    /// Create a new patch.
33    #[must_use]
34    pub fn new(
35        unit: impl Into<String>,
36        range: impl Into<String>,
37        content: impl Into<Bytes>,
38    ) -> Self {
39        let content_bytes = content.into();
40        let content_length = content_bytes.len();
41        Patch {
42            unit: unit.into(),
43            range: range.into(),
44            content: content_bytes,
45            content_length: Some(content_length),
46        }
47    }
48
49    #[inline]
50    #[must_use]
51    pub fn json(range: impl Into<String>, content: impl Into<Bytes>) -> Self {
52        Self::new("json", range, content)
53    }
54
55    #[inline]
56    #[must_use]
57    pub fn bytes(range: impl Into<String>, content: impl Into<Bytes>) -> Self {
58        Self::new("bytes", range, content)
59    }
60
61    #[inline]
62    #[must_use]
63    pub fn text(range: impl Into<String>, content: impl Into<String>) -> Self {
64        let content_str = content.into();
65        Self::new("text", range, Bytes::from(content_str))
66    }
67
68    #[inline]
69    #[must_use]
70    pub fn lines(range: impl Into<String>, content: impl Into<String>) -> Self {
71        let content_str = content.into();
72        Self::new("lines", range, Bytes::from(content_str))
73    }
74
75    #[must_use]
76    pub fn with_length(
77        unit: impl Into<String>,
78        range: impl Into<String>,
79        content: impl Into<Bytes>,
80        length: usize,
81    ) -> Self {
82        Patch {
83            unit: unit.into(),
84            range: range.into(),
85            content: content.into(),
86            content_length: Some(length),
87        }
88    }
89
90    #[inline]
91    #[must_use]
92    pub fn is_json(&self) -> bool {
93        self.unit == "json"
94    }
95
96    #[inline]
97    #[must_use]
98    pub fn is_bytes(&self) -> bool {
99        self.unit == "bytes"
100    }
101
102    #[inline]
103    #[must_use]
104    pub fn is_text(&self) -> bool {
105        self.unit == "text"
106    }
107
108    #[inline]
109    #[must_use]
110    pub fn is_lines(&self) -> bool {
111        self.unit == "lines"
112    }
113
114    #[inline]
115    #[must_use]
116    pub fn content_str(&self) -> Option<&str> {
117        std::str::from_utf8(&self.content).ok()
118    }
119
120    #[inline]
121    #[must_use]
122    pub fn content_text(&self) -> Option<&str> {
123        self.content_str()
124    }
125
126    #[inline]
127    #[must_use]
128    pub fn len(&self) -> usize {
129        self.content_length.unwrap_or_else(|| self.content.len())
130    }
131
132    #[inline]
133    #[must_use]
134    pub fn is_empty(&self) -> bool {
135        self.len() == 0
136    }
137
138    #[must_use]
139    pub fn content_range_header(&self) -> String {
140        format!("{} {}", self.unit, self.range)
141    }
142
143    pub fn validate(&self) -> crate::error::Result<()> {
144        if self.unit.is_empty() {
145            return Err(crate::error::BraidError::Protocol(
146                "Patch unit cannot be empty".into(),
147            ));
148        }
149        if self.range.is_empty() {
150            return Err(crate::error::BraidError::Protocol(
151                "Patch range cannot be empty".into(),
152            ));
153        }
154        Ok(())
155    }
156}
157
158impl Default for Patch {
159    fn default() -> Self {
160        Patch {
161            unit: "bytes".to_string(),
162            range: String::new(),
163            content: Bytes::new(),
164            content_length: Some(0),
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_patch_new() {
175        let patch = Patch::new("custom", "range", "content");
176        assert_eq!(patch.unit, "custom");
177        assert_eq!(patch.range, "range");
178        assert_eq!(patch.content_length, Some(7));
179    }
180
181    #[test]
182    fn test_patch_json() {
183        let patch = Patch::json(".field", "value");
184        assert_eq!(patch.unit, "json");
185        assert_eq!(patch.range, ".field");
186        assert!(patch.is_json());
187    }
188
189    #[test]
190    fn test_patch_bytes() {
191        let patch = Patch::bytes("0:100", &b"content"[..]);
192        assert_eq!(patch.unit, "bytes");
193        assert_eq!(patch.range, "0:100");
194        assert!(patch.is_bytes());
195    }
196
197    #[test]
198    fn test_patch_text() {
199        let patch = Patch::text(".title", "New Title");
200        assert_eq!(patch.unit, "text");
201        assert_eq!(patch.range, ".title");
202        assert!(patch.is_text());
203    }
204
205    #[test]
206    fn test_patch_lines() {
207        let patch = Patch::lines("10:20", "new lines\n");
208        assert_eq!(patch.unit, "lines");
209        assert_eq!(patch.range, "10:20");
210        assert!(patch.is_lines());
211    }
212
213    #[test]
214    fn test_content_str() {
215        let patch = Patch::json(".field", "value");
216        assert_eq!(patch.content_str(), Some("value"));
217    }
218
219    #[test]
220    fn test_len_and_is_empty() {
221        let patch = Patch::json(".field", "value");
222        assert_eq!(patch.len(), 5);
223        assert!(!patch.is_empty());
224
225        let empty = Patch::json(".field", "");
226        assert_eq!(empty.len(), 0);
227        assert!(empty.is_empty());
228    }
229
230    #[test]
231    fn test_content_range_header() {
232        let patch = Patch::json(".users[0]", "{}");
233        assert_eq!(patch.content_range_header(), "json .users[0]");
234    }
235
236    #[test]
237    fn test_default() {
238        let patch = Patch::default();
239        assert_eq!(patch.unit, "bytes");
240        assert!(patch.range.is_empty());
241        assert!(patch.is_empty());
242    }
243
244    #[test]
245    fn test_validate_valid_patch() {
246        let patch = Patch::json(".field", "value");
247        assert!(patch.validate().is_ok());
248    }
249
250    #[test]
251    fn test_validate_empty_unit() {
252        let patch = Patch {
253            unit: String::new(),
254            range: ".field".to_string(),
255            content: Bytes::from("value"),
256            content_length: Some(5),
257        };
258        assert!(patch.validate().is_err());
259    }
260
261    #[test]
262    fn test_validate_empty_range() {
263        let patch = Patch {
264            unit: "json".to_string(),
265            range: String::new(),
266            content: Bytes::from("value"),
267            content_length: Some(5),
268        };
269        assert!(patch.validate().is_err());
270    }
271
272    #[test]
273    fn test_validate_all_types() {
274        assert!(Patch::json(".f", "v").validate().is_ok());
275        assert!(Patch::bytes("0:10", &b"data"[..]).validate().is_ok());
276        assert!(Patch::text(".t", "text").validate().is_ok());
277        assert!(Patch::lines("1:5", "lines").validate().is_ok());
278    }
279}