From ad78896f722a5f51a665ebf4bb4b7fcff4a0ac9d Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Thu, 19 Feb 2026 20:14:21 -0500 Subject: [PATCH] library test suite for drop validation, fixed drop return structures --- build.rs | 8 +- flow | 2 +- src/drop.rs | 6 +- src/lib.rs | 68 +++++++++-------- src/{tests.rs => tests/fixtures.rs} | 0 src/validator.rs | 4 + tests/{tests.rs => fixtures.rs} | 0 tests/lib.rs | 113 ++++++++++++++++++++++++++++ 8 files changed, 160 insertions(+), 41 deletions(-) rename src/{tests.rs => tests/fixtures.rs} (100%) rename tests/{tests.rs => fixtures.rs} (100%) create mode 100644 tests/lib.rs diff --git a/build.rs b/build.rs index 8cf7781..7c00a7a 100644 --- a/build.rs +++ b/build.rs @@ -24,12 +24,12 @@ fn main() { println!("cargo:rerun-if-changed=tests/fixtures"); println!("cargo:rerun-if-changed=Cargo.toml"); - // File 1: src/tests.rs for #[pg_test] - let pg_dest_path = Path::new("src/tests.rs"); + // File 1: src/tests/fixtures.rs for #[pg_test] + let pg_dest_path = Path::new("src/tests/fixtures.rs"); let mut pg_file = File::create(&pg_dest_path).unwrap(); - // File 2: tests/tests.rs for standard #[test] integration - let std_dest_path = Path::new("tests/tests.rs"); + // File 2: tests/fixtures.rs for standard #[test] integration + let std_dest_path = Path::new("tests/fixtures.rs"); let mut std_file = File::create(&std_dest_path).unwrap(); // Write headers diff --git a/flow b/flow index dddf5e1..6623ebd 100755 --- a/flow +++ b/flow @@ -100,7 +100,7 @@ install() { test() { info "Running jspg tests..." - cargo test --test tests "$@" || return $? + cargo test --tests "$@" || return $? } clean() { diff --git a/src/drop.rs b/src/drop.rs index 1b8c7ad..06bf47e 100644 --- a/src/drop.rs +++ b/src/drop.rs @@ -13,7 +13,7 @@ pub struct Drop { #[serde(skip_serializing_if = "Option::is_none")] pub response: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub errors: Vec, } @@ -29,7 +29,7 @@ impl Drop { pub fn success() -> Self { Self { type_: "drop".to_string(), - response: Some(serde_json::json!({ "result": "success" })), // Or appropriate success response + response: Some(serde_json::json!("success")), errors: vec![], } } @@ -53,8 +53,6 @@ impl Drop { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Error { - #[serde(skip_serializing_if = "Option::is_none")] - pub punc: Option, pub code: String, pub message: String, pub details: ErrorDetails, diff --git a/src/lib.rs b/src/lib.rs index 43eb40e..ce163eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ lazy_static::lazy_static! { } #[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) 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); } - JsonB(json!({ "response": "success" })) + let drop = crate::drop::Drop::success(); + JsonB(serde_json::to_value(drop).unwrap()) } #[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 let validator_arc = { let lock = GLOBAL_VALIDATOR.read().unwrap(); @@ -135,7 +136,6 @@ fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB { .errors .into_iter() .map(|e| crate::drop::Error { - punc: None, code: e.code, message: e.message, details: crate::drop::ErrorDetails { path: e.path }, @@ -148,7 +148,6 @@ fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB { Err(e) => { // Schema Not Found or other fatal error let error = crate::drop::Error { - punc: None, code: e.code, message: e.message, details: crate::drop::ErrorDetails { path: e.path }, @@ -158,19 +157,20 @@ fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB { } } } else { - JsonB(json!({ - "punc": null, - "errors": [{ - "code": "VALIDATOR_NOT_INITIALIZED", - "message": "JSON Schemas have not been cached yet. Run cache_json_schemas()", - "details": { "path": "" } - }] - })) + let error = crate::drop::Error { + code: "VALIDATOR_NOT_INITIALIZED".to_string(), + message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(), + details: crate::drop::ErrorDetails { + path: "".to_string(), + }, + }; + let drop = crate::drop::Drop::with_errors(vec![error]); + JsonB(serde_json::to_value(drop).unwrap()) } } #[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 let validator_arc = { let lock = GLOBAL_VALIDATOR.read().unwrap(); @@ -189,7 +189,6 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB { .errors .into_iter() .map(|e| crate::drop::Error { - punc: None, code: e.code, message: e.message, details: crate::drop::ErrorDetails { path: e.path }, @@ -201,7 +200,6 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB { } Err(e) => { let error = crate::drop::Error { - punc: None, code: e.code, message: e.message, details: crate::drop::ErrorDetails { path: e.path }, @@ -211,19 +209,20 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB { } } } else { - JsonB(json!({ - "punc": null, - "errors": [{ - "code": "VALIDATOR_NOT_INITIALIZED", - "message": "JSON Schemas have not been cached yet. Run cache_json_schemas()", - "details": { "path": "" } - }] - })) + let error = crate::drop::Error { + code: "VALIDATOR_NOT_INITIALIZED".to_string(), + message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(), + details: crate::drop::ErrorDetails { + path: "".to_string(), + }, + }; + let drop = crate::drop::Drop::with_errors(vec![error]); + JsonB(serde_json::to_value(drop).unwrap()) } } #[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() { match validator.validate(schema_id, &serde_json::Value::Null) { Err(e) if e.code == "SCHEMA_NOT_FOUND" => false, @@ -235,18 +234,23 @@ fn json_schema_cached(schema_id: &str) -> bool { } #[pg_extern(strict)] -fn clear_json_schemas() -> JsonB { +pub fn clear_json_schemas() -> JsonB { let mut lock = GLOBAL_VALIDATOR.write().unwrap(); *lock = None; - JsonB(json!({ "response": "success" })) + let drop = crate::drop::Drop::success(); + JsonB(serde_json::to_value(drop).unwrap()) } #[pg_extern(strict, parallel_safe)] -fn show_json_schemas() -> JsonB { - if let Some(_validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() { - JsonB(json!({ "response": "success", "status": "active" })) +pub fn show_json_schemas() -> JsonB { + if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() { + 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 { - 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] mod tests { use pgrx::prelude::*; - include!("tests.rs"); + include!("tests/fixtures.rs"); } #[cfg(test)] diff --git a/src/tests.rs b/src/tests/fixtures.rs similarity index 100% rename from src/tests.rs rename to src/tests/fixtures.rs diff --git a/src/validator.rs b/src/validator.rs index 1145912..e398f42 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -1279,6 +1279,10 @@ impl Validator { Self { registry } } + pub fn get_schema_ids(&self) -> Vec { + self.registry.schemas.keys().cloned().collect() + } + pub fn check_type(t: &str, val: &Value) -> bool { if let Value::String(s) = val { if s.is_empty() { diff --git a/tests/tests.rs b/tests/fixtures.rs similarity index 100% rename from tests/tests.rs rename to tests/fixtures.rs diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..1d5f130 --- /dev/null +++ b/tests/lib.rs @@ -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")); +}