jspg progress

This commit is contained in:
2026-02-17 17:41:54 -05:00
parent 6e06b6fdc2
commit 32ed463df8
188 changed files with 36654 additions and 15058 deletions

243
old_code/lib.rs Normal file
View File

@ -0,0 +1,243 @@
use pgrx::*;
pg_module_magic!();
// mod schema;
mod registry;
mod validator;
mod util;
use crate::registry::REGISTRY;
// use crate::schema::Schema;
use crate::validator::{Validator, ValidationOptions};
use lazy_static::lazy_static;
use serde_json::{json, Value};
use std::collections::{HashMap, HashSet};
#[derive(Clone, Copy, Debug, PartialEq)]
enum SchemaType {
Enum,
Type,
Family,
PublicPunc,
PrivatePunc,
}
struct CachedSchema {
t: SchemaType,
}
lazy_static! {
static ref SCHEMA_META: std::sync::RwLock<HashMap<String, CachedSchema>> = std::sync::RwLock::new(HashMap::new());
}
#[pg_extern(strict)]
fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
let mut meta = SCHEMA_META.write().unwrap();
let enums_value: Value = enums.0;
let types_value: Value = types.0;
let puncs_value: Value = puncs.0;
let mut schemas_to_register = Vec::new();
// Phase 1: Enums
if let Some(enums_array) = enums_value.as_array() {
for enum_row in enums_array {
if let Some(schemas_raw) = enum_row.get("schemas") {
if let Some(schemas_array) = schemas_raw.as_array() {
for schema_def in schemas_array {
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
schemas_to_register.push((schema_id.to_string(), schema_def.clone(), SchemaType::Enum));
}
}
}
}
}
}
// Phase 2: Types & Hierarchy
let mut hierarchy_map: HashMap<String, HashSet<String>> = HashMap::new();
if let Some(types_array) = types_value.as_array() {
for type_row in types_array {
if let Some(schemas_raw) = type_row.get("schemas") {
if let Some(schemas_array) = schemas_raw.as_array() {
for schema_def in schemas_array {
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
schemas_to_register.push((schema_id.to_string(), schema_def.clone(), SchemaType::Type));
}
}
}
}
if let Some(type_name) = type_row.get("name").and_then(|v| v.as_str()) {
if let Some(hierarchy_raw) = type_row.get("hierarchy") {
if let Some(hierarchy_array) = hierarchy_raw.as_array() {
for ancestor_val in hierarchy_array {
if let Some(ancestor_name) = ancestor_val.as_str() {
hierarchy_map.entry(ancestor_name.to_string()).or_default().insert(type_name.to_string());
}
}
}
}
}
}
}
for (base_type, descendant_types) in hierarchy_map {
let family_id = format!("{}.family", base_type);
let values: Vec<String> = descendant_types.into_iter().collect();
let family_schema = json!({ "$id": family_id, "type": "string", "enum": values });
schemas_to_register.push((family_id, family_schema, SchemaType::Family));
}
// Phase 3: Puncs
if let Some(puncs_array) = puncs_value.as_array() {
for punc_row in puncs_array {
if let Some(punc_obj) = punc_row.as_object() {
if let Some(punc_name) = punc_obj.get("name").and_then(|v| v.as_str()) {
let is_public = punc_obj.get("public").and_then(|v| v.as_bool()).unwrap_or(false);
let punc_type = if is_public { SchemaType::PublicPunc } else { SchemaType::PrivatePunc };
if let Some(schemas_raw) = punc_obj.get("schemas") {
if let Some(schemas_array) = schemas_raw.as_array() {
for schema_def in schemas_array {
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
let req_id = format!("{}.request", punc_name);
let resp_id = format!("{}.response", punc_name);
let st = if schema_id == req_id || schema_id == resp_id { punc_type } else { SchemaType::Type };
schemas_to_register.push((schema_id.to_string(), schema_def.clone(), st));
}
}
}
}
}
}
}
}
let mut all_errors = Vec::new();
for (id, value, st) in schemas_to_register {
// Meta-validation: Check 'type' enum if present
if let Some(type_val) = value.get("type") {
let types = match type_val {
Value::String(s) => vec![s.as_str()],
Value::Array(a) => a.iter().filter_map(|v| v.as_str()).collect(),
_ => vec![],
};
let valid_primitives = ["string", "number", "integer", "boolean", "array", "object", "null"];
for t in types {
if !valid_primitives.contains(&t) {
all_errors.push(json!({ "code": "ENUM_VIOLATED", "message": format!("Invalid type: {}", t) }));
}
}
}
// Clone value for insertion since it might be consumed/moved if we were doing other things
let value_for_registry = value.clone();
// Validation: just ensure it is an object or boolean
if value.is_object() || value.is_boolean() {
REGISTRY.insert(id.clone(), value_for_registry);
meta.insert(id, CachedSchema { t: st });
} else {
all_errors.push(json!({ "code": "INVALID_SCHEMA_TYPE", "message": format!("Schema {} must be an object or boolean", id) }));
}
}
if !all_errors.is_empty() {
return JsonB(json!({ "errors": all_errors }));
}
JsonB(json!({ "response": "success" }))
}
#[pg_extern(strict, parallel_safe)]
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
let schema = match REGISTRY.get(schema_id) {
Some(s) => s,
None => return JsonB(json!({
"errors": [{
"code": "SCHEMA_NOT_FOUND",
"message": format!("Schema '{}' not found", schema_id),
"details": { "schema": schema_id }
}]
})),
};
let meta = SCHEMA_META.read().unwrap();
let st = meta.get(schema_id).map(|m| m.t).unwrap_or(SchemaType::Type);
let be_strict = match st {
SchemaType::PublicPunc => true,
_ => false,
};
let options = ValidationOptions {
be_strict,
};
let mut validator = Validator::new(options, schema_id);
match validator.validate(&schema, &instance.0) {
Ok(_) => JsonB(json!({ "response": "success" })),
Err(errors) => {
let drop_errors: Vec<Value> = errors.into_iter().map(|e| json!({
"code": e.code,
"message": e.message,
"details": {
"path": e.path,
"context": e.context,
"cause": e.cause,
"schema": e.schema_id
}
})).collect();
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/debug_jspg_errors.log") {
use std::io::Write;
let _ = writeln!(f, "VALIDATION FAILED for {}: {:?}", schema_id, drop_errors);
}
JsonB(json!({ "errors": drop_errors }))
}
}
}
#[pg_extern(strict, parallel_safe)]
fn json_schema_cached(schema_id: &str) -> bool {
REGISTRY.get(schema_id).is_some()
}
#[pg_extern(strict)]
fn clear_json_schemas() -> JsonB {
REGISTRY.reset();
let mut meta = SCHEMA_META.write().unwrap();
meta.clear();
JsonB(json!({ "response": "success" }))
}
#[pg_extern(strict, parallel_safe)]
fn show_json_schemas() -> JsonB {
let meta = SCHEMA_META.read().unwrap();
let ids: Vec<String> = meta.keys().cloned().collect();
JsonB(json!({ "response": ids }))
}
/// This module is required by `cargo pgrx test` invocations.
/// It must be visible at the root of your extension crate.
#[cfg(test)]
pub mod pg_test {
pub fn setup(_options: Vec<&str>) {
// perform one-off initialization when the pg_test framework starts
}
#[must_use]
pub fn postgresql_conf_options() -> Vec<&'static str> {
// return any postgresql.conf settings that are required for your tests
vec![]
}
}
#[cfg(any(test, feature = "pg_test"))]
#[pg_schema]
mod tests {
use pgrx::pg_test;
include!("suite.rs");
}

217
old_code/registry.rs Normal file
View File

@ -0,0 +1,217 @@
use serde_json::Value;
use std::collections::HashMap;
use std::sync::RwLock;
use lazy_static::lazy_static;
lazy_static! {
pub static ref REGISTRY: Registry = Registry::new();
}
pub struct Registry {
schemas: RwLock<HashMap<String, Value>>,
}
impl Registry {
pub fn new() -> Self {
Self {
schemas: RwLock::new(HashMap::new()),
}
}
pub fn reset(&self) {
let mut schemas = self.schemas.write().unwrap();
schemas.clear();
}
pub fn insert(&self, id: String, schema: Value) {
let mut schemas = self.schemas.write().unwrap();
// Index the schema and its sub-resources (IDs and anchors)
self.index_schema(&schema, &mut schemas, Some(&id));
// Ensure the root ID is inserted (index_schema handles it, but let's be explicit)
schemas.insert(id, schema);
}
fn index_schema(&self, schema: &Value, registry: &mut HashMap<String, Value>, current_scope: Option<&str>) {
if let Value::Object(map) = schema {
// Only strictly index $id for scope resolution
let mut my_scope = current_scope.map(|s| s.to_string());
if let Some(Value::String(id)) = map.get("$id") {
if id.contains("://") {
my_scope = Some(id.clone());
} else if let Some(scope) = current_scope {
if let Some(pos) = scope.rfind('/') {
my_scope = Some(format!("{}{}", &scope[..pos + 1], id));
} else {
my_scope = Some(id.clone());
}
} else {
my_scope = Some(id.clone());
}
if let Some(final_id) = &my_scope {
registry.insert(final_id.clone(), schema.clone());
}
}
// Minimal recursion only for definitions where sub-IDs often live
// This is a tradeoff: we don't index EVERYWHERE, but we catch the 90% common case of
// bundled definitions without full tree traversal.
if let Some(Value::Object(defs)) = map.get("$defs").or_else(|| map.get("definitions")) {
for (_, def_schema) in defs {
self.index_schema(def_schema, registry, my_scope.as_deref());
}
}
}
}
pub fn get(&self, id: &str) -> Option<Value> {
let schemas = self.schemas.read().unwrap();
schemas.get(id).cloned()
}
pub fn resolve(&self, ref_str: &str, current_id: Option<&str>) -> Option<(Value, String)> {
// 1. Try full lookup (Absolute or explicit ID)
if let Some(s) = self.get(ref_str) {
return Some((s, ref_str.to_string()));
}
// 2. Try Relative lookup against current scope
if let Some(curr) = current_id {
if let Some(pos) = curr.rfind('/') {
let joined = format!("{}{}", &curr[..pos + 1], ref_str);
if let Some(s) = self.get(&joined) {
return Some((s, joined));
}
}
}
// 3. Pointer Resolution
// Split into Base URI + Fragment
let (base, fragment) = match ref_str.split_once('#') {
Some((b, f)) => (b, Some(f)),
None => (ref_str, None),
};
// If base is empty, we stay in current schema.
// If base is present, we resolve it first.
let (root_schema, scope) = if base.is_empty() {
if let Some(curr) = current_id {
// If we are looking up internally, we rely on the caller having passed the correct current ID
// But typically internal refs are just fragments.
if let Some(s) = self.get(curr) {
(s, curr.to_string())
} else {
return None;
}
} else {
return None;
}
} else {
// Resolve external base
if let Some(s) = self.get(base) {
(s, base.to_string())
} else if let Some(curr) = current_id {
// Try relative base
if let Some(pos) = curr.rfind('/') {
let joined = format!("{}{}", &curr[..pos + 1], base);
if let Some(s) = self.get(&joined) {
(s, joined)
} else {
return None;
}
} else {
return None;
}
} else {
return None;
}
};
if let Some(frag_raw) = fragment {
if frag_raw.is_empty() {
return Some((root_schema, scope));
}
// Decode fragment (it is URI encoded)
let frag_cow = percent_encoding::percent_decode_str(frag_raw).decode_utf8().unwrap_or(std::borrow::Cow::Borrowed(frag_raw));
let frag = frag_cow.as_ref();
if frag.starts_with('/') {
if let Some(sub) = root_schema.pointer(frag) {
return Some((sub.clone(), scope));
}
} else {
// It is an anchor. We scan for it at runtime to avoid complex indexing at insertion.
if let Some(sub) = self.find_anchor(&root_schema, frag) {
return Some((sub, scope));
}
}
None
} else {
Some((root_schema, scope))
}
}
fn find_anchor(&self, schema: &Value, anchor: &str) -> Option<Value> {
match schema {
Value::Object(map) => {
// Check if this schema itself has the anchor
if let Some(Value::String(a)) = map.get("$anchor") {
if a == anchor {
return Some(schema.clone());
}
}
// Recurse into $defs / definitions (Map of Schemas)
if let Some(Value::Object(defs)) = map.get("$defs").or_else(|| map.get("definitions")) {
for val in defs.values() {
if let Some(found) = self.find_anchor(val, anchor) { return Some(found); }
}
}
// Recurse into properties / patternProperties / dependentSchemas (Map of Schemas)
for key in ["properties", "patternProperties", "dependentSchemas"] {
if let Some(Value::Object(props)) = map.get(key) {
for val in props.values() {
if let Some(found) = self.find_anchor(val, anchor) { return Some(found); }
}
}
}
// Recurse into arrays of schemas
for key in ["allOf", "anyOf", "oneOf", "prefixItems"] {
if let Some(Value::Array(arr)) = map.get(key) {
for item in arr {
if let Some(found) = self.find_anchor(item, anchor) { return Some(found); }
}
}
}
// Recurse into single sub-schemas
for key in ["items", "contains", "additionalProperties", "unevaluatedProperties", "not", "if", "then", "else"] {
if let Some(val) = map.get(key) {
if val.is_object() || val.is_boolean() {
if let Some(found) = self.find_anchor(val, anchor) { return Some(found); }
}
}
}
None
}
Value::Array(arr) => {
// Should not happen for a schema object, but if we are passed an array of schemas?
// Standard schema is object or bool.
// But let's be safe.
for item in arr {
if let Some(found) = self.find_anchor(item, anchor) {
return Some(found);
}
}
None
}
_ => None,
}
}
}

236
old_code/suite.rs Normal file
View File

@ -0,0 +1,236 @@
// use crate::schema::Schema;
use crate::registry::REGISTRY;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use pgrx::JsonB;
use std::{fs, path::Path};
#[derive(Debug, Serialize, Deserialize)]
struct ExpectedError {
code: String,
path: String,
message_contains: Option<String>,
cause: Option<Value>,
context: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Group {
description: String,
schema: Option<Value>,
enums: Option<Value>,
types: Option<Value>,
puncs: Option<Value>,
tests: Vec<TestCase>,
}
#[derive(Debug, Serialize, Deserialize)]
struct TestCase {
description: String,
data: Value,
valid: bool,
action: Option<String>,
schema_id: Option<String>,
expect_errors: Option<Vec<ExpectedError>>,
}
include!("tests.rs");
fn load_remotes(dir: &Path, base_url: &str) {
if !dir.exists() { return; }
for entry in fs::read_dir(dir).expect("Failed to read remotes directory") {
let entry = entry.unwrap();
let path = entry.path();
let file_name = path.file_name().unwrap().to_str().unwrap();
if path.is_file() && file_name.ends_with(".json") {
let content = fs::read_to_string(&path).expect("Failed to read remote file");
if let Ok(schema_value) = serde_json::from_str::<Value>(&content) {
// Just check if it's a valid JSON value for a schema (object or bool)
if schema_value.is_object() || schema_value.is_boolean() {
let schema_id = format!("{}{}", base_url, file_name);
REGISTRY.insert(schema_id, schema_value);
}
}
} else if path.is_dir() {
load_remotes(&path, &format!("{}{}/", base_url, file_name));
}
}
// Mock the meta-schema for testing recursive refs
let meta_id = "https://json-schema.org/draft/2020-12/schema";
if REGISTRY.get(meta_id).is_none() {
// Just mock it as a permissive schema for now so refs resolve
REGISTRY.insert(meta_id.to_string(), json!({ "$id": meta_id }));
}
}
#[allow(dead_code)]
fn run_dir(dir: &Path, base_url: Option<&str>) -> (usize, usize) {
let mut file_count = 0;
let mut test_count = 0;
for entry in fs::read_dir(dir).expect("Failed to read directory") {
let entry = entry.unwrap();
let path = entry.path();
let file_name = path.file_name().unwrap().to_str().unwrap();
if path.is_file() && file_name.ends_with(".json") {
let count = run_file(&path, base_url);
test_count += count;
file_count += 1;
} else if path.is_dir() {
if !file_name.starts_with('.') && file_name != "optional" {
let (f, t) = run_dir(&path, base_url);
file_count += f;
test_count += t;
}
}
}
(file_count, test_count)
}
fn run_file(path: &Path, base_url: Option<&str>) -> usize {
let content = fs::read_to_string(path).expect("Failed to read file");
let groups: Vec<Group> = serde_json::from_str(&content).expect("Failed to parse JSON");
let filename = path.file_name().unwrap().to_str().unwrap();
let mut test_count = 0;
for group in groups {
// Handle JSPG setup if any JSPG fields are present
if group.enums.is_some() || group.types.is_some() || group.puncs.is_some() {
let enums = group.enums.clone().unwrap_or(json!([]));
let types = group.types.clone().unwrap_or(json!([]));
let puncs = group.puncs.clone().unwrap_or(json!([]));
// Use internal helper to register without clearing
let result = crate::cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs));
if let Some(errors) = result.0.get("errors") {
// If the group has a test specifically for caching failures, don't panic here
let has_cache_test = group.tests.iter().any(|t| t.action.as_deref() == Some("cache"));
if !has_cache_test {
panic!("FAILED: File: {}, Group: {}\nCache failed: {:?}", filename, group.description, errors);
}
}
}
let mut temp_id = "test_root".to_string();
if let Some(schema_value) = &group.schema {
temp_id = base_url.map(|b| format!("{}schema.json", b)).unwrap_or_else(|| "test_root".to_string());
if schema_value.is_object() || schema_value.is_boolean() {
REGISTRY.insert(temp_id.clone(), schema_value.clone());
}
} else {
// Fallback for JSPG style tests where the schema is in the puncs/types
let get_first_id = |items: &Option<Value>| {
items.as_ref()
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|item| item.get("schemas"))
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|sch| sch.get("$id"))
.and_then(|id| id.as_str())
.map(|s| s.to_string())
};
if let Some(id) = get_first_id(&group.puncs).or_else(|| get_first_id(&group.types)) {
temp_id = id;
}
}
for test in &group.tests {
test_count += 1;
let sid = test.schema_id.clone().unwrap_or_else(|| temp_id.clone());
let action = test.action.as_deref().unwrap_or("validate");
pgrx::notice!("Starting Test: {}", test.description);
let result = if action == "cache" {
let enums = group.enums.clone().unwrap_or(json!([]));
let types = group.types.clone().unwrap_or(json!([]));
let puncs = group.puncs.clone().unwrap_or(json!([]));
crate::cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs))
} else {
crate::validate_json_schema(&sid, JsonB(test.data.clone()))
};
let is_success = result.0.get("response").is_some();
pgrx::notice!("TEST: file={}, group={}, test={}, valid={}, outcome={}",
filename,
&group.description,
&test.description,
test.valid,
if is_success { "SUCCESS" } else { "ERRORS" }
);
if is_success != test.valid {
if let Some(errs) = result.0.get("errors") {
panic!(
"FAILED: File: {}, Group: {}, Test: {}\nExpected valid: {}, got ERRORS: {:?}",
filename,
group.description,
test.description,
test.valid,
errs
);
} else {
panic!(
"FAILED: File: {}, Group: {}, Test: {}\nExpected invalid, got SUCCESS",
filename,
group.description,
test.description
);
}
}
// Perform detailed assertions if present
if let Some(expectations) = &test.expect_errors {
let actual_errors = result.0.get("errors").and_then(|e| e.as_array()).expect("Expected errors array in failure response");
for expected in expectations {
let found = actual_errors.iter().any(|e| {
let code = e["code"].as_str().unwrap_or("");
let path = e["details"]["path"].as_str().unwrap_or("");
let message = e["message"].as_str().unwrap_or("");
let code_match = code == expected.code;
let path_match = path == expected.path;
let msg_match = if let Some(sub) = &expected.message_contains {
message.contains(sub)
} else {
true
};
let matches_cause = if let Some(expected_cause) = &expected.cause {
e["details"]["cause"] == *expected_cause
} else {
true
};
let matches_context = if let Some(expected_context) = &expected.context {
e["details"]["context"] == *expected_context
} else {
true
};
code_match && path_match && msg_match && matches_cause && matches_context
});
if !found {
panic!(
"FAILED: File: {}, Group: {}, Test: {}\nMissing expected error: code='{}', path='{}'\nActual errors: {:?}",
filename,
group.description,
test.description,
expected.code,
expected.path,
actual_errors
);
}
}
}
} // end of test loop
} // end of group loop
test_count
}

482
old_code/tests.rs Normal file
View File

@ -0,0 +1,482 @@
#[pg_test]
fn test_jspg_additional_properties() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/additionalProperties.json"), None);
}
#[pg_test]
fn test_jspg_cache() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/cache.json"), None);
}
#[pg_test]
fn test_jspg_const() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/const.json"), None);
}
#[pg_test]
fn test_jspg_dependencies() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/dependencies.json"), None);
}
#[pg_test]
fn test_jspg_enum() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/enum.json"), None);
}
#[pg_test]
fn test_jspg_errors() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/errors.json"), None);
}
#[pg_test]
fn test_jspg_format() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/format.json"), None);
}
#[pg_test]
fn test_jspg_infinite_loop_detection() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/infinite-loop-detection.json"), None);
}
#[pg_test]
fn test_jspg_one_of() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/oneOf.json"), None);
}
#[pg_test]
fn test_jspg_properties() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/properties.json"), None);
}
#[pg_test]
fn test_jspg_punc() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/punc.json"), None);
}
#[pg_test]
fn test_jspg_ref() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/ref.json"), None);
}
#[pg_test]
fn test_jspg_required() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/required.json"), None);
}
#[pg_test]
fn test_jspg_simple() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/simple.json"), None);
}
#[pg_test]
fn test_jspg_strict() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/strict.json"), None);
}
#[pg_test]
fn test_jspg_title() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/title.json"), None);
}
#[pg_test]
fn test_jspg_type() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/type.json"), None);
}
#[pg_test]
fn test_jspg_unevaluated_properties() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/unevaluatedProperties.json"), None);
}
#[pg_test]
fn test_jspg_unique_items() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/uniqueItems.json"), None);
}
#[pg_test]
fn test_json_schema_additional_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/additionalProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_all_of() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/allOf.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_anchor() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/anchor.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_any_of() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/anyOf.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_boolean_schema() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/boolean_schema.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_const() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/const.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_contains() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/contains.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_content() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/content.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_default() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/default.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_defs() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/defs.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_dependent_required() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/dependentRequired.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_dependent_schemas() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/dependentSchemas.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_dynamic_ref() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/dynamicRef.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_enum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/enum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_exclusive_maximum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/exclusiveMaximum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_exclusive_minimum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/exclusiveMinimum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_format() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/format.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_if_then_else() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/if-then-else.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_infinite_loop_detection() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/infinite-loop-detection.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/items.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_max_contains() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maxContains.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_max_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maxItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_max_length() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maxLength.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_max_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maxProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_maximum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maximum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_min_contains() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minContains.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_min_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_min_length() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minLength.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_min_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_minimum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minimum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_multiple_of() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/multipleOf.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_not() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/not.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_one_of() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/oneOf.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_pattern() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/pattern.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_pattern_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/patternProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_prefix_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/prefixItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/properties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_property_names() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/propertyNames.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_ref() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/ref.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_ref_remote() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/refRemote.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_required() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/required.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_type() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/type.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_unevaluated_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_unevaluated_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_unique_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/uniqueItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_vocabulary() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/vocabulary.json"), Some("http://localhost:1234/"));
}

53
old_code/util.rs Normal file
View File

@ -0,0 +1,53 @@
use serde_json::Value;
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
pub fn equals(v1: &Value, v2: &Value) -> bool {
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,
}
}
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,
}
}

621
old_code/validator.rs Normal file
View File

@ -0,0 +1,621 @@
use crate::registry::REGISTRY;
use crate::util::{equals, is_integer};
use serde_json::{Value, json, Map};
use std::collections::HashSet;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ValidationError {
pub code: String,
pub message: String,
pub path: String,
pub context: Value,
pub cause: Value,
pub schema_id: String,
}
#[derive(Default, Clone, Copy)]
pub struct ValidationOptions {
pub be_strict: bool,
}
pub struct Validator<'a> {
options: ValidationOptions,
// The top-level root schema ID we started with
root_schema_id: String,
// Accumulated errors
errors: Vec<ValidationError>,
// Max depth to prevent stack overflow
max_depth: usize,
_phantom: std::marker::PhantomData<&'a ()>,
}
/// Context passed down through the recursion
#[derive(Clone)]
struct ValidationContext {
// Current JSON pointer path in the instance (e.g. "/users/0/name")
current_path: String,
// The properties overridden by parent schemas (for JSPG inheritance)
overrides: HashSet<String>,
// Current resolution scope for $ref (changes when following refs)
resolution_scope: String,
// Current recursion depth
depth: usize,
}
impl ValidationContext {
fn append_path(&self, extra: &str) -> ValidationContext {
let mut new_ctx = self.clone();
if new_ctx.current_path.ends_with('/') {
new_ctx.current_path.push_str(extra);
} else if new_ctx.current_path.is_empty() {
new_ctx.current_path.push('/');
new_ctx.current_path.push_str(extra);
} else {
new_ctx.current_path.push('/');
new_ctx.current_path.push_str(extra);
}
new_ctx
}
fn append_path_new_scope(&self, extra: &str) -> ValidationContext {
let mut new_ctx = self.append_path(extra);
// Structural recursion clears overrides
new_ctx.overrides.clear();
new_ctx
}
}
impl<'a> Validator<'a> {
pub fn new(options: ValidationOptions, root_schema_id: &str) -> Self {
Self {
options,
root_schema_id: root_schema_id.to_string(),
errors: Vec::new(),
max_depth: 100,
_phantom: std::marker::PhantomData,
}
}
pub fn validate(&mut self, schema: &Value, instance: &Value) -> Result<(), Vec<ValidationError>> {
let ctx = ValidationContext {
current_path: String::new(),
overrides: HashSet::new(),
resolution_scope: self.root_schema_id.clone(),
depth: 0,
};
// We treat the top-level validate as "not lax" by default, unless specific schema logic says otherwise.
let is_lax = !self.options.be_strict;
self.validate_node(schema, instance, ctx, is_lax, false, false);
if self.errors.is_empty() {
Ok(())
} else {
Err(self.errors.clone())
}
}
fn validate_node(
&mut self,
schema: &Value,
instance: &Value,
mut ctx: ValidationContext,
is_lax: bool,
skip_strict: bool,
skip_id: bool,
) -> HashSet<String> {
let mut evaluated = HashSet::new();
// Recursion limit
if ctx.depth > self.max_depth {
self.add_error("MAX_DEPTH_REACHED", "Maximum recursion depth exceeded".to_string(), instance, json!({ "depth": ctx.depth }), &ctx);
return evaluated;
}
ctx.depth += 1;
// Handle Boolean Schemas
if let Value::Bool(b) = schema {
if !b {
self.add_error("FALSE_SCHEMA", "Schema is always false".to_string(), instance, Value::Null, &ctx);
}
return evaluated;
}
let schema_obj = match schema.as_object() {
Some(o) => o,
None => return evaluated, // Should be object or bool
};
// 1. Update Resolution Scope ($id)
if !skip_id {
if let Some(Value::String(id)) = schema_obj.get("$id") {
if id.contains("://") {
ctx.resolution_scope = id.clone();
} else {
if let Some(pos) = ctx.resolution_scope.rfind('/') {
let base = &ctx.resolution_scope[..pos + 1];
ctx.resolution_scope = format!("{}{}", base, id);
} else {
ctx.resolution_scope = id.clone();
}
}
}
}
// 2. Identify Overrides (JSPG Custom Logic)
let mut inheritance_ctx = ctx.clone();
if let Some(Value::Object(props)) = schema_obj.get("properties") {
for (pname, pval) in props {
if let Some(Value::Bool(true)) = pval.get("override") {
inheritance_ctx.overrides.insert(pname.clone());
}
}
}
// 3. Determine Laxness
let mut current_lax = is_lax;
if let Some(Value::Bool(true)) = schema_obj.get("unevaluatedProperties") { current_lax = true; }
if let Some(Value::Bool(true)) = schema_obj.get("additionalProperties") { current_lax = true; }
// ======== VALIDATION KEYWORDS ========
// Type
if let Some(type_val) = schema_obj.get("type") {
if !self.check_type(type_val, instance) {
let got = value_type_name(instance);
let want_json = serde_json::to_value(type_val).unwrap_or(json!("unknown"));
self.add_error("TYPE_MISMATCH", format!("Expected type {:?} but got {}", type_val, got), instance, json!({ "want": type_val, "got": got }), &ctx);
}
}
// Enum
if let Some(Value::Array(vals)) = schema_obj.get("enum") {
if !vals.iter().any(|v| equals(v, instance)) {
self.add_error("ENUM_VIOLATED", "Value not in enum".to_string(), instance, json!({ "want": vals }), &ctx);
}
}
// Const
if let Some(c) = schema_obj.get("const") {
if !equals(c, instance) {
self.add_error("CONST_VIOLATED", "Value does not match constant".to_string(), instance, json!({ "want": c }), &ctx);
}
}
// Object Validation
if let Value::Object(obj) = instance {
let obj_eval = self.validate_object(schema_obj, obj, instance, &ctx, current_lax);
evaluated.extend(obj_eval);
}
// Array Validation
if let Value::Array(arr) = instance {
self.validate_array(schema_obj, arr, &ctx, current_lax);
}
// Primitive Validation
self.validate_primitives(schema_obj, instance, &ctx);
// Combinators
evaluated.extend(self.validate_combinators(schema_obj, instance, &inheritance_ctx, current_lax));
// Conditionals
evaluated.extend(self.validate_conditionals(schema_obj, instance, &inheritance_ctx, current_lax));
// $ref
if let Some(Value::String(ref_str)) = schema_obj.get("$ref") {
if let Some((ref_schema, scope_uri)) = REGISTRY.resolve(ref_str, Some(&inheritance_ctx.resolution_scope)) {
let mut new_ctx = inheritance_ctx.clone();
new_ctx.resolution_scope = scope_uri;
let ref_evaluated = self.validate_node(&ref_schema, instance, new_ctx, is_lax, true, true);
evaluated.extend(ref_evaluated);
} else {
self.add_error("SCHEMA_NOT_FOUND", format!("Ref '{}' not found", ref_str), instance, json!({ "ref": ref_str }), &ctx);
}
}
// Unevaluated / Strictness Check
self.check_unevaluated(schema_obj, instance, &evaluated, &ctx, current_lax, skip_strict);
evaluated
}
fn validate_object(
&mut self,
schema: &Map<String, Value>,
obj: &Map<String, Value>,
instance: &Value,
ctx: &ValidationContext,
is_lax: bool,
) -> HashSet<String> {
let mut evaluated = HashSet::new();
// required
if let Some(Value::Array(req)) = schema.get("required") {
for field_val in req {
if let Some(field) = field_val.as_str() {
if !obj.contains_key(field) {
self.add_error("REQUIRED_FIELD_MISSING", format!("Required field '{}' is missing", field), &Value::Null, json!({ "want": [field] }), &ctx.append_path(field));
}
}
}
}
// properties
if let Some(Value::Object(props)) = schema.get("properties") {
for (pname, psch) in props {
if obj.contains_key(pname) {
if ctx.overrides.contains(pname) {
evaluated.insert(pname.clone());
continue;
}
evaluated.insert(pname.clone());
let sub_ctx = ctx.append_path_new_scope(pname);
self.validate_node(psch, &obj[pname], sub_ctx, is_lax, false, false);
}
}
}
// patternProperties
if let Some(Value::Object(pprops)) = schema.get("patternProperties") {
for (pattern, psch) in pprops {
if let Ok(re) = regex::Regex::new(pattern) {
for (pname, pval) in obj {
if re.is_match(pname) {
if ctx.overrides.contains(pname) {
evaluated.insert(pname.clone());
continue;
}
evaluated.insert(pname.clone());
let sub_ctx = ctx.append_path_new_scope(pname);
self.validate_node(psch, pval, sub_ctx, is_lax, false, false);
}
}
}
}
}
// additionalProperties
if let Some(apsch) = schema.get("additionalProperties") {
if apsch.is_object() || apsch.is_boolean() {
for (key, val) in obj {
let in_props = schema.get("properties").and_then(|p| p.as_object()).map_or(false, |p| p.contains_key(key));
let in_patterns = schema.get("patternProperties").and_then(|p| p.as_object()).map_or(false, |pp| {
pp.keys().any(|k| regex::Regex::new(k).map(|re| re.is_match(key)).unwrap_or(false))
});
if !in_props && !in_patterns {
evaluated.insert(key.clone());
let sub_ctx = ctx.append_path_new_scope(key);
self.validate_node(apsch, val, sub_ctx, is_lax, false, false);
}
}
}
}
// dependentRequired
if let Some(Value::Object(dep_req)) = schema.get("dependentRequired") {
for (prop, required_fields_val) in dep_req {
if obj.contains_key(prop) {
if let Value::Array(required_fields) = required_fields_val {
for req_field_val in required_fields {
if let Some(req_field) = req_field_val.as_str() {
if !obj.contains_key(req_field) {
self.add_error("DEPENDENCY_FAILED", format!("Field '{}' is required when '{}' is present", req_field, prop), &Value::Null, json!({ "prop": prop, "missing": [req_field] }), &ctx.append_path(req_field));
}
}
}
}
}
}
}
// dependentSchemas
if let Some(Value::Object(dep_sch)) = schema.get("dependentSchemas") {
for (prop, psch) in dep_sch {
if obj.contains_key(prop) {
let sub_evaluated = self.validate_node(psch, instance, ctx.clone(), is_lax, false, false);
evaluated.extend(sub_evaluated);
}
}
}
// legacy dependencies (Draft 4-7 compat)
if let Some(Value::Object(deps)) = schema.get("dependencies") {
for (prop, dep_val) in deps {
if obj.contains_key(prop) {
match dep_val {
Value::Array(arr) => {
for req_val in arr {
if let Some(req_field) = req_val.as_str() {
if !obj.contains_key(req_field) {
self.add_error(
"DEPENDENCY_FAILED",
format!("Field '{}' is required when '{}' is present", req_field, prop),
&Value::Null,
json!({ "prop": prop, "missing": [req_field] }),
&ctx.append_path(req_field),
);
}
}
}
}
Value::Object(_) => {
// Schema dependency
let sub_evaluated = self.validate_node(dep_val, instance, ctx.clone(), is_lax, false, false);
evaluated.extend(sub_evaluated);
}
_ => {}
}
}
}
}
// minProperties / maxProperties
if let Some(min) = schema.get("minProperties").and_then(|v| v.as_u64()) {
if (obj.len() as u64) < min {
self.add_error("MIN_PROPERTIES_VIOLATED", format!("Object must have at least {} properties", min), &json!(obj.len()), json!({ "want": min, "got": obj.len() }), ctx);
}
}
if let Some(max) = schema.get("maxProperties").and_then(|v| v.as_u64()) {
if (obj.len() as u64) > max {
self.add_error("MAX_PROPERTIES_VIOLATED", format!("Object must have at most {} properties", max), &json!(obj.len()), json!({ "want": max, "got": obj.len() }), ctx);
}
}
evaluated
}
fn validate_array(
&mut self,
schema: &Map<String, Value>,
arr: &Vec<Value>,
ctx: &ValidationContext,
is_lax: bool,
) {
if let Some(min) = schema.get("minItems").and_then(|v| v.as_u64()) {
if (arr.len() as u64) < min {
self.add_error("MIN_ITEMS_VIOLATED", format!("Array must have at least {} items", min), &json!(arr.len()), json!({ "want": min, "got": arr.len() }), ctx);
}
}
if let Some(max) = schema.get("maxItems").and_then(|v| v.as_u64()) {
if (arr.len() as u64) > max {
self.add_error("MAX_ITEMS_VIOLATED", format!("Array must have at most {} items", max), &json!(arr.len()), json!({ "want": max, "got": arr.len() }), ctx);
}
}
let mut evaluated_index = 0;
if let Some(Value::Array(prefix)) = schema.get("prefixItems") {
for (i, psch) in prefix.iter().enumerate() {
if let Some(item) = arr.get(i) {
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
self.validate_node(psch, item, sub_ctx, is_lax, false, false);
evaluated_index = i + 1;
}
}
}
if let Some(items_val) = schema.get("items") {
if let Value::Bool(false) = items_val {
if arr.len() > evaluated_index {
self.add_error("ADDITIONAL_ITEMS_NOT_ALLOWED", "Extra items not allowed".to_string(), &json!(arr.len()), json!({ "got": arr.len() - evaluated_index }), ctx);
}
} else {
// Schema or true
for i in evaluated_index..arr.len() {
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
self.validate_node(items_val, &arr[i], sub_ctx, is_lax, false, false);
}
}
}
if let Some(contains_sch) = schema.get("contains") {
let mut matches = 0;
for (i, item) in arr.iter().enumerate() {
let mut sub = self.branch();
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
sub.validate_node(contains_sch, item, sub_ctx, is_lax, false, false);
if sub.errors.is_empty() {
matches += 1;
}
}
if matches == 0 {
self.add_error("CONTAINS_FAILED", "No items match 'contains' schema".to_string(), &json!(arr), json!({}), ctx);
}
if let Some(min) = schema.get("minContains").and_then(|v| v.as_u64()) {
if (matches as u64) < min {
self.add_error("MIN_CONTAINS_VIOLATED", format!("Expected at least {} items to match 'contains'", min), &json!(arr), json!({ "want": min, "got": matches }), ctx);
}
}
if let Some(max) = schema.get("maxContains").and_then(|v| v.as_u64()) {
if (matches as u64) > max {
self.add_error("MAX_CONTAINS_VIOLATED", format!("Expected at most {} items to match 'contains'", max), &json!(arr), json!({ "want": max, "got": matches }), ctx);
}
}
}
// uniqueItems
if let Some(Value::Bool(true)) = schema.get("uniqueItems") {
for i in 0..arr.len() {
for j in (i + 1)..arr.len() {
if equals(&arr[i], &arr[j]) {
self.add_error("UNIQUE_ITEMS_VIOLATED", format!("Array items at indices {} and {} are equal", i, j), &json!(arr), json!({ "got": [i, j] }), ctx);
return;
}
}
}
}
}
fn validate_primitives(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext) {
if let Some(s) = instance.as_str() {
if let Some(min) = schema.get("minLength").and_then(|v| v.as_u64()) {
if (s.chars().count() as u64) < min { self.add_error("MIN_LENGTH_VIOLATED", format!("String too short (min {})", min), instance, json!({ "want": min, "got": s.len() }), ctx); }
}
if let Some(max) = schema.get("maxLength").and_then(|v| v.as_u64()) {
if (s.chars().count() as u64) > max { self.add_error("MAX_LENGTH_VIOLATED", format!("String too long (max {})", max), instance, json!({ "want": max, "got": s.len() }), ctx); }
}
if let Some(Value::String(pat)) = schema.get("pattern") {
if let Ok(re) = regex::Regex::new(pat) {
if !re.is_match(s) { self.add_error("PATTERN_VIOLATED", format!("String does not match pattern '{}'", pat), instance, json!({ "want": pat, "got": s }), ctx); }
}
}
if let Some(Value::String(fmt)) = schema.get("format") {
if !s.is_empty() {
match fmt.as_str() {
"uuid" => { if uuid::Uuid::parse_str(s).is_err() { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid UUID", s), instance, json!({ "format": "uuid" }), ctx); } }
"date-time" => { if chrono::DateTime::parse_from_rfc3339(s).is_err() { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid date-time", s), instance, json!({ "format": "date-time" }), ctx); } }
"email" => { if !s.contains('@') { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid email", s), instance, json!({ "format": "email" }), ctx); } }
_ => {}
}
}
}
}
if let Some(n) = instance.as_f64() {
if let Some(min) = schema.get("minimum").and_then(|v| v.as_f64()) {
if n < min { self.add_error("MINIMUM_VIOLATED", format!("Value {} < minimum {}", n, min), instance, json!({ "want": min, "got": n }), ctx); }
}
if let Some(max) = schema.get("maximum").and_then(|v| v.as_f64()) {
if n > max { self.add_error("MAXIMUM_VIOLATED", format!("Value {} > maximum {}", n, max), instance, json!({ "want": max, "got": n }), ctx); }
}
if let Some(min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
if n <= min { self.add_error("EXCLUSIVE_MINIMUM_VIOLATED", format!("Value {} <= exclusive minimum {}", n, min), instance, json!({ "want": min, "got": n }), ctx); }
}
if let Some(max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
if n >= max { self.add_error("EXCLUSIVE_MAXIMUM_VIOLATED", format!("Value {} >= exclusive maximum {}", n, max), instance, json!({ "want": max, "got": n }), ctx); }
}
if let Some(mult) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
let rem = (n / mult).fract();
if rem.abs() > f64::EPSILON && (1.0 - rem).abs() > f64::EPSILON {
self.add_error("MULTIPLE_OF_VIOLATED", format!("Value {} not multiple of {}", n, mult), instance, json!({ "want": mult, "got": n }), ctx);
}
}
}
}
fn validate_combinators(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext, is_lax: bool) -> HashSet<String> {
let mut evaluated = HashSet::new();
if let Some(Value::Array(all_of)) = schema.get("allOf") {
for sch in all_of { evaluated.extend(self.validate_node(sch, instance, ctx.clone(), is_lax, true, false)); }
}
if let Some(Value::Array(any_of)) = schema.get("anyOf") {
let mut matched = false;
let mut errors_acc = Vec::new();
for sch in any_of {
let mut sub = self.branch();
let sub_eval = sub.validate_node(sch, instance, ctx.clone(), is_lax, false, false);
if sub.errors.is_empty() { matched = true; evaluated.extend(sub_eval); } else { errors_acc.extend(sub.errors); }
}
if !matched { self.add_error("ANY_OF_VIOLATED", "Value did not match any allowed schema".to_string(), instance, json!({ "causes": errors_acc }), ctx); }
}
if let Some(Value::Array(one_of)) = schema.get("oneOf") {
let mut match_count = 0;
let mut last_eval = HashSet::new();
let mut error_causes = Vec::new();
for sch in one_of {
let mut sub = self.branch();
let sub_eval = sub.validate_node(sch, instance, ctx.clone(), is_lax, false, false);
if sub.errors.is_empty() { match_count += 1; last_eval = sub_eval; } else { error_causes.extend(sub.errors); }
}
if match_count == 1 { evaluated.extend(last_eval); }
else { self.add_error("ONE_OF_VIOLATED", format!("Value matched {} schemas, expected 1", match_count), instance, json!({ "matched": match_count, "causes": error_causes }), ctx); }
}
if let Some(not_sch) = schema.get("not") {
let mut sub = self.branch();
sub.validate_node(not_sch, instance, ctx.clone(), is_lax, false, false);
if sub.errors.is_empty() { self.add_error("NOT_VIOLATED", "Value matched 'not' schema".to_string(), instance, Value::Null, ctx); }
}
evaluated
}
fn validate_conditionals(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext, is_lax: bool) -> HashSet<String> {
let mut evaluated = HashSet::new();
if let Some(if_sch) = schema.get("if") {
let mut sub = self.branch();
let sub_eval = sub.validate_node(if_sch, instance, ctx.clone(), is_lax, true, false);
if sub.errors.is_empty() {
evaluated.extend(sub_eval);
if let Some(then_sch) = schema.get("then") { evaluated.extend(self.validate_node(then_sch, instance, ctx.clone(), is_lax, false, false)); }
} else if let Some(else_sch) = schema.get("else") {
evaluated.extend(self.validate_node(else_sch, instance, ctx.clone(), is_lax, false, false));
}
}
evaluated
}
fn check_unevaluated(&mut self, schema: &Map<String, Value>, instance: &Value, evaluated: &HashSet<String>, ctx: &ValidationContext, is_lax: bool, skip_strict: bool) {
if let Value::Object(obj) = instance {
if let Some(Value::Bool(false)) = schema.get("additionalProperties") {
for key in obj.keys() {
let in_props = schema.get("properties").and_then(|p| p.as_object()).map_or(false, |p| p.contains_key(key));
let in_pattern = schema.get("patternProperties").and_then(|p| p.as_object()).map_or(false, |pp| pp.keys().any(|k| regex::Regex::new(k).map(|re| re.is_match(key)).unwrap_or(false)));
if !in_props && !in_pattern {
if ctx.overrides.contains(key) { continue; }
self.add_error("ADDITIONAL_PROPERTIES_NOT_ALLOWED", format!("Property '{}' is not allowed", key), &Value::Null, json!({ "got": [key] }), &ctx.append_path(key));
}
}
}
let explicit_opts = schema.contains_key("unevaluatedProperties") || schema.contains_key("additionalProperties");
let should_check_strict = self.options.be_strict && !is_lax && !explicit_opts && !skip_strict;
let check_unevaluated = matches!(schema.get("unevaluatedProperties"), Some(Value::Bool(false)));
if should_check_strict || check_unevaluated {
for key in obj.keys() {
if !evaluated.contains(key) {
if ctx.overrides.contains(key) { continue; }
self.add_error("ADDITIONAL_PROPERTIES_NOT_ALLOWED", format!("Property '{}' is not allowed (strict/unevaluated)", key), &Value::Null, json!({ "got": [key] }), &ctx.append_path(key));
}
}
}
}
}
fn check_type(&self, expected: &Value, instance: &Value) -> bool {
match expected {
Value::String(s) => self.is_primitive_type(s, instance),
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).any(|pt| self.is_primitive_type(pt, instance)),
_ => false
}
}
fn is_primitive_type(&self, pt: &str, instance: &Value) -> bool {
match pt {
"string" => instance.is_string(),
"number" => instance.is_number(),
"integer" => is_integer(instance),
"boolean" => instance.is_boolean(),
"array" => instance.is_array(),
"object" => instance.is_object(),
"null" => instance.is_null(),
_ => false
}
}
fn branch(&self) -> Self {
Self { options: self.options, root_schema_id: self.root_schema_id.clone(), errors: Vec::new(), max_depth: self.max_depth, _phantom: std::marker::PhantomData }
}
fn add_error(&mut self, code: &str, message: String, context: &Value, cause: Value, ctx: &ValidationContext) {
let path = ctx.current_path.clone();
if self.errors.iter().any(|e| e.code == code && e.path == path) { return; }
self.errors.push(ValidationError { code: code.to_string(), message, path, context: context.clone(), cause, schema_id: self.root_schema_id.clone() });
}
fn extend_unique(&mut self, errors: Vec<ValidationError>) {
for e in errors { if !self.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) { self.errors.push(e); } }
}
}
fn value_type_name(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(n) => if n.is_i64() { "integer" } else { "number" },
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}