185 lines
7.7 KiB
Rust
185 lines
7.7 KiB
Rust
use dever::{
|
|
compiler::{check, model::Program},
|
|
syntax::{diagnostic::Diagnostic, source::Source},
|
|
};
|
|
|
|
fn linked(text: &str) -> Result<Program, Diagnostic> {
|
|
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::<Result<Vec<_>, _>>()?;
|
|
check::check(
|
|
modules,
|
|
&inputs
|
|
.iter()
|
|
.map(|(package, _)| (*package).into())
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
}
|
|
|
|
#[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<Int> }\nmain(values: List<Int>) (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<Row?>) (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<Text, Int>) (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<Row?>) (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<State> }\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<Text>\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<Text> }\nmain(value: Json) (names: List<Text>) { row: Row = dever.json.decode(value)\n names = row.names }").is_ok());
|
|
assert!(linked("main() (answer: Int) { values: List<Int> = []\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<Int?> = 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"
|
|
);
|
|
}
|
|
}
|