Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,202 @@
|
||||
use super::*;
|
||||
|
||||
fn rest(model: &str, domain: &str) -> Result<Program, Diagnostic> {
|
||||
packages(&[
|
||||
("dever.database", ERROR),
|
||||
("dever.api", "public type Error { error Unauthorized }"),
|
||||
(
|
||||
"dever.auth",
|
||||
"public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }",
|
||||
),
|
||||
(
|
||||
"text",
|
||||
"public trim(input: Text) (value: Text) pure { value = input }",
|
||||
),
|
||||
(
|
||||
"demo.account.model",
|
||||
"global type Account { subject: Text\nsession: Text }",
|
||||
),
|
||||
("demo.account.model.profile", model),
|
||||
("demo.account.domain", domain),
|
||||
("demo.account.app", APP),
|
||||
(
|
||||
"demo.account.api.admin.session",
|
||||
"public get health = app.health",
|
||||
),
|
||||
("demo.account.api.admin.profile", "rest model.profile"),
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_wire_reuses_schema_proof_without_skipping_json_validation() {
|
||||
for (kind, decoder) in [
|
||||
("Text", "decode_fixed"),
|
||||
("Int?", "decode_fixed"),
|
||||
("Json", "decode"),
|
||||
("Json?", "decode"),
|
||||
] {
|
||||
let program = rest(&format!("type Profile {{ value: {kind} }}"), "").unwrap();
|
||||
let generated = dever::compiler::emit::application::generate(&program);
|
||||
assert!(!generated.contains("wire::decode_json("));
|
||||
for operation in ["Create", "Replace"] {
|
||||
assert!(
|
||||
generated.contains(&format!(
|
||||
"let args: Rest{operation}0 = input.decode(dever_runtime::wire::{decoder})"
|
||||
)),
|
||||
"{kind}/{operation}"
|
||||
);
|
||||
}
|
||||
if kind == "Json" {
|
||||
assert!(
|
||||
generated.contains("field0: Box<dever_runtime::wire::serde_json::value::RawValue>")
|
||||
);
|
||||
assert!(generated.contains("Json::from_validated_raw(decoded.field0)"));
|
||||
} else if kind == "Json?" {
|
||||
assert!(
|
||||
generated.contains(
|
||||
"field0: Option<Box<dever_runtime::wire::serde_json::value::RawValue>>"
|
||||
)
|
||||
);
|
||||
assert!(generated.contains(
|
||||
"decoded.field0.map(dever_runtime::wire::json::Json::from_validated_raw)"
|
||||
));
|
||||
} else {
|
||||
assert!(!generated.contains("Json::deserialize_validated"));
|
||||
assert!(!generated.contains("Json::deserialize_optional_validated"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_json_output_projects_only_public_fields() {
|
||||
let program = rest(
|
||||
"type Profile { value: Json\nprivate secret: Text create = \"hidden\"\nextra: Json? }",
|
||||
"",
|
||||
)
|
||||
.unwrap();
|
||||
let generated = dever::compiler::emit::application::generate(&program);
|
||||
let view = generated
|
||||
.split_once("impl RestView0<'_> {")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once("struct RestItems0")
|
||||
.unwrap()
|
||||
.0;
|
||||
assert!(view.contains("field2"));
|
||||
assert!(!view.contains("field3"));
|
||||
assert!(view.contains("field4"));
|
||||
assert!(!view.contains("secret"));
|
||||
assert!(!view.contains("serialize_field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_json_defaults_keep_their_typed_decoder() {
|
||||
for (kind, value, decoder) in [
|
||||
("Json", r#""{}""#, "deserialize_validated"),
|
||||
("Json?", "null", "deserialize_optional_validated"),
|
||||
] {
|
||||
let program = rest(
|
||||
&format!("type Profile {{ value: {kind} default {value} }}"),
|
||||
"",
|
||||
)
|
||||
.unwrap();
|
||||
let generated = dever::compiler::emit::application::generate(&program);
|
||||
assert!(generated.contains(&format!("Json::{decoder}")));
|
||||
assert!(generated.contains("#[serde(default=\"rest_create_0_default0\")]"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_bindings_use_checked_functions_and_preserve_owner_identity() {
|
||||
let program = rest("type Profile { private owner user_id: model.id = dever.auth.userId()\nname: Text unique create = domain.normalize(input) replace = text.trim(input) search = input\nprivate source: Text create = \"source\" replace = \"replaced\" }","normalize(input: Text) (value: Text) pure { value = input }").unwrap();
|
||||
let entry = &program.rest[0];
|
||||
assert_eq!(entry.owner.as_ref().unwrap().position, 2);
|
||||
assert!(!entry.read.contains(&2));
|
||||
assert_eq!(
|
||||
entry.create.iter().filter(|binding| binding.input).count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(entry.search.as_ref().unwrap().position, 3);
|
||||
assert!(
|
||||
entry
|
||||
.functions()
|
||||
.all(|function| program.entries().any(|entry| entry == function))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_rejects_unsafe_field_sources_and_ambiguous_search() {
|
||||
for model in [
|
||||
"type Profile { private name: Text create = input }",
|
||||
"type Profile { owner id_value: Int? = dever.auth.userId()\nname: Text }",
|
||||
"type Profile { owner first_id: Int = dever.auth.userId()\nowner second_id: Int = dever.auth.tenantId()\nname: Text }",
|
||||
"type Profile { name: Text search = input }",
|
||||
"type Profile { name: Text index search = \"fixed\" }",
|
||||
"type Profile { name: Text index search = input\nother_name: Text index search = input }",
|
||||
"type Profile { private required: Text\nname: Text }",
|
||||
"type Profile { name: Text create = domain.normalize(input) }",
|
||||
] {
|
||||
let error = rest(
|
||||
model,
|
||||
if model.contains("domain.normalize") {
|
||||
"normalize(input: Text) (value: Text) { value = input }"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, "C014", "{model}: {error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_search_reserves_pagination_names_without_banning_storage_fields() {
|
||||
for name in ["page", "size"] {
|
||||
let model = format!("type Profile {{ {name}: Int index search = input }}");
|
||||
let error = rest(&model, "").unwrap_err();
|
||||
assert_eq!(error.code, "C014");
|
||||
assert_eq!(
|
||||
error.message,
|
||||
"REST search field cannot use reserved pagination names"
|
||||
);
|
||||
let model = format!("type Profile {{ {name}: Int index }}");
|
||||
assert!(
|
||||
rest(&model, "").is_ok(),
|
||||
"{name} remains a valid storage field"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_preserves_required_private_fields_with_only_a_create_source() {
|
||||
let program = rest(
|
||||
"type Profile { private created_by: Text create = \"creator\"\nname: Text }",
|
||||
"",
|
||||
)
|
||||
.unwrap();
|
||||
let entry = &program.rest[0];
|
||||
let source = entry
|
||||
.create
|
||||
.iter()
|
||||
.find(|binding| binding.position == 2)
|
||||
.unwrap();
|
||||
assert!(source.function.is_some());
|
||||
assert!(!source.input);
|
||||
assert!(!entry.read.contains(&2));
|
||||
assert!(entry.replace.iter().all(|binding| binding.position != 2));
|
||||
let error = rest(
|
||||
"type Profile { private created_by: Text replace = \"replacement\"\nname: Text }",
|
||||
"",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, "C014");
|
||||
assert_eq!(
|
||||
error.message,
|
||||
"REST field is private and required without a source"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_field_metadata_does_not_escape_into_handler_contracts() {
|
||||
assert!(packages(&[("main","apply(callback: handler(value: Int create = input) (answer: Int)) (answer: Int) { answer = callback(1) }")]).is_err());
|
||||
}
|
||||
Reference in new issue
Block a user