use super::*; fn rest(model: &str, domain: &str) -> Result { 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") ); assert!(generated.contains("Json::from_validated_raw(decoded.field0)")); } else if kind == "Json?" { assert!( generated.contains( "field0: Option>" ) ); 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()); }