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" ); }