diff --git a/Cargo.lock b/Cargo.lock index 80b06a9..07b5cae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,7 @@ name = "pgxn-meta-spec" version = "0.1.0" dependencies = [ "boon", + "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 6002b8e..e161f33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,6 @@ exclude = [ ".github", ".gitattributes", "target", ".vscode", ".gitignore" ] [dev-dependencies] boon = "0.6" serde_json = "1.0" + +[dependencies] +serde = { version = "1", features = ["derive"] } diff --git a/Makefile b/Makefile index 7be34ba..b1e3080 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: test # Validate the JSON schema. test: - @cargo test -- --show-output + @cargo test .git/hooks/pre-commit: @printf "#!/bin/sh\nmake lint\n" > $@ diff --git a/schema/v1/bugtracker.schema.json b/schema/v1/bugtracker.schema.json index d31623e..0165d50 100644 --- a/schema/v1/bugtracker.schema.json +++ b/schema/v1/bugtracker.schema.json @@ -16,13 +16,9 @@ "description": "An email address to which bug reports can be sent" } }, - "anyOf": [ - { "required": ["web"] }, - { "required": ["mailto"] }, - { "required": ["web", "mailto"] } - ], + "anyOf": [{ "required": ["web"] }, { "required": ["mailto"] }], "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, diff --git a/schema/v1/distribution.schema.json b/schema/v1/distribution.schema.json index 58679ce..ec2b9d2 100644 --- a/schema/v1/distribution.schema.json +++ b/schema/v1/distribution.schema.json @@ -51,7 +51,7 @@ "resources": { "$ref": "resources.schema.json" } }, "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, diff --git a/schema/v1/extension.schema.json b/schema/v1/extension.schema.json index 7967a4b..c00ecc9 100644 --- a/schema/v1/extension.schema.json +++ b/schema/v1/extension.schema.json @@ -27,7 +27,7 @@ }, "required": ["file", "version"], "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, diff --git a/schema/v1/meta-spec.schema.json b/schema/v1/meta-spec.schema.json index 2f76ee9..2b69de9 100644 --- a/schema/v1/meta-spec.schema.json +++ b/schema/v1/meta-spec.schema.json @@ -8,7 +8,7 @@ "version": { "type": "string", "pattern": "^1[.]0[.][[:digit:]]+$", - "description": "The version of the PGXN Meta Spec against which the document was generated." + "description": "The version of the PGXN Meta Spec against which the document was generated. Must be 1.0.x." }, "url": { "type": "string", @@ -18,7 +18,7 @@ }, "required": ["version"], "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, diff --git a/schema/v1/no_index.schema.json b/schema/v1/no_index.schema.json index 7fd4057..f9dee5d 100644 --- a/schema/v1/no_index.schema.json +++ b/schema/v1/no_index.schema.json @@ -7,32 +7,16 @@ "properties": { "file": { "description": "A list of relative paths to files. Paths **must be** specified with unix conventions.", - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "description": "Relative path in unix convention to a file to ignore.", - "minLength": 1 - } + "$ref": "#/$defs/fileList" }, "directory": { "description": "A list of relative paths to directories. Paths **must be** specified with unix conventions.", - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "description": "Relative path in unix convention to a directory to ignore.", - "minLength": 1 - } + "$ref": "#/$defs/fileList" } }, - "anyOf": [ - { "required": ["file"] }, - { "required": ["directory"] }, - { "required": ["file", "directory"] } - ], + "anyOf": [{ "required": ["file"] }, { "required": ["directory"] }], "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, @@ -42,5 +26,25 @@ "file": ["src/file.sql"], "directory": ["src/private"] } - ] + ], + "$defs": { + "fileList": { + "oneOf": [ + { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "description": "Relative path in unix convention to a file to ignore.", + "minLength": 1 + } + }, + { + "type": "string", + "description": "Relative path in unix convention to a file to ignore.", + "minLength": 1 + } + ] + } + } } diff --git a/schema/v1/prereq_phase.schema.json b/schema/v1/prereq_phase.schema.json index 5f0904c..9f6da7f 100644 --- a/schema/v1/prereq_phase.schema.json +++ b/schema/v1/prereq_phase.schema.json @@ -23,13 +23,17 @@ } }, "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, - "minProperties": 1, - "$comment": "Really should require at least one of the named properties; this allows for a single _x property. Good enough for now.", + "anyOf": [ + { "required": ["requires"] }, + { "required": ["recommends"] }, + { "required": ["suggests"] }, + { "required": ["conflicts"] } + ], "examples": [ { "requires": { diff --git a/schema/v1/prereqs.schema.json b/schema/v1/prereqs.schema.json index 1da32ad..d2e56e2 100644 --- a/schema/v1/prereqs.schema.json +++ b/schema/v1/prereqs.schema.json @@ -26,10 +26,15 @@ "description": "The develop phase’s prereqs are extensions needed to work on the distribution’s source code as its maintainer does. These tools might be needed to build a release tarball, to run maintainer-only tests, or to perform other tasks related to developing new versions of the distribution." } }, - "minProperties": 1, - "$comment": "Really should require at least one of the named properties; this allows for a single _x property. Good enough for now.", + "anyOf": [ + { "required": ["configure"] }, + { "required": ["build"] }, + { "required": ["test"] }, + { "required": ["runtime"] }, + { "required": ["develop"] } + ], "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, diff --git a/schema/v1/repository.schema.json b/schema/v1/repository.schema.json index a31b1e8..bc9e3b9 100644 --- a/schema/v1/repository.schema.json +++ b/schema/v1/repository.schema.json @@ -17,17 +17,13 @@ }, "type": { "type": "string", - "format": "email", - "description": "a lowercase string indicating the VCS used." + "pattern": "^\\p{lower}*$", + "description": "A lowercase string indicating the VCS used." } }, - "anyOf": [ - { "required": ["url", "type"] }, - { "required": ["web"] }, - { "required": ["web", "url", "type"] } - ], + "anyOf": [{ "required": ["url", "type"] }, { "required": ["web"] }], "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, diff --git a/schema/v1/resources.schema.json b/schema/v1/resources.schema.json index 8ec9b33..22911b6 100644 --- a/schema/v1/resources.schema.json +++ b/schema/v1/resources.schema.json @@ -14,13 +14,16 @@ "repository": { "$ref": "repository.schema.json" } }, "patternProperties": { - "^[xX]_": { + "^[xX]_.": { "description": "Custom key" } }, "additionalProperties": false, - "minProperties": 1, - "$comment": "Really should require at least one of the named properties; this allows for a single _x property. Good enough for now.", + "anyOf": [ + { "required": ["homepage"] }, + { "required": ["bugtracker"] }, + { "required": ["repository"] } + ], "examples": [ { "homepage": "https://pgxn.org/", diff --git a/spec.md b/spec.md index 7854e5e..c9c9574 100644 --- a/spec.md +++ b/spec.md @@ -349,7 +349,7 @@ are valid in the [List](#List) representation: mozilla_1_0 | Mozilla Public License, Version 1.0 mozilla_1_1 | Mozilla Public License, Version 1.1 openssl | OpenSSL License - perl_5 | The Perl 5 License (Artistic 1 & GPL 1 or later) + perl_5 | The Perl 5 License (Artistic 1 & GPL 1 or later) postgresql | The PostgreSQL License qpl_1_0 | Q Public License, Version 1.0 ssleay | Original SSLeay License diff --git a/tests/corpus/v1/invalid.txt b/tests/corpus/v1/invalid.txt new file mode 100644 index 0000000..0cccc65 --- /dev/null +++ b/tests/corpus/v1/invalid.txt @@ -0,0 +1 @@ +{"test":"no_version","error":"missing properties 'version'","meta":{"name":"pair","abstract":"A key/value pair data type","maintainer":"David E. Wheeler ","license":"postgresql","provides":{"pair":{"abstract":"A key/value pair data type","file":"sql/pair.sql","docfile":"doc/pair.md","version":"0.1.0"}},"meta-spec":{"version":"1.0.0","url":"https://pgxn.org/meta/spec.txt"}}} diff --git a/tests/corpus/v1/valid.txt b/tests/corpus/v1/valid.txt new file mode 100644 index 0000000..ae9d804 --- /dev/null +++ b/tests/corpus/v1/valid.txt @@ -0,0 +1,3 @@ +{"test":"howto1","meta":{"name":"pair","abstract":"A key/value pair data type","version":"0.1.0","maintainer":"David E. Wheeler ","license":"postgresql","provides":{"pair":{"abstract":"A key/value pair data type","file":"sql/pair.sql","docfile":"doc/pair.md","version":"0.1.0"}},"meta-spec":{"version":"1.0.0","url":"https://pgxn.org/meta/spec.txt"}}} +{"test":"howto2","meta":{"name":"pair","abstract":"A key/value pair data type","description":"This library contains a single PostgreSQL extension, a key/value pair data type called “pair”, along with a convenience function for constructing key/value pairs.","version":"0.1.4","maintainer":["David E. Wheeler "],"license":"postgresql","provides":{"pair":{"abstract":"A key/value pair data type","file":"sql/pair.sql","docfile":"doc/pair.md","version":"0.1.0"}},"resources":{"bugtracker":{"web":"https://github.com/theory/kv-pair/issues/"},"repository":{"url":"git://github.com/theory/kv-pair.git","web":"https://github.com/theory/kv-pair/","type":"git"}},"generated_by":"David E. Wheeler","meta-spec":{"version":"1.0.0","url":"https://pgxn.org/meta/spec.txt"},"tags":["variadic function","ordered pair","pair","key value","key value pair","data type"]}} +{"test":"widget","meta":{"name":"widget","abstract":"Widget for PostgreSQL","description":"¿A widget is just thing thing, yoŭ know?","version":"0.2.5","maintainer":["David E. Wheeler "],"license":{"PostgreSQL":"https://www.postgresql.org/about/licence"},"prereqs":{"runtime":{"requires":{"plpgsql":0,"PostgreSQL":"8.0.0"},"recommends":{"PostgreSQL":"8.4.0"}}},"provides":{"widget":{"file":"sql/widget.sql.in","version":"0.2.5"}},"resources":{"homepage":"http://widget.example.org/"},"generated_by":"theory","meta-spec":{"version":"1.0.0","url":"https://pgxn.org/meta/spec.txt"},"tags":["widget","gadget","full text search"]}} diff --git a/tests/test.rs b/tests/test.rs deleted file mode 100644 index 36d32d1..0000000 --- a/tests/test.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::{ - collections::HashMap, - error::Error, - fs::{self, File}, -}; - -use boon::{Compiler, Schemas}; -use serde_json::Value; - -#[test] -fn test_schema_v1() -> Result<(), Box> { - let mut compiler = Compiler::new(); - 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()); - } - } - } - - // Make sure we found schemas. - assert!(!loaded.is_empty()); - - // 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 new file mode 100644 index 0000000..4bbc3f0 --- /dev/null +++ b/tests/v1_schema_test.rs @@ -0,0 +1,2310 @@ +use std::fs::{self, File}; +use std::io::{prelude::*, BufReader}; +use std::{collections::HashMap, error::Error}; + +use boon::{Compiler, 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"; + +#[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) +} + +#[derive(Deserialize, Serialize)] +struct CorpusCase { + test: String, + error: Option, + meta: Value, +} + +#[test] +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)?; + + // Test each meta JSON in the corpus. + let file = File::open("tests/corpus/v1/valid.txt")?; + let reader = BufReader::new(file); + for line in reader.lines() { + let tc: CorpusCase = serde_json::from_str(&line?)?; + + if let Err(e) = schemas.validate(&tc.meta, index) { + panic!("{} failed: {e}", &tc.test); + } + println!("Example {} ok", &tc.test); + } + + Ok(()) +} + +#[test] +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)?; + + // Test each meta JSON in the corpus. + let file = File::open("tests/corpus/v1/invalid.txt")?; + let reader = BufReader::new(file); + for line in reader.lines() { + let tc: CorpusCase = serde_json::from_str(&line?)?; + match schemas.validate(&tc.meta, index) { + Ok(_) => panic!("{} unexpectedly passed!", &tc.test), + Err(e) => assert!( + e.to_string().contains(&tc.error.unwrap()), + "{} error: {e}", + &tc.test, + ), + } + } + + Ok(()) +} + +#[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(()) +} + +#[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(()) +} + +// 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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_version in VALID_VERSIONS { + 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 { + let iv = json!(invalid_version); + if schemas.validate(&iv, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_version) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_version in VALID_VERSIONS { + for op in ["", "==", "!=", ">", "<", ">=", "<="] { + for append in [ + "", + ",<= 1.1.2+meta", + ",>= 2.0.0, 1.5.6", + ", >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!("extension {} 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) + } + } + } + + // Bar integer 0 allowed. + let zero = json!(0); + if let Err(e) = schemas.validate(&zero, idx) { + panic!("extension {} 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!("extension {} failed: {e}", range); + } + } + + for invalid_version in INVALID_VERSIONS { + for op in ["", "==", "!=", ">", "<", ">=", "<="] { + for append in [ + "", + ",<= 1.1.2+meta", + ",>= 2.0.0, 1.5.6", + ", >1.2.0, != 12.0.0, < 19.2.0", + ] { + let range = json!(format!("{}{}{}", op, invalid_version, append)); + if schemas.validate(&range, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_version) + } + } + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + // Test valid license values. + for valid_license in [ + json!("agpl_3"), + json!("apache_1_1"), + json!("apache_2_0"), + json!("artistic_1"), + json!("artistic_2"), + json!("bsd"), + json!("freebsd"), + json!("gfdl_1_2"), + json!("gfdl_1_3"), + json!("gpl_1"), + json!("gpl_2"), + json!("gpl_3"), + json!("lgpl_2_1"), + json!("lgpl_3_0"), + json!("mit"), + json!("mozilla_1_0"), + json!("mozilla_1_1"), + json!("openssl"), + json!("perl_5"), + json!("postgresql"), + json!("qpl_1_0"), + json!("ssleay"), + json!("sun"), + json!("zlib"), + json!("open_source"), + json!("restricted"), + json!("unrestricted"), + json!("unknown"), + json!(["postgresql", "perl_5"]), + json!({"foo": "https://foo.com"}), + json!({"foo": "https://foo.com", "bar": "https://bar.com"}), + ] { + if let Err(e) = schemas.validate(&valid_license, idx) { + panic!("license {} failed: {e}", valid_license); + } + } + + // Test invalid license values. + for invalid_license in [ + json!("nonesuch"), + json!("crank"), + json!(""), + json!(true), + json!(false), + json!(null), + json!(["nonesuch"]), + json!([]), + json!({}), + json!({"foo": ":hello"}), + ] { + if schemas.validate(&invalid_license, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_license) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_provides in [ + ( + "required fields", + json!({"pgtap": { + "file": "widget.sql", + "version": "0.26.0", + }}), + ), + ( + "all fields", + json!({"pgtap": { + "docfile": "foo/bar.txt", + "abstract": "This and that", + "file": "widget.sql", + "version": "0.26.0", + }}), + ), + ( + "x field", + json!({"pgtap": { + "file": "widget.sql", + "version": "0.26.0", + "x_foo": 1, + }}), + ), + ( + "X field", + json!({"pgtap": { + "file": "widget.sql", + "version": "0.26.0", + "X_foo": 1, + }}), + ), + ( + "two extensions", + json!({ + "pgtap": { + "file": "widget.sql", + "version": "0.26.0", + }, + "pgtap_common": { + "file": "common.sql", + "version": "0.26.0", + }, + }), + ), + ] { + if let Err(e) = schemas.validate(&valid_provides.1, idx) { + panic!("{} failed: {e}", valid_provides.0); + } + } + + for invalid_provides in [ + // Basics + ("array", json!([])), + ("string", json!("crank")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ( + "invalid key", + json!({"x": {"file": "x.sql", "version": "1.0.0"}}), + ), + ( + "invalid field", + json!({"xy": {"file": "x.sql", "version": "1.0.0", "foo": "foo"}}), + ), + ("no file", json!({"pgtap": {"version": "0.26.0"}})), + ( + "invalid version", + json!({"x": {"file": "x.sql", "version": "1.0"}}), + ), + ( + "null file", + json!({"x": {"file": null, "version": "1.0.0"}}), + ), + ( + "bare x_", + json!({"x": {"file": "x.txt", "version": "1.0.0", "x_": 0}}), + ), + ] { + if schemas.validate(&invalid_provides.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_provides.0) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_extension in [ + ( + "required fields", + json!( { + "file": "widget.sql", + "version": "0.26.0", + }), + ), + ( + "with abstract", + json!( { + "file": "widget.sql", + "version": "0.26.0", + "abstract": "This and that", + }), + ), + ( + "all fields", + json!({ + "docfile": "foo/bar.txt", + "abstract": "This and that", + "file": "widget.sql", + "version": "0.26.0", + }), + ), + ( + "x field", + json!({ + "version": "0.26.0", + "file": "widget.sql", + "x_hi": true, + }), + ), + ( + "X field", + json!({ + "version": "0.26.0", + "file": "widget.sql", + "X_bar": 42, + }), + ), + ] { + if let Err(e) = schemas.validate(&valid_extension.1, idx) { + panic!("extension {} 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!({"file": "widget.sql", "version": "0.26.0", "foo": "hi", }), + ), + ( + "bare x_", + json!( { + "file": "widget.sql", + "version": "0.26.0", + "x_": "hi", + }), + ), + // File + ("no file", json!({"version": "0.26.0"})), + ("null file", json!({"file": null, "version": "0.26.0"})), + ( + "empty string file", + json!({"file": "", "version": "0.26.0"}), + ), + ("number file", json!({"file": 42, "version": "0.26.0"})), + ("bool file", json!({"file": true, "version": "0.26.0"})), + ("array file", json!({"file": ["hi"], "version": "0.26.0"})), + ("object file", json!({"file": {}, "version": "0.26.0"})), + // Version + ("no version", json!({"file": "widget.sql"})), + ( + "invalid version", + json!({"file": "widget.sql", "version": "1.0"}), + ), + ( + "null version", + json!({"file": "widget.sql", "version": null}), + ), + ( + "empty version", + json!({"file": "widget.sql", "version": ""}), + ), + ( + "number version", + json!({"file": "widget.sql", "version": 42}), + ), + ( + "bool version", + json!({"file": "widget.sql", "version": false}), + ), + ( + "array version", + json!({"file": "widget.sql", "version": ["1.0.0"]}), + ), + ( + "objet version", + json!({"file": "widget.sql", "version": {}}), + ), + // Abstract + ( + "empty abstract", + json!({"file": "widget.sql", "version": "1.0.0", "abstract": ""}), + ), + ( + "null abstract", + json!({"file": "widget.sql", "version": "1.0.0", "abstract": null}), + ), + ( + "number abstract", + json!({"file": "widget.sql", "version": "1.0.0", "abstract": 42}), + ), + ( + "bool abstract", + json!({"file": "widget.sql", "version": "1.0.0", "abstract": true}), + ), + ( + "array abstract", + json!({"file": "widget.sql", "version": "1.0.0", "abstract": ["hi"]}), + ), + ( + "object abstract", + json!({"file": "widget.sql", "version": "1.0.0", "abstract": {}}), + ), + // Docfile + ( + "empty docfile", + json!({"file": "widget.sql", "version": "1.0.0", "docfile": ""}), + ), + ( + "null docfile", + json!({"file": "widget.sql", "version": "1.0.0", "docfile": null}), + ), + ( + "number docfile", + json!({"file": "widget.sql", "version": "1.0.0", "docfile": 42}), + ), + ( + "bool docfile", + json!({"file": "widget.sql", "version": "1.0.0", "docfile": true}), + ), + ( + "array docfile", + json!({"file": "widget.sql", "version": "1.0.0", "docfile": ["hi"]}), + ), + ( + "object docfile", + json!({"file": "widget.sql", "version": "1.0.0", "docfile": {}}), + ), + ] { + if schemas.validate(&invalid_extension.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_extension.0) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_maintainer in [ + ("min length", json!("x")), + ("min length_array", json!(["x"])), + ( + "name and email", + json!("David E. Wheeler "), + ), + ( + "two names and emails", + json!([ + "David E. Wheeler ", + "Josh Berkus " + ]), + ), + ("space", json!("hi there")), + ("slash", json!("hi/there")), + ("backslash", json!("hi\\there")), + ("null byte", json!("hi\x00there")), + ] { + if let Err(e) = schemas.validate(&valid_maintainer.1, idx) { + panic!("extension {} failed: {e}", valid_maintainer.0); + } + } + + for invalid_maintainer in [ + ("empty array", json!([])), + ("empty string", json!("")), + ("empty string in array", json!(["hi", ""])), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ] { + if schemas.validate(&invalid_maintainer.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_maintainer.0) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_meta_spec in [ + ("version 1.0.0 only", json!({"version": "1.0.0"})), + ("version 1.0.1 only", json!({"version": "1.0.1"})), + ("version 1.0.2 only", json!({"version": "1.0.2"})), + ("version 1.0.99 only", json!({"version": "1.0.99"})), + ("x key", json!({"version": "1.0.99", "x_y": true})), + ("X key", json!({"version": "1.0.99", "X_x": true})), + ( + "version plus URL", + json!({"version": "1.0.0", "url": "https://pgxn.org/meta/spec.txt"}), + ), + ] { + if let Err(e) = schemas.validate(&valid_meta_spec.1, idx) { + panic!("extension {} failed: {e}", valid_meta_spec.0); + } + } + + for invalid_meta_spec in [ + ("array", json!([])), + ("string", json!("1.0.0")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("unknown field", json!({"version": "1.0.0", "foo": "hi"})), + ("bare x_", json!({"version": "1.0.0", "x_": "hi"})), + ("version 1.1.0", json!({"version": "1.1.0"})), + ("version 2.0.0", json!({"version": "2.0.0"})), + ( + "no_version", + json!({"url": "https://pgxn.org/meta/spec.txt"}), + ), + ( + "invalid url", + json!({"version": "1.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_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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_bugtracker in [ + ("web only", json!({"web": "https://foo.com"})), + ("mailto only", json!({"mailto": "hi@example.com"})), + ( + "web and mailto", + json!({"web": "https://foo.com", "mailto": "hi@example.com"}), + ), + ("x key", json!({"web": "https://foo.com", "x_q": true})), + ("X key", json!({"web": "https://foo.com", "X_hi": true})), + ] { + if let Err(e) = schemas.validate(&valid_bugtracker.1, idx) { + panic!("extension {} failed: {e}", valid_bugtracker.0); + } + } + + for invalid_bugtracker in [ + ("array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("unknown field", json!({"web": "https://foo.com", "foo": 0})), + ("bare x_", json!({"web": "https://foo.com", "x_": 0})), + ("web array", json!({"web": []})), + ("web object", json!({"web": {}})), + ("web bool", json!({"web": true})), + ("web null", json!({"web": null})), + ("web number", json!({"web": 52})), + ("mailto array", json!({"mailto": []})), + ("mailto object", json!({"mailto": {}})), + ("mailto bool", json!({"mailto": true})), + ("mailto null", json!({"mailto": null})), + ("mailto number", json!({"mailto": 52})), + ("invalid web url", json!({"web": "3ttp://a.com"})), + ("missing required", json!({"x_y": "https://foo.com"})), + ] { + if schemas.validate(&invalid_bugtracker.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_bugtracker.0) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_no_index in [ + ("file only", json!({"file": ["x.txt"]})), + ("file string", json!({"file": "x.txt"})), + ("directory only", json!({"directory": [".git"]})), + ("directory string", json!({"directory": ".git"})), + ( + "both arrays", + json!({"file": ["x.txt"], "directory": [".git"]}), + ), + ( + "file string dir array", + json!({"file": "x.txt", "directory": [".git"]}), + ), + ( + "file array dir string", + json!({"file": ["x.txt"], "directory": ".git"}), + ), + ("two files", json!({"file": ["x.txt", "y.md"]})), + ("two dirs", json!({"directory": ["x", "y"]})), + ("x_ field", json!({"file": ["x.txt"], "x_Y": 0})), + ("X_ field", json!({"file": ["x.txt"], "X_y": 0})), + ] { + if let Err(e) = schemas.validate(&valid_no_index.1, idx) { + panic!("extension {} failed: {e}", valid_no_index.0); + } + } + + for invalid_no_index in [ + ("array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("empty file", json!({"file": []})), + ("empty file string", json!({"file": [""]})), + ("file object", json!({"file": {}})), + ("file bool", json!({"file": true})), + ("file null", json!({"file": null})), + ("empty directory", json!({"directory": []})), + ("empty directory string", json!({"directory": [""]})), + ("directory object", json!({"directory": {}})), + ("directory bool", json!({"directory": true})), + ("directory null", json!({"directory": null})), + ("unknown field", json!({"file": ["x"], "hi": 0})), + ("bare x_", json!({"file": ["x"], "x_": 0})), + ("missing required", json!({"x_y": 0})), + ] { + if schemas.validate(&invalid_no_index.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_no_index.0) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_prereq_relationship in [ + ("single prereq", json!({"citext": "2.0.0"})), + ("two prereqs", json!({"citext": "2.0.0", "pgtap": "0.98.3"})), + ("version op", json!({"citext": ">=2.0.0"})), + ("version zero", json!({"citext": 0})), + ("version zero string", json!({"citext": "0"})), + ( + "version range", + json!({"citext": ">= 1.2.0, != 1.5.0, < 2.0.0"}), + ), + ] { + if let Err(e) = schemas.validate(&valid_prereq_relationship.1, idx) { + panic!("extension {} failed: {e}", valid_prereq_relationship.0); + } + } + + for invalid_prereq_relationship in [ + ("array", json!([])), + ("string", json!("web")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("short term", json!({"x": "1.0.0"})), + ("null byte term", json!({"x\x00y": "1.0.0"})), + ("invalid version", json!({"xy": "1.0"})), + ("invalid version range", json!({"xy": "!1.0.0"})), + ("number value", json!({"xx": 42})), + ("empty string value", json!({"xx": ""})), + ("null value", json!({"xx": null})), + ("bool value", json!({"xx": true})), + ("array value", json!({"xx": []})), + ] { + if schemas + .validate(&invalid_prereq_relationship.1, idx) + .is_ok() + { + panic!("{} unexpectedly passed!", invalid_prereq_relationship.0) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_prereq_phase in [ + ("requires", json!({"requires": {"citext": "2.0.0"}})), + ("recommends", json!({"recommends": {"citext": "2.0.0"}})), + ("suggests", json!({"suggests": {"citext": "2.0.0"}})), + ("conflicts", json!({"conflicts": {"citext": "2.0.0"}})), + ( + "two phases", + json!({ + "requires": {"citext": "1.0.0"}, + "recommends": {"citext": "2.0.0"}, + }), + ), + ( + "three phases", + json!({ + "requires": {"citext": "1.0.0"}, + "recommends": {"citext": "2.0.0"}, + "suggests": {"citext": "3.0.0"}, + }), + ), + ( + "four phases", + json!({ + "requires": {"citext": "1.0.0"}, + "recommends": {"citext": "2.0.0"}, + "suggests": {"citext": "3.0.0"}, + "conflicts": { "alligator": 0} + }), + ), + ("bare zero", json!({"requires": {"citext": 0}})), + ("string zero", json!({"requires": {"citext": "0"}})), + ("range op", json!({"requires": {"citext": "==2.0.0"}})), + ( + "range", + json!({"requires": {"citext": ">= 1.2.0, != 1.5.0, < 2.0.0"}}), + ), + ( + "x_ field", + json!({"requires": {"citext": "2.0.0"}, "x_y": 1}), + ), + ( + "X_ field", + json!({"requires": {"citext": "2.0.0"}, "X_y": 1}), + ), + ] { + if let Err(e) = schemas.validate(&valid_prereq_phase.1, idx) { + panic!("extension {} 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_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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_prereqs in [ + ( + "runtime", + json!({"runtime": {"requires": {"citext": "2.0.0"}}}), + ), + ("build", json!({"build": {"requires": {"citext": "2.0.0"}}})), + ("test", json!({"test": {"requires": {"citext": "2.0.0"}}})), + ( + "configure", + json!({"configure": {"requires": {"citext": "2.0.0"}}}), + ), + ( + "develop", + json!({"develop": {"requires": {"citext": "2.0.0"}}}), + ), + ( + "two phases", + json!({ + "build": {"requires": {"citext": "2.0.0"}}, + "test": {"requires": {"citext": "2.0.0"}} + }), + ), + ( + "three phases", + json!({ + "configure": {"requires": {"citext": "2.0.0"}}, + "build": {"requires": {"citext": "2.0.0"}}, + "test": {"requires": {"citext": "2.0.0"}} + }), + ), + ( + "four phases", + json!({ + "configure": {"requires": {"citext": "2.0.0"}}, + "build": {"requires": {"citext": "2.0.0"}}, + "test": {"requires": {"citext": "2.0.0"}}, + "runtime": {"requires": {"citext": "2.0.0"}}, + }), + ), + ( + "all phases", + json!({ + "configure": {"requires": {"citext": "2.0.0"}}, + "build": {"requires": {"citext": "2.0.0"}}, + "test": {"requires": {"citext": "2.0.0"}}, + "runtime": {"requires": {"citext": "2.0.0"}}, + "develop": {"requires": {"citext": "2.0.0"}}, + }), + ), + ( + "runtime plus custom field", + json!({ + "runtime": {"requires": {"citext": "2.0.0"}}, + "x_Y": 0, + }), + ), + ( + "all phases plus custom", + json!({ + "configure": {"requires": {"citext": "2.0.0"}}, + "build": {"requires": {"citext": "2.0.0"}}, + "test": {"requires": {"citext": "2.0.0"}}, + "runtime": {"requires": {"citext": "2.0.0"}}, + "develop": {"requires": {"citext": "2.0.0"}}, + "x_Y": 0, + }), + ), + ] { + if let Err(e) = schemas.validate(&valid_prereqs.1, idx) { + panic!("extension {} failed: {e}", valid_prereqs.0); + } + } + + for invalid_prereqs 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})), + // runtime + ("runtime array", json!({"runtime": ["2.0.0"]})), + ("runtime object", json!({"runtime": {}})), + ("runtime string", json!({"runtime": "2.0.0"})), + ("runtime bool", json!({"runtime": true})), + ("runtime number", json!({"runtime": 42})), + ("runtime null", json!({"runtime": 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_prereqs.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_prereqs.0) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_repository in [ + ("web", json!({"web": "https://example.com"})), + ( + "type and url", + json!({"type": "git", "url": "https://example.com"}), + ), + ( + "x_ property", + json!({"web": "https://example.com", "x_y": 0}), + ), + ( + "X_ property", + json!({"web": "https://example.com", "X_y": 0}), + ), + ] { + if let Err(e) = schemas.validate(&valid_repository.1, idx) { + panic!("extension {} failed: {e}", valid_repository.0); + } + } + + for invalid_repository in [ + ("empty array", json!([])), + ("empty string", json!("")), + ("empty string in array", json!(["hi", ""])), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("object", json!({})), + ( + "bare x_ property", + json!({"web": "https://example.com", "x_": 0}), + ), + ( + "unknown property", + json!({"web": "https://example.com", "foo": 0}), + ), + ("url without type", json!({"url": "x:y"})), + ("type without url", json!({"type": "cvs"})), + // web + ("bad web URL", json!({"web": ":hello"})), + ("web array", json!({"web": ["x:y"]})), + ("web object", json!({"web": {}})), + ("web bool", json!({"web": true})), + ("web number", json!({"web": 42})), + ("web null", json!({"web": null})), + // url + ("bad url", json!({"type": "git", "url": ":hello"})), + ("url array", json!({"type": "git", "url": ["x:y"]})), + ("url object", json!({"type": "git", "url": {}})), + ("url bool", json!({"type": "git", "url": true})), + ("url number", json!({"type": "git", "url": 42})), + ("url null", json!({"type": "git", "url": null})), + // type + ("uppercase type", json!({"url": "FOO"})), + ("mixed type", json!({"url": "Foo"})), + ] { + if schemas.validate(&invalid_repository.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_repository.0) + } + } + + Ok(()) +} + +#[test] +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 idx = compiler.compile(&id, &mut schemas)?; + + for valid_resources in [ + ("homepage", json!({"homepage": "https://example.com"})), + ( + "bugtracker web", + json!({"bugtracker": {"web": "https://foo.com"}}), + ), + ( + "bugtracker mailto", + json!({"bugtracker": {"mailto": "hi@example.com"}}), + ), + ( + "repository web", + json!({"repository": {"web": "https://example.com"}}), + ), + ( + "repository url and type", + json!({"repository": {"type": "git", "url": "https://example.com"}}), + ), + ("x_ property", json!({"homepage": "x:y", "x_y": 0})), + ("X_ property", json!({"homepage": "x:y", "X_y": 0})), + ] { + if let Err(e) = schemas.validate(&valid_resources.1, idx) { + panic!("extension {} failed: {e}", valid_resources.0); + } + } + + for invalid_resources in [ + ("empty array", json!([])), + ("empty string", json!("")), + ("empty string in array", json!(["hi", ""])), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("empty object", json!({})), + ("bare x_ property", json!({"homepage": "x:y", "x_": 0})), + ("unknown property", json!({"homepage": "x:y", "foo": 0})), + // homepage + ("bad homepage url", json!({"homepage": ":hi"})), + ("homepage array", json!({"homepage": ["x:y"]})), + ("homepage object", json!({"homepage": {}})), + ("homepage bool", json!({"homepage": true})), + ("homepage number", json!({"homepage": 42})), + ("homepage null", json!({"homepage": null})), + // bugtracker + ( + "bad bugtracker url", + json!({"bugtracker": {"web": "3ttp://a.com"}}), + ), + ("bugtracker array", json!({"bugtracker": ["x:y"]})), + ("bugtracker empty object", json!({"bugtracker": {}})), + ("bugtracker bool", json!({"bugtracker": true})), + ("bugtracker number", json!({"bugtracker": 42})), + ("bugtracker null", json!({"bugtracker": null})), + // repository + ( + "bad repository url", + json!({"repository": {"web": "3ttp://a.com"}}), + ), + ("repository array", json!({"repository": ["x:y"]})), + ("repository empty object", json!({"repository": {}})), + ("repository bool", json!({"repository": true})), + ("repository number", json!({"repository": 42})), + ("repository null", json!({"repository": null})), + ] { + if schemas.validate(&invalid_resources.1, idx).is_ok() { + panic!("{} unexpectedly passed!", invalid_resources.0) + } + } + + Ok(()) +} + +fn valid_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", + "maintainer": [ + "David E. Wheeler ", + "pgTAP List " + ], + "license": { + "PostgreSQL": "http://www.postgresql.org/about/licence" + }, + "prereqs": { + "runtime": { + "requires": { + "plpgsql": 0, + "PostgreSQL": "8.0.0" + }, + "recommends": { + "PostgreSQL": "8.4.0" + } + } + }, + "provides": { + "pgtap": { + "abstract": "Unit testing for PostgreSQL", + "file": "pgtap.sql", + "version": "0.26.0" + } + }, + "resources": { + "homepage": "http://pgtap.org/", + "bugtracker": { + "web": "https://github.com/theory/pgtap/issues" + }, + "repository": { + "url": "https://github.com/theory/pgtap.git", + "web": "https://github.com/theory/pgtap", + "type": "git" + } + }, + "generated_by": "David E. Wheeler", + "meta-spec": { + "version": "1.0.0", + "url": "https://pgxn.org/meta/spec.txt" + }, + "tags": [ + "testing", + "unit testing", + "tap", + "tddd", + "test driven database development" + ] + }) +} + +#[test] +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)?; + + // Make sure the valid distribution is in fact valid. + let meta = valid_distribution(); + if let Err(e) = schemas.validate(&meta, idx) { + panic!("valid_distribution meta failed: {e}"); + } + + // Cases ported from https://github.com/pgxn/pgxn-meta-validator/blob/v0.16.0/t/validator.t + + // type Checker = fn(&Value) -> bool; + type Obj = Map; + type Callback = fn(&mut Obj); + + static VALID_TEST_CASES: &[(&str, Callback)] = &[ + ("no change", |_: &mut Obj| {}), + ("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 array", |m: &mut Obj| { + m.insert("license".to_string(), json!(["postgresql", "perl_5"])); + }), + ("license object", |m: &mut Obj| { + m.insert("license".to_string(), json!({"foo": "https://example.com"})); + }), + ("provides docfile", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("docfile".to_string(), json!("foo/bar.txt")); + }), + ("provides no abstract", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.remove("abstract"); + }), + ("provides custom key", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("x_foo".to_string(), json!(1)); + }), + ("no spec URL", |m: &mut Obj| { + let spec = m.get_mut("meta-spec").unwrap().as_object_mut().unwrap(); + spec.remove("url"); + }), + ("spec custom key", |m: &mut Obj| { + let spec = m.get_mut("meta-spec").unwrap().as_object_mut().unwrap(); + spec.insert("x_foo".to_string(), json!(1)); + }), + ("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")); + }), + ("no generated_by", |m: &mut Obj| { + m.remove("generated_by"); + }), + ("one tag", |m: &mut Obj| { + m.insert("tags".to_string(), json!(["foo"])); + }), + ("no_index file", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": ["foo"]})); + }), + ("no_index file string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": "foo"})); + }), + ("no_index directory", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": ["foo"]})); + }), + ("no_index directory string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": "foo"})); + }), + ("no_index file and directory", |m: &mut Obj| { + m.insert( + "no_index".to_string(), + json!({"file": ["foo", "bar"], "directory": "foo"}), + ); + }), + ("no_index custom key", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": "foo", "X_foo": 1})); + }), + // configure + ("configure requires prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "configure".to_string(), + json!({"requires": {"foo": "1.0.0"}}), + ); + }), + ("configure recommends prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "configure".to_string(), + json!({"recommends": {"foo": "1.0.0"}}), + ); + }), + ("configure suggests prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "configure".to_string(), + json!({"suggests": {"foo": "1.0.0"}}), + ); + }), + ("configure conflicts prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "configure".to_string(), + json!({"conflicts": {"foo": "1.0.0"}}), + ); + }), + // build + ("build requires prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("build".to_string(), json!({"requires": {"foo": "1.0.0"}})); + }), + ("build recommends prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("build".to_string(), json!({"recommends": {"foo": "1.0.0"}})); + }), + ("build suggests prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("build".to_string(), json!({"suggests": {"foo": "1.0.0"}})); + }), + ("build conflicts prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("build".to_string(), json!({"conflicts": {"foo": "1.0.0"}})); + }), + // test + ("test requires prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("test".to_string(), json!({"requires": {"foo": "1.0.0"}})); + }), + ("test recommends prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("test".to_string(), json!({"recommends": {"foo": "1.0.0"}})); + }), + ("test suggests prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("test".to_string(), json!({"suggests": {"foo": "1.0.0"}})); + }), + ("test conflicts prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("test".to_string(), json!({"conflicts": {"foo": "1.0.0"}})); + }), + // runtime + ("runtime requires prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("runtime".to_string(), json!({"requires": {"foo": "1.0.0"}})); + }), + ("runtime recommends prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"recommends": {"foo": "1.0.0"}}), + ); + }), + ("runtime suggests prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("runtime".to_string(), json!({"suggests": {"foo": "1.0.0"}})); + }), + ("runtime conflicts prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"conflicts": {"foo": "1.0.0"}}), + ); + }), + // develop + ("develop requires prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("develop".to_string(), json!({"requires": {"foo": "1.0.0"}})); + }), + ("develop recommends prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "develop".to_string(), + json!({"recommends": {"foo": "1.0.0"}}), + ); + }), + ("develop suggests prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("develop".to_string(), json!({"suggests": {"foo": "1.0.0"}})); + }), + ("develop conflicts prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "develop".to_string(), + json!({"conflicts": {"foo": "1.0.0"}}), + ); + }), + // version range operators + ("version range with == operator", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"requires": {"foo": "==1.0.0"}}), + ); + }), + ("version range with != operator", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"requires": {"foo": "!=1.0.0"}}), + ); + }), + ("version range with > operator", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"requires": {"foo": ">1.0.0"}}), + ); + }), + ("version range with < operator", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"requires": {"foo": "<1.0.0"}}), + ); + }), + ("version range with >= operator", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"requires": {"foo": ">=1.0.0"}}), + ); + }), + ("version range with <= operator", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"requires": {"foo": "<=1.0.0"}}), + ); + }), + ("prereq complex version range", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert( + "runtime".to_string(), + json!({"requires": {"foo": ">= 1.2.0, != 1.5.0, < 2.0.0"}}), + ); + }), + ("prereq version 0", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + prereqs.insert("runtime".to_string(), json!({"requires": {"foo": 0}})); + }), + // release_status + ("no release_status", |m: &mut Obj| { + m.remove("release_status"); + }), + ("release_status stable", |m: &mut Obj| { + m.insert("release_status".to_string(), json!("stable")); + }), + ("release_status testing", |m: &mut Obj| { + m.insert("release_status".to_string(), json!("testing")); + }), + ("release_status unstable", |m: &mut Obj| { + m.insert("release_status".to_string(), json!("unstable")); + }), + // resources + ("no resources", |m: &mut Obj| { + m.remove("resources"); + }), + ("homepage resource", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert("homepage".to_string(), json!("https://foo.com")); + }), + ("bugtracker resource", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert( + "bugtracker".to_string(), + json!({ + "web": "https://example.com/", + "mailto": "foo@example.com", + }), + ); + }), + ("bugtracker web", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert( + "bugtracker".to_string(), + json!({"web": "https://example.com/"}), + ); + }), + ("bugtracker mailto", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert( + "bugtracker".to_string(), + json!({"mailto": "foo@example.com"}), + ); + }), + ("bugtracker custom", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert( + "bugtracker".to_string(), + json!({"mailto": "foo@example.com", "x_foo": 1}), + ); + }), + ("repository resource", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert( + "repository".to_string(), + json!({ + "web": "https://example.com", + "url": "git://example.com", + "type": "git", + }), + ); + }), + ("repository resource url", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert( + "repository".to_string(), + json!({ + "url": "git://example.com", + "type": "git", + }), + ); + }), + ("repository resource web", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert( + "repository".to_string(), + json!({"web": "https://example.com"}), + ); + }), + ("repository custom", |m: &mut Obj| { + let resources = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + resources.insert( + "repository".to_string(), + json!({"web": "https://example.com", "x_foo": 1}), + ); + }), + ]; + + for tc in VALID_TEST_CASES { + let mut meta = valid_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 maintainer", |m: &mut Obj| { + m.remove("maintainer"); + }), + ("no license", |m: &mut Obj| { + m.remove("license"); + }), + ("no meta-spec", |m: &mut Obj| { + m.remove("meta-spec"); + }), + ("no provides", |m: &mut Obj| { + m.remove("provides"); + }), + ("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)); + }), + ("provides version 0", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("version".to_string(), json!(0)); + }), + ("bad provides version", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("version".to_string(), json!("hi")); + }), + ("bad prereq version", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + let requires = runtime + .get_mut("requires") + .unwrap() + .as_object_mut() + .unwrap(); + requires.insert("plpgsql".to_string(), json!("1.2.0b1")); + }), + ("prereq null version", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + let requires = runtime + .get_mut("requires") + .unwrap() + .as_object_mut() + .unwrap(); + requires.insert("plpgsql".to_string(), json!(null)); + }), + ("prereq invalid version", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + let requires = runtime + .get_mut("requires") + .unwrap() + .as_object_mut() + .unwrap(); + requires.insert("plpgsql".to_string(), json!("1.0")); + }), + ("prereq invalid version op", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + let requires = runtime + .get_mut("requires") + .unwrap() + .as_object_mut() + .unwrap(); + requires.insert("plpgsql".to_string(), json!("= 1.0.0")); + }), + ("prereq wtf version op", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + let requires = runtime + .get_mut("requires") + .unwrap() + .as_object_mut() + .unwrap(); + requires.insert("plpgsql".to_string(), json!("*** 1.0.0")); + }), + ("prereq wtf version leading comma", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + let requires = runtime + .get_mut("requires") + .unwrap() + .as_object_mut() + .unwrap(); + requires.insert("plpgsql".to_string(), json!(", 1.0.0")); + }), + ("invalid prereq phase", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + runtime.insert("genesis".to_string(), json!("1.0.0")); + }), + ("non-map prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + runtime.insert("requires".to_string(), json!(["PostgreSQL", "1.0.0"])); + }), + ("non-term prereq", |m: &mut Obj| { + let prereqs = m.get_mut("prereqs").unwrap().as_object_mut().unwrap(); + let runtime = prereqs.get_mut("runtime").unwrap().as_object_mut().unwrap(); + runtime.insert("requires".to_string(), json!({"foo/bar": "1.0.0"})); + }), + ("invalid key", |m: &mut Obj| { + m.insert("foo".to_string(), json!(1)); + }), + ("invalid license", |m: &mut Obj| { + m.insert("license".to_string(), json!("gobbledygook")); + }), + ("invalid licenses", |m: &mut Obj| { + m.insert("license".to_string(), json!(["bsd", "gobbledygook"])); + }), + ("invalid license URL", |m: &mut Obj| { + m.insert("license".to_string(), json!({"foo": ":not a url"})); + }), + ("no provides file", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.remove("file"); + }), + ("no provides version", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.remove("version"); + }), + ("provides array", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("version".to_string(), json!(["pgtap", "0.24.0"])); + }), + ("null provides file", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("file".to_string(), json!(null)); + }), + ("null provides abstract", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("abstract".to_string(), json!(null)); + }), + ("null provides version", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("abstract".to_string(), json!(null)); + }), + ("null provides docfile", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("docfile".to_string(), json!(null)); + }), + ("bad provides custom key", |m: &mut Obj| { + let provides = m.get_mut("provides").unwrap().as_object_mut().unwrap(); + let pgtap = provides.get_mut("pgtap").unwrap().as_object_mut().unwrap(); + pgtap.insert("w00t".to_string(), json!("hi")); + }), + ("alt spec version", |m: &mut Obj| { + let spec = m.get_mut("meta-spec").unwrap().as_object_mut().unwrap(); + spec.insert("version".to_string(), json!("2.0.0")); + }), + ("no spec version", |m: &mut Obj| { + let spec = m.get_mut("meta-spec").unwrap().as_object_mut().unwrap(); + spec.remove("version"); + }), + ("bad spec URL", |m: &mut Obj| { + let spec = m.get_mut("meta-spec").unwrap().as_object_mut().unwrap(); + spec.insert("url".to_string(), json!("not a url")); + }), + ("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")); + }), + ("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)); + }), + ("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 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 generated_by", |m: &mut Obj| { + m.insert("generated_by".to_string(), json!(null)); + }), + ("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)); + }), + ("empty tags", |m: &mut Obj| { + m.insert("tags".to_string(), json!([])); + }), + ("object tags", |m: &mut Obj| { + m.insert("tags".to_string(), json!({})); + }), + ("bool tags", |m: &mut Obj| { + m.insert("tags".to_string(), json!(false)); + }), + ("number tags", |m: &mut Obj| { + m.insert("tags".to_string(), json!(42)); + }), + ("null tag", |m: &mut Obj| { + m.insert("tags".to_string(), json!([null])); + }), + ("empty tag", |m: &mut Obj| { + m.insert("tags".to_string(), json!(["", "foo"])); + }), + ("long tag", |m: &mut Obj| { + m.insert("tags".to_string(), json!(["x".repeat(256)])); + }), + ("object tag", |m: &mut Obj| { + m.insert("tags".to_string(), json!([{}])); + }), + ("bool tags", |m: &mut Obj| { + m.insert("tags".to_string(), json!([false])); + }), + ("number tags", |m: &mut Obj| { + m.insert("tags".to_string(), json!([42])); + }), + ("no_index empty file string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": ""})); + }), + ("no_index null file string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": null})); + }), + ("no_index null file empty array", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": []})); + }), + ("no_index null file object", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": {}})); + }), + ("no_index null file bool", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": true})); + }), + ("no_index null file number", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": 42})); + }), + ("no_index empty file array string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": [""]})); + }), + ("no_index undef file array string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": [null]})); + }), + ("no_index undef file array number", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": [42]})); + }), + ("no_index undef file array bool", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": [true]})); + }), + ("no_index undef file array obj", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": [{}]})); + }), + ("no_index undef file array array", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"file": [[]]})); + }), + ("no_index empty directory string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": ""})); + }), + ("no_index null directory string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": null})); + }), + ("no_index null directory empty array", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": []})); + }), + ("no_index null directory object", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": {}})); + }), + ("no_index null directory bool", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": true})); + }), + ("no_index null directory number", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": 42})); + }), + ("no_index empty directory array string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": [""]})); + }), + ("no_index undef directory array string", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": [null]})); + }), + ("no_index undef directory array number", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": [42]})); + }), + ("no_index undef directory array bool", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": [true]})); + }), + ("no_index undef directory array obj", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": [{}]})); + }), + ("no_index undef directory array array", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"directory": [[]]})); + }), + ("no_index bad key", |m: &mut Obj| { + m.insert("no_index".to_string(), json!({"foo": "hi"})); + }), + ("invalid release_status", |m: &mut Obj| { + m.insert("release_status".to_string(), json!("rocking")); + }), + ("null release_status", |m: &mut Obj| { + m.insert("release_status".to_string(), json!(null)); + }), + ("bool release_status", |m: &mut Obj| { + m.insert("release_status".to_string(), json!(true)); + }), + ("number release_status", |m: &mut Obj| { + m.insert("release_status".to_string(), json!(42)); + }), + ("object release_status", |m: &mut Obj| { + m.insert("release_status".to_string(), json!({})); + }), + ("array release_status", |m: &mut Obj| { + m.insert("release_status".to_string(), json!([])); + }), + ("null resources", |m: &mut Obj| { + m.insert("resources".to_string(), json!(null)); + }), + ("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!([])); + }), + ("homepage resource null", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("homepage".to_string(), json!(null)); + }), + ("homepage resource non-url", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("homepage".to_string(), json!("not a url")); + }), + ("bugtracker resource null", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("bugtracker".to_string(), json!(null)); + }), + ("bugtracker resource array", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("bugtracker".to_string(), json!(["hi"])); + }), + ("bugtracker resource invalid key", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("bugtracker".to_string(), json!({"foo": 1})); + }), + ("bugtracker resource array", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("bugtracker".to_string(), json!(["hi"])); + }), + ("bugtracker invalid url", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("bugtracker".to_string(), json!({"web": "not a url"})); + }), + ("bugtracker invalid email", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("bugtracker".to_string(), json!({"mailto": "not an email"})); + }), + ("repository resource undef", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("repository".to_string(), json!(null)); + }), + ("repository resource array", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("repository".to_string(), json!(["hi"])); + }), + ("repository resource invalid key", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("repository".to_string(), json!({"foo": 1})); + }), + ("repository resource invalid url", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert( + "repository".to_string(), + json!({"url": "not a url", "type": "x"}), + ); + }), + ("repository resource invalid web url", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert("repository".to_string(), json!({"web": "not a url"})); + }), + ("repository resource invalid type", |m: &mut Obj| { + let res = m.get_mut("resources").unwrap().as_object_mut().unwrap(); + res.insert( + "repository".to_string(), + json!({"url": "x:y", "type": "Foo"}), + ); + }), + ]; + for tc in INVALID_TEST_CASES { + let mut meta = valid_distribution(); + let map = meta.as_object_mut().unwrap(); + tc.1(map); + if schemas.validate(&meta, idx).is_ok() { + panic!("{} unexpectedly passed!", tc.0) + } + } + + Ok(()) +}