use pushkin_compiler::{compile, CompileRequest, Target};
const ALL_TARGETS: [Target; 4] = [Target::Zod, Target::Pydantic, Target::Rust, Target::Sql];
const CANONICAL: &str = include_str!("../../../schemas/user.schema.json");
const RETIRED_DIVERGENT_PATTERN: &str = r"^[A-Za-z0-9_+-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,}$";
fn request(schema_json: &str, target: Target) -> CompileRequest {
CompileRequest {
contract_name: "user".to_owned(),
schema_json: schema_json.to_owned(),
target,
epoch: 1,
}
}
fn top_level_with(extra_key: &str, extra_value: &str) -> String {
format!(
r#"{{
"type": "object",
"{extra_key}": {extra_value},
"properties": {{ "name": {{ "type": "string" }} }},
"required": ["name"],
"additionalProperties": false
}}"#
)
}
fn property_with(extra_key: &str, extra_value: &str) -> String {
format!(
r#"{{
"type": "object",
"properties": {{
"name": {{ "type": "string", "{extra_key}": {extra_value} }}
}},
"required": ["name"],
"additionalProperties": false
}}"#
)
}
#[test]
fn committed_canonical_compiles_for_every_target() {
for target in ALL_TARGETS {
assert!(
compile(&request(CANONICAL, target)).is_ok(),
"the committed canonical contract must compile for {target:?}"
);
}
}
#[test]
fn committed_canonical_emission_is_byte_identical_twice() {
for target in ALL_TARGETS {
let first = compile(&request(CANONICAL, target)).unwrap().content;
let second = compile(&request(CANONICAL, target)).unwrap().content;
assert_eq!(
first, second,
"emission must stay deterministic for {target:?}"
);
}
}
#[test]
fn annotation_keywords_are_rejected_by_name_at_top_level() {
for (keyword, value) in [
("title", r#""UserCreate""#),
("description", r#""a user""#),
("examples", r#"[{"name":"Ada"}]"#),
("$defs", r#"{"Helper":{"type":"string"}}"#),
] {
let schema = top_level_with(keyword, value);
let message = compile(&request(&schema, Target::Rust))
.unwrap_err()
.to_string();
assert!(
message.contains(keyword),
"rejection must name the keyword '{keyword}': {message}"
);
}
}
#[test]
fn annotation_keywords_are_rejected_by_name_at_property_level() {
for (keyword, value) in [
("title", r#""Name""#),
("description", r#""the name""#),
("examples", r#"["Ada"]"#),
] {
let schema = property_with(keyword, value);
let message = compile(&request(&schema, Target::Rust))
.unwrap_err()
.to_string();
assert!(
message.contains(keyword),
"rejection must name the keyword '{keyword}': {message}"
);
}
}
#[test]
fn an_authored_title_can_never_be_silently_clobbered() {
let schema = top_level_with("title", r#""AuthoredName""#);
let message = compile(&request(&schema, Target::Rust))
.unwrap_err()
.to_string();
assert!(message.contains("title"), "must name 'title': {message}");
}
#[test]
fn pattern_without_email_format_is_rejected_with_the_alternative() {
let schema = property_with("pattern", r#""^[a-z]+$""#);
let message = compile(&request(&schema, Target::Rust))
.unwrap_err()
.to_string();
assert!(
message.contains("pattern"),
"must name 'pattern': {message}"
);
assert!(
message.contains("email"),
"must offer format 'email' as the alternative: {message}"
);
assert!(
message.contains("widening"),
"must point at the widening-step path: {message}"
);
}
#[test]
fn pattern_with_email_format_but_wrong_regex_is_rejected() {
let schema = format!(
r#"{{
"type": "object",
"properties": {{
"email": {{
"type": "string",
"format": "email",
"pattern": {}
}}
}},
"required": ["email"],
"additionalProperties": false
}}"#,
serde_json::Value::String(RETIRED_DIVERGENT_PATTERN.to_owned())
);
let message = compile(&request(&schema, Target::Rust))
.unwrap_err()
.to_string();
assert!(
message.contains("pattern"),
"must name 'pattern': {message}"
);
}
#[test]
fn the_authoring_pipelines_email_companion_is_accepted() {
let canonical: serde_json::Value = serde_json::from_str(CANONICAL).unwrap();
let email = &canonical["properties"]["email"];
let schema = format!(
r#"{{
"type": "object",
"properties": {{ "email": {email} }},
"required": ["email"],
"additionalProperties": false
}}"#
);
for target in ALL_TARGETS {
assert!(
compile(&request(&schema, target)).is_ok(),
"the email format's pinned pattern companion must be accepted for {target:?}"
);
}
}
#[test]
fn a_non_string_pattern_is_rejected_by_keyword() {
let schema = property_with("pattern", "42");
let message = compile(&request(&schema, Target::Rust))
.unwrap_err()
.to_string();
assert!(
message.contains("pattern"),
"must name 'pattern': {message}"
);
assert!(
message.contains("string"),
"must say the value has to be a string: {message}"
);
}
#[test]
fn property_names_matching_allowlisted_keywords_still_compile() {
let schema = r#"{
"type": "object",
"properties": {
"title": { "type": "string" },
"pattern": { "type": "string" },
"description": { "type": "string" }
},
"required": ["title"],
"additionalProperties": false
}"#;
for target in ALL_TARGETS {
assert!(
compile(&request(schema, target)).is_ok(),
"property names are data, not keywords ({target:?})"
);
}
}
#[test]
fn no_emitter_output_changes_for_the_committed_contract() {
for (target, artifact) in [
(Target::Zod, "../../generated/user.zod.gen.ts"),
(Target::Pydantic, "../../generated/user_models.gen.py"),
(Target::Rust, "../../generated/user.gen.rs"),
(Target::Sql, "../../generated/user.gen.sql"),
] {
let emitted = compile(&CompileRequest {
contract_name: "user".to_owned(),
schema_json: CANONICAL.to_owned(),
target,
epoch: artifact_epoch(artifact).unwrap(),
})
.unwrap()
.content;
let committed = read_artifact(artifact).unwrap();
assert_eq!(
emitted, committed,
"emission drifted from the committed artifact for {target:?}"
);
}
}
fn read_artifact(relative: &str) -> Result<String, std::io::Error> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(relative);
std::fs::read_to_string(path)
}
fn artifact_epoch(artifact: &str) -> Option<u32> {
read_artifact(artifact).ok()?.lines().find_map(|line| {
line.split("pushkin-epoch:")
.nth(1)
.and_then(|rest| rest.split_whitespace().next())
.and_then(|value| value.parse().ok())
})
}