Skip to main content

cove_runtime/
schema.rs

1//! The Host API schema, and what a value has to be for a declared type to
2//! admit it.
3//!
4//! The schema itself is [`cove_schema`], a crate below both this one and the
5//! compiler, because ADR 0001 makes it "shared by the compiler, runtime, and
6//! CLI" and the dependency between those two runs one way. Everything it
7//! declares is re-exported here, so a host written against the runtime still
8//! names one crate: `cove_runtime::schema::HostType` is
9//! `cove_schema::HostType`.
10//!
11//! What is *not* there is [`Admits`], which is the only part of the schema
12//! that needs values. A `HostType` is a description and a [`Value`] is the
13//! runtime's; this is where the two meet, so it lives on the side of the
14//! boundary where values live. The compiler answers the same question against
15//! its own `Ty`, at the call site, where a mistake still has a span.
16
17pub 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
27/// Whether a value is one a declared type admits.
28///
29/// This is an extension of [`HostType`] rather than an inherent method
30/// because the type lives in a crate that has no values to check. Everything
31/// it says about *which* values a type admits is the schema's; only the
32/// walking of a [`Value`] is this crate's.
33pub trait Admits {
34    /// Whether `value` is one this type admits, and where it stops being one
35    /// when it is not.
36    ///
37    /// The check follows this type's own recursion rather than looking only
38    /// at the outermost constructor, because a shallow check would admit an
39    /// `Array<Int>` where an `Array<String>` was declared and the schema says
40    /// more than "an array". [`HostType::Any`] admits everything, which is
41    /// not a hole: it is the type of an operation whose meaning does not
42    /// depend on which value it was given — the work `clock.timeout` bounds,
43    /// the body `clock.every` repeats — so there is nothing there to check.
44    ///
45    /// [`HostType::Named`] is checked by name. Every value carries the
46    /// qualified type it was built with and a [`crate::host::ResourceHandle`]
47    /// carries the module and kind it was issued for, so the comparison is
48    /// one the value itself can answer, with no registry to consult and no
49    /// second lookup on a path every host call takes. What that leaves
50    /// unchecked is a [`TypeSchema`]'s *fields*: a value calling itself an
51    /// `http.Response` is taken at its word about what is inside it. See ADR
52    /// 0013's amendment for why that line is drawn there.
53    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            // The builtin error struct, which is what `Err` carries
66            // everywhere a host declares one.
67            (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            // A `Set` element and a `Map` key are `MapKey`s by construction,
78            // so the restriction a schema declares them under is already kept
79            // and what is left to check is the type. Each is read back as the
80            // value it stands for, which costs an allocation for a `Str` key
81            // and nothing for a scalar one; it is what lets one `admits` walk
82            // answer for both halves of a map rather than a second walk over a
83            // second vocabulary.
84            (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            // The two builtin enums, whose cases `cove_schema::builtins`
102            // declares: a case's name and how much it carries are read off
103            // the same entry `Value::some` and `Value::ok` build from.
104            (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            // A handle names its module and its kind, which together are the
129            // qualified name a signature writes.
130            (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
144/// A declared type and a value that is none of it, as a mismatch at the point
145/// the two part company.
146fn mismatched(declared: &HostType, value: &Value) -> Mismatch {
147    Mismatch {
148        path: String::new(),
149        expected: *declared,
150        found: value.type_name(),
151    }
152}
153
154/// Which part of a call a mismatch was found in.
155///
156/// The two are the same check on the same table read from opposite sides: a
157/// wrong result is the host breaking its own word, and a wrong argument is
158/// the program breaking it, so the diagnostic says which happened rather than
159/// leaving the reader to work it out from the operation's name.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub enum Part {
162    /// The value the host answered with.
163    Result,
164    /// The argument at this position, counted from one as a reader counts.
165    Argument(usize),
166}
167
168/// Where a value stopped agreeing with the type declared for it, and what was
169/// found there instead.
170///
171/// The disagreement is reported where it happens rather than at the top of
172/// the value: an operation declaring `Result<Array<String>, Error>` that
173/// answers `Ok([3])` disagrees at one element, and saying which one is the
174/// difference between a diagnostic the host's author can act on and one that
175/// says only that the two do not match. Nothing here is built until a value
176/// fails, so the path costs a run that never fails nothing at all.
177#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct Mismatch {
179    /// How the offending part is reached from the whole value, such as
180    /// `Ok(_)[0]`. Empty when the value itself, rather than something nested
181    /// inside it, is the disagreement.
182    pub path: String,
183    /// The type declared at that point.
184    pub expected: HostType,
185    /// What was found there, named the way a diagnostic names a value.
186    pub found: String,
187}
188
189impl Mismatch {
190    /// Re-anchors this mismatch one level out, where the part that disagrees
191    /// is reached by `step`.
192    fn inside(mut self, step: &str) -> Mismatch {
193        self.path.insert_str(0, step);
194        self
195    }
196
197    /// The disagreement, phrased for a diagnostic about the operation
198    /// `shown` names.
199    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                // A result is the whole of what an operation answers, so
208                // naming the place it was found would add nothing.
209                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    // ------------------------------------------- what a declared type admits
233    //
234    // ADR 0001 asks each operation to describe its argument, result, and
235    // error types, and ADR 0013's amendment makes both the result and the
236    // arguments ones the boundary holds a call to. These pin the vocabulary
237    // of that check; `host.rs` pins what the boundary does with it.
238
239    /// The one kind of resource a host in these tests can open.
240    static CONNECTION: ResourceSchema = ResourceSchema {
241        name: "Connection",
242        task_safe: true,
243        operations: &[],
244    };
245
246    /// A struct value named the way a host builds one: qualified by module.
247    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    /// The same mismatch, read from the other side of the call. A wrong
288    /// argument is the program's mistake rather than the host's, so it is
289    /// phrased as one.
290    #[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    /// A `Set` the host built crosses whole, which is what having the variant
334    /// is for: the element type is followed exactly as an `Array`'s is.
335    #[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        // Ascending key order, which is the order a `Set` has: the `Int` sorts
356        // before the `Str`.
357        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    /// A `Map` has two halves and the diagnostic says which one disagreed: a
366    /// key is named by its position and a value by the key it was held under,
367    /// because that is how a reader finds each of them again.
368    #[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    /// The two are ordinary compound types everywhere else a compound type is
395    /// read, so a `Set` nested in a `Result` is followed through both.
396    #[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        // The name is what is checked; the fields behind it are not. A
462        // `Response` with nothing inside it is still a `Response` here.
463        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}