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 attachments: HashMap<String, AttachmentMeta> = obj
123            .remove("_attachments")
124            .map(|v| serde_json::from_value(v).unwrap_or_default())
125            .unwrap_or_default();
126
127        Ok(Document {
128            id,
129            rev,
130            deleted,
131            data: value,
132            attachments,
133        })
134    }
135
136    /// Convert back to a JSON value with CouchDB underscore fields.
137    pub fn to_json(&self) -> serde_json::Value {
138        let mut obj = match &self.data {
139            serde_json::Value::Object(m) => m.clone(),
140            _ => serde_json::Map::new(),
141        };
142
143        obj.insert("_id".into(), serde_json::Value::String(self.id.clone()));
144
145        if let Some(rev) = &self.rev {
146            obj.insert("_rev".into(), serde_json::Value::String(rev.to_string()));
147        }
148
149        if self.deleted {
150            obj.insert("_deleted".into(), serde_json::Value::Bool(true));
151        }
152
153        if !self.attachments.is_empty() {
154            obj.insert(
155                "_attachments".into(),
156                serde_json::to_value(&self.attachments).unwrap(),
157            );
158        }
159
160        serde_json::Value::Object(obj)
161    }
162}
163
164// ---------------------------------------------------------------------------
165// DocumentMetadata — stored in the database alongside the rev tree
166// ---------------------------------------------------------------------------
167
168/// Internal metadata stored per document in the adapter.
169#[derive(Debug, Clone)]
170pub struct DocMetadata {
171    pub id: String,
172    pub rev_tree: RevTree,
173    pub seq: u64,
174}
175
176// ---------------------------------------------------------------------------
177// Option / response types shared across the crate
178// ---------------------------------------------------------------------------
179
180#[derive(Debug, Clone, Default)]
181pub struct GetOptions {
182    /// Retrieve a specific revision.
183    pub rev: Option<String>,
184    /// Include conflicting revisions in `_conflicts`.
185    pub conflicts: bool,
186    /// Return all open (leaf) revisions.
187    pub open_revs: Option<OpenRevs>,
188    /// Include full revision history.
189    pub revs: bool,
190}
191
192#[derive(Debug, Clone)]
193pub enum OpenRevs {
194    All,
195    Specific(Vec<String>),
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct PutResponse {
200    pub ok: bool,
201    pub id: String,
202    pub rev: String,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct DocResult {
207    pub ok: bool,
208    pub id: String,
209    pub rev: Option<String>,
210    pub error: Option<String>,
211    pub reason: Option<String>,
212}
213
214#[derive(Debug, Clone, Default)]
215pub struct BulkDocsOptions {
216    /// When false (replication), accept revisions as-is.
217    /// When true (default), generate new revisions and check conflicts.
218    pub new_edits: bool,
219}
220
221impl BulkDocsOptions {
222    pub fn new() -> Self {
223        Self { new_edits: true }
224    }
225
226    pub fn replication() -> Self {
227        Self { new_edits: false }
228    }
229}
230
231#[derive(Debug, Clone, Default)]
232pub struct AllDocsOptions {
233    pub start_key: Option<String>,
234    pub end_key: Option<String>,
235    pub key: Option<String>,
236    pub keys: Option<Vec<String>>,
237    pub include_docs: bool,
238    pub descending: bool,
239    pub skip: u64,
240    pub limit: Option<u64>,
241    pub inclusive_end: bool,
242}
243
244impl AllDocsOptions {
245    pub fn new() -> Self {
246        Self {
247            inclusive_end: true,
248            ..Default::default()
249        }
250    }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct AllDocsRow {
255    pub id: String,
256    pub key: String,
257    pub value: AllDocsRowValue,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub doc: Option<serde_json::Value>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct AllDocsRowValue {
264    pub rev: String,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub deleted: Option<bool>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct AllDocsResponse {
271    pub total_rows: u64,
272    pub offset: u64,
273    pub rows: Vec<AllDocsRow>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct DbInfo {
278    pub db_name: String,
279    pub doc_count: u64,
280    pub update_seq: Seq,
281}
282
283// ---------------------------------------------------------------------------
284// Changes types
285// ---------------------------------------------------------------------------
286
287#[derive(Debug, Clone, Default)]
288pub struct ChangesOptions {
289    pub since: Seq,
290    pub limit: Option<u64>,
291    pub descending: bool,
292    pub include_docs: bool,
293    pub live: bool,
294    pub doc_ids: Option<Vec<String>>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct ChangeEvent {
299    pub seq: Seq,
300    pub id: String,
301    pub changes: Vec<ChangeRev>,
302    #[serde(default)]
303    pub deleted: bool,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub doc: Option<serde_json::Value>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ChangeRev {
310    pub rev: String,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct ChangesResponse {
315    pub results: Vec<ChangeEvent>,
316    pub last_seq: Seq,
317}
318
319// ---------------------------------------------------------------------------
320// Replication-related types
321// ---------------------------------------------------------------------------
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct BulkGetItem {
325    pub id: String,
326    pub rev: Option<String>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct BulkGetResponse {
331    pub results: Vec<BulkGetResult>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct BulkGetResult {
336    pub id: String,
337    pub docs: Vec<BulkGetDoc>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct BulkGetDoc {
342    pub ok: Option<serde_json::Value>,
343    pub error: Option<BulkGetError>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct BulkGetError {
348    pub id: String,
349    pub rev: String,
350    pub error: String,
351    pub reason: String,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct RevsDiffResponse {
356    #[serde(flatten)]
357    pub results: HashMap<String, RevsDiffResult>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct RevsDiffResult {
362    pub missing: Vec<String>,
363    #[serde(default, skip_serializing_if = "Vec::is_empty")]
364    pub possible_ancestors: Vec<String>,
365}
366
367// ---------------------------------------------------------------------------
368// Sequence type — supports both numeric (local) and opaque string (CouchDB)
369// ---------------------------------------------------------------------------
370
371/// A database sequence identifier.
372///
373/// Local adapters use numeric sequences (0, 1, 2, ...).
374/// CouchDB 3.x uses opaque string sequences that must be passed back as-is.
375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
376#[serde(untagged)]
377pub enum Seq {
378    Num(u64),
379    Str(String),
380}
381
382impl Seq {
383    /// The zero sequence (start from the beginning).
384    pub fn zero() -> Self {
385        Seq::Num(0)
386    }
387
388    /// Extract the numeric value. For opaque strings, parses the numeric
389    /// prefix (e.g., `"13-abc..."` → `13`). Returns 0 if unparseable.
390    pub fn as_num(&self) -> u64 {
391        match self {
392            Seq::Num(n) => *n,
393            Seq::Str(s) => s
394                .split('-')
395                .next()
396                .and_then(|n| n.parse().ok())
397                .unwrap_or(0),
398        }
399    }
400
401    /// Format for use in HTTP query parameters.
402    pub fn to_query_string(&self) -> String {
403        match self {
404            Seq::Num(n) => n.to_string(),
405            Seq::Str(s) => s.clone(),
406        }
407    }
408}
409
410impl Default for Seq {
411    fn default() -> Self {
412        Seq::Num(0)
413    }
414}
415
416impl From<u64> for Seq {
417    fn from(n: u64) -> Self {
418        Seq::Num(n)
419    }
420}
421
422impl std::fmt::Display for Seq {
423    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424        match self {
425            Seq::Num(n) => write!(f, "{}", n),
426            Seq::Str(s) => write!(f, "{}", s),
427        }
428    }
429}
430
431// ---------------------------------------------------------------------------
432// Attachment options
433// ---------------------------------------------------------------------------
434
435#[derive(Debug, Clone, Default)]
436pub struct GetAttachmentOptions {
437    pub rev: Option<String>,
438}
439
440// ---------------------------------------------------------------------------
441// Tests
442// ---------------------------------------------------------------------------
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn revision_display_and_parse() {
450        let rev = Revision::new(3, "abc123".into());
451        assert_eq!(rev.to_string(), "3-abc123");
452
453        let parsed: Revision = "3-abc123".parse().unwrap();
454        assert_eq!(parsed, rev);
455    }
456
457    #[test]
458    fn revision_ordering() {
459        let r1 = Revision::new(1, "aaa".into());
460        let r2 = Revision::new(2, "aaa".into());
461        let r3 = Revision::new(2, "bbb".into());
462        assert!(r1 < r2);
463        assert!(r2 < r3);
464    }
465
466    #[test]
467    fn invalid_revision() {
468        assert!("nope".parse::<Revision>().is_err());
469        assert!("abc-123".parse::<Revision>().is_err());
470    }
471
472    #[test]
473    fn document_from_json_roundtrip() {
474        let json = serde_json::json!({
475            "_id": "doc1",
476            "_rev": "1-abc",
477            "name": "Alice",
478            "age": 30
479        });
480
481        let doc = Document::from_json(json).unwrap();
482        assert_eq!(doc.id, "doc1");
483        assert_eq!(doc.rev.as_ref().unwrap().to_string(), "1-abc");
484        assert_eq!(doc.data["name"], "Alice");
485        assert!(!doc.data.as_object().unwrap().contains_key("_id"));
486
487        let back = doc.to_json();
488        assert_eq!(back["_id"], "doc1");
489        assert_eq!(back["_rev"], "1-abc");
490        assert_eq!(back["name"], "Alice");
491    }
492
493    #[test]
494    fn document_from_json_minimal() {
495        let json = serde_json::json!({"hello": "world"});
496        let doc = Document::from_json(json).unwrap();
497        assert!(doc.id.is_empty());
498        assert!(doc.rev.is_none());
499        assert!(!doc.deleted);
500    }
501
502    #[test]
503    fn bulk_docs_options_defaults() {
504        let opts = BulkDocsOptions::new();
505        assert!(opts.new_edits);
506
507        let repl = BulkDocsOptions::replication();
508        assert!(!repl.new_edits);
509    }
510
511    #[test]
512    fn to_json_deleted_document() {
513        let doc = Document {
514            id: "doc1".into(),
515            rev: Some(Revision::new(2, "def".into())),
516            deleted: true,
517            data: serde_json::json!({}),
518            attachments: HashMap::new(),
519        };
520        let json = doc.to_json();
521        assert_eq!(json["_deleted"], true);
522        assert_eq!(json["_id"], "doc1");
523        assert_eq!(json["_rev"], "2-def");
524    }
525
526    #[test]
527    fn to_json_with_attachments() {
528        let mut attachments = HashMap::new();
529        attachments.insert("file.txt".into(), AttachmentMeta {
530            content_type: "text/plain".into(),
531            digest: "md5-abc".into(),
532            length: 100,
533            stub: true,
534            data: None,
535        });
536        let doc = Document {
537            id: "doc1".into(),
538            rev: None,
539            deleted: false,
540            data: serde_json::json!({"key": "val"}),
541            attachments,
542        };
543        let json = doc.to_json();
544        assert!(json["_attachments"]["file.txt"].is_object());
545        assert_eq!(json["_attachments"]["file.txt"]["content_type"], "text/plain");
546    }
547
548    #[test]
549    fn to_json_non_object_data() {
550        let doc = Document {
551            id: "doc1".into(),
552            rev: None,
553            deleted: false,
554            data: serde_json::json!("just a string"),
555            attachments: HashMap::new(),
556        };
557        let json = doc.to_json();
558        assert_eq!(json["_id"], "doc1");
559    }
560
561    #[test]
562    fn document_from_json_with_deleted_and_attachments() {
563        let json = serde_json::json!({
564            "_id": "doc1",
565            "_rev": "1-abc",
566            "_deleted": true,
567            "_attachments": {
568                "photo.jpg": {
569                    "content_type": "image/jpeg",
570                    "digest": "md5-xyz",
571                    "length": 500,
572                    "stub": true
573                }
574            },
575            "name": "test"
576        });
577        let doc = Document::from_json(json).unwrap();
578        assert!(doc.deleted);
579        assert_eq!(doc.attachments.len(), 1);
580        assert_eq!(doc.attachments["photo.jpg"].content_type, "image/jpeg");
581    }
582
583    #[test]
584    fn document_from_json_not_object() {
585        let json = serde_json::json!("just a string");
586        assert!(Document::from_json(json).is_err());
587    }
588
589    #[test]
590    fn seq_str_as_num() {
591        let seq = Seq::Str("42-g1AAAABXeJzLY".into());
592        assert_eq!(seq.as_num(), 42);
593
594        let seq2 = Seq::Str("not-a-number".into());
595        assert_eq!(seq2.as_num(), 0);
596    }
597
598    #[test]
599    fn seq_to_query_string() {
600        assert_eq!(Seq::Num(5).to_query_string(), "5");
601        let opaque = "13-g1AAAABXeJzLY".to_string();
602        assert_eq!(Seq::Str(opaque.clone()).to_query_string(), opaque);
603    }
604
605    #[test]
606    fn seq_display() {
607        assert_eq!(format!("{}", Seq::Num(42)), "42");
608        assert_eq!(format!("{}", Seq::Str("opaque-seq".into())), "opaque-seq");
609    }
610
611    #[test]
612    fn seq_from_u64() {
613        let seq: Seq = 7u64.into();
614        assert_eq!(seq, Seq::Num(7));
615    }
616}