coven_database/operation_outcome.rs
1//! Why the applied state refuses an administrative operation (§10), and
2//! how an operation an app's write journaled ended, as `coven_operations`
3//! lists it until a write acknowledges it (§11.4).
4
5use coven_foundation::id_source::{CircleId, DeviceId, MemberId};
6
7/// The target a proposed change can no longer affect.
8#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9pub enum EntryTarget {
10 /// A store member.
11 Member(MemberId),
12 /// An installation.
13 Device(DeviceId),
14 /// A circle.
15 Circle(CircleId),
16}
17
18/// Why the current applied state refuses an administrative operation.
19#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub enum OperationRejection {
21 /// The provider account owns the store: its member can't be removed, and
22 /// their access can't move to another account (§10.1): OneDrive and
23 /// CloudKit can't hand a store to another account, and coven doesn't use
24 /// Dropbox's ownership transfer (bae-fm/coven#237).
25 StoreOwner,
26 /// The target is absent, removed, or cannot be reused.
27 TargetGone(EntryTarget),
28 /// The resulting store would have no active admin.
29 NoAdminLeft,
30 /// Either proposed public key was already bound.
31 KeysAlreadyUsed,
32 /// Another replacement changed the expected binding.
33 MemberKeysChanged(MemberId),
34 /// The author's device, membership or role does not permit this change,
35 /// or, for a snapshot, this device's raise of its audience has not landed
36 /// yet (§16.2).
37 NotAllowed,
38 /// The admission approving a join would carry a copy of every store key
39 /// in history, more than one slot holds (format.md D2): about 22,000
40 /// keys, one per removal or exposed key (§13.2).
41 AdmissionTooLarge,
42}
43
44/// How an operation an app's write journaled ended (§11.4):
45/// `coven_operations.outcome` holds it from the transaction that ends the
46/// operation's journal row until a write acknowledges it, so an outcome
47/// nobody awaited is still there to read.
48#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49pub enum OperationOutcome {
50 /// Its entry landed at this slot, which this device has applied.
51 Landed(#[serde(with = "slot_json")] coven_merge::Slot),
52 /// The applied state already had what it asked for, so it claimed no
53 /// slot (§11.2).
54 Satisfied,
55 /// The applied state refused it once this device had caught up; its
56 /// preparation was undone (§11.2, §11.3).
57 Rejected(OperationRejection),
58 /// This device stopped for good before the operation ended, so it never
59 /// will here. Nothing refused it: another device may still make the
60 /// same change, and for a copy of this installation, the other copy may
61 /// still land it (§14.1, §19.3).
62 Stopped(DeviceStop),
63 /// The app cancelled it before its point of no return, and what it had
64 /// written was undone (§11.6).
65 Cancelled,
66 /// It was undone before its point of no return, or refused after it
67 /// before anything it claimed landed (§11.6).
68 Undone(UndoCause),
69 /// An invite's operation ended because the provider refused to share the
70 /// store for a reason only a person can fix (§13.2). The invite is gone
71 /// from `coven_invites`; the outcome stays until a write acknowledges
72 /// it, for the app to say what went wrong and what to fix.
73 SharingRefused {
74 /// The address the invite was to share with.
75 account: String,
76 /// What was refused.
77 refusal: coven_foundation::sharing::SharingRefusal,
78 },
79}
80
81/// Why an operation was undone (§11.6).
82#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
83pub enum UndoCause {
84 /// The app or the device stopped before its point of no return, and
85 /// opening the store undid it.
86 Interrupted,
87 /// A step failed, with this error's text; the call that ran it returned
88 /// the error itself.
89 Failed {
90 /// The failure's text, secrets and URLs already cut.
91 error: String,
92 },
93}
94
95/// Why a device stopped for good (§14.1, §19.3).
96#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
97pub enum DeviceStop {
98 /// The device, or its member, was removed.
99 Removed,
100 /// The admission that published this joined store was refused: its
101 /// device never became a member (§13.2).
102 NotAdmitted,
103 /// A reset registered another installation in this device's place.
104 Replaced,
105 /// History shows another copy of this installation using its device id.
106 Copied,
107 /// Its member's keys were replaced with a pair it doesn't hold.
108 MemberKeysReplaced,
109}
110
111impl OperationOutcome {
112 /// The outcome as `_coven_app_operations.outcome` keeps it.
113 pub(crate) fn encode(&self) -> Vec<u8> {
114 serde_json::to_vec(self).expect("an outcome is plain data, which always encodes")
115 }
116}
117
118/// Reads `coven_operations.outcome`, once it is not NULL.
119impl rusqlite::types::FromSql for OperationOutcome {
120 fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
121 serde_json::from_slice(value.as_blob()?)
122 .map_err(|error| rusqlite::types::FromSqlError::Other(Box::new(error)))
123 }
124}
125
126/// A slot as the JSON coven keeps it in: its number. For
127/// `#[serde(with = "coven_database::slot_json")]`.
128pub mod slot_json {
129 use serde::{Deserialize, Serialize};
130 /// Write the slot's number.
131 pub fn serialize<S: serde::Serializer>(
132 value: &coven_merge::Slot,
133 output: S,
134 ) -> Result<S::Ok, S::Error> {
135 value.get().serialize(output)
136 }
137 /// Read a slot from its number.
138 pub fn deserialize<'de, D: serde::Deserializer<'de>>(
139 input: D,
140 ) -> Result<coven_merge::Slot, D::Error> {
141 let number = u64::deserialize(input)?;
142 coven_merge::Slot::new(number).ok_or_else(|| {
143 serde::de::Error::invalid_value(
144 serde::de::Unexpected::Unsigned(number),
145 &"a positive slot number",
146 )
147 })
148 }
149
150 /// A list of slots in the same form, for
151 /// `#[serde(with = "coven_database::slot_json::list")]`.
152 pub mod list {
153 use serde::{Deserialize, Serialize};
154 /// Write each slot's number.
155 pub fn serialize<S: serde::Serializer>(
156 value: &[coven_merge::Slot],
157 output: S,
158 ) -> Result<S::Ok, S::Error> {
159 value.iter().map(|slot| slot.get()).collect::<Vec<_>>().serialize(output)
160 }
161 /// Read slots from their numbers, refusing 0.
162 pub fn deserialize<'de, D: serde::Deserializer<'de>>(
163 input: D,
164 ) -> Result<Vec<coven_merge::Slot>, D::Error> {
165 Vec::<u64>::deserialize(input)?
166 .into_iter()
167 .map(|number| {
168 coven_merge::Slot::new(number).ok_or_else(|| {
169 serde::de::Error::invalid_value(
170 serde::de::Unexpected::Unsigned(number),
171 &"a positive slot number",
172 )
173 })
174 })
175 .collect()
176 }
177 }
178}