Compare commits

...

44 Commits

Author SHA1 Message Date
b97879ff61 version: 1.0.31 2025-07-08 07:27:14 -04:00
ea0b139f87 upgraded rust and pgrx versions 2025-07-08 07:27:05 -04:00
dccaa0a46e version: 1.0.30 2025-07-04 04:23:15 -04:00
441597e604 need to allow empty strings when a string property has a format 2025-07-04 04:23:06 -04:00
710598752f version: 1.0.29 2025-06-17 18:55:27 -04:00
5fbf64bac5 serializing ErrorKind directly to drop error cause 2025-06-17 18:55:16 -04:00
2dd17f0b37 version: 1.0.28 2025-06-12 22:27:59 -04:00
cbda45e610 fixed conditional errors with false schemas and unevaluatedProperties 2025-06-12 22:27:49 -04:00
1085964c17 version: 1.0.27 2025-06-12 17:07:37 -04:00
65971d9b93 splitting up errorkind paths to produce multiple drop errors 2025-06-12 17:07:28 -04:00
d938058d34 version: 1.0.26 2025-06-12 00:59:44 -04:00
69ab6165bb improvements to error handling again 2025-06-12 00:59:33 -04:00
03beada825 version: 1.0.25 2025-06-11 20:28:46 -04:00
efdd7528cc switched strict validation from additionalProperties to unevaluatedProperties to catch conditional properties automatically in verification 2025-06-11 20:28:39 -04:00
59395a33ac version: 1.0.24 2025-06-11 19:38:56 -04:00
92c0a6fc0b even more jspg improved error handling, missing some codes before 2025-06-11 19:38:46 -04:00
7f66a4a35a no-op 2025-06-10 16:01:58 -04:00
d37aadb0dd version: 1.0.23 2025-06-09 18:09:33 -04:00
d0ccc47d97 added strict validation option 2025-06-09 18:09:15 -04:00
2d19bf100e version: 1.0.22 2025-06-06 14:25:18 -04:00
fb333c6cbb slight improvements to error messaging 2025-06-06 14:25:13 -04:00
d8a9a7b76b version: 1.0.21 2025-06-06 14:05:24 -04:00
c9022aefb9 fixed env 2025-06-06 14:05:19 -04:00
ccf0465e45 fixed gitignore 2025-06-06 14:02:43 -04:00
dce50d9dc3 error handling improvements to jspg to match drop structure 2025-06-06 13:58:50 -04:00
8ec6a5b58a flow updates 2025-05-29 17:51:16 -04:00
6ef7e0c55e flow update 2025-04-25 13:34:06 -04:00
1cb5fb0ecf removed random .env 2025-04-25 12:22:07 -04:00
d66aae8ae2 flow update 2025-04-24 20:02:18 -04:00
3b18901bda version: 1.0.20 2025-04-21 17:11:30 -04:00
b8c0e08068 more filtering 2025-04-21 17:11:24 -04:00
c734983a59 version: 1.0.19 2025-04-21 16:15:08 -04:00
9b11f661bc fixed release bug 2025-04-21 16:15:02 -04:00
f3a733626e version: 1.0.18 2025-04-21 16:13:16 -04:00
2bcdb8adbb version: 1.0.17 2025-04-21 16:11:31 -04:00
3988308965 branch error filtering 2025-04-21 16:11:12 -04:00
b7f528d1f6 flow 2025-04-16 21:14:07 -04:00
2febb292dc flow update 2025-04-16 20:00:35 -04:00
d1831a28ec flow update 2025-04-16 19:34:09 -04:00
c5834ac544 flow updated 2025-04-16 18:07:41 -04:00
eb25f8489e version: 1.0.16 2025-04-16 14:43:07 -04:00
21937db8de improved compile schema error messages 2025-04-16 14:42:57 -04:00
28b689cac0 version: 1.0.15 2025-04-16 01:00:57 -04:00
cc04a1a8bb made errors consistent 2025-04-16 01:00:51 -04:00
10 changed files with 1861 additions and 828 deletions

13
.env
View File

@ -1,13 +0,0 @@
ENVIRONMENT=local
DATABASE_PASSWORD=QgSvstSjoc6fKphMzNgT3SliNY10eSRS
DATABASE_ROLE=agreego_admin
DATABASE_HOST=127.1.27.9
DATABASE_PORT=5432
POSTGRES_PASSWORD=xzIq5JT0xY3F+2m1GtnrKDdK29sNSXVVYZHPKJVh8pI=
DATABASE_NAME=agreego
DEV_DATABASE_NAME=agreego_dev
GITEA_TOKEN=3d70c23673517330623a5122998fb304e3c73f0a
MOOV_ACCOUNT_ID=69a0d2f6-77a2-4e26-934f-d869134f87d3
MOOV_PUBLIC_KEY=9OMhK5qGnh7Tmk2Z
MOOV_SECRET_KEY=DrRox7B-YWfO9IheiUUX7lGP8-7VY-Ni
MOOV_DOMAIN=http://localhost

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
/package
.env

581
Cargo.lock generated
View File

@ -26,7 +26,6 @@ dependencies = [
"cfg-if",
"getrandom 0.2.15",
"once_cell",
"serde",
"version_check",
"zerocopy 0.7.35",
]
@ -113,7 +112,7 @@ dependencies = [
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
@ -215,12 +214,6 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytecount"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
[[package]]
name = "byteorder"
version = "1.5.0"
@ -369,6 +362,15 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "codepage"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4"
dependencies = [
"encoding_rs",
]
[[package]]
name = "convert_case"
version = "0.8.0"
@ -426,12 +428,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "email_address"
version = "0.2.9"
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"serde",
"cfg-if",
]
[[package]]
@ -486,17 +488,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fancy-regex"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@ -517,7 +508,6 @@ checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
dependencies = [
"borrow-or-share",
"ref-cast",
"serde",
]
[[package]]
@ -541,16 +531,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fraction"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7"
dependencies = [
"lazy_static",
"num",
]
[[package]]
name = "funty"
version = "2.0.0"
@ -573,12 +553,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
@ -609,11 +583,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@ -730,85 +702,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "http"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hyper"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"httparse",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"libc",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
@ -964,12 +857,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "is-terminal"
version = "0.4.16"
@ -1012,37 +899,11 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsonschema"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161c33c3ec738cfea3288c5c53dfcdb32fd4fc2954de86ea06f71b5a1a40bfcd"
dependencies = [
"ahash",
"base64",
"bytecount",
"email_address",
"fancy-regex",
"fraction",
"idna",
"itoa",
"num-cmp",
"once_cell",
"percent-encoding",
"referencing",
"regex-syntax",
"reqwest",
"serde",
"serde_json",
"uuid-simd",
]
[[package]]
name = "jspg"
version = "0.1.0"
dependencies = [
"boon",
"jsonschema",
"lazy_static",
"pgrx",
"pgrx-tests",
@ -1069,7 +930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
@ -1116,12 +977,6 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1167,76 +1022,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-cmp"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1270,12 +1055,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "outref"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "owo-colors"
version = "4.2.0"
@ -1306,7 +1085,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
@ -1345,9 +1124,9 @@ dependencies = [
[[package]]
name = "pgrx"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e1b41219b12cfcaa5d58f946a7ff1e7ddf0a4f7f930a7cdab612916e8a12c64"
checksum = "bab5bc1d60d3bc3c966d307a3c7313b1ebfb49a0ec183be3f1a057df0bcc9988"
dependencies = [
"atomic-traits",
"bitflags",
@ -1369,9 +1148,9 @@ dependencies = [
[[package]]
name = "pgrx-bindgen"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6afcef51e801bb18662716f1c524cedfb7943844593171734fe4d3a94c9afa12"
checksum = "9804b74c211a9edd550cd974718f8cc407dec50d8e9cafb906e0b042ba434af0"
dependencies = [
"bindgen",
"cc",
@ -1388,9 +1167,9 @@ dependencies = [
[[package]]
name = "pgrx-macros"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729af3e6954d2f76230d700efd8606121f13f71f800e5c76173add2c02097948"
checksum = "f230769493bf567f137de23264d604d267dd72b8a77c596528e43cf423c6208e"
dependencies = [
"pgrx-sql-entity-graph",
"proc-macro2",
@ -1400,11 +1179,13 @@ dependencies = [
[[package]]
name = "pgrx-pg-config"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "116e33a329f3fac976b5f3150f14f2612735dfc56a15cb0a0800f25a3bd90aa7"
checksum = "49b64c071c2a46a19ab4521120a25b02b598f4abf6e9b4b1769a7922edeee3de"
dependencies = [
"cargo_toml",
"codepage",
"encoding_rs",
"eyre",
"home",
"owo-colors",
@ -1414,13 +1195,14 @@ dependencies = [
"thiserror 2.0.12",
"toml",
"url",
"winapi",
]
[[package]]
name = "pgrx-pg-sys"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd074044513f1f7fc63fd1ed0117ad0fbe690ef1b445f6d72b92e611b3846490"
checksum = "fcbfa98ec7a90252d13a78ac666541173dbb01a2fc1ba20131db6490c0711125"
dependencies = [
"cee-scape",
"libc",
@ -1433,9 +1215,9 @@ dependencies = [
[[package]]
name = "pgrx-sql-entity-graph"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0eb73c4b916d4abb422fff66c2606c46bf4b99136209306836e89766a8d49cd"
checksum = "e79bbf5a33cff6cfdc6dda3a976cd931c995eaa2c073a7c59b8f8fe8f6faa073"
dependencies = [
"convert_case",
"eyre",
@ -1449,9 +1231,9 @@ dependencies = [
[[package]]
name = "pgrx-tests"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aba4e9a97fd148c9f65cf0c56d33a4cde4deb9941f5c0d914a39148e8148a7a6"
checksum = "9791c709882f3af9545bcca71670fdd82768f67a428b416b6210eae3773dbd0d"
dependencies = [
"clap-cargo",
"eyre",
@ -1710,20 +1492,6 @@ dependencies = [
"syn",
]
[[package]]
name = "referencing"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a64b3a635fad9000648b4d8a59c8710c523ab61a23d392a7d91d47683f5adc"
dependencies = [
"ahash",
"fluent-uri",
"once_cell",
"parking_lot",
"percent-encoding",
"serde_json",
]
[[package]]
name = "regex"
version = "1.11.1"
@ -1753,43 +1521,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-registry",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -1824,12 +1555,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "rusty-fork"
version = "0.3.0"
@ -1929,18 +1654,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.10.8"
@ -2048,15 +1761,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.1"
@ -2253,58 +1957,6 @@ dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.18.0"
@ -2394,29 +2046,12 @@ dependencies = [
"getrandom 0.3.2",
]
[[package]]
name = "uuid-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vsimd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "wait-timeout"
version = "0.2.1"
@ -2436,15 +2071,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -2474,7 +2100,6 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
@ -2492,19 +2117,6 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
@ -2596,7 +2208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core",
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
@ -2607,8 +2219,8 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result 0.1.2",
"windows-targets 0.52.6",
"windows-result",
"windows-targets",
]
[[package]]
@ -2633,48 +2245,13 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.2",
"windows-strings",
"windows-targets 0.53.0",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
"windows-targets",
]
[[package]]
@ -2683,7 +2260,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
@ -2692,7 +2269,7 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
"windows-targets",
]
[[package]]
@ -2701,30 +2278,14 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
@ -2733,96 +2294,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.6"

View File

@ -1,18 +1,17 @@
[package]
name = "jspg"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
pgrx = "0.14.0"
pgrx = "0.15.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
jsonschema = "0.29.1"
lazy_static = "1.5.0"
boon = "0.6.1"
[dev-dependencies]
pgrx-tests = "0.14.0"
pgrx-tests = "0.15.0"
[lib]
crate-type = ["cdylib", "lib"]

121
flow
View File

@ -1,155 +1,132 @@
#!/bin/bash
#!/usr/bin/env bash
# Flows
source ./flows/base
source ./flows/git
source ./flows/kube
source ./flows/packaging
source ./flows/rust
# Vars
POSTGRES_VERSION="17"
POSTGRES_CONFIG_PATH="/opt/homebrew/opt/postgresql@${POSTGRES_VERSION}/bin/pg_config"
DEPENDENCIES=(cargo git icu4c pkg-config "postgresql@${POSTGRES_VERSION}")
CARGO_DEPENDENCIES=(cargo-pgrx==0.14.0)
DEPENDENCIES+=(icu4c pkg-config "postgresql@${POSTGRES_VERSION}")
CARGO_DEPENDENCIES=(cargo-pgrx==0.15.0)
GITEA_ORGANIZATION="cellular"
GITEA_REPOSITORY="jspg"
env() {
# Check if GITEA_TOKEN is set
if [ -z "$GITEA_TOKEN" ]; then
# If not set, try to get it from kubectl
GITEA_TOKEN=$(kubectl get secret -n cellular gitea-git -o jsonpath='{.data.token}' | base64 --decode)
if [ -z "$GITEA_TOKEN" ]; then
echo -e "❌ ${RED}GITEA_TOKEN is not set and couldn't be retrieved from kubectl${RESET}" >&2
exit 1
fi
export GITEA_TOKEN
fi
echo -e "💰 ${GREEN}Environment variables set${RESET}"
}
pgrx-prepare() {
echo -e "${BLUE}Initializing pgrx...${RESET}"
info "Initializing pgrx..."
# Explicitly point to the postgresql@${POSTGRES_VERSION} pg_config, don't rely on 'which'
local POSTGRES_CONFIG_PATH="/opt/homebrew/opt/postgresql@${POSTGRES_VERSION}/bin/pg_config"
if [ ! -x "$POSTGRES_CONFIG_PATH" ]; then
echo -e "${RED}Error: pg_config not found or not executable at $POSTGRES_CONFIG_PATH.${RESET}"
echo -e "${YELLOW}Ensure postgresql@${POSTGRES_VERSION} is installed correctly via Homebrew.${RESET}"
exit 1
error "pg_config not found or not executable at $POSTGRES_CONFIG_PATH."
warning "Ensure postgresql@${POSTGRES_VERSION} is installed correctly via Homebrew."
return 2
fi
if cargo pgrx init --pg"$POSTGRES_VERSION"="$POSTGRES_CONFIG_PATH"; then
echo -e "${GREEN}pgrx initialized successfully.${RESET}"
success "pgrx initialized successfully."
else
echo -e "${RED}Failed to initialize pgrx. Check PostgreSQL development packages are installed and $POSTGRES_CONFIG_PATH is valid.${RESET}"
exit 1
error "Failed to initialize pgrx. Check PostgreSQL development packages are installed and $POSTGRES_CONFIG_PATH is valid."
return 2
fi
}
build() {
local version
version=$(get-version) || return 1
version=$(get-version) || return $?
local package_dir="./package"
local tarball_name="${GITEA_REPOSITORY}.tar.gz"
local tarball_path="${package_dir}/${tarball_name}"
echo -e "📦 Creating source tarball v$version for ${GITEA_REPOSITORY} in $package_dir..."
info "Creating source tarball v$version for ${GITEA_REPOSITORY} in $package_dir..."
# Clean previous package dir
rm -rf "${package_dir}"
mkdir -p "${package_dir}"
# Create the source tarball excluding specified patterns
echo -e " ${CYAN}Creating tarball: ${tarball_path}${RESET}"
info "Creating tarball: ${tarball_path}"
if tar --exclude='.git*' --exclude='./target' --exclude='./package' --exclude='./flows' --exclude='./flow' -czf "${tarball_path}" .; then
echo -e "✨ ${GREEN}Successfully created source tarball: ${tarball_path}${RESET}"
success "Successfully created source tarball: ${tarball_path}"
else
echo -e "❌ ${RED}Failed to create source tarball.${RESET}" >&2
return 1
error "Failed to create source tarball."
return 2
fi
}
install() {
local version
version=$(get-version) || return 1
version=$(get-version) || return $? # Propagate error
echo -e "🔧 ${CYAN}Building and installing PGRX extension v$version into local PostgreSQL...${RESET}"
info "Building and installing PGRX extension v$version into local PostgreSQL..."
# Run the pgrx install command
# It implicitly uses --release unless --debug is passed
# It finds pg_config or you can add flags like --pg-config if needed
if ! cargo pgrx install; then
echo -e "❌ ${RED}cargo pgrx install command failed.${RESET}" >&2
return 1
error "cargo pgrx install command failed."
return 2
fi
echo -e "✨ ${GREEN}PGRX extension v$version successfully built and installed.${RESET}"
success "PGRX extension v$version successfully built and installed."
# Post-install modification to allow non-superuser usage
# Get the installation path dynamically using pg_config
local pg_sharedir
pg_sharedir=$("$POSTGRES_CONFIG_PATH" --sharedir)
if [ -z "$pg_sharedir" ]; then
echo -e "❌ ${RED}Failed to determine PostgreSQL shared directory using pg_config.${RESET}" >&2
return 1
local pg_config_status=$?
if [ $pg_config_status -ne 0 ] || [ -z "$pg_sharedir" ]; then
error "Failed to determine PostgreSQL shared directory using pg_config."
return 2
fi
local installed_control_path="${pg_sharedir}/extension/jspg.control"
# Modify the control file
if [ ! -f "$installed_control_path" ]; then
echo -e "❌ ${RED}Installed control file not found: '$installed_control_path'${RESET}" >&2
return 1
error "Installed control file not found: '$installed_control_path'"
return 2
fi
echo -e "🔧 ${CYAN}Modifying control file for non-superuser access: ${installed_control_path}${RESET}"
info "Modifying control file for non-superuser access: ${installed_control_path}"
# Use sed -i '' for macOS compatibility
if sed -i '' '/^superuser = false/d' "$installed_control_path" && \
echo 'trusted = true' >> "$installed_control_path"; then
echo -e "✨ ${GREEN}Control file modified successfully.${RESET}"
success "Control file modified successfully."
else
echo -e "❌ ${RED}Failed to modify control file: ${installed_control_path}${RESET}" >&2
return 1
error "Failed to modify control file: ${installed_control_path}"
return 2
fi
}
test() {
echo -e "🧪 ${CYAN}Running jspg tests...${RESET}"
cargo pgrx test "pg${POSTGRES_VERSION}" "$@"
info "Running jspg tests..."
cargo pgrx test "pg${POSTGRES_VERSION}" "$@" || return $?
}
clean() {
echo -e "🧹 ${CYAN}Cleaning build artifacts...${RESET}"
cargo clean # Use standard cargo clean
info "Cleaning build artifacts..."
cargo clean || return $?
}
jspg-usage() {
echo -e " ${CYAN}JSPG Commands:${RESET}"
echo -e " prepare Check OS, Cargo, and PGRX dependencies."
echo -e " install [opts] Run prepare, then build and install the extension locally."
echo -e " reinstall [opts] Run prepare, clean, then build and install the extension locally."
echo -e " test [opts] Run pgrx integration tests."
echo -e " clean Remove pgrx build artifacts."
echo -e " build Build release artifacts into ./package/ (called by release)."
echo -e " tag Tag the current version (called by release)."
echo -e " package Upload artifacts from ./package/ (called by release)."
echo -e " release Perform a full release (increments patch, builds, tags, pushes, packages)."
printf "prepare\tCheck OS, Cargo, and PGRX dependencies.\n"
printf "install\tBuild and install the extension locally (after prepare).\n"
printf "reinstall\tClean, build, and install the extension locally (after prepare).\n"
printf "test\t\tRun pgrx integration tests.\n"
printf "clean\t\tRemove pgrx build artifacts.\n"
}
jspg-flow() {
case "$1" in
env) env; return 0;;
prepare) base prepare; cargo-prepare; pgrx-prepare; return 0;;
build) build; return 0;;
install) install; return 0;;
reinstall) clean; install; return 0;;
test) test; return 0;;
package) env; package; return 0;;
release) env; release; return 0;;
clean) clean; return 0;;
prepare) prepare && cargo-prepare && pgrx-prepare; return $?;;
build) build; return $?;;
install) install; return $?;;
reinstall) clean && install; return $?;;
test) test "${@:2}"; return $?;;
clean) clean; return $?;;
*) return 1 ;;
esac
}
register-flow "jspg-flow" "jspg-usage"
register-flow "jspg-usage" "jspg-flow"
dispatch "$@"

2
flows

Submodule flows updated: 9d758d581e...e154758056

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
tab_spaces = 2

View File

@ -2,16 +2,27 @@ use pgrx::*;
pg_module_magic!();
use serde_json::{json, Value};
use std::{collections::HashMap, sync::RwLock};
use boon::{Compiler, Schemas, ValidationError, SchemaIndex, CompileError};
use boon::{CompileError, Compiler, ErrorKind, SchemaIndex, Schemas, ValidationError, Type, Types};
use lazy_static::lazy_static;
use serde_json::{json, Value, Number};
use std::borrow::Cow;
use std::collections::hash_map::Entry;
use std::{collections::HashMap, sync::RwLock};
struct BoonCache {
schemas: Schemas,
id_to_index: HashMap<String, SchemaIndex>,
}
// Structure to hold error information without lifetimes
#[derive(Debug)]
struct Error {
path: String,
code: String,
message: String,
cause: Value, // Changed from String to Value to store JSON
}
lazy_static! {
static ref SCHEMA_CACHE: RwLock<BoonCache> = RwLock::new(BoonCache {
schemas: Schemas::new(),
@ -20,22 +31,31 @@ lazy_static! {
}
#[pg_extern(strict)]
fn cache_json_schema(schema_id: &str, schema: JsonB) -> JsonB {
fn cache_json_schema(schema_id: &str, schema: JsonB, strict: bool) -> JsonB {
let mut cache = SCHEMA_CACHE.write().unwrap();
let schema_value: Value = schema.0;
let mut schema_value: Value = schema.0;
let schema_path = format!("urn:{}", schema_id);
// Apply strict validation to all objects in the schema if requested
if strict {
apply_strict_validation(&mut schema_value);
}
// Create the boon compiler and enable format assertions
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
"errors": [{
"code": "SCHEMA_RESOURCE_ADD_FAILED",
"message": format!("Failed to add schema resource '{}'", schema_id),
"details": {
"schema": schema_id,
"cause": format!("{}", e)
}
}]
}));
}
@ -44,93 +64,699 @@ fn cache_json_schema(schema_id: &str, schema: JsonB) -> JsonB {
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 }))
JsonB(json!({ "response": "success" }))
}
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 errors = match &e {
CompileError::ValidationError { url: _url, src } => {
// Collect leaf errors from the meta-schema validation failure
let mut error_list = Vec::new();
collect_errors(src, &mut error_list);
// Filter and format errors properly - no instance for schema compilation
format_errors(error_list, &schema_value, schema_id)
}
_ => {
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),
})
// Other compilation errors
vec![json!({
"code": "SCHEMA_COMPILATION_FAILED",
"message": format!("Schema '{}' compilation failed", schema_id),
"details": {
"schema": schema_id,
"cause": format!("{:?}", e)
}
})]
}
};
JsonB(json!({
"success": false,
"error": error
}))
JsonB(json!({ "errors": errors }))
}
}
}
// Helper function to apply strict validation to a schema
//
// This recursively adds unevaluatedProperties: false to object-type schemas,
// but SKIPS schemas inside if/then/else to avoid breaking conditional validation.
fn apply_strict_validation(schema: &mut Value) {
apply_strict_validation_recursive(schema, false);
}
fn apply_strict_validation_recursive(schema: &mut Value, inside_conditional: bool) {
match schema {
Value::Object(map) => {
// Skip adding strict validation if we're inside a conditional
if !inside_conditional {
// Add strict validation to object schemas only at top level
if let Some(Value::String(t)) = map.get("type") {
if t == "object" && !map.contains_key("unevaluatedProperties") && !map.contains_key("additionalProperties") {
// At top level, use unevaluatedProperties: false
// This considers all evaluated properties from all schemas
map.insert("unevaluatedProperties".to_string(), Value::Bool(false));
}
}
}
// Recurse into all properties
for (key, value) in map.iter_mut() {
// Mark when we're inside conditional branches
let in_conditional = inside_conditional || matches!(key.as_str(), "if" | "then" | "else");
apply_strict_validation_recursive(value, in_conditional);
}
}
Value::Array(arr) => {
// Recurse into array items
for item in arr.iter_mut() {
apply_strict_validation_recursive(item, inside_conditional);
}
}
_ => {}
}
}
#[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,
"errors": [json!({
"message": format!("Schema with id '{}' not found in cache", schema_id),
"schema_path": "",
"instance_path": ""
})]
"errors": [{
"code": "SCHEMA_NOT_FOUND",
"message": format!("Schema '{}' not found in cache", schema_id),
"details": {
"schema": schema_id,
"cause": "Schema must be cached before validation"
}
}]
})),
Some(sch_index) => {
let instance_value: Value = instance.0;
match cache.schemas.validate(&instance_value, *sch_index) {
Ok(_) => JsonB(json!({ "success": true })),
Ok(_) => JsonB(json!({ "response": "success" })),
Err(validation_error) => {
// Directly use the result of format_validation_error
// which now includes the top-level success indicator and flat error list
JsonB(format_validation_error(&validation_error))
let mut error_list = Vec::new();
collect_errors(&validation_error, &mut error_list);
let errors = format_errors(error_list, &instance_value, schema_id);
// Filter out FALSE_SCHEMA errors if there are other validation errors
let filtered_errors = filter_false_schema_errors(errors);
if filtered_errors.is_empty() {
JsonB(json!({ "response": "success" }))
} else {
JsonB(json!({ "errors": filtered_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
// Recursively collects validation errors
fn collect_errors(error: &ValidationError, errors_list: &mut Vec<Error>) {
// Check if this is a structural error that we should skip
let is_structural = matches!(
&error.kind,
ErrorKind::Group | ErrorKind::AllOf | ErrorKind::AnyOf | ErrorKind::Not | ErrorKind::OneOf(_)
);
// Special handling for FalseSchema - if it has causes, use those instead
if matches!(&error.kind, ErrorKind::FalseSchema) {
if !error.causes.is_empty() {
// FalseSchema often wraps more specific errors in if/then conditionals
for cause in &error.causes {
collect_errors(cause, errors_list);
}
return;
}
// If FalseSchema has no causes, it's likely from unevaluatedProperties
// We'll handle it as a leaf error below
}
if error.causes.is_empty() && !is_structural {
let base_path = error.instance_location.to_string();
// Match on error kind and handle each type
let errors_to_add = match &error.kind {
ErrorKind::Type { got, want } => handle_type_error(&base_path, got, want),
ErrorKind::Required { want } => handle_required_error(&base_path, want),
ErrorKind::Dependency { prop, missing } => handle_dependency_error(&base_path, prop, missing, false),
ErrorKind::DependentRequired { prop, missing } => handle_dependency_error(&base_path, prop, missing, true),
ErrorKind::AdditionalProperties { got } => handle_additional_properties_error(&base_path, got),
ErrorKind::Enum { want } => handle_enum_error(&base_path, want),
ErrorKind::Const { want } => handle_const_error(&base_path, want),
ErrorKind::MinLength { got, want } => handle_min_length_error(&base_path, *got, *want),
ErrorKind::MaxLength { got, want } => handle_max_length_error(&base_path, *got, *want),
ErrorKind::Pattern { got, want } => handle_pattern_error(&base_path, got, want),
ErrorKind::Minimum { got, want } => handle_minimum_error(&base_path, got, want),
ErrorKind::Maximum { got, want } => handle_maximum_error(&base_path, got, want),
ErrorKind::ExclusiveMinimum { got, want } => handle_exclusive_minimum_error(&base_path, got, want),
ErrorKind::ExclusiveMaximum { got, want } => handle_exclusive_maximum_error(&base_path, got, want),
ErrorKind::MultipleOf { got, want } => handle_multiple_of_error(&base_path, got, want),
ErrorKind::MinItems { got, want } => handle_min_items_error(&base_path, *got, *want),
ErrorKind::MaxItems { got, want } => handle_max_items_error(&base_path, *got, *want),
ErrorKind::UniqueItems { got } => handle_unique_items_error(&base_path, got),
ErrorKind::MinProperties { got, want } => handle_min_properties_error(&base_path, *got, *want),
ErrorKind::MaxProperties { got, want } => handle_max_properties_error(&base_path, *got, *want),
ErrorKind::AdditionalItems { got } => handle_additional_items_error(&base_path, *got),
ErrorKind::Format { want, got, err } => handle_format_error(&base_path, want, got, err),
ErrorKind::PropertyName { prop } => handle_property_name_error(&base_path, prop),
ErrorKind::Contains => handle_contains_error(&base_path),
ErrorKind::MinContains { got, want } => handle_min_contains_error(&base_path, got, *want),
ErrorKind::MaxContains { got, want } => handle_max_contains_error(&base_path, got, *want),
ErrorKind::ContentEncoding { want, err } => handle_content_encoding_error(&base_path, want, err),
ErrorKind::ContentMediaType { want, err, .. } => handle_content_media_type_error(&base_path, want, err),
ErrorKind::FalseSchema => handle_false_schema_error(&base_path),
ErrorKind::Not => handle_not_error(&base_path),
ErrorKind::RefCycle { url, kw_loc1, kw_loc2 } => handle_ref_cycle_error(&base_path, url, kw_loc1, kw_loc2),
ErrorKind::Reference { kw, url } => handle_reference_error(&base_path, kw, url),
ErrorKind::Schema { url } => handle_schema_error(&base_path, url),
ErrorKind::ContentSchema => handle_content_schema_error(&base_path),
ErrorKind::Group => handle_group_error(&base_path),
ErrorKind::AllOf => handle_all_of_error(&base_path),
ErrorKind::AnyOf => handle_any_of_error(&base_path),
ErrorKind::OneOf(matched) => handle_one_of_error(&base_path, matched),
};
errors_list.push(json!({
"message": message,
"schema_path": error.schema_url.to_string(),
"instance_path": error.instance_location.to_string(),
}));
// Add all generated errors
for error in errors_to_add {
errors_list.push(error);
}
} else {
// Recurse into causes
for cause in &error.causes {
collect_leaf_errors(cause, errors_list);
collect_errors(cause, errors_list);
}
}
}
// Formats validation errors into a flat list JSON structure
fn format_validation_error(error: &ValidationError) -> Value {
let mut all_errors = Vec::new();
collect_leaf_errors(error, &mut all_errors);
// Handler functions for each error kind
fn handle_type_error(base_path: &str, got: &Type, want: &Types) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "TYPE_MISMATCH".to_string(),
message: format!("Expected {} but got {}",
want.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" or "),
got
),
cause: json!({
"got": got.to_string(),
"want": want.iter().map(|t| t.to_string()).collect::<Vec<_>>()
}),
}]
}
json!({
"success": false,
"errors": all_errors // Flat list of specific errors
fn handle_required_error(base_path: &str, want: &[&str]) -> Vec<Error> {
// Create a separate error for each missing required field
want.iter().map(|missing_field| {
let field_path = if base_path.is_empty() {
format!("/{}", missing_field)
} else {
format!("{}/{}", base_path, missing_field)
};
Error {
path: field_path,
code: "REQUIRED_FIELD_MISSING".to_string(),
message: format!("Required field '{}' is missing", missing_field),
cause: json!({ "want": [missing_field] }),
}
}).collect()
}
fn handle_dependency_error(base_path: &str, prop: &str, missing: &[&str], is_dependent_required: bool) -> Vec<Error> {
// Create a separate error for each missing field
missing.iter().map(|missing_field| {
let field_path = if base_path.is_empty() {
format!("/{}", missing_field)
} else {
format!("{}/{}", base_path, missing_field)
};
let (code, message) = if is_dependent_required {
(
"DEPENDENT_REQUIRED_MISSING".to_string(),
format!("Field '{}' is required when '{}' is present", missing_field, prop),
)
} else {
(
"DEPENDENCY_FAILED".to_string(),
format!("Field '{}' is required when '{}' is present", missing_field, prop),
)
};
Error {
path: field_path,
code,
message,
cause: json!({ "prop": prop, "missing": [missing_field] }),
}
}).collect()
}
fn handle_additional_properties_error(base_path: &str, got: &[Cow<str>]) -> Vec<Error> {
// Create a separate error for each additional property that's not allowed
got.iter().map(|extra_prop| {
let field_path = if base_path.is_empty() {
format!("/{}", extra_prop)
} else {
format!("{}/{}", base_path, extra_prop)
};
Error {
path: field_path,
code: "ADDITIONAL_PROPERTIES_NOT_ALLOWED".to_string(),
message: format!("Property '{}' is not allowed", extra_prop),
cause: json!({ "got": [extra_prop.to_string()] }),
}
}).collect()
}
fn handle_enum_error(base_path: &str, want: &[Value]) -> Vec<Error> {
let message = if want.len() == 1 {
format!("Value must be {}", serde_json::to_string(&want[0]).unwrap_or_else(|_| "unknown".to_string()))
} else {
format!("Value must be one of: {}",
want.iter()
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "unknown".to_string()))
.collect::<Vec<_>>()
.join(", ")
)
};
vec![Error {
path: base_path.to_string(),
code: "ENUM_VIOLATED".to_string(),
message,
cause: json!({ "want": want }),
}]
}
fn handle_const_error(base_path: &str, want: &Value) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "CONST_VIOLATED".to_string(),
message: format!("Value must be exactly {}", serde_json::to_string(want).unwrap_or_else(|_| "unknown".to_string())),
cause: json!({ "want": want }),
}]
}
fn handle_min_length_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MIN_LENGTH_VIOLATED".to_string(),
message: format!("String length must be at least {} characters, but got {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_max_length_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MAX_LENGTH_VIOLATED".to_string(),
message: format!("String length must be at most {} characters, but got {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_pattern_error(base_path: &str, got: &Cow<str>, want: &str) -> Vec<Error> {
let display_value = if got.len() > 50 {
format!("{}...", &got[..50])
} else {
got.to_string()
};
vec![Error {
path: base_path.to_string(),
code: "PATTERN_VIOLATED".to_string(),
message: format!("Value '{}' does not match pattern '{}'", display_value, want),
cause: json!({ "got": got.to_string(), "want": want }),
}]
}
fn handle_minimum_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MINIMUM_VIOLATED".to_string(),
message: format!("Value must be at least {}, but got {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_maximum_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MAXIMUM_VIOLATED".to_string(),
message: format!("Value must be at most {}, but got {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_exclusive_minimum_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
message: format!("Value must be greater than {}, but got {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_exclusive_maximum_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
message: format!("Value must be less than {}, but got {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_multiple_of_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MULTIPLE_OF_VIOLATED".to_string(),
message: format!("{} is not a multiple of {}", got, want),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_min_items_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MIN_ITEMS_VIOLATED".to_string(),
message: format!("Array must have at least {} items, but has {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_max_items_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MAX_ITEMS_VIOLATED".to_string(),
message: format!("Array must have at most {} items, but has {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_unique_items_error(base_path: &str, got: &[usize; 2]) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "UNIQUE_ITEMS_VIOLATED".to_string(),
message: format!("Array items at positions {} and {} are duplicates", got[0], got[1]),
cause: json!({ "got": got }),
}]
}
fn handle_min_properties_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MIN_PROPERTIES_VIOLATED".to_string(),
message: format!("Object must have at least {} properties, but has {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_max_properties_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MAX_PROPERTIES_VIOLATED".to_string(),
message: format!("Object must have at most {} properties, but has {}", want, got),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_additional_items_error(base_path: &str, got: usize) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "ADDITIONAL_ITEMS_NOT_ALLOWED".to_string(),
message: format!("Last {} array items are not allowed", got),
cause: json!({ "got": got }),
}]
}
fn handle_format_error(base_path: &str, want: &str, got: &Cow<Value>, err: &Box<dyn std::error::Error>) -> Vec<Error> {
// If the value is an empty string, skip format validation.
if let Value::String(s) = got.as_ref() {
if s.is_empty() {
return vec![];
}
}
vec![Error {
path: base_path.to_string(),
code: "FORMAT_INVALID".to_string(),
message: format!("Value {} is not a valid {} format",
serde_json::to_string(got.as_ref()).unwrap_or_else(|_| "unknown".to_string()),
want
),
cause: json!({ "got": got, "want": want, "err": err.to_string() }),
}]
}
fn handle_property_name_error(base_path: &str, prop: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "INVALID_PROPERTY_NAME".to_string(),
message: format!("Property name '{}' is invalid", prop),
cause: json!({ "prop": prop }),
}]
}
fn handle_contains_error(base_path: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "CONTAINS_FAILED".to_string(),
message: "No array items match the required schema".to_string(),
cause: json!({}),
}]
}
fn handle_min_contains_error(base_path: &str, got: &[usize], want: usize) -> Vec<Error> {
let message = if got.is_empty() {
format!("At least {} array items must match the schema, but none do", want)
} else {
format!("At least {} array items must match the schema, but only {} do (at positions {})",
want,
got.len(),
got.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ")
)
};
vec![Error {
path: base_path.to_string(),
code: "MIN_CONTAINS_VIOLATED".to_string(),
message,
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_max_contains_error(base_path: &str, got: &[usize], want: usize) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "MAX_CONTAINS_VIOLATED".to_string(),
message: format!("At most {} array items can match the schema, but {} do (at positions {})",
want,
got.len(),
got.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ")
),
cause: json!({ "got": got, "want": want }),
}]
}
fn handle_content_encoding_error(base_path: &str, want: &str, err: &Box<dyn std::error::Error>) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "CONTENT_ENCODING_INVALID".to_string(),
message: format!("Content is not valid {} encoding: {}", want, err),
cause: json!({ "want": want, "err": err.to_string() }),
}]
}
fn handle_content_media_type_error(base_path: &str, want: &str, err: &Box<dyn std::error::Error>) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "CONTENT_MEDIA_TYPE_INVALID".to_string(),
message: format!("Content is not valid {} media type: {}", want, err),
cause: json!({ "want": want, "err": err.to_string() }),
}]
}
fn handle_false_schema_error(base_path: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "FALSE_SCHEMA".to_string(),
message: "This schema always fails validation".to_string(),
cause: json!({}),
}]
}
fn handle_not_error(base_path: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "NOT_VIOLATED".to_string(),
message: "Value matches a schema that it should not match".to_string(),
cause: json!({}),
}]
}
fn handle_ref_cycle_error(base_path: &str, url: &str, kw_loc1: &str, kw_loc2: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "REFERENCE_CYCLE".to_string(),
message: format!("Reference cycle detected: both '{}' and '{}' resolve to '{}'", kw_loc1, kw_loc2, url),
cause: json!({ "url": url, "kw_loc1": kw_loc1, "kw_loc2": kw_loc2 }),
}]
}
fn handle_reference_error(base_path: &str, kw: &str, url: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "REFERENCE_FAILED".to_string(),
message: format!("{} reference to '{}' failed validation", kw, url),
cause: json!({ "kw": kw, "url": url }),
}]
}
fn handle_schema_error(base_path: &str, url: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "SCHEMA_FAILED".to_string(),
message: format!("Schema '{}' validation failed", url),
cause: json!({ "url": url }),
}]
}
fn handle_content_schema_error(base_path: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "CONTENT_SCHEMA_FAILED".to_string(),
message: "Content schema validation failed".to_string(),
cause: json!({}),
}]
}
fn handle_group_error(base_path: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "VALIDATION_FAILED".to_string(),
message: "Validation failed".to_string(),
cause: json!({}),
}]
}
fn handle_all_of_error(base_path: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "ALL_OF_VIOLATED".to_string(),
message: "Value does not match all required schemas".to_string(),
cause: json!({}),
}]
}
fn handle_any_of_error(base_path: &str) -> Vec<Error> {
vec![Error {
path: base_path.to_string(),
code: "ANY_OF_VIOLATED".to_string(),
message: "Value does not match any of the allowed schemas".to_string(),
cause: json!({}),
}]
}
fn handle_one_of_error(base_path: &str, matched: &Option<(usize, usize)>) -> Vec<Error> {
let (message, cause) = match matched {
None => (
"Value must match exactly one schema, but matches none".to_string(),
json!({ "matched_indices": null })
),
Some((i, j)) => (
format!("Value must match exactly one schema, but matches schemas at positions {} and {}", i, j),
json!({ "matched_indices": [i, j] })
),
};
vec![Error {
path: base_path.to_string(),
code: "ONE_OF_VIOLATED".to_string(),
message,
cause,
}]
}
// Filter out FALSE_SCHEMA errors if there are other validation errors
fn filter_false_schema_errors(errors: Vec<Value>) -> Vec<Value> {
// Check if there are any non-FALSE_SCHEMA errors
let has_non_false_schema = errors.iter().any(|e| {
e.get("code")
.and_then(|c| c.as_str())
.map(|code| code != "FALSE_SCHEMA")
.unwrap_or(false)
});
if has_non_false_schema {
// Filter out FALSE_SCHEMA errors
errors.into_iter()
.filter(|e| {
e.get("code")
.and_then(|c| c.as_str())
.map(|code| code != "FALSE_SCHEMA")
.unwrap_or(true)
})
.collect()
} else {
// Keep all errors (they're all FALSE_SCHEMA)
errors
}
}
// Formats errors according to DropError structure
fn format_errors(errors: Vec<Error>, instance: &Value, schema_id: &str) -> Vec<Value> {
// Deduplicate by instance_path and format as DropError
let mut unique_errors: HashMap<String, Value> = HashMap::new();
for error in errors {
if let Entry::Vacant(entry) = unique_errors.entry(error.path.clone()) {
// Extract the failing value from the instance
let failing_value = extract_value_at_path(instance, &error.path);
entry.insert(json!({
"code": error.code,
"message": error.message,
"details": {
"path": error.path,
"context": failing_value,
"cause": error.cause,
"schema": schema_id
}
}));
}
}
unique_errors.into_values().collect()
}
// Helper function to extract value at a JSON pointer path
fn extract_value_at_path(instance: &Value, path: &str) -> Value {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut current = instance;
for part in parts {
match current {
Value::Object(map) => {
if let Some(value) = map.get(part) {
current = value;
} else {
return Value::Null;
}
}
Value::Array(arr) => {
if let Ok(index) = part.parse::<usize>() {
if let Some(value) = arr.get(index) {
current = value;
} else {
return Value::Null;
}
} else {
return Value::Null;
}
}
_ => return Value::Null,
}
}
current.clone()
}
#[pg_extern(strict, parallel_safe)]
@ -140,19 +766,20 @@ fn json_schema_cached(schema_id: &str) -> bool {
}
#[pg_extern(strict)]
fn clear_json_schemas() {
fn clear_json_schemas() -> JsonB {
let mut cache = SCHEMA_CACHE.write().unwrap();
*cache = BoonCache {
schemas: Schemas::new(),
id_to_index: HashMap::new(),
};
JsonB(json!({ "response": "success" }))
}
#[pg_extern(strict, parallel_safe)]
fn show_json_schemas() -> Vec<String> {
fn show_json_schemas() -> JsonB {
let cache = SCHEMA_CACHE.read().unwrap();
let ids: Vec<String> = cache.id_to_index.keys().cloned().collect();
ids
JsonB(json!({ "response": ids }))
}
/// This module is required by `cargo pgrx test` invocations.
@ -170,7 +797,6 @@ pub mod pg_test {
}
}
#[cfg(any(test, feature = "pg_test"))]
#[pg_schema]
mod tests {

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
1.0.14
1.0.31