Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55b93d9957 | |||
| 7ec6e09ae0 | |||
| 9d9c6d2c06 | |||
| 12e952fa94 | |||
| 776a912098 |
49
.agent/workflows/jspg.md
Normal file
49
.agent/workflows/jspg.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
description: jspg work preparation
|
||||
---
|
||||
|
||||
This workflow will get you up-to-speed on the JSPG custom json-schema-based cargo pgrx postgres validation extension. Everything you read will be in the jspg directory/project.
|
||||
|
||||
Read over this entire workflow and commit to every section of work in a task list, so that you don't stop half way through before reviewing all of the directories and files mentioned. Do not ask for confirmation after generating this task list and proceed through all sections in your list.
|
||||
|
||||
Please analyze the files and directories and do not use cat, find, or the terminal to discover or read in any of these files. Analyze every file mentioned. If a directory is mentioned or a /*, please analyze the directory, every single file at its root, and recursively analyze every subdirectory and every single file in every subdirectory to capture not just critical files, but the entirety of what is requested. I state again, DO NOT just review a cherry picking of files in any folder or wildcard specified. Review 100% of all files discovered recursively!
|
||||
|
||||
Section 1: Documentation
|
||||
|
||||
- GEMINI.md at the root
|
||||
|
||||
Section 2: Flow file for cmd interface
|
||||
|
||||
- flow at the root
|
||||
|
||||
Section 3: Source
|
||||
|
||||
- src/*
|
||||
|
||||
Section 4: Test Fixtures
|
||||
|
||||
- Just review some of the *.json files in tests/fixtures/*
|
||||
|
||||
Section 5: Build
|
||||
|
||||
- build.rs
|
||||
|
||||
Section 6: Cargo TOML
|
||||
|
||||
- Cargo.toml
|
||||
|
||||
Section 7: Some PUNC Syntax
|
||||
|
||||
Now, review some punc type and enum source in the api project with api/ these files:
|
||||
|
||||
- punc/sql/tables.sql
|
||||
- punc/sql/domains.sql
|
||||
- punc/sql/indexes.sql
|
||||
- punc/sql/functions/entity.sql
|
||||
- punc/sql/functions/puncs.sql
|
||||
- punc/sql/puncs/entity.sql
|
||||
- punc/sql/puncs/persons.sql
|
||||
- punc/sql/puncs/puncs.sql
|
||||
- punc/sql/puncs/job.sql
|
||||
|
||||
Now you are ready to help me work on this extension.
|
||||
@ -86,8 +86,9 @@ To support polymorphic fields (e.g., a field that accepts any "User" type), JSPG
|
||||
### 3. Strict by Default & Extensibility
|
||||
JSPG enforces a "Secure by Default" philosophy. All schemas are treated as if `unevaluatedProperties: false` (and `unevaluatedItems: false`) is set, unless explicitly overridden.
|
||||
|
||||
* **Strictness**: By default, any property in the instance data that is not explicitly defined in the schema causes a validation error. This prevents clients from sending undeclared fields.
|
||||
* **Extensibility (`extensible: true`)**: To allow additional, undefined properties, you must add `"extensible": true` to the schema. This is useful for types that are designed to be open for extension.
|
||||
* **Strictness**: By default, any property or array item in the instance data that is not explicitly defined in the schema causes a validation error. This prevents clients from sending undeclared fields or extra array elements.
|
||||
* **Extensibility (`extensible: true`)**: To allow a free-for-all of additional, undefined properties or extra array items, you must add `"extensible": true` to the schema. This globally disables the strictness check for that object or array, useful for types designed to be completely open.
|
||||
* **Structured Additional Properties (`additionalProperties: {...}`)**: Instead of a boolean free-for-all, you can define `additionalProperties` as a schema object (e.g., `{"type": "string"}`). This maintains strictness (no arbitrary keys) but allows any extra keys as long as their values match the defined structure.
|
||||
* **Ref Boundaries**: Strictness is reset when crossing `$ref` boundaries. The referenced schema's strictness is determined by its own definition (strict by default unless `extensible: true`), ignoring the caller's state.
|
||||
* **Inheritance**: Strictness is inherited. A schema extending a strict parent will also be strict unless it declares itself `extensible: true`. Conversely, a schema extending a loose parent will also be loose unless it declares itself `extensible: false`.
|
||||
|
||||
|
||||
53
build.rs
53
build.rs
@ -3,6 +3,23 @@ use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
fn to_safe_identifier(name: &str) -> String {
|
||||
let mut safe = String::new();
|
||||
for (i, c) in name.chars().enumerate() {
|
||||
if c.is_uppercase() {
|
||||
if i > 0 {
|
||||
safe.push('_');
|
||||
}
|
||||
safe.push(c.to_ascii_lowercase());
|
||||
} else if c == '-' || c == '.' {
|
||||
safe.push('_');
|
||||
} else {
|
||||
safe.push(c);
|
||||
}
|
||||
}
|
||||
safe
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=tests/fixtures");
|
||||
println!("cargo:rerun-if-changed=Cargo.toml");
|
||||
@ -18,23 +35,6 @@ fn main() {
|
||||
// Write headers
|
||||
writeln!(std_file, "use jspg::util;").unwrap();
|
||||
|
||||
// Helper for snake_case conversion
|
||||
// let _to_snake_case = |s: &str| -> String {
|
||||
// s.chars().fold(String::new(), |mut acc, c| {
|
||||
// if c.is_uppercase() {
|
||||
// if !acc.is_empty() {
|
||||
// acc.push('_');
|
||||
// }
|
||||
// acc.push(c.to_ascii_lowercase());
|
||||
// } else if c == '-' || c == ' ' || c == '.' || c == '/' || c == ':' {
|
||||
// acc.push('_');
|
||||
// } else if c.is_alphanumeric() {
|
||||
// acc.push(c);
|
||||
// }
|
||||
// acc
|
||||
// })
|
||||
// };
|
||||
|
||||
// Walk tests/fixtures directly
|
||||
let fixtures_path = "tests/fixtures";
|
||||
if Path::new(fixtures_path).exists() {
|
||||
@ -51,24 +51,7 @@ fn main() {
|
||||
if let Some(arr) = val.as_array() {
|
||||
for (i, _item) in arr.iter().enumerate() {
|
||||
// Use deterministic names: test_{filename}_{index}
|
||||
// We sanitize the filename to be a valid identifier
|
||||
// Use manual snake_case logic since we don't want to add a build-dependency just yet if not needed,
|
||||
// but `dynamicRef` -> `dynamic_ref` requires parsing.
|
||||
// Let's implement a simple camelToSnake helper.
|
||||
let mut safe_filename = String::new();
|
||||
for (i, c) in file_name.chars().enumerate() {
|
||||
if c.is_uppercase() {
|
||||
if i > 0 {
|
||||
safe_filename.push('_');
|
||||
}
|
||||
safe_filename.push(c.to_ascii_lowercase());
|
||||
} else if c == '-' || c == '.' {
|
||||
safe_filename.push('_');
|
||||
} else {
|
||||
safe_filename.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
let safe_filename = to_safe_identifier(file_name);
|
||||
let fn_name = format!("test_{}_{}", safe_filename, i);
|
||||
|
||||
// Write to src/tests.rs (PG Test)
|
||||
|
||||
12
flow
12
flow
@ -98,14 +98,9 @@ install() {
|
||||
fi
|
||||
}
|
||||
|
||||
test-jspg() {
|
||||
test() {
|
||||
info "Running jspg tests..."
|
||||
cargo pgrx test "pg${POSTGRES_VERSION}" "$@" || return $?
|
||||
}
|
||||
|
||||
test-validator() {
|
||||
info "Running validator tests..."
|
||||
cargo test -p boon --features "pgrx/pg${POSTGRES_VERSION}" "$@" || return $?
|
||||
cargo test --test tests "$@" || return $?
|
||||
}
|
||||
|
||||
clean() {
|
||||
@ -128,8 +123,7 @@ jspg-flow() {
|
||||
build) build; return $?;;
|
||||
install) install; return $?;;
|
||||
reinstall) clean && install; return $?;;
|
||||
test-jspg) test-jspg "${@:2}"; return $?;;
|
||||
test-validator) test-validator "${@:2}"; return $?;;
|
||||
test) test "${@:2}"; return $?;;
|
||||
clean) clean; return $?;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
2
flows
2
flows
Submodule flows updated: e154758056...404da626c7
@ -113,6 +113,9 @@ impl Compiler {
|
||||
Self::compile_recursive(Arc::make_mut(s));
|
||||
}
|
||||
}
|
||||
if let Some(add_props) = &mut schema.additional_properties {
|
||||
Self::compile_recursive(Arc::make_mut(add_props));
|
||||
}
|
||||
|
||||
// ... Recurse logic ...
|
||||
if let Some(items) = &mut schema.items {
|
||||
@ -323,6 +326,11 @@ impl Compiler {
|
||||
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
|
||||
}
|
||||
}
|
||||
if let Some(add_props) = &schema.additional_properties {
|
||||
let mut sub = child_pointer.clone();
|
||||
sub.push("additionalProperties".to_string());
|
||||
Self::compile_index(add_props, registry, current_base.clone(), sub);
|
||||
}
|
||||
if let Some(contains) = &schema.contains {
|
||||
let mut sub = child_pointer.clone();
|
||||
sub.push("contains".to_string());
|
||||
|
||||
@ -33,6 +33,8 @@ pub struct SchemaObject {
|
||||
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
|
||||
#[serde(rename = "patternProperties")]
|
||||
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
|
||||
#[serde(rename = "additionalProperties")]
|
||||
pub additional_properties: Option<Arc<Schema>>,
|
||||
pub required: Option<Vec<String>>,
|
||||
|
||||
// dependencies can be schema dependencies or property dependencies
|
||||
|
||||
18
src/tests.rs
18
src/tests.rs
@ -155,6 +155,24 @@ fn test_puncs_7() {
|
||||
crate::util::run_test_file_at_index(&path, 7).unwrap();
|
||||
}
|
||||
|
||||
#[pg_test]
|
||||
fn test_additional_properties_0() {
|
||||
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::util::run_test_file_at_index(&path, 0).unwrap();
|
||||
}
|
||||
|
||||
#[pg_test]
|
||||
fn test_additional_properties_1() {
|
||||
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::util::run_test_file_at_index(&path, 1).unwrap();
|
||||
}
|
||||
|
||||
#[pg_test]
|
||||
fn test_additional_properties_2() {
|
||||
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::util::run_test_file_at_index(&path, 2).unwrap();
|
||||
}
|
||||
|
||||
#[pg_test]
|
||||
fn test_exclusive_minimum_0() {
|
||||
let path = format!("{}/tests/fixtures/exclusiveMinimum.json", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
@ -910,6 +910,51 @@ impl<'a, I: ValidationInstance<'a>> ValidationContext<'a, I> {
|
||||
}
|
||||
}
|
||||
|
||||
// 6.5. Additional Properties
|
||||
if let Some(ref additional_schema) = self.schema.additional_properties {
|
||||
for (key, _) in obj {
|
||||
let mut locally_matched = false;
|
||||
if let Some(props) = &self.schema.properties {
|
||||
if props.contains_key(key) {
|
||||
locally_matched = true;
|
||||
}
|
||||
}
|
||||
if !locally_matched {
|
||||
if let Some(ref compiled_pp) = self.schema.compiled_pattern_properties {
|
||||
for (compiled_re, _) in compiled_pp {
|
||||
if compiled_re.0.is_match(key) {
|
||||
locally_matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !locally_matched {
|
||||
if let Some(child_instance) = self.instance.child_at_key(key) {
|
||||
let new_path = format!("{}/{}", self.path, key);
|
||||
let is_ref = additional_schema.ref_string.is_some()
|
||||
|| additional_schema.obj.dynamic_ref.is_some();
|
||||
let next_extensible = if is_ref { false } else { self.extensible };
|
||||
|
||||
let derived = self.derive(
|
||||
additional_schema,
|
||||
child_instance,
|
||||
&new_path,
|
||||
self.scope.clone(),
|
||||
HashSet::new(),
|
||||
next_extensible,
|
||||
false,
|
||||
);
|
||||
let item_res = derived.validate()?;
|
||||
result.merge(item_res);
|
||||
// Mark as evaluated so it doesn't trigger strictness failure
|
||||
result.evaluated_keys.insert(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Property Names
|
||||
if let Some(ref property_names) = self.schema.property_names {
|
||||
for key in obj.keys() {
|
||||
|
||||
132
tests/fixtures/additionalProperties.json
vendored
Normal file
132
tests/fixtures/additionalProperties.json
vendored
Normal file
@ -0,0 +1,132 @@
|
||||
[
|
||||
{
|
||||
"description": "additionalProperties validates properties not matched by properties",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
},
|
||||
"bar": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "defined properties are valid",
|
||||
"data": {
|
||||
"foo": "value",
|
||||
"bar": 123
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "additional property matching schema is valid",
|
||||
"data": {
|
||||
"foo": "value",
|
||||
"is_active": true,
|
||||
"hidden": false
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "additional property not matching schema is invalid",
|
||||
"data": {
|
||||
"foo": "value",
|
||||
"is_active": 1
|
||||
},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "extensible: true with additionalProperties still validates structure",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extensible": true,
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "additional property matching schema is valid",
|
||||
"data": {
|
||||
"foo": "hello",
|
||||
"count": 5,
|
||||
"age": 42
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "additional property not matching schema is invalid despite extensible: true",
|
||||
"data": {
|
||||
"foo": "hello",
|
||||
"count": "five"
|
||||
},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "complex additionalProperties with object and array items",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid array of strings",
|
||||
"data": {
|
||||
"type": "my_type",
|
||||
"group_a": [
|
||||
"field1",
|
||||
"field2"
|
||||
],
|
||||
"group_b": [
|
||||
"field3"
|
||||
]
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "invalid array of integers",
|
||||
"data": {
|
||||
"type": "my_type",
|
||||
"group_a": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "invalid non-array type",
|
||||
"data": {
|
||||
"type": "my_type",
|
||||
"group_a": "field1"
|
||||
},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -156,6 +156,24 @@ fn test_puncs_7() {
|
||||
util::run_test_file_at_index(&path, 7).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_additional_properties_0() {
|
||||
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
|
||||
util::run_test_file_at_index(&path, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_additional_properties_1() {
|
||||
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
|
||||
util::run_test_file_at_index(&path, 1).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_additional_properties_2() {
|
||||
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
|
||||
util::run_test_file_at_index(&path, 2).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exclusive_minimum_0() {
|
||||
let path = format!("{}/tests/fixtures/exclusiveMinimum.json", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
Reference in New Issue
Block a user