OXYGENHUB
# ac4af91530fc oxygen / crates / oxy-core / src / oplog.rs
5.0 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month ago source blame history raw
1
//! Operation log objects (spec §12.1): every mutation of local repository
2
//! state records an `Op` whose `view` is the complete set of references and
3
//! workspaces afterwards.
4
//!
5
//! Encoding: `bookmarks` is an array of `[name, [heads]]` sorted by name and
6
//! `workspaces` an array of `[workspace_id, change_hash]` sorted by id, since
7
//! canonical maps carry only integer keys.
8
9
use crate::cbor::{self, Value};
10
use crate::hash::{hash_object, ChangeHash, ObjectRef, ObjectType};
11
use crate::ids::WorkspaceId;
12
use crate::objects::{array, bytes, check_version, field, oref, text, Hlc, ObjectError, FORMAT_VERSION};
13
use std::collections::BTreeMap;
14
15
#[derive(Clone, Debug, Default, PartialEq, Eq)]
16
pub struct View {
17
    pub bookmarks: BTreeMap<String, Vec<ChangeHash>>,
18
    pub workspaces: BTreeMap<WorkspaceId, ChangeHash>,
19
}
20
21
#[derive(Clone, Debug, PartialEq, Eq)]
22
pub struct Op {
23
    pub parents: Vec<ObjectRef>,
24
    pub hlc: Hlc,
25
    pub command: String,
26
    pub view: View,
27
}
28
29
impl View {
30
    fn to_value(&self) -> Value {
31
        Value::map([
32
            (
33
                0,
34
                Value::Array(
35
                    self.bookmarks
36
                        .iter()
37
                        .map(|(n, hs)| {
38
                            Value::Array(vec![
39
                                Value::Text(n.clone()),
40
                                Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
41
                            ])
42
                        })
43
                        .collect(),
44
                ),
45
            ),
46
            (
47
                1,
48
                Value::Array(
49
                    self.workspaces
50
                        .iter()
51
                        .map(|(w, h)| Value::Array(vec![Value::Bytes(w.0.to_vec()), Value::Bytes(h.0.to_vec())]))
52
                        .collect(),
53
                ),
54
            ),
55
        ])
56
    }
57
58
    fn from_value(v: &Value) -> Result<View, ObjectError> {
59
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("view: expected map".into()))?;
60
        let mut view = View::default();
61
        for b in array(field(m, 0, "bookmarks")?, "bookmarks")? {
62
            let pair = array(b, "bookmark")?;
63
            if pair.len() != 2 {
64
                return Err(ObjectError::Schema("bookmark: expected [name, heads]".into()));
65
            }
66
            let name = text(&pair[0], "bookmark name")?.to_owned();
67
            let mut heads = array(&pair[1], "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?;
68
            heads.sort();
69
            heads.dedup();
70
            view.bookmarks.insert(name, heads);
71
        }
72
        for w in array(field(m, 1, "workspaces")?, "workspaces")? {
73
            let pair = array(w, "workspace")?;
74
            if pair.len() != 2 {
75
                return Err(ObjectError::Schema("workspace: expected [id, change]".into()));
76
            }
77
            let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?)
78
                .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?;
79
            view.workspaces.insert(id, oref(&pair[1], "workspace change")?);
80
        }
81
        Ok(view)
82
    }
83
}
84
85
impl Op {
86
    pub fn to_value(&self) -> Value {
87
        Value::map([
88
            (0, Value::Uint(FORMAT_VERSION)),
89
            (1, Value::Array(self.parents.iter().map(|p| Value::Bytes(p.0.to_vec())).collect())),
90
            (2, self.hlc.to_value()),
91
            (3, Value::Text(self.command.clone())),
92
            (4, self.view.to_value()),
93
        ])
94
    }
95
    pub fn encode(&self) -> Vec<u8> {
96
        cbor::encode(&self.to_value())
97
    }
98
    pub fn hash(&self) -> ObjectRef {
99
        hash_object(ObjectType::Op, &self.encode())
100
    }
101
    pub fn decode(b: &[u8]) -> Result<Op, ObjectError> {
102
        let v = cbor::decode(b)?;
103
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("op: expected map".into()))?;
104
        check_version(m)?;
105
        Ok(Op {
106
            parents: array(field(m, 1, "parents")?, "parents")?.iter().map(|p| oref(p, "parent")).collect::<Result<Vec<_>, _>>()?,
107
            hlc: Hlc::from_value(field(m, 2, "hlc")?)?,
108
            command: text(field(m, 3, "command")?, "command")?.to_owned(),
109
            view: View::from_value(field(m, 4, "view")?)?,
110
        })
111
    }
112
}
113
114
#[cfg(test)]
115
mod tests {
116
    use super::*;
117
118
    #[test]
119
    fn op_round_trip() {
120
        let mut view = View::default();
121
        view.bookmarks.insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]);
122
        view.bookmarks.insert("dev".into(), vec![]);
123
        view.workspaces.insert(WorkspaceId([5; 16]), ObjectRef([9; 32]));
124
        let op = Op { parents: vec![ObjectRef([7; 32])], hlc: Hlc { wall_ms: 5, logical: 1 }, command: "oxy commit".into(), view };
125
        let d = Op::decode(&op.encode()).unwrap();
126
        // Heads are normalized to sorted order on decode.
127
        assert_eq!(d.view.bookmarks["main"], vec![ObjectRef([1; 32]), ObjectRef([2; 32])]);
128
        assert_eq!(d.command, op.command);
129
        assert_eq!(d.parents, op.parents);
130
        assert_eq!(d.hash(), Op::decode(&d.encode()).unwrap().hash());
131
    }
132
}