1use std::collections::BTreeMap;
34use std::sync::atomic::{AtomicU64, Ordering};
35use std::sync::Mutex;
36
37use crate::error::RuntimeError;
38use crate::host::{HostApi, Reentry, ResourceHandle};
39use crate::schema::ModuleSchema;
40use crate::value::{Repr, Value};
41
42pub struct Database {
44 source: DatabaseSource,
45 open: Mutex<BTreeMap<u64, String>>,
53 next_id: AtomicU64,
55}
56
57enum DatabaseSource {
58 Recorded(BTreeMap<String, Vec<String>>),
60 Denied,
62}
63
64const SCHEMA: ModuleSchema = cove_schema::hosts::DATABASE;
70
71impl Database {
72 pub fn recorded(rows: BTreeMap<String, Vec<String>>) -> Self {
78 Database::with_source(DatabaseSource::Recorded(rows))
79 }
80
81 pub fn denied() -> Self {
89 Database::with_source(DatabaseSource::Denied)
90 }
91
92 fn with_source(source: DatabaseSource) -> Self {
93 Database {
94 source,
95 open: Mutex::new(BTreeMap::new()),
96 next_id: AtomicU64::new(1),
97 }
98 }
99
100 fn connect(&self, name: &str) -> Value {
102 match &self.source {
103 DatabaseSource::Recorded(_) => {
104 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
105 self.locked().insert(id, name.to_string());
106 Value::ok(Value(Repr::Resource(ResourceHandle::new(
107 "database",
108 &SCHEMA.resources[0],
109 id,
110 ))))
111 }
112 DatabaseSource::Denied => Value::err(Value::error(
113 "database: this host has no database, so nothing can connect",
114 )),
115 }
116 }
117
118 fn locked(&self) -> std::sync::MutexGuard<'_, BTreeMap<u64, String>> {
119 self.open
120 .lock()
121 .unwrap_or_else(|poisoned| poisoned.into_inner())
122 }
123
124 fn query(&self, sql: &str) -> Result<Vec<String>, String> {
125 match &self.source {
126 DatabaseSource::Recorded(rows) => rows
127 .get(sql)
128 .cloned()
129 .ok_or_else(|| format!("database: no recorded answer for `{sql}`")),
130 DatabaseSource::Denied => {
131 Err("database: this host has no database, so no query can run".to_string())
132 }
133 }
134 }
135}
136
137impl HostApi for Database {
138 fn module_schema(&self) -> ModuleSchema {
139 SCHEMA
140 }
141
142 fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
143 match op {
144 "query" => {
145 let [Value(Repr::Str(sql))] = args.as_slice() else {
146 unreachable!("checked by HostRegistry::call")
147 };
148 Ok(rows_of(self.query(sql)))
149 }
150 "connect" => {
151 let [Value(Repr::Str(name))] = args.as_slice() else {
152 unreachable!("checked by HostRegistry::call")
153 };
154 Ok(self.connect(name))
155 }
156 _ => unreachable!("checked by HostRegistry::call"),
157 }
158 }
159
160 fn call_resource(
161 &self,
162 handle: &ResourceHandle,
163 op: &str,
164 args: Vec<Value>,
165 _back: &mut dyn Reentry,
166 ) -> Result<Value, RuntimeError> {
167 match op {
168 "query" => {
169 let [Value(Repr::Str(sql))] = args.as_slice() else {
170 unreachable!("checked by HostRegistry::call")
171 };
172 if !self.locked().contains_key(&handle.id) {
173 return Err(closed(handle, "query"));
174 }
175 Ok(rows_of(self.query(sql)))
176 }
177 "close" => match self.locked().remove(&handle.id) {
178 Some(_) => Ok(Value::ok(Value(Repr::Unit))),
179 None => Err(closed(handle, "close")),
180 },
181 _ => unreachable!("checked by HostRegistry::call_resource"),
182 }
183 }
184}
185
186fn rows_of(answer: Result<Vec<String>, String>) -> Value {
188 match answer {
189 Ok(rows) => Value::ok(Value(Repr::Array(
190 rows.into_iter()
191 .map(|row| Value(Repr::Str(row.into())))
192 .collect(),
193 ))),
194 Err(message) => Value::err(Value::error(message)),
195 }
196}
197
198fn closed(handle: &ResourceHandle, op: &str) -> RuntimeError {
205 RuntimeError::new(format!(
206 "`{handle}` is closed, so `{op}` has nothing to act on"
207 ))
208 .with_rule(
209 "A host resource handle names a resource the host owns. Closing the resource ends the handle; the name outlives it and addresses nothing.",
210 )
211 .with_help("open a new one, or move the `close` after the last use")
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::host::{Grants, HostRegistry, NoReentry};
218
219 fn str_arg(text: &str) -> Value {
220 Value(Repr::Str(text.into()))
221 }
222
223 fn rows(value: Value) -> Vec<String> {
224 match value.ok_payload() {
225 Some(payload) => match payload.first() {
226 Some(Value(Repr::Array(items))) => items.iter().map(ToString::to_string).collect(),
227 other => panic!("expected `Ok(Array)`, found {other:?}"),
228 },
229 None => panic!("expected `Ok(...)`, found {value}"),
230 }
231 }
232
233 fn err_message(value: Value) -> String {
234 match value.err_payload() {
235 Some(payload) => payload.first().map(ToString::to_string).unwrap_or_default(),
236 None => panic!("expected `Err(...)`, found {value}"),
237 }
238 }
239
240 fn recorded() -> Database {
241 Database::recorded(BTreeMap::from([(
242 "select id from bookings".to_string(),
243 vec!["b-1".to_string(), "b-2".to_string()],
244 )]))
245 }
246
247 #[test]
248 fn a_recorded_query_answers_its_rows() {
249 let database = recorded();
250
251 let answer = database
252 .call("query", vec![str_arg("select id from bookings")])
253 .unwrap();
254 assert_eq!(rows(answer), ["b-1", "b-2"]);
255 }
256
257 #[test]
258 fn a_query_the_fake_has_no_answer_for_says_so() {
259 let database = recorded();
260
261 let answer = database
262 .call("query", vec![str_arg("select id from invoices")])
263 .unwrap();
264 assert_eq!(
265 err_message(answer),
266 "database: no recorded answer for `select id from invoices`"
267 );
268 }
269
270 #[test]
271 fn a_denied_host_refuses_every_query() {
272 let database = Database::denied();
273
274 for sql in ["select 1", "select id from bookings"] {
275 let answer = database.call("query", vec![str_arg(sql)]).unwrap();
276 assert_eq!(
277 err_message(answer),
278 "database: this host has no database, so no query can run"
279 );
280 }
281 }
282
283 #[test]
284 fn a_run_without_the_database_grant_cannot_query() {
285 let mut hosts = HostRegistry::new(Grants::new(["console"]));
286 hosts.register(Box::new(Database::denied()));
287
288 let error = hosts
289 .call("database", "query", vec![str_arg("select 1")])
290 .expect_err("the call should be rejected");
291 assert_eq!(
292 error.message,
293 "`database.query` requires the `database` capability, which this run was not granted"
294 );
295 }
296
297 #[test]
301 fn a_granted_denied_host_answers_the_call_with_the_refusal() {
302 let mut hosts = HostRegistry::new(Grants::new(["database"]));
303 hosts.register(Box::new(Database::denied()));
304
305 let answer = hosts
306 .call("database", "query", vec![str_arg("select 1")])
307 .expect("the call should be allowed");
308 assert_eq!(
309 err_message(answer),
310 "database: this host has no database, so no query can run"
311 );
312 }
313
314 #[test]
315 fn signatures_read_like_source() {
316 let database = Database::denied();
317 let rendered: Vec<String> = database
318 .module_schema()
319 .operations
320 .iter()
321 .map(|op| op.signature())
322 .collect();
323 assert_eq!(
324 rendered,
325 [
326 "query(String) -> Result<Array<String>, Error>",
327 "connect(String) -> Result<database.Connection, Error>",
328 ]
329 );
330 let rendered: Vec<String> = SCHEMA.resources[0]
331 .operations
332 .iter()
333 .map(|op| op.signature())
334 .collect();
335 assert_eq!(
336 rendered,
337 [
338 "query(String) -> Result<Array<String>, Error>",
339 "close() -> Result<Unit, Error>",
340 ]
341 );
342 }
343
344 #[test]
346 fn a_closed_connection_reports_that_its_handle_addresses_nothing() {
347 let mut hosts = HostRegistry::new(Grants::new(["database"]));
348 hosts.register(Box::new(recorded()));
349
350 let opened = hosts
351 .call("database", "connect", vec![str_arg("bookings")])
352 .expect("the call should be allowed");
353 let Value(Repr::Enum(result)) = opened else {
354 panic!("expected `Ok(...)`");
355 };
356 let Some(Value(Repr::Resource(handle))) = result.payload.into_vec().into_iter().next()
357 else {
358 panic!("`connect` answers with a handle");
359 };
360 assert_eq!(handle.qualified_type(), "database.Connection");
361 assert!(handle.task_safe);
362
363 let rows = hosts
364 .call_resource(
365 &handle,
366 "query",
367 vec![str_arg("select id from bookings")],
368 &mut NoReentry,
369 )
370 .expect("a query on an open connection is allowed");
371 assert_eq!(super::tests::rows(rows), ["b-1", "b-2"]);
372
373 hosts
374 .call_resource(&handle, "close", Vec::new(), &mut NoReentry)
375 .expect("closing an open connection is allowed");
376
377 let error = hosts
378 .call_resource(
379 &handle,
380 "query",
381 vec![str_arg("select id from bookings")],
382 &mut NoReentry,
383 )
384 .expect_err("a query on a closed connection is refused");
385 assert_eq!(
386 error.message,
387 "`database.Connection#1` is closed, so `query` has nothing to act on"
388 );
389 }
390
391 #[test]
394 fn a_run_without_the_database_grant_cannot_use_a_handle() {
395 let mut hosts = HostRegistry::new(Grants::new(["console"]));
396 hosts.register(Box::new(Database::denied()));
397 let handle = ResourceHandle::new("database", &SCHEMA.resources[0], 1);
398
399 let error = hosts
400 .call_resource(&handle, "query", vec![str_arg("select 1")], &mut NoReentry)
401 .expect_err("the call should be rejected");
402 assert_eq!(
403 error.message,
404 "`database.Connection.query` requires the `database` capability, which this run was not granted"
405 );
406 }
407}