added type family support
This commit is contained in:
38
src/lib.rs
38
src/lib.rs
@ -7,12 +7,13 @@ use lazy_static::lazy_static;
|
||||
use serde_json::{json, Value, Number};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::{collections::HashMap, sync::RwLock};
|
||||
use std::{collections::{HashMap, HashSet}, sync::RwLock};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum SchemaType {
|
||||
Enum,
|
||||
Type,
|
||||
Family, // Added for generated hierarchy schemas
|
||||
PublicPunc,
|
||||
PrivatePunc,
|
||||
}
|
||||
@ -20,7 +21,6 @@ enum SchemaType {
|
||||
struct Schema {
|
||||
index: SchemaIndex,
|
||||
t: SchemaType,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
struct Cache {
|
||||
@ -77,9 +77,11 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Types
|
||||
// Phase 2: Types & Hierarchy Pre-processing
|
||||
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 {
|
||||
// Process main schemas for the type
|
||||
if let Some(schemas_raw) = type_row.get("schemas") {
|
||||
if let Some(schemas_array) = schemas_raw.as_array() {
|
||||
for schema_def in schemas_array {
|
||||
@ -89,9 +91,37 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process hierarchy to build .family enums
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and add the .family schemas
|
||||
for (base_type, descendant_types) in hierarchy_map {
|
||||
let family_schema_id = format!("{}.family", base_type);
|
||||
let enum_values: Vec<String> = descendant_types.into_iter().collect();
|
||||
let family_schema = json!({
|
||||
"$id": family_schema_id,
|
||||
"type": "string",
|
||||
"enum": enum_values
|
||||
});
|
||||
schemas_to_compile.push((family_schema_id, family_schema, SchemaType::Family));
|
||||
}
|
||||
|
||||
// Phase 3: Puncs
|
||||
if let Some(puncs_array) = puncs_value.as_array() {
|
||||
for punc_row in puncs_array {
|
||||
@ -166,7 +196,7 @@ fn compile_all_schemas(
|
||||
for (id, value, schema_type) in schemas_to_compile {
|
||||
match compiler.compile(id, &mut cache.schemas) {
|
||||
Ok(index) => {
|
||||
cache.map.insert(id.clone(), Schema { index, t: *schema_type, value: value.clone() });
|
||||
cache.map.insert(id.clone(), Schema { index, t: *schema_type });
|
||||
}
|
||||
Err(e) => {
|
||||
match &e {
|
||||
|
||||
@ -1056,4 +1056,73 @@ pub fn nullable_union_schemas() -> JsonB {
|
||||
}]);
|
||||
|
||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hierarchy_schemas() -> JsonB {
|
||||
let enums = json!([]);
|
||||
let types = json!([
|
||||
{
|
||||
"name": "entity",
|
||||
"hierarchy": ["entity"],
|
||||
"schemas": [{
|
||||
"$id": "entity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"type": { "$ref": "entity.family", "override": true }
|
||||
},
|
||||
"required": ["id", "type"]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "organization",
|
||||
"hierarchy": ["entity", "organization"],
|
||||
"schemas": [{
|
||||
"$id": "organization",
|
||||
"$ref": "entity",
|
||||
"properties": {
|
||||
"type": { "$ref": "organization.family", "override": true },
|
||||
"name": { "type": "string" }
|
||||
},
|
||||
"required": ["name"]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"hierarchy": ["entity", "organization", "user"],
|
||||
"schemas": [{
|
||||
"$id": "user",
|
||||
"$ref": "organization",
|
||||
"properties": {
|
||||
"type": { "$ref": "user.family", "override": true },
|
||||
"password": { "type": "string" }
|
||||
},
|
||||
"required": ["password"]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "person",
|
||||
"hierarchy": ["entity", "organization", "user", "person"],
|
||||
"schemas": [{
|
||||
"$id": "person",
|
||||
"$ref": "user",
|
||||
"properties": {
|
||||
"type": { "$ref": "person.family", "override": true },
|
||||
"first_name": { "type": "string" }
|
||||
},
|
||||
"required": ["first_name"]
|
||||
}]
|
||||
}
|
||||
]);
|
||||
|
||||
let puncs = json!([{
|
||||
"name": "test_org_punc",
|
||||
"public": false,
|
||||
"schemas": [{
|
||||
"$id": "test_org_punc.request",
|
||||
"$ref": "organization"
|
||||
}]
|
||||
}]);
|
||||
|
||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||
}
|
||||
|
||||
52
src/tests.rs
52
src/tests.rs
@ -1036,4 +1036,54 @@ fn test_validate_nullable_union() {
|
||||
let result_invalid = validate_json_schema("nullable_union_test.request", jsonb(invalid_string));
|
||||
assert_failure(&result_invalid);
|
||||
assert_has_error(&result_invalid, "TYPE_MISMATCH", "/nullable_prop");
|
||||
}
|
||||
}
|
||||
|
||||
#[pg_test]
|
||||
fn test_validate_type_hierarchy() {
|
||||
clear_json_schemas();
|
||||
let cache_result = hierarchy_schemas();
|
||||
assert_success(&cache_result);
|
||||
|
||||
// 1. Test success case: validating a derived type (person) against a base schema (organization)
|
||||
let person_instance = json!({
|
||||
"id": "person-id",
|
||||
"type": "person",
|
||||
"name": "person-name",
|
||||
"password": "person-password",
|
||||
"first_name": "person-first-name"
|
||||
});
|
||||
let result_success = validate_json_schema("organization", jsonb(person_instance.clone()));
|
||||
assert_success(&result_success);
|
||||
|
||||
// 2. Test success case: validating a base type (organization) against its own schema
|
||||
let org_instance = json!({
|
||||
"id": "org-id",
|
||||
"type": "organization",
|
||||
"name": "org-name"
|
||||
});
|
||||
let result_org_success = validate_json_schema("organization", jsonb(org_instance));
|
||||
assert_success(&result_org_success);
|
||||
|
||||
// 3. Test failure case: validating an ancestor type (entity) against a derived schema (organization)
|
||||
let entity_instance = json!({
|
||||
"id": "entity-id",
|
||||
"type": "entity"
|
||||
});
|
||||
let result_fail_ancestor = validate_json_schema("organization", jsonb(entity_instance));
|
||||
assert_failure(&result_fail_ancestor);
|
||||
assert_has_error(&result_fail_ancestor, "ENUM_VIOLATED", "/type");
|
||||
|
||||
// 4. Test failure case: validating a completely unrelated type
|
||||
let unrelated_instance = json!({
|
||||
"id": "job-id",
|
||||
"type": "job",
|
||||
"name": "job-name"
|
||||
});
|
||||
let result_fail_unrelated = validate_json_schema("organization", jsonb(unrelated_instance));
|
||||
assert_failure(&result_fail_unrelated);
|
||||
assert_has_error(&result_fail_unrelated, "ENUM_VIOLATED", "/type");
|
||||
|
||||
// 5. Test that the punc using the schema also works
|
||||
let punc_success = validate_json_schema("test_org_punc.request", jsonb(person_instance.clone()));
|
||||
assert_success(&punc_success);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user