```rust
use std::collections::{BTreeSet, HashMap};
use std::ops::Deref;
use radicle::crypto::Signer;
use radicle::hash;
use radicle::prelude::PublicKey;
use radicle::{cob::Timestamp, crypto::Signature};
use serde::{Deserialize, Serialize};
pub type ChangeId = radicle::hash::Digest;
pub type Author = PublicKey;
/// The `Change` is the unit of replication.
/// Everything that can be done in the system is represented by a `Change` object.
/// Changes are applied to an accumulator to yield a final state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Change {
/// The action carried out by this change.
action: Action,
/// The author of the change.
author: Author,
/// The time at which this change was authored.
timestamp: Timestamp,
/// Other observed changes. (UNUSED)
_observed: BTreeSet<ChangeId>,
}
impl Change {
/// Get the change id.
pub fn id(&self) -> ChangeId {
hash::Digest::new(self.encode())
}
/// Serialize the change into a byte string.
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new());
self.serialize(&mut serializer).unwrap();
buf
}
}
/// Change envelope. Carries signed changes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Envelope {
/// Changes included in this envelope, serialized as JSON.
pub changes: Vec<u8>,
/// Signature over the change, by the change author.
pub signature: Signature,
}
/// An object that can be either present or removed.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Redactable<T> {
/// When the object is present.
Present(T),
/// When the object has been removed.
#[default]
Redacted,
}
/// A comment on a discussion thread.
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Comment {
/// The comment body.
body: String,
/// Thread or comment this is a reaply to.
reply_to: ChangeId,
}
impl Comment {
/// Create a new comment.
pub fn new(body: String, reply_to: ChangeId) -> Self {
Self { body, reply_to }
}
}
/// A discussion thread.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Thread {
/// The id of the thread.
id: ChangeId,
/// The thread title.
title: String,
/// The thread author.
author: Author,
/// The thread timestamp.
timestamp: Timestamp,
/// The comments under the thread.
comments: HashMap<ChangeId, Redactable<Comment>>,
}
impl Deref for Thread {
type Target = HashMap<ChangeId, Redactable<Comment>>;
fn deref(&self) -> &Self::Target {
&self.comments
}
}
/// An action that can be carried out in a change.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub enum Action {
/// Initialize a new thread.
Thread { title: String },
/// Comment on a thread.
Comment { comment: Comment },
/// Redact an action. Not all actions can be redacted.
Redact { id: ChangeId },
}
impl Thread {
pub fn new(root: Change) -> Self {
let id = root.id();
let Action::Thread { title } = root.action else {
panic!("Threads need to be initialized with a `Thread` message");
};
Self {
id,
title,
author: root.author,
timestamp: root.timestamp,
comments: HashMap::default(),
}
}
pub fn clear(&mut self) {
self.comments.clear();
}
pub fn apply(&mut self, msgs: impl IntoIterator<Item = Change>) {
for msg in msgs.into_iter() {
let id = msg.id();
match msg.action {
Action::Comment { comment } => {
match self.comments.get(&id) {
Some(Redactable::Present(_)) => {
// Do nothing, the action was already processed.
}
Some(Redactable::Redacted) => {
// Do nothing, the action was redacted.
}
None => {
self.comments.insert(id, Redactable::Present(comment));
}
}
}
Action::Redact { id } => {
self.comments.insert(id, Redactable::Redacted);
}
Action::Thread { .. } => {
// Ignored
}
}
}
}
}
/// An object that can be used to create and sign changes.
#[derive(Default)]
pub struct Actor<G> {
signer: G,
}
impl<G: Signer> Actor<G> {
/// Create a new thread.
pub fn thread(&self, title: &str, timestamp: Timestamp) -> Change {
self.change(
Action::Thread {
title: title.to_owned(),
},
timestamp,
)
}
/// Create a new comment.
pub fn comment(&self, body: &str, timestamp: Timestamp, parent: ChangeId) -> Change {
self.change(
Action::Comment {
comment: Comment::new(String::from(body), parent),
},
timestamp,
)
}
/// Create a new redaction.
pub fn redact(&self, id: ChangeId, timestamp: Timestamp) -> Change {
self.change(Action::Redact { id }, timestamp)
}
/// Create a new change.
pub fn change(&self, action: Action, timestamp: Timestamp) -> Change {
let author = *self.signer.public_key();
let _observed = BTreeSet::new();
Change {
action,
author,
timestamp,
_observed,
}
}
pub fn sign(&self, changes: impl IntoIterator<Item = Change>) -> Envelope {
let changes = changes.into_iter().collect::<Vec<_>>();
let json = serde_json::to_value(changes).unwrap();
let mut buffer = Vec::new();
let mut serializer = serde_json::Serializer::with_formatter(
&mut buffer,
olpc_cjson::CanonicalFormatter::new(),
);
json.serialize(&mut serializer).unwrap();
let signature = self.signer.sign(&buffer);
Envelope {
changes: buffer,
signature,
}
}
}
```
Tests
```rust
#[cfg(test)]
mod tests {
use itertools::Itertools;
use radicle::crypto::test::signer::MockSigner;
use super::*;
#[test]
fn test_invariants() {
let alice = Actor::<MockSigner>::default();
let bob = Actor::<MockSigner>::default();
let time = Timestamp::now();
let b0 = bob.thread("Dinner Ingredients", time);
let a0 = alice.comment("Ham", time, b0.id());
let a1 = alice.comment("Rye", time, a0.id());
let a2 = alice.comment("Dough", time, a1.id());
let a3 = alice.redact(a1.id(), time);
let t = Thread::new(b0);
assert_order_invariance(&t, [&a0, &a1, &a2, &a3]);
assert_idempotence(&t, [&a0, &a1, &a2, &a3]);
}
fn assert_order_invariance<'a>(t: &Thread, msgs: impl IntoIterator<Item = &'a Change>) {
let msgs = msgs.into_iter().cloned().collect::<Vec<_>>();
let count = msgs.len();
let mut actual = t.clone();
let mut expected = t.clone();
expected.clear();
expected.apply(msgs.clone());
for permutation in msgs.into_iter().permutations(count) {
actual.clear();
actual.apply(permutation);
assert_eq!(actual, expected);
}
}
fn assert_idempotence<'a>(t: &Thread, msgs: impl IntoIterator<Item = &'a Change>) {
let msgs = msgs.into_iter().cloned().collect::<Vec<_>>();
let mut actual = t.clone();
let mut expected = t.clone();
expected.clear();
expected.apply(msgs.clone());
actual.clear();
actual.apply(msgs.clone());
actual.apply(msgs.clone());
actual.apply(msgs);
assert_eq!(actual, expected);
}
}
```