1pub use cove_schema::hosts;
18pub use cove_schema::{
19 module, shipped, Effect, FieldSchema, HostType, ModuleSchema, OperationSchema, ResourceSchema,
20 TypeSchema,
21};
22
23use cove_schema::builtins::{ERROR, ERR_CASE, NONE_CASE, OK_CASE, OPTION, RESULT, SOME_CASE};
24
25use crate::value::{Repr, Value};
26
27pub trait Admits {
34 fn admits(&self, value: &Value) -> Result<(), Mismatch>;
54}
55
56impl Admits for HostType {
57 fn admits(&self, value: &Value) -> Result<(), Mismatch> {
58 match (self, value) {
59 (HostType::Any, _)
60 | (HostType::Unit, Value(Repr::Unit))
61 | (HostType::Bool, Value(Repr::Bool(_)))
62 | (HostType::Int, Value(Repr::Int(_)))
63 | (HostType::String, Value(Repr::Str(_)))
64 | (HostType::Duration, Value(Repr::Duration(_))) => Ok(()),
65 (HostType::Error, Value(Repr::Struct(fields))) if &*fields.type_name == ERROR.name => {
68 Ok(())
69 }
70 (HostType::Array(item), Value(Repr::Array(items))) => {
71 for (index, element) in items.iter().enumerate() {
72 item.admits(element)
73 .map_err(|mismatch| mismatch.inside(&format!("[{index}]")))?;
74 }
75 Ok(())
76 }
77 (HostType::Set(item), Value(Repr::Set(items))) => {
85 for (index, element) in items.iter().enumerate() {
86 item.admits(&element.to_value())
87 .map_err(|mismatch| mismatch.inside(&format!("[{index}]")))?;
88 }
89 Ok(())
90 }
91 (HostType::Map(key, value), Value(Repr::Map(entries))) => {
92 for (index, (found, held)) in entries.iter().enumerate() {
93 key.admits(&found.to_value())
94 .map_err(|mismatch| mismatch.inside(&format!("key[{index}]")))?;
95 value
96 .admits(held)
97 .map_err(|mismatch| mismatch.inside(&format!("[{found}]")))?;
98 }
99 Ok(())
100 }
101 (HostType::Option(some), Value(Repr::Enum(case)))
105 if &*case.type_name == OPTION.name =>
106 {
107 match (&*case.case, case.payload.as_slice()) {
108 (name, [inner]) if name == SOME_CASE.name => some
109 .admits(inner)
110 .map_err(|m| m.inside(&SOME_CASE.wildcard_pattern())),
111 (name, []) if name == NONE_CASE.name => Ok(()),
112 _ => Err(mismatched(self, value)),
113 }
114 }
115 (HostType::Result(ok, error), Value(Repr::Enum(case)))
116 if &*case.type_name == RESULT.name =>
117 {
118 match (&*case.case, case.payload.as_slice()) {
119 (name, [inner]) if name == OK_CASE.name => ok
120 .admits(inner)
121 .map_err(|m| m.inside(&OK_CASE.wildcard_pattern())),
122 (name, [inner]) if name == ERR_CASE.name => error
123 .admits(inner)
124 .map_err(|m| m.inside(&ERR_CASE.wildcard_pattern())),
125 _ => Err(mismatched(self, value)),
126 }
127 }
128 (HostType::Named(name), Value(Repr::Resource(handle)))
131 if handle.qualified_type() == *name =>
132 {
133 Ok(())
134 }
135 (HostType::Named(name), Value(Repr::Struct(fields))) if &*fields.type_name == *name => {
136 Ok(())
137 }
138 (HostType::Named(name), Value(Repr::Enum(case))) if &*case.type_name == *name => Ok(()),
139 _ => Err(mismatched(self, value)),
140 }
141 }
142}
143
144fn mismatched(declared: &HostType, value: &Value) -> Mismatch {
147 Mismatch {
148 path: String::new(),
149 expected: *declared,
150 found: value.type_name(),
151 }
152}
153
154#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub enum Part {
162 Result,
164 Argument(usize),
166}
167
168#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct Mismatch {
179 pub path: String,
183 pub expected: HostType,
185 pub found: String,
187}
188
189impl Mismatch {
190 fn inside(mut self, step: &str) -> Mismatch {
193 self.path.insert_str(0, step);
194 self
195 }
196
197 pub fn describe(&self, shown: &str, part: Part) -> String {
200 let (verb, whole) = match part {
201 Part::Result => ("answered", "its result".to_string()),
202 Part::Argument(position) => ("was given", format!("argument {position}")),
203 };
204 let (found, expected) = (&self.found, &self.expected);
205 if self.path.is_empty() {
206 match part {
207 Part::Result => {
210 format!("`{shown}` {verb} `{found}`, but its schema declares `{expected}`")
211 }
212 Part::Argument(_) => format!(
213 "`{shown}` {verb} `{found}` as {whole}, but its schema declares `{expected}` there"
214 ),
215 }
216 } else {
217 format!(
218 "`{shown}` {verb} `{found}` at `{}` of {whole}, but its schema declares `{expected}` there",
219 self.path
220 )
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::host::ResourceHandle;
229 use crate::value::{MapKey, StructValue};
230 use std::rc::Rc;
231
232 static CONNECTION: ResourceSchema = ResourceSchema {
241 name: "Connection",
242 task_safe: true,
243 operations: &[],
244 };
245
246 fn host_struct(type_name: &str, fields: Vec<(&str, Value)>) -> Value {
248 Value(Repr::Struct(Rc::new(StructValue {
249 type_name: type_name.into(),
250 fields: fields
251 .into_iter()
252 .map(|(name, value)| (name.into(), value))
253 .collect(),
254 opaque: false,
255 })))
256 }
257
258 #[test]
259 fn a_declared_type_admits_the_value_it_names() {
260 assert!(HostType::Unit.admits(&Value(Repr::Unit)).is_ok());
261 assert!(HostType::Bool.admits(&Value(Repr::Bool(true))).is_ok());
262 assert!(HostType::Int.admits(&Value(Repr::Int(3))).is_ok());
263 assert!(HostType::String
264 .admits(&Value(Repr::Str("text".into())))
265 .is_ok());
266 assert!(HostType::Duration
267 .admits(&Value(Repr::Duration(500)))
268 .is_ok());
269 assert!(HostType::Error.admits(&Value::error("gone")).is_ok());
270 }
271
272 #[test]
273 fn a_value_of_another_type_is_a_mismatch_where_the_two_part_company() {
274 let mismatch = HostType::String
275 .admits(&Value(Repr::Int(3)))
276 .expect_err("an `Int` is not a `String`");
277
278 assert_eq!(mismatch.path, "");
279 assert_eq!(mismatch.expected, HostType::String);
280 assert_eq!(mismatch.found, "Int");
281 assert_eq!(
282 mismatch.describe("wayward.read", Part::Result),
283 "`wayward.read` answered `Int`, but its schema declares `String`"
284 );
285 }
286
287 #[test]
291 fn a_mismatch_names_the_argument_it_was_found_in() {
292 let mismatch = HostType::String
293 .admits(&Value(Repr::Int(3)))
294 .expect_err("an `Int` is not a `String`");
295
296 assert_eq!(
297 mismatch.describe("documents.read", Part::Argument(1)),
298 "`documents.read` was given `Int` as argument 1, but its schema declares `String` there"
299 );
300 }
301
302 #[test]
303 fn a_declared_type_is_followed_all_the_way_down() {
304 let declared = HostType::Result(&HostType::Array(&HostType::String), &HostType::Error);
305
306 assert!(declared
307 .admits(&Value::ok(Value(Repr::Array(
308 vec![Value(Repr::Str("one".into()))].into()
309 ))))
310 .is_ok());
311 assert!(
312 declared.admits(&Value::err(Value::error("gone"))).is_ok(),
313 "the declared error type is the one inside `Err`"
314 );
315
316 let mismatch = declared
317 .admits(&Value::ok(Value(Repr::Array(
318 vec![Value(Repr::Str("one".into())), Value(Repr::Int(2))].into(),
319 ))))
320 .expect_err("an `Int` among the declared strings is not admitted");
321 assert_eq!(mismatch.path, "Ok(_)[1]");
322 assert_eq!(mismatch.expected, HostType::String);
323 assert_eq!(
324 mismatch.describe("wayward.list", Part::Result),
325 "`wayward.list` answered `Int` at `Ok(_)[1]` of its result, but its schema declares `String` there"
326 );
327 assert_eq!(
328 mismatch.describe("wayward.list", Part::Argument(2)),
329 "`wayward.list` was given `Int` at `Ok(_)[1]` of argument 2, but its schema declares `String` there"
330 );
331 }
332
333 #[test]
336 fn a_set_is_followed_into_its_elements() {
337 let declared = HostType::Set(&HostType::String);
338 assert!(declared
339 .admits(&Value::set([
340 MapKey::Str("docs".to_string()),
341 MapKey::Str("migration".to_string()),
342 ]))
343 .is_ok());
344 assert!(
345 declared.admits(&Value::set([])).is_ok(),
346 "an empty set is a set of anything"
347 );
348
349 let mismatch = declared
350 .admits(&Value::set([
351 MapKey::Str("docs".to_string()),
352 MapKey::Int(2),
353 ]))
354 .expect_err("an `Int` among the declared strings is not admitted");
355 assert_eq!(mismatch.path, "[0]");
358 assert_eq!(mismatch.expected, HostType::String);
359 assert_eq!(
360 mismatch.describe("reviews.labels", Part::Result),
361 "`reviews.labels` answered `Int` at `[0]` of its result, but its schema declares `String` there"
362 );
363 }
364
365 #[test]
369 fn a_map_is_followed_into_both_of_its_halves() {
370 let declared = HostType::Map(&HostType::String, &HostType::Int);
371 assert!(declared
372 .admits(&Value::map([(
373 MapKey::Str("breaking-change".to_string()),
374 Value(Repr::Int(3)),
375 )]))
376 .is_ok());
377
378 let wrong_value = declared
379 .admits(&Value::map([(
380 MapKey::Str("breaking-change".to_string()),
381 Value(Repr::Str("three".into())),
382 )]))
383 .expect_err("a `String` is not the declared `Int`");
384 assert_eq!(wrong_value.path, "[breaking-change]");
385 assert_eq!(wrong_value.expected, HostType::Int);
386
387 let wrong_key = declared
388 .admits(&Value::map([(MapKey::Int(3), Value(Repr::Int(3)))]))
389 .expect_err("an `Int` is not the declared `String`");
390 assert_eq!(wrong_key.path, "key[0]");
391 assert_eq!(wrong_key.expected, HostType::String);
392 }
393
394 #[test]
397 fn a_set_nested_in_a_result_is_reached_through_it() {
398 let declared = HostType::Result(&HostType::Set(&HostType::String), &HostType::Error);
399 assert!(declared
400 .admits(&Value::ok(Value::set([MapKey::Str("docs".to_string())])))
401 .is_ok());
402 assert_eq!(
403 declared
404 .admits(&Value::ok(Value::set([MapKey::Int(1)])))
405 .expect_err("the element type is checked through the `Ok`")
406 .path,
407 "Ok(_)[0]"
408 );
409 assert_eq!(
410 declared
411 .admits(&Value::ok(Value(Repr::Array(vec![].into()))))
412 .expect_err("an array is not a set")
413 .found,
414 "Array"
415 );
416 }
417
418 #[test]
419 fn an_option_is_admitted_by_either_case() {
420 let declared = HostType::Option(&HostType::String);
421
422 assert!(declared.admits(&Value::none()).is_ok());
423 assert!(declared
424 .admits(&Value::some(Value(Repr::Str("set".into()))))
425 .is_ok());
426 assert_eq!(
427 declared
428 .admits(&Value::some(Value(Repr::Int(3))))
429 .expect_err("`Some(3)` is not an `Option<String>`")
430 .path,
431 "Some(_)"
432 );
433 }
434
435 #[test]
436 fn any_admits_whatever_it_is_given() {
437 assert!(HostType::Any.admits(&Value(Repr::Int(3))).is_ok());
438 assert!(HostType::Any.admits(&Value(Repr::Unit)).is_ok());
439 assert!(HostType::Any
440 .admits(&host_struct("demo.Point", Vec::new()))
441 .is_ok());
442 assert!(HostType::Array(&HostType::Any)
443 .admits(&Value(Repr::Array(
444 vec![Value(Repr::Int(1)), Value(Repr::Unit)].into()
445 )))
446 .is_ok());
447 }
448
449 #[test]
450 fn a_named_type_is_checked_by_the_name_the_value_carries() {
451 let declared = HostType::Named("http.Response");
452 let response = host_struct(
453 "http.Response",
454 vec![
455 ("status", Value(Repr::Int(200))),
456 ("body", Value(Repr::Str("ok".into()))),
457 ],
458 );
459 assert!(declared.admits(&response).is_ok());
460
461 assert!(declared
464 .admits(&host_struct("http.Response", Vec::new()))
465 .is_ok());
466
467 assert_eq!(
468 declared
469 .admits(&host_struct("demo.Point", Vec::new()))
470 .expect_err("another struct is not an `http.Response`")
471 .found,
472 "demo.Point"
473 );
474 }
475
476 #[test]
477 fn a_named_resource_is_checked_by_the_kind_the_handle_was_issued_for() {
478 let declared = HostType::Named("database.Connection");
479 let handle = Value(Repr::Resource(ResourceHandle::new(
480 "database",
481 &CONNECTION,
482 7,
483 )));
484 assert!(declared.admits(&handle).is_ok());
485
486 let elsewhere = Value(Repr::Resource(ResourceHandle::new("http", &CONNECTION, 7)));
487 assert_eq!(
488 declared
489 .admits(&elsewhere)
490 .expect_err("the same kind of another module is another type")
491 .found,
492 "http.Connection"
493 );
494 }
495}