use jtd::{Schema, Type};
use rand::seq::IteratorRandom;
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
const MAX_SEQ_LENGTH: u8 = 8;
const METADATA_KEY_FUZZ_HINT: &'static str = "fuzzHint";
pub fn fuzz<R: rand::Rng>(schema: &Schema, rng: &mut R) -> Value {
fuzz_with_root(schema, rng, schema)
}
fn fuzz_with_root<R: rand::Rng>(root: &Schema, rng: &mut R, schema: &Schema) -> Value {
match schema {
Schema::Empty { .. } => {
let range_max_value = if root as *const _ == schema as *const _ {
7
} else {
5
};
let val = rng.gen_range(0..range_max_value);
match val {
0 => Value::Null,
1 => rng.gen::<bool>().into(),
2 => rng.gen::<u8>().into(),
3 => rng.gen::<f64>().into(),
4 => fuzz_string(rng).into(),
5 => {
let schema = Schema::Elements {
metadata: Default::default(),
definitions: Default::default(),
nullable: false,
elements: Box::new(Schema::Empty {
metadata: Default::default(),
definitions: Default::default(),
}),
};
fuzz(&schema, rng)
}
6 => {
let schema = Schema::Values {
metadata: Default::default(),
definitions: Default::default(),
nullable: false,
values: Box::new(Schema::Empty {
metadata: Default::default(),
definitions: Default::default(),
}),
};
fuzz(&schema, rng)
}
_ => unreachable!(),
}
}
Schema::Ref {
ref ref_, nullable, ..
} => {
if *nullable && rng.gen() {
return Value::Null;
}
fuzz_with_root(root, rng, &root.definitions()[ref_])
}
Schema::Type {
ref metadata,
ref type_,
nullable,
..
} => {
if *nullable && rng.gen() {
return Value::Null;
}
match type_ {
Type::Boolean => rng.gen::<bool>().into(),
Type::Float32 => rng.gen::<f32>().into(),
Type::Float64 => rng.gen::<f64>().into(),
Type::Int8 => rng.gen::<i8>().into(),
Type::Uint8 => rng.gen::<u8>().into(),
Type::Int16 => rng.gen::<i16>().into(),
Type::Uint16 => rng.gen::<u16>().into(),
Type::Int32 => rng.gen::<i32>().into(),
Type::Uint32 => rng.gen::<u32>().into(),
Type::String => {
match metadata.get(METADATA_KEY_FUZZ_HINT).and_then(Value::as_str) {
Some("en_us/addresses/address") => rng
.gen::<faker_rand::en_us::addresses::Address>()
.to_string()
.into(),
Some("en_us/addresses/city_name") => rng
.gen::<faker_rand::en_us::addresses::CityName>()
.to_string()
.into(),
Some("en_us/addresses/division") => rng
.gen::<faker_rand::en_us::addresses::Division>()
.to_string()
.into(),
Some("en_us/addresses/division_abbreviation") => rng
.gen::<faker_rand::en_us::addresses::DivisionAbbreviation>()
.to_string()
.into(),
Some("en_us/addresses/postal_code") => rng
.gen::<faker_rand::en_us::addresses::PostalCode>()
.to_string()
.into(),
Some("en_us/addresses/secondary_address") => rng
.gen::<faker_rand::en_us::addresses::SecondaryAddress>()
.to_string()
.into(),
Some("en_us/addresses/street_address") => rng
.gen::<faker_rand::en_us::addresses::StreetAddress>()
.to_string()
.into(),
Some("en_us/addresses/street_name") => rng
.gen::<faker_rand::en_us::addresses::StreetName>()
.to_string()
.into(),
Some("en_us/company/company_name") => rng
.gen::<faker_rand::en_us::company::CompanyName>()
.to_string()
.into(),
Some("en_us/company/slogan") => rng
.gen::<faker_rand::en_us::company::Slogan>()
.to_string()
.into(),
Some("en_us/internet/domain") => rng
.gen::<faker_rand::en_us::internet::Domain>()
.to_string()
.into(),
Some("en_us/internet/email") => rng
.gen::<faker_rand::en_us::internet::Email>()
.to_string()
.into(),
Some("en_us/internet/username") => rng
.gen::<faker_rand::en_us::internet::Username>()
.to_string()
.into(),
Some("en_us/names/first_name") => rng
.gen::<faker_rand::en_us::names::FirstName>()
.to_string()
.into(),
Some("en_us/names/full_name") => rng
.gen::<faker_rand::en_us::names::FullName>()
.to_string()
.into(),
Some("en_us/names/last_name") => rng
.gen::<faker_rand::en_us::names::LastName>()
.to_string()
.into(),
Some("en_us/names/name_prefix") => rng
.gen::<faker_rand::en_us::names::NamePrefix>()
.to_string()
.into(),
Some("en_us/names/name_suffix") => rng
.gen::<faker_rand::en_us::names::NameSuffix>()
.to_string()
.into(),
Some("en_us/phones/phone_number") => rng
.gen::<faker_rand::en_us::phones::PhoneNumber>()
.to_string()
.into(),
Some("fr_fr/addresses/address") => rng
.gen::<faker_rand::fr_fr::addresses::Address>()
.to_string()
.into(),
Some("fr_fr/addresses/city_name") => rng
.gen::<faker_rand::fr_fr::addresses::CityName>()
.to_string()
.into(),
Some("fr_fr/addresses/division") => rng
.gen::<faker_rand::fr_fr::addresses::Division>()
.to_string()
.into(),
Some("fr_fr/addresses/postal_code") => rng
.gen::<faker_rand::fr_fr::addresses::PostalCode>()
.to_string()
.into(),
Some("fr_fr/addresses/secondary_address") => rng
.gen::<faker_rand::fr_fr::addresses::SecondaryAddress>()
.to_string()
.into(),
Some("fr_fr/addresses/street_address") => rng
.gen::<faker_rand::fr_fr::addresses::StreetAddress>()
.to_string()
.into(),
Some("fr_fr/addresses/street_name") => rng
.gen::<faker_rand::fr_fr::addresses::StreetName>()
.to_string()
.into(),
Some("fr_fr/company/company_name") => rng
.gen::<faker_rand::fr_fr::company::CompanyName>()
.to_string()
.into(),
Some("fr_fr/internet/domain") => rng
.gen::<faker_rand::fr_fr::internet::Domain>()
.to_string()
.into(),
Some("fr_fr/internet/email") => rng
.gen::<faker_rand::fr_fr::internet::Email>()
.to_string()
.into(),
Some("fr_fr/internet/username") => rng
.gen::<faker_rand::fr_fr::internet::Username>()
.to_string()
.into(),
Some("fr_fr/names/first_name") => rng
.gen::<faker_rand::fr_fr::names::FirstName>()
.to_string()
.into(),
Some("fr_fr/names/full_name") => rng
.gen::<faker_rand::fr_fr::names::FullName>()
.to_string()
.into(),
Some("fr_fr/names/last_name") => rng
.gen::<faker_rand::fr_fr::names::LastName>()
.to_string()
.into(),
Some("fr_fr/names/name_prefix") => rng
.gen::<faker_rand::fr_fr::names::NamePrefix>()
.to_string()
.into(),
Some("fr_fr/phones/phone_number") => rng
.gen::<faker_rand::fr_fr::phones::PhoneNumber>()
.to_string()
.into(),
Some("lorem/word") => {
rng.gen::<faker_rand::lorem::Word>().to_string().into()
}
Some("lorem/sentence") => {
rng.gen::<faker_rand::lorem::Sentence>().to_string().into()
}
Some("lorem/paragraph") => {
rng.gen::<faker_rand::lorem::Paragraph>().to_string().into()
}
Some("lorem/paragraphs") => rng
.gen::<faker_rand::lorem::Paragraphs>()
.to_string()
.into(),
_ => fuzz_string(rng).into(),
}
}
Type::Timestamp => {
use chrono::TimeZone;
let max_offset = 14 * 60 * 60;
chrono::FixedOffset::east(rng.gen_range(-max_offset..=max_offset))
.timestamp(rng.gen::<i32>() as i64, 0)
.to_rfc3339()
.into()
}
}
}
Schema::Enum {
ref enum_,
nullable,
..
} => {
if *nullable && rng.gen() {
return Value::Null;
}
enum_.iter().choose(rng).unwrap().clone().into()
}
Schema::Elements {
ref elements,
nullable,
..
} => {
if *nullable && rng.gen() {
return Value::Null;
}
(0..rng.gen_range(0..MAX_SEQ_LENGTH))
.map(|_| fuzz_with_root(root, rng, elements))
.collect::<Vec<_>>()
.into()
}
Schema::Properties {
ref properties,
ref optional_properties,
additional_properties,
nullable,
..
} => {
if *nullable && rng.gen() {
return Value::Null;
}
let mut members = BTreeMap::new();
let mut required_keys: Vec<_> = properties.keys().cloned().collect();
required_keys.sort();
for k in required_keys {
let v = fuzz_with_root(root, rng, &properties[&k]);
members.insert(k, v);
}
let mut optional_keys: Vec<_> = optional_properties.keys().cloned().collect();
optional_keys.sort();
for k in optional_keys {
if rng.gen() {
continue;
}
let v = fuzz_with_root(root, rng, &optional_properties[&k]);
members.insert(k, v);
}
if *additional_properties {
let defined_properties_lowercase: BTreeSet<_> = properties
.keys()
.chain(optional_properties.keys())
.map(|s| s.to_lowercase())
.collect();
for _ in 0..rng.gen_range(0..MAX_SEQ_LENGTH) {
let key = fuzz_string(rng);
if !defined_properties_lowercase.contains(&key.to_lowercase()) {
members.insert(
key,
fuzz(
&Schema::Empty {
metadata: Default::default(),
definitions: Default::default(),
},
rng,
),
);
}
}
}
members
.into_iter()
.collect::<serde_json::Map<String, Value>>()
.into()
}
Schema::Values {
ref values,
nullable,
..
} => {
if *nullable && rng.gen() {
return Value::Null;
}
(0..rng.gen_range(0..MAX_SEQ_LENGTH))
.map(|_| (fuzz_string(rng), fuzz_with_root(root, rng, values)))
.collect::<serde_json::Map<String, Value>>()
.into()
}
Schema::Discriminator {
ref mapping,
ref discriminator,
nullable,
..
} => {
if *nullable && rng.gen() {
return Value::Null;
}
let (discriminator_value, sub_schema) = mapping.iter().choose(rng).unwrap();
let mut obj = fuzz_with_root(root, rng, sub_schema);
obj.as_object_mut().unwrap().insert(
discriminator.to_owned(),
discriminator_value.to_owned().into(),
);
obj
}
}
}
fn fuzz_string<R: rand::Rng>(rng: &mut R) -> String {
(0..rng.gen_range(0..MAX_SEQ_LENGTH))
.map(|_| rng.gen_range(32u8..=127u8) as char)
.collect::<String>()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_fuzz_empty() {
assert_valid_fuzz(json!({}));
}
#[test]
fn test_fuzz_ref() {
assert_valid_fuzz(json!({
"definitions": {
"a": { "type": "timestamp" },
"b": { "type": "timestamp", "nullable": true },
"c": { "ref": "b" },
},
"properties": {
"a": { "ref": "a" },
"b": { "ref": "b" },
"c": { "ref": "c" },
}
}));
}
#[test]
fn test_fuzz_type() {
assert_valid_fuzz(json!({ "type": "boolean" }));
assert_valid_fuzz(json!({ "type": "boolean", "nullable": true }));
assert_valid_fuzz(json!({ "type": "float32" }));
assert_valid_fuzz(json!({ "type": "float32", "nullable": true }));
assert_valid_fuzz(json!({ "type": "float64" }));
assert_valid_fuzz(json!({ "type": "float64", "nullable": true }));
assert_valid_fuzz(json!({ "type": "int8" }));
assert_valid_fuzz(json!({ "type": "int8", "nullable": true }));
assert_valid_fuzz(json!({ "type": "uint8" }));
assert_valid_fuzz(json!({ "type": "uint8", "nullable": true }));
assert_valid_fuzz(json!({ "type": "uint16" }));
assert_valid_fuzz(json!({ "type": "uint16", "nullable": true }));
assert_valid_fuzz(json!({ "type": "uint32" }));
assert_valid_fuzz(json!({ "type": "uint32", "nullable": true }));
assert_valid_fuzz(json!({ "type": "string" }));
assert_valid_fuzz(json!({ "type": "string", "nullable": true }));
assert_valid_fuzz(json!({ "type": "timestamp" }));
assert_valid_fuzz(json!({ "type": "timestamp", "nullable": true }));
}
#[test]
fn test_fuzz_enum() {
assert_valid_fuzz(json!({ "enum": ["a", "b", "c" ]}));
assert_valid_fuzz(json!({ "enum": ["a", "b", "c" ], "nullable": true }));
}
#[test]
fn test_fuzz_elements() {
assert_valid_fuzz(json!({ "elements": { "type": "uint8" }}));
assert_valid_fuzz(json!({ "elements": { "type": "uint8" }, "nullable": true }));
}
#[test]
fn test_fuzz_properties() {
assert_valid_fuzz(json!({
"properties": {
"a": { "type": "uint8" },
"b": { "type": "string" },
},
"optionalProperties": {
"c": { "type": "uint32" },
"d": { "type": "timestamp" },
},
"additionalProperties": true,
"nullable": true,
}));
}
#[test]
fn test_fuzz_values() {
assert_valid_fuzz(json!({ "values": { "type": "uint8" }}));
assert_valid_fuzz(json!({ "values": { "type": "uint8" }, "nullable": true }));
}
#[test]
fn test_fuzz_discriminator() {
assert_valid_fuzz(json!({
"discriminator": "version",
"mapping": {
"v1": {
"properties": {
"foo": { "type": "string" },
"bar": { "type": "timestamp" }
}
},
"v2": {
"properties": {
"foo": { "type": "uint8" },
"bar": { "type": "float32" }
}
}
},
"nullable": true,
}));
}
fn assert_valid_fuzz(schema: Value) {
use rand::SeedableRng;
let mut rng = rand_pcg::Pcg32::seed_from_u64(8927);
let schema = Schema::from_serde_schema(serde_json::from_value(schema).unwrap()).unwrap();
for _ in 0..1000 {
let instance = super::fuzz(&schema, &mut rng);
let errors = jtd::validate(&schema, &instance, Default::default()).unwrap();
assert!(errors.is_empty(), "{}", instance);
}
}
}