use dever::{ compiler::{check, model::Program}, syntax::{diagnostic::Diagnostic, source::Source}, }; fn linked(text: &str) -> Result { let inputs = [ ("main", text), ("dever.json", include_str!("../../../lib/dever/json.dever")), ( "dever.json.value", include_str!("../../../lib/dever/json/value.dever"), ), ]; let modules = inputs .iter() .enumerate() .map(|(index, (_, text))| { dever::syntax::parse(&Source::new( index, if index == 0 { "main.dever".into() } else { format!("library{index}.dever") }, (*text).into(), )) }) .collect::, _>>()?; check::check( modules, &inputs .iter() .map(|(package, _)| (*package).into()) .collect::>(), ) } #[test] fn encode_rejects_non_wire_types_and_untyped_empty_values() { for source in [ "main(value: Secret) (answer: Json) { answer = dever.json.encode(value) }", "type Row { value: Secret }\nmain(value: Secret) (answer: Json) { row = Row { value = value } answer = dever.json.encode(row) }", "type Row { private values: List }\nmain(values: List) (answer: Json) { row = Row { values = values } answer = dever.json.encode(row) }", "type Row { private value: Int }\nmain(value: Row) (answer: Json) { answer = dever.json.encode(value) }", "type Row { value: Int > 0 }\nmain(values: List) (answer: Json) { answer = dever.json.encode(values) }", "type State { Ready(value: Int) }\nmain(value: State) (answer: Json) { answer = dever.json.encode(value) }", "type State { Ready\n error Failed }\nmain(value: State) (answer: Json) { answer = dever.json.encode(value) }", "main(value: Map) (answer: Json) { answer = dever.json.encode(value) }", "main(value: dever.system.File) (answer: Json) { answer = dever.json.encode(value) }", "main() (answer: Json) { answer = dever.json.encode([]) }", "main() (answer: Json) { answer = dever.json.encode(null) }", ] { assert!(linked(source).is_err(), "{source}"); } assert!(linked("type Row { amount: Decimal }\nmain(values: List) (answer: Json) pure { answer = dever.json.encode(values) }").is_ok()); } #[test] fn payload_free_choices_use_the_existing_wire_codec() { assert!(linked("type State { Ready\n Busy }\nmain(value: State) (answer: Json) { answer = dever.json.encode(value) }").is_ok()); assert!(linked("type State { Ready\n Busy }\ntype Row { state: State\n optional: State?\n states: List }\nmain(value: Json) (row: Row) { row = dever.json.decode(value) }").is_ok()); } #[test] fn typed_json_decode_can_be_captured_without_an_intermediate_function() { let prefix = "type Row { name: Text }\ntype Result { Decoded(value: Row)\n error Invalid(cause: dever.json.JsonResult) }\n"; let program = linked(&format!("{prefix}main(payload: Json) (output: Result) {{ output = result(dever.json.decode(payload)) }}")).unwrap(); let main = program .functions .iter() .find(|function| function.signature.name == "main") .unwrap(); assert!(main.signature.failures.is_empty()); assert!( linked(&format!( "{prefix}main(payload: Json) () {{ lost: Result = result(dever.json.decode(payload)) }}" )) .unwrap_err() .code == "C012" ); let source = format!( "{prefix}type Failure {{ error Missing }}\nraise() (payload: Json) {{ fail(Failure.Missing) }}\nmain() (output: Result) {{ output = result(dever.json.decode(raise())) }}" ); let program = linked(&source).unwrap(); let main = program .functions .iter() .find(|function| function.signature.name == "main") .unwrap(); assert_eq!(main.signature.failures.len(), 1); assert_eq!( program.definitions[main.signature.failures[0].definition].name, "Failure" ); for source in [ format!("{prefix}main(payload: Json) () {{ lost = result(dever.json.decode(payload)) }}"), "type Wrong { Done(value: Int)\n error Invalid(message: Text) }\nmain(payload: Json) (output: Wrong) { output = result(dever.json.decode(payload)) }".into(), "type Limited { Done(value: Int > 0)\n error Invalid(cause: dever.json.JsonResult) }\nmain(payload: Json) (output: Limited) { output = result(dever.json.decode(payload)) }".into(), ] { assert!(linked(&source).is_err(), "{source}"); } } #[test] fn json_failures_preserve_capture_and_argument_propagation() { let source = "type Failure { error Missing }\nraise() (value: Int) { fail(Failure.Missing) }\nmain() (answer: dever.json.JsonResult) { answer = result(dever.json.encode(raise())) }"; let program = linked(source).unwrap(); let main = program .functions .iter() .find(|function| function.signature.name == "main") .unwrap(); assert_eq!(main.signature.failures.len(), 1); assert_eq!( program.definitions[main.signature.failures[0].definition].name, "Failure" ); for expression in [ "dever.json.encode(1)", "dever.json.propertyValue(dever.json.fromText(\"42\"), \"a\")", ] { let source = format!("main() () {{ lost = result({expression}) }}"); assert_eq!(linked(&source).unwrap_err().code, "C012"); } } #[test] fn typed_decode_requires_an_explicit_wire_type_and_propagates_errors() { let source = "type Row { names: List\noptional: Int? }\nmain(value: Json) (row: Row) { row = dever.json.decode(value) }"; let program = linked(source).unwrap(); let main = program .functions .iter() .find(|function| function.signature.name == "main") .unwrap(); assert_eq!(main.signature.failures.len(), 1); for source in [ "main(value: Json) () { row = dever.json.decode(value) }", "main(value: Text) (row: Int) { row = dever.json.decode(value) }", "type Row { private secret: Text }\nmain(value: Json) (row: Row) { row = dever.json.decode(value) }", ] { assert!(linked(source).is_err(), "{source}"); } } #[test] fn local_type_annotations_supply_context_without_wrapper_functions() { assert!(linked("type Row { names: List }\nmain(value: Json) (names: List) { row: Row = dever.json.decode(value)\n names = row.names }").is_ok()); assert!(linked("main() (answer: Int) { values: List = []\n values = [1]\n answer = length(values) }").is_ok()); for (source, code) in [ ("main(value: Int) () { value: Int = 1 }", "C002"), ("main() () { value = 1\n value: Int = 2 }", "C002"), ("main() () { value: Int = true }", "C005"), ("main() () { value: Int = 1\n value = true }", "C005"), ( "main() () { value: Channel = channel(Int?, 1) }", "C013", ), ] { assert_eq!(linked(source).unwrap_err().code, code, "{source}"); } } #[test] fn document_is_a_nominal_source_type_and_intrinsics_remain_private() { assert!( linked("main(document: dever.json.value.Document) (value: Int) { value = document.root }") .is_ok() ); assert!(linked("type Document { root: Int }\nmain(document: Document) (value: Text) { value = dever.json.stringify(document) }").is_err()); for expression in [ "dever.system.jsonFromText(\"{}\")", "dever.system.jsonObject({})", ] { assert_eq!( linked(&format!("main() () {{ value = {expression} }}")) .unwrap_err() .code, "C006" ); } }