This repository has no description
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}