Files
jspg/src/lib.rs
2025-04-15 23:30:47 -04:00

170 lines
4.9 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 } => { // Prefix url with _
json!({
"message": format!("Schema '{}' failed validation against its metaschema: {}", schema_id, src),
"schema_path": schema_path,
"error": format!("{:?}", src),
})
}
_ => {
let _error_type = format!("{:?}", e).split('(').next().unwrap_or("Unknown").to_string(); // Prefix error_type with _
json!({
"message": format!("Schema '{}' compilation failed: {}", schema_id, e),
"schema_path": schema_path,
"error": format!("{:?}", e),
})
}
};
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) => {
let error = format_validation_error(&validation_error);
JsonB(json!({
"success": false,
"error": error
}))
}
}
}
}
}
fn format_validation_error(error: &ValidationError) -> Value {
let nested_errors: Vec<Value> = error.causes.iter().map(format_validation_error).collect();
// Use specific message for leaf errors, generic for containers
let message = if error.causes.is_empty() {
let default_message = format!("{}", error); // Use boon's default message for specific errors
// Try to strip the "at '<path>': " prefix
if let Some(start_index) = default_message.find("': ") {
default_message[start_index + 3..].to_string()
} else {
default_message // Fallback if pattern not found
}
} else {
"Validation failed due to nested errors".to_string() // Generic message for container errors
};
json!({
"message": message,
"schema_path": error.schema_url.to_string(), // Use schema_url directly
"instance_path": error.instance_location.to_string(),
"error": nested_errors // Recursively format nested errors
})
}
#[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");
}