use pushkin_compiler::{compile, CompileError, CompileRequest, Target};
const ALL_TARGETS: [Target; 4] = [Target::Zod, Target::Pydantic, Target::Rust, Target::Sql];
fn emit(schema: &str, target: Target) -> Result<String, CompileError> {
compile(&CompileRequest {
contract_name: "array_widening".to_owned(),
schema_json: schema.to_owned(),
target,
epoch: 1,
})
.map(|output| output.content)
}
fn object_with(property_spec: &str) -> String {
format!(
r#"{{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {{
"items_under_test": {property_spec}
}},
"required": ["items_under_test"],
"additionalProperties": false
}}"#
)
}
#[test]
fn arrays_of_each_supported_scalar_compile_on_every_target() {
for element in ["string", "integer", "number", "boolean"] {
let schema = object_with(&format!(
r#"{{ "type": "array", "items": {{ "type": "{element}" }} }}"#
));
for target in ALL_TARGETS {
let out = emit(&schema, target);
assert!(
out.is_ok(),
"array of {element} must compile on {target:?}: {out:?}"
);
}
}
}
#[test]
fn non_required_array_rejects_on_every_target() {
const SCHEMA: &str = r#"{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"tags": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": false
}"#;
for target in ALL_TARGETS {
let err = emit(SCHEMA, target).expect_err("a non-required array must reject");
let message = err.to_string();
assert!(
message.contains("tags"),
"{target:?} rejection must name the property: {message}"
);
assert!(
message.contains("required"),
"{target:?} rejection must state the requirement: {message}"
);
assert!(
message.contains("own widening step"),
"{target:?} rejection must name the alternative: {message}"
);
}
}
fn rejection_messages(property_spec: &str) -> Vec<(Target, Result<String, CompileError>)> {
let schema = object_with(property_spec);
ALL_TARGETS
.into_iter()
.map(|target| (target, emit(&schema, target)))
.collect()
}
macro_rules! assert_rejects_naming {
($property_spec:expr, $expected_fragment:expr $(,)?) => {
for (target, outcome) in rejection_messages($property_spec) {
let err = outcome.expect_err("construct outside the subset must reject");
let message = err.to_string();
assert!(
message.contains($expected_fragment),
"{target:?} rejection must name {:?}: {message}",
$expected_fragment
);
assert!(
message.contains("items_under_test"),
"{target:?} rejection must name the property: {message}"
);
}
};
}
#[test]
fn array_without_items_rejects() {
assert_rejects_naming!(r#"{ "type": "array" }"#, "items");
}
#[test]
fn nested_array_rejects() {
assert_rejects_naming!(
r#"{ "type": "array", "items": { "type": "array", "items": { "type": "string" } } }"#,
"array",
);
}
#[test]
fn array_of_objects_rejects() {
assert_rejects_naming!(
r#"{ "type": "array", "items": { "type": "object" } }"#,
"object",
);
}
#[test]
fn array_items_with_format_rejects() {
assert_rejects_naming!(
r#"{ "type": "array", "items": { "type": "string", "format": "email" } }"#,
"format",
);
}
#[test]
fn array_items_with_max_length_rejects() {
assert_rejects_naming!(
r#"{ "type": "array", "items": { "type": "string", "maxLength": 10 } }"#,
"maxLength",
);
}
#[test]
fn array_items_with_enum_rejects() {
assert_rejects_naming!(
r#"{ "type": "array", "items": { "type": "string", "enum": ["a"] } }"#,
"enum",
);
}
#[test]
fn array_with_min_items_rejects() {
assert_rejects_naming!(
r#"{ "type": "array", "items": { "type": "string" }, "minItems": 1 }"#,
"minItems",
);
}
#[test]
fn array_with_unique_items_rejects() {
assert_rejects_naming!(
r#"{ "type": "array", "items": { "type": "string" }, "uniqueItems": true }"#,
"uniqueItems",
);
}
#[test]
fn array_with_default_rejects() {
assert_rejects_naming!(
r#"{ "type": "array", "items": { "type": "string" }, "default": [] }"#,
"default",
);
}
#[test]
fn array_rejection_carries_the_allowlist() {
let schema =
object_with(r#"{ "type": "array", "items": { "type": "string" }, "minItems": 1 }"#);
let err = emit(&schema, Target::Zod).expect_err("minItems must reject");
let message = err.to_string();
assert!(
message.contains("type") && message.contains("items"),
"the rejection must state what IS admitted, not only what is not: {message}"
);
}
#[test]
fn existing_scalar_widening_contract_still_compiles() {
const WIDENING_SCHEMA: &str = r#"{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"age": { "type": "integer", "default": 30 },
"ratio": { "type": "number", "default": 0.5 },
"active": { "type": "boolean", "default": true },
"score": { "type": "number" },
"count": { "type": "integer" }
},
"required": ["score"],
"additionalProperties": false
}"#;
for target in ALL_TARGETS {
let out = emit(WIDENING_SCHEMA, target);
assert!(
out.is_ok(),
"{target:?} must still compile the scalar widening contract: {out:?}"
);
}
}