superstac_engine/
discovery.rs1use std::collections::HashMap;
2
3use superstac_core::models::catalog::Catalog;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct CollectionAvailability {
8 pub id: String,
9 pub catalogs: Vec<String>,
11}
12
13pub fn aggregate_collections(catalogs: &[Catalog]) -> Vec<CollectionAvailability> {
17 let mut by_id: HashMap<String, Vec<String>> = HashMap::new();
18 for catalog in catalogs {
19 if let Some(supported) = &catalog.supported_collections {
20 for collection_id in supported {
21 by_id
22 .entry(collection_id.clone())
23 .or_default()
24 .push(catalog.id.clone());
25 }
26 }
27 }
28 let mut result: Vec<CollectionAvailability> = by_id
29 .into_iter()
30 .map(|(id, mut catalogs)| {
31 catalogs.sort();
32 CollectionAvailability { id, catalogs }
33 })
34 .collect();
35 result.sort_by(|a, b| a.id.cmp(&b.id));
36 result
37}
38
39pub fn catalogs_supporting(catalogs: &[Catalog], collection_id: &str) -> Vec<String> {
43 catalogs
44 .iter()
45 .filter(|c| {
46 c.supported_collections
47 .as_ref()
48 .map(|s| s.contains(collection_id))
49 .unwrap_or(false)
50 })
51 .map(|c| c.id.clone())
52 .collect()
53}
54
55pub fn collections_by_catalog(catalogs: &[Catalog]) -> HashMap<String, Vec<String>> {
58 catalogs
59 .iter()
60 .filter_map(|c| {
61 c.supported_collections.as_ref().map(|s| {
62 let mut ids: Vec<String> = s.iter().cloned().collect();
63 ids.sort();
64 (c.id.clone(), ids)
65 })
66 })
67 .collect()
68}
69
70pub fn unsupported_collections(catalogs: &[Catalog], requested: &[String]) -> Vec<String> {
74 requested
75 .iter()
76 .filter(|q| {
77 let could_be_supported = catalogs.iter().any(|c| match &c.supported_collections {
78 None => true,
79 Some(set) => set.contains(q.as_str()),
80 });
81 !could_be_supported
82 })
83 .cloned()
84 .collect()
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::collections::HashSet;
91
92 fn catalog(id: &str, supported: Option<&[&str]>) -> Catalog {
93 let mut c = Catalog::new(id, Some(id), "https://example.com", None::<String>, None).unwrap();
94 c.supported_collections = supported.map(|s| s.iter().map(|x| x.to_string()).collect());
95 c
96 }
97
98 #[test]
99 fn aggregate_collections_groups_by_id_and_sorts() {
100 let catalogs = vec![
101 catalog("a", Some(&["sentinel-2-l2a", "landsat-c2-l2"])),
102 catalog("b", Some(&["sentinel-2-l2a", "naip"])),
103 catalog("c", None), ];
105
106 let result = aggregate_collections(&catalogs);
107
108 assert_eq!(result.len(), 3);
109 assert_eq!(result[0].id, "landsat-c2-l2");
111 assert_eq!(result[0].catalogs, vec!["a"]);
112 assert_eq!(result[1].id, "naip");
113 assert_eq!(result[1].catalogs, vec!["b"]);
114 assert_eq!(result[2].id, "sentinel-2-l2a");
115 assert_eq!(result[2].catalogs, vec!["a", "b"]);
116 }
117
118 #[test]
119 fn catalogs_supporting_returns_only_definite_matches() {
120 let catalogs = vec![
121 catalog("a", Some(&["sentinel-2-l2a"])),
122 catalog("b", Some(&["landsat-c2-l2"])),
123 catalog("c", None),
124 ];
125
126 assert_eq!(
127 catalogs_supporting(&catalogs, "sentinel-2-l2a"),
128 vec!["a".to_string()]
129 );
130 assert!(catalogs_supporting(&catalogs, "made-up").is_empty());
131 }
132
133 #[test]
134 fn collections_by_catalog_skips_uninitialized() {
135 let catalogs = vec![
136 catalog("a", Some(&["x", "y"])),
137 catalog("b", None),
138 ];
139
140 let map = collections_by_catalog(&catalogs);
141 assert_eq!(map.len(), 1);
142 assert_eq!(map["a"], vec!["x".to_string(), "y".to_string()]);
143 assert!(!map.contains_key("b"));
144 }
145
146 #[test]
147 fn unsupported_collections_is_conservative_when_uncertain() {
148 let catalogs = vec![
149 catalog("a", Some(&["sentinel-2-l2a"])),
150 catalog("b", None), ];
152
153 assert!(unsupported_collections(&catalogs, &["typo-collection".to_string()]).is_empty());
156 }
157
158 #[test]
159 fn unsupported_collections_flags_when_all_catalogs_known() {
160 let catalogs = vec![
161 catalog("a", Some(&["sentinel-2-l2a"])),
162 catalog("b", Some(&["landsat-c2-l2"])),
163 ];
164
165 let unsupported = unsupported_collections(
166 &catalogs,
167 &[
168 "sentinel-2-l2a".to_string(),
169 "made-up-collection".to_string(),
170 ],
171 );
172
173 assert_eq!(unsupported, vec!["made-up-collection".to_string()]);
174 }
175
176 #[test]
177 fn unsupported_collections_empty_for_empty_request() {
178 let catalogs = vec![catalog("a", Some(&["x"]))];
179 assert!(unsupported_collections(&catalogs, &[]).is_empty());
180 }
181
182 #[test]
183 #[allow(dead_code)]
184 fn aggregate_collections_no_panic_with_empty_input() {
185 let _: HashSet<String> = HashSet::new(); assert!(aggregate_collections(&[]).is_empty());
187 }
188}