Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6610b069db | |||
| bb84f9aa73 | |||
| 704770051c | |||
| 88c77deede |
118
src/lib.rs
118
src/lib.rs
@ -9,7 +9,7 @@ use std::borrow::Cow;
|
|||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::{collections::HashMap, sync::RwLock};
|
use std::{collections::HashMap, sync::RwLock};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
enum SchemaType {
|
enum SchemaType {
|
||||||
Enum,
|
Enum,
|
||||||
Type,
|
Type,
|
||||||
@ -20,6 +20,7 @@ enum SchemaType {
|
|||||||
struct BoonCache {
|
struct BoonCache {
|
||||||
schemas: Schemas,
|
schemas: Schemas,
|
||||||
id_to_index: HashMap<String, SchemaIndex>,
|
id_to_index: HashMap<String, SchemaIndex>,
|
||||||
|
id_to_type: HashMap<String, SchemaType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structure to hold error information without lifetimes
|
// Structure to hold error information without lifetimes
|
||||||
@ -35,6 +36,7 @@ lazy_static! {
|
|||||||
static ref SCHEMA_CACHE: RwLock<BoonCache> = RwLock::new(BoonCache {
|
static ref SCHEMA_CACHE: RwLock<BoonCache> = RwLock::new(BoonCache {
|
||||||
schemas: Schemas::new(),
|
schemas: Schemas::new(),
|
||||||
id_to_index: HashMap::new(),
|
id_to_index: HashMap::new(),
|
||||||
|
id_to_type: HashMap::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +51,7 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
|
|||||||
*cache = BoonCache {
|
*cache = BoonCache {
|
||||||
schemas: Schemas::new(),
|
schemas: Schemas::new(),
|
||||||
id_to_index: HashMap::new(),
|
id_to_index: HashMap::new(),
|
||||||
|
id_to_type: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the boon compiler and enable format assertions
|
// Create the boon compiler and enable format assertions
|
||||||
@ -85,6 +88,7 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
all_schema_ids.push(schema_id.to_string());
|
all_schema_ids.push(schema_id.to_string());
|
||||||
|
cache.id_to_type.insert(schema_id.to_string(), SchemaType::Enum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,6 +123,7 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
all_schema_ids.push(schema_id.to_string());
|
all_schema_ids.push(schema_id.to_string());
|
||||||
|
cache.id_to_type.insert(schema_id.to_string(), SchemaType::Type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,15 +145,24 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let punc_schema_type = if is_public { SchemaType::PublicPunc } else { SchemaType::PrivatePunc };
|
let punc_schema_type = if is_public { SchemaType::PublicPunc } else { SchemaType::PrivatePunc };
|
||||||
|
|
||||||
// Add punc local schemas as resources (from schemas field) - use $id directly (universal)
|
// Add punc schemas from the 'schemas' array
|
||||||
if let Some(schemas_raw) = punc_obj.get("schemas") {
|
if let Some(schemas_raw) = punc_obj.get("schemas") {
|
||||||
if let Some(schemas_array) = schemas_raw.as_array() {
|
if let Some(schemas_array) = schemas_raw.as_array() {
|
||||||
for schema_def in schemas_array {
|
for schema_def in schemas_array {
|
||||||
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
|
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
|
||||||
if let Err(e) = add_schema_resource(&mut compiler, schema_id, schema_def.clone(), SchemaType::Type, &mut errors) {
|
let request_schema_id = format!("{}.request", punc_name);
|
||||||
|
let response_schema_id = format!("{}.response", punc_name);
|
||||||
|
|
||||||
|
let schema_type_for_def = if schema_id == request_schema_id || schema_id == response_schema_id {
|
||||||
|
punc_schema_type
|
||||||
|
} else {
|
||||||
|
SchemaType::Type // For local/nested schemas
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = add_schema_resource(&mut compiler, schema_id, schema_def.clone(), schema_type_for_def, &mut errors) {
|
||||||
errors.push(json!({
|
errors.push(json!({
|
||||||
"code": "PUNC_LOCAL_SCHEMA_RESOURCE_FAILED",
|
"code": "PUNC_SCHEMA_RESOURCE_FAILED",
|
||||||
"message": format!("Failed to add local schema resource '{}' for punc '{}'", schema_id, punc_name),
|
"message": format!("Failed to add schema resource '{}' for punc '{}'", schema_id, punc_name),
|
||||||
"details": {
|
"details": {
|
||||||
"punc_name": punc_name,
|
"punc_name": punc_name,
|
||||||
"schema_id": schema_id,
|
"schema_id": schema_id,
|
||||||
@ -157,51 +171,12 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
all_schema_ids.push(schema_id.to_string());
|
all_schema_ids.push(schema_id.to_string());
|
||||||
|
cache.id_to_type.insert(schema_id.to_string(), schema_type_for_def);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add request schema as resource if present - use {punc_name}.request
|
|
||||||
if let Some(request_schema) = punc_obj.get("request") {
|
|
||||||
if !request_schema.is_null() {
|
|
||||||
let request_schema_id = format!("{}.request", punc_name);
|
|
||||||
if let Err(e) = add_schema_resource(&mut compiler, &request_schema_id, request_schema.clone(), punc_schema_type, &mut errors) {
|
|
||||||
errors.push(json!({
|
|
||||||
"code": "PUNC_REQUEST_SCHEMA_RESOURCE_FAILED",
|
|
||||||
"message": format!("Failed to add request schema resource for punc '{}'", punc_name),
|
|
||||||
"details": {
|
|
||||||
"punc_name": punc_name,
|
|
||||||
"schema_id": request_schema_id,
|
|
||||||
"cause": format!("{}", e)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
all_schema_ids.push(request_schema_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add response schema as resource if present - use {punc_name}.response
|
|
||||||
if let Some(response_schema) = punc_obj.get("response") {
|
|
||||||
if !response_schema.is_null() {
|
|
||||||
let response_schema_id = format!("{}.response", punc_name);
|
|
||||||
if let Err(e) = add_schema_resource(&mut compiler, &response_schema_id, response_schema.clone(), punc_schema_type, &mut errors) {
|
|
||||||
errors.push(json!({
|
|
||||||
"code": "PUNC_RESPONSE_SCHEMA_RESOURCE_FAILED",
|
|
||||||
"message": format!("Failed to add response schema resource for punc '{}'", punc_name),
|
|
||||||
"details": {
|
|
||||||
"punc_name": punc_name,
|
|
||||||
"schema_id": response_schema_id,
|
|
||||||
"cause": format!("{}", e)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
all_schema_ids.push(response_schema_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -353,6 +328,47 @@ fn apply_strict_validation_recursive(schema: &mut Value, inside_conditional: boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_type_against_schema_id(instance: &Value, schema_id: &str) -> JsonB {
|
||||||
|
let expected_type = schema_id.split('.').next().unwrap_or(schema_id);
|
||||||
|
|
||||||
|
if let Some(actual_type) = instance.get("type").and_then(|v| v.as_str()) {
|
||||||
|
if actual_type == expected_type {
|
||||||
|
return JsonB(json!({ "response": "success" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach here, validation failed. Now we build the specific error.
|
||||||
|
let (message, cause, context) =
|
||||||
|
if let Some(actual_type) = instance.get("type").and_then(|v| v.as_str()) {
|
||||||
|
// This handles the case where the type is a string but doesn't match.
|
||||||
|
(
|
||||||
|
format!("Instance type '{}' does not match expected type '{}' derived from schema ID", actual_type, expected_type),
|
||||||
|
json!({ "expected": expected_type, "actual": actual_type }),
|
||||||
|
json!(actual_type)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// This handles the case where 'type' is missing or not a string.
|
||||||
|
(
|
||||||
|
"Instance 'type' property is missing or not a string".to_string(),
|
||||||
|
json!("The 'type' property must be a string and is required for this validation."),
|
||||||
|
instance.get("type").unwrap_or(&Value::Null).clone()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
JsonB(json!({
|
||||||
|
"errors": [{
|
||||||
|
"code": "TYPE_MISMATCH",
|
||||||
|
"message": message,
|
||||||
|
"details": {
|
||||||
|
"path": "/type",
|
||||||
|
"context": context,
|
||||||
|
"cause": cause,
|
||||||
|
"schema": schema_id
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[pg_extern(strict, parallel_safe)]
|
#[pg_extern(strict, parallel_safe)]
|
||||||
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
|
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
|
||||||
let cache = SCHEMA_CACHE.read().unwrap();
|
let cache = SCHEMA_CACHE.read().unwrap();
|
||||||
@ -371,7 +387,16 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
|
|||||||
Some(sch_index) => {
|
Some(sch_index) => {
|
||||||
let instance_value: Value = instance.0;
|
let instance_value: Value = instance.0;
|
||||||
match cache.schemas.validate(&instance_value, *sch_index) {
|
match cache.schemas.validate(&instance_value, *sch_index) {
|
||||||
Ok(_) => JsonB(json!({ "response": "success" })),
|
Ok(_) => {
|
||||||
|
// After standard validation, perform custom type check if it's a Type schema
|
||||||
|
if let Some(&schema_type) = cache.id_to_type.get(schema_id) {
|
||||||
|
if schema_type == SchemaType::Type {
|
||||||
|
return validate_type_against_schema_id(&instance_value, schema_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For non-Type schemas, or if type not found (shouldn't happen), success.
|
||||||
|
JsonB(json!({ "response": "success" }))
|
||||||
|
}
|
||||||
Err(validation_error) => {
|
Err(validation_error) => {
|
||||||
let mut error_list = Vec::new();
|
let mut error_list = Vec::new();
|
||||||
collect_errors(&validation_error, &mut error_list);
|
collect_errors(&validation_error, &mut error_list);
|
||||||
@ -992,6 +1017,7 @@ fn clear_json_schemas() -> JsonB {
|
|||||||
*cache = BoonCache {
|
*cache = BoonCache {
|
||||||
schemas: Schemas::new(),
|
schemas: Schemas::new(),
|
||||||
id_to_index: HashMap::new(),
|
id_to_index: HashMap::new(),
|
||||||
|
id_to_type: HashMap::new(),
|
||||||
};
|
};
|
||||||
JsonB(json!({ "response": "success" }))
|
JsonB(json!({ "response": "success" }))
|
||||||
}
|
}
|
||||||
|
|||||||
208
src/schemas.rs
208
src/schemas.rs
@ -13,14 +13,15 @@ pub fn simple_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "simple",
|
"name": "simple",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "simple.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
||||||
"age": { "type": "integer", "minimum": 0 }
|
"age": { "type": "integer", "minimum": 0 }
|
||||||
},
|
},
|
||||||
"required": ["name", "age"]
|
"required": ["name", "age"]
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -32,10 +33,10 @@ pub fn invalid_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "invalid_punc",
|
"name": "invalid_punc",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
"$id": "urn:invalid_schema",
|
"$id": "invalid_punc.request",
|
||||||
"type": ["invalid_type_value"]
|
"type": ["invalid_type_value"]
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -47,7 +48,8 @@ pub fn errors_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "detailed_errors_test",
|
"name": "detailed_errors_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "detailed_errors_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"address": {
|
"address": {
|
||||||
@ -60,7 +62,7 @@ pub fn errors_schemas() -> JsonB {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["address"]
|
"required": ["address"]
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -72,7 +74,8 @@ pub fn oneof_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "oneof_test",
|
"name": "oneof_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "oneof_test.request",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -89,7 +92,7 @@ pub fn oneof_schemas() -> JsonB {
|
|||||||
"required": ["number_prop"]
|
"required": ["number_prop"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -102,19 +105,21 @@ pub fn root_types_schemas() -> JsonB {
|
|||||||
{
|
{
|
||||||
"name": "object_test",
|
"name": "object_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "object_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
||||||
"age": { "type": "integer", "minimum": 0 }
|
"age": { "type": "integer", "minimum": 0 }
|
||||||
},
|
},
|
||||||
"required": ["name", "age"]
|
"required": ["name", "age"]
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "array_test",
|
"name": "array_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "array_test.request",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -122,7 +127,7 @@ pub fn root_types_schemas() -> JsonB {
|
|||||||
"id": { "type": "string", "format": "uuid" }
|
"id": { "type": "string", "format": "uuid" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -136,27 +141,30 @@ pub fn strict_schemas() -> JsonB {
|
|||||||
{
|
{
|
||||||
"name": "basic_strict_test",
|
"name": "basic_strict_test",
|
||||||
"public": true,
|
"public": true,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "basic_strict_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" }
|
"name": { "type": "string" }
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "non_strict_test",
|
"name": "non_strict_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "non_strict_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" }
|
"name": { "type": "string" }
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nested_strict_test",
|
"name": "nested_strict_test",
|
||||||
"public": true,
|
"public": true,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "nested_strict_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"user": {
|
"user": {
|
||||||
@ -175,34 +183,37 @@ pub fn strict_schemas() -> JsonB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "already_unevaluated_test",
|
"name": "already_unevaluated_test",
|
||||||
"public": true,
|
"public": true,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "already_unevaluated_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" }
|
"name": { "type": "string" }
|
||||||
},
|
},
|
||||||
"unevaluatedProperties": true
|
"unevaluatedProperties": true
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "already_additional_test",
|
"name": "already_additional_test",
|
||||||
"public": true,
|
"public": true,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "already_additional_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" }
|
"name": { "type": "string" }
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "conditional_strict_test",
|
"name": "conditional_strict_test",
|
||||||
"public": true,
|
"public": true,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "conditional_strict_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"creating": { "type": "boolean" }
|
"creating": { "type": "boolean" }
|
||||||
@ -218,7 +229,7 @@ pub fn strict_schemas() -> JsonB {
|
|||||||
},
|
},
|
||||||
"required": ["name"]
|
"required": ["name"]
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -231,14 +242,15 @@ pub fn required_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "basic_validation_test",
|
"name": "basic_validation_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "basic_validation_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
||||||
"age": { "type": "integer", "minimum": 0 }
|
"age": { "type": "integer", "minimum": 0 }
|
||||||
},
|
},
|
||||||
"required": ["name", "age"]
|
"required": ["name", "age"]
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -250,7 +262,8 @@ pub fn dependencies_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "dependency_split_test",
|
"name": "dependency_split_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "dependency_split_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"creating": { "type": "boolean" },
|
"creating": { "type": "boolean" },
|
||||||
@ -261,7 +274,7 @@ pub fn dependencies_schemas() -> JsonB {
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"creating": ["name", "kind"]
|
"creating": ["name", "kind"]
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -273,7 +286,8 @@ pub fn nested_req_deps_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "nested_dep_test",
|
"name": "nested_dep_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "nested_dep_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"items": {
|
"items": {
|
||||||
@ -294,7 +308,7 @@ pub fn nested_req_deps_schemas() -> JsonB {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["items"]
|
"required": ["items"]
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -307,19 +321,21 @@ pub fn additional_properties_schemas() -> JsonB {
|
|||||||
{
|
{
|
||||||
"name": "additional_props_test",
|
"name": "additional_props_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "additional_props_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
||||||
"age": { "type": "number" }
|
"age": { "type": "number" }
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nested_additional_props_test",
|
"name": "nested_additional_props_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "nested_additional_props_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"user": {
|
"user": {
|
||||||
@ -330,7 +346,7 @@ pub fn additional_properties_schemas() -> JsonB {
|
|||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -344,7 +360,8 @@ pub fn unevaluated_properties_schemas() -> JsonB {
|
|||||||
{
|
{
|
||||||
"name": "simple_unevaluated_test",
|
"name": "simple_unevaluated_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "simple_unevaluated_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
||||||
@ -354,12 +371,13 @@ pub fn unevaluated_properties_schemas() -> JsonB {
|
|||||||
"^attr_": { "type": "string" }
|
"^attr_": { "type": "string" }
|
||||||
},
|
},
|
||||||
"unevaluatedProperties": false
|
"unevaluatedProperties": false
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "conditional_unevaluated_test",
|
"name": "conditional_unevaluated_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "conditional_unevaluated_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@ -377,7 +395,7 @@ pub fn unevaluated_properties_schemas() -> JsonB {
|
|||||||
"age": { "type": "number" }
|
"age": { "type": "number" }
|
||||||
},
|
},
|
||||||
"unevaluatedProperties": false
|
"unevaluatedProperties": false
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -390,14 +408,15 @@ pub fn format_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "format_test",
|
"name": "format_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "format_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"uuid": { "type": "string", "format": "uuid" },
|
"uuid": { "type": "string", "format": "uuid" },
|
||||||
"date_time": { "type": "string", "format": "date-time" },
|
"date_time": { "type": "string", "format": "date-time" },
|
||||||
"email": { "type": "string", "format": "email" }
|
"email": { "type": "string", "format": "email" }
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -602,16 +621,18 @@ pub fn punc_with_refs_schemas() -> JsonB {
|
|||||||
{
|
{
|
||||||
"name": "public_ref_test",
|
"name": "public_ref_test",
|
||||||
"public": true,
|
"public": true,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "public_ref_test.request",
|
||||||
"$ref": "person"
|
"$ref": "person"
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "private_ref_test",
|
"name": "private_ref_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "private_ref_test.request",
|
||||||
"$ref": "person"
|
"$ref": "person"
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -636,13 +657,14 @@ pub fn enum_schemas() -> JsonB {
|
|||||||
let puncs = json!([{
|
let puncs = json!([{
|
||||||
"name": "enum_ref_test",
|
"name": "enum_ref_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"request": {
|
"schemas": [{
|
||||||
|
"$id": "enum_ref_test.request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"priority": { "$ref": "task_priority" }
|
"priority": { "$ref": "task_priority" }
|
||||||
},
|
},
|
||||||
"required": ["priority"]
|
"required": ["priority"]
|
||||||
}
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
@ -669,34 +691,40 @@ pub fn punc_local_refs_schemas() -> JsonB {
|
|||||||
{
|
{
|
||||||
"name": "punc_with_local_ref_test",
|
"name": "punc_with_local_ref_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"schemas": [{
|
"schemas": [
|
||||||
"$id": "local_address",
|
{
|
||||||
"type": "object",
|
"$id": "local_address",
|
||||||
"properties": {
|
"type": "object",
|
||||||
"street": { "type": "string" },
|
"properties": {
|
||||||
"city": { "type": "string" }
|
"street": { "type": "string" },
|
||||||
|
"city": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["street", "city"]
|
||||||
},
|
},
|
||||||
"required": ["street", "city"]
|
{
|
||||||
}],
|
"$id": "punc_with_local_ref_test.request",
|
||||||
"request": {
|
"$ref": "local_address"
|
||||||
"$ref": "local_address"
|
}
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "punc_with_local_ref_to_global_test",
|
"name": "punc_with_local_ref_to_global_test",
|
||||||
"public": false,
|
"public": false,
|
||||||
"schemas": [{
|
"schemas": [
|
||||||
"$id": "local_user_with_thing",
|
{
|
||||||
"type": "object",
|
"$id": "local_user_with_thing",
|
||||||
"properties": {
|
"type": "object",
|
||||||
"user_name": { "type": "string" },
|
"properties": {
|
||||||
"thing": { "$ref": "global_thing" }
|
"user_name": { "type": "string" },
|
||||||
|
"thing": { "$ref": "global_thing" }
|
||||||
|
},
|
||||||
|
"required": ["user_name", "thing"]
|
||||||
},
|
},
|
||||||
"required": ["user_name", "thing"]
|
{
|
||||||
}],
|
"$id": "punc_with_local_ref_to_global_test.request",
|
||||||
"request": {
|
"$ref": "local_user_with_thing"
|
||||||
"$ref": "local_user_with_thing"
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -731,5 +759,47 @@ pub fn title_override_schemas() -> JsonB {
|
|||||||
|
|
||||||
let puncs = json!([]);
|
let puncs = json!([]);
|
||||||
|
|
||||||
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_matching_schemas() -> JsonB {
|
||||||
|
let enums = json!([]);
|
||||||
|
let types = json!([
|
||||||
|
{
|
||||||
|
"name": "entity",
|
||||||
|
"schemas": [{
|
||||||
|
"$id": "entity",
|
||||||
|
"type": "object",
|
||||||
|
"properties": { "type": { "type": "string" }, "name": { "type": "string" } },
|
||||||
|
"required": ["type", "name"]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "job",
|
||||||
|
"schemas": [{
|
||||||
|
"$id": "job",
|
||||||
|
"$ref": "entity",
|
||||||
|
"properties": { "job_id": { "type": "string" } },
|
||||||
|
"required": ["job_id"]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "super_job",
|
||||||
|
"schemas": [
|
||||||
|
{
|
||||||
|
"$id": "super_job",
|
||||||
|
"$ref": "job",
|
||||||
|
"properties": { "manager_id": { "type": "string" } },
|
||||||
|
"required": ["manager_id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$id": "super_job.short",
|
||||||
|
"$ref": "super_job",
|
||||||
|
"properties": { "name": { "maxLength": 10 } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
let puncs = json!([]);
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
}
|
}
|
||||||
69
src/tests.rs
69
src/tests.rs
@ -506,6 +506,7 @@ fn test_validate_property_merging() {
|
|||||||
// entity (id, name) + user (password) + person (first_name, last_name)
|
// entity (id, name) + user (password) + person (first_name, last_name)
|
||||||
|
|
||||||
let valid_person_with_all_properties = json!({
|
let valid_person_with_all_properties = json!({
|
||||||
|
"type": "person", // Added to satisfy new type check
|
||||||
// From entity
|
// From entity
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
"name": "John Doe",
|
"name": "John Doe",
|
||||||
@ -523,6 +524,7 @@ fn test_validate_property_merging() {
|
|||||||
|
|
||||||
// Test that properties validate according to their schema definitions across the chain
|
// Test that properties validate according to their schema definitions across the chain
|
||||||
let invalid_mixed_properties = json!({
|
let invalid_mixed_properties = json!({
|
||||||
|
"type": "person", // Added to satisfy new type check
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
"name": "John Doe",
|
"name": "John Doe",
|
||||||
"password": "short", // Too short from user schema
|
"password": "short", // Too short from user schema
|
||||||
@ -546,15 +548,13 @@ fn test_validate_required_merging() {
|
|||||||
// user: ["password"] (conditional when type=user)
|
// user: ["password"] (conditional when type=user)
|
||||||
// person: ["first_name", "last_name"] (conditional when type=person)
|
// person: ["first_name", "last_name"] (conditional when type=person)
|
||||||
|
|
||||||
let missing_all_required = json!({});
|
let missing_all_required = json!({ "type": "person" }); // Add type to pass initial check
|
||||||
|
|
||||||
let result = validate_json_schema("person", jsonb(missing_all_required));
|
let result = validate_json_schema("person", jsonb(missing_all_required));
|
||||||
// Should fail for all required fields across inheritance chain
|
// Should fail for all required fields across inheritance chain, except for the conditional 'password'
|
||||||
assert_error_count(&result, 6); // id, type, created_by, password, first_name, last_name
|
assert_error_count(&result, 4); // id, created_by, first_name, last_name
|
||||||
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/id");
|
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/id");
|
||||||
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/type");
|
|
||||||
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/created_by");
|
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/created_by");
|
||||||
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/password");
|
|
||||||
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/first_name");
|
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/first_name");
|
||||||
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/last_name");
|
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/last_name");
|
||||||
|
|
||||||
@ -777,14 +777,69 @@ fn test_validate_title_override() {
|
|||||||
// Test that a schema with an overridden title still inherits validation keywords correctly.
|
// Test that a schema with an overridden title still inherits validation keywords correctly.
|
||||||
|
|
||||||
// This instance is valid because it provides the 'name' required by the base schema.
|
// This instance is valid because it provides the 'name' required by the base schema.
|
||||||
let valid_instance = json!({ "name": "Test Name" });
|
let valid_instance = json!({ "type": "override_with_title", "name": "Test Name" });
|
||||||
let result_valid = validate_json_schema("override_with_title", jsonb(valid_instance));
|
let result_valid = validate_json_schema("override_with_title", jsonb(valid_instance));
|
||||||
assert_success(&result_valid);
|
assert_success(&result_valid);
|
||||||
|
|
||||||
// This instance is invalid because it's missing the 'name' required by the base schema.
|
// This instance is invalid because it's missing the 'name' required by the base schema.
|
||||||
// This proves that validation keywords are inherited even when metadata keywords are overridden.
|
// This proves that validation keywords are inherited even when metadata keywords are overridden.
|
||||||
let invalid_instance = json!({});
|
let invalid_instance = json!({ "type": "override_with_title" });
|
||||||
let result_invalid = validate_json_schema("override_with_title", jsonb(invalid_instance));
|
let result_invalid = validate_json_schema("override_with_title", jsonb(invalid_instance));
|
||||||
assert_error_count(&result_invalid, 1);
|
assert_error_count(&result_invalid, 1);
|
||||||
assert_has_error(&result_invalid, "REQUIRED_FIELD_MISSING", "/name");
|
assert_has_error(&result_invalid, "REQUIRED_FIELD_MISSING", "/name");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pg_test]
|
||||||
|
fn test_validate_type_matching() {
|
||||||
|
let cache_result = type_matching_schemas();
|
||||||
|
assert_success(&cache_result);
|
||||||
|
|
||||||
|
// 1. Test 'job' which extends 'entity'
|
||||||
|
let valid_job = json!({
|
||||||
|
"type": "job",
|
||||||
|
"name": "my job",
|
||||||
|
"job_id": "job123"
|
||||||
|
});
|
||||||
|
let result_valid_job = validate_json_schema("job", jsonb(valid_job));
|
||||||
|
assert_success(&result_valid_job);
|
||||||
|
|
||||||
|
let invalid_job = json!({
|
||||||
|
"type": "not_job",
|
||||||
|
"name": "my job",
|
||||||
|
"job_id": "job123"
|
||||||
|
});
|
||||||
|
let result_invalid_job = validate_json_schema("job", jsonb(invalid_job));
|
||||||
|
assert_error_count(&result_invalid_job, 1);
|
||||||
|
assert_has_error(&result_invalid_job, "TYPE_MISMATCH", "/type");
|
||||||
|
|
||||||
|
// 2. Test 'super_job' which extends 'job'
|
||||||
|
let valid_super_job = json!({
|
||||||
|
"type": "super_job",
|
||||||
|
"name": "my super job",
|
||||||
|
"job_id": "job123",
|
||||||
|
"manager_id": "mgr1"
|
||||||
|
});
|
||||||
|
let result_valid_super_job = validate_json_schema("super_job", jsonb(valid_super_job));
|
||||||
|
assert_success(&result_valid_super_job);
|
||||||
|
|
||||||
|
// 3. Test 'super_job.short' which should still expect type 'super_job'
|
||||||
|
let valid_short_super_job = json!({
|
||||||
|
"type": "super_job",
|
||||||
|
"name": "short", // maxLength: 10
|
||||||
|
"job_id": "job123",
|
||||||
|
"manager_id": "mgr1"
|
||||||
|
});
|
||||||
|
let result_valid_short = validate_json_schema("super_job.short", jsonb(valid_short_super_job));
|
||||||
|
assert_success(&result_valid_short);
|
||||||
|
|
||||||
|
let invalid_short_super_job = json!({
|
||||||
|
"type": "job", // Should be 'super_job'
|
||||||
|
"name": "short",
|
||||||
|
"job_id": "job123",
|
||||||
|
"manager_id": "mgr1"
|
||||||
|
});
|
||||||
|
let result_invalid_short = validate_json_schema("super_job.short", jsonb(invalid_short_super_job));
|
||||||
|
assert_error_count(&result_invalid_short, 1);
|
||||||
|
let error = find_error_with_code_and_path(&result_invalid_short, "TYPE_MISMATCH", "/type");
|
||||||
|
assert_error_message_contains(error, "Instance type 'job' does not match expected type 'super_job'");
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user