This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-cobs / src / grant.rs
6.3 kB 214 lines
1use std::collections::BTreeMap; 2 3use knot_types::{AccountDid, UnixSeconds}; 4use serde::{Deserialize, Serialize}; 5 6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 7pub struct Grant { 8 pub subject: AccountDid, 9 pub added_by: AccountDid, 10 pub created_at: UnixSeconds, 11} 12 13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 14pub struct Removal { 15 pub subject: AccountDid, 16} 17 18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 19pub struct Entry { 20 pub added_by: AccountDid, 21 pub created_at: UnixSeconds, 22} 23 24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 25pub struct Roster { 26 entries: BTreeMap<AccountDid, Entry>, 27} 28 29impl Roster { 30 pub fn empty() -> Self { 31 Self { 32 entries: BTreeMap::new(), 33 } 34 } 35 36 pub fn admit(mut self, grant: Grant) -> Self { 37 self.entries.entry(grant.subject).or_insert(Entry { 38 added_by: grant.added_by, 39 created_at: grant.created_at, 40 }); 41 self 42 } 43 44 pub fn revoke(mut self, removal: Removal) -> Self { 45 self.entries.remove(&removal.subject); 46 self 47 } 48 49 pub fn get(&self, subject: &AccountDid) -> Option<&Entry> { 50 self.entries.get(subject) 51 } 52 53 pub fn contains(&self, subject: &AccountDid) -> bool { 54 self.entries.contains_key(subject) 55 } 56 57 pub fn len(&self) -> usize { 58 self.entries.len() 59 } 60 61 pub fn is_empty(&self) -> bool { 62 self.entries.is_empty() 63 } 64 65 pub fn entries(&self) -> impl Iterator<Item = (&AccountDid, &Entry)> { 66 self.entries.iter() 67 } 68} 69 70pub trait GrantChange { 71 fn subject(&self) -> &AccountDid; 72 fn adds(&self) -> bool; 73 fn as_grant(&self) -> Option<&Grant>; 74} 75 76macro_rules! grant_set_cob { 77 ( 78 change = $change:ident, 79 cob = $cob:ident, 80 state = $state:ident, 81 type_name = $type_name:literal, 82 add = $add:ident, 83 remove = $remove:ident $(,)? 84 ) => { 85 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)] 86 #[serde(tag = "op", content = "data", rename_all = "snake_case")] 87 pub enum $change { 88 Add($crate::grant::Grant), 89 Remove($crate::grant::Removal), 90 } 91 92 impl ::knot_cob::ChangePayload for $change { 93 const TYPE: &'static str = $type_name; 94 } 95 96 impl $crate::grant::GrantChange for $change { 97 fn subject(&self) -> &::knot_types::AccountDid { 98 match self { 99 $change::Add(grant) => &grant.subject, 100 $change::Remove(removal) => &removal.subject, 101 } 102 } 103 104 fn adds(&self) -> bool { 105 ::core::matches!(self, $change::Add(_)) 106 } 107 108 fn as_grant(&self) -> ::core::option::Option<&$crate::grant::Grant> { 109 match self { 110 $change::Add(grant) => ::core::option::Option::Some(grant), 111 $change::Remove(_) => ::core::option::Option::None, 112 } 113 } 114 } 115 116 pub type $state = $crate::grant::Roster; 117 118 pub struct $cob; 119 120 impl ::knot_cob::Evaluate for $cob { 121 type State = $state; 122 type Change = $change; 123 124 const HISTORY: ::knot_cob::HistoryModel = ::knot_cob::HistoryModel::Linear; 125 126 fn initial() -> Self::State { 127 $crate::grant::Roster::empty() 128 } 129 130 fn apply( 131 state: Self::State, 132 change: Self::Change, 133 _author: &::knot_types::ActorId, 134 ) -> Self::State { 135 match change { 136 $change::Add(grant) => state.admit(grant), 137 $change::Remove(removal) => state.revoke(removal), 138 } 139 } 140 } 141 142 impl ::knot_cob::Checkpoint for $cob { 143 const SNAPSHOT_STRIDE: ::knot_cob::SnapshotStride = 144 ::knot_cob::SnapshotStride::new(256); 145 fn checkpoint_size(state: &Self::State) -> ::knot_cob::StateSize { 146 ::knot_cob::StateSize::new(state.len()) 147 } 148 } 149 150 pub fn $add( 151 store: &::knot_cob::CobStore, 152 home: &::knot_cob::CobHome, 153 object: ::knot_cob::CobId, 154 grant: $crate::grant::Grant, 155 signer: &dyn ::knot_runtime::Signer, 156 timestamp: ::knot_types::UnixSeconds, 157 ) -> ::core::result::Result<::knot_cob::ChangeId, ::knot_cob::CobError> { 158 store.update_with_checkpointed::<$cob, ::knot_cob::CobError>( 159 home, 160 object, 161 signer, 162 timestamp, 163 |_state| ::core::result::Result::Ok($change::Add(grant.clone())), 164 ) 165 } 166 167 pub fn $remove( 168 store: &::knot_cob::CobStore, 169 home: &::knot_cob::CobHome, 170 object: ::knot_cob::CobId, 171 removal: $crate::grant::Removal, 172 signer: &dyn ::knot_runtime::Signer, 173 timestamp: ::knot_types::UnixSeconds, 174 ) -> ::core::result::Result<::knot_cob::ChangeId, ::knot_cob::CobError> { 175 store.update_with_checkpointed::<$cob, ::knot_cob::CobError>( 176 home, 177 object, 178 signer, 179 timestamp, 180 |_state| ::core::result::Result::Ok($change::Remove(removal.clone())), 181 ) 182 } 183 }; 184} 185 186pub(crate) use grant_set_cob; 187 188#[cfg(test)] 189mod tests { 190 use super::*; 191 192 fn did(suffix: &str) -> AccountDid { 193 AccountDid::new(format!("did:plc:{suffix}")).unwrap() 194 } 195 196 fn grant(subject: &str, added_by: &str, at: i64) -> Grant { 197 Grant { 198 subject: did(subject), 199 added_by: did(added_by), 200 created_at: UnixSeconds::new(at), 201 } 202 } 203 204 #[test] 205 fn admit_keeps_the_first_provenance() { 206 let roster = Roster::empty() 207 .admit(grant("nel", "olaren", 1)) 208 .admit(grant("nel", "teq", 5)); 209 let entry = roster.get(&did("nel")).unwrap(); 210 assert_eq!(entry.added_by, did("olaren")); 211 assert_eq!(entry.created_at, UnixSeconds::new(1)); 212 assert_eq!(roster.len(), 1); 213 } 214}