Monorepo for Tangled
tangled.org
1use std::collections::{HashMap, HashSet};
2use std::sync::Mutex;
3
4use bobbin_types::knot_acl::KnotHostKey;
5use jacquard_common::DefaultStr;
6use jacquard_common::types::did::Did;
7use jacquard_common::types::string::AtUri;
8
9#[derive(Default)]
10struct Inner {
11 hosts: HashSet<KnotHostKey>,
12 repos: HashMap<KnotHostKey, HashSet<Did<DefaultStr>>>,
13 repo_host: HashMap<Did<DefaultStr>, KnotHostKey>,
14 legacy_members: HashMap<AtUri<DefaultStr>, KnotHostKey>,
15}
16
17#[derive(Default)]
18pub struct KnotRegistry {
19 inner: Mutex<Inner>,
20}
21
22impl KnotRegistry {
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn observe_host(&self, host: &KnotHostKey) {
28 self.inner.lock().unwrap().hosts.insert(host.clone());
29 }
30
31 pub fn observe_repo(&self, host: &KnotHostKey, repo: Did<DefaultStr>) {
32 let mut inner = self.inner.lock().unwrap();
33 inner.hosts.insert(host.clone());
34 inner
35 .repos
36 .entry(host.clone())
37 .or_default()
38 .insert(repo.clone());
39 inner.repo_host.insert(repo, host.clone());
40 }
41
42 pub fn hosts(&self) -> Vec<KnotHostKey> {
43 self.inner.lock().unwrap().hosts.iter().cloned().collect()
44 }
45
46 pub fn repos(&self, host: &KnotHostKey) -> Vec<Did<DefaultStr>> {
47 self.inner
48 .lock()
49 .unwrap()
50 .repos
51 .get(host)
52 .map(|set| set.iter().cloned().collect())
53 .unwrap_or_default()
54 }
55
56 pub fn repo_on_host(&self, host: &KnotHostKey, repo: &Did<DefaultStr>) -> bool {
57 self.inner
58 .lock()
59 .unwrap()
60 .repos
61 .get(host)
62 .is_some_and(|set| set.contains(repo))
63 }
64
65 pub fn host_of_repo(&self, repo: &Did<DefaultStr>) -> Option<KnotHostKey> {
66 self.inner.lock().unwrap().repo_host.get(repo).cloned()
67 }
68
69 pub fn note_legacy_member(&self, source: AtUri<DefaultStr>, host: &KnotHostKey) {
70 self.inner
71 .lock()
72 .unwrap()
73 .legacy_members
74 .insert(source, host.clone());
75 }
76
77 pub fn forget_legacy_member(&self, source: &AtUri<DefaultStr>) {
78 self.inner.lock().unwrap().legacy_members.remove(source);
79 }
80
81 pub fn drain_legacy_members(&self, host: &KnotHostKey) -> Vec<AtUri<DefaultStr>> {
82 let mut inner = self.inner.lock().unwrap();
83 let matched: Vec<AtUri<DefaultStr>> = inner
84 .legacy_members
85 .iter()
86 .filter(|(_, member_host)| member_host.as_str() == host.as_str())
87 .map(|(source, _)| source.clone())
88 .collect();
89 matched.iter().for_each(|source| {
90 inner.legacy_members.remove(source);
91 });
92 matched
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 fn did(s: &str) -> Did<DefaultStr> {
101 Did::new_owned(s).unwrap()
102 }
103
104 fn at(s: &str) -> AtUri<DefaultStr> {
105 AtUri::new_owned(s).unwrap()
106 }
107
108 fn host(s: &str) -> KnotHostKey {
109 KnotHostKey::new(s)
110 }
111
112 #[test]
113 fn observe_repo_registers_host_and_repo() {
114 let registry = KnotRegistry::new();
115 registry.observe_repo(&host("oyster.cafe"), did("did:plc:scallop"));
116 registry.observe_repo(&host("oyster.cafe"), did("did:plc:limpet"));
117
118 assert_eq!(registry.hosts(), vec![host("oyster.cafe")]);
119 let mut repos = registry.repos(&host("oyster.cafe"));
120 repos.sort_by(|a, b| a.as_ref().cmp(b.as_ref()));
121 assert_eq!(repos, vec![did("did:plc:limpet"), did("did:plc:scallop")]);
122 }
123
124 #[test]
125 fn observe_repo_dedups() {
126 let registry = KnotRegistry::new();
127 registry.observe_repo(&host("nel.pet"), did("did:plc:whelk"));
128 registry.observe_repo(&host("nel.pet"), did("did:plc:whelk"));
129 assert_eq!(registry.repos(&host("nel.pet")), vec![did("did:plc:whelk")]);
130 }
131
132 #[test]
133 fn observe_host_without_repos() {
134 let registry = KnotRegistry::new();
135 registry.observe_host(&host("oyster.cafe"));
136 assert_eq!(registry.hosts(), vec![host("oyster.cafe")]);
137 assert!(registry.repos(&host("oyster.cafe")).is_empty());
138 }
139
140 #[test]
141 fn host_lookups_are_case_insensitive() {
142 let registry = KnotRegistry::new();
143 registry.observe_repo(&host("KT.Oyster.Cafe"), did("did:plc:scallop"));
144 assert!(registry.repo_on_host(&host("kt.oyster.cafe"), &did("did:plc:scallop")));
145 assert_eq!(
146 registry.host_of_repo(&did("did:plc:scallop")),
147 Some(host("kt.oyster.cafe"))
148 );
149 }
150
151 #[test]
152 fn host_of_repo_resolves_owning_knot() {
153 let registry = KnotRegistry::new();
154 registry.observe_repo(&host("oyster.cafe"), did("did:plc:scallop"));
155 assert_eq!(
156 registry.host_of_repo(&did("did:plc:scallop")),
157 Some(host("oyster.cafe"))
158 );
159 assert_eq!(registry.host_of_repo(&did("did:plc:limpet")), None);
160 }
161
162 #[test]
163 fn drain_legacy_members_returns_only_matching_host() {
164 let registry = KnotRegistry::new();
165 let here = at("at://did:plc:akshay/sh.tangled.knot.member/r1");
166 let elsewhere = at("at://did:plc:akshay/sh.tangled.knot.member/r2");
167 registry.note_legacy_member(here.clone(), &host("oyster.cafe"));
168 registry.note_legacy_member(elsewhere.clone(), &host("nel.pet"));
169
170 assert_eq!(
171 registry.drain_legacy_members(&host("oyster.cafe")),
172 vec![here]
173 );
174 assert!(
175 registry
176 .drain_legacy_members(&host("oyster.cafe"))
177 .is_empty()
178 );
179 assert_eq!(
180 registry.drain_legacy_members(&host("nel.pet")),
181 vec![elsewhere]
182 );
183 }
184
185 #[test]
186 fn forget_legacy_member_drops_source() {
187 let registry = KnotRegistry::new();
188 let source = at("at://did:plc:akshay/sh.tangled.knot.member/r1");
189 registry.note_legacy_member(source.clone(), &host("oyster.cafe"));
190 registry.forget_legacy_member(&source);
191 assert!(
192 registry
193 .drain_legacy_members(&host("oyster.cafe"))
194 .is_empty()
195 );
196 }
197}