Stub out backend interface
This commit is contained in:
parent
4c428702f6
commit
2afcd1bbb6
5 changed files with 560 additions and 2 deletions
|
@ -3,5 +3,5 @@
|
|||
members = [
|
||||
"automerge",
|
||||
"automerge-backend",
|
||||
"automege-wasm",
|
||||
"automerge-wasm",
|
||||
]
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "automerge-backend"
|
||||
version = "0.1.0"
|
||||
version = "0.0.1"
|
||||
authors = ["Alex Good <alex@memoryandthought.me>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "^1.0", features=["derive"] }
|
||||
serde_json = "^1.0"
|
||||
uuid = { version = "^0.5.1", features=["v4"] }
|
||||
|
|
42
automerge-backend/src/error.rs
Normal file
42
automerge-backend/src/error.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use crate::protocol::ObjectID;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AutomergeError {
|
||||
DuplicateObjectError,
|
||||
MissingObjectError(ObjectID),
|
||||
InvalidLinkTarget,
|
||||
NotImplemented(String),
|
||||
InvalidChange(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AutomergeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AutomergeError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidElementID(pub String);
|
||||
|
||||
impl fmt::Display for InvalidElementID {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for InvalidElementID {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidChangeRequest(pub String);
|
||||
|
||||
impl Error for InvalidChangeRequest {}
|
||||
|
||||
impl fmt::Display for InvalidChangeRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,52 @@
|
|||
mod protocol;
|
||||
mod error;
|
||||
|
||||
use crate::protocol::{Change, ActorID, Clock};
|
||||
|
||||
pub struct Backend {
|
||||
}
|
||||
|
||||
pub struct Patch {}
|
||||
|
||||
impl Backend {
|
||||
pub fn init() -> Backend {
|
||||
Backend{}
|
||||
}
|
||||
|
||||
pub fn apply_changes(&mut self, _changes: Vec<Change>) -> Patch {
|
||||
Patch{}
|
||||
}
|
||||
|
||||
pub fn apply_local_change(&mut self, _change: Change) -> Patch {
|
||||
Patch{}
|
||||
}
|
||||
|
||||
pub fn get_patch(&self) -> Patch {
|
||||
Patch{}
|
||||
}
|
||||
|
||||
pub fn get_changes(&self) -> Vec<Change> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn get_changes_for_actor_id(&self, _actor_id: ActorID) -> Vec<Change> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn get_missing_changes(&self, _clock: Clock) -> Vec<Change> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn get_missing_deps(&self) -> Clock {
|
||||
Clock::empty()
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, _remote: &Backend) -> Patch {
|
||||
Patch{}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
|
|
464
automerge-backend/src/protocol.rs
Normal file
464
automerge-backend/src/protocol.rs
Normal file
|
@ -0,0 +1,464 @@
|
|||
//! This module contains types which are deserialized from the changes which
|
||||
//! are produced by the automerge JS library. Given the following code
|
||||
//!
|
||||
//! ```javascript
|
||||
//! doc = ... // create and edit an automerge document
|
||||
//! let changes = Automerge.getHistory(doc).map(h => h.change)
|
||||
//! console.log(JSON.stringify(changes, null, 4))
|
||||
//! ```
|
||||
//!
|
||||
//! The output of this can then be deserialized like so
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use automerge::Change;
|
||||
//! let changes_str = "<paste the contents of the output here>";
|
||||
//! let changes: Vec<Change> = serde_json::from_str(changes_str).unwrap();
|
||||
//! ```
|
||||
use core::cmp::max;
|
||||
use serde::de;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::cmp::{Ordering, PartialOrd};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::error;
|
||||
|
||||
#[derive(Eq, PartialEq, Debug, Hash, Clone)]
|
||||
pub enum ObjectID {
|
||||
ID(String),
|
||||
Root,
|
||||
}
|
||||
|
||||
impl ObjectID {
|
||||
fn parse(s: &str) -> ObjectID {
|
||||
match s {
|
||||
"00000000-0000-0000-0000-000000000000" => ObjectID::Root,
|
||||
_ => ObjectID::ID(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ObjectID {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(ObjectID::parse(&s))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ObjectID {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let id_str = match self {
|
||||
ObjectID::Root => "00000000-0000-0000-0000-000000000000",
|
||||
ObjectID::ID(id) => id,
|
||||
};
|
||||
serializer.serialize_str(id_str)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Hash, Clone)]
|
||||
pub struct Key(pub String);
|
||||
#[derive(Deserialize, Serialize, Eq, PartialEq, Hash, Debug, Clone, PartialOrd, Ord)]
|
||||
pub struct ActorID(pub String);
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
|
||||
pub struct Clock(pub HashMap<ActorID, u32>);
|
||||
|
||||
impl Clock {
|
||||
pub fn empty() -> Clock {
|
||||
Clock(HashMap::new())
|
||||
}
|
||||
|
||||
pub(crate) fn with_dependency(&self, actor_id: &ActorID, new_seq: u32) -> Clock {
|
||||
let mut result = self.0.clone();
|
||||
result.insert(actor_id.clone(), new_seq);
|
||||
Clock(result)
|
||||
}
|
||||
|
||||
pub fn upper_bound(&self, other: &Clock) -> Clock {
|
||||
let mut result: HashMap<ActorID, u32> = HashMap::new();
|
||||
self.0.iter().for_each(|(actor_id, seq)| {
|
||||
result.insert(
|
||||
actor_id.clone(),
|
||||
max(*seq, *other.0.get(actor_id).unwrap_or(&(0 as u32))),
|
||||
);
|
||||
});
|
||||
other.0.iter().for_each(|(actor_id, seq)| {
|
||||
result.insert(
|
||||
actor_id.clone(),
|
||||
max(*seq, *self.0.get(actor_id).unwrap_or(&(0 as u32))),
|
||||
);
|
||||
});
|
||||
Clock(result)
|
||||
}
|
||||
|
||||
pub fn is_before_or_concurrent_with(&self, other: &Clock) -> bool {
|
||||
other
|
||||
.0
|
||||
.iter()
|
||||
.all(|(actor_id, seq)| self.0.get(actor_id).unwrap_or(&0) >= seq)
|
||||
}
|
||||
|
||||
pub(crate) fn seq_for(&self, actor_id: &ActorID) -> u32 {
|
||||
*self.0.get(actor_id).unwrap_or(&0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum PrimitiveValue {
|
||||
Str(String),
|
||||
Number(f64),
|
||||
Boolean(bool),
|
||||
Null,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Clone)]
|
||||
pub enum ElementID {
|
||||
Head,
|
||||
SpecificElementID(ActorID, u32),
|
||||
}
|
||||
|
||||
impl ElementID {
|
||||
pub fn as_key(&self) -> Key {
|
||||
match self {
|
||||
ElementID::Head => Key("_head".to_string()),
|
||||
ElementID::SpecificElementID(actor_id, elem) => Key(format!("{}:{}", actor_id.0, elem)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ElementID {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
ElementID::from_str(&s).map_err(|_| de::Error::custom("invalid element ID"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ElementID {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
ElementID::Head => serializer.serialize_str("_head"),
|
||||
ElementID::SpecificElementID(actor_id, elem) => {
|
||||
serializer.serialize_str(&format!("{}:{}", actor_id.0, elem))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ElementID {
|
||||
type Err = error::InvalidElementID;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"_head" => Ok(ElementID::Head),
|
||||
id => {
|
||||
let components: Vec<&str> = id.split(':').collect();
|
||||
match components.as_slice() {
|
||||
[actor_id, elem_str] => {
|
||||
let elem = u32::from_str(elem_str)
|
||||
.map_err(|_| error::InvalidElementID(id.to_string()))?;
|
||||
Ok(ElementID::SpecificElementID(
|
||||
ActorID((*actor_id).to_string()),
|
||||
elem,
|
||||
))
|
||||
}
|
||||
_ => Err(error::InvalidElementID(id.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ElementID {
|
||||
fn partial_cmp(&self, other: &ElementID) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ElementID {
|
||||
fn cmp(&self, other: &ElementID) -> Ordering {
|
||||
match (self, other) {
|
||||
(ElementID::Head, ElementID::Head) => Ordering::Equal,
|
||||
(ElementID::Head, _) => Ordering::Less,
|
||||
(_, ElementID::Head) => Ordering::Greater,
|
||||
(
|
||||
ElementID::SpecificElementID(self_actor, self_elem),
|
||||
ElementID::SpecificElementID(other_actor, other_elem),
|
||||
) => {
|
||||
if self_elem == other_elem {
|
||||
self_actor.cmp(other_actor)
|
||||
} else {
|
||||
self_elem.cmp(other_elem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
|
||||
pub enum DataType {
|
||||
#[serde(rename = "counter")]
|
||||
Counter,
|
||||
#[serde(rename = "timestamp")]
|
||||
Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
|
||||
#[serde(tag = "action")]
|
||||
pub enum Operation {
|
||||
#[serde(rename = "makeMap")]
|
||||
MakeMap {
|
||||
#[serde(rename = "obj")]
|
||||
object_id: ObjectID,
|
||||
},
|
||||
#[serde(rename = "makeList")]
|
||||
MakeList {
|
||||
#[serde(rename = "obj")]
|
||||
object_id: ObjectID,
|
||||
},
|
||||
#[serde(rename = "makeText")]
|
||||
MakeText {
|
||||
#[serde(rename = "obj")]
|
||||
object_id: ObjectID,
|
||||
},
|
||||
#[serde(rename = "makeTable")]
|
||||
MakeTable {
|
||||
#[serde(rename = "obj")]
|
||||
object_id: ObjectID,
|
||||
},
|
||||
#[serde(rename = "ins")]
|
||||
Insert {
|
||||
#[serde(rename = "obj")]
|
||||
list_id: ObjectID,
|
||||
key: ElementID,
|
||||
elem: u32,
|
||||
},
|
||||
#[serde(rename = "set")]
|
||||
Set {
|
||||
#[serde(rename = "obj")]
|
||||
object_id: ObjectID,
|
||||
key: Key,
|
||||
value: PrimitiveValue,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
datatype: Option<DataType>,
|
||||
},
|
||||
#[serde(rename = "link")]
|
||||
Link {
|
||||
#[serde(rename = "obj")]
|
||||
object_id: ObjectID,
|
||||
key: Key,
|
||||
value: ObjectID,
|
||||
},
|
||||
#[serde(rename = "del")]
|
||||
Delete {
|
||||
#[serde(rename = "obj")]
|
||||
object_id: ObjectID,
|
||||
key: Key,
|
||||
},
|
||||
#[serde(rename = "inc")]
|
||||
Increment {
|
||||
#[serde(rename = "obj")]
|
||||
object_id: ObjectID,
|
||||
key: Key,
|
||||
value: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
|
||||
pub struct Change {
|
||||
#[serde(rename = "actor")]
|
||||
pub(crate) actor_id: ActorID,
|
||||
#[serde(rename = "ops")]
|
||||
pub(crate) operations: Vec<Operation>,
|
||||
pub(crate) seq: u32,
|
||||
pub(crate) message: Option<String>,
|
||||
#[serde(rename = "deps")]
|
||||
pub(crate) dependencies: Clock,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
#[test]
|
||||
fn test_deserializing_operations() {
|
||||
let json_str = r#"{
|
||||
"ops": [
|
||||
{
|
||||
"action": "makeMap",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945"
|
||||
},
|
||||
{
|
||||
"action": "makeList",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945"
|
||||
},
|
||||
{
|
||||
"action": "makeText",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945"
|
||||
},
|
||||
{
|
||||
"action": "makeTable",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945"
|
||||
},
|
||||
{
|
||||
"action": "ins",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945",
|
||||
"key": "someactorid:6",
|
||||
"elem": 5
|
||||
},
|
||||
{
|
||||
"action": "ins",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945",
|
||||
"key": "_head",
|
||||
"elem": 6
|
||||
},
|
||||
{
|
||||
"action": "set",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945",
|
||||
"key": "sometimestamp",
|
||||
"value": 123456,
|
||||
"datatype": "timestamp"
|
||||
},
|
||||
{
|
||||
"action": "set",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945",
|
||||
"key": "somekeyid",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"action": "set",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945",
|
||||
"key": "somekeyid",
|
||||
"value": 123
|
||||
},
|
||||
{
|
||||
"action": "set",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945",
|
||||
"key": "somekeyid",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"action": "link",
|
||||
"obj": "00000000-0000-0000-0000-000000000000",
|
||||
"key": "cards",
|
||||
"value": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945"
|
||||
},
|
||||
{
|
||||
"action": "del",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945",
|
||||
"key": "somekey"
|
||||
},
|
||||
{
|
||||
"action": "inc",
|
||||
"obj": "2ed3ffe8-0ff3-4671-9777-aa16c3e09945",
|
||||
"key": "somekey",
|
||||
"value": 123
|
||||
}
|
||||
],
|
||||
"actor": "741e7221-11cc-4ef8-86ee-4279011569fd",
|
||||
"seq": 1,
|
||||
"deps": {
|
||||
"someid": 0
|
||||
},
|
||||
"message": "Initialization"
|
||||
}"#;
|
||||
let change: Change = serde_json::from_str(&json_str).unwrap();
|
||||
assert_eq!(
|
||||
change,
|
||||
Change {
|
||||
actor_id: ActorID("741e7221-11cc-4ef8-86ee-4279011569fd".to_string()),
|
||||
operations: vec![
|
||||
Operation::MakeMap {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string())
|
||||
},
|
||||
Operation::MakeList {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string())
|
||||
},
|
||||
Operation::MakeText {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string())
|
||||
},
|
||||
Operation::MakeTable {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string())
|
||||
},
|
||||
Operation::Insert {
|
||||
list_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string()),
|
||||
key: ElementID::SpecificElementID(ActorID("someactorid".to_string()), 6),
|
||||
elem: 5,
|
||||
},
|
||||
Operation::Insert {
|
||||
list_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string()),
|
||||
key: ElementID::Head,
|
||||
elem: 6,
|
||||
},
|
||||
Operation::Set {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string()),
|
||||
key: Key("sometimestamp".to_string()),
|
||||
value: PrimitiveValue::Number(123_456.0),
|
||||
datatype: Some(DataType::Timestamp)
|
||||
},
|
||||
Operation::Set {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string()),
|
||||
key: Key("somekeyid".to_string()),
|
||||
value: PrimitiveValue::Boolean(true),
|
||||
datatype: None
|
||||
},
|
||||
Operation::Set {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string()),
|
||||
key: Key("somekeyid".to_string()),
|
||||
value: PrimitiveValue::Number(123.0),
|
||||
datatype: None,
|
||||
},
|
||||
Operation::Set {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string()),
|
||||
key: Key("somekeyid".to_string()),
|
||||
value: PrimitiveValue::Null,
|
||||
datatype: None,
|
||||
},
|
||||
Operation::Link {
|
||||
object_id: ObjectID::Root,
|
||||
key: Key("cards".to_string()),
|
||||
value: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string())
|
||||
},
|
||||
Operation::Delete {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string()),
|
||||
key: Key("somekey".to_string())
|
||||
},
|
||||
Operation::Increment {
|
||||
object_id: ObjectID::ID("2ed3ffe8-0ff3-4671-9777-aa16c3e09945".to_string()),
|
||||
key: Key("somekey".to_string()),
|
||||
value: 123.0,
|
||||
}
|
||||
],
|
||||
seq: 1,
|
||||
message: Some("Initialization".to_string()),
|
||||
dependencies: Clock(HashMap::from_iter(vec![(ActorID("someid".to_string()), 0)]))
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_elementid() {
|
||||
let json_str = "\"_head\"";
|
||||
let elem: ElementID = serde_json::from_str(json_str).unwrap();
|
||||
assert_eq!(elem, ElementID::Head);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_elementid() {
|
||||
let result = serde_json::to_value(ElementID::Head).unwrap();
|
||||
assert_eq!(result, serde_json::Value::String("_head".to_string()));
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue