protocol_droid/application/registry/
listing.rs1use std::sync::Arc;
26
27use crate::application::registry::store::RegistryStore;
28use crate::domain::FeatureProfile;
29use crate::domain::registry::{AuthContext, PrimitiveKind, Registered, is_visible};
30
31#[derive(Debug)]
38pub struct Listing {
39 visible: Vec<Arc<Registered>>,
40 total: usize,
41 generation: u64,
42}
43
44impl Listing {
45 #[must_use]
47 pub fn visible(&self) -> &[Arc<Registered>] {
48 &self.visible
49 }
50
51 #[must_use]
53 pub const fn total(&self) -> usize {
54 self.total
55 }
56
57 #[must_use]
59 pub const fn hidden(&self) -> usize {
60 self.total.saturating_sub(self.visible.len())
61 }
62
63 #[must_use]
65 pub const fn generation(&self) -> u64 {
66 self.generation
67 }
68}
69
70impl RegistryStore {
71 #[must_use]
77 pub fn list_visible(
78 &self,
79 session: &AuthContext,
80 profile: &FeatureProfile,
81 kind: PrimitiveKind,
82 ) -> Listing {
83 let snapshot = self.snapshot();
84 let all = snapshot.list(kind);
85 let total = all.len();
86 let visible = all
87 .iter()
88 .filter(|primitive| is_visible(session, profile, primitive.meta()))
89 .map(Arc::clone)
90 .collect();
91 Listing {
92 visible,
93 total,
94 generation: snapshot.generation(),
95 }
96 }
97
98 #[must_use]
105 pub fn get_visible(
106 &self,
107 session: &AuthContext,
108 profile: &FeatureProfile,
109 kind: PrimitiveKind,
110 id: &str,
111 ) -> Option<Arc<Registered>> {
112 let snapshot = self.snapshot();
113 let registered = snapshot.get(kind, &id.into())?.clone();
114 is_visible(session, profile, registered.meta()).then_some(registered)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::RegistryStore;
121 use crate::domain::registry::TestError;
122 use crate::domain::registry::{AuthContext, Primitive, PrimitiveKind, Registered, Tool};
123 use crate::domain::{Feature, FeatureProfile};
124
125 fn tool(name: &str) -> Result<Registered, TestError> {
126 Ok(Registered::from(Primitive::from(Tool::new(
127 name,
128 r#"{"type":"object"}"#,
129 )?)))
130 }
131
132 fn everything() -> FeatureProfile {
133 FeatureProfile::empty()
134 .with(Feature::Tools)
135 .with(Feature::ToolStructuredOutput)
136 }
137
138 #[test]
141 fn a_listing_is_in_registration_order() -> Result<(), TestError> {
142 let store = RegistryStore::new();
143 for name in ["c", "a", "b"] {
144 store.register(tool(name)?);
145 }
146 let session = AuthContext::unrestricted("alice");
147 let listing = store.list_visible(&session, &everything(), PrimitiveKind::Tool);
148
149 let names: Vec<&str> = listing
150 .visible()
151 .iter()
152 .map(|held| held.id().as_str())
153 .collect();
154 assert_eq!(names, vec!["c", "a", "b"]);
155 Ok(())
156 }
157
158 #[test]
161 fn the_total_counts_what_the_filter_hid() -> Result<(), TestError> {
162 let store = RegistryStore::new();
163 let structured = Registered::from(Primitive::from(
164 Tool::new("structured", r#"{"type":"object"}"#)?
165 .with_output_schema(r#"{"type":"object"}"#)?,
166 ));
167 store.register(structured);
168 store.register(tool("plain")?);
169
170 let session = AuthContext::unrestricted("alice");
171 let without = store.list_visible(
172 &session,
173 &FeatureProfile::empty().with(Feature::Tools),
174 PrimitiveKind::Tool,
175 );
176
177 assert_eq!(without.visible().len(), 1);
178 assert_eq!(without.total(), 2);
179 assert_eq!(without.hidden(), 1);
180 assert_eq!(
181 without.total(),
182 without.visible().len() + without.hidden(),
183 "visible plus hidden must account for every primitive"
184 );
185 Ok(())
186 }
187
188 #[test]
191 fn a_listing_reports_the_generation_it_came_from() -> Result<(), TestError> {
192 let store = RegistryStore::new();
193 store.register(tool("a")?);
194 let session = AuthContext::unrestricted("alice");
195 let listing = store.list_visible(&session, &everything(), PrimitiveKind::Tool);
196 assert_eq!(listing.generation(), 1);
197 Ok(())
198 }
199
200 #[test]
204 fn a_hidden_primitive_and_an_absent_one_both_read_as_none() -> Result<(), TestError> {
205 let store = RegistryStore::new();
206 let guarded = Registered::from(Primitive::from(Tool::new(
207 "guarded",
208 r#"{"type":"object"}"#,
209 )?))
210 .requiring_scope(crate::domain::registry::Scope::Admin);
211 store.register(guarded);
212
213 let nobody = AuthContext::new("mallory", crate::domain::registry::ScopeSet::empty());
214 let hidden = store.get_visible(&nobody, &everything(), PrimitiveKind::Tool, "guarded");
215 let absent = store.get_visible(&nobody, &everything(), PrimitiveKind::Tool, "nope");
216
217 assert!(hidden.is_none());
218 assert!(absent.is_none());
219
220 let root = AuthContext::unrestricted("root");
221 assert!(
222 store
223 .get_visible(&root, &everything(), PrimitiveKind::Tool, "guarded")
224 .is_some(),
225 "the same lookup must succeed for a principal that does hold the scope"
226 );
227 Ok(())
228 }
229}