217 lines
8.0 KiB
Rust
217 lines
8.0 KiB
Rust
use super::package::packages;
|
|
use dever::compiler::model::{Node, Program, Signature, Statement};
|
|
|
|
mod boundary;
|
|
mod effects;
|
|
mod settings;
|
|
|
|
const APP: &str = "type Problem { error Missing(message: Text)\n error Denied(code: Int) }\nread(value: Int) (answer: Int) { answer = port.read(value) }";
|
|
const PORT: &str = "read(value: Int) (answer: Int) fails app.Problem";
|
|
const ADAPTER: &str = "port.read(value: Int) (answer: Int) { answer = helper(value) }\nhelper(value: Int) (answer: Int) { answer = value + 1 }";
|
|
|
|
fn signature<'program>(program: &'program Program, name: &str) -> &'program Signature {
|
|
&program
|
|
.functions
|
|
.iter()
|
|
.find(|function| function.signature.name == name)
|
|
.unwrap()
|
|
.signature
|
|
}
|
|
|
|
#[test]
|
|
fn port_alias_binds_directly_and_preserves_the_declared_error_set() {
|
|
let program = packages(&[
|
|
("shop.account.app", APP),
|
|
("shop.account.port", PORT),
|
|
("shop.account.adapter", ADAPTER),
|
|
("shop.account.api", "cmd read = app.read"),
|
|
])
|
|
.unwrap();
|
|
assert_eq!(program.functions.len(), 3);
|
|
let app = &program.functions[program.commands[0].function];
|
|
let Statement::Assign { value, .. } = &app.clauses[0].body[0] else {
|
|
panic!()
|
|
};
|
|
let Node::Call(index, _) = value.kind else {
|
|
panic!()
|
|
};
|
|
assert_eq!(program.functions[index].signature.name, "port.read");
|
|
assert_eq!(app.signature.failures.len(), 2);
|
|
assert_eq!(
|
|
app.signature.failures,
|
|
program.functions[index].signature.failures
|
|
);
|
|
assert!(!app.signature.effects.unmediated_io);
|
|
assert!(program.api_snapshot().contains("Problem.Denied"));
|
|
}
|
|
|
|
#[test]
|
|
fn topics_dtos_static_handlers_and_adapter_clauses_share_existing_type_checks() {
|
|
let program = packages(&[
|
|
(
|
|
"shop.account.app",
|
|
"type Problem { error Missing(message: Text) }\nread(value: Int) (answer: Int) { row = port.mail.Row { value = value }\nanswer = apply(port.mail.read, row) }\napply(action: handler(row: port.mail.Row) (answer: Int), row: port.mail.Row) (answer: Int) { answer = action(row) }",
|
|
),
|
|
(
|
|
"shop.account.port.mail",
|
|
"type Row { value: Int }\nread(row: Row) (answer: Int) fails app.Problem",
|
|
),
|
|
(
|
|
"shop.account.adapter.local",
|
|
"port.mail.read(row: port.mail.Row) (answer: Int) { answer = row.value }",
|
|
),
|
|
]);
|
|
// App 的公开 handler 参数不能泄漏 Port DTO;在领域内经普通 App 函数也不能绕过。
|
|
assert_eq!(program.unwrap_err().code, "C006");
|
|
let program = packages(&[
|
|
("shop.account.app", APP),
|
|
("shop.account.port", PORT),
|
|
("shop.account.adapter", "port.read(value: Int < 0) (answer: Int) { fail(app.Problem.Missing(\"negative\")) }\nport.read(value: other) (answer: Int) { answer = value }"),
|
|
]).unwrap();
|
|
assert_eq!(signature(&program, "read").failures.len(), 2);
|
|
assert!(packages(&[
|
|
("shop.account.app", "type Problem { error Missing(message: Text) }\nread(value: Int) (answer: Int) { row = port.mail.Row { value = value }\nanswer = port.mail.read(row) }"),
|
|
("shop.account.port.mail", "type Row { value: Int }\nread(row: Row) (answer: Int) fails app.Problem"),
|
|
("shop.account.adapter.local", "port.mail.read(row: port.mail.Row) (answer: Int) { answer = row.value }"),
|
|
]).is_ok());
|
|
assert!(
|
|
packages(&[
|
|
(
|
|
"shop.account.app",
|
|
&APP.replace("port.read(value)", "sum(each(port.read, [value]))")
|
|
),
|
|
("shop.account.port", PORT),
|
|
("shop.account.adapter", ADAPTER),
|
|
])
|
|
.is_ok()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ports_reject_missing_mismatched_and_partial_implementations() {
|
|
for adapter in [
|
|
"port.read(wrong: Int) (answer: Int) { answer = wrong }",
|
|
"port.read(value: Text) (answer: Int) { answer = 1 }",
|
|
"port.read(value: Int) (wrong: Int) { wrong = value }",
|
|
"port.read(value: Int, action: handler() ()) (answer: Int) { answer = value }",
|
|
"port.unknown(value: Int) (answer: Int) { answer = value }",
|
|
"helper(value: Int) (answer: Int) { answer = value }",
|
|
] {
|
|
assert!(
|
|
packages(&[
|
|
("shop.account.app", APP),
|
|
("shop.account.port", PORT),
|
|
("shop.account.adapter", adapter)
|
|
])
|
|
.is_err(),
|
|
"{adapter}"
|
|
);
|
|
}
|
|
assert!(packages(&[("shop.account.app", APP), ("shop.account.port", PORT)]).is_err());
|
|
assert!(
|
|
packages(&[
|
|
("shop.account.app", APP),
|
|
(
|
|
"shop.account.port",
|
|
&format!("{PORT}\nwrite(value: Int) () fails app.Problem")
|
|
),
|
|
("shop.account.adapter.first", ADAPTER),
|
|
(
|
|
"shop.account.adapter.second",
|
|
"port.write(value: Int) () {}"
|
|
),
|
|
])
|
|
.unwrap_err()
|
|
.message
|
|
.contains("complete Port")
|
|
);
|
|
assert!(
|
|
packages(&[
|
|
("shop.account.app", APP),
|
|
("shop.account.port", PORT),
|
|
("shop.account.adapter.first", ADAPTER),
|
|
("shop.account.adapter.second", ADAPTER),
|
|
])
|
|
.is_ok()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn port_declarations_require_plain_types_and_public_explicit_errors() {
|
|
for port in [
|
|
"read(value: Int) (answer: Int) { answer = value }",
|
|
"read(value: Int < 0) (answer: Int) fails app.Problem",
|
|
"read(value: other) (answer: Int) fails app.Problem",
|
|
"read(value: handler() ()) (answer: Int) fails app.Problem",
|
|
"read(value: Int) (answer: Int > 0) fails app.Problem",
|
|
"read(value: Int) (answer: Int) pure fails app.Problem",
|
|
"read(value: Int) (answer: Int) recover(\"ignore\") fails app.Problem",
|
|
"read(value: Int) (answer: Int) fails Int",
|
|
"read(value: Int) (answer: Int) fails app.Problem?",
|
|
"read(value: Int) (answer: Int) fails app.Record",
|
|
"read(value: Int) (answer: Int) fails app.State",
|
|
"type Local { error Failed }\nread(value: Int) (answer: Int) fails Local",
|
|
"public read(value: Int) (answer: Int) fails app.Problem",
|
|
] {
|
|
let app = format!("{APP}\ntype Record {{ value: Int }}\ntype State {{ Ready }}");
|
|
assert!(
|
|
packages(&[
|
|
("shop.account.app", &app),
|
|
("shop.account.port", port),
|
|
("shop.account.adapter", ADAPTER)
|
|
])
|
|
.is_err(),
|
|
"{port}"
|
|
);
|
|
}
|
|
assert!(packages(&[("main", "read(value: Int) (answer: Int) fails Problem")]).is_err());
|
|
assert_eq!(
|
|
packages(&[
|
|
("shop.account.app", APP),
|
|
("shop.account.port", &format!("{PORT}\n{PORT}")),
|
|
("shop.account.adapter", ADAPTER)
|
|
])
|
|
.unwrap_err()
|
|
.code,
|
|
"C002"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn implementations_cannot_propagate_undeclared_errors() {
|
|
let adapter = "type Private { error Other }\nport.read(value: Int) (answer: Int) { fail(Private.Other()) }";
|
|
assert_eq!(
|
|
packages(&[
|
|
("shop.account.app", APP),
|
|
("shop.account.port", PORT),
|
|
("shop.account.adapter", adapter)
|
|
])
|
|
.unwrap_err()
|
|
.code,
|
|
"C012"
|
|
);
|
|
let app = "type Problem { error Missing(message: Text)\nerror Denied(code: Int) }\ntype ReadResult { Ready(value: Int)\nerror Failed(problem: Problem) }\nread(value: Int) (answer: ReadResult) { answer = result(port.read(value)) }";
|
|
assert!(
|
|
packages(&[
|
|
("shop.account.app", app),
|
|
("shop.account.port", PORT),
|
|
("shop.account.adapter", ADAPTER)
|
|
])
|
|
.is_ok()
|
|
);
|
|
let app = app.replace(
|
|
"answer = result(port.read(value))",
|
|
"lost = result(port.read(value))\nanswer = ReadResult.Ready(value)",
|
|
);
|
|
assert_eq!(
|
|
packages(&[
|
|
("shop.account.app", &app),
|
|
("shop.account.port", PORT),
|
|
("shop.account.adapter", ADAPTER)
|
|
])
|
|
.unwrap_err()
|
|
.code,
|
|
"C012"
|
|
);
|
|
}
|