From fbd591dbd894e28cc525a1208082aba2ca668005 Mon Sep 17 00:00:00 2001 From: "David E. Wheeler" Date: Wed, 24 Jul 2024 18:07:32 -0400 Subject: [PATCH] Implement v2 Spec JSON Schema Based on pgxn/rfcs#3. As with the v1 schemas, use JSON Schema v2020 for the v2 schemas. Fix a few minor issues with the v1 schema, as well. Add full test suite in Rust; move code shared between the v1 and v2 tests to `tests/common/mod.rs`. This includes custom format functions not required for v1, including spdx version expression validation with the `spdx` crate, and path validation to ensure no `..` is included in paths. --- .pre-commit-config.yaml | 2 +- Cargo.lock | 16 + Cargo.toml | 1 + schema/v1/bugtracker.schema.json | 6 +- schema/v1/distribution.schema.json | 6 +- schema/v1/extension.schema.json | 6 +- schema/v1/license.schema.json | 1 + schema/v1/maintainer.schema.json | 1 + schema/v1/meta-spec.schema.json | 6 +- schema/v1/no_index.schema.json | 7 +- schema/v1/prereq_phase.schema.json | 6 +- schema/v1/prereqs.schema.json | 6 +- schema/v1/repository.schema.json | 6 +- schema/v1/resources.schema.json | 10 +- schema/v1/tags.schema.json | 1 + schema/v1/version_range.schema.json | 2 +- schema/v2/app.schema.json | 62 + schema/v2/artifacts.schema.json | 63 + schema/v2/badges.schema.json | 40 + schema/v2/categories.schema.json | 35 + schema/v2/classifications.schema.json | 26 + schema/v2/contents.schema.json | 109 + schema/v2/dependencies.schema.json | 131 + schema/v2/distribution.schema.json | 127 + schema/v2/extension.schema.json | 48 + schema/v2/glob.schema.json | 12 + schema/v2/ignore.schema.json | 10 + schema/v2/license.schema.json | 19 + schema/v2/maintainers.schema.json | 76 + schema/v2/meta-spec.schema.json | 29 + schema/v2/module.schema.json | 53 + schema/v2/packages.schema.json | 70 + schema/v2/path.schema.json | 12 + schema/v2/phase.schema.json | 81 + schema/v2/pipeline.schema.json | 17 + schema/v2/platform.schema.json | 23 + schema/v2/platforms.schema.json | 20 + schema/v2/postgres.schema.json | 28 + schema/v2/purl.schema.json | 15 + schema/v2/resources.schema.json | 61 + schema/v2/semver.schema.json | 15 + schema/v2/tags.schema.json | 21 + schema/v2/term.schema.json | 10 + schema/v2/variations.schema.json | 66 + schema/v2/version_range.schema.json | 30 + tests/common/mod.rs | 302 +++ tests/v1_schema_test.rs | 334 +-- tests/v2_schema_test.rs | 3333 +++++++++++++++++++++++++ 48 files changed, 5037 insertions(+), 324 deletions(-) create mode 100644 schema/v2/app.schema.json create mode 100644 schema/v2/artifacts.schema.json create mode 100644 schema/v2/badges.schema.json create mode 100644 schema/v2/categories.schema.json create mode 100644 schema/v2/classifications.schema.json create mode 100644 schema/v2/contents.schema.json create mode 100644 schema/v2/dependencies.schema.json create mode 100644 schema/v2/distribution.schema.json create mode 100644 schema/v2/extension.schema.json create mode 100644 schema/v2/glob.schema.json create mode 100644 schema/v2/ignore.schema.json create mode 100644 schema/v2/license.schema.json create mode 100644 schema/v2/maintainers.schema.json create mode 100644 schema/v2/meta-spec.schema.json create mode 100644 schema/v2/module.schema.json create mode 100644 schema/v2/packages.schema.json create mode 100644 schema/v2/path.schema.json create mode 100644 schema/v2/phase.schema.json create mode 100644 schema/v2/pipeline.schema.json create mode 100644 schema/v2/platform.schema.json create mode 100644 schema/v2/platforms.schema.json create mode 100644 schema/v2/postgres.schema.json create mode 100644 schema/v2/purl.schema.json create mode 100644 schema/v2/resources.schema.json create mode 100644 schema/v2/semver.schema.json create mode 100644 schema/v2/tags.schema.json create mode 100644 schema/v2/term.schema.json create mode 100644 schema/v2/variations.schema.json create mode 100644 schema/v2/version_range.schema.json create mode 100644 tests/common/mod.rs create mode 100644 tests/v2_schema_test.rs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2de3a8..306f7b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: fmt - id: check - id: clippy - - id: test + # - id: test - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.1.0 diff --git a/Cargo.lock b/Cargo.lock index 07b5cae..6eb6aee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,7 @@ dependencies = [ "boon", "serde", "serde_json", + "spdx", ] [[package]] @@ -230,6 +231,21 @@ dependencies = [ "serde", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "spdx" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc" +dependencies = [ + "smallvec", +] + [[package]] name = "syn" version = "2.0.68" diff --git a/Cargo.toml b/Cargo.toml index e161f33..6f33a3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ serde_json = "1.0" [dependencies] serde = { version = "1", features = ["derive"] } +spdx = "0.10.6" diff --git a/schema/v1/bugtracker.schema.json b/schema/v1/bugtracker.schema.json index 0165d50..70b4edd 100644 --- a/schema/v1/bugtracker.schema.json +++ b/schema/v1/bugtracker.schema.json @@ -17,11 +17,7 @@ } }, "anyOf": [{ "required": ["web"] }, { "required": ["mailto"] }], - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "examples": [ { diff --git a/schema/v1/distribution.schema.json b/schema/v1/distribution.schema.json index ec2b9d2..fd7ec15 100644 --- a/schema/v1/distribution.schema.json +++ b/schema/v1/distribution.schema.json @@ -50,11 +50,7 @@ }, "resources": { "$ref": "resources.schema.json" } }, - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "required": [ "name", diff --git a/schema/v1/extension.schema.json b/schema/v1/extension.schema.json index c00ecc9..ab8f089 100644 --- a/schema/v1/extension.schema.json +++ b/schema/v1/extension.schema.json @@ -26,11 +26,7 @@ } }, "required": ["file", "version"], - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "examples": [ { diff --git a/schema/v1/license.schema.json b/schema/v1/license.schema.json index 6cbfce5..c529fe7 100644 --- a/schema/v1/license.schema.json +++ b/schema/v1/license.schema.json @@ -14,6 +14,7 @@ "items": { "$ref": "#/$defs/validLicense" }, "description": "A list of shortcuts to identify well-known licenses for the distribution.", "minItems": 1, + "uniqueItems": true, "examples": [["apache_2_0", "postgresql"], ["mit"]] }, { diff --git a/schema/v1/maintainer.schema.json b/schema/v1/maintainer.schema.json index 8c02cf8..a44f396 100644 --- a/schema/v1/maintainer.schema.json +++ b/schema/v1/maintainer.schema.json @@ -14,6 +14,7 @@ "type": "string", "minLength": 1 }, + "uniqueItems": true, "minItems": 1 } ], diff --git a/schema/v1/meta-spec.schema.json b/schema/v1/meta-spec.schema.json index 2b69de9..ba1a648 100644 --- a/schema/v1/meta-spec.schema.json +++ b/schema/v1/meta-spec.schema.json @@ -17,11 +17,7 @@ } }, "required": ["version"], - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "examples": [ { "version": "1.0.0" }, diff --git a/schema/v1/no_index.schema.json b/schema/v1/no_index.schema.json index f9dee5d..515aa62 100644 --- a/schema/v1/no_index.schema.json +++ b/schema/v1/no_index.schema.json @@ -15,11 +15,7 @@ } }, "anyOf": [{ "required": ["file"] }, { "required": ["directory"] }], - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "examples": [ { @@ -33,6 +29,7 @@ { "type": "array", "minItems": 1, + "uniqueItems": true, "items": { "type": "string", "description": "Relative path in unix convention to a file to ignore.", diff --git a/schema/v1/prereq_phase.schema.json b/schema/v1/prereq_phase.schema.json index 9f6da7f..cb9c3db 100644 --- a/schema/v1/prereq_phase.schema.json +++ b/schema/v1/prereq_phase.schema.json @@ -22,11 +22,7 @@ "description": "These dependencies cannot be installed when the phase is in operation. This is a very rare situation, and the conflicts relationship should be used with great caution, or not at all." } }, - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "anyOf": [ { "required": ["requires"] }, diff --git a/schema/v1/prereqs.schema.json b/schema/v1/prereqs.schema.json index d2e56e2..3c2a728 100644 --- a/schema/v1/prereqs.schema.json +++ b/schema/v1/prereqs.schema.json @@ -33,11 +33,7 @@ { "required": ["runtime"] }, { "required": ["develop"] } ], - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "examples": [ { diff --git a/schema/v1/repository.schema.json b/schema/v1/repository.schema.json index f9497f6..2033f69 100644 --- a/schema/v1/repository.schema.json +++ b/schema/v1/repository.schema.json @@ -22,11 +22,7 @@ } }, "anyOf": [{ "required": ["url", "type"] }, { "required": ["web"] }], - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "examples": [ { diff --git a/schema/v1/resources.schema.json b/schema/v1/resources.schema.json index 22911b6..557a97d 100644 --- a/schema/v1/resources.schema.json +++ b/schema/v1/resources.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://pgxn.org/meta/v1/resources.schema.json", - "title": "Source Control Repository", - "description": "An Extension is provided by a distribution.", + "title": "Resources", + "description": "Resources related to this distribution.", "type": "object", "properties": { "homepage": { @@ -13,11 +13,7 @@ "bugtracker": { "$ref": "bugtracker.schema.json" }, "repository": { "$ref": "repository.schema.json" } }, - "patternProperties": { - "^[xX]_.": { - "description": "Custom key" - } - }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, "anyOf": [ { "required": ["homepage"] }, diff --git a/schema/v1/tags.schema.json b/schema/v1/tags.schema.json index 357d132..4ff6216 100644 --- a/schema/v1/tags.schema.json +++ b/schema/v1/tags.schema.json @@ -5,6 +5,7 @@ "description": "A list of keywords that describe the distribution.", "type": "array", "minItems": 1, + "uniqueItems": true, "items": { "title": "Tag", "description": "A Tag is a subtype of String that must be fewer than 256 characters long contain no slash (/), backslash (\\), or control characters.", diff --git a/schema/v1/version_range.schema.json b/schema/v1/version_range.schema.json index bc054e7..470b6f7 100644 --- a/schema/v1/version_range.schema.json +++ b/schema/v1/version_range.schema.json @@ -7,7 +7,7 @@ { "type": "string", "pattern": "^(([=!]=|[<>]=?)\\s*)?((0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?|0)(,\\s*((([=!]=|[<>]=?)\\s*)?)(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)*$", - "$comment": "https://regex101.com/r/Uy7XWK/1" + "$comment": "https://regex101.com/r/Uy7XWK" }, { "const": 0 diff --git a/schema/v2/app.schema.json b/schema/v2/app.schema.json new file mode 100644 index 0000000..3a934bb --- /dev/null +++ b/schema/v2/app.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/app.schema.json", + "title": "App", + "description": "An App represents an applications, command-line or otherwise.", + "type": "object", + "properties": { + "lang": { + "$ref": "term.schema.json", + "description": "A short string representing the implementation language. Required for apps that need the `'#!{cmd}` shebang line modified before installing." + }, + "bin": { + "$ref": "path.schema.json", + "description": "A path pointing to the app binary, which may be generated by the build process." + }, + "doc": { + "$ref": "path.schema.json", + "description": "A path pointing to the documentation file for the app, which **SHOULD** be more than a README." + }, + "abstract": { + "type": "string", + "description": "A short String value describing the extension.", + "minLength": 1 + }, + "lib": { + "$ref": "path.schema.json", + "description": "A path pointing a directory of additional files to install, such as support libraries or modules." + }, + "man": { + "$ref": "path.schema.json", + "description": "A path pointing to a man page or directory of man pages created by the build process." + }, + "html": { + "$ref": "path.schema.json", + "description": "A path pointing to an HTML file or directory of HTML files created by the build process." + } + }, + "required": ["bin"], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "examples": [ + { + "lang": "perl", + "bin": "blib/script/app", + "lib": "blib/lib", + "man": "blib/libdoc", + "html": "blib/libhtml", + "doc": "doc/app.md", + "abstract": "blah blah blah" + }, + { + "lang": "python", + "bin": "bin/common/check_unique_constraint.py", + "abstract": "Check that all rows in a partition set are unique for the given columns" + }, + { + "bin": "pg_top", + "man": "pg_top.1", + "abstract": "pg_top is 'top' for PostgreSQL" + } + ] +} diff --git a/schema/v2/artifacts.schema.json b/schema/v2/artifacts.schema.json new file mode 100644 index 0000000..1430636 --- /dev/null +++ b/schema/v2/artifacts.schema.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/artifacts.schema.json", + "title": "Artifacts", + "description": "*Artifacts* describes non-PGXN links and checksums for downloading the distribution in one or more formats, including source code, binaries, system packages, etc. Consumers **MAY** use this information to determine the best option for installing an extension on a particular system. Useful for projects that publish their own binaries, such as in GitHub releases.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "The URL to download the artifact." + }, + "type": { + "type": "string", + "minLength": 2, + "pattern": "^[a-z0-9]{2,}$", + "description": "The type of artifact. **MUST** be a single lowercase word describing the artifact, such as none of `binary`, `source`, `rpm`, `homebrew`, etc." + }, + "platform": { + "$ref": "platform.schema.json", + "description": "Identifies the platform the artifact was built for. **RECOMMENDED** for packages compiled for a specific platform, such as a C extension compiled for `linux-arm64`." + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-fA-F]{64}$", + "description": "The SHA-256 checksum for the artifact in hex format." + }, + "sha512": { + "type": "string", + "pattern": "^[0-9a-fA-F]{128}$", + "description": "The SHA-512 checksum for the artifact in hex format." + } + }, + "anyOf": [ + { "required": ["url", "type", "sha256"] }, + { "required": ["url", "type", "sha512"] } + ], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false + }, + "examples": [ + [ + { + "type": "source", + "url": "https://github.com/theory/pg-pair/releases/download/v1.1.0/pair-1.1.0.zip", + "sha256": "2b9d2416096d2930be51e5332b70bcd97846947777a93e4a3d65fe1b5fd7b004" + }, + { + "type": "binary", + "url": "https://github.com/theory/pg-pair/releases/download/v1.1.0/pair-1.1.0-linux-amd64.tar.gz", + "sha256": "ec33656ba693c01be2b2b500c639846fb0ede7f7f8ee1f4c157bc9cab53c8500" + }, + { + "type": "binary", + "url": "https://github.com/theory/pg-pair/releases/download/v1.1.0/pair-1.1.0-linux-arm64.tar.gz", + "sha512": "612ad0a8b7e292daf0c723bd0ac8029a838357b2d3abbada7cd7445f7690191abd6593a1336742e705314df81fc1c0063423f62e4abd846f350c251ef6a6a24f" + } + ] + ] +} diff --git a/schema/v2/badges.schema.json b/schema/v2/badges.schema.json new file mode 100644 index 0000000..702c6c8 --- /dev/null +++ b/schema/v2/badges.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/badges.schema.json", + "title": "Badges", + "description": "*Badges* represents links to a [Shields](https://github.com/badges/shields/blob/master/spec/SPECIFICATION.md \"Shields badge specification\")-conformant badges.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "src": { + "type": "string", + "format": "uri", + "description": "The URI for the badge." + }, + "alt": { + "type": "string", + "minLength": 4, + "maxLength": 4048, + "description": "Alternate text for accessability." + }, + "url": { + "type": "string", + "format": "uri", + "description": "The URL the badge links to." + } + }, + "required": ["src", "alt"], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false + }, + "examples": [ + [ + { + "alt": "Test Status", + "src": "https://test.packages.postgresql.org/github.com/example/pair.svg" + } + ] + ] +} diff --git a/schema/v2/categories.schema.json b/schema/v2/categories.schema.json new file mode 100644 index 0000000..12a81b7 --- /dev/null +++ b/schema/v2/categories.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/categories.schema.json", + "title": "Categories", + "description": "A list of 1-3 categories describe the distribution.", + "type": "array", + "minItems": 1, + "maxItems": 3, + "uniqueItems": true, + "items": { + "title": "Category", + "enum": [ + "Analytics", + "Auditing and Logging", + "Change Data Capture", + "Connectors", + "Data and Transformations", + "Debugging", + "Index and Table Optimizations", + "Machine Learning", + "Metrics", + "Orchestration", + "Procedural Languages", + "Query Optimizations", + "Search", + "Security", + "Tooling and Admin" + ] + }, + "examples": [ + ["Change Data Capture"], + ["Analytics", "Data and Transformations"], + ["Metrics", "Orchestration", "Search"] + ] +} diff --git a/schema/v2/classifications.schema.json b/schema/v2/classifications.schema.json new file mode 100644 index 0000000..1e86794 --- /dev/null +++ b/schema/v2/classifications.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/classifications.schema.json", + "title": "Classifications", + "description": "Classification metadata associates additional information about the distribution to improve discovery.", + "type": "object", + "properties": { + "tags": { "$ref": "tags.schema.json" }, + "categories": { "$ref": "categories.schema.json" } + }, + "anyOf": [{ "required": ["tags"] }, { "required": ["categories"] }], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "examples": [ + { + "tags": ["testing", "unit testing", "tap", "tddd"] + }, + { + "categories": ["Analytics", "Data and Transformations"] + }, + { + "tags": ["background worker", "foreign data wrapper", "parquet"], + "categories": ["Metrics", "Orchestration", "Search"] + } + ] +} diff --git a/schema/v2/contents.schema.json b/schema/v2/contents.schema.json new file mode 100644 index 0000000..4fc9ce9 --- /dev/null +++ b/schema/v2/contents.schema.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/contents.schema.json", + "title": "Contents", + "description": "A description of what's included in the package provided by the distribution. This information is used by PGXN to build indexes identifying in which package various extensions can be found, and to create binary distribution packages.", + "type": "object", + "properties": { + "extensions": { + "propertyNames": { "$ref": "term.schema.json" }, + "minProperties": 1, + "additionalProperties": { "$ref": "extension.schema.json" } + }, + "modules": { + "propertyNames": { "$ref": "term.schema.json" }, + "minProperties": 1, + "additionalProperties": { "$ref": "module.schema.json" } + }, + "apps": { + "propertyNames": { "$ref": "term.schema.json" }, + "minProperties": 1, + "additionalProperties": { "$ref": "app.schema.json" } + } + }, + "anyOf": [ + { "required": ["extensions"] }, + { "required": ["modules"] }, + { "required": ["apps"] } + ], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "examples": [ + { + "extensions": { + "pair": { + "control": "pair.control", + "sql": "pair-1.2.0.sql", + "doc": "doc/pair.md", + "abstract": "A key/value pair data type", + "tle": true + } + } + }, + { + "modules": { + "my_worker": { + "type": "bgw", + "lib": "lib/my_bgw", + "doc": "doc/my_bgw.md", + "preload": "server", + "abstract": "My background worker" + }, + "my_hook": { + "type": "hook", + "lib": "lib/my_hook", + "doc": "doc/my_hook.md", + "preload": "session", + "abstract": "My hook" + } + } + }, + { + "apps": { + "my_app": { + "lang": "perl", + "bin": "blib/script/app", + "lib": "blib/lib", + "man": "blib/libdoc", + "html": "blib/libhtml", + "doc": "doc/app.md", + "abstract": "blah blah blah" + } + } + }, + { + "extensions": { + "pg_partman": { + "control": "pg_partman.control", + "sql": "sql/types/types.sql", + "doc": "doc/pg_partman.md", + "abstract": "Extension to manage partitioned tables by time or ID" + } + }, + "modules": { + "pg_partman_bgw": { + "type": "bgw", + "lib": "src/pg_partman_bgw", + "preload": "server" + } + }, + "apps": { + "check_unique_constraint": { + "lang": "python", + "bin": "bin/common/check_unique_constraint.py", + "abstract": "Check that all rows in a partition set are unique for the given columns" + }, + "dump_partition": { + "lang": "python", + "bin": "bin/common/dump_partition.py", + "abstract": "Dump out and then drop all tables contained in a schema." + }, + "vacuum_maintenance": { + "lang": "python", + "bin": "bin/common/vacuum_maintenance.py", + "abstract": "Performing vacuum maintenance on to avoid excess vacuuming and transaction id wraparound issues" + } + } + } + ] +} diff --git a/schema/v2/dependencies.schema.json b/schema/v2/dependencies.schema.json new file mode 100644 index 0000000..f87d983 --- /dev/null +++ b/schema/v2/dependencies.schema.json @@ -0,0 +1,131 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/dependencies.schema.json", + "title": "Dependencies", + "description": "*Dependencies* identify dependencies required to configure, build, test, install, and run the package provided by the distribution. These include not only PGXN packages, but also external libraries, system dependencies, and versions of PostgreSQL --- as well as any OS and architectures.", + "type": "object", + "properties": { + "platforms": { "$ref": "platforms.schema.json" }, + "postgres": { "$ref": "postgres.schema.json" }, + "pipeline": { "$ref": "pipeline.schema.json" }, + "packages": { "$ref": "packages.schema.json" }, + "variations": { "$ref": "variations.schema.json" } + }, + "anyOf": [ + { "required": ["platforms"] }, + { "required": ["postgres"] }, + { "required": ["pipeline"] }, + { "required": ["packages"] }, + { "required": ["variations"] } + ], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "examples": [ + { "postgres": { "version": "14.0" } }, + { + "postgres": { + "version": ">= 12.0, < 17.0", + "with": ["xml", "uuid", "perl"] + }, + "pipeline": "pgxs", + "packages": { + "build": { + "requires": { + "pkg:generic/awk": 0, + "pkg:generic/perl": "5.20" + }, + "recommends": { + "pkg:generic/jq": 0, + "pkg:generic/perl": "5.40" + } + } + } + }, + { + "pipeline": "pgrx", + "platforms": [ + "linux-amd64", + "linux-amd64v3", + "gnulinux-arm64", + "musllinux-amd64", + "darwin-23.5.0-arm64" + ], + "packages": { + "configure": { + "requires": { "pkg:cargo/cargo-pgrx": "==0.11.4" } + }, + "test": { + "requires": { + "pkg:postgres/pg_regress": 0, + "pkg:postgres/plpgsql": 0, + "pkg:pgxn/pgtap": "1.1.0" + } + }, + "run": { + "requires": { + "pkg:postgres/plperl": 0, + "pkg:pgxn/hostname": 0 + } + } + } + }, + { + "postgres": { + "version": ">= 15.0, < 16.0" + }, + "pipeline": "pgxs", + "platforms": [ + "linux-amd64", + "linux-arm64", + "darwin-amd64", + "darwin-arm64" + ], + "packages": { + "configure": { + "requires": { + "pkg:cargo/cargo-pgrx": "==0.11.4", + "pkg:generic/bison": 0, + "pkg:generic/cmake": 0, + "pkg:generic/flex": 0, + "pkg:generic/readline": 0, + "pkg:generic/openssl": 0, + "pkg:generic/pkg-config": 0 + } + }, + "run": { + "requires": { + "pkg:generic/penblas": 0, + "pkg:generic/python3": 0, + "pkg:generic/readline": 0, + "pkg:generic/openssl": 0, + "pkg:generic/bison": 0 + }, + "recommends": { + "pkg:pypi/pyarrow": "11.0.0", + "pkg:pypi/catboost": 0, + "pkg:pypi/lightgbm": 0, + "pkg:pypi/torch": 0, + "pkg:pypi/langchain": 0 + } + } + }, + "variations": [ + { + "where": { + "platforms": ["linux"] + }, + "dependencies": { + "packages": { + "run": { + "recommends": { + "pkg:pypi/auto-gptq": 0, + "pkg:pypi/xformers": 0 + } + } + } + } + } + ] + } + ] +} diff --git a/schema/v2/distribution.schema.json b/schema/v2/distribution.schema.json new file mode 100644 index 0000000..16af65d --- /dev/null +++ b/schema/v2/distribution.schema.json @@ -0,0 +1,127 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/distribution.schema.json", + "title": "Distribution", + "description": "This schema describes version 2.0.0 of the [PGXN](https://pgxn.org) source distribution metadata specification, also known as the “PGXN Meta Spec.” PGXN metadata ships with PGXN source distribution archives, and serves to describe the their contents for the benefit of automated indexing, distribution, discovery, full-text search, binary packaging, and more.", + "type": "object", + "properties": { + "name": { + "$ref": "term.schema.json", + "description": "The name of the package provided by the distribution. This is usually the same as the name of the “main extension” in the contents of the package but **MAY** be completely unrelated. This value will be used in the distribution file name on [PGXN](https://pgxn.org).", + "examples": ["pgTAP", "vector"] + }, + "version": { + "$ref": "semver.schema.json", + "description": "The version of the distribution to which the metadata structure refers. Its values **MUST** be a [SemVer](https://semver.org/).\n\nAll of the items listed in `contents will be considered to have this version; any references they make to a version, such as the control file, **SHOULD** be compatible with this version." + }, + "abstract": { + "description": "A short description of the purpose of the package provided by the distribution.", + "type": "string", + "minLength": 1, + "examples": [ + "Unit testing for PostgreSQL", + "Open-source vector similarity search for Postgres" + ] + }, + "maintainers": { "$ref": "maintainers.schema.json" }, + "license": { "$ref": "license.schema.json" }, + "contents": { "$ref": "contents.schema.json" }, + "meta-spec": { "$ref": "meta-spec.schema.json" }, + "description": { + "description": "A longer, more complete description of the purpose or intended use of the package provided by the distribution,answering the question “what is this thing and what value is it?”", + "type": "string", + "minLength": 1, + "examples": [ + "pgTAP is a suite of database functions that make it easy to write TAP-emitting unit tests in psql scripts or xUnit-style test functions." + ] + }, + "producer": { + "description": "The **Producer** that created the metadata. There are no defined semantics for this property, but it is traditional to use a string in the form “Software package version 1.23.0”, or the maintainer's name if the metadata was generated by hand.", + "type": "string", + "minLength": 1, + "examples": ["pgxn CLI version 2.0.4"] + }, + "classifications": { "$ref": "classifications.schema.json" }, + "ignore": { "$ref": "ignore.schema.json" }, + "dependencies": { "$ref": "dependencies.schema.json" }, + "resources": { "$ref": "resources.schema.json" }, + "artifacts": { "$ref": "artifacts.schema.json" } + }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "required": [ + "name", + "version", + "abstract", + "maintainers", + "license", + "contents", + "meta-spec" + ], + "examples": [ + { + "name": "pgTAP", + "abstract": "Unit testing for PostgreSQL", + "description": "pgTAP is a suite of database functions that make it easy to write TAP-emitting unit tests in psql scripts or xUnit-style test functions.", + "version": "0.26.0", + "maintainers": [ + { "name": "Josh Berkus", "email": "jberkus@pgxn.org" }, + { "name": "David E. Wheeler", "url": "https://pgxn.org/user/theory" } + ], + "license": "MIT OR PostgreSQL", + "dependencies": { + "postgres": { "version": "8.4" }, + "packages": { + "run": { + "requires": { + "pkg:postgres/plpgsql": 0 + } + } + } + }, + "contents": { + "extensions": { + "pgtap": { + "abstract": "Unit testing for PostgreSQL", + "sql": "pgtap.sql", + "control": "pgtap.control" + } + } + }, + "resources": { + "homepage": "https://pgtap.org", + "issues": "https://github.com/theory/pgtap/issues", + "repository": "https://github.com/theory/pgtap", + "docs": "https://pgtap.org/documentation.html", + "support": "https://github.com/theory/pgtap", + "badges": [ + { + "src": "https://img.shields.io/badge/License-PostgreSQL-blue.svg", + "alt": "PostgreSQL License", + "url": "https://www.postgresql.org/about/licence/" + }, + { + "src": "https://github.com/theory/pgtap/actions/workflows/test.yml/badge.svg", + "alt": "Test Status", + "url": "https://github.com/theory/pgtap/actions/workflows/ci.yml" + } + ] + }, + "producer": "David E. Wheeler", + "meta-spec": { + "version": "2.0.0", + "url": "https://rfcs.pgxn.org/0003-meta-spec-v2.html" + }, + "classifications": { + "tags": [ + "testing", + "unit testing", + "tap", + "tddd", + "test driven database development" + ], + "categories": ["Tooling and Admin"] + } + } + ] +} diff --git a/schema/v2/extension.schema.json b/schema/v2/extension.schema.json new file mode 100644 index 0000000..debbe00 --- /dev/null +++ b/schema/v2/extension.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/extension.schema.json", + "title": "Extension", + "description": "An Extension represents a `CREATE EXTENSION` extension provided by the distribution.", + "type": "object", + "properties": { + "control": { + "$ref": "path.schema.json", + "description": "A path pointing to the [control file](https://www.postgresql.org/docs/current/extend-extensions.html) used by `CREATE EXTENSION`." + }, + "sql": { + "$ref": "path.schema.json", + "description": "A path pointing to the SQL file used by `CREATE EXTENSION`." + }, + "doc": { + "$ref": "path.schema.json", + "description": "A path pointing to the documentation file for the extension, which **SHOULD** be more than a README." + }, + "abstract": { + "type": "string", + "description": "A short String value describing the extension.", + "minLength": 1 + }, + "tle": { + "type": "boolean", + "description": "Indicates that the extension can be used as a [trusted language extension](https://github.com/aws/pg_tle)." + } + }, + "required": ["sql", "control"], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "examples": [ + { + "control": "pair.control", + "sql": "sql/pair.sql", + "doc": "doc/pair.md", + "abstract": "A key/value pair data type", + "tle": true + }, + { + "sql": "sql/schematap.sql", + "control": "schematap.control", + "doc": "doc/schematap.md", + "abstract": "Schema testing assertions for PostgreSQL" + } + ] +} diff --git a/schema/v2/glob.schema.json b/schema/v2/glob.schema.json new file mode 100644 index 0000000..ae2ed58 --- /dev/null +++ b/schema/v2/glob.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/glob.schema.json", + "title": "Glob", + "description": "*Glob* defines a pattern to identify one or more files in the distribution.", + "type": "string", + "minLength": 2, + "format": "", + "pattern": "^(?:[^\\\\]|\\\\\\\\)+$", + "$comment": "https://regex101.com/r/d49AVj; Crates: fast-glob or wax", + "examples": ["/.git", "src/private.c", "doc/*.html"] +} diff --git a/schema/v2/ignore.schema.json b/schema/v2/ignore.schema.json new file mode 100644 index 0000000..a0d059d --- /dev/null +++ b/schema/v2/ignore.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/ignore.schema.json", + "title": "Ignore", + "description": "Describes any files or directories that are private to the distribution and **SHOULD** be ignored by indexing or search tools.", + "type": "array", + "minItems": 1, + "items": { "$ref": "glob.schema.json" }, + "examples": [["/src/private", "/src/file.sql", "*.html"]] +} diff --git a/schema/v2/license.schema.json b/schema/v2/license.schema.json new file mode 100644 index 0000000..0a473aa --- /dev/null +++ b/schema/v2/license.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/license.schema.json", + "title": "License Expression", + "description": "A *License Expression* represents one or more licenses from the [SPDX License List](https://github.com/spdx/license-list-data/) in a single value. The format is defined by [SPDX Standard License Expression](https://spdx.github.io/spdx-spec/v3.0/annexes/SPDX-license-expressions/).", + "type": "string", + "format": "license", + "pattern": "^(?:[\\w.()-]+(?:[+]|:[\\w.-]+)?|\\s*(?:OR|or|AND|and|WITH|with)\\s*)+$", + "$comment": "Over-simplified pattern; rely on format when possible.", + "examples": [ + "PostgreSQL", + "MIT", + "Apache-2.0", + "LGPL-2.1-only OR MIT", + "LGPL-2.1-only AND MIT AND BSD-2-Clause", + "GPL-2.0-or-later WITH Bison-exception-2.2", + "LGPL-2.1-only OR BSD-3-Clause AND MIT" + ] +} diff --git a/schema/v2/maintainers.schema.json b/schema/v2/maintainers.schema.json new file mode 100644 index 0000000..aaf2b03 --- /dev/null +++ b/schema/v2/maintainers.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/maintainers.schema.json", + "title": "Maintainers", + "description": "*Maintainers* are person(s) to contact concerning the distribution. This property provides a general contact list independent of other structured fields provided within the resources schema, such as `issues`. The addressee(s) can be contacted for any purpose including but not limited to: (security) problems with the distribution, questions about the distribution, or bugs in the distribution.\n\nA distribution's original author is usually the contact listed within this field. Co-maintainers, successor maintainers, or mailing lists devoted to the distribution **MAY** also be listed in addition to or instead of the original author.", + "type": "array", + "items": { "$ref": "#/$defs/maintainer" }, + "minItems": 1, + "uniqueItems": true, + "examples": [ + [ + { + "name": "David E. Wheeler", + "url": "https://pgxn.org/user/theory" + } + ], + [ + { + "name": "David E. Wheeler", + "email": "theory@pgxn.org", + "url": "https://pgxn.org/user/theory" + }, + { + "name": "Josh Berkus", + "email": "jberkus@pgxn.org" + } + ] + ], + "$defs": { + "maintainer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the maintainer.", + "minLength": 1 + }, + "email": { + "type": "string", + "format": "email", + "description": "The email address of the maintainer." + }, + "url": { + "type": "string", + "format": "uri", + "description": "The URL for the maintainer." + } + }, + "patternProperties": { + "^[xX]_.": { + "description": "Custom key" + } + }, + "additionalProperties": false, + "anyOf": [ + { "required": ["name", "email"] }, + { "required": ["name", "url"] } + ], + "examples": [ + { + "name": "David E. Wheeler", + "url": "https://pgxn.org/user/theory" + }, + { + "name": "David E. Wheeler", + "email": "theory@pgxn.org", + "url": "https://pgxn.org/user/theory" + }, + { + "name": "Josh Berkus", + "email": "jberkus@pgxn.org" + } + ] + } + } +} diff --git a/schema/v2/meta-spec.schema.json b/schema/v2/meta-spec.schema.json new file mode 100644 index 0000000..c1af282 --- /dev/null +++ b/schema/v2/meta-spec.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/meta-spec.schema.json", + "title": "Meta Spec", + "description": "This field indicates the Version of the PGXN Meta Spec that should be used to interpret the metadata. Consumers must check this key as soon as possible and abort further metadata processing if the meta-spec SemVer is not supported by the consumer.", + "type": "object", + "properties": { + "version": { + "type": "string", + "pattern": "^2[.]0[.][[:digit:]]+$", + "description": "The version of the PGXN Meta Spec against which the document was generated. Must be 2.0.x." + }, + "url": { + "type": "string", + "const": "https://rfcs.pgxn.org/0003-meta-spec-v2.html", + "description": "The URI of the metadata specification document corresponding to the given version. This is strictly for human-consumption and should not impact the interpretation of the document." + } + }, + "required": ["version"], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "examples": [ + { "version": "2.0.0" }, + { + "version": "2.0.2", + "url": "https://rfcs.pgxn.org/0003-meta-spec-v2.html" + } + ] +} diff --git a/schema/v2/module.schema.json b/schema/v2/module.schema.json new file mode 100644 index 0000000..6b8bae1 --- /dev/null +++ b/schema/v2/module.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/module.schema.json", + "title": "Module", + "description": "An Extension represents a loadable module (a.k.a., shared library) that can be loaded into PostgreSQL.", + "type": "object", + "properties": { + "type": { + "enum": ["extension", "hook", "bgw"], + "description": "The type of the module, one of \"extension\" for a module supporting a `CREATE EXTENSION` extension, \"bgw\" for a background worker, or \"hook\" for a hook." + }, + "lib": { + "$ref": "path.schema.json", + "description": "A path pointing to the pointing to the shared object file, without the suffix." + }, + "doc": { + "$ref": "path.schema.json", + "description": "A path pointing to the documentation file for the module, which **SHOULD** be more than a README." + }, + "abstract": { + "type": "string", + "description": "A short String value describing the module.", + "minLength": 1 + }, + "preload": { + "enum": ["server", "session"], + "description": "A string indicating that the module requires loading before it can be used. A value of `server` means that the module requires loading on server start, while `session` means it can be loaded in a session. Omit this field if the module does not require preloading." + } + }, + "required": ["type", "lib"], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "examples": [ + { + "type": "extension", + "lib": "src/my_extension" + }, + { + "type": "bgw", + "lib": "lib/my_bgw", + "doc": "doc/my_bgw.md", + "preload": "server", + "abstract": "My background worker" + }, + { + "type": "hook", + "lib": "lib/my_hook", + "doc": "doc/my_hook.md", + "preload": "session", + "abstract": "My hook" + } + ] +} diff --git a/schema/v2/packages.schema.json b/schema/v2/packages.schema.json new file mode 100644 index 0000000..f9c85d5 --- /dev/null +++ b/schema/v2/packages.schema.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/packages.schema.json", + "title": "Packages", + "description": "*Packages* define dependencies required for different phases of the build process,runtime, and development.", + "type": "object", + "properties": { + "configure": { + "description": "The configure phase occurs before any dynamic configuration has been attempted. Dependencies required by the configure phase **MUST** be available for use before the build tool has been executed.", + "$ref": "phase.schema.json" + }, + "build": { + "description": "The build phase is when the distribution's source code is compiled (if necessary) and otherwise made ready for installation.", + "$ref": "phase.schema.json" + }, + "test": { + "description": "The test phase is when the distribution's automated test suite is run. Any dependency needed only for testing and not for subsequent use **SHOULD** be listed here.", + "$ref": "phase.schema.json" + }, + "run": { + "description": "The runtime phase refers not only to when the contents of the package provided by the distribution are installed, but also to its continued use. Any package that is a dependency for regular use of this [Package](#package) **SHOULD** be indicated here.", + "$ref": "phase.schema.json" + }, + "develop": { + "description": "The develop phase's packages are needed to work on the package's source code as its maintainer does. These tools might be needed to build a release archive, to run maintainer-only tests, or to perform other tasks related to developing new versions of the package.", + "$ref": "phase.schema.json" + } + }, + "anyOf": [ + { "required": ["configure"] }, + { "required": ["build"] }, + { "required": ["test"] }, + { "required": ["run"] }, + { "required": ["develop"] } + ], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "examples": [ + { + "build": { + "requires": { + "pkg:generic/awk": 0, + "pkg:generic/perl": "5.20" + }, + "recommends": { + "pkg:generic/jq": 0, + "pkg:generic/perl": "5.40" + } + } + }, + { + "configure": { + "requires": { "pkg:cargo/cargo-pgrx": "==0.11.4" } + }, + "test": { + "requires": { + "pkg:postgres/pg_regress": 0, + "pkg:postgres/plpgsql": 0, + "pkg:pgxn/pgtap": "1.1.0" + } + }, + "run": { + "requires": { + "pkg:postgres/plperl": 0, + "pkg:pgxn/hostname": 0 + } + } + } + ] +} diff --git a/schema/v2/path.schema.json b/schema/v2/path.schema.json new file mode 100644 index 0000000..f39b10a --- /dev/null +++ b/schema/v2/path.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/path.schema.json", + "title": "Path", + "description": "A *Path* is a string with a relative file path that identifies a file in the Distribution. The path **MUST** be specified with Unix conventions.", + "type": "string", + "minLength": 2, + "format": "path", + "pattern": "^(?:[^/\\\\]|\\\\\\\\)(?:[^\\\\]|\\\\\\\\)+$", + "$comment": "https://regex101.com/r/d49AVj", + "examples": [".git", "src/pair.c", "doc/pair.md"] +} diff --git a/schema/v2/phase.schema.json b/schema/v2/phase.schema.json new file mode 100644 index 0000000..e36157d --- /dev/null +++ b/schema/v2/phase.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/phase.schema.json", + "title": "Dependency Phase", + "description": "A Phase maps dependency relationships, such as `requires`, `recommends`, `suggests`, and `conflicts`, to their dependencies.", + "type": "object", + "properties": { + "requires": { + "$ref": "#/$defs/relationship", + "description": "These dependencies **MUST** be installed for proper completion of the phase." + }, + "recommends": { + "$ref": "#/$defs/relationship", + "description": "Recommended dependencies are *strongly* encouraged and **SHOULD** be satisfied except in resource constrained environments." + }, + "suggests": { + "$ref": "#/$defs/relationship", + "description": "These dependencies are **OPTIONAL**, are suggested for enhanced operation of the described distribution, and **MAY** be satisfied." + }, + "conflicts": { + "$ref": "#/$defs/relationship", + "description": "These dependencies **MUST NOT** be installed when the phase is in operation. This is a very rare situation, and the conflicts relationship **SHOULD** be used with great caution, or not at all." + } + }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "anyOf": [ + { "required": ["requires"] }, + { "required": ["recommends"] }, + { "required": ["suggests"] }, + { "required": ["conflicts"] } + ], + "examples": [ + { + "requires": { "pkg:cargo/cargo-pgrx": "==0.11.4" } + }, + { + "requires": { + "pkg:postgres/pg_regress": 0, + "pkg:postgres/plpgsql": 0, + "pkg:pgxn/pgtap": "1.1.0" + } + }, + { + "requires": { + "pkg:generic/awk": 0, + "pkg:generic/perl": "5.20" + }, + "recommends": { + "pkg:generic/jq": 0, + "pkg:generic/perl": "5.40" + } + }, + { + "requires": { + "pkg:generic/penblas": 0, + "pkg:generic/python3": 0, + "pkg:generic/readline": 0, + "pkg:generic/openssl": 0, + "pkg:generic/bison": 0 + }, + "suggests": { + "pkg:pypi/pyarrow": "11.0.0", + "pkg:pypi/catboost": 0, + "pkg:pypi/lightgbm": 0, + "pkg:pypi/torch": 0, + "pkg:pypi/langchain": 0 + } + } + ], + "$defs": { + "relationship": { + "title": "Dependency Relationship", + "description": "A Dependency Relationship lists dependencies and their version ranges.", + "type": "object", + "minProperties": 1, + "propertyNames": { "$ref": "purl.schema.json" }, + "additionalProperties": { "$ref": "version_range.schema.json" } + } + } +} diff --git a/schema/v2/pipeline.schema.json b/schema/v2/pipeline.schema.json new file mode 100644 index 0000000..2f5db6b --- /dev/null +++ b/schema/v2/pipeline.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/pipeline.schema.json", + "title": "Pipeline", + "description": "The build pipeline required to configure, build, test, and install the package provided by the distribution.", + "enum": [ + "pgxs", + "meson", + "pgrx", + "autoconf", + "cmake", + "npm", + "cpanm", + "go", + "cargo" + ] +} diff --git a/schema/v2/platform.schema.json b/schema/v2/platform.schema.json new file mode 100644 index 0000000..9083222 --- /dev/null +++ b/schema/v2/platform.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/platform.schema.json", + "title": "Platform", + "description": "A *Platform* identifies a computing platform as a one to three dash-delimited substrings: An OS name, the OS version, and the architecture: `$os-$version-$architecture`.\n\nIf the string contains no dash, it represents only the OS. If it contains a single dash, the first value represents the OS and second value is a version if it starts with an integer followed by a dot and the architecture if it does not start with a digit.", + "type": "string", + "pattern": "^(?:(any)|([a-zA-Z][a-zA-Z0-9]+)(?:-(?:0|[1-9]\\d*)[.][^\\s-]+[^-\\s]*)?(?:-([a-zA-Z0-9]+))?)$", + "$comment": "https://regex101.com/r/vJv9cK", + "examples": [ + "any", + "linux", + "gnulinux", + "musllinux", + "linux-amd64", + "gnulinux-amd64", + "musllinux-1.2", + "musllinux-1.2-arm64", + "darwin", + "darwin-arm64", + "darwin-23.5.0", + "darwin-23.5.0-arm64" + ] +} diff --git a/schema/v2/platforms.schema.json b/schema/v2/platforms.schema.json new file mode 100644 index 0000000..463174d --- /dev/null +++ b/schema/v2/platforms.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/platforms.schema.json", + "title": "Platforms", + "description": "*Platforms* identify the OSes and architectures supported by the package provided by the distribution. If this property is not present, consumers **SHOULD** assume that the package supports any platform that PostgreSQL supports. This property is typically needed only when the package depends on platform-specific features.", + "type": "array", + "minItems": 1, + "items": { "$ref": "platform.schema.json" }, + "examples": [ + ["any"], + ["linux", "darwin", "windows"], + ["gnulinux", "musllinux"], + ["gnulinux-amd64", "musllinux-amd64"], + ["musllinux-1.2"], + ["musllinux-1.2-arm64"], + ["darwin-arm64"], + ["darwin-23.5.0"], + ["darwin-23.5.0-arm64"] + ] +} diff --git a/schema/v2/postgres.schema.json b/schema/v2/postgres.schema.json new file mode 100644 index 0000000..59ef3ae --- /dev/null +++ b/schema/v2/postgres.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/postgres.schema.json", + "title": "Postgres", + "description": "Describes the versions of PostgreSQL supported by the package provided by the distribution.", + "type": "object", + "properties": { + "version": { + "description": "A version range identifying the supported versions of PostgreSQL.", + "$ref": "version_range.schema.json" + }, + "with": { + "description": "The features that are required to be compiled into PostgreSQL. Each corresponds to the appropriate `--with` configure flag. Omit if the package requires no features.", + "type": "array", + "items": { "$ref": "term.schema.json" } + } + }, + "required": ["version"], + "additionalProperties": false, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "examples": [ + { "version": "14.0" }, + { + "version": ">= 12.0, < 17.0", + "with": ["xml", "uuid", "perl"] + } + ] +} diff --git a/schema/v2/purl.schema.json b/schema/v2/purl.schema.json new file mode 100644 index 0000000..1e64975 --- /dev/null +++ b/schema/v2/purl.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/purl.schema.json", + "title": "Path", + "description": "A *purl* is specifies a valid package in the format defined by the [purl spec](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst). All known [purl Types](https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst) **MAY** be used, as well as `pgxn` for PGXN packages and `postgres` for PostgreSQL core [contrib](https://www.postgresql.org/docs/current/contrib.html) or development packages. Versions appearing after a `@` are valid but ignored.", + "type": "string", + "format": "uri", + "pattern": "^pkg:[a-zA-Z.+-][a-zA-Z0-9.+-]+/[^@?#]+(?:@[^?#]+)?(?:\\?[^#]+)?(?:#\\S+)?$", + "examples": [ + "pkg:pgxn/pgtap", + "pkg:postgres/pg_regress", + "pkg:generic/python3", + "pkg:pypi/pyarrow@11.0.0" + ] +} diff --git a/schema/v2/resources.schema.json b/schema/v2/resources.schema.json new file mode 100644 index 0000000..c697f6d --- /dev/null +++ b/schema/v2/resources.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/resources.schema.json", + "title": "Resources", + "description": "*Resources* provide external information about the package provided by the distribution. Consumers **MAY** use this data for links and displaying useful information about the package.", + "type": "object", + "properties": { + "homepage": { + "type": "string", + "format": "uri", + "description": "A URI for the official home of this project on the web." + }, + "issues": { + "type": "string", + "format": "uri", + "description": "A URI for the package’s issue tracking system." + }, + "repository": { + "type": "string", + "format": "uri", + "description": "A URI for the package’s source code repository." + }, + "docs": { + "type": "string", + "format": "uri", + "description": "A URI for the package’s documentation." + }, + "support": { + "type": "string", + "format": "uri", + "description": "A URI for support resources and contacts for the package" + }, + "badges": { "$ref": "badges.schema.json" } + }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "anyOf": [ + { "required": ["homepage"] }, + { "required": ["issues"] }, + { "required": ["repository"] }, + { "required": ["docs"] }, + { "required": ["support"] }, + { "required": ["docs"] }, + { "required": ["badges"] } + ], + "examples": [ + { + "homepage": "https://pair.example.com", + "issues": "https://github.com/example/pair/issues", + "docs": "https://pair.example.com/docs", + "support": "https://github.com/example/pair/discussions", + "repository": "https://github.com/example/pair", + "badges": [ + { + "alt": "Test Status", + "src": "https://test.packages.postgresql.org/github.com/example/pair.svg" + } + ] + } + ] +} diff --git a/schema/v2/semver.schema.json b/schema/v2/semver.schema.json new file mode 100644 index 0000000..bf55141 --- /dev/null +++ b/schema/v2/semver.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/semver.schema.json", + "title": "SemVer", + "description": "A *SemVer* is a string containing a value that describes the version number of extensions or distributions, and adheres to the format of the [Semantic Versioning 2.0.0 Specification][semver] with the exception of [build metadata], which is reserved for use by downstream packaging systems.", + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$", + "$comment": "https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string", + "examples": [ + "1.3.6", + "10.20.30", + "1.0.0-alpha", + "1.0.0-alpha-a.b-c-something-long" + ] +} diff --git a/schema/v2/tags.schema.json b/schema/v2/tags.schema.json new file mode 100644 index 0000000..da41864 --- /dev/null +++ b/schema/v2/tags.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/tags.schema.json", + "title": "Tags", + "description": "A list of keywords that describe the distribution.", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "title": "Tag", + "description": "A *Tag* is a string that **MUST** be at least two and no more than 255 characters long, and contain no slash (`/`), backslash (`\\`), or control characters.", + "type": "string", + "minLength": 2, + "maxLength": 255, + "pattern": "^[^/\\\\\\p{Cntrl}]{2,}$" + }, + "examples": [ + ["jsonschema", "validation", "json", "schema", "constraint"], + ["testing", "unit testing", "tap", "tddd"] + ] +} diff --git a/schema/v2/term.schema.json b/schema/v2/term.schema.json new file mode 100644 index 0000000..1dba1ed --- /dev/null +++ b/schema/v2/term.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/term.schema.json", + "title": "Term", + "description": "A *Term* is a string that **MUST** be at least two characters long, and contain no slash (`/`), backslash (`\\`), dot (.), control, or space characters.", + "type": "string", + "minLength": 2, + "pattern": "^[^./\\\\\\p{Space}\\p{Cntrl}]{2,}$", + "examples": ["hi", "go-on", "pg_vectorize"] +} diff --git a/schema/v2/variations.schema.json b/schema/v2/variations.schema.json new file mode 100644 index 0000000..efa734c --- /dev/null +++ b/schema/v2/variations.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/variations.schema.json", + "title": "Dependency Variations", + "description": "*Variations* describe rule-based dependency variations.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "where": { + "description": "Contains the subset of dependencies to identify a variation", + "$ref": "dependencies.schema.json", + "patternProperties": { "^variations$": false } + }, + "dependencies": { + "description": "Contains the subset of dependencies required for the `where` property’s configuration.", + "$ref": "dependencies.schema.json", + "patternProperties": { "^variations$": false } + } + }, + "required": ["where", "dependencies"], + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false + }, + "examples": [ + [ + { + "where": { "platforms": ["linux"] }, + "dependencies": { + "packages": { + "configure": { + "requires": { + "pkg:generic/libelf": 0, + "pkg:generic/libbsd": 0 + } + } + } + } + } + ], + [ + { + "where": { "postgres": { "version": ">= 16.0" } }, + "dependencies": { + "postgres": { "version": ">= 16.0", "with": ["zstd"] } + } + }, + { + "where": { + "platforms": ["linux"] + }, + "dependencies": { + "packages": { + "run": { + "recommends": { + "pkg:pypi/auto-gptq": 0, + "pkg:pypi/xformers": 0 + } + } + } + } + } + ] + ] +} diff --git a/schema/v2/version_range.schema.json b/schema/v2/version_range.schema.json new file mode 100644 index 0000000..428af5e --- /dev/null +++ b/schema/v2/version_range.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/version_range.schema.json", + "title": "Version Range", + "description": "A *Version Range* a range of versions that **may** be present or installed to fulfill dependencies.\n\nA Version Range of the number `0` indicates all available versions. No other number values are allowed.\n\nOften a Version Range is just a single version. For a [SemVer](https://semver.org), for example, `2.4.2` means that **at least** version 2.4.2 must be present.\n\nAlternatively, a version range **may** use the operators `<` (less than), `<=` (less than or equal), `>` (greater than), `>=` (greater than or equal), `==` (equal), and `!=` (not equal). For example, the specification `< 2.0` means that any version less than version 2.0 is suitable.\n\nFor more complicated situations, version specifications **may** be AND-ed together using commas. The specification `>= 1.2.0, != 1.5.2, < 2.0.0` indicates a version that must be **at least** 1.2.0, **less than** 2.0.0, and **not equal to** 1.5.2.", + "oneOf": [ + { + "type": "string", + "pattern": "^(([=!]=|[<>]=?)\\s*)?((0|[1-9]\\d*)(?:\\.(0|[1-9]\\d*)(?:\\.(0|[1-9]\\d*))?)?(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?|0)(,\\s*((([=!]=|[<>]=?)\\s*)?)(?:(0\\.(?:[1-9]\\d*))|([1-9]\\d*)(?:\\.(0|[1-9]\\d*))?)(?:\\.(0|[1-9]\\d*))?(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)*$", + "$comment": "https://regex101.com/r/8ECGOy" + }, + { + "const": 0 + } + ], + "examples": [ + 0, + "0", + "2", + "1.0", + "==1.1", + "1.3.6", + "==0.0.4", + "<= 1.1.2+meta", + ">= 2.0.0, 1.5.6", + ">= 1.2.0, != 1.5.0, < 2.0.0", + ">=1,<2", + ">= 1.0, < 2.0" + ] +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..3b868ca --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,302 @@ +use std::fs::{self, File}; +use std::path::{Component, Path}; +use std::{collections::HashMap, error::Error}; + +use boon::{Compiler, Schemas}; +use serde_json::{json, Value}; + +const SCHEMA_BASE: &str = "https://pgxn.org/meta/v"; + +// https://regex101.com/r/Ly7O1x/3/ +pub const VALID_SEMVERS: &[&str] = &[ + "0.0.4", + "1.2.3", + "10.20.30", + "1.1.2-prerelease+meta", + "1.1.2+meta", + "1.1.2+meta-valid", + "1.0.0-alpha", + "1.0.0-beta", + "1.0.0-alpha.beta", + "1.0.0-alpha.beta.1", + "1.0.0-alpha.1", + "1.0.0-alpha0.valid", + "1.0.0-alpha.0valid", + "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay", + "1.0.0-rc.1+build.1", + "2.0.0-rc.1+build.123", + "1.2.3-beta", + "10.2.3-DEV-SNAPSHOT", + "1.2.3-SNAPSHOT-123", + "1.0.0", + "2.0.0", + "1.1.7", + "2.0.0+build.1848", + "2.0.1-alpha.1227", + "1.0.0-alpha+beta", + "1.2.3----RC-SNAPSHOT.12.9.1--.12+788", + "1.2.3----R-S.12.9.1--.12+meta", + "1.2.3----RC-SNAPSHOT.12.9.1--.12", + "1.0.0+0.build.1-rc.10000aaa-kk-0.1", + "1.0.0-0A.is.legal", +]; + +pub const INVALID_SEMVERS: &[&str] = &[ + "1", + "1.2", + "1.2.3-0123", + "1.2.3-0123.0123", + "1.1.2+.123", + "+invalid", + "-invalid", + "-invalid+invalid", + "-invalid.01", + "alpha", + "alpha.beta", + "alpha.beta.1", + "alpha.1", + "alpha+beta", + "alpha_beta", + "alpha.", + "alpha..", + "beta", + "1.0.0-alpha_beta", + "-alpha.", + "1.0.0-alpha..", + "1.0.0-alpha..1", + "1.0.0-alpha...1", + "1.0.0-alpha....1", + "1.0.0-alpha.....1", + "1.0.0-alpha......1", + "1.0.0-alpha.......1", + "01.1.1", + "1.01.1", + "1.1.01", + "1.2", + "1.2.3.DEV", + "1.2-SNAPSHOT", + "1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788", + "1.2-RC-SNAPSHOT", + "-1.0.3-gamma+b7718", + "+justmeta", + "9.8.7+meta+meta", + "9.8.7-whatever+meta+meta", + "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12", +]; + +pub fn id_for(version: u8, schema: &str) -> String { + format!("{SCHEMA_BASE}{version}/{schema}.schema.json") +} + +fn spec_compiler() -> Compiler { + let mut compiler = Compiler::new(); + compiler.enable_format_assertions(); + compiler.register_format(boon::Format { + name: "path", + func: is_path, + }); + compiler.register_format(boon::Format { + name: "license", + func: is_license, + }); + compiler +} + +pub fn new_compiler(dir: &str) -> Result> { + let mut compiler = spec_compiler(); + + let paths = fs::read_dir(dir)?; + for path in paths { + let path = path?.path(); + let bn = path.file_name().unwrap().to_str().unwrap(); + if bn.ends_with(".schema.json") { + let schema: Value = serde_json::from_reader(File::open(path.clone())?)?; + if let Value::String(s) = &schema["$id"] { + // Add the schema to the compiler. + compiler.add_resource(s, schema.to_owned())?; + } else { + panic!("Unable to find ID in {}", path.display()); + } + } else { + println!("Skipping {}", path.display()); + } + } + + Ok(compiler) +} + +fn is_path(v: &Value) -> Result<(), Box> { + let Value::String(s) = v else { + return Ok(()); // applicable only on strings + }; + + let path = Path::new(s); + for c in path.components() { + match c { + Component::ParentDir => Err("parent dir")?, + Component::Prefix(_) => Err("windows path")?, + Component::RootDir => Err("absolute path")?, + _ => (), + }; + } + + Ok(()) +} + +fn is_license(v: &Value) -> Result<(), Box> { + let Value::String(s) = v else { + return Ok(()); // applicable only on strings + }; + _ = spdx::Expression::parse(s)?; + Ok(()) +} + +pub fn test_term_schema(mut compiler: Compiler, version: u8) -> Result<(), Box> { + let mut schemas = Schemas::new(); + let id = id_for(version, "term"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_term in [ + ("two chars", json!("hi")), + ("underscores", json!("hi_this_is_a_valid_term")), + ("dashes", json!("hi-this-is-a-valid-term")), + ("punctuation", json!("!@#$%^&*()-=+{}<>,?")), + ("unicode", json!("😀🍒📸")), + ] { + if let Err(e) = schemas.validate(&valid_term.1, idx) { + panic!("term {} failed: {e}", valid_term.0); + } + } + + for invalid_term in [ + ("array", json!([])), + ("empty string", json!("")), + ("too short", json!("x")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ("space", json!("hi there")), + ("slash", json!("hi/there")), + ("backslash", json!("hi\\there")), + ("null byte", json!("hi\x00there")), + ] { + if schemas.validate(&invalid_term.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_term.0) + } + } + + // Schema v1 allows a dot but v2 does not. + let dot_term = json!("this.that"); + let res = schemas.validate(&dot_term, idx); + if version == 1 { + if let Err(e) = res { + panic!("term with dot failed: {e}"); + } + } else if res.is_ok() { + panic!("term with dot unexpectedly passed!") + } + + Ok(()) +} + +pub fn test_tags_schema(mut compiler: Compiler, version: u8) -> Result<(), Box> { + // Load the schemas and compile the tags schema. + let mut schemas = Schemas::new(); + let id = id_for(version, "tags"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_tags in [ + ("two chars", json!(["hi"])), + ("underscores", json!(["hi_this_is_a_valid_tags"])), + ("dashes", json!(["hi-this-is-a-valid-tags"])), + ("punctuation", json!(["!@#$%^&*()-=+{}<>,.?"])), + ("unicode", json!(["😀🍒📸"])), + ("space", json!(["hi there"])), + ("multiple", json!(["testing", "json", "😀🍒📸"])), + ("max length", json!(["x".repeat(255)])), + ] { + if let Err(e) = schemas.validate(&valid_tags.1, idx) { + panic!("extension {} failed: {e}", valid_tags.0); + } + } + + for invalid_tags in [ + ("empty array", json!([])), + ("string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ("true tag", json!([true])), + ("false tag", json!([false])), + ("null tag", json!([null])), + ("object tag", json!([{}])), + ("empty tag", json!([""])), + ("too short", json!(["x"])), + ("object tag", json!({})), + ("slash", json!(["hi/there"])), + ("backslash", json!(["hi\\there"])), + ("null byte", json!(["hi\x00there"])), + ("too long", json!(["x".repeat(256)])), + ("dupe", json!(["abc", "abc"])), + ] { + if schemas.validate(&invalid_tags.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_tags.0) + } + } + + Ok(()) +} + +pub fn test_schema_version(version: u8) -> Result<(), Box> { + let mut compiler = spec_compiler(); + let mut loaded: HashMap> = HashMap::new(); + + let paths = fs::read_dir(format!("./schema/v{version}"))?; + for path in paths { + let path = path?.path(); + let bn = path.file_name().unwrap().to_str().unwrap(); + if bn.ends_with(".schema.json") { + let schema: Value = serde_json::from_reader(File::open(path.clone())?)?; + if let Value::String(s) = &schema["$id"] { + // Make sure that the ID is correct. + assert_eq!(format!("https://pgxn.org/meta/v{version}/{bn}"), *s); + + // Add the schema to the compiler. + compiler.add_resource(s, schema.to_owned())?; + + // Grab the examples, if any, to test later. + if let Value::Array(a) = &schema["examples"] { + loaded.insert(s.clone(), a.to_owned()); + } else { + loaded.insert(s.clone(), Vec::new()); + } + } else { + panic!("Unable to find ID in {}", path.display()); + } + } else { + println!("Skipping {}", path.display()); + } + } + + // Make sure we found schemas. + assert!(!loaded.is_empty(), "No schemas loaded!"); + + // Make sure each schema we loaded is valid. + let mut schemas = Schemas::new(); + for (id, examples) in loaded { + let index = compiler.compile(id.as_str(), &mut schemas)?; + println!("{} ok", id); + + // Test the schema's examples. + for (i, example) in examples.iter().enumerate() { + if let Err(e) = schemas.validate(example, index) { + panic!("Example {i} failed: {e}"); + } + // println!(" Example {i} ok"); + } + } + + Ok(()) +} diff --git a/tests/v1_schema_test.rs b/tests/v1_schema_test.rs index 960ce72..4ce8ec4 100644 --- a/tests/v1_schema_test.rs +++ b/tests/v1_schema_test.rs @@ -1,89 +1,20 @@ -use std::fs::{self, File}; +use std::error::Error; +use std::fs::File; use std::io::{prelude::*, BufReader}; -use std::{collections::HashMap, error::Error}; -use boon::{Compiler, Schemas}; +use boon::Schemas; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -const SCHEMA_BASE: &str = "https://pgxn.org/meta/v1"; -const SCHEMA_ID: &str = "https://pgxn.org/meta/v1/distribution.schema.json"; +// importing common module. +mod common; +use common::*; + +const SCHEMA_VERSION: u8 = 1; #[test] fn test_schema_v1() -> Result<(), Box> { - let mut compiler = Compiler::new(); - compiler.enable_format_assertions(); - let mut loaded: HashMap> = HashMap::new(); - - let paths = fs::read_dir("./schema/v1")?; - for path in paths { - let path = path?.path(); - let bn = path.file_name().unwrap().to_str().unwrap(); - if bn.ends_with(".schema.json") { - let schema: Value = serde_json::from_reader(File::open(path.clone())?)?; - if let Value::String(s) = &schema["$id"] { - // Make sure that the ID is correct. - assert_eq!(format!("https://pgxn.org/meta/v1/{bn}"), *s); - - // Add the schema to the compiler. - compiler.add_resource(s, schema.to_owned())?; - - // Grab the examples, if any, to test later. - if let Value::Array(a) = &schema["examples"] { - loaded.insert(s.clone(), a.to_owned()); - } else { - loaded.insert(s.clone(), Vec::new()); - } - } else { - panic!("Unable to find ID in {}", path.display()); - } - } else { - println!("Skipping {}", path.display()); - } - } - - // Make sure we found schemas. - assert!(!loaded.is_empty(), "No schemas loaded!"); - - // Make sure each schema we loaded is valid. - let mut schemas = Schemas::new(); - for (id, examples) in loaded { - let index = compiler.compile(id.as_str(), &mut schemas)?; - println!("{} ok", id); - - // Test the schema's examples. - for (i, example) in examples.iter().enumerate() { - if let Err(e) = schemas.validate(example, index) { - panic!("Example {i} failed: {e}"); - } - // println!(" Example {i} ok"); - } - } - - Ok(()) -} - -fn new_compiler(dir: &str) -> Result> { - let mut compiler = Compiler::new(); - compiler.enable_format_assertions(); - let paths = fs::read_dir(dir)?; - for path in paths { - let path = path?.path(); - let bn = path.file_name().unwrap().to_str().unwrap(); - if bn.ends_with(".schema.json") { - let schema: Value = serde_json::from_reader(File::open(path.clone())?)?; - if let Value::String(s) = &schema["$id"] { - // Add the schema to the compiler. - compiler.add_resource(s, schema.to_owned())?; - } else { - panic!("Unable to find ID in {}", path.display()); - } - } else { - println!("Skipping {}", path.display()); - } - } - - Ok(compiler) + test_schema_version(SCHEMA_VERSION) } #[derive(Deserialize, Serialize)] @@ -98,7 +29,8 @@ fn test_corpus_v1_valid() -> Result<(), Box> { // Load the schemas and compile the root schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let index = compiler.compile(SCHEMA_ID, &mut schemas)?; + let id = id_for(SCHEMA_VERSION, "distribution"); + let index = compiler.compile(&id, &mut schemas)?; // Test each meta JSON in the corpus. let file = File::open("tests/corpus/v1/valid.txt")?; @@ -120,7 +52,8 @@ fn test_corpus_v1_invalid() -> Result<(), Box> { // Load the schemas and compile the root schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let index = compiler.compile(SCHEMA_ID, &mut schemas)?; + let id = id_for(SCHEMA_VERSION, "distribution"); + let index = compiler.compile(&id, &mut schemas)?; // Test each meta JSON in the corpus. let file = File::open("tests/corpus/v1/invalid.txt")?; @@ -143,187 +76,33 @@ fn test_corpus_v1_invalid() -> Result<(), Box> { #[test] fn test_v1_term() -> Result<(), Box> { // Load the schemas and compile the term schema. - let mut compiler = new_compiler("schema/v1")?; - let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/term.schema.json"); - let idx = compiler.compile(&id, &mut schemas)?; - - for valid_term in [ - ("two chars", json!("hi")), - ("underscores", json!("hi_this_is_a_valid_term")), - ("dashes", json!("hi-this-is-a-valid-term")), - ("punctuation", json!("!@#$%^&*()-=+{}<>,.?")), - ("unicode", json!("😀🍒📸")), - ] { - if let Err(e) = schemas.validate(&valid_term.1, idx) { - panic!("extension {} failed: {e}", valid_term.0); - } - } - - for invalid_term in [ - ("array", json!([])), - ("empty string", json!("")), - ("too short", json!("x")), - ("true", json!(true)), - ("false", json!(false)), - ("null", json!(null)), - ("object", json!({})), - ("space", json!("hi there")), - ("slash", json!("hi/there")), - ("backslash", json!("hi\\there")), - ("null byte", json!("hi\x00there")), - ] { - if schemas.validate(&invalid_term.1, idx).is_ok() { - panic!("{} unexpectedly passed!", invalid_term.0) - } - } - - Ok(()) + let compiler = new_compiler("schema/v1")?; + test_term_schema(compiler, SCHEMA_VERSION) } #[test] fn test_v1_tags() -> Result<(), Box> { // Load the schemas and compile the tags schema. - let mut compiler = new_compiler("schema/v1")?; - let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/tags.schema.json"); - let idx = compiler.compile(&id, &mut schemas)?; - - for valid_tags in [ - ("two chars", json!(["hi"])), - ("underscores", json!(["hi_this_is_a_valid_tags"])), - ("dashes", json!(["hi-this-is-a-valid-tags"])), - ("punctuation", json!(["!@#$%^&*()-=+{}<>,.?"])), - ("unicode", json!(["😀🍒📸"])), - ("space", json!(["hi there"])), - ("multiple", json!(["testing", "json", "😀🍒📸"])), - ("max length", json!(["x".repeat(255)])), - ] { - if let Err(e) = schemas.validate(&valid_tags.1, idx) { - panic!("extension {} failed: {e}", valid_tags.0); - } - } - - for invalid_tags in [ - ("empty array", json!([])), - ("string", json!("")), - ("true", json!(true)), - ("false", json!(false)), - ("null", json!(null)), - ("object", json!({})), - ("true tag", json!([true])), - ("false tag", json!([false])), - ("null tag", json!([null])), - ("object tag", json!([{}])), - ("empty tag", json!([""])), - ("too short", json!(["x"])), - ("object tag", json!({})), - ("slash", json!(["hi/there"])), - ("backslash", json!(["hi\\there"])), - ("null byte", json!(["hi\x00there"])), - ("too long", json!("x".repeat(256))), - ] { - if schemas.validate(&invalid_tags.1, idx).is_ok() { - panic!("{} unexpectedly passed!", invalid_tags.0) - } - } - - Ok(()) + let compiler = new_compiler("schema/v1")?; + test_tags_schema(compiler, SCHEMA_VERSION) } -// https://regex101.com/r/Ly7O1x/3/ -const VALID_VERSIONS: &[&str] = &[ - "0.0.4", - "1.2.3", - "10.20.30", - "1.1.2-prerelease+meta", - "1.1.2+meta", - "1.1.2+meta-valid", - "1.0.0-alpha", - "1.0.0-beta", - "1.0.0-alpha.beta", - "1.0.0-alpha.beta.1", - "1.0.0-alpha.1", - "1.0.0-alpha0.valid", - "1.0.0-alpha.0valid", - "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay", - "1.0.0-rc.1+build.1", - "2.0.0-rc.1+build.123", - "1.2.3-beta", - "10.2.3-DEV-SNAPSHOT", - "1.2.3-SNAPSHOT-123", - "1.0.0", - "2.0.0", - "1.1.7", - "2.0.0+build.1848", - "2.0.1-alpha.1227", - "1.0.0-alpha+beta", - "1.2.3----RC-SNAPSHOT.12.9.1--.12+788", - "1.2.3----R-S.12.9.1--.12+meta", - "1.2.3----RC-SNAPSHOT.12.9.1--.12", - "1.0.0+0.build.1-rc.10000aaa-kk-0.1", - "1.0.0-0A.is.legal", -]; - -const INVALID_VERSIONS: &[&str] = &[ - "1", - "1.2", - "1.2.3-0123", - "1.2.3-0123.0123", - "1.1.2+.123", - "+invalid", - "-invalid", - "-invalid+invalid", - "-invalid.01", - "alpha", - "alpha.beta", - "alpha.beta.1", - "alpha.1", - "alpha+beta", - "alpha_beta", - "alpha.", - "alpha..", - "beta", - "1.0.0-alpha_beta", - "-alpha.", - "1.0.0-alpha..", - "1.0.0-alpha..1", - "1.0.0-alpha...1", - "1.0.0-alpha....1", - "1.0.0-alpha.....1", - "1.0.0-alpha......1", - "1.0.0-alpha.......1", - "01.1.1", - "1.01.1", - "1.1.01", - "1.2", - "1.2.3.DEV", - "1.2-SNAPSHOT", - "1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788", - "1.2-RC-SNAPSHOT", - "-1.0.3-gamma+b7718", - "+justmeta", - "9.8.7+meta+meta", - "9.8.7-whatever+meta+meta", - "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12", -]; - #[test] fn test_v1_version() -> Result<(), Box> { // Load the schemas and compile the version schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/version.schema.json"); + let id = id_for(SCHEMA_VERSION, "version"); let idx = compiler.compile(&id, &mut schemas)?; - for valid_version in VALID_VERSIONS { + for valid_version in VALID_SEMVERS { let vv = json!(valid_version); if let Err(e) = schemas.validate(&vv, idx) { panic!("extension {} failed: {e}", valid_version); } } - for invalid_version in INVALID_VERSIONS { + for invalid_version in INVALID_SEMVERS { let iv = json!(invalid_version); if schemas.validate(&iv, idx).is_ok() { panic!("{} unexpectedly passed!", invalid_version) @@ -338,10 +117,10 @@ fn test_v1_version_range() -> Result<(), Box> { // Load the schemas and compile the version_range schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/version_range.schema.json"); + let id = id_for(SCHEMA_VERSION, "version_range"); let idx = compiler.compile(&id, &mut schemas)?; - for valid_version in VALID_VERSIONS { + for valid_version in VALID_SEMVERS { for op in ["", "==", "!=", ">", "<", ">=", "<="] { for append in [ "", @@ -371,7 +150,7 @@ fn test_v1_version_range() -> Result<(), Box> { } } - // Bar integer 0 allowed. + // Bare integer 0 allowed. let zero = json!(0); if let Err(e) = schemas.validate(&zero, idx) { panic!("extension {} failed: {e}", zero); @@ -385,7 +164,7 @@ fn test_v1_version_range() -> Result<(), Box> { } } - for invalid_version in INVALID_VERSIONS { + for invalid_version in INVALID_SEMVERS { for op in ["", "==", "!=", ">", "<", ">=", "<="] { for append in [ "", @@ -409,7 +188,7 @@ fn test_v1_license() -> Result<(), Box> { // Load the schemas and compile the license schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/license.schema.json"); + let id = id_for(SCHEMA_VERSION, "license"); let idx = compiler.compile(&id, &mut schemas)?; // Test valid license values. @@ -463,6 +242,7 @@ fn test_v1_license() -> Result<(), Box> { json!([]), json!({}), json!({"foo": ":hello"}), + json!(["mit", "mit"]), ] { if schemas.validate(&invalid_license, idx).is_ok() { panic!("{} unexpectedly passed!", invalid_license) @@ -477,7 +257,7 @@ fn test_v1_provides() -> Result<(), Box> { // Load the schemas and compile the provides schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/provides.schema.json"); + let id = id_for(SCHEMA_VERSION, "provides"); let idx = compiler.compile(&id, &mut schemas)?; for valid_provides in [ @@ -576,20 +356,20 @@ fn test_v1_extension() -> Result<(), Box> { // Load the schemas and compile the extension schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/extension.schema.json"); + let id = id_for(SCHEMA_VERSION, "extension"); let idx = compiler.compile(&id, &mut schemas)?; for valid_extension in [ ( "required fields", - json!( { + json!({ "file": "widget.sql", "version": "0.26.0", }), ), ( "with abstract", - json!( { + json!({ "file": "widget.sql", "version": "0.26.0", "abstract": "This and that", @@ -641,7 +421,7 @@ fn test_v1_extension() -> Result<(), Box> { ), ( "bare x_", - json!( { + json!({ "file": "widget.sql", "version": "0.26.0", "x_": "hi", @@ -752,7 +532,7 @@ fn test_v1_maintainer() -> Result<(), Box> { // Load the schemas and compile the maintainer schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/maintainer.schema.json"); + let id = id_for(SCHEMA_VERSION, "maintainer"); let idx = compiler.compile(&id, &mut schemas)?; for valid_maintainer in [ @@ -787,6 +567,7 @@ fn test_v1_maintainer() -> Result<(), Box> { ("false", json!(false)), ("null", json!(null)), ("object", json!({})), + ("dupe", json!(["x", "x"])), ] { if schemas.validate(&invalid_maintainer.1, idx).is_ok() { panic!("{} unexpectedly passed!", invalid_maintainer.0) @@ -801,7 +582,7 @@ fn test_v1_meta_spec() -> Result<(), Box> { // Load the schemas and compile the maintainer schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/meta-spec.schema.json"); + let id = id_for(SCHEMA_VERSION, "meta-spec"); let idx = compiler.compile(&id, &mut schemas)?; for valid_meta_spec in [ @@ -855,7 +636,7 @@ fn test_v1_bugtracker() -> Result<(), Box> { // Load the schemas and compile the maintainer schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/bugtracker.schema.json"); + let id = id_for(SCHEMA_VERSION, "bugtracker"); let idx = compiler.compile(&id, &mut schemas)?; for valid_bugtracker in [ @@ -909,7 +690,7 @@ fn test_v1_no_index() -> Result<(), Box> { // Load the schemas and compile the maintainer schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/no_index.schema.json"); + let id = id_for(SCHEMA_VERSION, "no_index"); let idx = compiler.compile(&id, &mut schemas)?; for valid_no_index in [ @@ -959,6 +740,8 @@ fn test_v1_no_index() -> Result<(), Box> { ("directory null", json!({"directory": null})), ("unknown field", json!({"file": ["x"], "hi": 0})), ("bare x_", json!({"file": ["x"], "x_": 0})), + ("dupe", json!({"file": ["x", "x"]})), + ("dupe", json!({"dir": ["x", "x"]})), ("missing required", json!({"x_y": 0})), ] { if schemas.validate(&invalid_no_index.1, idx).is_ok() { @@ -974,7 +757,7 @@ fn test_v1_prereq_relationship() -> Result<(), Box> { // Load the schemas and compile the maintainer schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/prereq_relationship.schema.json"); + let id = id_for(SCHEMA_VERSION, "prereq_relationship"); let idx = compiler.compile(&id, &mut schemas)?; for valid_prereq_relationship in [ @@ -1027,7 +810,7 @@ fn test_v1_prereq_phase() -> Result<(), Box> { // Load the schemas and compile the maintainer schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/prereq_phase.schema.json"); + let id = id_for(SCHEMA_VERSION, "prereq_phase"); let idx = compiler.compile(&id, &mut schemas)?; for valid_prereq_phase in [ @@ -1139,7 +922,7 @@ fn test_v1_prereqs() -> Result<(), Box> { // Load the schemas and compile the maintainer schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/prereqs.schema.json"); + let id = id_for(SCHEMA_VERSION, "prereqs"); let idx = compiler.compile(&id, &mut schemas)?; for valid_prereqs in [ @@ -1287,7 +1070,7 @@ fn test_v1_repository() -> Result<(), Box> { // Load the schemas and compile the repository schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/repository.schema.json"); + let id = id_for(SCHEMA_VERSION, "repository"); let idx = compiler.compile(&id, &mut schemas)?; for valid_repository in [ @@ -1364,7 +1147,7 @@ fn test_v1_resources() -> Result<(), Box> { // Load the schemas and compile the resources schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let id = format!("{SCHEMA_BASE}/resources.schema.json"); + let id = id_for(SCHEMA_VERSION, "resources"); let idx = compiler.compile(&id, &mut schemas)?; for valid_resources in [ @@ -1501,7 +1284,8 @@ fn test_v1_distribution() -> Result<(), Box> { // Load the schemas and compile the distribution schema. let mut compiler = new_compiler("schema/v1")?; let mut schemas = Schemas::new(); - let idx = compiler.compile(SCHEMA_ID, &mut schemas)?; + let id = id_for(SCHEMA_VERSION, "distribution"); + let idx = compiler.compile(&id, &mut schemas)?; // Make sure the valid distribution is in fact valid. let meta = valid_distribution(); @@ -2062,11 +1846,23 @@ fn test_v1_distribution() -> Result<(), Box> { ("number name", |m: &mut Obj| { m.insert("name".to_string(), json!(42)); }), + ("empty description", |m: &mut Obj| { + m.insert("description".to_string(), json!("")); + }), ("null description", |m: &mut Obj| { m.insert("description".to_string(), json!(null)); }), - ("null description", |m: &mut Obj| { - m.insert("generated_by".to_string(), json!(null)); + ("array description", |m: &mut Obj| { + m.insert("description".to_string(), json!([])); + }), + ("object description", |m: &mut Obj| { + m.insert("description".to_string(), json!({})); + }), + ("bool description", |m: &mut Obj| { + m.insert("description".to_string(), json!(false)); + }), + ("number description", |m: &mut Obj| { + m.insert("description".to_string(), json!(42)); }), ("array generated_by", |m: &mut Obj| { m.insert("generated_by".to_string(), json!([])); @@ -2086,18 +1882,6 @@ fn test_v1_distribution() -> Result<(), Box> { ("null generated_by", |m: &mut Obj| { m.insert("generated_by".to_string(), json!(null)); }), - ("array generated_by", |m: &mut Obj| { - m.insert("generated_by".to_string(), json!([])); - }), - ("object generated_by", |m: &mut Obj| { - m.insert("generated_by".to_string(), json!({})); - }), - ("bool generated_by", |m: &mut Obj| { - m.insert("generated_by".to_string(), json!(false)); - }), - ("number generated_by", |m: &mut Obj| { - m.insert("generated_by".to_string(), json!(42)); - }), ("null tags", |m: &mut Obj| { m.insert("tags".to_string(), json!(null)); }), diff --git a/tests/v2_schema_test.rs b/tests/v2_schema_test.rs new file mode 100644 index 0000000..45c21b3 --- /dev/null +++ b/tests/v2_schema_test.rs @@ -0,0 +1,3333 @@ +use std::error::Error; + +use boon::Schemas; +use serde_json::{json, Map, Value}; + +// importing common module. +mod common; +use common::*; + +const SCHEMA_VERSION: u8 = 2; + +#[test] +fn test_schema_v2() -> Result<(), Box> { + test_schema_version(SCHEMA_VERSION) +} + +#[test] +fn test_v2_term() -> Result<(), Box> { + // Load the schemas and compile the term schema. + let compiler = new_compiler("schema/v2")?; + test_term_schema(compiler, SCHEMA_VERSION) +} + +#[test] +fn test_v2_tags() -> Result<(), Box> { + // Load the schemas and compile the tags schema. + let compiler = new_compiler("schema/v2")?; + test_tags_schema(compiler, SCHEMA_VERSION) +} + +#[test] +fn test_v2_semver() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "semver"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_version in VALID_SEMVERS { + let vv = json!(valid_version); + if valid_version.contains('+') { + // Metadata is forbidden in v2 semvers. + if schemas.validate(&vv, idx).is_ok() { + panic!("{} unexpectedly passed!", valid_version) + } + } else if let Err(e) = schemas.validate(&vv, idx) { + panic!("{} failed: {e}", valid_version); + } + } + + for invalid_version in INVALID_SEMVERS { + let iv = json!(invalid_version); + if schemas.validate(&iv, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_version) + } + } + + Ok(()) +} + +#[test] +fn test_v2_path() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "path"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Test valid paths. + for valid in [ + json!("README.txt"), + json!(".git"), + json!("src/pair.c"), + json!(".github/workflows/"), + json!("this\\\\and\\\\that.txt"), + ] { + if let Err(e) = schemas.validate(&valid, idx) { + panic!("{} failed: {e}", valid); + } + } + + // Test invalid paths. + for invalid in [ + json!("\\foo.md"), + json!("this\\and\\that.txt"), + json!("/absolute/path"), + // Enforced only by custom format for now. + // https://github.com/santhosh-tekuri/boon/issues/19 + json!("../outside/path"), + json!("thing/../other"), + json!(null), + json!(""), + json!("C:\\foo"), + ] { + if schemas.validate(&invalid, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid) + } + } + Ok(()) +} + +#[test] +fn test_v2_glob() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "glob"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Test valid globs. + for valid in [ + json!("README.txt"), + json!("/.git"), + json!("/src/pair.c"), + json!("/src/private.*"), + json!("*.html"), + json!("*.?tml"), + json!("foo.?tml"), + json!("[xX]_*.*"), + json!("[a-z]*.txt"), + json!("this\\\\and\\\\that.txt"), + ] { + if let Err(e) = schemas.validate(&valid, idx) { + panic!("{} failed: {e}", valid); + } + } + + // Test invalid globs. + for invalid in [ + json!("this\\and\\that.txt"), + json!(null), + json!(""), + json!("C:\\foo"), + ] { + if schemas.validate(&invalid, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid) + } + } + + Ok(()) +} + +#[test] +fn test_v2_version_range() -> Result<(), Box> { + // Load the schemas and compile the version_range schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "version_range"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_version in [VALID_SEMVERS, &["1", "3", "2.1", "3.14"]].concat() { + for op in ["", "==", "!=", ">", "<", ">=", "<="] { + for append in [ + "", + ",<= 1.1.2+meta", + ",>= 2.0.0, 1.5.6", + ",>= 2.0, 1.5", + ",>= 2, ==1", + ", >1.2.0, != 12.0.0, < 19.2.0", + ] { + let range = json!(format!("{}{}{}", op, valid_version, append)); + if let Err(e) = schemas.validate(&range, idx) { + panic!("{} failed: {e}", range); + } + + // Version zero must not appear in a range. + let range = json!(format!("{}{}{},0", op, valid_version, append)); + if schemas.validate(&range, idx).is_ok() { + panic!("{} unexpectedly passed!", range) + } + } + } + + // Test with unknown operators. + for bad_op in ["!", "=", "<>", "=>", "=<"] { + let range = json!(format!("{}{}", bad_op, valid_version)); + if schemas.validate(&range, idx).is_ok() { + panic!("{} unexpectedly passed!", range) + } + } + } + + // Bare integer 0 allowed. + let zero = json!(0); + if let Err(e) = schemas.validate(&zero, idx) { + panic!("{} failed: {e}", zero); + } + + // But version 0 cannot appear with any range operator or in any range. + for op in ["", "==", "!=", ">", "<", ">=", "<="] { + let range = json!(format!("{op}0")); + if let Err(e) = schemas.validate(&range, idx) { + panic!("{} failed: {e}", range); + } + } + + // Test invalid ranges. + for invalid_range in [ + json!("x.y.z"), + json!(null), + json!(""), + json!(">2.0 and <3.0"), + json!("==2.0 or ==3.0"), + ] { + if schemas.validate(&invalid_range, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_range) + } + } + + Ok(()) +} + +#[test] +fn test_v2_license() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "license"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Test valid relative licenses. + for valid_license in [ + json!("MIT"), + json!("PostgreSQL"), + json!("Apache-2.0 OR MIT"), + json!("Apache-2.0 OR MIT OR PostgreSQL"), + json!("Apache-2.0 AND MIT"), + json!("MIT OR Apache-2.0 AND BSD-2-Clause"), + json!("(MIT AND (LGPL-2.1-or-later OR BSD-3-Clause))"), + json!("((Apache-2.0 WITH LLVM-exception) OR Apache-2.0) AND OpenSSL OR MIT"), + json!("Apache-2.0 WITH LLVM-exception OR Apache-2.0 AND (OpenSSL OR MIT)"), + json!("Apache-2.0 WITH LLVM-exception OR (Apache-2.0 AND OpenSSL) OR MIT"), + json!("((((Apache-2.0 WITH LLVM-exception) OR (Apache-2.0)) AND (OpenSSL)) OR (MIT))"), + json!("CDDL-1.0+"), + json!("LicenseRef-23"), + json!("LicenseRef-MIT-Style-1"), + json!("DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2"), + ] { + if let Err(e) = schemas.validate(&valid_license, idx) { + panic!("{} failed: {e}", valid_license); + } + } + + // Test invalid licenses. + for invalid_license in [ + json!(""), + json!(null), + json!("0"), + json!(0), + json!("\n\t"), + json!("()"), + json!("AND"), + json!("OR"), + ] { + if schemas.validate(&invalid_license, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_license) + } + } + Ok(()) +} + +#[test] +fn test_v2_purl() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "purl"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Test valid relative purls. + for valid_purl in [ + json!("pkg:pgxn/pgtap"), + json!("pkg:postgres/pg_regress"), + json!("pkg:generic/python3"), + json!("pkg:pypi/pyarrow@11.0.0"), + json!("pkg:type/namespace/name"), + json!("pkg:type/namespace/name@version"), + json!("pkg:type/namespace/name@version?qualifiers"), + json!("pkg:type/namespace/name@version?qualifiers#subpath"), + ] { + if let Err(e) = schemas.validate(&valid_purl, idx) { + panic!("{} failed: {e}", valid_purl); + } + } + + // Test invalid purls. + for invalid_purl in [ + json!("http://example.com"), + json!("https://example.com"), + json!("mailto:hi@example.com"), + json!(null), + json!("0"), + json!(0), + json!("\n\t"), + json!("()"), + json!("AND"), + json!("OR"), + ] { + if schemas.validate(&invalid_purl, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_purl) + } + } + Ok(()) +} + +#[test] +fn test_v2_platform() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "platform"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Test valid relative platforms. + for os in [ + "any", + "aix", + "android", + "darwin", + "dragonfly", + "freebsd", + "illumos", + "ios", + "js", + "linux", + "netbsd", + "openbsd", + "plan9", + "solaris", + "wasip1", + "windows", + ] { + let platform = json!(os); + if let Err(e) = schemas.validate(&platform, idx) { + panic!("path pattern {} failed: {e}", platform); + } + + let architectures = [ + "386", "amd64", "arm", "arm64", "loong64", "mips", "mips64", "mips64le", "mipsle", + "ppc64", "ppc64le", "riscv64", "s390x", "sparc64", "wasm", + ]; + + for arch in architectures { + let platform = json!(format!("{os}-{arch}")); + if let Err(e) = schemas.validate(&platform, idx) { + panic!("path pattern {} failed: {e}", platform); + } + } + + for version in [ + VALID_SEMVERS, + &["1.0", "3.2.5", "2.1+beta", "3.14", "16.beta1", "17.+foo"], + ] + .concat() + { + if version.contains('-') { + continue; + } + let platform = json!(format!("{os}-{version}")); + if let Err(e) = schemas.validate(&platform, idx) { + panic!("path pattern {} failed: {e}", platform); + } + + for arch in architectures { + let platform = json!(format!("{os}-{version}-{arch}")); + if let Err(e) = schemas.validate(&platform, idx) { + panic!("path pattern {} failed: {e}", platform); + } + } + } + } + + // Test invalid platforms. + for invalid_platform in [ + json!("darwin amd64"), + json!("linux/amd64"), + json!("x86_64"), + json!("darwin_23.5.0_arm64"), + json!(null), + json!("0"), + json!(0), + json!("\n\t"), + json!("()"), + ] { + if schemas.validate(&invalid_platform, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_platform) + } + } + Ok(()) +} + +#[test] +fn test_v2_platforms() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "platforms"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Test valid platforms. + for valid in [ + ("two", json!(["darwin", "linux"])), + ("three", json!(["darwin", "linux", "windows"])), + ("versions", json!(["musllinux-2.5", "gnulinux-3.3"])), + ( + "architectures", + json!(["musllinux-amd64", "gnulinux-amd64"]), + ), + ("full", json!(["musllinux-2.5-amd64", "gnulinux-3.3-amd64"])), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + // Test invalid platforms. + for invalid in [ + json!(["darwin amd64"]), + json!(["linux/amd64"]), + json!(["x86_64"]), + json!(["darwin_23.5.0_arm64"]), + json!([]), + json!([null]), + json!(["0"]), + json!([0]), + json!({}), + json!(true), + json!(42), + json!(["\n\t"]), + json!(["()"]), + json!(["darwin", "x86_64"]), + ] { + if schemas.validate(&invalid, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid) + } + } + Ok(()) +} + +#[test] +fn test_v2_maintainers() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "maintainers"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_maintainer in [ + ( + "min name length", + json!([{"name": "x", "email": "x@example.com"}]), + ), + ( + "name and email", + json!([{"name": "David E. Wheeler", "email": "theory@pgxn.org"}]), + ), + ( + "name and URL", + json!([{"name": "David E. Wheeler", "url": "https://pgxn.org/user/theory"}]), + ), + ( + "two names and emails", + json!([ + {"name": "Josh Berkus", "email": "jberkus@pgxn.org"}, + ]), + ), + ( + "two names and URLs", + json!([ + {"name": "Josh Berkus", "url": "https://pgxn.org/user/jberkus"}, + {"name": "David E. Wheeler", "url": "https://pgxn.org/user/theory"}, + ]), + ), + ( + "all fields", + json!([{ + "name": "David E. Wheeler", + "email": "theory@pgxn.org", + "url": "https://pgxn.org/user/theory", + }]), + ), + ( + "multiple all fields", + json!([ + { + "name": "David E. Wheeler", + "email": "theory@pgxn.org", + "url": "https://pgxn.org/user/theory", + }, + { + "name": "Josh Berkus", + "email": "jberkus@pgxn.org", + "url": "https://pgxn.org/user/jberkus", + }, + ]), + ), + ( + "custom x_", + json!([{"name": "x", "email": "x@example.com", "x_y": true}]), + ), + ( + "custom X_", + json!([{"name": "x", "email": "x@example.com", "X_z": true}]), + ), + ] { + if let Err(e) = schemas.validate(&valid_maintainer.1, idx) { + panic!("{} failed: {e}", valid_maintainer.0); + } + } + + for invalid_maintainer in [ + ("empty array", json!([])), + ("empty string", json!("")), + ("string in array", json!(["hi", ""])), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ("true in array", json!([true])), + ("false in array", json!([false])), + ("null in array", json!([null])), + ("empty object", json!([{}])), + ("name only", json!([{"name": "hi"}])), + ("email only", json!([{"email": "hi@x.com"}])), + ("url only", json!([{"url": "x:y"}])), + ( + "url and email only", + json!([{"url": "x:y", "email": "hi@x.com"}]), + ), + ( + "dupe", + json!([ + {"name": "x", "email": "x@example.com"}, + {"name": "x", "email": "x@example.com"}, + ]), + ), + // Name + ("empty name", json!([{"name": "", "url": "x:y"}])), + ("null name", json!([{"name": null, "url": "x:y"}])), + ("bool name", json!([{"name": true, "url": "x:y"}])), + ("number name", json!([{"name": 42, "url": "x:y"}])), + ("array name", json!([{"name": [], "url": "x:y"}])), + ("object name", json!([{"name": {}, "url": "x:y"}])), + // Email: + ("invalid email", json!([{"name": "hi", "email": "not"}])), + ("empty email", json!([{"name": "hi", "email": ""}])), + ("null email", json!([{"name": "hi", "email": null}])), + ("bool email", json!([{"name": "hi", "email": false}])), + ("number email", json!([{"name": "hi", "email": 42}])), + ("array email", json!([{"name": "hi", "email": []}])), + ("object email", json!([{"name": "hi", "email": {}}])), + // URL + ("invalid url", json!([{"name": "hi", "url": "not a url"}])), + ("empty url", json!([{"name": "hi", "url": ""}])), + ("null url", json!([{"name": "hi", "url": null}])), + ("bool url", json!([{"name": "hi", "url": false}])), + ("number url", json!([{"name": "hi", "url": 42}])), + ("array url", json!([{"name": "hi", "url": []}])), + ("object url", json!([{"name": "hi", "url": {}}])), + // Custom + ( + "bare X_", + json!([{"name": "x", "email": "x@example.com", "X_": true}]), + ), + ( + "bare x_", + json!([{"name": "x", "email": "x@example.com", "x_": true}]), + ), + ] { + if schemas.validate(&invalid_maintainer.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_maintainer.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_extension() -> Result<(), Box> { + // Load the schemas and compile the extension schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "extension"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_extension in [ + ( + "required fields", + json!({ + "sql": "widget.sql", + "control": "widget.control", + }), + ), + ( + "with abstract", + json!({ + "sql": "widget.sql", + "control": "widget.control", + "abstract": "This and that", + }), + ), + ( + "all fields", + json!({ + "sql": "widget.sql", + "control": "widget.control", + "doc": "foo/bar.txt", + "abstract": "This and that", + "tle": true, + }), + ), + ( + "x field", + json!({ + "sql": "widget.sql", + "control": "widget.control", + "x_hi": true, + }), + ), + ( + "X field", + json!({ + "sql": "widget.sql", + "control": "widget.control", + "X_bar": 42, + }), + ), + ] { + if let Err(e) = schemas.validate(&valid_extension.1, idx) { + panic!("{} failed: {e}", valid_extension.0); + } + } + + for invalid_extension in [ + // Basics + ("array", json!([])), + ("string", json!("crank")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ( + "invalid field", + json!({"sql": "widget.sql", "control": "x.control", "foo": "hi", }), + ), + ( + "bare x_", + json!({ "sql": "widget.sql", "control": "x.control", "x_": "hi" }), + ), + ( + "bare X_", + json!({ "sql": "widget.sql", "control": "x.control", "X_": "hi" }), + ), + // sql + ("no sql", json!({"control": "x.control"})), + ("null sql", json!({"sql": null, "control": "x.control"})), + ("empty sql", json!({"sql": "", "control": "x.control"})), + ("number sql", json!({"sql": 42, "control": "x.control"})), + ("bool sql", json!({"sql": true, "control": "x.control"})), + ("array sql", json!({"sql": [], "control": "x.control"})), + ("object sql", json!({"sql": {}, "control": "x.control"})), + // control + ("no control", json!({"sql": "x.sql"})), + ("null control", json!({"control": null, "sql": "x.sql"})), + ("empty control", json!({"control": "", "sql": "x.sql"})), + ("number control", json!({"control": 42, "sql": "x.sql"})), + ("bool control", json!({"control": true, "sql": "x.sql"})), + ("array control", json!({"control": [], "sql": "x.sql"})), + ("object control", json!({"control": {}, "sql": "x.sql"})), + // doc + ( + "empty doc", + json!({"sql": "widget.sql", "control": "widget.control", "doc": ""}), + ), + ( + "null doc", + json!({"sql": "widget.sql", "control": "widget.control", "doc": null}), + ), + ( + "number doc", + json!({"sql": "widget.sql", "control": "widget.control", "doc": 42}), + ), + ( + "bool doc", + json!({"sql": "widget.sql", "control": "widget.control", "doc": true}), + ), + ( + "array doc", + json!({"sql": "widget.sql", "control": "widget.control", "doc": ["hi"]}), + ), + ( + "object doc", + json!({"sql": "widget.sql", "control": "widget.control", "doc": {}}), + ), + // abstract + ( + "empty abstract", + json!({"sql": "widget.sql", "control": "widget.control", "abstract": ""}), + ), + ( + "null abstract", + json!({"sql": "widget.sql", "control": "widget.control", "abstract": null}), + ), + ( + "number abstract", + json!({"sql": "widget.sql", "control": "widget.control", "abstract": 42}), + ), + ( + "bool abstract", + json!({"sql": "widget.sql", "control": "widget.control", "abstract": true}), + ), + ( + "array abstract", + json!({"sql": "widget.sql", "control": "widget.control", "abstract": ["hi"]}), + ), + ( + "object abstract", + json!({"sql": "widget.sql", "control": "widget.control", "abstract": {}}), + ), + // tle + ( + "empty tle", + json!({"sql": "widget.sql", "control": "widget.control", "tle": ""}), + ), + ( + "tle string", + json!({"sql": "widget.sql", "control": "widget.control", "tle": "true"}), + ), + ( + "null tle", + json!({"sql": "widget.sql", "control": "widget.control", "tle": null}), + ), + ( + "number tle", + json!({"sql": "widget.sql", "control": "widget.control", "tle": 42}), + ), + ( + "array tle", + json!({"sql": "widget.sql", "control": "widget.control", "tle": ["hi"]}), + ), + ( + "object tle", + json!({"sql": "widget.sql", "control": "widget.control", "tle": {}}), + ), + ] { + if schemas.validate(&invalid_extension.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_extension.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_module() -> Result<(), Box> { + // Load the schemas and compile the extension schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "module"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_module in [ + ("hook", json!({"type": "hook", "lib": "src/hook"})), + ("bgw", json!({"type": "bgw", "lib": "src/bgw"})), + ("extension", json!({"type": "extension", "lib": "src/ext"})), + ( + "with abstract", + json!({"type": "hook", "lib": "src/hook", "abstract": "This and that"}), + ), + ( + "server", + json!({"type": "hook", "lib": "src/hook", "preload": "server"}), + ), + ( + "session", + json!({"type": "hook", "lib": "src/hook", "preload": "session"}), + ), + ( + "all fields", + json!({"type": "hook", "lib": "src/hook", "doc": "bog.md", "abstract": "This and that", "preload": "session"}), + ), + ( + "x field", + json!({"type": "hook", "lib": "src/hook", "x_hi": true}), + ), + ( + "X field", + json!({"type": "hook", "lib": "src/hook", "X_bar": 42}), + ), + ] { + if let Err(e) = schemas.validate(&valid_module.1, idx) { + panic!("{} failed: {e}", valid_module.0); + } + } + + for invalid_module in [ + // Basics + ("array", json!([])), + ("string", json!("crank")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ( + "invalid field", + json!({"type": "bgw", "lib": "src/bgw", "foo": "hi"}), + ), + ( + "bare x_", + json!({ "type": "bgw", "lib": "src/bgw", "x_": "hi"}), + ), + ( + "bare X_", + json!({ "type": "bgw", "lib": "src/bgw", "X_": "hi"}), + ), + // type + ("no type", json!({"lib": "bgw"})), + ("empty type", json!({"type": "", "lib": "bgw"})), + ("invalid type", json!({"type": "burp", "lib": "bgw"})), + ("null type", json!({"type": null, "lib": "bgw"})), + ("empty type", json!({"type": "", "lib": "bgw"})), + ("number type", json!({"type": 42, "lib": "bgw"})), + ("bool type", json!({"type": true, "lib": "bgw"})), + ("array type", json!({"type": [], "lib": "bgw"})), + ("object type", json!({"type": {}, "lib": "bgw"})), + // lib + ("no lib", json!({"type": "bgw"})), + ("null lib", json!({"lib": null, "type": "bgw"})), + ("empty lib", json!({"lib": "", "type": "bgw"})), + ("number lib", json!({"lib": 42, "type": "bgw"})), + ("bool lib", json!({"lib": true, "type": "bgw"})), + ("array lib", json!({"lib": [], "type": "bgw"})), + ("object lib", json!({"lib": {}, "type": "bgw"})), + // doc + ( + "empty doc", + json!({"type": "bgw", "lib": "src/bgw", "doc": ""}), + ), + ( + "null doc", + json!({"type": "bgw", "lib": "src/bgw", "doc": null}), + ), + ( + "number doc", + json!({"type": "bgw", "lib": "src/bgw", "doc": 42}), + ), + ( + "bool doc", + json!({"type": "bgw", "lib": "src/bgw", "doc": true}), + ), + ( + "array doc", + json!({"type": "bgw", "lib": "src/bgw", "doc": ["hi"]}), + ), + ( + "object doc", + json!({"type": "bgw", "lib": "src/bgw", "doc": {}}), + ), + // abstract + ( + "empty abstract", + json!({"type": "bgw", "lib": "src/bgw", "abstract": ""}), + ), + ( + "null abstract", + json!({"type": "bgw", "lib": "src/bgw", "abstract": null}), + ), + ( + "number abstract", + json!({"type": "bgw", "lib": "src/bgw", "abstract": 42}), + ), + ( + "bool abstract", + json!({"type": "bgw", "lib": "src/bgw", "abstract": true}), + ), + ( + "array abstract", + json!({"type": "bgw", "lib": "src/bgw", "abstract": ["hi"]}), + ), + ( + "object abstract", + json!({"type": "bgw", "lib": "src/bgw", "abstract": {}}), + ), + // preload + ( + "empty preload", + json!({"type": "bgw", "lib": "src/bgw", "preload": ""}), + ), + ( + "invalid preload", + json!({"type": "bgw", "lib": "src/bgw", "preload": "startup"}), + ), + ( + "null preload", + json!({"type": "bgw", "lib": "src/bgw", "preload": null}), + ), + ( + "number preload", + json!({"type": "bgw", "lib": "src/bgw", "preload": 42}), + ), + ( + "bool preload", + json!({"type": "bgw", "lib": "src/bgw", "preload": true}), + ), + ( + "array preload", + json!({"type": "bgw", "lib": "src/bgw", "preload": ["hi"]}), + ), + ( + "object preload", + json!({"type": "bgw", "lib": "src/bgw", "preload": {}}), + ), + ] { + if schemas.validate(&invalid_module.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_module.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_app() -> Result<(), Box> { + // Load the schemas and compile the extension schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "app"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_app in [ + ("bin only", json!({"bin": "bog"})), + ("bin lang", json!({"bin": "bog", "lang": "perl"})), + ("short lang", json!({"bin": "bog", "lang": "sh"})), + ("doc", json!({"bin": "bog", "doc": "hi.md"})), + ("lib", json!({"bin": "bog", "lib": "lib"})), + ("man", json!({"bin": "bog", "man": "man"})), + ("html", json!({"bin": "bog", "html": "html"})), + ( + "abstract", + json!({"bin": "bog", "abstract": "This and that"}), + ), + ( + "all fields", + json!({ + "bin": "bog", + "doc": "bog.md", + "abstract": "This and that", + "lib": "lib", + "man": "man", + "html": "html", + }), + ), + ("x field", json!({"bin": "bog", "x_hi": true})), + ("X field", json!({"bin": "bog", "X_bar": 42})), + ] { + if let Err(e) = schemas.validate(&valid_app.1, idx) { + panic!("{} failed: {e}", valid_app.0); + } + } + + for invalid_app in [ + // Basics + ("array", json!([])), + ("string", json!("crank")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("invalid field", json!({"bin": "bog", "foo": "hi", })), + ("bare x_", json!({ "bin": "bog", "x_": "hi" })), + ("bare X_", json!({ "bin": "bog", "X_": "hi" })), + // bin + ("no bin", json!({"src": "x.src"})), + ("null bin", json!({"bin": null, "src": "x.src"})), + ("empty bin", json!({"bin": "", "src": "x.src"})), + ("number bin", json!({"bin": 42, "src": "x.src"})), + ("bool bin", json!({"bin": true, "src": "x.src"})), + ("array bin", json!({"bin": [], "src": "x.src"})), + ("object bin", json!({"bin": {}, "src": "x.src"})), + // doc + ("empty doc", json!({"bin": "bog", "doc": ""})), + ("null doc", json!({"bin": "bog", "doc": null})), + ("windows doc", json!({"bin": "bog", "doc": "c:\\foo"})), + ("number doc", json!({"bin": "bog", "doc": 42})), + ("bool doc", json!({"bin": "bog", "doc": true})), + ("array doc", json!({"bin": "bog", "doc": ["hi"]})), + ("object doc", json!({"bin": "bog", "doc": {}})), + // lang + ("empty lang", json!({"bin": "bog", "lang": ""})), + ("null lang", json!({"bin": "bog", "lang": null})), + ("number lang", json!({"bin": "bog", "lang": 42})), + ("bool lang", json!({"bin": "bog", "lang": true})), + ("array lang", json!({"bin": "bog", "lang": ["hi"]})), + ("object lang", json!({"bin": "bog", "lang": {}})), + // abstract + ("empty abstract", json!({"bin": "bog", "abstract": ""})), + ("null abstract", json!({"bin": "bog", "abstract": null})), + ("number abstract", json!({"bin": "bog", "abstract": 42})), + ("bool abstract", json!({"bin": "bog", "abstract": true})), + ("array abstract", json!({"bin": "bog", "abstract": ["hi"]})), + ("object abstract", json!({"bin": "bog", "abstract": {}})), + // lib + ("empty lib", json!({"bin": "bog", "lib": ""})), + ("null lib", json!({"bin": "bog", "lib": null})), + ("windows lib", json!({"bin": "bog", "lib": "c:\\foo"})), + ("number lib", json!({"bin": "bog", "lib": 42})), + ("bool lib", json!({"bin": "bog", "lib": true})), + ("array lib", json!({"bin": "bog", "lib": ["hi"]})), + ("object lib", json!({"bin": "bog", "lib": {}})), + // man + ("empty man", json!({"bin": "bog", "man": ""})), + ("null man", json!({"bin": "bog", "man": null})), + ("windows man", json!({"bin": "bog", "man": "c:\\foo"})), + ("number man", json!({"bin": "bog", "man": 42})), + ("bool man", json!({"bin": "bog", "man": true})), + ("array man", json!({"bin": "bog", "man": ["hi"]})), + ("object man", json!({"bin": "bog", "man": {}})), + // html + ("empty html", json!({"bin": "bog", "html": ""})), + ("null html", json!({"bin": "bog", "html": null})), + ("windows html", json!({"bin": "bog", "html": "c:\\foo"})), + ("number html", json!({"bin": "bog", "html": 42})), + ("bool html", json!({"bin": "bog", "html": true})), + ("array html", json!({"bin": "bog", "html": ["hi"]})), + ("object html", json!({"bin": "bog", "html": {}})), + ] { + if schemas.validate(&invalid_app.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_app.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_contents() -> Result<(), Box> { + // Load the schemas and compile the extension schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "contents"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ( + "module", + json!({"modules": {"my_hook": {"type": "hook", "lib": "src/hook"}}}), + ), + ( + "modules", + json!({"modules": { + "my_hook": {"type": "hook", "lib": "src/hook"}, + "preload": {"type": "hook", "lib": "src/hook", "preload": "server"}, + }}), + ), + ( + "extension", + json!({"extensions": { + "my_ext": {"sql": "widget.sql", "control": "widget.control"}, + }}), + ), + ( + "extensions", + json!({"extensions": { + "my_ext": {"sql": "widget.sql", "control": "widget.control"}, + "ext2": { + "sql": "widget.sql", + "control": "widget.control", + "abstract": "This and that", + } + }}), + ), + ("app", json!({"apps": {"sqitch": {"bin": "sqitch"}}})), + ( + "apps", + json!({"apps": { + "sqitch": {"bin": "sqitch"}, + "bog": {"bin": "bog", "lang": "perl"} + }}), + ), + ( + "all three", + json!({ + "apps": { + "sqitch": {"bin": "sqitch"}, + "bog": {"bin": "bog", "lang": "perl"} + }, + "modules": { + "my_hook": {"type": "hook", "lib": "src/hook"}, + }, + "extensions": { + "my_ext": {"sql": "widget.sql", "control": "widget.control"}, + } + }), + ), + ( + "x field", + json!({"apps": {"sqitch": {"bin": "sqitch"}},"x_hi": true}), + ), + ( + "X field", + json!({"apps": {"sqitch": {"bin": "sqitch"}},"X_yo": 42}), + ), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid in [ + // Basics + ("array", json!([])), + ("string", json!("crank")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ( + "invalid field", + json!({"apps": {"sqitch": {"bin": "sqitch"}}, "foo": 1}), + ), + ( + "bare x_", + json!({"apps": {"sqitch": {"bin": "sqitch"}}, "x_": 1}), + ), + ( + "bare X_", + json!({"apps": {"sqitch": {"bin": "sqitch"}}, "X_": 1}), + ), + ("short app key", json!({"apps": {"x": {"bin": "sqitch"}}})), + ( + "short ext key", + json!({"extensions": { + "x": {"sql": "widget.sql", "control": "widget.control"}, + }}), + ), + ( + "short mod key", + json!({"modules": {"x": {"type": "hook", "lib": "src/hook"}}}), + ), + // extensions + ("empty extensions", json!({"extensions": {}})), + ("null extensions", json!({"extensions": null, "lib": "bgw"})), + ("empty extensions", json!({"extensions": "", "lib": "bgw"})), + ("number extensions", json!({"extensions": 42, "lib": "bgw"})), + ("bool extensions", json!({"extensions": true, "lib": "bgw"})), + ("array extensions", json!({"extensions": [], "lib": "bgw"})), + ("object extensions", json!({"extensions": {}, "lib": "bgw"})), + // modules + ("empty modules", json!({"modules": {}})), + ("null modules", json!({"modules": null, "lib": "bgw"})), + ("empty modules", json!({"modules": "", "lib": "bgw"})), + ("number modules", json!({"modules": 42, "lib": "bgw"})), + ("bool modules", json!({"modules": true, "lib": "bgw"})), + ("array modules", json!({"modules": [], "lib": "bgw"})), + ("object modules", json!({"modules": {}, "lib": "bgw"})), + // apps + ("empty apps", json!({"apps": {}})), + ("null apps", json!({"apps": null, "lib": "bgw"})), + ("empty apps", json!({"apps": "", "lib": "bgw"})), + ("number apps", json!({"apps": 42, "lib": "bgw"})), + ("bool apps", json!({"apps": true, "lib": "bgw"})), + ("array apps", json!({"apps": [], "lib": "bgw"})), + ("object apps", json!({"apps": {}, "lib": "bgw"})), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_meta_spec() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "meta-spec"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_meta_spec in [ + ("version 2.0.0 only", json!({"version": "2.0.0"})), + ("version 2.0.1 only", json!({"version": "2.0.1"})), + ("version 2.0.2 only", json!({"version": "2.0.2"})), + ("version 2.0.99 only", json!({"version": "2.0.99"})), + ("x key", json!({"version": "2.0.99", "x_y": true})), + ("X key", json!({"version": "2.0.99", "X_x": true})), + ( + "version plus URL", + json!({"version": "2.0.0", "url": "https://rfcs.pgxn.org/0003-meta-spec-v2.html"}), + ), + ] { + if let Err(e) = schemas.validate(&valid_meta_spec.1, idx) { + panic!("{} failed: {e}", valid_meta_spec.0); + } + } + + for invalid_meta_spec in [ + ("array", json!([])), + ("string", json!("2.0.0")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("unknown field", json!({"version": "2.0.0", "foo": "hi"})), + ("bare x_", json!({"version": "2.0.0", "x_": "hi"})), + ("version 1.2.0", json!({"version": "1.2.0"})), + ("version 2.2.0", json!({"version": "2.2.0"})), + ( + "no_version", + json!({"url": "https://rfcs.pgxn.org/0003-meta-spec-v2.html"}), + ), + ( + "invalid url", + json!({"version": "2.0.1", "url": "https://pgxn.org/meta/spec.html"}), + ), + ] { + if schemas.validate(&invalid_meta_spec.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_meta_spec.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_categories() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "categories"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_cats in [ + ("Analytics", json!(["Analytics"])), + ("Auditing and Logging", json!(["Auditing and Logging"])), + ("Change Data Capture", json!(["Change Data Capture"])), + ("Connectors", json!(["Connectors"])), + ( + "Data and Transformations", + json!(["Data and Transformations"]), + ), + ("Debugging", json!(["Debugging"])), + ( + "Index and Table Optimizations", + json!(["Index and Table Optimizations"]), + ), + ("Machine Learning", json!(["Machine Learning"])), + ("Metrics", json!(["Metrics"])), + ("Orchestration", json!(["Orchestration"])), + ("Procedural Languages", json!(["Procedural Languages"])), + ("Query Optimizations", json!(["Query Optimizations"])), + ("Search", json!(["Search"])), + ("Security", json!(["Security"])), + ("Tooling and Admin", json!(["Tooling and Admin"])), + ("two categories", json!(["Analytics", "Debugging"])), + ( + "three categories", + json!(["Analytics", "Debugging", "Metrics"]), + ), + ] { + if let Err(e) = schemas.validate(&valid_cats.1, idx) { + panic!("{} failed: {e}", valid_cats.0); + } + } + + for invalid_cats in [ + ("empty array", json!([])), + ("string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ("true category", json!([true])), + ("false category", json!([false])), + ("null category", json!([null])), + ("object category", json!([{}])), + ("empty category", json!([""])), + ("object category", json!({})), + ("invalid", json!(["Hackers Convention"])), + ("dupe", json!(["Metrics", "Metrics"])), + ( + "too many", + json!(["Analytics", "Debugging", "Metrics", "Security"]), + ), + ] { + if schemas.validate(&invalid_cats.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_cats.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_classifications() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "classifications"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ("one tag", json!({"tags": ["xy"]})), + ("one cat", json!({"categories": ["Metrics"]})), + ( + "one each", + json!({"tags": ["xy"], "categories": ["Metrics"]}), + ), + ("unicode tag", json!({"tags": ["😀🍒📸"]})), + ("space tag", json!({"tags": ["hi there"]})), + ( + "two categories", + json!({"categories": ["Analytics", "Debugging"]}), + ), + ( + "three categories", + json!({"categories": ["Analytics", "Debugging", "Metrics"]}), + ), + ("x field", json!({"tags": ["xy"], "x_hi": true})), + ("X field", json!({"tags": ["xy"], "X_bar": 42})), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid_cats in [ + ("empty array", json!([])), + ("string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("true item", json!([true])), + ("false item", json!([false])), + ("null item", json!([null])), + ("object item", json!([{}])), + ("empty item", json!([""])), + ("object item", json!([{}])), + ("empty tags", json!({"tags": []})), + ("empty tag", json!({"tags": [""]})), + ("dupe tag", json!({"tags": ["x", "x"]})), + ("empty categories", json!({"categories": []})), + ("invalid category", json!({"categories": ["Bogus"]})), + ( + "dupe category", + json!({"categories": ["Metrics", "Metrics"]}), + ), + ( + "too many", + json!(["Analytics", "Debugging", "Metrics", "Security"]), + ), + ("unknown field", json!({"tags": ["xy"], "foo": 1})), + ("bare x_", json!({"tags": ["xy"], "x_": 1})), + ("bare X_", json!({"tags": ["xy"], "X_": 1})), + ] { + if schemas.validate(&invalid_cats.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_cats.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_ignore() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "ignore"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Test valid ignores. + for valid in [ + ("README.txt", json!(["README.txt"])), + ("/.git", json!(["/.git"])), + ("/src/pair.c", json!(["/src/pair.c"])), + ("/src/private.*", json!(["/src/private.*"])), + ("*.html", json!(["*.html"])), + ("*.?tml", json!(["*.?tml"])), + ("foo.?tml", json!(["foo.?tml"])), + ("[xX]_*.*", json!(["[xX]_*.*"])), + ("[a-z]*.txt", json!(["[a-z]*.txt"])), + ( + "this\\\\and\\\\that.txt", + json!(["this\\\\and\\\\that.txt"]), + ), + ( + "multiple files", + json!(["ignore_me.txt", "*.tmp", ".git*",]), + ), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + // Test invalid ignores. + for invalid in [ + ("empty array", json!([])), + ("string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("true item", json!([true])), + ("false item", json!([false])), + ("null item", json!([null])), + ("object item", json!([{}])), + ("empty item", json!([""])), + ("object item", json!([{}])), + ("backslashes", json!("this\\and\\that.txt")), + ("windows", json!("C:\\foo")), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_phase() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "phase"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid_prereq_phase in [ + ( + "requires", + json!({"requires": {"pkg:pgxn/citext": "2.0.0"}}), + ), + ( + "recommends", + json!({"recommends": {"pkg:pgxn/citext": "2.0.0"}}), + ), + ( + "suggests", + json!({"suggests": {"pkg:pgxn/citext": "2.0.0"}}), + ), + ( + "conflicts", + json!({"conflicts": {"pkg:pgxn/citext": "2.0.0"}}), + ), + ( + "two phases", + json!({ + "requires": {"pkg:pgxn/citext": "1.0.0"}, + "recommends": {"pkg:pgxn/citext": "2.0.0"}, + }), + ), + ( + "three phases", + json!({ + "requires": {"pkg:pgxn/citext": "1.0.0"}, + "recommends": {"pkg:pgxn/citext": "2.0.0"}, + "suggests": {"pkg:pgxn/citext": "3.0.0"}, + }), + ), + ( + "four phases", + json!({ + "requires": {"pkg:pgxn/citext": "1.0.0"}, + "recommends": {"pkg:pgxn/citext": "2.0.0"}, + "suggests": {"pkg:pgxn/citext": "3.0.0"}, + "conflicts": { "pkg:pypi/alligator": 0} + }), + ), + ("bare zero", json!({"requires": {"pkg:pgxn/citext": 0}})), + ("string zero", json!({"requires": {"pkg:pgxn/citext": "0"}})), + ( + "range op", + json!({"requires": {"pkg:pgxn/citext": "==2.0.0"}}), + ), + ( + "range", + json!({"requires": {"pkg:pgxn/citext": ">= 1.2.0, != 1.5.0, < 2.0.0"}}), + ), + ( + "x_ field", + json!({"requires": {"pkg:pgxn/citext": "2.0.0"}, "x_y": 1}), + ), + ( + "X_ field", + json!({"requires": {"pkg:pgxn/citext": "2.0.0"}, "X_y": 1}), + ), + ] { + if let Err(e) = schemas.validate(&valid_prereq_phase.1, idx) { + panic!("{} failed: {e}", valid_prereq_phase.0); + } + } + + for invalid_prereq_phase in [ + ("array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("only x_ property", json!({"x_y": 0})), + ( + "unknown property", + json!({"requires": {"citext": "2.0.0"}, "foo": 0}), + ), + ( + "bare x_ property", + json!({"requires": {"citext": "2.0.0"}, "x_": 0}), + ), + // requires + ("requires array", json!({"requires": ["2.0.0"]})), + ("requires object", json!({"requires": {}})), + ("requires string", json!({"requires": "2.0.0"})), + ("requires bool", json!({"requires": true})), + ("requires number", json!({"requires": 42})), + ("requires null", json!({"requires": null})), + // recommends + ("recommends array", json!({"recommends": ["2.0.0"]})), + ("recommends object", json!({"recommends": {}})), + ("recommends string", json!({"recommends": "2.0.0"})), + ("recommends bool", json!({"recommends": true})), + ("recommends number", json!({"recommends": 42})), + ("recommends null", json!({"recommends": null})), + // suggests + ("suggests array", json!({"suggests": ["2.0.0"]})), + ("suggests object", json!({"suggests": {}})), + ("suggests string", json!({"suggests": "2.0.0"})), + ("suggests bool", json!({"suggests": true})), + ("suggests number", json!({"suggests": 42})), + ("suggests null", json!({"suggests": null})), + // conflicts + ("conflicts array", json!({"conflicts": ["2.0.0"]})), + ("conflicts object", json!({"conflicts": {}})), + ("conflicts string", json!({"conflicts": "2.0.0"})), + ("conflicts bool", json!({"conflicts": true})), + ("conflicts number", json!({"conflicts": 42})), + ("conflicts null", json!({"conflicts": null})), + ] { + if schemas.validate(&invalid_prereq_phase.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_prereq_phase.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_packages() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "packages"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ( + "run", + json!({"run": {"requires": {"pkg:pgxn/citext": "2.0.0"}}}), + ), + ( + "build", + json!({"build": {"requires": {"pkg:pgxn/citext": "2.0.0"}}}), + ), + ( + "test", + json!({"test": {"requires": {"pkg:pgxn/citext": "2.0.0"}}}), + ), + ( + "configure", + json!({"configure": {"requires": {"pkg:pgxn/citext": "2.0.0"}}}), + ), + ( + "develop", + json!({"develop": {"requires": {"pkg:pgxn/citext": "2.0.0"}}}), + ), + ( + "two phases", + json!({ + "build": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "test": {"requires": {"pkg:pgxn/citext": "2.0.0"}} + }), + ), + ( + "three phases", + json!({ + "configure": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "build": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "test": {"requires": {"pkg:pgxn/citext": "2.0.0"}} + }), + ), + ( + "four phases", + json!({ + "configure": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "build": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "test": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "run": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + }), + ), + ( + "all phases", + json!({ + "configure": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "build": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "test": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "run": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "develop": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + }), + ), + ( + "run plus custom field", + json!({ + "run": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "x_Y": 0, + }), + ), + ( + "all phases plus custom", + json!({ + "configure": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "build": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "test": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "run": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "develop": {"requires": {"pkg:pgxn/citext": "2.0.0"}}, + "x_Y": 0, + }), + ), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid in [ + ("array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("only x_ field", json!({"x_y": 0})), + ( + "bare x_ property", + json!({ + "test": {"requires": {"xy": "2.0.0"}}, + "x_": 0, + }), + ), + ( + "unknown property", + json!({ + "test": {"requires": {"xy": "2.0.0"}}, + "foo": 0, + }), + ), + // configure + ("configure array", json!({"configure": ["2.0.0"]})), + ("configure object", json!({"configure": {}})), + ("configure string", json!({"configure": "2.0.0"})), + ("configure bool", json!({"configure": true})), + ("configure number", json!({"configure": 42})), + ("configure null", json!({"configure": null})), + // build + ("build array", json!({"build": ["2.0.0"]})), + ("build object", json!({"build": {}})), + ("build string", json!({"build": "2.0.0"})), + ("build bool", json!({"build": true})), + ("build number", json!({"build": 42})), + ("build null", json!({"build": null})), + // test + ("test array", json!({"test": ["2.0.0"]})), + ("test object", json!({"test": {}})), + ("test string", json!({"test": "2.0.0"})), + ("test bool", json!({"test": true})), + ("test number", json!({"test": 42})), + ("test null", json!({"test": null})), + // run + ("run array", json!({"run": ["2.0.0"]})), + ("run object", json!({"run": {}})), + ("run string", json!({"run": "2.0.0"})), + ("run bool", json!({"run": true})), + ("run number", json!({"run": 42})), + ("run null", json!({"run": null})), + // develop + ("develop array", json!({"develop": ["2.0.0"]})), + ("develop object", json!({"develop": {}})), + ("develop string", json!({"develop": "2.0.0"})), + ("develop bool", json!({"develop": true})), + ("develop number", json!({"develop": 42})), + ("develop null", json!({"develop": null})), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_postgres() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "postgres"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ("version", json!({"version": "14.0"})), + ("version 0", json!({"version": 0})), + ("range", json!({"version": ">=14.0, <18.1"})), + ("with xml", json!({"version": "14.0", "with": ["xml"]})), + ("custom x_", json!({"version": "14.0", "x_y": 1})), + ("custom X_", json!({"version": "14.0", "X_z": true})), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid in [ + ("array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("only x_", json!({"x_y": 0})), + ("only X_", json!({"X_y": 0})), + ("bare x_", json!({"version": 0, "x_": 0})), + ("bare X_", json!({"version": 0, "x_": 0})), + ("unknown", json!({"version": 0, "foo": 0})), + // version + ("version array", json!({"version": ["2.0.0"]})), + ("version object", json!({"version": {}})), + ("version empty string", json!({"version": ""})), + ("version bool", json!({"version": true})), + ("version number", json!({"version": 42})), + ("version null", json!({"version": null})), + ("version invalid", json!({"version": "xyz"})), + // with + ("with empty array", json!({"with": []})), + ("with object", json!({"with": {}})), + ("with string", json!({"with": "2.0.0"})), + ("with bool", json!({"with": true})), + ("with number", json!({"with": 42})), + ("with null", json!({"with": null})), + ("with empty string", json!({"with": [""]})), + ("with too short", json!({"with": ["x"]})), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_pipeline() -> Result<(), Box> { + // Load the schemas and compile the semver schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "pipeline"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Test valid pipelines. + for valid in [ + json!("pgxs"), + json!("meson"), + json!("pgrx"), + json!("autoconf"), + json!("cmake"), + json!("npm"), + json!("cpanm"), + json!("go"), + json!("cargo"), + ] { + if let Err(e) = schemas.validate(&valid, idx) { + panic!("{} failed: {e}", valid); + } + } + + // Test invalid pipelines. + for invalid in [ + json!("vroom"), + json!("🎃🎃"), + json!("pgx"), + json!(""), + json!(true), + json!(false), + json!(null), + json!([]), + json!({}), + ] { + if schemas.validate(&invalid, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid) + } + } + Ok(()) +} + +#[test] +fn test_v2_dependencies() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "dependencies"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ("postgres", json!({"postgres": {"version": "14"}})), + ( + "pg with", + json!({"postgres": {"version": "14", "with": ["xml"]}}), + ), + ("any", json!({"platforms": ["any"]})), + ("linux", json!({"platforms": ["linux"]})), + ("2platform", json!({"platforms": ["linux", "darwin"]})), + ("pgxs", json!({"pipeline": "pgxs"})), + ("pgrx", json!({"pipeline": "pgrx"})), + ( + "configure", + json!({"packages": { + "configure": { "requires": { "pkg:generic/cmake": 0} } + }}), + ), + ( + "test", + json!({"packages": { + "test": { "requires": { "pkg:pgxn/pgtap": "1.0.0" } } + }}), + ), + ( + "packages", + json!({"packages": { + "configure": { "requires": { "pkg:generic/cmake": 0 } }, + "build": { "recommends": { "pkg:generic/jq": 0 } }, + "test": { "requires": { "pkg:pgxn/pgtap": "1.0.0" } }, + "run": { "suggests": { "pkg:postgres/hstore": 0 } }, + "develop": { "suggests": { "pkg:generic/python": 0 } }, + }}), + ), + ( + "variation", + json!({"variations": [ + { + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + } + ]}), + ), + ( + "variations", + json!({"variations": [ + { + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + }, + { + "where": { "postgres": { "version": ">= 16.0" } }, + "dependencies": { + "postgres": { "version": ">= 16.0", "with": ["zstd"] } + } + }, + ]}), + ), + ("custom x_", json!({"pipeline": "pgxs", "x_y": 1})), + ("custom X_", json!({"pipeline": "pgxs", "X_z": true})), + ( + "everything", + json!({ + "postgres": {"version": "14", "with": ["xml"]}, + "platforms": ["linux", "darwin"], + "pipeline": "pgrx", + "packages": { + "configure": { "requires": { "pkg:generic/cmake": 0 } }, + "build": { "recommends": { "pkg:generic/jq": 0 } }, + "test": { "requires": { "pkg:pgxn/pgtap": "1.0.0" } }, + "run": { "suggests": { "pkg:postgres/hstore": 0 } }, + "develop": { "suggests": { "pkg:generic/python": 0 } }, + }, + "variations": [ + { + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + }, + ] + }), + ), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid in [ + ("array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("only x_", json!({"x_y": 0})), + ("only X_", json!({"X_y": 0})), + ("bare x_", json!({"pipeline": "pgxs", "x_": 0})), + ("bare X_", json!({"pipeline": "pgxs", "x_": 0})), + ("unknown", json!({"pipeline": "pgxs", "foo": 0})), + // postgres + ("postgres array", json!({"postgres": ["2.0.0"]})), + ("postgres empty", json!({"postgres": {}})), + ("postgres string", json!({"postgres": ""})), + ("postgres bool", json!({"postgres": true})), + ("postgres number", json!({"postgres": 42})), + ("postgres null", json!({"postgres": null})), + ( + "postgres version array", + json!({"postgres": {"version": ["2.0.0"]}}), + ), + ( + "postgres version empty", + json!({"postgres": {"version": [""]}}), + ), + ( + "postgres version bool", + json!({"postgres": {"version": [true]}}), + ), + ( + "postgres version number", + json!({"postgres": {"version": [42]}}), + ), + ( + "postgres version null", + json!({"postgres": {"version": [null]}}), + ), + ( + "postgres version invalid", + json!({"postgres": {"version": "x.y.z"}}), + ), + ("postgres with empty", json!({"postgres": {"with": []}})), + ("postgres with null", json!({"postgres": {"with": null}})), + ("postgres with bool", json!({"postgres": {"with": true}})), + ("postgres with number", json!({"postgres": {"with": 42}})), + ("postgres with object", json!({"postgres": {"with": {}}})), + ( + "postgres with empty string item", + json!({"postgres": {"with": [""]}}), + ), + ( + "postgres with short string item", + json!({"postgres": {"with": ["x"]}}), + ), + ( + "postgres with null item", + json!({"postgres": {"with": [null]}}), + ), + ( + "postgres with bool item", + json!({"postgres": {"with": [false]}}), + ), + ( + "postgres with number item", + json!({"postgres": {"with": [42]}}), + ), + ( + "postgres with array item", + json!({"postgres": {"with": [["xml"]]}}), + ), + ( + "postgres with object item", + json!({"postgres": {"with": {}}}), + ), + // platforms + ("platforms empty", json!({"platforms": []})), + ("platforms object", json!({"platforms": {}})), + ("platforms string", json!({"platforms": ""})), + ("platforms bool", json!({"platforms": true})), + ("platforms number", json!({"platforms": 42})), + ("platforms null", json!({"platforms": null})), + ("platforms empty string", json!({"platforms": [""]})), + ("platforms short string", json!({"platforms": ["x"]})), + ("platforms item array", json!({"platforms": [[]]})), + ("platforms item object", json!({"platforms": [{}]})), + ("platforms item empty string", json!({"platforms": [""]})), + ("platforms item bool", json!({"platforms": [true]})), + ("platforms item number", json!({"platforms": [42]})), + ("platforms item null", json!({"platforms": [null]})), + // pipeline + ("pipeline empty", json!({"pipeline": ""})), + ("pipeline invalid", json!({"pipeline": "nope"})), + ("pipeline object", json!({"pipeline": {}})), + ("pipeline bool", json!({"pipeline": true})), + ("pipeline number", json!({"pipeline": 42})), + ("pipeline null", json!({"pipeline": null})), + // packages + ("packages array", json!({"packages": []})), + ("packages empty", json!({"packages": {}})), + ("packages string", json!({"packages": ""})), + ("packages bool", json!({"packages": true})), + ("packages number", json!({"packages": 42})), + ("packages null", json!({"packages": null})), + // configure + ( + "packages configure array", + json!({"packages": {"configure": []}}), + ), + ("packages build empty", json!({"packages": {"build": {}}})), + ("packages test string", json!({"packages": {"test": "hi"}})), + ("packages run bool", json!({"packages": {"run": true}})), + ("packages run null", json!({"packages": {"run": null}})), + ( + "packages develop number", + json!({"packages": {"develop": 42}}), + ), + // variations + ("variations empty", json!({"variations": []})), + ("variations object", json!({"variations": {}})), + ("variations string", json!({"variations": ""})), + ("variations bool", json!({"variations": true})), + ("variations number", json!({"variations": 42})), + ("variations null", json!({"variations": null})), + ( + "nested where variations", + json!({"variations": [ + { + "where": { + "platforms": ["darwin", "bsd"], + "variations": {"pipeline": "pgxs"}, + }, + "dependencies": { + "postgres": {"version": "14"}, + }, + } + ]}), + ), + ( + "nested dependencies variations", + json!({"variations": [ + { + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": { + "postgres": {"version": "14"}, + "variations": {"pipeline": "pgxs"}, + }, + } + ]}), + ), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_variations() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "variations"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ( + "one", + json!([{ + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + }]), + ), + ( + "two", + json!([ + { + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + }, + { + "where": { "postgres": { "version": ">= 16.0" } }, + "dependencies": { + "postgres": { "version": ">= 16.0", "with": ["zstd"] } + } + }, + ]), + ), + ( + "with x_", + json!([{ + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + "x_y": true, + }]), + ), + ( + "with X_", + json!([{ + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + "X_y": 42, + }]), + ), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid in [ + ("empty", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ("only x_", json!({"x_y": 0})), + ("only X_", json!({"X_y": 0})), + ( + "no dependencies", + json!({"where": { "platforms": ["darwin", "bsd"] }}), + ), + ( + "no where", + json!({"dependencies": { "platforms": ["darwin", "bsd"] }}), + ), + ( + "bare x_", + json!([{ + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + "x_": true, + }]), + ), + ( + "bare X_", + json!([{ + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + "X_": 42, + }]), + ), + ( + "unknown x_", + json!([{ + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + "foo": true, + }]), + ), + ( + "nested where", + json!([{ + "where": { + "platforms": ["darwin", "bsd"], + "variations": { + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + }, + }, + "dependencies": {"postgres": {"version": "14"}}, + }]), + ), + ( + "nested dependencies", + json!([{ + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": { + "postgres": {"version": "14"}, + "variations": { + "where": { "platforms": ["darwin", "bsd"] }, + "dependencies": {"postgres": {"version": "14"}}, + }, + }, + }]), + ), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_badges() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "badges"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ("short", json!([{"src": "x:y", "alt": "food"}])), + ( + "long", + json!([{ + "src": "https://github.com/theory/kv-pair/workflows/CI/badge.svg", + "alt": "CI/CD Test Status", + "url": "https://github.com/theory/pgtap/actions/workflows/ci.yml" + }]), + ), + ( + "multi", + json!([ + {"src": "x:y", "alt": "food"}, + {"src": "a:b", "alt": "tests"}, + {"src": "mailto:x@example.com", "alt": "Contact Me!"}, + ]), + ), + ( + "custom x_", + json!([{"src": "x:y", "alt": "food", "x_y": 1}]), + ), + ( + "custom X_", + json!([{"src": "x:y", "alt": "food", "X_z": true}]), + ), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid in [ + ("empty array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ("empty item", json!([{}])), + ("array item", json!([[]])), + ("string item", json!([""])), + ("null item", json!([null])), + ("number item", json!([42])), + ("bool item", json!([true])), + ("only x_", json!([{"x_y": 0}])), + ("only X_", json!([{"X_y": 0}])), + ("bare x_", json!([{"src": "x:y", "alt": "food", "x_": 0}])), + ("bare X_", json!([{"src": "x:y", "alt": "food", "x_": 0}])), + ("unknown", json!([{"src": "x:y", "alt": "food", "foo": 0}])), + // src + ("src array", json!([{"alt": "abcd", "src": []}])), + ("src object", json!([{"alt": "abcd", "src": {}}])), + ("src empty", json!([{"alt": "abcd", "src": ""}])), + ("src bool", json!([{"alt": "abcd", "src": true}])), + ("src number", json!([{"alt": "abcd", "src": 42}])), + ("src null", json!([{"alt": "abcd", "src": null}])), + ("src invalid", json!([{"alt": "abcd", "src": "not a uri"}])), + // alt + ("alt array", json!([{"src": "x:y", "alt": []}])), + ("alt object", json!([{"src": "x:y", "alt": {}}])), + ("alt empty", json!([{"src": "x:y", "alt": ""}])), + ("alt bool", json!([{"src": "x:y", "alt": true}])), + ("alt number", json!([{"src": "x:y", "alt": 42}])), + ("alt null", json!([{"src": "x:y", "alt": null}])), + ("alt too short", json!([{"src": "x:y", "alt": "xyz"}])), + // url + ( + "url array", + json!([{"src": "x:y", "alt": "abcd", "url": []}]), + ), + ( + "url object", + json!([{"src": "x:y", "alt": "abcd", "url": {}}]), + ), + ( + "url empty", + json!([{"src": "x:y", "alt": "abcd", "url": ""}]), + ), + ( + "url bool", + json!([{"src": "x:y", "alt": "abcd", "url": true}]), + ), + ( + "url number", + json!([{"src": "x:y", "alt": "abcd", "url": 42}]), + ), + ( + "url null", + json!([{"src": "x:y", "alt": "abcd", "url": null}]), + ), + ( + "url invalid", + json!([{"src": "x:y", "alt": "abcd", "url": "not a url"}]), + ), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_resources() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "resources"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ("homepage", json!({"homepage": "https://example.com"})), + ("issues", json!({"issues": "https://github.com/issues"})), + ("repo", json!({"repository": "https://github.com/repo"})), + ("docs", json!({"repository": "https://example.com"})), + ("support", json!({"repository": "https://example.com"})), + ("badges", json!({"badges": [{"src": "x:y", "alt": "abcd"}]})), + ("custom x_", json!({"docs": "x:y", "x_y": 1})), + ("custom X_", json!({"docs": "x:y", "X_z": true})), + ( + "everything", + json!({ + "homepage": "https://pgtap.org", + "issues": "https://github.com/theory/pgtap/issues", + "repository": "https://github.com/theory/pgtap", + "docs": "https://pgtap.org/documentation.html", + "support": "https://github.com/theory/pgtap", + "badges": [ + { + "src": "https://img.shields.io/badge/License-PostgreSQL-blue.svg", + "alt": "PostgreSQL License", + "url": "https://www.postgresql.org/about/licence/" + }, + { + "src": "https://github.com/theory/pgtap/actions/workflows/test.yml/badge.svg", + "alt": "Test Status", + "url": "https://github.com/theory/pgtap/actions/workflows/ci.yml" + }, + ] + }), + ), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid in [ + ("array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("only x_", json!({"x_y": 0})), + ("only X_", json!({"X_y": 0})), + ("bare x_", json!({"docs": "x:y", "x_": 0})), + ("bare X_", json!({"docs": "x:y", "x_": 0})), + ("unknown", json!({"docs": "x:y", "foo": 0})), + // homepage + ("homepage array", json!([{"homepage": []}])), + ("homepage object", json!([{"homepage": {}}])), + ("homepage empty", json!([{"homepage": ""}])), + ("homepage bool", json!([{"homepage": true}])), + ("homepage number", json!([{"homepage": 42}])), + ("homepage null", json!([{"homepage": null}])), + ("homepage empty", json!([{"homepage": ""}])), + ("homepage invalid", json!([{"homepage": "not a uri"}])), + // issues + ("issues array", json!([{"issues": []}])), + ("issues object", json!([{"issues": {}}])), + ("issues empty", json!([{"issues": ""}])), + ("issues bool", json!([{"issues": true}])), + ("issues number", json!([{"issues": 42}])), + ("issues null", json!([{"issues": null}])), + ("issues empty", json!([{"issues": ""}])), + ("issues invalid", json!([{"issues": "not a uri"}])), + // repository + ("repository array", json!([{"repository": []}])), + ("repository object", json!([{"repository": {}}])), + ("repository empty", json!([{"repository": ""}])), + ("repository bool", json!([{"repository": true}])), + ("repository number", json!([{"repository": 42}])), + ("repository null", json!([{"repository": null}])), + ("repository empty", json!([{"repository": ""}])), + ("repository invalid", json!([{"repository": "not a uri"}])), + // docs + ("docs array", json!([{"docs": []}])), + ("docs object", json!([{"docs": {}}])), + ("docs empty", json!([{"docs": ""}])), + ("docs bool", json!([{"docs": true}])), + ("docs number", json!([{"docs": 42}])), + ("docs null", json!([{"docs": null}])), + ("docs empty", json!([{"docs": ""}])), + ("docs invalid", json!([{"docs": "not a uri"}])), + // support + ("support array", json!([{"support": []}])), + ("support object", json!([{"support": {}}])), + ("support empty", json!([{"support": ""}])), + ("support bool", json!([{"support": true}])), + ("support number", json!([{"support": 42}])), + ("support null", json!([{"support": null}])), + ("support empty", json!([{"support": ""}])), + ("support invalid", json!([{"support": "not a uri"}])), + // badges + ("badges empty", json!([{"badges": []}])), + ("badges object", json!([{"badges": {}}])), + ("badges empty", json!([{"badges": ""}])), + ("badges bool", json!([{"badges": true}])), + ("badges number", json!([{"badges": 42}])), + ("badges null", json!([{"badges": null}])), + ("badges empty", json!([{"badges": ""}])), + ("badges src only", json!([{"badges": {"src": "x:y"}}])), + ("badges alt only", json!([{"badges": {"alt": "abcd"}}])), + ( + "badges src invalid", + json!([{"badges": {"alt": "abcd", "src": "not a uri"}}]), + ), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +#[test] +fn test_v2_artifacts() -> Result<(), Box> { + // Load the schemas and compile the maintainer schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "artifacts"); + let idx = compiler.compile(&id, &mut schemas)?; + + for valid in [ + ( + "basic", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + }]), + ), + ( + "two", + json!([ + { + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + }, + { + "url": "a:b", + "type": "zip", + "sha512": "22E06F682A7FEC79F814F06B5DCEA0B06133890775DDC624DE744CD5D4E8D5FE29863BA5E77C6D3690B610DBCDF7D79A973561FDFBD8454508998446AF8F2C58", + }, + ]), + ), + ( + "all fields", + json!([ + { + "url": "x:y", + "type": "bin", + "platform": "linux", + "sha256": "0B68EE2CE5B2C0641C6C429ED2CE17E2ED76DDD58BF9A16E698C5069D60AA34E", + }, + { + "url": "a:b", + "type": "zip", + "platform": "darwin", + "sha512": "22e06f682a7fec79f814f06b5dcea0b06133890775ddc624de744cd5d4e8d5fe29863ba5e77c6d3690b610dbcdf7d79a973561fdfbd8454508998446af8f2c58", + }, + ]), + ), + ( + "custom x_", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "x_y": 1, + }]), + ), + ( + "custom X_", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "X_y": true, + }]), + ), + ] { + if let Err(e) = schemas.validate(&valid.1, idx) { + panic!("{} failed: {e}", valid.0); + } + } + + for invalid in [ + ("empty array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ("only x_", json!({"x_y": 0})), + ("only X_", json!({"X_y": 0})), + ( + "bare x_", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "x_": 1, + }]), + ), + ( + "bare X_", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "X_": 1, + }]), + ), + ( + "unknown", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "foo": 1, + }]), + ), + // url + ( + "url array", + json!([{ + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "url": [], + }]), + ), + ( + "url object", + json!([{ + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "url": {}, + }]), + ), + ( + "url empty", + json!([{ + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "url": "", + }]), + ), + ( + "url bool", + json!([{ + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "url": false, + }]), + ), + ( + "url number", + json!([{ + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "url": 42, + }]), + ), + ( + "url null", + json!([{ + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "url": null, + }]), + ), + ( + "url invalid", + json!([{ + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "url": "not a uri", + }]), + ), + // type + ( + "type array", + json!([{ + "url": "x:y", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "type": [], + }]), + ), + ( + "type object", + json!([{ + "url": "x:y", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "type": {}, + }]), + ), + ( + "type empty", + json!([{ + "url": "x:y", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "type": "", + }]), + ), + ( + "type bool", + json!([{ + "url": "x:y", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "type": false, + }]), + ), + ( + "type number", + json!([{ + "url": "x:y", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "type": 42, + }]), + ), + ( + "type null", + json!([{ + "url": "x:y", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "type": null, + }]), + ), + ( + "type too short", + json!([{ + "url": "x:y", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34e", + "type": "x", + }]), + ), + // sha256 + ( + "sha256 array", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": [], + }]), + ), + ( + "sha256 object", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": {}, + }]), + ), + ( + "sha256 empty", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "", + }]), + ), + ( + "sha256 bool", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": false, + }]), + ), + ( + "sha256 number", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": 42, + }]), + ), + ( + "sha256 null", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": null, + }]), + ), + ( + "sha256 not hex", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34x", + }]), + ), + ( + "sha256 too long", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34ee", + }]), + ), + ( + "sha256 too short", + json!([{ + "url": "x:y", + "type": "bin", + "sha256": "0b68ee2ce5b2c0641c6c429ed2ce17e2ed76ddd58bf9a16e698c5069d60aa34", + }]), + ), + // sha512 + ( + "sha512 array", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": [], + }]), + ), + ( + "sha512 object", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": {}, + }]), + ), + ( + "sha512 empty", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": "", + }]), + ), + ( + "sha512 bool", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": false, + }]), + ), + ( + "sha512 number", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": 42, + }]), + ), + ( + "sha512 null", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": null, + }]), + ), + ( + "sha512 not hex", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": "22e06f682a7fec79f814f06b5dcea0b06133890775ddc624de744cd5d4e8d5fe29863ba5e77c6d3690b610dbcdf7d79a973561fdfbd8454508998446af8f2c5x", + }]), + ), + ( + "sha512 too long", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": "22e06f682a7fec79f814f06b5dcea0b06133890775ddc624de744cd5d4e8d5fe29863ba5e77c6d3690b610dbcdf7d79a973561fdfbd8454508998446af8f2c58e", + }]), + ), + ( + "sha512 too short", + json!([{ + "url": "x:y", + "type": "bin", + "sha512": "22e06f682a7fec79f814f06b5dcea0b06133890775ddc624de744cd5d4e8d5fe29863ba5e77c6d3690b610dbcdf7d79a973561fdfbd8454508998446af8f2c5", + }]), + ), + ] { + if schemas.validate(&invalid.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid.0) + } + } + + Ok(()) +} + +fn valid_v2_distribution() -> Value { + json!({ + "name": "pgTAP", + "abstract": "Unit testing for PostgreSQL", + "description": "pgTAP is a suite of database functions that make it easy to write TAP-emitting unit tests in psql scripts or xUnit-style test functions.", + "version": "0.26.0", + "maintainers": [ + { "name": "Josh Berkus", "email": "jberkus@pgxn.org" }, + { "name": "David E. Wheeler", "url": "https://pgxn.org/user/theory" } + ], + "license": "MIT OR PostgreSQL", + "dependencies": { + "postgres": { "version": "8.4" }, + "packages": { + "run": { + "requires": { + "pkg:postgres/plpgsql": 0 + } + } + } + }, + "contents": { + "extensions": { + "pgtap": { + "abstract": "Unit testing for PostgreSQL", + "sql": "pgtap.sql", + "control": "pgtap.control" + } + } + }, + "resources": { + "homepage": "https://pgtap.org", + "issues": "https://github.com/theory/pgtap/issues", + "repository": "https://github.com/theory/pgtap", + "docs": "https://pgtap.org/documentation.html", + "support": "https://github.com/theory/pgtap", + "badges": [ + { + "src": "https://img.shields.io/badge/License-PostgreSQL-blue.svg", + "alt": "PostgreSQL License", + "url": "https://www.postgresql.org/about/licence/" + }, + { + "src": "https://github.com/theory/pgtap/actions/workflows/test.yml/badge.svg", + "alt": "Test Status", + "url": "https://github.com/theory/pgtap/actions/workflows/ci.yml" + } + ] + }, + "producer": "David E. Wheeler", + "meta-spec": { + "version": "2.0.0", + "url": "https://rfcs.pgxn.org/0003-meta-spec-v2.html" + }, + "classifications": { + "tags": [ + "testing", + "unit testing", + "tap", + "tddd", + "test driven database development" + ], + "categories": [ "Tooling and Admin" ] + } + }) +} + +#[test] +fn test_v2_distribution() -> Result<(), Box> { + // Load the schemas and compile the distribution schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "distribution"); + let idx = compiler.compile(&id, &mut schemas)?; + + // Make sure the valid distribution is in fact valid. + let meta = valid_v2_distribution(); + if let Err(e) = schemas.validate(&meta, idx) { + panic!("valid_distribution meta failed: {e}"); + } + + type Obj = Map; + type Callback = fn(&mut Obj); + + static VALID_TEST_CASES: &[(&str, Callback)] = &[ + ("no change", |_: &mut Obj| {}), + ("custom key x_y", |m: &mut Obj| { + m.insert("x_y".to_string(), json!("hello")); + }), + ("custom key X_y", |m: &mut Obj| { + m.insert("X_y".to_string(), json!(42)); + }), + ("license apache_2_0", |m: &mut Obj| { + m.insert("license".to_string(), json!("Apache-2.0")); + }), + ("license postgresql", |m: &mut Obj| { + m.insert("license".to_string(), json!("PostgreSQL")); + }), + ("license AND", |m: &mut Obj| { + m.insert("license".to_string(), json!("MIT AND PostgreSQL")); + }), + ("contents extension doc", |m: &mut Obj| { + let contents = m.get_mut("contents").unwrap().as_object_mut().unwrap(); + let ext = contents + .get_mut("extensions") + .unwrap() + .as_object_mut() + .unwrap(); + let pgtap = ext.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("doc".to_string(), json!("foo/bar.txt")); + }), + ("contents extension no abstract", |m: &mut Obj| { + let contents = m.get_mut("contents").unwrap().as_object_mut().unwrap(); + let ext = contents + .get_mut("extensions") + .unwrap() + .as_object_mut() + .unwrap(); + let pgtap = ext.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.remove("abstract"); + }), + ("add modules", |m: &mut Obj| { + let contents = m.get_mut("contents").unwrap().as_object_mut().unwrap(); + contents.insert( + "modules".to_string(), + json!({"my_hook": {"type": "hook", "lib": "src/hook"}}), + ); + }), + ("add apps", |m: &mut Obj| { + let contents = m.get_mut("contents").unwrap().as_object_mut().unwrap(); + contents.insert( + "apps".to_string(), + json!({ + "sqitch": {"bin": "sqitch"}, + "bog": {"bin": "bog", "lang": "perl"} + }), + ); + }), + ("no spec URL", |m: &mut Obj| { + let spec = m.get_mut("meta-spec").unwrap().as_object_mut().unwrap(); + spec.remove("url"); + }), + ("multibyte name", |m: &mut Obj| { + m.insert("name".to_string(), json!("yoŭknow")); + }), + ("emoji name", |m: &mut Obj| { + m.insert("name".to_string(), json!("📀📟🎱")); + }), + ("name with dash", |m: &mut Obj| { + m.insert("name".to_string(), json!("foo-bar")); + }), + ("multibyte abstract", |m: &mut Obj| { + m.insert("abstract".to_string(), json!("yoŭknow")); + }), + ("emoji abstract", |m: &mut Obj| { + m.insert("abstract".to_string(), json!("📀📟🎱")); + }), + ("no producer", |m: &mut Obj| { + m.remove("producer"); + }), + ("one tag", |m: &mut Obj| { + m.insert("classifications".to_string(), json!({"tags": ["foo"]})); + }), + ("no ignore", |m: &mut Obj| { + m.remove("ignore"); + }), + ("no resources", |m: &mut Obj| { + m.remove("resources"); + }), + ("one resource", |m: &mut Obj| { + m.insert( + "resources".to_string(), + json!({"docs": "https://example.com"}), + ); + }), + ("on maintainer", |m: &mut Obj| { + m.insert( + "maintainers".to_string(), + json!([{"name": "Hi There", "url": "x:y"}]), + ); + }), + ("pre-release version", |m: &mut Obj| { + m.insert("version".to_string(), json!("1.2.1-beta1")); + }), + ("multibyte description", |m: &mut Obj| { + m.insert("description".to_string(), json!("yoŭknow")); + }), + ("emoji description", |m: &mut Obj| { + m.insert("description".to_string(), json!("📀📟🎱")); + }), + ("multibyte producer", |m: &mut Obj| { + m.insert("producer".to_string(), json!("yoŭknow")); + }), + ("emoji producer", |m: &mut Obj| { + m.insert("producer".to_string(), json!("📀📟🎱")); + }), + ("postgres dependencies", |m: &mut Obj| { + m.insert( + "dependencies".to_string(), + json!({"postgres": { "version": "12", "with": ["xml"] }}), + ); + }), + ("dependency variations", |m: &mut Obj| { + m.insert( + "dependencies".to_string(), + json!({"variations": [{ + "where": {"postgres": { "version": "16" }}, + "dependencies": {"platforms": ["linux"]}, + }]}), + ); + }), + ("artifacts", |m: &mut Obj| { + m.insert( + "artifacts".to_string(), + json!([{ + "type": "source", + "url": "https://github.com/theory/pg-pair/releases/download/v1.1.0/pair-1.1.0.zip", + "sha256": "2b9d2416096d2930be51e5332b70bcd97846947777a93e4a3d65fe1b5fd7b004" + }]), + ); + }), + ]; + + for tc in VALID_TEST_CASES { + let mut meta = valid_v2_distribution(); + let map = meta.as_object_mut().unwrap(); + tc.1(map); + if let Err(e) = schemas.validate(&meta, idx) { + panic!("distribution {} failed: {e}", tc.0); + } + } + + static INVALID_TEST_CASES: &[(&str, Callback)] = &[ + ("no name", |m: &mut Obj| { + m.remove("name"); + }), + ("no version", |m: &mut Obj| { + m.remove("version"); + }), + ("no abstract", |m: &mut Obj| { + m.remove("abstract"); + }), + ("no maintainers", |m: &mut Obj| { + m.remove("maintainers"); + }), + ("no license", |m: &mut Obj| { + m.remove("license"); + }), + ("no meta-spec", |m: &mut Obj| { + m.remove("meta-spec"); + }), + ("no contents", |m: &mut Obj| { + m.remove("contents"); + }), + ("bad version", |m: &mut Obj| { + m.insert("version".to_string(), json!("1.0")); + }), + ("deprecated version", |m: &mut Obj| { + m.insert("version".to_string(), json!("1.0.0v1")); + }), + ("version 0", |m: &mut Obj| { + m.insert("version".to_string(), json!(0)); + }), + ("contents no control", |m: &mut Obj| { + let contents = m.get_mut("contents").unwrap().as_object_mut().unwrap(); + let ext = contents + .get_mut("extensions") + .unwrap() + .as_object_mut() + .unwrap(); + let pgtap = ext.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.remove("control"); + }), + ("no postgres version", |m: &mut Obj| { + let deps = m.get_mut("dependencies").unwrap().as_object_mut().unwrap(); + deps.insert("postgres".to_string(), json!({"with": ["xml"]})); + }), + ("invalid key", |m: &mut Obj| { + m.insert("foo".to_string(), json!(1)); + }), + ("invalid license", |m: &mut Obj| { + m.insert("license".to_string(), json!("gobbledygook")); + }), + ("name with newline", |m: &mut Obj| { + m.insert("name".to_string(), json!("foo\nbar")); + }), + ("name with return", |m: &mut Obj| { + m.insert("name".to_string(), json!("foo\rbar")); + }), + ("name with slash", |m: &mut Obj| { + m.insert("name".to_string(), json!("foo/bar")); + }), + ("name with backslash", |m: &mut Obj| { + m.insert("name".to_string(), json!("foo\\\\bar")); + }), + ("name with space", |m: &mut Obj| { + m.insert("name".to_string(), json!("foo bar")); + }), + ("empty", |m: &mut Obj| { + m.insert("name".to_string(), json!("")); + }), + ("short name", |m: &mut Obj| { + m.insert("name".to_string(), json!("x")); + }), + ("null name", |m: &mut Obj| { + m.insert("name".to_string(), json!(null)); + }), + ("array name", |m: &mut Obj| { + m.insert("name".to_string(), json!([])); + }), + ("object name", |m: &mut Obj| { + m.insert("name".to_string(), json!({})); + }), + ("bool name", |m: &mut Obj| { + m.insert("name".to_string(), json!(false)); + }), + ("number name", |m: &mut Obj| { + m.insert("name".to_string(), json!(42)); + }), + ("empty description", |m: &mut Obj| { + m.insert("description".to_string(), json!("")); + }), + ("null description", |m: &mut Obj| { + m.insert("description".to_string(), json!(null)); + }), + ("array description", |m: &mut Obj| { + m.insert("description".to_string(), json!([])); + }), + ("object description", |m: &mut Obj| { + m.insert("description".to_string(), json!({})); + }), + ("bool description", |m: &mut Obj| { + m.insert("description".to_string(), json!(false)); + }), + ("number description", |m: &mut Obj| { + m.insert("description".to_string(), json!(42)); + }), + ("empty producer", |m: &mut Obj| { + m.insert("producer".to_string(), json!("")); + }), + ("null producer", |m: &mut Obj| { + m.insert("producer".to_string(), json!(null)); + }), + ("array producer", |m: &mut Obj| { + m.insert("producer".to_string(), json!([])); + }), + ("object producer", |m: &mut Obj| { + m.insert("producer".to_string(), json!({})); + }), + ("bool producer", |m: &mut Obj| { + m.insert("producer".to_string(), json!(false)); + }), + ("number producer", |m: &mut Obj| { + m.insert("producer".to_string(), json!(42)); + }), + ("null classifications", |m: &mut Obj| { + m.insert("classifications".to_string(), json!(null)); + }), + ("string classifications", |m: &mut Obj| { + m.insert("classifications".to_string(), json!("")); + }), + ("array classifications", |m: &mut Obj| { + m.insert("classifications".to_string(), json!([])); + }), + ("empty classifications", |m: &mut Obj| { + m.insert("classifications".to_string(), json!({})); + }), + ("bool classifications", |m: &mut Obj| { + m.insert("classifications".to_string(), json!(false)); + }), + ("number classifications", |m: &mut Obj| { + m.insert("classifications".to_string(), json!(42)); + }), + ("null tag", |m: &mut Obj| { + m.insert("classifications".to_string(), json!({"tags": [null]})); + }), + ("short tag", |m: &mut Obj| { + m.insert("classifications".to_string(), json!({"tags": ["x"]})); + }), + ("ignore null file string", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": null})); + }), + ("ignore null file empty array", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": []})); + }), + ("ignore null file object", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": {}})); + }), + ("ignore null file bool", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": true})); + }), + ("ignore null file number", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": 42})); + }), + ("ignore empty file array string", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": [""]})); + }), + ("ignore undef file array string", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": [null]})); + }), + ("ignore undef file array number", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": [42]})); + }), + ("ignore undef file array bool", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": [true]})); + }), + ("ignore undef file array obj", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": [{}]})); + }), + ("ignore undef file array array", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"file": [[]]})); + }), + ("ignore empty directory string", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": ""})); + }), + ("ignore null directory string", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": null})); + }), + ("ignore null directory empty array", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": []})); + }), + ("ignore null directory object", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": {}})); + }), + ("ignore null directory bool", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": true})); + }), + ("ignore null directory number", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": 42})); + }), + ("ignore empty directory array string", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": [""]})); + }), + ("ignore undef directory array string", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": [null]})); + }), + ("ignore undef directory array number", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": [42]})); + }), + ("ignore undef directory array bool", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": [true]})); + }), + ("ignore undef directory array obj", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": [{}]})); + }), + ("ignore undef directory array array", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"directory": [[]]})); + }), + ("ignore bad key", |m: &mut Obj| { + m.insert("ignore".to_string(), json!({"foo": "hi"})); + }), + ("null resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!(null)); + }), + ("array resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!([])); + }), + ("empty resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!({})); + }), + ("string resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!("")); + }), + ("bool resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!(true)); + }), + ("number resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!(42)); + }), + ("object resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!({})); + }), + ("array resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!([])); + }), + ("object artifacts", |m: &mut Obj| { + m.insert("artifacts".to_string(), json!({})); + }), + ("null artifacts", |m: &mut Obj| { + m.insert("artifacts".to_string(), json!(null)); + }), + ("empty artifacts", |m: &mut Obj| { + m.insert("artifacts".to_string(), json!([])); + }), + ("string artifacts", |m: &mut Obj| { + m.insert("artifacts".to_string(), json!("")); + }), + ("bool artifacts", |m: &mut Obj| { + m.insert("artifacts".to_string(), json!(true)); + }), + ("number artifacts", |m: &mut Obj| { + m.insert("artifacts".to_string(), json!(42)); + }), + ("object artifacts", |m: &mut Obj| { + m.insert("artifacts".to_string(), json!({})); + }), + ("array artifacts", |m: &mut Obj| { + m.insert("artifacts".to_string(), json!([])); + }), + ("invalid artifact sha", |m: &mut Obj| { + m.insert( + "artifacts".to_string(), + json!([{"url": "x:y", "type": "bin", "sha256": {}}]), + ); + }), + ]; + for tc in INVALID_TEST_CASES { + let mut meta = valid_v2_distribution(); + let map = meta.as_object_mut().unwrap(); + tc.1(map); + if schemas.validate(&meta, idx).is_ok() { + panic!("{} unexpectedly passed!", tc.0) + } + } + + Ok(()) +}