cob: Move tags out of `Thread`

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-12-01 13:21:22 +01:00
parent 64ce15dbe2
commit ce11d69f23
No known key found for this signature in database
3 changed files with 74 additions and 113 deletions

View File

@ -4,7 +4,7 @@ use std::str::FromStr;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use radicle_crdt as crdt; use radicle_crdt as crdt;
use radicle_crdt::clock; use radicle_crdt::clock;
use radicle_crdt::{ChangeId, LWWReg, Max, Semilattice}; use radicle_crdt::{ChangeId, LWWReg, LWWSet, Max, Semilattice};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
@ -76,6 +76,7 @@ pub struct Issue {
// TODO(cloudhead): Title should bias towards shorter strings. // TODO(cloudhead): Title should bias towards shorter strings.
title: LWWReg<Max<String>, clock::Lamport>, title: LWWReg<Max<String>, clock::Lamport>,
status: LWWReg<Max<Status>, clock::Lamport>, status: LWWReg<Max<Status>, clock::Lamport>,
tags: LWWSet<Tag>,
thread: Thread, thread: Thread,
} }
@ -92,6 +93,7 @@ impl Default for Issue {
Self { Self {
title: Max::from(String::default()).into(), title: Max::from(String::default()).into(),
status: Max::from(Status::default()).into(), status: Max::from(Status::default()).into(),
tags: LWWSet::default(),
thread: Thread::default(), thread: Thread::default(),
} }
} }
@ -138,7 +140,7 @@ impl Issue {
} }
pub fn tags(&self) -> impl Iterator<Item = &Tag> { pub fn tags(&self) -> impl Iterator<Item = &Tag> {
self.thread.tags() self.tags.iter()
} }
pub fn author(&self) -> Option<Author> { pub fn author(&self) -> Option<Author> {
@ -164,6 +166,14 @@ impl Issue {
Action::Lifecycle { status } => { Action::Lifecycle { status } => {
self.status.set(status, change.clock); self.status.set(status, change.clock);
} }
Action::Tag { add, remove } => {
for tag in add {
self.tags.insert(tag, change.clock);
}
for tag in remove {
self.tags.remove(tag, change.clock);
}
}
Action::Thread { action } => { Action::Thread { action } => {
self.thread.apply([crdt::Change { self.thread.apply([crdt::Change {
action, action,
@ -221,13 +231,14 @@ impl<'a, 'g> IssueMut<'a, 'g> {
/// Tag an issue. /// Tag an issue.
pub fn tag<G: Signer>( pub fn tag<G: Signer>(
&mut self, &mut self,
tags: impl IntoIterator<Item = Tag>, add: impl IntoIterator<Item = Tag>,
remove: impl IntoIterator<Item = Tag>,
signer: &G, signer: &G,
) -> Result<ChangeId, Error> { ) -> Result<ChangeId, Error> {
let tags = tags.into_iter().collect::<Vec<_>>(); let add = add.into_iter().collect::<Vec<_>>();
let action = Action::Thread { let remove = remove.into_iter().collect::<Vec<_>>();
action: thread::Action::Tag { tags }, let action = Action::Tag { add, remove };
};
self.apply("Tag", action, signer) self.apply("Tag", action, signer)
} }
@ -362,7 +373,7 @@ impl<'a> Issues<'a> {
}; };
issue.comment(description, signer)?; issue.comment(description, signer)?;
issue.tag(tags.to_owned(), signer)?; issue.tag(tags.to_owned(), [], signer)?;
Ok(issue) Ok(issue)
} }
@ -378,6 +389,7 @@ impl<'a> Issues<'a> {
pub enum Action { pub enum Action {
Title { title: String }, Title { title: String },
Lifecycle { status: Status }, Lifecycle { status: Status },
Tag { add: Vec<Tag>, remove: Vec<Tag> },
Thread { action: thread::Action }, Thread { action: thread::Action },
} }
@ -519,8 +531,8 @@ mod test {
let bug_tag = Tag::new("bug").unwrap(); let bug_tag = Tag::new("bug").unwrap();
let wontfix_tag = Tag::new("wontfix").unwrap(); let wontfix_tag = Tag::new("wontfix").unwrap();
issue.tag([bug_tag.clone()], &signer).unwrap(); issue.tag([bug_tag.clone()], [], &signer).unwrap();
issue.tag([wontfix_tag.clone()], &signer).unwrap(); issue.tag([wontfix_tag.clone()], [], &signer).unwrap();
let id = issue.id; let id = issue.id;
let issue = issues.get(&id).unwrap().unwrap(); let issue = issues.get(&id).unwrap().unwrap();

View File

@ -823,7 +823,7 @@ mod test {
impl<const N: usize> Arbitrary for Changes<N> { impl<const N: usize> Arbitrary for Changes<N> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
type State = (clock::Lamport, Vec<ChangeId>); type State = (clock::Lamport, Vec<ChangeId>, Vec<Tag>);
let author = ActorId::from([0; 32]); let author = ActorId::from([0; 32]);
let rng = fastrand::Rng::with_seed(u64::arbitrary(g)); let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
@ -840,7 +840,7 @@ mod test {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let gen = WeightedGenerator::<(clock::Lamport, Action), State>::new(rng.clone()) let gen = WeightedGenerator::<(clock::Lamport, Action), State>::new(rng.clone())
.variant(1, |(clock, _), rng| { .variant(1, |(clock, _, _), rng| {
Some(( Some((
clock.tick(), clock.tick(),
Action::Edit { Action::Edit {
@ -850,7 +850,7 @@ mod test {
}, },
)) ))
}) })
.variant(1, |(clock, revisions), rng| { .variant(1, |(clock, revisions, _), rng| {
if revisions.is_empty() { if revisions.is_empty() {
return None; return None;
} }
@ -859,7 +859,22 @@ mod test {
Some((clock.tick(), Action::Merge { revision, commit })) Some((clock.tick(), Action::Merge { revision, commit }))
}) })
.variant(1, |(clock, revisions), rng| { .variant(1, |(clock, _, tags), rng| {
let add = iter::repeat_with(|| rng.alphabetic())
.take(rng.usize(0..=3))
.map(|c| Tag::new(c).unwrap())
.collect::<Vec<_>>();
let remove = tags
.iter()
.take(rng.usize(0..=tags.len()))
.cloned()
.collect();
for tag in &add {
tags.push(tag.clone());
}
Some((clock.tick(), Action::Tag { add, remove }))
})
.variant(1, |(clock, revisions, _), rng| {
let oid = oids[rng.usize(..oids.len())]; let oid = oids[rng.usize(..oids.len())];
let base = oids[rng.usize(..oids.len())]; let base = oids[rng.usize(..oids.len())];

View File

@ -8,13 +8,12 @@ use once_cell::sync::Lazy;
use radicle_crdt as crdt; use radicle_crdt as crdt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::cob::common::{Reaction, Tag, Timestamp}; use crate::cob::common::{Reaction, Timestamp};
use crate::cob::store; use crate::cob::store;
use crate::cob::{History, TypeName}; use crate::cob::{History, TypeName};
use crate::crypto::Signer; use crate::crypto::Signer;
use crdt::clock::Lamport; use crdt::clock::Lamport;
use crdt::lwwreg::LWWReg;
use crdt::lwwset::LWWSet; use crdt::lwwset::LWWSet;
use crdt::redactable::Redactable; use crdt::redactable::Redactable;
use crdt::{ActorId, Change, ChangeId, Semilattice}; use crdt::{ActorId, Change, ChangeId, Semilattice};
@ -70,10 +69,6 @@ pub enum Action {
}, },
/// Redact a change. Not all changes can be redacted. /// Redact a change. Not all changes can be redacted.
Redact { id: ChangeId }, Redact { id: ChangeId },
/// Add tags to the thread.
Tag { tags: Vec<Tag> },
/// Remove tags from the thread.
Untag { tags: Vec<Tag> },
/// React to a change. /// React to a change.
React { React {
to: ChangeId, to: ChangeId,
@ -94,8 +89,6 @@ impl Action {
pub struct Thread { pub struct Thread {
/// The comments under the thread. /// The comments under the thread.
comments: BTreeMap<CommentId, Redactable<Comment>>, comments: BTreeMap<CommentId, Redactable<Comment>>,
/// Associated tags.
tags: BTreeMap<Tag, LWWReg<bool, Lamport>>,
/// Reactions to changes. /// Reactions to changes.
reactions: BTreeMap<CommentId, LWWSet<(ActorId, Reaction), Lamport>>, reactions: BTreeMap<CommentId, LWWSet<(ActorId, Reaction), Lamport>>,
} }
@ -128,7 +121,6 @@ impl store::FromHistory for Thread {
impl Semilattice for Thread { impl Semilattice for Thread {
fn merge(&mut self, other: Self) { fn merge(&mut self, other: Self) {
self.comments.merge(other.comments); self.comments.merge(other.comments);
self.tags.merge(other.tags);
self.reactions.merge(other.reactions); self.reactions.merge(other.reactions);
} }
} }
@ -211,22 +203,6 @@ impl Thread {
.and_modify(|e| e.merge(Redactable::Redacted)) .and_modify(|e| e.merge(Redactable::Redacted))
.or_insert(Redactable::Redacted); .or_insert(Redactable::Redacted);
} }
Action::Tag { tags } => {
for tag in tags {
self.tags
.entry(tag)
.and_modify(|r| r.set(true, change.clock))
.or_insert_with(|| LWWReg::new(true, change.clock));
}
}
Action::Untag { tags } => {
for tag in tags {
self.tags
.entry(tag)
.and_modify(|r| r.set(false, change.clock))
.or_insert_with(|| LWWReg::new(false, change.clock));
}
}
Action::React { Action::React {
to, to,
reaction, reaction,
@ -264,12 +240,6 @@ impl Thread {
} }
}) })
} }
pub fn tags(&self) -> impl Iterator<Item = &Tag> + '_ {
self.tags
.iter()
.filter_map(|(tag, r)| if *r.get() { Some(tag) } else { None })
}
} }
/// An object that can be used to create and sign changes. /// An object that can be used to create and sign changes.
@ -305,16 +275,6 @@ impl<G: Signer> Actor<G> {
}) })
} }
/// Add a tag.
pub fn tag(&mut self, tag: Tag) -> Change<Action> {
self.change(Action::Tag { tags: vec![tag] })
}
/// Remove a tag.
pub fn untag(&mut self, tag: Tag) -> Change<Action> {
self.change(Action::Untag { tags: vec![tag] })
}
/// Create a new redaction. /// Create a new redaction.
pub fn redact(&mut self, id: ChangeId) -> Change<Action> { pub fn redact(&mut self, id: ChangeId) -> Change<Action> {
self.change(Action::Redact { id }) self.change(Action::Redact { id })
@ -371,10 +331,8 @@ mod tests {
let author = ActorId::from([0; 32]); let author = ActorId::from([0; 32]);
let rng = fastrand::Rng::with_seed(u64::arbitrary(g)); let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
let gen = let gen =
WeightedGenerator::<(Lamport, Action), (Lamport, Vec<Tag>, Vec<ChangeId>)>::new( WeightedGenerator::<(Lamport, Action), (Lamport, Vec<ChangeId>)>::new(rng.clone())
rng.clone(), .variant(3, |(clock, changes), rng| {
)
.variant(3, |(clock, _, changes), rng| {
changes.push((clock.tick(), author)); changes.push((clock.tick(), author));
Some(( Some((
@ -385,7 +343,7 @@ mod tests {
}, },
)) ))
}) })
.variant(2, |(clock, _, changes), rng| { .variant(2, |(clock, changes), rng| {
if changes.is_empty() { if changes.is_empty() {
return None; return None;
} }
@ -400,37 +358,13 @@ mod tests {
}, },
)) ))
}) })
.variant(2, |(clock, _, changes), rng| { .variant(2, |(clock, changes), rng| {
if changes.is_empty() { if changes.is_empty() {
return None; return None;
} }
let id = changes[rng.usize(..changes.len())]; let id = changes[rng.usize(..changes.len())];
Some((clock.tick(), Action::Redact { id })) Some((clock.tick(), Action::Redact { id }))
})
.variant(2, |(clock, tags, _), rng| {
let tag = if tags.is_empty() || rng.bool() {
let tag = iter::repeat_with(|| rng.alphabetic())
.take(8)
.collect::<String>();
let tag = Tag::new(tag).unwrap();
tags.push(tag.clone());
tag
} else {
tags[rng.usize(..tags.len())].clone()
};
Some((clock.tick(), Action::Tag { tags: vec![tag] }))
})
.variant(2, |(clock, tags, _), rng| {
if tags.is_empty() {
return None;
}
let tag = tags[rng.usize(..tags.len())].clone();
clock.tick();
Some((clock.tick(), Action::Untag { tags: vec![tag] }))
}); });
let mut changes = Vec::new(); let mut changes = Vec::new();