diff --git a/Cargo.lock b/Cargo.lock index c417a5f..44f56dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,16 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e149dc73cd30538307e7ffa2acd3d2221148eaeed4871f246657b1c3eaa1cbd2" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "base64" version = "0.21.7" @@ -207,6 +217,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" name = "pgxn_meta" version = "0.1.0" dependencies = [ + "assert-json-diff", "boon", "email_address", "lexopt", diff --git a/Cargo.toml b/Cargo.toml index d26cc38..2641916 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ exclude = [ ".github", ".vscode", ".gitignore", ".ci", ".pre-*.yaml"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +assert-json-diff = "2.0.2" boon = "0.6" email_address = "0.2.9" lexopt = "0.3.0" diff --git a/corpus/v2/typical-pgrx.json b/corpus/v2/typical-pgrx.json index a7bec8d..4df6035 100644 --- a/corpus/v2/typical-pgrx.json +++ b/corpus/v2/typical-pgrx.json @@ -19,7 +19,6 @@ "jsonschema": { "abstract": "JSON Schema validation functions for PostgreSQL", "doc": "doc/jsonschema.md", - "x_comment": "Paths non-deterministic until pgrx ", "sql": "target/release/**/jsonschema--0.1.1.sql", "control": "target/release/**/jsonschema.control" } diff --git a/src/meta/mod.rs b/src/meta/mod.rs index 4dd07e1..317dcd5 100644 --- a/src/meta/mod.rs +++ b/src/meta/mod.rs @@ -8,23 +8,21 @@ use serde_json::Value; mod v1; mod v2; -fn meta_url() -> String { - "https://rfcs.pgxn.org/0003-meta-spec-v2.html".to_string() -} - /// Represents the `meta-spec` object in [`Meta`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Spec { version: String, - #[serde(default = "meta_url")] - url: String, + #[serde(skip_serializing_if = "Option::is_none")] + url: Option, } /// Maintainer represents an object in the list of `maintainers` in [`Meta`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Maintainer { name: String, + #[serde(skip_serializing_if = "Option::is_none")] email: Option, + #[serde(skip_serializing_if = "Option::is_none")] url: Option, } @@ -33,9 +31,12 @@ pub struct Maintainer { pub struct Extension { control: RelativePathBuf, #[serde(rename = "abstract")] + #[serde(skip_serializing_if = "Option::is_none")] abs_tract: Option, + #[serde(skip_serializing_if = "Option::is_none")] tle: Option, sql: RelativePathBuf, + #[serde(skip_serializing_if = "Option::is_none")] doc: Option, } @@ -65,30 +66,42 @@ pub struct Module { #[serde(rename = "type")] kind: ModuleType, #[serde(rename = "abstract")] + #[serde(skip_serializing_if = "Option::is_none")] abs_tract: Option, + #[serde(skip_serializing_if = "Option::is_none")] preload: Option, lib: RelativePathBuf, + #[serde(skip_serializing_if = "Option::is_none")] doc: Option, } /// Represents an app under `apps` in [`Contents`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct App { + #[serde(skip_serializing_if = "Option::is_none")] lang: Option, #[serde(rename = "abstract")] + #[serde(skip_serializing_if = "Option::is_none")] abs_tract: Option, bin: RelativePathBuf, + #[serde(skip_serializing_if = "Option::is_none")] doc: Option, + #[serde(skip_serializing_if = "Option::is_none")] lib: Option, + #[serde(skip_serializing_if = "Option::is_none")] man: Option, + #[serde(skip_serializing_if = "Option::is_none")] html: Option, } /// Represents the contents of a distribution, under `contents` in [`Meta`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Contents { + #[serde(skip_serializing_if = "Option::is_none")] extensions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] modules: Option>, + #[serde(skip_serializing_if = "Option::is_none")] apps: Option>, } @@ -96,7 +109,9 @@ pub struct Contents { /// in [`Meta`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Classifications { + #[serde(skip_serializing_if = "Option::is_none")] tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] categories: Option>, } @@ -104,6 +119,7 @@ pub struct Classifications { #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Postgres { version: String, + #[serde(skip_serializing_if = "Option::is_none")] with: Option>, } @@ -141,9 +157,13 @@ pub enum VersionRange { /// Defines the relationships for a build phase in [`Packages`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Phase { + #[serde(skip_serializing_if = "Option::is_none")] requires: Option>, + #[serde(skip_serializing_if = "Option::is_none")] recommends: Option>, + #[serde(skip_serializing_if = "Option::is_none")] suggests: Option>, + #[serde(skip_serializing_if = "Option::is_none")] conflicts: Option>, } @@ -151,10 +171,15 @@ pub struct Phase { /// [`Dependencies`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Packages { + #[serde(skip_serializing_if = "Option::is_none")] configure: Option, + #[serde(skip_serializing_if = "Option::is_none")] build: Option, + #[serde(skip_serializing_if = "Option::is_none")] test: Option, + #[serde(skip_serializing_if = "Option::is_none")] run: Option, + #[serde(skip_serializing_if = "Option::is_none")] develop: Option, } @@ -169,21 +194,42 @@ pub struct Variations { /// Defines the distribution dependencies under `dependencies` in [`Meta`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Dependencies { + #[serde(skip_serializing_if = "Option::is_none")] platforms: Option>, + #[serde(skip_serializing_if = "Option::is_none")] postgres: Option, + #[serde(skip_serializing_if = "Option::is_none")] pipeline: Option, + #[serde(skip_serializing_if = "Option::is_none")] packages: Option, + #[serde(skip_serializing_if = "Option::is_none")] variations: Option>, } +/// Defines the badges under `badges` in [`Resources`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Badge { + src: String, + alt: String, + #[serde(skip_serializing_if = "Option::is_none")] + url: Option, +} + /// Defines the resources under `resources` in [`Meta`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Resources { + #[serde(skip_serializing_if = "Option::is_none")] homepage: Option, + #[serde(skip_serializing_if = "Option::is_none")] issues: Option, + #[serde(skip_serializing_if = "Option::is_none")] repository: Option, + #[serde(skip_serializing_if = "Option::is_none")] docs: Option, + #[serde(skip_serializing_if = "Option::is_none")] support: Option, + #[serde(skip_serializing_if = "Option::is_none")] + badges: Option>, } /// Defines the artifacts in the array under `artifacts` in [`Meta`]. @@ -192,8 +238,11 @@ pub struct Artifact { url: String, #[serde(rename = "type")] kind: String, + #[serde(skip_serializing_if = "Option::is_none")] platform: Option, + #[serde(skip_serializing_if = "Option::is_none")] sha256: Option, + #[serde(skip_serializing_if = "Option::is_none")] sha512: Option, } @@ -204,17 +253,24 @@ pub struct Meta { version: Version, #[serde(rename = "abstract")] abs_tract: String, + #[serde(skip_serializing_if = "Option::is_none")] description: Option, + #[serde(skip_serializing_if = "Option::is_none")] producer: Option, license: String, // use spdx::Expression. #[serde(rename = "meta-spec")] spec: Spec, maintainers: Vec, + #[serde(skip_serializing_if = "Option::is_none")] classifications: Option, contents: Contents, + #[serde(skip_serializing_if = "Option::is_none")] ignore: Option>, + #[serde(skip_serializing_if = "Option::is_none")] dependencies: Option, + #[serde(skip_serializing_if = "Option::is_none")] resources: Option, + #[serde(skip_serializing_if = "Option::is_none")] artifacts: Option>, } @@ -241,6 +297,14 @@ impl TryFrom for Meta { } } +impl TryFrom for Value { + type Error = Box; + fn try_from(meta: Meta) -> Result { + let val = serde_json::to_value(meta)?; + Ok(val) + } +} + impl TryFrom<&PathBuf> for Meta { type Error = Box; fn try_from(file: &PathBuf) -> Result { @@ -257,5 +321,13 @@ impl TryFrom<&String> for Meta { } } +impl TryFrom for String { + type Error = Box; + fn try_from(meta: Meta) -> Result { + let val = serde_json::to_string(&meta)?; + Ok(val) + } +} + #[cfg(test)] mod tests; diff --git a/src/meta/tests.rs b/src/meta/tests.rs index afccb99..cf5b0b5 100644 --- a/src/meta/tests.rs +++ b/src/meta/tests.rs @@ -13,6 +13,7 @@ fn test_corpus() -> Result<(), Box> { for path in glob.walk(dir) { let path = path?.into_path(); + let contents: Value = serde_json::from_reader(File::open(&path)?)?; // Test try_from path. if let Err(e) = Meta::try_from(&path) { @@ -21,14 +22,38 @@ fn test_corpus() -> Result<(), Box> { // Test try_from str. let str: String = fs::read_to_string(&path)?; - if let Err(e) = Meta::try_from(&str) { - panic!("{v_dir}/{:?} failed: {e}", path.file_name().unwrap()); + match Meta::try_from(&str) { + Err(e) => panic!("{v_dir}/{:?} failed: {e}", path.file_name().unwrap()), + Ok(m) => { + if v_dir == "v2" { + // Make sure round-trip produces the same JSON. + let output: Result> = m.try_into(); + match output { + Err(e) => panic!("{v_dir}/{:?} failed: {e}", path.file_name().unwrap()), + Ok(val) => { + assert_json_diff::assert_json_eq!(&contents, &val); + } + }; + } + } } // Test try_from value. - let meta: Value = serde_json::from_reader(File::open(&path)?)?; - if let Err(e) = Meta::try_from(meta) { - panic!("{v_dir}/{:?} failed: {e}", path.file_name().unwrap()); + match Meta::try_from(contents.clone()) { + Err(e) => panic!("{v_dir}/{:?} failed: {e}", path.file_name().unwrap()), + Ok(m) => { + if v_dir == "v2" { + // Make sure value round-trip produces the same JSON. + let output: Result> = m.try_into(); + match output { + Err(e) => panic!("{v_dir}/{:?} failed: {e}", path.file_name().unwrap()), + Ok(val) => { + let val: Value = serde_json::from_str(&val)?; + assert_json_diff::assert_json_eq!(&contents, &val); + } + }; + } + } } println!("Example {v_dir}/{:?} ok", path.file_name().unwrap()); diff --git a/src/meta/v1/mod.rs b/src/meta/v1/mod.rs index bb82a36..954c942 100644 --- a/src/meta/v1/mod.rs +++ b/src/meta/v1/mod.rs @@ -97,7 +97,7 @@ fn v1_to_v2_maintainers(v1: &Value) -> Result> { /// Otherwise the string will be saved as the maintainer `name` and the `url` /// set to either the `homepage` in the `resources` object in `v1`, or else /// `https://pgxn.org`. -fn parse_v1_maintainers(v1: &Value, list: &Vec) -> Result> { +fn parse_v1_maintainers(v1: &Value, list: &[Value]) -> Result> { let mut new_list: Vec = Vec::with_capacity(list.len()); for v in list { if let Some(str) = v.as_str() {