library test suite for drop validation, fixed drop return structures

This commit is contained in:
2026-02-19 20:14:21 -05:00
parent 55b93d9957
commit ad78896f72
8 changed files with 160 additions and 41 deletions

View File

@ -24,12 +24,12 @@ fn main() {
println!("cargo:rerun-if-changed=tests/fixtures"); println!("cargo:rerun-if-changed=tests/fixtures");
println!("cargo:rerun-if-changed=Cargo.toml"); println!("cargo:rerun-if-changed=Cargo.toml");
// File 1: src/tests.rs for #[pg_test] // File 1: src/tests/fixtures.rs for #[pg_test]
let pg_dest_path = Path::new("src/tests.rs"); let pg_dest_path = Path::new("src/tests/fixtures.rs");
let mut pg_file = File::create(&pg_dest_path).unwrap(); let mut pg_file = File::create(&pg_dest_path).unwrap();
// File 2: tests/tests.rs for standard #[test] integration // File 2: tests/fixtures.rs for standard #[test] integration
let std_dest_path = Path::new("tests/tests.rs"); let std_dest_path = Path::new("tests/fixtures.rs");
let mut std_file = File::create(&std_dest_path).unwrap(); let mut std_file = File::create(&std_dest_path).unwrap();
// Write headers // Write headers

2
flow
View File

@ -100,7 +100,7 @@ install() {
test() { test() {
info "Running jspg tests..." info "Running jspg tests..."
cargo test --test tests "$@" || return $? cargo test --tests "$@" || return $?
} }
clean() { clean() {

View File

@ -13,7 +13,7 @@ pub struct Drop {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub response: Option<Value>, pub response: Option<Value>,
#[serde(default)] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<Error>, pub errors: Vec<Error>,
} }
@ -29,7 +29,7 @@ impl Drop {
pub fn success() -> Self { pub fn success() -> Self {
Self { Self {
type_: "drop".to_string(), type_: "drop".to_string(),
response: Some(serde_json::json!({ "result": "success" })), // Or appropriate success response response: Some(serde_json::json!("success")),
errors: vec![], errors: vec![],
} }
} }
@ -53,8 +53,6 @@ impl Drop {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Error { pub struct Error {
#[serde(skip_serializing_if = "Option::is_none")]
pub punc: Option<String>,
pub code: String, pub code: String,
pub message: String, pub message: String,
pub details: ErrorDetails, pub details: ErrorDetails,

View File

@ -25,7 +25,7 @@ lazy_static::lazy_static! {
} }
#[pg_extern(strict)] #[pg_extern(strict)]
fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB { pub fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
// 1. Build a new Registry LOCALLY (on stack) // 1. Build a new Registry LOCALLY (on stack)
let mut registry = registry::Registry::new(); let mut registry = registry::Registry::new();
@ -107,11 +107,12 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
*lock = Some(new_arc); *lock = Some(new_arc);
} }
JsonB(json!({ "response": "success" })) let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
} }
#[pg_extern(strict, parallel_safe)] #[pg_extern(strict, parallel_safe)]
fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB { pub fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot // 1. Acquire Snapshot
let validator_arc = { let validator_arc = {
let lock = GLOBAL_VALIDATOR.read().unwrap(); let lock = GLOBAL_VALIDATOR.read().unwrap();
@ -135,7 +136,6 @@ fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
.errors .errors
.into_iter() .into_iter()
.map(|e| crate::drop::Error { .map(|e| crate::drop::Error {
punc: None,
code: e.code, code: e.code,
message: e.message, message: e.message,
details: crate::drop::ErrorDetails { path: e.path }, details: crate::drop::ErrorDetails { path: e.path },
@ -148,7 +148,6 @@ fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
Err(e) => { Err(e) => {
// Schema Not Found or other fatal error // Schema Not Found or other fatal error
let error = crate::drop::Error { let error = crate::drop::Error {
punc: None,
code: e.code, code: e.code,
message: e.message, message: e.message,
details: crate::drop::ErrorDetails { path: e.path }, details: crate::drop::ErrorDetails { path: e.path },
@ -158,19 +157,20 @@ fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
} }
} }
} else { } else {
JsonB(json!({ let error = crate::drop::Error {
"punc": null, code: "VALIDATOR_NOT_INITIALIZED".to_string(),
"errors": [{ message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(),
"code": "VALIDATOR_NOT_INITIALIZED", details: crate::drop::ErrorDetails {
"message": "JSON Schemas have not been cached yet. Run cache_json_schemas()", path: "".to_string(),
"details": { "path": "" } },
}] };
})) let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
} }
} }
#[pg_extern(strict, parallel_safe)] #[pg_extern(strict, parallel_safe)]
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB { pub fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot // 1. Acquire Snapshot
let validator_arc = { let validator_arc = {
let lock = GLOBAL_VALIDATOR.read().unwrap(); let lock = GLOBAL_VALIDATOR.read().unwrap();
@ -189,7 +189,6 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
.errors .errors
.into_iter() .into_iter()
.map(|e| crate::drop::Error { .map(|e| crate::drop::Error {
punc: None,
code: e.code, code: e.code,
message: e.message, message: e.message,
details: crate::drop::ErrorDetails { path: e.path }, details: crate::drop::ErrorDetails { path: e.path },
@ -201,7 +200,6 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
} }
Err(e) => { Err(e) => {
let error = crate::drop::Error { let error = crate::drop::Error {
punc: None,
code: e.code, code: e.code,
message: e.message, message: e.message,
details: crate::drop::ErrorDetails { path: e.path }, details: crate::drop::ErrorDetails { path: e.path },
@ -211,19 +209,20 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
} }
} }
} else { } else {
JsonB(json!({ let error = crate::drop::Error {
"punc": null, code: "VALIDATOR_NOT_INITIALIZED".to_string(),
"errors": [{ message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(),
"code": "VALIDATOR_NOT_INITIALIZED", details: crate::drop::ErrorDetails {
"message": "JSON Schemas have not been cached yet. Run cache_json_schemas()", path: "".to_string(),
"details": { "path": "" } },
}] };
})) let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
} }
} }
#[pg_extern(strict, parallel_safe)] #[pg_extern(strict, parallel_safe)]
fn json_schema_cached(schema_id: &str) -> bool { pub fn json_schema_cached(schema_id: &str) -> bool {
if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() { if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() {
match validator.validate(schema_id, &serde_json::Value::Null) { match validator.validate(schema_id, &serde_json::Value::Null) {
Err(e) if e.code == "SCHEMA_NOT_FOUND" => false, Err(e) if e.code == "SCHEMA_NOT_FOUND" => false,
@ -235,18 +234,23 @@ fn json_schema_cached(schema_id: &str) -> bool {
} }
#[pg_extern(strict)] #[pg_extern(strict)]
fn clear_json_schemas() -> JsonB { pub fn clear_json_schemas() -> JsonB {
let mut lock = GLOBAL_VALIDATOR.write().unwrap(); let mut lock = GLOBAL_VALIDATOR.write().unwrap();
*lock = None; *lock = None;
JsonB(json!({ "response": "success" })) let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
} }
#[pg_extern(strict, parallel_safe)] #[pg_extern(strict, parallel_safe)]
fn show_json_schemas() -> JsonB { pub fn show_json_schemas() -> JsonB {
if let Some(_validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() { if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() {
JsonB(json!({ "response": "success", "status": "active" })) let mut keys = validator.get_schema_ids();
keys.sort();
let drop = crate::drop::Drop::success_with_val(json!(keys));
JsonB(serde_json::to_value(drop).unwrap())
} else { } else {
JsonB(json!({ "response": "success", "status": "empty" })) let drop = crate::drop::Drop::success_with_val(json!([]));
JsonB(serde_json::to_value(drop).unwrap())
} }
} }
@ -254,7 +258,7 @@ fn show_json_schemas() -> JsonB {
#[pg_schema] #[pg_schema]
mod tests { mod tests {
use pgrx::prelude::*; use pgrx::prelude::*;
include!("tests.rs"); include!("tests/fixtures.rs");
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1279,6 +1279,10 @@ impl Validator {
Self { registry } Self { registry }
} }
pub fn get_schema_ids(&self) -> Vec<String> {
self.registry.schemas.keys().cloned().collect()
}
pub fn check_type(t: &str, val: &Value) -> bool { pub fn check_type(t: &str, val: &Value) -> bool {
if let Value::String(s) = val { if let Value::String(s) = val {
if s.is_empty() { if s.is_empty() {

113
tests/lib.rs Normal file
View File

@ -0,0 +1,113 @@
use jspg::*;
use pgrx::JsonB;
use serde_json::json;
#[test]
fn test_library_api() {
// 1. Initially, schemas are not cached.
assert!(!json_schema_cached("test_schema"));
// Expected uninitialized drop format: errors + null response
let uninitialized_drop = validate_json_schema("test_schema", JsonB(json!({})));
assert_eq!(
uninitialized_drop.0,
json!({
"type": "drop",
"errors": [{
"code": "VALIDATOR_NOT_INITIALIZED",
"message": "JSON Schemas have not been cached yet. Run cache_json_schemas()",
"details": { "path": "" }
}]
})
);
// 2. Cache schemas
let puncs = json!([]);
let types = json!([{
"schemas": [{
"$id": "test_schema",
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}]
}]);
let enums = json!([]);
let cache_drop = cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs));
assert_eq!(
cache_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
// 3. Check schemas are cached
assert!(json_schema_cached("test_schema"));
let show_drop = show_json_schemas();
assert_eq!(
show_drop.0,
json!({
"type": "drop",
"response": ["test_schema"]
})
);
// 4. Validate Happy Path
let happy_drop = validate_json_schema("test_schema", JsonB(json!({"name": "Neo"})));
assert_eq!(
happy_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
// 5. Validate Unhappy Path
let unhappy_drop = validate_json_schema("test_schema", JsonB(json!({"wrong": "data"})));
assert_eq!(
unhappy_drop.0,
json!({
"type": "drop",
"errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"message": "Missing name",
"details": { "path": "/name" }
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"message": "Unexpected property 'wrong'",
"details": { "path": "/wrong" }
}
]
})
);
// 6. Mask Happy Path
let mask_drop = mask_json_schema(
"test_schema",
JsonB(json!({"name": "Neo", "extra": "data"})),
);
assert_eq!(
mask_drop.0,
json!({
"type": "drop",
"response": {"name": "Neo"}
})
);
// 7. Clear Schemas
let clear_drop = clear_json_schemas();
assert_eq!(
clear_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
assert!(!json_schema_cached("test_schema"));
}