Skip to main content

rouchdb_core/
document.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::{Result, RouchError};
8use crate::rev_tree::RevTree;
9
10// ---------------------------------------------------------------------------
11// Revision
12// ---------------------------------------------------------------------------
13
14/// A CouchDB revision identifier: `{pos}-{hash}`.
15///
16/// - `pos` is the generation number (starts at 1, increments each edit).
17/// - `hash` is a 32-character hex MD5 digest.
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct Revision {
20    pub pos: u64,
21    pub hash: String,
22}
23
24impl Revision {
25    pub fn new(pos: u64, hash: String) -> Self {
26        Self { pos, hash }
27    }
28}
29
30impl fmt::Display for Revision {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(f, "{}-{}", self.pos, self.hash)
33    }
34}
35
36impl FromStr for Revision {
37    type Err = RouchError;
38
39    fn from_str(s: &str) -> Result<Self> {
40        let (pos_str, hash) = s
41            .split_once('-')
42            .ok_or_else(|| RouchError::InvalidRev(s.to_string()))?;
43        let pos: u64 = pos_str
44            .parse()
45            .map_err(|_| RouchError::InvalidRev(s.to_string()))?;
46        Ok(Revision {
47            pos,
48            hash: hash.to_string(),
49        })
50    }
51}
52
53impl Ord for Revision {
54    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
55        self.pos
56            .cmp(&other.pos)
57            .then_with(|| self.hash.cmp(&other.hash))
58    }
59}
60
61impl PartialOrd for Revision {
62    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
63        Some(self.cmp(other))
64    }
65}
66
67// ---------------------------------------------------------------------------
68// AttachmentMeta
69// ---------------------------------------------------------------------------
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct AttachmentMeta {
73    pub content_type: String,
74    pub digest: String,
75    pub length: u64,
76    #[serde(default)]
77    pub stub: bool,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub data: Option<Vec<u8>>,
80}
81
82// ---------------------------------------------------------------------------
83// Document
84// ---------------------------------------------------------------------------
85
86/// A CouchDB-compatible document.
87#[derive(Debug, Clone)]
88pub struct Document {
89    pub id: String,
90    pub rev: Option<Revision>,
91    pub deleted: bool,
92    pub data: serde_json::Value,
93    pub attachments: HashMap<String, AttachmentMeta>,
94}
95
96impl Document {
97    /// Create a new document from a JSON value.
98    ///
99    /// Extracts `_id`, `_rev`, `_deleted`, and `_attachments` from the value
100    /// and puts the remaining fields in `data`.
101    pub fn from_json(mut value: serde_json::Value) -> Result<Self> {
102        let obj = value
103            .as_object_mut()
104            .ok_or_else(|| RouchError::BadRequest("document must be a JSON object".into()))?;
105
106        let id = obj
107            .remove("_id")
108            .and_then(|v| v.as_str().map(String::from))
109            .unwrap_or_default();
110
111        let rev = obj
112            .remove("_rev")
113            .and_then(|v| v.as_str().map(String::from))
114            .map(|s| s.parse::<Revision>())
115            .transpose()?;
116
117        let deleted = obj
118            .remove("_deleted")
119            .and_then(|v| v.as_bool())
120            .unwrap_or(false);
121
122        let mut attachments: HashMap<String, AttachmentMeta> = HashMap::new();
123        if let Some(att_val) = obj.remove("_attachments")
124            && let Some(att_obj) = att_val.as_object()
125        {
126            for (name, meta) in att_obj {
127                // Strip inline Base64 `data` string before serde parsing
128                // (serde expects Vec<u8> as an array, not a string).
129                let mut meta_for_parse = meta.clone();
130                let inline_b64 = if let Some(obj) = meta_for_parse.as_object_mut() {
131                    match obj.remove("data") {
132                        Some(serde_json::Value::String(s)) => Some(s),
133                        Some(other) => {
134                            obj.insert("data".to_string(), other);
135                            None
136                        }
137                        None => None,
138                    }
139                } else {
140                    None
141                };
142
143                if let Ok(mut att) = serde_json::from_value::<AttachmentMeta>(meta_for_parse) {
144                    // Decode inline Base64 data if present
145                    if att.data.is_none()
146                        && let Some(ref data_str) = inline_b64
147                    {
148                        use base64::Engine;
149                        if let Ok(bytes) =
150                            base64::engine::general_purpose::STANDARD.decode(data_str)
151                        {
152                            att.length = bytes.len() as u64;
153                            att.data = Some(bytes);
154                            att.stub = false;
155                        }
156                    }
157                    attachments.insert(name.clone(), att);
158                }
159            }
160        }
161
162        Ok(Document {
163            id,
164            rev,
165            deleted,
166            data: value,
167            attachments,
168        })
169    }
170
171    /// Convert back to a JSON value with CouchDB underscore fields.
172    pub fn to_json(&self) -> serde_json::Value {
173        let mut obj = match &self.data {
174            serde_json::Value::Object(m) => m.clone(),
175            _ => serde_json::Map::new(),
176        };
177
178        obj.insert("_id".into(), serde_json::Value::String(self.id.clone()));
179
180        if let Some(rev) = &self.rev {
181            obj.insert("_rev".into(), serde_json::Value::String(rev.to_string()));
182        }
183
184        if self.deleted {
185            obj.insert("_deleted".into(), serde_json::Value::Bool(true));
186        }
187
188        if !self.attachments.is_empty()
189            && let Ok(att_json) = serde_json::to_value(&self.attachments)
190        {
191            obj.insert("_attachments".into(), att_json);
192        }
193
194        serde_json::Value::Object(obj)
195    }
196}
197
198// ---------------------------------------------------------------------------
199// DocumentMetadata — stored in the database alongside the rev tree
200// ---------------------------------------------------------------------------
201
202/// Internal metadata stored per document in the adapter.
203#[derive(Debug, Clone)]
204pub struct DocMetadata {
205    pub id: String,
206    pub rev_tree: RevTree,
207    pub seq: u64,
208}
209
210// ---------------------------------------------------------------------------
211// Option / response types shared across the crate
212// ---------------------------------------------------------------------------
213
214#[derive(Debug, Clone, Default)]
215pub struct GetOptions {
216    /// Retrieve a specific revision.
217    pub rev: Option<String>,
218    /// Include conflicting revisions in `_conflicts`.
219    pub conflicts: bool,
220    /// Return all open (leaf) revisions.
221    pub open_revs: Option<OpenRevs>,
222    /// Include full revision history.
223    pub revs: bool,
224    /// Include full revision info with status (available/missing/deleted).
225    pub revs_info: bool,
226    /// If rev is specified and not a leaf, return the latest leaf instead.
227    pub latest: bool,
228    /// Include inline Base64 attachment data.
229    pub attachments: bool,
230}
231
232/// Revision info entry returned when `revs_info` is requested.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct RevInfo {
235    pub rev: String,
236    pub status: String, // "available", "missing", "deleted"
237}
238
239#[derive(Debug, Clone)]
240pub enum OpenRevs {
241    All,
242    Specific(Vec<String>),
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct PutResponse {
247    pub ok: bool,
248    pub id: String,
249    pub rev: String,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct DocResult {
254    pub ok: bool,
255    pub id: String,
256    pub rev: Option<String>,
257    pub error: Option<String>,
258    pub reason: Option<String>,
259}
260
261#[derive(Debug, Clone, Default)]
262pub struct BulkDocsOptions {
263    /// When false (replication), accept revisions as-is.
264    /// When true (default), generate new revisions and check conflicts.
265    pub new_edits: bool,
266}
267
268impl BulkDocsOptions {
269    pub fn new() -> Self {
270        Self { new_edits: true }
271    }
272
273    pub fn replication() -> Self {
274        Self { new_edits: false }
275    }
276}
277
278#[derive(Debug, Clone, Default)]
279pub struct AllDocsOptions {
280    pub start_key: Option<String>,
281    pub end_key: Option<String>,
282    pub key: Option<String>,
283    pub keys: Option<Vec<String>>,
284    pub include_docs: bool,
285    pub descending: bool,
286    pub skip: u64,
287    pub limit: Option<u64>,
288    pub inclusive_end: bool,
289    /// Include `_conflicts` for each document (requires `include_docs`).
290    pub conflicts: bool,
291    /// Include `update_seq` in the response.
292    pub update_seq: bool,
293}
294
295impl AllDocsOptions {
296    pub fn new() -> Self {
297        Self {
298            inclusive_end: true,
299            ..Default::default()
300        }
301    }
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct AllDocsRow {
306    pub id: String,
307    pub key: String,
308    pub value: AllDocsRowValue,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub doc: Option<serde_json::Value>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct AllDocsRowValue {
315    pub rev: String,
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub deleted: Option<bool>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct AllDocsResponse {
322    pub total_rows: u64,
323    pub offset: u64,
324    pub rows: Vec<AllDocsRow>,
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub update_seq: Option<Seq>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct DbInfo {
331    pub db_name: String,
332    pub doc_count: u64,
333    pub update_seq: Seq,
334}
335
336// ---------------------------------------------------------------------------
337// Changes types
338// ---------------------------------------------------------------------------
339
340#[derive(Debug, Clone, Default)]
341pub struct ChangesOptions {
342    pub since: Seq,
343    pub limit: Option<u64>,
344    pub descending: bool,
345    pub include_docs: bool,
346    pub live: bool,
347    pub doc_ids: Option<Vec<String>>,
348    pub selector: Option<serde_json::Value>,
349    /// Include conflicting revisions per change event.
350    pub conflicts: bool,
351    /// Changes style: `MainOnly` (default) returns only winning rev,
352    /// `AllDocs` returns all leaf revisions.
353    pub style: ChangesStyle,
354}
355
356/// Controls which revisions appear in each change event.
357#[derive(Debug, Clone, Default, PartialEq, Eq)]
358pub enum ChangesStyle {
359    /// Default: only the winning revision.
360    #[default]
361    MainOnly,
362    /// All non-deleted leaf revisions.
363    AllDocs,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct ChangeEvent {
368    pub seq: Seq,
369    pub id: String,
370    pub changes: Vec<ChangeRev>,
371    #[serde(default)]
372    pub deleted: bool,
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub doc: Option<serde_json::Value>,
375    /// Conflicting revisions (when `conflicts: true` requested).
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub conflicts: Option<Vec<String>>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct ChangeRev {
382    pub rev: String,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct ChangesResponse {
387    pub results: Vec<ChangeEvent>,
388    pub last_seq: Seq,
389}
390
391// ---------------------------------------------------------------------------
392// Replication-related types
393// ---------------------------------------------------------------------------
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct BulkGetItem {
397    pub id: String,
398    pub rev: Option<String>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct BulkGetResponse {
403    pub results: Vec<BulkGetResult>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct BulkGetResult {
408    pub id: String,
409    pub docs: Vec<BulkGetDoc>,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct BulkGetDoc {
414    pub ok: Option<serde_json::Value>,
415    pub error: Option<BulkGetError>,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct BulkGetError {
420    pub id: String,
421    pub rev: String,
422    pub error: String,
423    pub reason: String,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct RevsDiffResponse {
428    #[serde(flatten)]
429    pub results: HashMap<String, RevsDiffResult>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct RevsDiffResult {
434    pub missing: Vec<String>,
435    #[serde(default, skip_serializing_if = "Vec::is_empty")]
436    pub possible_ancestors: Vec<String>,
437}
438
439// ---------------------------------------------------------------------------
440// Sequence type — supports both numeric (local) and opaque string (CouchDB)
441// ---------------------------------------------------------------------------
442
443/// A database sequence identifier.
444///
445/// Local adapters use numeric sequences (0, 1, 2, ...).
446/// CouchDB 3.x uses opaque string sequences that must be passed back as-is.
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
448#[serde(untagged)]
449pub enum Seq {
450    Num(u64),
451    Str(String),
452}
453
454impl Seq {
455    /// The zero sequence (start from the beginning).
456    pub fn zero() -> Self {
457        Seq::Num(0)
458    }
459
460    /// Extract the numeric value. For opaque strings, parses the numeric
461    /// prefix (e.g., `"13-abc..."` → `13`). Returns 0 if unparseable.
462    pub fn as_num(&self) -> u64 {
463        match self {
464            Seq::Num(n) => *n,
465            Seq::Str(s) => s
466                .split('-')
467                .next()
468                .and_then(|n| n.parse().ok())
469                .unwrap_or(0),
470        }
471    }
472
473    /// Format for use in HTTP query parameters.
474    pub fn to_query_string(&self) -> String {
475        match self {
476            Seq::Num(n) => n.to_string(),
477            Seq::Str(s) => s.clone(),
478        }
479    }
480}
481
482impl Default for Seq {
483    fn default() -> Self {
484        Seq::Num(0)
485    }
486}
487
488impl From<u64> for Seq {
489    fn from(n: u64) -> Self {
490        Seq::Num(n)
491    }
492}
493
494impl std::fmt::Display for Seq {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        match self {
497            Seq::Num(n) => write!(f, "{}", n),
498            Seq::Str(s) => write!(f, "{}", s),
499        }
500    }
501}
502
503// ---------------------------------------------------------------------------
504// Purge types
505// ---------------------------------------------------------------------------
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct PurgeResponse {
509    pub purge_seq: Option<u64>,
510    pub purged: HashMap<String, Vec<String>>,
511}
512
513// ---------------------------------------------------------------------------
514// Security document
515// ---------------------------------------------------------------------------
516
517#[derive(Debug, Clone, Default, Serialize, Deserialize)]
518pub struct SecurityDocument {
519    #[serde(default)]
520    pub admins: SecurityGroup,
521    #[serde(default)]
522    pub members: SecurityGroup,
523}
524
525#[derive(Debug, Clone, Default, Serialize, Deserialize)]
526pub struct SecurityGroup {
527    #[serde(default)]
528    pub names: Vec<String>,
529    #[serde(default)]
530    pub roles: Vec<String>,
531}
532
533// ---------------------------------------------------------------------------
534// Attachment options
535// ---------------------------------------------------------------------------
536
537#[derive(Debug, Clone, Default)]
538pub struct GetAttachmentOptions {
539    pub rev: Option<String>,
540}
541
542// ---------------------------------------------------------------------------
543// Tests
544// ---------------------------------------------------------------------------
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn revision_display_and_parse() {
552        let rev = Revision::new(3, "abc123".into());
553        assert_eq!(rev.to_string(), "3-abc123");
554
555        let parsed: Revision = "3-abc123".parse().unwrap();
556        assert_eq!(parsed, rev);
557    }
558
559    #[test]
560    fn revision_ordering() {
561        let r1 = Revision::new(1, "aaa".into());
562        let r2 = Revision::new(2, "aaa".into());
563        let r3 = Revision::new(2, "bbb".into());
564        assert!(r1 < r2);
565        assert!(r2 < r3);
566    }
567
568    #[test]
569    fn invalid_revision() {
570        assert!("nope".parse::<Revision>().is_err());
571        assert!("abc-123".parse::<Revision>().is_err());
572    }
573
574    #[test]
575    fn document_from_json_roundtrip() {
576        let json = serde_json::json!({
577            "_id": "doc1",
578            "_rev": "1-abc",
579            "name": "Alice",
580            "age": 30
581        });
582
583        let doc = Document::from_json(json).unwrap();
584        assert_eq!(doc.id, "doc1");
585        assert_eq!(doc.rev.as_ref().unwrap().to_string(), "1-abc");
586        assert_eq!(doc.data["name"], "Alice");
587        assert!(!doc.data.as_object().unwrap().contains_key("_id"));
588
589        let back = doc.to_json();
590        assert_eq!(back["_id"], "doc1");
591        assert_eq!(back["_rev"], "1-abc");
592        assert_eq!(back["name"], "Alice");
593    }
594
595    #[test]
596    fn document_from_json_minimal() {
597        let json = serde_json::json!({"hello": "world"});
598        let doc = Document::from_json(json).unwrap();
599        assert!(doc.id.is_empty());
600        assert!(doc.rev.is_none());
601        assert!(!doc.deleted);
602    }
603
604    #[test]
605    fn bulk_docs_options_defaults() {
606        let opts = BulkDocsOptions::new();
607        assert!(opts.new_edits);
608
609        let repl = BulkDocsOptions::replication();
610        assert!(!repl.new_edits);
611    }
612
613    #[test]
614    fn to_json_deleted_document() {
615        let doc = Document {
616            id: "doc1".into(),
617            rev: Some(Revision::new(2, "def".into())),
618            deleted: true,
619            data: serde_json::json!({}),
620            attachments: HashMap::new(),
621        };
622        let json = doc.to_json();
623        assert_eq!(json["_deleted"], true);
624        assert_eq!(json["_id"], "doc1");
625        assert_eq!(json["_rev"], "2-def");
626    }
627
628    #[test]
629    fn to_json_with_attachments() {
630        let mut attachments = HashMap::new();
631        attachments.insert(
632            "file.txt".into(),
633            AttachmentMeta {
634                content_type: "text/plain".into(),
635                digest: "md5-abc".into(),
636                length: 100,
637                stub: true,
638                data: None,
639            },
640        );
641        let doc = Document {
642            id: "doc1".into(),
643            rev: None,
644            deleted: false,
645            data: serde_json::json!({"key": "val"}),
646            attachments,
647        };
648        let json = doc.to_json();
649        assert!(json["_attachments"]["file.txt"].is_object());
650        assert_eq!(
651            json["_attachments"]["file.txt"]["content_type"],
652            "text/plain"
653        );
654    }
655
656    #[test]
657    fn to_json_non_object_data() {
658        let doc = Document {
659            id: "doc1".into(),
660            rev: None,
661            deleted: false,
662            data: serde_json::json!("just a string"),
663            attachments: HashMap::new(),
664        };
665        let json = doc.to_json();
666        assert_eq!(json["_id"], "doc1");
667    }
668
669    #[test]
670    fn document_from_json_with_deleted_and_attachments() {
671        let json = serde_json::json!({
672            "_id": "doc1",
673            "_rev": "1-abc",
674            "_deleted": true,
675            "_attachments": {
676                "photo.jpg": {
677                    "content_type": "image/jpeg",
678                    "digest": "md5-xyz",
679                    "length": 500,
680                    "stub": true
681                }
682            },
683            "name": "test"
684        });
685        let doc = Document::from_json(json).unwrap();
686        assert!(doc.deleted);
687        assert_eq!(doc.attachments.len(), 1);
688        assert_eq!(doc.attachments["photo.jpg"].content_type, "image/jpeg");
689    }
690
691    #[test]
692    fn document_from_json_not_object() {
693        let json = serde_json::json!("just a string");
694        assert!(Document::from_json(json).is_err());
695    }
696
697    #[test]
698    fn seq_str_as_num() {
699        let seq = Seq::Str("42-g1AAAABXeJzLY".into());
700        assert_eq!(seq.as_num(), 42);
701
702        let seq2 = Seq::Str("not-a-number".into());
703        assert_eq!(seq2.as_num(), 0);
704    }
705
706    #[test]
707    fn seq_to_query_string() {
708        assert_eq!(Seq::Num(5).to_query_string(), "5");
709        let opaque = "13-g1AAAABXeJzLY".to_string();
710        assert_eq!(Seq::Str(opaque.clone()).to_query_string(), opaque);
711    }
712
713    #[test]
714    fn seq_display() {
715        assert_eq!(format!("{}", Seq::Num(42)), "42");
716        assert_eq!(format!("{}", Seq::Str("opaque-seq".into())), "opaque-seq");
717    }
718
719    #[test]
720    fn seq_from_u64() {
721        let seq: Seq = 7u64.into();
722        assert_eq!(seq, Seq::Num(7));
723    }
724}