Skip to main content

braid_http/types/
update.rs

1//! Complete update in the Braid protocol.
2
3use crate::types::{ContentRange, Patch, Version};
4use bytes::Bytes;
5use std::collections::BTreeMap;
6
7#[cfg(feature = "server")]
8use axum::{
9    body::Body,
10    http::{header, HeaderValue, StatusCode},
11    response::{IntoResponse, Response},
12};
13
14#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
15pub struct Update {
16    /// Version ID(s)
17    pub version: Vec<Version>,
18    /// Parent version(s)
19    pub parents: Vec<Version>,
20    /// Catch-up signaling
21    pub current_version: Option<Vec<Version>>,
22    /// Conflict resolution strategy
23    pub merge_type: Option<String>,
24    /// Incremental updates
25    pub patches: Option<Vec<Patch>>,
26    /// Complete state content
27    pub body: Option<Bytes>,
28    /// Content range for single patch
29    pub content_range: Option<ContentRange>,
30    /// Media type
31    pub content_type: Option<String>,
32    /// HTTP status code
33    pub status: u16,
34    /// Additional headers
35    pub extra_headers: BTreeMap<String, String>,
36    /// Target URL
37    pub url: Option<String>,
38}
39
40#[cfg(feature = "fuzzing")]
41impl<'a> arbitrary::Arbitrary<'a> for Update {
42    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
43        Ok(Update {
44            version: u.arbitrary()?,
45            parents: u.arbitrary()?,
46            current_version: u.arbitrary()?,
47            merge_type: u.arbitrary()?,
48            patches: u.arbitrary()?,
49            body: {
50                let v: Option<Vec<u8>> = u.arbitrary()?;
51                v.map(bytes::Bytes::from)
52            },
53            content_range: u.arbitrary()?,
54            content_type: u.arbitrary()?,
55            status: u.arbitrary()?,
56            extra_headers: u.arbitrary()?,
57            url: u.arbitrary()?,
58        })
59    }
60}
61
62impl Update {
63    /// Create a snapshot update with complete state.
64    #[must_use]
65    pub fn snapshot(version: Version, body: impl Into<Bytes>) -> Self {
66        Update {
67            version: vec![version],
68            parents: vec![],
69            current_version: None,
70            merge_type: None,
71            patches: None,
72            body: Some(body.into()),
73            content_range: None,
74            content_type: None,
75            status: 200,
76            extra_headers: BTreeMap::new(),
77            url: None,
78        }
79    }
80
81    /// Create a patch update with incremental changes.
82    #[must_use]
83    pub fn patched(version: Version, patches: Vec<Patch>) -> Self {
84        Update {
85            version: vec![version],
86            parents: vec![],
87            current_version: None,
88            merge_type: None,
89            patches: Some(patches),
90            body: None,
91            content_range: None,
92            content_type: None,
93            status: 200,
94            url: None,
95            extra_headers: BTreeMap::new(),
96        }
97    }
98
99    #[inline]
100    #[must_use]
101    pub fn is_snapshot(&self) -> bool {
102        self.body.is_some()
103    }
104
105    #[inline]
106    #[must_use]
107    pub fn is_patched(&self) -> bool {
108        self.patches.is_some()
109    }
110
111    #[inline]
112    #[must_use]
113    pub fn primary_version(&self) -> Option<&Version> {
114        self.version.first()
115    }
116
117    #[inline]
118    #[must_use]
119    pub fn body_str(&self) -> Option<&str> {
120        self.body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
121    }
122
123    #[must_use]
124    pub fn subscription_snapshot(version: Version, body: impl Into<Bytes>) -> Self {
125        Update::snapshot(version, body).with_status(209)
126    }
127
128    #[must_use]
129    pub fn subscription_patched(version: Version, patches: Vec<Patch>) -> Self {
130        Update::patched(version, patches).with_status(209)
131    }
132
133    #[must_use]
134    pub fn with_parent(mut self, parent: Version) -> Self {
135        self.parents.push(parent);
136        self
137    }
138
139    #[must_use]
140    pub fn with_parents(mut self, parents: Vec<Version>) -> Self {
141        self.parents.extend(parents);
142        self
143    }
144
145    #[must_use]
146    pub fn with_current_version(mut self, version: Version) -> Self {
147        if self.current_version.is_none() {
148            self.current_version = Some(Vec::new());
149        }
150        if let Some(ref mut versions) = self.current_version {
151            versions.push(version);
152        }
153        self
154    }
155
156    #[must_use]
157    pub fn with_merge_type(mut self, merge_type: impl Into<String>) -> Self {
158        self.merge_type = Some(merge_type.into());
159        self
160    }
161
162    #[must_use]
163    pub fn with_content_range(mut self, content_range: ContentRange) -> Self {
164        self.content_range = Some(content_range);
165        self
166    }
167
168    #[must_use]
169    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
170        self.content_type = Some(content_type.into());
171        self
172    }
173
174    #[must_use]
175    pub fn with_status(mut self, status: u16) -> Self {
176        self.status = status;
177        self
178    }
179
180    #[must_use]
181    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
182        self.extra_headers.insert(name.into(), value.into());
183        self
184    }
185
186    #[must_use]
187    pub fn to_json(&self) -> serde_json::Value {
188        let mut obj = serde_json::Map::new();
189
190        obj.insert(
191            "version".to_string(),
192            serde_json::Value::Array(self.version.iter().map(|v| v.to_json()).collect()),
193        );
194
195        obj.insert(
196            "parents".to_string(),
197            serde_json::Value::Array(self.parents.iter().map(|v| v.to_json()).collect()),
198        );
199
200        if let Some(body) = &self.body {
201            obj.insert(
202                "body".to_string(),
203                serde_json::Value::String(String::from_utf8_lossy(body).into_owned()),
204            );
205        }
206
207        if let Some(merge_type) = &self.merge_type {
208            obj.insert(
209                "merge_type".to_string(),
210                serde_json::Value::String(merge_type.clone()),
211            );
212        }
213
214        serde_json::Value::Object(obj)
215    }
216}
217
218#[cfg(feature = "server")]
219impl IntoResponse for Update {
220    fn into_response(self) -> Response {
221        let mut response_builder = UpdateResponse::new(self.status);
222
223        if !self.version.is_empty() {
224            response_builder = response_builder.with_version(self.version.clone());
225        }
226
227        if !self.parents.is_empty() {
228            response_builder = response_builder.with_parents(self.parents.clone());
229        }
230
231        if let Some(current_version) = &self.current_version {
232            response_builder = response_builder.with_current_version(current_version.clone());
233        }
234
235        if let Some(content_type) = &self.content_type {
236            response_builder = response_builder.with_header(
237                header::CONTENT_TYPE.as_str().to_string(),
238                content_type.clone(),
239            );
240        } else if self.patches.is_some() {
241            response_builder = response_builder.with_header(
242                header::CONTENT_TYPE.as_str().to_string(),
243                crate::protocol::constants::media_types::BRAID_PATCH.to_string(),
244            );
245        }
246
247        for (key, value) in &self.extra_headers {
248            response_builder = response_builder.with_header(key.clone(), value.clone());
249        }
250
251        if let Some(body) = &self.body {
252            response_builder = response_builder.with_body(body.clone());
253        } else if let Some(patches) = &self.patches {
254            let patches_str = patches.len().to_string();
255            response_builder = response_builder.with_header(
256                crate::protocol::constants::headers::PATCHES
257                    .as_str()
258                    .to_string(),
259                patches_str,
260            );
261
262            if patches.len() == 1 {
263                let patch = &patches[0];
264                let content_range = format!("{} {}", patch.unit, patch.range);
265                response_builder = response_builder.with_header(
266                    crate::protocol::constants::headers::CONTENT_RANGE
267                        .as_str()
268                        .to_string(),
269                    content_range,
270                );
271                response_builder = response_builder.with_body(patch.content.clone());
272            } else if patches.len() > 1 {
273                let mut multi_body = bytes::BytesMut::new();
274                for patch in patches {
275                    use bytes::BufMut;
276                    let patch_headers = format!(
277                        "Content-Length: {}\r\nContent-Range: {} {}\r\n\r\n",
278                        patch.len(),
279                        patch.unit,
280                        patch.range
281                    );
282                    multi_body.put_slice(patch_headers.as_bytes());
283                    multi_body.put_slice(&patch.content);
284                    multi_body.put_slice(b"\r\n");
285                }
286                response_builder = response_builder.with_body(multi_body.freeze());
287            }
288        }
289
290        response_builder.build()
291    }
292}
293
294#[cfg(feature = "server")]
295pub struct UpdateResponse {
296    status: u16,
297    headers: BTreeMap<String, String>,
298    body: Option<Bytes>,
299}
300
301#[cfg(feature = "server")]
302impl UpdateResponse {
303    pub fn new(status: u16) -> Self {
304        UpdateResponse {
305            status,
306            headers: BTreeMap::new(),
307            body: None,
308        }
309    }
310
311    pub fn with_version(mut self, versions: Vec<Version>) -> Self {
312        let version_str = crate::protocol::format_version_header(&versions);
313        self.headers.insert(
314            crate::protocol::constants::headers::VERSION
315                .as_str()
316                .to_string(),
317            version_str,
318        );
319        self
320    }
321
322    pub fn with_parents(mut self, parents: Vec<Version>) -> Self {
323        let parents_str = crate::protocol::format_version_header(&parents);
324        self.headers.insert(
325            crate::protocol::constants::headers::PARENTS
326                .as_str()
327                .to_string(),
328            parents_str,
329        );
330        self
331    }
332
333    pub fn with_current_version(mut self, versions: Vec<Version>) -> Self {
334        let current_version_str = crate::protocol::format_version_header(&versions);
335        self.headers.insert(
336            crate::protocol::constants::headers::CURRENT_VERSION
337                .as_str()
338                .to_string(),
339            current_version_str,
340        );
341        self
342    }
343
344    pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
345        self.body = Some(body.into());
346        self
347    }
348
349    pub fn with_header(mut self, key: String, value: String) -> Self {
350        self.headers.insert(key, value);
351        self
352    }
353
354    pub fn build(self) -> Response {
355        let mut response = match self.status {
356            200 => Response::builder().status(StatusCode::OK),
357            209 => Response::builder().status(StatusCode::from_u16(209).unwrap()),
358            404 => Response::builder().status(StatusCode::NOT_FOUND),
359            500 => Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR),
360            _ => Response::builder().status(StatusCode::from_u16(self.status).unwrap()),
361        };
362
363        for (key, value) in &self.headers {
364            if let Ok(header_value) = value.parse::<HeaderValue>() {
365                response = response.header(key, header_value);
366            }
367        }
368
369        if let Some(body) = self.body {
370            response
371                .header(header::CONTENT_LENGTH, body.len())
372                .body(Body::from(body))
373                .unwrap_or_else(|_| Response::default())
374        } else {
375            response
376                .body(Body::empty())
377                .unwrap_or_else(|_| Response::default())
378        }
379    }
380}
381
382impl Default for Update {
383    fn default() -> Self {
384        Update {
385            version: vec![],
386            parents: vec![],
387            current_version: None,
388            merge_type: None,
389            patches: None,
390            body: None,
391            content_range: None,
392            content_type: None,
393            status: 200,
394            extra_headers: BTreeMap::new(),
395            url: None,
396        }
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_update_snapshot() {
406        let update = Update::snapshot(Version::new("v1"), "body");
407        assert_eq!(update.version.len(), 1);
408        assert!(update.body.is_some());
409        assert!(update.patches.is_none());
410        assert!(update.is_snapshot());
411        assert!(!update.is_patched());
412    }
413
414    #[test]
415    fn test_update_patched() {
416        let update = Update::patched(Version::new("v1"), vec![Patch::json(".field", "value")]);
417        assert_eq!(update.version.len(), 1);
418        assert!(update.patches.is_some());
419        assert!(update.body.is_none());
420        assert!(update.is_patched());
421        assert!(!update.is_snapshot());
422    }
423
424    #[test]
425    fn test_update_builder() {
426        let update = Update::snapshot(Version::new("v1"), "body")
427            .with_parent(Version::new("v0"))
428            .with_merge_type("diamond");
429        assert_eq!(update.parents.len(), 1);
430        assert_eq!(update.merge_type, Some("diamond".to_string()));
431    }
432
433    #[test]
434    fn test_primary_version() {
435        let update = Update::snapshot(Version::new("v1"), "body");
436        assert_eq!(update.primary_version(), Some(&Version::new("v1")));
437    }
438
439    #[test]
440    fn test_body_str() {
441        let update = Update::snapshot(Version::new("v1"), "hello");
442        assert_eq!(update.body_str(), Some("hello"));
443    }
444
445    #[test]
446    fn test_with_parents() {
447        let update = Update::snapshot(Version::new("v3"), "merged")
448            .with_parents(vec![Version::new("v1"), Version::new("v2")]);
449        assert_eq!(update.parents.len(), 2);
450    }
451
452    #[test]
453    fn test_with_header() {
454        let update = Update::snapshot(Version::new("v1"), "data").with_header("X-Custom", "value");
455        assert_eq!(
456            update.extra_headers.get("X-Custom"),
457            Some(&"value".to_string())
458        );
459    }
460
461    #[test]
462    fn test_to_json() {
463        let update = Update::snapshot(Version::new("v1"), "data")
464            .with_parent(Version::new("v0"))
465            .with_merge_type("diamond");
466        let json = update.to_json();
467        assert!(json.get("version").is_some());
468        assert!(json.get("parents").is_some());
469        assert!(json.get("body").is_some());
470        assert!(json.get("merge_type").is_some());
471    }
472
473    #[test]
474    fn test_default() {
475        let update = Update::default();
476        assert!(update.version.is_empty());
477        assert!(update.parents.is_empty());
478        assert_eq!(update.status, 200);
479    }
480
481    #[test]
482    fn test_subscription_snapshot() {
483        let update = Update::subscription_snapshot(Version::new("v1"), "data");
484        assert_eq!(update.status, 209);
485        assert!(update.is_snapshot());
486        assert!(!update.is_patched());
487    }
488
489    #[test]
490    fn test_subscription_patched() {
491        let update =
492            Update::subscription_patched(Version::new("v2"), vec![Patch::json(".field", "value")]);
493        assert_eq!(update.status, 209);
494        assert!(update.is_patched());
495        assert!(!update.is_snapshot());
496    }
497
498    #[test]
499    fn test_subscription_with_parents() {
500        let update = Update::subscription_snapshot(Version::new("v2"), "data")
501            .with_parent(Version::new("v1"));
502        assert_eq!(update.status, 209);
503        assert_eq!(update.parents.len(), 1);
504    }
505
506    #[test]
507    fn test_zero_length_body() {
508        // Zero-length bodies are valid per spec Section 4
509        let update = Update::snapshot(Version::new("v1"), "");
510        assert!(update.body.is_some());
511        assert_eq!(update.body.as_ref().unwrap().len(), 0);
512    }
513
514    #[test]
515    fn test_zero_length_subscription() {
516        // Zero-length subscription updates are valid (e.g., deletion marker)
517        let update = Update::subscription_snapshot(Version::new("v1"), Bytes::new());
518        assert_eq!(update.status, 209);
519        assert!(update.body.is_some());
520        assert_eq!(update.body.as_ref().unwrap().len(), 0);
521    }
522}