use crate::schema::{Column, Schema, TypeHint};
use crate::types::{DxArray, DxObject, DxTable, DxValue};
use proptest::prelude::*;
use proptest::strategy::ValueTree;
pub fn arb_key() -> impl Strategy<Value = String> {
"[a-z][a-z0-9]{0,4}".prop_map(|s| s)
}
pub fn arb_safe_string() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9_]{0,14}".prop_map(|s| s)
}
pub fn arb_dx_value_leaf() -> impl Strategy<Value = DxValue> {
prop_oneof![
Just(DxValue::Null),
proptest::bool::ANY.prop_map(DxValue::Bool),
(-10000i64..10000i64).prop_map(DxValue::Int),
(-1000.0f64..1000.0f64).prop_map(|f| DxValue::Float((f * 100.0).round() / 100.0)),
arb_safe_string().prop_map(DxValue::String),
]
}
pub fn arb_dx_array_leaf() -> impl Strategy<Value = DxArray> {
(
proptest::collection::vec(arb_dx_value_leaf(), 0..5),
proptest::bool::ANY,
)
.prop_map(|(values, is_stream)| DxArray { values, is_stream })
}
pub fn arb_dx_array() -> impl Strategy<Value = DxArray> {
(
proptest::collection::vec(arb_dx_value_shallow(), 0..5),
proptest::bool::ANY,
)
.prop_map(|(values, is_stream)| DxArray { values, is_stream })
}
pub fn arb_dx_object_leaf() -> impl Strategy<Value = DxObject> {
proptest::collection::vec((arb_key(), arb_dx_value_leaf()), 1..5).prop_map(|pairs| {
let mut obj = DxObject::with_capacity(pairs.len());
for (key, value) in pairs {
obj.insert(key, value);
}
obj
})
}
pub fn arb_dx_object() -> impl Strategy<Value = DxObject> {
proptest::collection::vec((arb_key(), arb_dx_value_shallow()), 1..5).prop_map(|pairs| {
let mut obj = DxObject::with_capacity(pairs.len());
for (key, value) in pairs {
obj.insert(key, value);
}
obj
})
}
pub fn arb_type_hint() -> impl Strategy<Value = TypeHint> {
prop_oneof![
Just(TypeHint::Int),
Just(TypeHint::String),
Just(TypeHint::Float),
Just(TypeHint::Bool),
Just(TypeHint::Auto),
]
}
pub fn arb_column() -> impl Strategy<Value = Column> {
(arb_key(), arb_type_hint()).prop_map(|(name, type_hint)| Column::new(name, type_hint))
}
pub fn arb_schema() -> impl Strategy<Value = Schema> {
(arb_key(), proptest::collection::vec(arb_column(), 1..4))
.prop_map(|(name, columns)| Schema::with_columns(name, columns))
}
fn arb_value_for_type_hint(hint: TypeHint) -> BoxedStrategy<DxValue> {
match hint {
TypeHint::Int | TypeHint::Base62 | TypeHint::AutoIncrement => {
(-10000i64..10000i64).prop_map(DxValue::Int).boxed()
}
TypeHint::Float => (-1000.0f64..1000.0f64)
.prop_map(|f| DxValue::Float((f * 100.0).round() / 100.0))
.boxed(),
TypeHint::Bool => proptest::bool::ANY.prop_map(DxValue::Bool).boxed(),
TypeHint::String | TypeHint::Auto => arb_safe_string().prop_map(DxValue::String).boxed(),
}
}
pub fn arb_dx_table() -> impl Strategy<Value = DxTable> {
arb_schema().prop_flat_map(|schema| {
let schema_clone = schema.clone();
let num_columns = schema.columns.len();
let row_strategies: Vec<BoxedStrategy<DxValue>> = schema
.columns
.iter()
.map(|col| arb_value_for_type_hint(col.type_hint))
.collect();
proptest::collection::vec(
row_strategies
.into_iter()
.collect::<Vec<_>>()
.prop_map(move |_| {
vec![DxValue::Null; num_columns]
}),
0..5,
)
.prop_flat_map(move |row_count_hint| {
let schema_inner = schema_clone.clone();
let num_rows = row_count_hint.len();
let row_value_strategies: Vec<BoxedStrategy<Vec<DxValue>>> = (0..num_rows)
.map(|_| {
let cols = schema_inner.columns.clone();
cols.into_iter()
.map(|col| arb_value_for_type_hint(col.type_hint))
.collect::<Vec<_>>()
.prop_map(|values| values)
.boxed()
})
.collect();
if row_value_strategies.is_empty() {
Just(DxTable::new(schema_inner)).boxed()
} else {
row_value_strategies
.prop_map(move |rows| {
let mut table = DxTable::new(schema_inner.clone());
for row in rows {
let _ = table.add_row(row);
}
table
})
.boxed()
}
})
})
}
pub fn arb_dx_value_shallow() -> impl Strategy<Value = DxValue> {
prop_oneof![
7 => arb_dx_value_leaf(),
2 => arb_dx_array_leaf().prop_map(DxValue::Array),
1 => arb_dx_object_leaf().prop_map(DxValue::Object),
]
}
pub fn arb_dx_value() -> impl Strategy<Value = DxValue> {
prop_oneof![
6 => arb_dx_value_leaf(),
2 => arb_dx_array().prop_map(DxValue::Array),
1 => arb_dx_object().prop_map(DxValue::Object),
1 => arb_dx_table().prop_map(DxValue::Table),
]
}
pub fn arb_dx_value_roundtrip() -> impl Strategy<Value = DxValue> {
arb_dx_value_leaf()
}
#[cfg(test)]
mod property_tests {
use super::*;
use crate::encoder::encode;
use crate::parser::parse;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_leaf_values_encode(value in arb_dx_value_leaf()) {
let mut obj = DxObject::new();
obj.insert("v".to_string(), value);
let wrapped = DxValue::Object(obj);
let result = encode(&wrapped);
prop_assert!(result.is_ok(), "Failed to encode leaf value: {:?}", result.err());
}
#[test]
fn prop_arrays_encode(arr in arb_dx_array()) {
let mut obj = DxObject::new();
obj.insert("arr".to_string(), DxValue::Array(arr));
let wrapped = DxValue::Object(obj);
let result = encode(&wrapped);
prop_assert!(result.is_ok(), "Failed to encode array: {:?}", result.err());
}
#[test]
fn prop_objects_encode(obj in arb_dx_object()) {
let wrapped = DxValue::Object(obj);
let result = encode(&wrapped);
prop_assert!(result.is_ok(), "Failed to encode object: {:?}", result.err());
}
#[test]
fn prop_tables_encode(table in arb_dx_table()) {
let mut obj = DxObject::new();
obj.insert("t".to_string(), DxValue::Table(table));
let wrapped = DxValue::Object(obj);
let result = encode(&wrapped);
prop_assert!(result.is_ok(), "Failed to encode table: {:?}", result.err());
}
#[test]
fn prop_all_values_encode(value in arb_dx_value()) {
let wrapped = match value {
DxValue::Object(_) => value,
other => {
let mut obj = DxObject::new();
obj.insert("v".to_string(), other);
DxValue::Object(obj)
}
};
let result = encode(&wrapped);
prop_assert!(result.is_ok(), "Failed to encode value: {:?}", result.err());
}
#[test]
fn prop_encode_then_parse(value in arb_dx_value_roundtrip()) {
let mut obj = DxObject::new();
obj.insert("v".to_string(), value);
let wrapped = DxValue::Object(obj);
let encoded = encode(&wrapped);
prop_assert!(encoded.is_ok(), "Failed to encode: {:?}", encoded.err());
let bytes = encoded.unwrap();
let parsed = parse(&bytes);
prop_assert!(parsed.is_ok(), "Failed to parse encoded value: {:?}\nEncoded: {:?}",
parsed.err(), String::from_utf8_lossy(&bytes));
}
}
#[test]
fn test_leaf_value_generation() {
use proptest::test_runner::TestRunner;
let mut runner = TestRunner::default();
for _ in 0..10 {
let value = arb_dx_value_leaf()
.new_tree(&mut runner)
.expect("Failed to generate")
.current();
match value {
DxValue::Null
| DxValue::Bool(_)
| DxValue::Int(_)
| DxValue::Float(_)
| DxValue::String(_) => {}
_ => panic!("Leaf generator produced non-leaf value: {:?}", value),
}
}
}
#[test]
fn test_object_generation() {
use proptest::test_runner::TestRunner;
let mut runner = TestRunner::default();
for _ in 0..10 {
let obj = arb_dx_object()
.new_tree(&mut runner)
.expect("Failed to generate")
.current();
assert!(!obj.fields.is_empty(), "Generated empty object");
for (key, _) in obj.iter() {
assert!(
key.chars()
.next()
.map(|c| c.is_ascii_lowercase())
.unwrap_or(false),
"Key doesn't start with lowercase letter: {}",
key
);
}
}
}
#[test]
fn test_table_generation() {
use proptest::test_runner::TestRunner;
let mut runner = TestRunner::default();
for _ in 0..10 {
let table = arb_dx_table()
.new_tree(&mut runner)
.expect("Failed to generate")
.current();
assert!(
!table.schema.columns.is_empty(),
"Generated table with empty schema"
);
for row in &table.rows {
assert_eq!(
row.len(),
table.schema.columns.len(),
"Row length {} doesn't match schema length {}",
row.len(),
table.schema.columns.len()
);
}
}
}
#[test]
fn test_simple_roundtrip() {
let mut obj = DxObject::new();
obj.insert("name".to_string(), DxValue::String("Test".to_string()));
obj.insert("count".to_string(), DxValue::Int(42));
obj.insert("active".to_string(), DxValue::Bool(true));
let value = DxValue::Object(obj);
let encoded = encode(&value).expect("Encode failed");
let parsed = parse(&encoded).expect("Parse failed");
if let DxValue::Object(parsed_obj) = parsed {
assert!(parsed_obj.get("name").is_some(), "name field missing");
assert!(parsed_obj.get("count").is_some(), "count field missing");
} else {
panic!("Expected object after round-trip");
}
}
}