Skip to main content

origin_storage/
namespace.rs

1//! The storage namespace convention (ADR-0019).
2//!
3//! Every record lives in a namespace, and the namespace decides who owns the data and
4//! when it may be removed. Three shapes, no exceptions:
5//!
6//! ```text
7//! origin.<area>                      platform data, not tied to an account
8//!                                    origin.settings · origin.accounts
9//!
10//! acct.<connector>.<account>.<area>  anything belonging to one connected account
11//!                                    acct.github.a1b2.notifications · …sync
12//!
13//! app.<module>.<area>                product data with no account behind it
14//!                                    app.planning.templates
15//! ```
16//!
17//! The account prefix is what makes disconnecting an account a mechanical operation:
18//! [`Storage::clear_prefix`] removes everything below it, without any module having to
19//! declare which namespaces it wrote.
20//!
21//! [`Storage::clear_prefix`]: crate::Storage::clear_prefix
22
23use origin_domain::{AccountId, ConnectorId};
24
25/// Namespace for platform-owned data.
26pub fn platform(area: &str) -> String {
27    format!("origin.{area}")
28}
29
30/// Namespace for data belonging to one account of one connector.
31pub fn account(connector: &ConnectorId, account: &AccountId, area: &str) -> String {
32    format!("acct.{connector}.{account}.{area}")
33}
34
35/// Prefix covering *every* namespace of one account.
36///
37/// Ends with a separator so that account `a1` cannot match account `a1b2`.
38pub fn account_prefix(connector: &ConnectorId, account: &AccountId) -> String {
39    format!("acct.{connector}.{account}.")
40}
41
42/// Namespace for product data that is not tied to an account.
43pub fn module(module: &str, area: &str) -> String {
44    format!("app.{module}.{area}")
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn an_account_prefix_covers_its_namespaces() {
53        let connector = ConnectorId::new("github");
54        let id = AccountId::new("a1b2");
55
56        let prefix = account_prefix(&connector, &id);
57
58        assert!(account(&connector, &id, "notifications").starts_with(&prefix));
59        assert!(account(&connector, &id, "sync").starts_with(&prefix));
60    }
61
62    #[test]
63    fn a_prefix_cannot_match_a_longer_account_id() {
64        let connector = ConnectorId::new("github");
65
66        let prefix = account_prefix(&connector, &AccountId::new("a1"));
67        let other = account(&connector, &AccountId::new("a1b2"), "sync");
68
69        assert!(
70            !other.starts_with(&prefix),
71            "the trailing separator is what prevents this: {prefix} vs {other}"
72        );
73    }
74
75    #[test]
76    fn platform_and_product_namespaces_never_collide_with_account_data() {
77        assert!(!platform("settings").starts_with("acct."));
78        assert!(!module("planning", "templates").starts_with("acct."));
79    }
80}