Skip to main content

dotloom_document/
migrate.rs

1//! Document schema versions and migrations.
2//!
3//! The schema version is independent from package versions and the protocol
4//! version. Each migration step transforms the raw JSON of schema `n` into schema
5//! `n + 1`; unknown fields are carried through untouched.
6
7use serde_json::Value;
8
9use crate::{DocError, Document, Limits};
10
11/// Current document schema version.
12pub const SCHEMA_VERSION: u32 = 1;
13
14/// Oldest schema version that can be migrated.
15pub const OLDEST_SUPPORTED_SCHEMA: u32 = 1;
16
17/// A note produced while migrating.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct MigrationNote {
20    /// Schema the step started from.
21    pub from: u32,
22    /// Human readable description.
23    pub message: String,
24}
25
26/// Schema version of a raw document.
27pub fn schema_of(v: &Value) -> Result<u32, DocError> {
28    let s =
29        v.get("schema").and_then(Value::as_u64).ok_or_else(|| DocError::Malformed("missing `schema` field".into()))?;
30    u32::try_from(s).map_err(|_| DocError::Malformed("schema out of range".into()))
31}
32
33type Step = fn(Value) -> Result<(Value, String), DocError>;
34
35/// Migration steps indexed by source version (`STEPS[i]` migrates `OLDEST + i`).
36const STEPS: &[Step] = &[];
37
38/// Migrate a raw document to [`SCHEMA_VERSION`].
39pub fn migrate(mut v: Value) -> Result<(Value, Vec<MigrationNote>), DocError> {
40    let mut schema = schema_of(&v)?;
41    if schema > SCHEMA_VERSION {
42        return Err(DocError::FutureSchema { found: schema, supported: SCHEMA_VERSION });
43    }
44    if schema < OLDEST_SUPPORTED_SCHEMA {
45        return Err(DocError::UnsupportedSchema(schema));
46    }
47    let mut notes = Vec::new();
48    while schema < SCHEMA_VERSION {
49        let idx = (schema - OLDEST_SUPPORTED_SCHEMA) as usize;
50        let step = STEPS.get(idx).ok_or(DocError::UnsupportedSchema(schema))?;
51        let (nv, message) = step(v)?;
52        notes.push(MigrationNote { from: schema, message });
53        v = nv;
54        schema += 1;
55        if let Some(obj) = v.as_object_mut() {
56            obj.insert("schema".into(), Value::from(schema));
57        }
58    }
59    Ok((v, notes))
60}
61
62/// Maximum JSON nesting depth accepted for documents.
63pub const MAX_JSON_DEPTH: usize = 128;
64
65fn depth(v: &Value, d: usize) -> usize {
66    if d > MAX_JSON_DEPTH {
67        return d;
68    }
69    match v {
70        Value::Array(a) => a.iter().map(|x| depth(x, d + 1)).max().unwrap_or(d + 1),
71        Value::Object(o) => o.values().map(|x| depth(x, d + 1)).max().unwrap_or(d + 1),
72        _ => d,
73    }
74}
75
76impl Document {
77    /// Parse, migrate and validate a document from JSON text.
78    ///
79    /// Documents of the current schema are deserialized directly; only older
80    /// schemas go through the JSON tree the migration steps work on (the tree costs
81    /// several times the document's own memory: ~150 MB for 100 000 shapes). Nesting
82    /// depth stays bounded by `serde_json`'s recursion limit (128) on both paths.
83    pub fn from_json_str(s: &str, limits: &Limits) -> Result<(Self, Vec<MigrationNote>), DocError> {
84        Self::from_json_slice(s.as_bytes(), limits)
85    }
86
87    /// [`Document::from_json_str`] on UTF-8 bytes.
88    pub fn from_json_slice(bytes: &[u8], limits: &Limits) -> Result<(Self, Vec<MigrationNote>), DocError> {
89        #[derive(serde::Deserialize)]
90        struct Peek {
91            schema: Option<u64>,
92        }
93        let peek: Peek = serde_json::from_slice(bytes).map_err(|e| DocError::Malformed(e.to_string()))?;
94        if peek.schema == Some(u64::from(SCHEMA_VERSION)) {
95            let doc: Self = serde_json::from_slice(bytes).map_err(|e| DocError::Malformed(e.to_string()))?;
96            if let Some(first) = doc.validate_all(limits).into_iter().next() {
97                return Err(first);
98            }
99            return Ok((doc, Vec::new()));
100        }
101        let v: Value = serde_json::from_slice(bytes).map_err(|e| DocError::Malformed(e.to_string()))?;
102        Self::from_json_value(v, limits)
103    }
104
105    /// Migrate and validate a parsed JSON value.
106    pub fn from_json_value(v: Value, limits: &Limits) -> Result<(Self, Vec<MigrationNote>), DocError> {
107        if depth(&v, 0) > MAX_JSON_DEPTH {
108            return Err(DocError::LimitExceeded(format!("JSON nesting deeper than {MAX_JSON_DEPTH}")));
109        }
110        let (v, notes) = migrate(v)?;
111        let doc: Self = serde_json::from_value(v).map_err(|e| DocError::Malformed(e.to_string()))?;
112        if let Some(first) = doc.validate_all(limits).into_iter().next() {
113            return Err(first);
114        }
115        Ok((doc, notes))
116    }
117
118    /// Serialize to pretty JSON.
119    pub fn to_json_string(&self) -> Result<String, DocError> {
120        serde_json::to_string_pretty(self).map_err(|e| DocError::Malformed(e.to_string()))
121    }
122}