laterite_core/
capabilities.rs1use std::collections::BTreeSet;
18
19use sqlx::AnyPool;
20
21use crate::error::{CoreError, CoreResult};
22use crate::migration::DbBackend;
23use crate::module::{Capability, ModuleRegistry};
24
25#[derive(Debug, Default, Clone)]
28pub struct CapabilitySet {
29 available: BTreeSet<String>,
30}
31
32impl CapabilitySet {
33 pub fn has(&self, name: &str) -> bool {
35 self.available.contains(name)
36 }
37
38 pub fn names(&self) -> impl Iterator<Item = &str> {
40 self.available.iter().map(String::as_str)
41 }
42}
43
44pub async fn check(
48 pool: &AnyPool,
49 backend: DbBackend,
50 registry: &ModuleRegistry,
51) -> CoreResult<CapabilitySet> {
52 let mut wanted: BTreeSet<&str> = BTreeSet::new();
53 for module in registry.iter() {
54 wanted.extend(
55 module
56 .requires_db_capabilities()
57 .iter()
58 .map(Capability::as_str),
59 );
60 wanted.extend(
61 module
62 .optional_db_capabilities()
63 .iter()
64 .map(Capability::as_str),
65 );
66 }
67
68 let available = detect(pool, backend, &wanted).await?;
69
70 for module in registry.iter() {
71 for cap in module.requires_db_capabilities() {
72 if !available.contains(cap.as_str()) {
73 return Err(CoreError::MissingDbCapability {
74 module: module.id().to_string(),
75 capability: cap.as_str().to_string(),
76 backend: backend.name().to_string(),
77 });
78 }
79 }
80 }
81
82 Ok(CapabilitySet {
83 available: available.into_iter().map(str::to_string).collect(),
84 })
85}
86
87async fn detect<'a>(
90 pool: &AnyPool,
91 backend: DbBackend,
92 wanted: &BTreeSet<&'a str>,
93) -> CoreResult<BTreeSet<&'a str>> {
94 if wanted.is_empty() || backend != DbBackend::Postgres {
95 return Ok(BTreeSet::new());
96 }
97 let rows: Vec<String> = sqlx::query_scalar("SELECT name::text FROM pg_available_extensions")
99 .fetch_all(pool)
100 .await?;
101 let installable: BTreeSet<String> = rows.into_iter().collect();
102 Ok(wanted
103 .iter()
104 .copied()
105 .filter(|name| installable.contains(*name))
106 .collect())
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::module::{Module, ModuleId};
113
114 async fn sqlite_pool() -> AnyPool {
115 sqlx::any::install_default_drivers();
116 sqlx::any::AnyPoolOptions::new()
117 .max_connections(1)
118 .connect("sqlite::memory:")
119 .await
120 .unwrap()
121 }
122
123 struct Needs(ModuleId, &'static [Capability], &'static [Capability]);
125
126 impl Module for Needs {
127 fn id(&self) -> ModuleId {
128 self.0
129 }
130 fn requires_db_capabilities(&self) -> &'static [Capability] {
131 self.1
132 }
133 fn optional_db_capabilities(&self) -> &'static [Capability] {
134 self.2
135 }
136 }
137
138 const POSTGIS: &[Capability] = &[Capability::new("postgis")];
139 const PG_TRGM: &[Capability] = &[Capability::new("pg_trgm")];
140 const NO_CAPS: &[Capability] = &[];
141
142 #[tokio::test]
143 async fn required_capability_unavailable_errors() {
144 let pool = sqlite_pool().await;
145 let mut reg = ModuleRegistry::new();
146 reg.register(Needs(ModuleId::new("acme.blog"), POSTGIS, NO_CAPS));
147 let err = check(&pool, DbBackend::Sqlite, ®).await.unwrap_err();
148 assert!(matches!(err, CoreError::MissingDbCapability { .. }));
149 }
150
151 #[tokio::test]
152 async fn no_requirements_ok_and_absent_optional_not_in_set() {
153 let pool = sqlite_pool().await;
154 let mut reg = ModuleRegistry::new();
155 reg.register(Needs(ModuleId::new("acme.shop"), NO_CAPS, PG_TRGM));
156 let caps = check(&pool, DbBackend::Sqlite, ®).await.unwrap();
157 assert!(!caps.has("pg_trgm"));
158 }
159}