jspg progress
This commit is contained in:
406
src/util.rs
Normal file
406
src/util.rs
Normal file
@ -0,0 +1,406 @@
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TestSuite {
|
||||
#[allow(dead_code)]
|
||||
description: String,
|
||||
schema: Option<serde_json::Value>,
|
||||
// Support JSPG-style test suites with explicit types/enums/puncs
|
||||
types: Option<serde_json::Value>,
|
||||
enums: Option<serde_json::Value>,
|
||||
puncs: Option<serde_json::Value>,
|
||||
tests: Vec<TestCase>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TestCase {
|
||||
description: String,
|
||||
data: serde_json::Value,
|
||||
valid: bool,
|
||||
// Support explicit schema ID target for test case
|
||||
schema_id: Option<String>,
|
||||
// Expected output for masking tests
|
||||
#[allow(dead_code)]
|
||||
expected: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
use crate::registry::REGISTRY;
|
||||
use crate::validator::Validator;
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let v = Value::deserialize(deserializer)?;
|
||||
Ok(Some(v))
|
||||
}
|
||||
|
||||
pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
|
||||
// Clear registry to ensure isolation
|
||||
{
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
registry.clear();
|
||||
}
|
||||
|
||||
let content =
|
||||
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
|
||||
let suite: Vec<TestSuite> = serde_json::from_str(&content)
|
||||
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
|
||||
|
||||
if index >= suite.len() {
|
||||
panic!("Index {} out of bounds for file {}", index, path);
|
||||
}
|
||||
|
||||
let group = &suite[index];
|
||||
let mut failures = Vec::<String>::new();
|
||||
|
||||
// Helper to register items with 'schemas'
|
||||
let register_schemas = |items_val: Option<&Value>| {
|
||||
if let Some(val) = items_val {
|
||||
if let Value::Array(arr) = val {
|
||||
for item in arr {
|
||||
if let Some(schemas_val) = item.get("schemas") {
|
||||
if let Value::Array(schemas) = schemas_val {
|
||||
for schema_val in schemas {
|
||||
if let Ok(schema) =
|
||||
serde_json::from_value::<crate::schema::Schema>(schema_val.clone())
|
||||
{
|
||||
// Clone ID upfront to avoid borrow issues
|
||||
if let Some(id_clone) = schema.obj.id.clone() {
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
// Utilize the new compile method which handles strictness
|
||||
let compiled =
|
||||
crate::compiler::Compiler::compile(schema, Some(id_clone.clone()));
|
||||
registry.insert(id_clone, compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Register Family Schemas if 'types' is present
|
||||
if let Some(types_val) = &group.types {
|
||||
if let Value::Array(arr) = types_val {
|
||||
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for item in arr {
|
||||
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
|
||||
if let Some(hierarchy) = item.get("hierarchy").and_then(|v| v.as_array()) {
|
||||
for ancestor in hierarchy {
|
||||
if let Some(anc_str) = ancestor.as_str() {
|
||||
family_map
|
||||
.entry(anc_str.to_string())
|
||||
.or_default()
|
||||
.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (family_name, members) in family_map {
|
||||
let id = format!("{}.family", family_name);
|
||||
let object_refs: Vec<Value> = members
|
||||
.iter()
|
||||
.map(|s| serde_json::json!({ "$ref": s }))
|
||||
.collect();
|
||||
|
||||
let schema_json = serde_json::json!({
|
||||
"$id": id,
|
||||
"oneOf": object_refs
|
||||
});
|
||||
|
||||
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_json) {
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
let compiled = crate::compiler::Compiler::compile(schema, Some(id.clone()));
|
||||
registry.insert(id, compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Register items directly
|
||||
register_schemas(group.enums.as_ref());
|
||||
register_schemas(group.types.as_ref());
|
||||
register_schemas(group.puncs.as_ref());
|
||||
|
||||
// 3. Register root 'schemas' if present (generic test support)
|
||||
// Some tests use a raw 'schema' or 'schemas' field at the group level
|
||||
if let Some(schema_val) = &group.schema {
|
||||
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_val.clone()) {
|
||||
let id = schema
|
||||
.obj
|
||||
.id
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
// Fallback ID if none provided in schema
|
||||
Some("root".to_string())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
let compiled = crate::compiler::Compiler::compile(schema, Some(id.clone()));
|
||||
registry.insert(id, compiled);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Run Tests
|
||||
for (test_index, test) in group.tests.iter().enumerate() {
|
||||
let mut schema_id = test.schema_id.clone();
|
||||
|
||||
// If no explicit schema_id, try to infer from the single schema in the group
|
||||
if schema_id.is_none() {
|
||||
if let Some(s) = &group.schema {
|
||||
// If 'schema' is a single object, use its ID or "root"
|
||||
if let Some(obj) = s.as_object() {
|
||||
if let Some(id_val) = obj.get("$id") {
|
||||
schema_id = id_val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
}
|
||||
if schema_id.is_none() {
|
||||
schema_id = Some("root".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to the first punc if present (for puncs.json style)
|
||||
if schema_id.is_none() {
|
||||
if let Some(Value::Array(puncs)) = &group.puncs {
|
||||
if let Some(first_punc) = puncs.first() {
|
||||
if let Some(Value::Array(schemas)) = first_punc.get("schemas") {
|
||||
if let Some(first_schema) = schemas.first() {
|
||||
if let Some(id) = first_schema.get("$id").and_then(|v| v.as_str()) {
|
||||
schema_id = Some(id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sid) = schema_id {
|
||||
let result = Validator::validate(&sid, &test.data);
|
||||
|
||||
if !result.errors.is_empty() != !test.valid {
|
||||
failures.push(format!(
|
||||
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {:?}",
|
||||
group.description,
|
||||
test.description,
|
||||
test.valid,
|
||||
!result.errors.is_empty(),
|
||||
result.errors
|
||||
));
|
||||
}
|
||||
} else {
|
||||
failures.push(format!(
|
||||
"[{}] Test '{}' skipped: No schema ID found.",
|
||||
group.description, test.description
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
return Err(failures.join("\n"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn run_test_file(path: &str) -> Result<(), String> {
|
||||
let content =
|
||||
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
|
||||
let suite: Vec<TestSuite> = serde_json::from_str(&content)
|
||||
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
|
||||
|
||||
let mut failures = Vec::<String>::new();
|
||||
for (group_index, group) in suite.into_iter().enumerate() {
|
||||
// Helper to register items with 'schemas'
|
||||
let register_schemas = |items_val: Option<Value>| {
|
||||
if let Some(val) = items_val {
|
||||
if let Value::Array(arr) = val {
|
||||
for item in arr {
|
||||
if let Some(schemas_val) = item.get("schemas") {
|
||||
if let Value::Array(schemas) = schemas_val {
|
||||
for schema_val in schemas {
|
||||
if let Ok(schema) =
|
||||
serde_json::from_value::<crate::schema::Schema>(schema_val.clone())
|
||||
{
|
||||
// Clone ID upfront to avoid borrow issues
|
||||
if let Some(id_clone) = schema.obj.id.clone() {
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
// Utilize the new compile method which handles strictness
|
||||
let compiled =
|
||||
crate::compiler::Compiler::compile(schema, Some(id_clone.clone()));
|
||||
registry.insert(id_clone, compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Register Family Schemas if 'types' is present
|
||||
if let Some(types_val) = &group.types {
|
||||
if let Value::Array(arr) = types_val {
|
||||
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for item in arr {
|
||||
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
|
||||
// Default hierarchy contains self if not specified?
|
||||
// Usually hierarchy is explicit in these tests.
|
||||
if let Some(hierarchy) = item.get("hierarchy").and_then(|v| v.as_array()) {
|
||||
for ancestor in hierarchy {
|
||||
if let Some(anc_str) = ancestor.as_str() {
|
||||
family_map
|
||||
.entry(anc_str.to_string())
|
||||
.or_default()
|
||||
.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (family_name, members) in family_map {
|
||||
let id = format!("{}.family", family_name);
|
||||
let object_refs: Vec<Value> = members
|
||||
.into_iter()
|
||||
.map(|s| serde_json::json!({ "$ref": s }))
|
||||
.collect();
|
||||
|
||||
let schema_json = serde_json::json!({
|
||||
"$id": id,
|
||||
"oneOf": object_refs
|
||||
});
|
||||
|
||||
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_json) {
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
let compiled = crate::compiler::Compiler::compile(schema, Some(id.clone()));
|
||||
registry.insert(id, compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register 'types', 'enums', and 'puncs' if present (JSPG style)
|
||||
register_schemas(group.types);
|
||||
register_schemas(group.enums);
|
||||
register_schemas(group.puncs);
|
||||
|
||||
// Register main 'schema' if present (Standard style)
|
||||
// Ensure ID is a valid URI to avoid Url::parse errors in Compiler
|
||||
let unique_id = format!("test:{}:{}", path, group_index);
|
||||
|
||||
// Register main 'schema' if present (Standard style)
|
||||
if let Some(ref schema_val) = group.schema {
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
let schema: crate::schema::Schema =
|
||||
serde_json::from_value(schema_val.clone()).expect("Failed to parse test schema");
|
||||
let compiled = crate::compiler::Compiler::compile(schema, Some(unique_id.clone()));
|
||||
registry.insert(unique_id.clone(), compiled);
|
||||
}
|
||||
|
||||
for test in group.tests {
|
||||
// Use explicit schema_id from test, or default to unique_id
|
||||
let schema_id = test.schema_id.as_deref().unwrap_or(&unique_id).to_string();
|
||||
|
||||
let drop = Validator::validate(&schema_id, &test.data);
|
||||
|
||||
if test.valid {
|
||||
if !drop.errors.is_empty() {
|
||||
let msg = format!(
|
||||
"Test failed (expected valid): {}\nSchema: {:?}\nData: {:?}\nErrors: {:?}",
|
||||
test.description,
|
||||
group.schema, // We might need to find the actual schema used if schema_id is custom
|
||||
test.data,
|
||||
drop.errors
|
||||
);
|
||||
eprintln!("{}", msg);
|
||||
failures.push(msg);
|
||||
}
|
||||
} else {
|
||||
if drop.errors.is_empty() {
|
||||
let msg = format!(
|
||||
"Test failed (expected invalid): {}\nSchema: {:?}\nData: {:?}\nErrors: (Empty)",
|
||||
test.description, group.schema, test.data
|
||||
);
|
||||
println!("{}", msg);
|
||||
failures.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
return Err(format!(
|
||||
"{} tests failed in file {}:\n\n{}",
|
||||
failures.len(),
|
||||
path,
|
||||
failures.join("\n\n")
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_integer(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Number(n) => {
|
||||
n.is_i64() || n.is_u64() || n.as_f64().filter(|n| n.fract() == 0.0).is_some()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
|
||||
pub fn equals(v1: &Value, v2: &Value) -> bool {
|
||||
// eprintln!("Comparing {:?} with {:?}", v1, v2);
|
||||
match (v1, v2) {
|
||||
(Value::Null, Value::Null) => true,
|
||||
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
|
||||
(Value::Number(n1), Value::Number(n2)) => {
|
||||
if let (Some(n1), Some(n2)) = (n1.as_u64(), n2.as_u64()) {
|
||||
return n1 == n2;
|
||||
}
|
||||
if let (Some(n1), Some(n2)) = (n1.as_i64(), n2.as_i64()) {
|
||||
return n1 == n2;
|
||||
}
|
||||
if let (Some(n1), Some(n2)) = (n1.as_f64(), n2.as_f64()) {
|
||||
return (n1 - n2).abs() < f64::EPSILON;
|
||||
}
|
||||
false
|
||||
}
|
||||
(Value::String(s1), Value::String(s2)) => s1 == s2,
|
||||
(Value::Array(arr1), Value::Array(arr2)) => {
|
||||
if arr1.len() != arr2.len() {
|
||||
return false;
|
||||
}
|
||||
arr1.iter().zip(arr2).all(|(e1, e2)| equals(e1, e2))
|
||||
}
|
||||
(Value::Object(obj1), Value::Object(obj2)) => {
|
||||
if obj1.len() != obj2.len() {
|
||||
return false;
|
||||
}
|
||||
for (k1, v1) in obj1 {
|
||||
if let Some(v2) = obj2.get(k1) {
|
||||
if !equals(v1, v2) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user