Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,442 @@
|
||||
use super::package::packages;
|
||||
|
||||
#[test]
|
||||
fn fixed_wire_budget_proof_retains_dynamic_checks_at_schema_limits() {
|
||||
for (levels, fields, input_fixed, output_fixed) in [
|
||||
(63, 1, true, true),
|
||||
(64, 1, false, false),
|
||||
(14, 2, true, true),
|
||||
(15, 2, true, false),
|
||||
(16, 2, false, false),
|
||||
] {
|
||||
let mut app = String::new();
|
||||
let mut kind = "Int".to_owned();
|
||||
for level in 0..levels {
|
||||
let members = if fields == 1 {
|
||||
format!("value: {kind}")
|
||||
} else {
|
||||
format!("first: {kind}\nsecond: {kind}")
|
||||
};
|
||||
app.push_str(&format!("type Layer{level} {{ {members} }}\n"));
|
||||
kind = format!("Layer{level}");
|
||||
}
|
||||
app.push_str(&format!(
|
||||
"run(value: {kind}) (answer: {kind}) {{ answer = value }}"
|
||||
));
|
||||
let program = packages(&[
|
||||
("shop.account.app", &app),
|
||||
(
|
||||
"shop.account.api",
|
||||
"cmd run = app.run\npublic post run = app.run",
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
let emitted = dever::compiler::emit::application::generate(&program);
|
||||
assert_eq!(
|
||||
emitted.contains("wire::decode_fixed(input)"),
|
||||
input_fixed,
|
||||
"{levels}/{fields}"
|
||||
);
|
||||
assert_eq!(
|
||||
emitted.contains("input.decode(dever_runtime::wire::decode_fixed)"),
|
||||
input_fixed,
|
||||
"HTTP {levels}/{fields}"
|
||||
);
|
||||
assert_eq!(
|
||||
emitted.matches("wire::response_fixed(&result)").count(),
|
||||
usize::from(output_fixed),
|
||||
"{levels}/{fields}"
|
||||
);
|
||||
assert_eq!(
|
||||
emitted.contains("wire::response_fixed_bytes(&result)"),
|
||||
output_fixed,
|
||||
"HTTP {levels}/{fields}"
|
||||
);
|
||||
}
|
||||
for (kind, decoder, encoder) in [
|
||||
("List<Int>", "decode_shaped", "response"),
|
||||
("Json", "decode_json", "response_with"),
|
||||
("List<Json>", "decode", "response_with"),
|
||||
("List<Text>?", "decode_shaped", "response"),
|
||||
] {
|
||||
let app = format!("run(value: {kind}) (answer: {kind}) {{ answer = value }}");
|
||||
let program = packages(&[
|
||||
("shop.account.app", &app),
|
||||
(
|
||||
"shop.account.api",
|
||||
"cmd run = app.run\npublic post run = app.run",
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
let emitted = dever::compiler::emit::application::generate(&program);
|
||||
if decoder == "decode_json" {
|
||||
assert!(emitted.contains("let args: Args0 = decode_args0(input)?;"));
|
||||
assert!(emitted.contains("input.decode(decode_httpargs0)"));
|
||||
assert_eq!(emitted.matches("wire::decode_json(input,").count(), 2);
|
||||
assert_eq!(
|
||||
emitted.contains("wire::decode(input)"),
|
||||
kind == "List<Json>"
|
||||
);
|
||||
} else {
|
||||
assert!(!emitted.contains("wire::decode_json("));
|
||||
assert!(
|
||||
emitted.contains(&format!("wire::{decoder}(input)")),
|
||||
"{kind}"
|
||||
);
|
||||
assert!(
|
||||
emitted.contains(&format!("input.decode(dever_runtime::wire::{decoder})")),
|
||||
"{kind}"
|
||||
);
|
||||
}
|
||||
assert!(emitted.contains(&format!("wire::{encoder}(")), "{kind}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_budget_includes_fixed_records_and_optional_json() {
|
||||
let program = packages(&[
|
||||
(
|
||||
"shop.account.app",
|
||||
"type Row { payload: Json?\nnote: Text? }\nrun(row: Row?, other: Json, label: Text) (answer: Row?, extra: Json, name: Text) { answer = row\nextra = other\nname = label }",
|
||||
),
|
||||
(
|
||||
"shop.account.api",
|
||||
"cmd run = app.run\npublic post run = app.run",
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
let generated = dever::compiler::emit::application::generate(&program);
|
||||
assert_eq!(generated.matches("wire::decode_json(input,").count(), 2);
|
||||
assert!(generated.contains("budget.open()?; budget.value(3)?;"));
|
||||
assert!(generated.contains("budget.open()?; budget.value(2)?;"));
|
||||
assert!(generated.contains("if let Some(item) = (&(args).field0).as_ref()"));
|
||||
assert!(generated.contains("budget.json(item)?;"));
|
||||
assert!(generated.contains("budget.json(&(args).field1)?;"));
|
||||
assert!(!generated.contains("Json::from_validated_raw"));
|
||||
assert!(!generated.contains("Json::deserialize_validated"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_with_any_typed_list_keeps_whole_input_validation() {
|
||||
for kind in ["List<Int>", "List<Json>", "List<Json?>?"] {
|
||||
for nested in [false, true] {
|
||||
let (definition, kind) = if nested {
|
||||
(format!("type Row {{ values: {kind} }}\n"), "Row?")
|
||||
} else {
|
||||
(String::new(), kind)
|
||||
};
|
||||
let app = format!(
|
||||
"{definition}run(payload: Json, other: {kind}) (answer: Json, extra: {kind}) {{ answer = payload\nextra = other }}"
|
||||
);
|
||||
let program = packages(&[
|
||||
("shop.account.app", &app),
|
||||
(
|
||||
"shop.account.api",
|
||||
"cmd run = app.run\npublic post run = app.run",
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
let generated = dever::compiler::emit::application::generate(&program);
|
||||
assert!(generated.contains("let args: Args0 = dever_runtime::wire::decode(input)?;"));
|
||||
assert!(generated.contains("input.decode(dever_runtime::wire::decode)"));
|
||||
assert!(!generated.contains("wire::decode_json("));
|
||||
assert!(!generated.contains("budget."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_wire_depth_is_proved_before_skipping_validation() {
|
||||
for (levels, decoder) in [(63, "decode_shaped"), (64, "decode")] {
|
||||
let kind = format!("{}Int{}", "List<".repeat(levels), ">".repeat(levels));
|
||||
let app = format!("run(value: {kind}) (answer: Int) {{ answer = length(value) }}");
|
||||
let program = packages(&[
|
||||
("shop.account.app", &app),
|
||||
(
|
||||
"shop.account.api",
|
||||
"cmd run = app.run\npublic post run = app.run",
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
let emitted = dever::compiler::emit::application::generate(&program);
|
||||
assert!(
|
||||
emitted.contains(&format!("wire::{decoder}(input)")),
|
||||
"{levels}"
|
||||
);
|
||||
assert!(
|
||||
emitted.contains(&format!("input.decode(dever_runtime::wire::{decoder})")),
|
||||
"{levels}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_entries_bind_typed_apps_and_keep_protected_entries_closed() {
|
||||
assert!(packages(&[("main", "get(value: Int) (answer: Int) { answer = value }")]).is_ok());
|
||||
let app = "echo(value: Int) (answer: Int) { answer = value }";
|
||||
let program = packages(&[
|
||||
("shop.account.app", app),
|
||||
(
|
||||
"shop.account.api.admin.session",
|
||||
"public get status = app.echo",
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(program.http[0].path, "/shop/account/admin/session/status");
|
||||
assert_eq!(program.http[0].topic, "admin/session");
|
||||
assert_eq!(program.http[0].method, "GET");
|
||||
for (declaration, code) in [
|
||||
("get status = app.echo", "C014"),
|
||||
("authenticated get status = app.echo", "C014"),
|
||||
(
|
||||
"public get status = app.echo\npublic get status = app.echo",
|
||||
"C002",
|
||||
),
|
||||
("public get status = other.echo", "C006"),
|
||||
] {
|
||||
assert_eq!(
|
||||
packages(&[("shop.account.app", app), ("shop.account.api", declaration)])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
code
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
(
|
||||
"shop.account.app",
|
||||
"echo(value: List<Int>) (answer: List<Int>) { answer = value }"
|
||||
),
|
||||
("shop.account.api", "public get status = app.echo")
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C005"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_wire_rejects_nested_constrained_records() {
|
||||
for kind in ["Port", "List<Port?>", "Envelope"] {
|
||||
let app = format!(
|
||||
"type Port {{ value: Int >= 1 }}\ntype Envelope {{ ports: List<Port?> }}\nrun(value: {kind}) (answer: {kind}) {{ answer = value }}"
|
||||
);
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", &app),
|
||||
("shop.account.api", "cmd run = app.run")
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C011"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_topics_share_app_contracts_and_domain_dependencies() {
|
||||
let program = packages(&[
|
||||
("shop.account.app.read", "type User { name: Text }\nread(name: Text) (answer: User) { answer = User { name = domain.label(name) } }"),
|
||||
("shop.account.app.write", "echo(user: User) (answer: User) { answer = user }"),
|
||||
("shop.account.domain", "label(name: Text) (answer: Text) { answer = name + \"!\" }"),
|
||||
("shop.account.api", "cmd read = app.read\ncmd echo = app.echo"),
|
||||
]).unwrap();
|
||||
assert_eq!(program.commands.len(), 2);
|
||||
assert_eq!(program.commands[0].name, "shop.account.read");
|
||||
assert!(
|
||||
program
|
||||
.functions
|
||||
.iter()
|
||||
.any(|function| function.signature.name == "label")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_app_fields_share_topic_ownership_and_reject_other_domains() {
|
||||
let declaration = "type Session { private key: Text\nlabel: Text }";
|
||||
assert!(packages(&[
|
||||
("shop.account.app.types", declaration),
|
||||
("shop.account.app.write", "make(key: Text) (answer: Session) { answer = Session { key = key, label = \"name\" } }"),
|
||||
("shop.account.app.read", "read(value: Session) (answer: Text) { value.key = value.key + \"!\"\nanswer = value.key }"),
|
||||
]).is_ok());
|
||||
for operation in [
|
||||
"answer = value.key",
|
||||
"value.key = \"changed\"\nanswer = value.label",
|
||||
"value = shop.account.Session { key = \"hidden\", label = \"name\" }\nanswer = value.label",
|
||||
] {
|
||||
let caller = format!("read(value: shop.account.Session) (answer: Text) {{ {operation} }}");
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", declaration),
|
||||
("shop.other.app", &caller)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_wire_rejects_nested_private_records_in_both_directions() {
|
||||
for kind in ["Session", "List<Session?>", "Envelope"] {
|
||||
for function in [
|
||||
format!("run(value: {kind}) () {{}}"),
|
||||
format!("run(value: {kind}) (answer: {kind}) {{ answer = value }}"),
|
||||
] {
|
||||
let app = format!(
|
||||
"type Session {{ private key: Text }}\ntype Envelope {{ sessions: List<Session?> }}\n{function}"
|
||||
);
|
||||
let error = packages(&[
|
||||
("shop.account.app", &app),
|
||||
("shop.account.api", "cmd run = app.run"),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, "C006", "{error:?}");
|
||||
assert!(error.message.contains("wire boundary"), "{error:?}");
|
||||
}
|
||||
}
|
||||
let app = "type Session { private key: Text }\nrun() (answer: Session) { answer = Session { key = \"hidden\" } }";
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", app),
|
||||
("shop.account.api", "cmd run = app.run")
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_role_access_cannot_be_bypassed_with_qualified_names() {
|
||||
for (source, target) in [
|
||||
("shop.first.domain", "shop.second.run"),
|
||||
("shop.first.domain", "app.run"),
|
||||
("shop.first.app", "shop.second.domain.hidden"),
|
||||
] {
|
||||
let caller = format!("entry() (answer: Int) {{ answer = {target}() }}");
|
||||
let result = packages(&[
|
||||
(source, &caller),
|
||||
("shop.first.app.extra", "run() (answer: Int) { answer = 1 }"),
|
||||
("shop.second.app", "run() (answer: Int) { answer = 2 }"),
|
||||
(
|
||||
"shop.second.domain",
|
||||
"hidden() (answer: Int) { answer = 3 }",
|
||||
),
|
||||
]);
|
||||
assert_eq!(result.unwrap_err().code, "C006", "{source}: {target}");
|
||||
}
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", "type Visible { secret: domain.Hidden }"),
|
||||
("shop.account.domain", "type Hidden { value: Int }"),
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
assert_eq!(
|
||||
packages(&[("shop.account.app", "public run() () {}")])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
assert_eq!(
|
||||
packages(&[("shop.account.api", "run() () {}")])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
assert_eq!(
|
||||
packages(&[("main", "cmd run = app.run")]).unwrap_err().code,
|
||||
"C006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_secret_inputs_use_the_existing_input_codec_without_output_access() {
|
||||
for kind in ["Secret", "Secret?", "List<Secret>"] {
|
||||
let app = format!("run(value: {kind}) (answer: Bool) {{ answer = true }}");
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", &app),
|
||||
("shop.account.api", "cmd run = app.run")
|
||||
])
|
||||
.is_ok()
|
||||
);
|
||||
let leaking = format!("run(value: {kind}) (answer: {kind}) {{ answer = value }}");
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", &leaking),
|
||||
("shop.account.api", "cmd run = app.run")
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands_reject_ambiguous_targets_and_invalid_wire_contracts() {
|
||||
for app in [
|
||||
"run() () {}\nrun(value: Int) () {}",
|
||||
"run(action: handler() ()) () { action() }",
|
||||
"type State { Ready(value: Int) }\nrun(value: State) (answer: State) { answer = value }",
|
||||
"type State { Ready\n error Failed }\nrun(value: State) (answer: State) { answer = value }",
|
||||
] {
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", app),
|
||||
("shop.account.api", "cmd run = app.run")
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C005"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", "run() () {}"),
|
||||
("shop.account.api", "cmd run = app.run\ncmd run = app.run"),
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C002"
|
||||
);
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", "run() () {}"),
|
||||
("shop.account.api", "cmd run = shop.account.run"),
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_entry_reachability_and_domain_cycles_are_checked() {
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", "run() () {}\nunused() () {}"),
|
||||
("shop.account.api", "cmd run = app.run"),
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C016"
|
||||
);
|
||||
assert_eq!(packages(&[
|
||||
("shop.first.app", "run() (answer: Int) { answer = second.read() }\nread() (answer: Int) { answer = 1 }"),
|
||||
("shop.second.app", "read() (answer: Int) { answer = first.read() }"),
|
||||
("shop.first.api", "cmd run = app.run"),
|
||||
]).unwrap_err().code, "C007");
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app.read", "run() () {}"),
|
||||
("shop.account.app.write", "run() () {}"),
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C013"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user