mod error;
mod generate;
pub use error::Error;
pub use generate::generate;
#[cfg(test)]
mod tests {
use rand::SeedableRng;
use rand::rngs::StdRng;
use serde_json::Value;
use serde_json::json;
use crate::generate;
fn seeded_rng() -> StdRng {
StdRng::seed_from_u64(42)
}
fn generate_and_validate(schema: &Value) {
let mut rng = seeded_rng();
let result = generate(schema, &mut rng).expect("generation should succeed");
assert!(
jsonschema::is_valid(schema, &result),
"generated value does not validate against schema.\nschema: {schema}\nvalue: {result}"
);
}
fn generate_and_validate_n(schema: &Value, n: usize) {
let mut rng = seeded_rng();
for i in 0..n {
let result = generate(schema, &mut rng).expect("generation should succeed");
assert!(
jsonschema::is_valid(schema, &result),
"sample {i} does not validate.\nschema: {schema}\nvalue: {result}"
);
}
}
#[test]
fn test_null() {
let schema = json!({"type": "null"});
generate_and_validate(&schema);
}
#[test]
fn test_boolean() {
let schema = json!({"type": "boolean"});
generate_and_validate_n(&schema, 20);
}
#[test]
fn test_string_basic() {
let schema = json!({"type": "string"});
generate_and_validate(&schema);
}
#[test]
fn test_string_min_max_length() {
let schema = json!({"type": "string", "minLength": 5, "maxLength": 10});
let mut rng = seeded_rng();
for _ in 0..50 {
let result = generate(&schema, &mut rng).unwrap();
let s = result.as_str().unwrap();
assert!(
s.len() >= 5 && s.len() <= 10,
"string length {} out of range [5, 10]: {s}",
s.len()
);
}
generate_and_validate_n(&schema, 50);
}
#[test]
fn test_integer_basic() {
let schema = json!({"type": "integer"});
generate_and_validate(&schema);
}
#[test]
fn test_integer_min_max() {
let schema = json!({"type": "integer", "minimum": 10, "maximum": 20});
let mut rng = seeded_rng();
for _ in 0..50 {
let result = generate(&schema, &mut rng).unwrap();
let n = result.as_i64().unwrap();
assert!((10..=20).contains(&n), "integer {n} out of range [10, 20]");
}
generate_and_validate_n(&schema, 50);
}
#[test]
fn test_number_basic() {
let schema = json!({"type": "number"});
generate_and_validate(&schema);
}
#[test]
fn test_number_min_max() {
let schema = json!({"type": "number", "minimum": 1.5, "maximum": 3.5});
let mut rng = seeded_rng();
for _ in 0..50 {
let result = generate(&schema, &mut rng).unwrap();
let n = result.as_f64().unwrap();
assert!(
(1.5..=3.5).contains(&n),
"number {n} out of range [1.5, 3.5]"
);
}
generate_and_validate_n(&schema, 50);
}
#[test]
fn test_const() {
let schema = json!({"const": 42});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng).unwrap();
assert_eq!(result, json!(42));
generate_and_validate(&schema);
}
#[test]
fn test_const_string() {
let schema = json!({"const": "hello"});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng).unwrap();
assert_eq!(result, json!("hello"));
generate_and_validate(&schema);
}
#[test]
fn test_enum() {
let schema = json!({"enum": [1, "two", true, null]});
let variants: Vec<Value> = vec![json!(1), json!("two"), json!(true), json!(null)];
let mut rng = seeded_rng();
for _ in 0..100 {
let result = generate(&schema, &mut rng).unwrap();
assert!(
variants.contains(&result),
"enum result {result} not in variants"
);
}
generate_and_validate_n(&schema, 100);
}
#[test]
fn test_object_basic() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"optional_field": {"type": "boolean"}
},
"required": ["name", "age"]
});
let mut rng = seeded_rng();
for _ in 0..20 {
let result = generate(&schema, &mut rng).unwrap();
let obj = result.as_object().unwrap();
assert!(obj.contains_key("name"), "missing required field 'name'");
assert!(obj.contains_key("age"), "missing required field 'age'");
}
generate_and_validate_n(&schema, 20);
}
#[test]
fn test_object_all_required() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": "string"},
"b": {"type": "integer"},
"c": {"type": "boolean"}
},
"required": ["a", "b", "c"]
});
let mut rng = seeded_rng();
for _ in 0..20 {
let result = generate(&schema, &mut rng).unwrap();
let obj = result.as_object().unwrap();
assert!(obj.contains_key("a"));
assert!(obj.contains_key("b"));
assert!(obj.contains_key("c"));
}
generate_and_validate_n(&schema, 20);
}
#[test]
fn test_object_nested() {
let schema = json!({
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"required": ["address"]
});
generate_and_validate_n(&schema, 20);
}
#[test]
fn test_array_basic() {
let schema = json!({"type": "array", "items": {"type": "integer"}});
generate_and_validate(&schema);
}
#[test]
fn test_array_min_max_items() {
let schema = json!({
"type": "array",
"items": {"type": "integer"},
"minItems": 2,
"maxItems": 5
});
let mut rng = seeded_rng();
for _ in 0..50 {
let result = generate(&schema, &mut rng).unwrap();
let arr = result.as_array().unwrap();
assert!(
arr.len() >= 2 && arr.len() <= 5,
"array length {} out of range [2, 5]",
arr.len()
);
}
generate_and_validate_n(&schema, 50);
}
#[test]
fn test_array_of_objects() {
let schema = json!({
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"}
},
"required": ["id", "name"]
},
"minItems": 1,
"maxItems": 3
});
generate_and_validate_n(&schema, 20);
}
#[test]
fn test_any_of() {
let schema = json!({"anyOf": [{"type": "string"}, {"type": "integer"}]});
generate_and_validate_n(&schema, 50);
}
#[test]
fn test_any_of_all_variants_generated() {
let schema =
json!({"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "boolean"}]});
let mut rng = seeded_rng();
let mut saw_string = false;
let mut saw_integer = false;
let mut saw_boolean = false;
for _ in 0..200 {
let result = generate(&schema, &mut rng).unwrap();
match &result {
Value::String(_) => saw_string = true,
Value::Number(n) if n.is_i64() => saw_integer = true,
Value::Bool(_) => saw_boolean = true,
other => panic!("unexpected variant: {other}"),
}
}
assert!(saw_string, "anyOf never generated a string variant");
assert!(saw_integer, "anyOf never generated an integer variant");
assert!(saw_boolean, "anyOf never generated a boolean variant");
}
#[test]
fn test_enum_all_variants_generated() {
let schema = json!({"enum": [1, "two", true, null]});
let mut rng = seeded_rng();
let mut seen = std::collections::HashSet::new();
for _ in 0..200 {
let result = generate(&schema, &mut rng).unwrap();
seen.insert(result.to_string());
}
assert!(seen.contains("1"), "enum never generated 1");
assert!(seen.contains("\"two\""), "enum never generated \"two\"");
assert!(seen.contains("true"), "enum never generated true");
assert!(seen.contains("null"), "enum never generated null");
}
#[test]
fn test_one_of() {
let schema = json!({"oneOf": [{"type": "string"}, {"type": "integer"}]});
generate_and_validate_n(&schema, 50);
}
#[test]
fn test_all_of() {
let schema = json!({
"allOf": [
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
},
{
"type": "object",
"properties": {
"age": {"type": "integer"}
},
"required": ["age"]
}
]
});
let mut rng = seeded_rng();
for _ in 0..20 {
let result = generate(&schema, &mut rng).unwrap();
let obj = result.as_object().unwrap();
assert!(obj.contains_key("name"));
assert!(obj.contains_key("age"));
}
}
#[test]
fn test_ref_basic() {
let schema = json!({
"$defs": {
"Name": {"type": "string", "minLength": 1, "maxLength": 20}
},
"type": "object",
"properties": {
"name": {"$ref": "#/$defs/Name"}
},
"required": ["name"]
});
generate_and_validate_n(&schema, 20);
}
#[test]
fn test_ref_not_found() {
let schema = json!({
"type": "object",
"properties": {
"name": {"$ref": "#/$defs/NonExistent"}
},
"required": ["name"]
});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng);
assert!(result.is_err());
}
#[test]
fn test_ref_nested() {
let schema = json!({
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"type": "object",
"properties": {
"home": {"$ref": "#/$defs/Address"},
"work": {"$ref": "#/$defs/Address"}
},
"required": ["home"]
});
generate_and_validate_n(&schema, 20);
}
#[test]
fn test_ref_resolves_to_correct_type() {
let schema = json!({
"$defs": {
"PositiveInt": {"type": "integer", "minimum": 1, "maximum": 1000}
},
"type": "object",
"properties": {
"count": {"$ref": "#/$defs/PositiveInt"}
},
"required": ["count"]
});
let mut rng = seeded_rng();
for _ in 0..50 {
let result = generate(&schema, &mut rng).unwrap();
let count = result.get("count").expect("missing 'count'");
let n = count.as_i64().expect("count should be integer");
assert!(
(1..=1000).contains(&n),
"ref-resolved value {n} doesn't match PositiveInt constraints"
);
}
}
#[test]
fn test_ref_definitions_legacy() {
let schema = json!({
"definitions": {
"Color": {"type": "string", "enum": ["red", "green", "blue"]}
},
"type": "object",
"properties": {
"color": {"$ref": "#/definitions/Color"}
},
"required": ["color"]
});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng).unwrap();
let color = result.get("color").unwrap().as_str().unwrap();
assert!(
["red", "green", "blue"].contains(&color),
"unexpected color: {color}"
);
generate_and_validate_n(&schema, 20);
}
#[test]
fn test_ref_external_returns_error() {
let schema = json!({
"type": "object",
"properties": {
"item": {"$ref": "https://example.com/schemas/item.json"}
},
"required": ["item"]
});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng);
assert!(result.is_err(), "external $ref should return an error");
let err = result.unwrap_err();
assert!(
matches!(err, crate::Error::RefNotFound { .. }),
"expected RefNotFound, got: {err}"
);
}
#[test]
fn test_empty_object_schema() {
let schema = json!({"type": "object"});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng).unwrap();
assert!(result.is_object());
generate_and_validate(&schema);
}
#[test]
fn test_empty_array_schema() {
let schema = json!({"type": "array"});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng).unwrap();
assert!(result.is_array());
generate_and_validate(&schema);
}
#[test]
fn test_string_zero_length() {
let schema = json!({"type": "string", "minLength": 0, "maxLength": 0});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng).unwrap();
assert_eq!(result, json!(""));
generate_and_validate(&schema);
}
#[test]
fn test_integer_equal_min_max() {
let schema = json!({"type": "integer", "minimum": 5, "maximum": 5});
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng).unwrap();
assert_eq!(result, json!(5));
generate_and_validate(&schema);
}
#[test]
fn test_boolean_schema_true() {
let schema = json!(true);
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng);
assert!(result.is_ok());
}
#[test]
fn test_boolean_schema_false() {
let schema = json!(false);
let mut rng = seeded_rng();
let result = generate(&schema, &mut rng);
assert!(result.is_err());
}
#[test]
fn test_person_schema() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "maxLength": 50},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"},
"zip": {"type": "string", "minLength": 5, "maxLength": 10}
},
"required": ["street", "city"]
},
"tags": {
"type": "array",
"items": {"type": "string"},
"minItems": 0,
"maxItems": 5
}
},
"required": ["name", "age"]
});
generate_and_validate_n(&schema, 100);
}
#[test]
fn test_product_schema() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "maxLength": 100},
"price": {"type": "number", "minimum": 0.01, "maximum": 99999.99},
"categories": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 5
},
"inStock": {"type": "boolean"}
},
"required": ["name", "price", "categories", "inStock"]
});
generate_and_validate_n(&schema, 100);
}
#[test]
fn test_schema_with_refs() {
let schema = json!({
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string", "minLength": 1},
"city": {"type": "string", "minLength": 1},
"country": {"type": "string", "minLength": 1}
},
"required": ["street", "city", "country"]
},
"ContactInfo": {
"type": "object",
"properties": {
"email": {"type": "string"},
"phone": {"type": "string"}
},
"required": ["email"]
}
},
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "maxLength": 50},
"homeAddress": {"$ref": "#/$defs/Address"},
"workAddress": {"$ref": "#/$defs/Address"},
"contact": {"$ref": "#/$defs/ContactInfo"}
},
"required": ["name", "homeAddress", "contact"]
});
generate_and_validate_n(&schema, 100);
}
}