Skip to main content

didbot_policy_records/
lookups.rs

1//! Which records a write's policies read, gathered from the whole set.
2//!
3//! A policy declares its lookups in its own record; the write path has to
4//! know them before it judges anything, because reading somebody else's
5//! repository is I/O and the gate is judged inside a synchronous section.
6//! So this module answers one question — *for a write to this collection,
7//! which records does any applicable policy want?* — and the write path
8//! reads exactly those, once, before the write is judged.
9//!
10//! The answer is the union over every policy that could be asked about the
11//! write, not over the ones that will actually deny it. A policy's
12//! statements are opaque here, so there is no cheaper filter than its
13//! declared applicability, and reading a record for a policy that then has
14//! nothing to say costs one cached fetch rather than a wrong verdict.
15
16use std::collections::HashMap;
17
18use didbot_policy::{Applicability, Scope};
19
20use crate::records::Lookup;
21use crate::{BuiltinPolicy, ParsedPolicy};
22
23/// Every lookup the policies a deployment has loaded declare, with the
24/// applicability of the policy that declared each.
25#[derive(Debug, Clone, Default)]
26pub struct Lookups {
27    declared: Vec<(Applicability, Lookup)>,
28}
29
30impl Lookups {
31    /// The lookups of every policy in one build of the tree: the built-ins,
32    /// the declarations supplied for the length of the run, and the ones
33    /// read from the operator's records. The same three sources
34    /// [`crate::merge::build`] takes, so a tree and its lookups are built
35    /// from one view.
36    #[must_use]
37    pub fn of(
38        builtins: &[BuiltinPolicy],
39        startup: &HashMap<String, ParsedPolicy>,
40        operator: &HashMap<String, ParsedPolicy>,
41    ) -> Self {
42        let mut declared = Vec::new();
43        let builtins = builtins.iter().map(|builtin| &builtin.policy);
44        for policy in builtins.chain(startup.values()).chain(operator.values()) {
45            for lookup in &policy.lookups {
46                declared.push((policy.applicability.clone(), lookup.clone()));
47            }
48        }
49        Self { declared }
50    }
51
52    /// Whether any policy declares a lookup at all. The ordinary answer,
53    /// and the one that lets a write skip the whole seam.
54    #[must_use]
55    pub fn is_empty(&self) -> bool {
56        self.declared.is_empty()
57    }
58
59    /// The lookups a write to `collection` may need, each once.
60    ///
61    /// Two policies declaring the same record under the same name is one
62    /// read, because the write path attaches one answer per lookup and an
63    /// engine asks for it by name.
64    #[must_use]
65    pub fn for_write(&self, collection: &str) -> Vec<&Lookup> {
66        let mut wanted: Vec<&Lookup> = Vec::new();
67        for (applicability, lookup) in &self.declared {
68            let Applicability::Write { collections, .. } = applicability else {
69                continue;
70            };
71            let reaches = match collections {
72                Scope::Any => true,
73                Scope::Only(named) => named.iter().any(|named| named == collection),
74            };
75            if reaches && !wanted.contains(&lookup) {
76                wanted.push(lookup);
77            }
78        }
79        wanted
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use didbot_policy::{ActionSet, KindSet};
87
88    fn lookup(name: &str) -> Lookup {
89        Lookup {
90            name: name.to_owned(),
91            accounts: crate::records::Accounts::Named,
92            collection: "community.lexicon.preference.ai".to_owned(),
93            rkey: None,
94        }
95    }
96
97    fn writes(collections: Scope<String>) -> Applicability {
98        Applicability::Write {
99            kinds: KindSet::ANY,
100            collections,
101            actions: ActionSet::ALL,
102            path_prefixes: Scope::Any,
103        }
104    }
105
106    /// A policy scoped to one collection does not make a write to another
107    /// collection pay for its lookup, and two policies wanting the same
108    /// record are one read.
109    #[test]
110    fn a_write_pays_only_for_the_lookups_that_could_reach_it() {
111        let lookups = Lookups {
112            declared: vec![
113                (writes(Scope::Any), lookup("aiPreference")),
114                (
115                    writes(Scope::Only(vec!["app.bsky.feed.post".to_owned()])),
116                    lookup("aiPreference"),
117                ),
118                (
119                    writes(Scope::Only(vec!["app.bsky.feed.post".to_owned()])),
120                    lookup("replyGate"),
121                ),
122                (
123                    Applicability::Grant {
124                        client_ids: Scope::Any,
125                    },
126                    lookup("neverAtAWrite"),
127                ),
128            ],
129        };
130
131        let post: Vec<&str> = lookups
132            .for_write("app.bsky.feed.post")
133            .iter()
134            .map(|lookup| lookup.name.as_str())
135            .collect();
136        assert_eq!(post, ["aiPreference", "replyGate"]);
137
138        let follow: Vec<&str> = lookups
139            .for_write("app.bsky.graph.follow")
140            .iter()
141            .map(|lookup| lookup.name.as_str())
142            .collect();
143        assert_eq!(follow, ["aiPreference"]);
144    }
145}