Skip to main content

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}