use std::collections::BTreeMap;
use std::rc::Rc;
use proptest::prelude::*;
use serde_json::{Map, Value, json};
const MAX_DEPTH: usize = 4;
pub type Defs = BTreeMap<String, Value>;
pub fn defs_of(schema: &Value) -> Defs {
schema
.get("$defs")
.or_else(|| schema.get("definitions"))
.and_then(Value::as_object)
.map(|members| {
members
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
})
.unwrap_or_default()
}
pub fn value_for_def(name: &str, defs: &Defs) -> Option<BoxedStrategy<Value>> {
let schema = defs.get(name)?.clone();
Some(value_at(&schema, defs, 0, &mut Vec::new()))
}
fn any_value() -> BoxedStrategy<Value> {
let leaf = prop_oneof![
6 => prop::sample::select(vec!["", "a", "hello", "x-unknown"])
.prop_map(|s| Value::String(s.to_owned())),
3 => any::<i64>().prop_map(|n| json!(n)),
3 => any::<bool>().prop_map(Value::Bool),
1 => Just(Value::Null),
];
leaf.prop_recursive(2, 8, 3, move |inner| {
prop_oneof![
6 => inner.clone(),
1 => proptest::collection::vec(inner.clone(), 0..3).prop_map(Value::Array),
1 => proptest::collection::hash_map("[a-zA-Z]{1,8}", inner, 0..3)
.prop_map(|members| Value::Object(members.into_iter().collect())),
]
})
.boxed()
}
fn value_at(
schema: &Value,
defs: &Defs,
depth: usize,
visiting: &mut Vec<String>,
) -> BoxedStrategy<Value> {
let Some(map) = schema.as_object() else {
return any_value();
};
if let Some(value) = map.get("const") {
return Just(value.clone()).boxed();
}
if let Some(Value::Array(values)) = map.get("enum") {
return prop::sample::select(values.clone()).boxed();
}
for keyword in ["anyOf", "oneOf"] {
let Some(Value::Array(branches)) = map.get(keyword) else {
continue;
};
if branches.is_empty() {
continue;
}
if depth >= MAX_DEPTH {
let Some(first) = branches.first() else {
return any_value();
};
return value_at(first, defs, depth, visiting);
}
let built: Vec<BoxedStrategy<Value>> = branches
.iter()
.map(|branch| value_at(branch, defs, depth + 1, visiting))
.collect();
let count = built.len();
return (0..count)
.prop_flat_map(move |i| built.get(i).map_or_else(any_value, Clone::clone))
.boxed();
}
if let Some(Value::Array(branches)) = map.get("allOf")
&& !branches.is_empty()
{
return fold_over(
branches
.iter()
.map(|branch| value_at(branch, defs, depth + 1, visiting)),
);
}
if let Some(reference) = map.get("$ref").and_then(Value::as_str) {
return resolve(reference, defs, depth, visiting);
}
match chosen_type(map) {
Some("array") => array_value(schema, defs, depth, visiting),
Some("string") => {
let min = bound(map.get("minLength"), 64);
map.get("format")
.and_then(Value::as_str)
.map_or_else(|| string_value(min), |format| formatted_string(format, min))
}
Some("integer") => integer_value(map),
Some("number") => number_value(map),
Some("boolean") => any::<bool>().prop_map(Value::Bool).boxed(),
Some("null") => Just(Value::Null).boxed(),
Some("object") | None => object_value(schema, defs, depth, visiting),
Some(_) => any_value(),
}
}
fn chosen_type(map: &Map<String, Value>) -> Option<&str> {
match map.get("type") {
Some(Value::String(name)) => Some(name),
Some(Value::Array(names)) => names.first().and_then(Value::as_str),
_ => None,
}
}
fn resolve(
reference: &str,
defs: &Defs,
depth: usize,
visiting: &mut Vec<String>,
) -> BoxedStrategy<Value> {
let Some(name) = reference.rsplit('/').next() else {
return any_value();
};
if visiting.iter().any(|seen| seen == name) {
return Just(Value::Object(Map::new())).boxed();
}
let Some(target) = defs.get(name) else {
return any_value();
};
visiting.push(name.to_owned());
let strategy = value_at(target, defs, depth, visiting);
visiting.pop();
strategy
}
fn fold_over<I>(parts: I) -> BoxedStrategy<Value>
where
I: IntoIterator<Item = BoxedStrategy<Value>>,
{
parts.into_iter().fold(
Just(Value::Object(Map::new())).boxed(),
|acc: BoxedStrategy<Value>, part: BoxedStrategy<Value>| {
let part = Rc::new(part);
acc.prop_flat_map(move |accumulated: Value| {
let part = Rc::clone(&part);
part.prop_map(move |part| merge(accumulated.clone(), part))
})
.boxed()
},
)
}
fn merge(base: Value, incoming: Value) -> Value {
match (base, incoming) {
(Value::Object(mut base), Value::Object(other)) => {
for (key, value) in other {
let merged = match base.remove(&key) {
Some(existing) => merge(existing, value),
None => value,
};
base.insert(key, merged);
}
Value::Object(base)
}
(_, incoming) => incoming,
}
}
fn object_value(
schema: &Value,
defs: &Defs,
depth: usize,
visiting: &mut Vec<String>,
) -> BoxedStrategy<Value> {
let Some(map) = schema.as_object() else {
return any_value();
};
let required: Vec<String> = map
.get("required")
.and_then(Value::as_array)
.map(|names| {
names
.iter()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect()
})
.unwrap_or_default();
let required_members: Vec<(String, BoxedStrategy<Value>)> = required
.iter()
.filter_map(|name| {
let member = map.get("properties")?.get(name)?;
Some((name.clone(), value_at(member, defs, depth + 1, visiting)))
})
.collect();
let required_part = draw_all(required_members);
let optional_members: Vec<(String, BoxedStrategy<Value>)> = map
.get("properties")
.and_then(Value::as_object)
.map(|all| {
all.iter()
.filter(|(name, _)| !required.contains(name))
.map(|(name, member)| (name.clone(), value_at(member, defs, depth + 1, visiting)))
.collect()
})
.unwrap_or_default();
let optional_count = optional_members.len();
let optional_part = (0..=optional_count)
.prop_flat_map(move |keep| {
draw_all(optional_members.clone())
.prop_map(move |drawn| drawn.into_iter().take(keep).collect::<Map<String, Value>>())
})
.boxed();
let extra_part = if depth >= MAX_DEPTH {
Just(None).boxed()
} else {
unknown_member(map, defs, depth, visiting)
};
(required_part, optional_part, extra_part)
.prop_map(|(mut members, optional, extra)| {
for (name, value) in optional {
members.insert(name, value);
}
if let Some((name, value)) = extra {
members.insert(name, value);
}
Value::Object(members)
})
.boxed()
}
fn draw_all(members: Vec<(String, BoxedStrategy<Value>)>) -> BoxedStrategy<Map<String, Value>> {
let names: Vec<String> = members.iter().map(|(name, _)| name.clone()).collect();
let strategies: Vec<BoxedStrategy<Value>> = members.into_iter().map(|(_, s)| s).collect();
strategies
.prop_map(move |drawn| {
names
.iter()
.cloned()
.zip(drawn)
.collect::<Map<String, Value>>()
})
.boxed()
}
fn unknown_member(
map: &Map<String, Value>,
defs: &Defs,
depth: usize,
visiting: &mut Vec<String>,
) -> BoxedStrategy<Option<(String, Value)>> {
match map.get("additionalProperties") {
Some(Value::Bool(false)) => Just(None).boxed(),
Some(scheme @ Value::Object(_)) => {
let value = value_at(scheme, defs, depth + 1, visiting);
prop_oneof![
3 => Just(None),
1 => (Just("x-unknown-member".to_owned()), value)
.prop_map(|(name, value)| Some((name, value))),
]
.boxed()
}
_ => prop_oneof![
3 => Just(None),
1 => (Just("x-unknown-member".to_owned()), any_value())
.prop_map(|(name, value)| Some((name, value))),
]
.boxed(),
}
}
fn array_value(
schema: &Value,
defs: &Defs,
depth: usize,
visiting: &mut Vec<String>,
) -> BoxedStrategy<Value> {
let Some(map) = schema.as_object() else {
return any_value();
};
let min = bound(map.get("minItems"), 4);
let max = map
.get("maxItems")
.map_or_else(|| min.max(2), |m| bound(Some(m), 4))
.clamp(min, 4);
let items = map.get("items").map_or_else(any_value, |member| {
value_at(member, defs, depth + 1, visiting)
});
proptest::collection::vec(items, min..=max)
.prop_map(Value::Array)
.boxed()
}
fn string_value(min: usize) -> BoxedStrategy<Value> {
const POOL: &[&str] = &[
"",
"a",
"hello",
"tool-name",
"file:///tmp/a.txt",
"text/plain",
"2026-10-02T00:00:00Z",
"client-1",
];
(0..64u32)
.prop_map(move |counter| {
let base = POOL.get((counter as usize) % POOL.len()).map_or("", |b| *b);
let mut text = format!("{base}{counter}");
while text.len() < min {
text.push('x');
}
Value::String(text)
})
.boxed()
}
fn formatted_string(format: &str, min: usize) -> BoxedStrategy<Value> {
let base: &'static str = match format {
"uri" => "https://example.test/resource",
"uri-template" => "https://example.test/resource/{id}",
"byte" => "aGVsbG8gd29ybGQ=",
_ => return string_value(min),
};
(0..32u32)
.prop_map(move |counter| {
let mut text = format!("{base}/{counter}");
while text.len() < min {
text.push('x');
}
Value::String(text)
})
.boxed()
}
fn integer_value(map: &Map<String, Value>) -> BoxedStrategy<Value> {
let min = bound_i64(map.get("minimum"), -1000).min(0);
let max = bound_i64(map.get("maximum"), 1000).max(0);
(min..=max.max(min)).prop_map(|n| json!(n)).boxed()
}
fn bound_i64(keyword: Option<&Value>, fallback: i64) -> i64 {
keyword
.and_then(Value::as_i64)
.or_else(|| {
keyword
.and_then(Value::as_u64)
.and_then(|v| i64::try_from(v).ok())
})
.unwrap_or(fallback)
}
fn bound(keyword: Option<&Value>, cap: usize) -> usize {
usize::try_from(bound_i64(keyword, 0).max(0)).map_or(cap, |value| value.min(cap))
}
fn number_value(map: &Map<String, Value>) -> BoxedStrategy<Value> {
let min = map.get("minimum").and_then(Value::as_f64).unwrap_or(-100.0);
let max = map.get("maximum").and_then(Value::as_f64).unwrap_or(100.0);
let (low, high) = (min.min(max), max.max(min));
(-100_000i32..=100_000)
.prop_map(move |n| json!((f64::from(n) / 1000.0).clamp(low, high)))
.boxed()
}