217 lines
7.0 KiB
Rust
217 lines
7.0 KiB
Rust
use pgrx::*;
|
|
|
|
pg_module_magic!();
|
|
|
|
use serde_json::{json, Value};
|
|
use std::{collections::HashMap, sync::RwLock};
|
|
use boon::{Compiler, Schemas, ValidationError, SchemaIndex, CompileError};
|
|
use lazy_static::lazy_static;
|
|
|
|
struct BoonCache {
|
|
schemas: Schemas,
|
|
id_to_index: HashMap<String, SchemaIndex>,
|
|
}
|
|
|
|
lazy_static! {
|
|
static ref SCHEMA_CACHE: RwLock<BoonCache> = RwLock::new(BoonCache {
|
|
schemas: Schemas::new(),
|
|
id_to_index: HashMap::new(),
|
|
});
|
|
}
|
|
|
|
#[pg_extern(strict)]
|
|
fn cache_json_schema(schema_id: &str, schema: JsonB) -> JsonB {
|
|
let mut cache = SCHEMA_CACHE.write().unwrap();
|
|
let schema_value: Value = schema.0;
|
|
let schema_path = format!("urn:{}", schema_id);
|
|
|
|
let mut compiler = Compiler::new();
|
|
compiler.enable_format_assertions();
|
|
|
|
// Use schema_path when adding the resource
|
|
if let Err(e) = compiler.add_resource(&schema_path, schema_value.clone()) {
|
|
return JsonB(json!({
|
|
"success": false,
|
|
"error": {
|
|
"message": format!("Failed to add schema resource '{}': {}", schema_id, e),
|
|
"schema_path": schema_path
|
|
}
|
|
}));
|
|
}
|
|
|
|
// Use schema_path when compiling
|
|
match compiler.compile(&schema_path, &mut cache.schemas) {
|
|
Ok(sch_index) => {
|
|
// Store the index using the original schema_id as the key
|
|
cache.id_to_index.insert(schema_id.to_string(), sch_index);
|
|
JsonB(json!({ "success": true }))
|
|
}
|
|
Err(e) => {
|
|
let error = match &e {
|
|
CompileError::ValidationError { url: _url, src } => {
|
|
// Collect leaf errors from the meta-schema validation failure
|
|
let mut error_list = Vec::new();
|
|
collect_leaf_errors(src, &mut error_list);
|
|
// Filter and deduplicate errors, returning as a single JSON Value (Array)
|
|
json!(filter_boon_errors(error_list))
|
|
}
|
|
_ => {
|
|
// Keep existing handling for other compilation errors
|
|
let _error_type = format!("{:?}", e).split('(').next().unwrap_or("Unknown").to_string();
|
|
json!({
|
|
"message": format!("Schema '{}' compilation failed: {}", schema_id, e),
|
|
"schema_path": schema_path,
|
|
"detail": format!("{:?}", e),
|
|
})
|
|
}
|
|
};
|
|
// Ensure the outer structure remains { success: false, error: ... }
|
|
JsonB(json!({
|
|
"success": false,
|
|
"error": error
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pg_extern(strict, parallel_safe)]
|
|
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
|
|
let cache = SCHEMA_CACHE.read().unwrap();
|
|
|
|
// Lookup uses the original schema_id
|
|
match cache.id_to_index.get(schema_id) {
|
|
None => JsonB(json!({
|
|
"success": false,
|
|
"error": {
|
|
"message": format!("Schema with id '{}' not found in cache", schema_id)
|
|
}
|
|
})),
|
|
Some(sch_index) => {
|
|
let instance_value: Value = instance.0;
|
|
match cache.schemas.validate(&instance_value, *sch_index) {
|
|
Ok(_) => JsonB(json!({ "success": true })),
|
|
Err(validation_error) => {
|
|
// Directly use the result of format_validation_error
|
|
// which now includes the top-level success indicator and flat error list
|
|
let mut error_list = Vec::new();
|
|
collect_leaf_errors(&validation_error, &mut error_list);
|
|
JsonB(json!({
|
|
"success": false,
|
|
"error": filter_boon_errors(error_list) // Filter and deduplicate errors
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recursively collects leaf errors into a flat list
|
|
fn collect_leaf_errors(error: &ValidationError, errors_list: &mut Vec<Value>) {
|
|
if error.causes.is_empty() {
|
|
let default_message = format!("{}", error);
|
|
let message = if let Some(start_index) = default_message.find("': ") {
|
|
default_message[start_index + 3..].to_string()
|
|
} else {
|
|
default_message
|
|
};
|
|
|
|
errors_list.push(json!({
|
|
"message": message,
|
|
"schema_path": error.schema_url.to_string(),
|
|
"instance_path": error.instance_location.to_string(),
|
|
}));
|
|
} else {
|
|
for cause in &error.causes {
|
|
collect_leaf_errors(cause, errors_list);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filters collected errors, removing structural noise and then deduplicating by instance_path
|
|
fn filter_boon_errors(raw_errors: Vec<Value>) -> Vec<Value> {
|
|
use std::collections::HashMap;
|
|
use std::collections::hash_map::Entry;
|
|
|
|
// Define schema keywords that indicate structural paths, not instance paths
|
|
let structural_path_segments = [
|
|
"/allOf/", "/anyOf/", "/oneOf/",
|
|
"/if/", "/then/", "/else/",
|
|
"/not/"
|
|
// Note: "/properties/" and "/items/" are generally valid,
|
|
// but might appear spuriously in boon's paths for complex types.
|
|
// We exclude only the explicitly logical/combinatorial ones for now.
|
|
];
|
|
|
|
// 1. Filter out errors with instance_paths containing structural segments
|
|
let plausible_errors: Vec<Value> = raw_errors.into_iter().filter(|error_value| {
|
|
if let Some(instance_path_value) = error_value.get("instance_path") {
|
|
if let Some(instance_path_str) = instance_path_value.as_str() {
|
|
// Keep if NONE of the structural segments are present
|
|
!structural_path_segments.iter().any(|&segment| instance_path_str.contains(segment))
|
|
} else {
|
|
false // Invalid instance_path type, filter out
|
|
}
|
|
} else {
|
|
false // No instance_path field, filter out
|
|
}
|
|
}).collect();
|
|
|
|
// 2. Deduplicate the remaining plausible errors by instance_path
|
|
let mut unique_errors: HashMap<String, Value> = HashMap::new();
|
|
for error_value in plausible_errors {
|
|
if let Some(instance_path_value) = error_value.get("instance_path") {
|
|
if let Some(instance_path_str) = instance_path_value.as_str() {
|
|
if let Entry::Vacant(entry) = unique_errors.entry(instance_path_str.to_string()) {
|
|
entry.insert(error_value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect the unique errors
|
|
unique_errors.into_values().collect()
|
|
}
|
|
|
|
#[pg_extern(strict, parallel_safe)]
|
|
fn json_schema_cached(schema_id: &str) -> bool {
|
|
let cache = SCHEMA_CACHE.read().unwrap();
|
|
cache.id_to_index.contains_key(schema_id)
|
|
}
|
|
|
|
#[pg_extern(strict)]
|
|
fn clear_json_schemas() {
|
|
let mut cache = SCHEMA_CACHE.write().unwrap();
|
|
*cache = BoonCache {
|
|
schemas: Schemas::new(),
|
|
id_to_index: HashMap::new(),
|
|
};
|
|
}
|
|
|
|
#[pg_extern(strict, parallel_safe)]
|
|
fn show_json_schemas() -> Vec<String> {
|
|
let cache = SCHEMA_CACHE.read().unwrap();
|
|
let ids: Vec<String> = cache.id_to_index.keys().cloned().collect();
|
|
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 {
|
|
include!("tests.rs");
|
|
} |