diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml index 67d5f19..3d5d1c1 100644 --- a/.github/workflows/test-and-lint.yml +++ b/.github/workflows/test-and-lint.yml @@ -43,3 +43,5 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} slug: pgxn/meta files: target/cover/coveralls + - name: Clear Badge Cache + uses: kevincobain2000/action-camo-purge@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 619a750..35bd90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file. It uses the [Semantic Versioning]: https://semver.org/spec/v2.0.0.html "Semantic Versioning 2.0.0" +## [Unreleased] — Date TBD + + ## [v0.1.0] — 2024-08-08 The theme of this release is *Cross Compilation.* diff --git a/Cargo.lock b/Cargo.lock index 67c7cb2..16ebd59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,7 @@ dependencies = [ "boon", "lexopt", "relative-path", + "semver", "serde", "serde_json", "spdx", @@ -268,6 +269,9 @@ name = "relative-path" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +dependencies = [ + "serde", +] [[package]] name = "ryu" @@ -284,6 +288,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + [[package]] name = "serde" version = "1.0.203" diff --git a/Cargo.toml b/Cargo.toml index 2642977..acc81eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,8 @@ exclude = [ ".github", ".vscode", ".gitignore", ".ci", ".pre-*.yaml"] [dependencies] boon = "0.6" lexopt = "0.3.0" -relative-path = "1.9.3" +relative-path = { version = "1.9", features = ["serde"] } +semver = { version = "1.0", features = ["std", "serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1.0" spdx = "0.10.6" diff --git a/schema/v1/meta-spec.schema.json b/schema/v1/meta-spec.schema.json index ba1a648..e2f3541 100644 --- a/schema/v1/meta-spec.schema.json +++ b/schema/v1/meta-spec.schema.json @@ -12,7 +12,10 @@ }, "url": { "type": "string", - "const": "https://pgxn.org/meta/spec.txt", + "enum": [ + "https://pgxn.org/meta/spec.txt", + "http://pgxn.org/meta/spec.txt" + ], "description": "The URI of the metadata specification document corresponding to the given version. This is strictly for human-consumption and should not impact the interpretation of the document." } }, diff --git a/src/lib.rs b/src/lib.rs index 3030dc1..e452393 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,8 @@ assert!(validator.validate(&meta).is_ok()); mod valid; pub use valid::{ValidationError, Validator}; +mod meta; +// pub use meta::*; #[cfg(test)] mod tests; diff --git a/src/meta/mod.rs b/src/meta/mod.rs new file mode 100644 index 0000000..4dd07e1 --- /dev/null +++ b/src/meta/mod.rs @@ -0,0 +1,261 @@ +use std::{collections::HashMap, error::Error, fs::File, path::PathBuf}; + +use relative_path::RelativePathBuf; +use semver::Version; +use serde::{Deserialize, Serialize}; +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, +} + +/// Maintainer represents an object in the list of `maintainers` in [`Meta`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Maintainer { + name: String, + email: Option, + url: Option, +} + +/// Describes an extension in under `extensions` in [`Contents`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Extension { + control: RelativePathBuf, + #[serde(rename = "abstract")] + abs_tract: Option, + tle: Option, + sql: RelativePathBuf, + doc: Option, +} + +/// Defines a type of module in [`Module`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +enum ModuleType { + #[serde(rename = "extension")] + Extension, + #[serde(rename = "hook")] + Hook, + #[serde(rename = "bgw")] + Bgw, +} + +/// Defines the values for the `preload` value in [`Module`]s. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +enum Preload { + #[serde(rename = "server")] + Server, + #[serde(rename = "session")] + Session, +} + +/// Represents a loadable module under `modules` in [`Contents`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Module { + #[serde(rename = "type")] + kind: ModuleType, + #[serde(rename = "abstract")] + abs_tract: Option, + preload: Option, + lib: RelativePathBuf, + doc: Option, +} + +/// Represents an app under `apps` in [`Contents`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct App { + lang: Option, + #[serde(rename = "abstract")] + abs_tract: Option, + bin: RelativePathBuf, + doc: Option, + lib: Option, + man: Option, + html: Option, +} + +/// Represents the contents of a distribution, under `contents` in [`Meta`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Contents { + extensions: Option>, + modules: Option>, + apps: Option>, +} + +/// Represents the classifications of a distribution, under `classifications` +/// in [`Meta`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Classifications { + tags: Option>, + categories: Option>, +} + +/// Represents Postgres requirements under `postgres` in [`Dependencies`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Postgres { + version: String, + with: Option>, +} + +/// Represents the name of a build pipeline under `pipeline` in +/// [`Dependencies`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub enum Pipeline { + /// PGXS + #[serde(rename = "pgxs")] + Pgxs, + #[serde(rename = "meson")] + /// Meson + Meson, + #[serde(rename = "pgrx")] + /// pgrx + Pgrx, + /// Autoconf + #[serde(rename = "autoconf")] + Autoconf, + /// cmake + #[serde(rename = "cmake")] + Cmake, +} + +/// Defines a version range for [`Phase`] dependencies. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(untagged)] +pub enum VersionRange { + /// Represents `0` as a shorthand for "no specific version". + Integer(u8), + /// Represents a string defining a version range. + String(String), +} + +/// Defines the relationships for a build phase in [`Packages`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Phase { + requires: Option>, + recommends: Option>, + suggests: Option>, + conflicts: Option>, +} + +/// Defines package dependencies for build phases under `packages` in +/// [`Dependencies`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Packages { + configure: Option, + build: Option, + test: Option, + run: Option, + develop: Option, +} + +/// Defines dependency variations under `variations`in [`Dependencies`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Variations { + #[serde(rename = "where")] + wheres: Box, + dependencies: Box, +} + +/// Defines the distribution dependencies under `dependencies` in [`Meta`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Dependencies { + platforms: Option>, + postgres: Option, + pipeline: Option, + packages: Option, + variations: Option>, +} + +/// Defines the resources under `resources` in [`Meta`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Resources { + homepage: Option, + issues: Option, + repository: Option, + docs: Option, + support: Option, +} + +/// Defines the artifacts in the array under `artifacts` in [`Meta`]. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Artifact { + url: String, + #[serde(rename = "type")] + kind: String, + platform: Option, + sha256: Option, + sha512: Option, +} + +/// Represents a complete PGXN Meta definition. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Meta { + name: String, + version: Version, + #[serde(rename = "abstract")] + abs_tract: String, + description: Option, + producer: Option, + license: String, // use spdx::Expression. + #[serde(rename = "meta-spec")] + spec: Spec, + maintainers: Vec, + classifications: Option, + contents: Contents, + ignore: Option>, + dependencies: Option, + resources: Option, + artifacts: Option>, +} + +impl Meta { + fn from_version(version: u8, meta: Value) -> Result> { + match version { + 1 => v1::from_value(meta), + 2 => v2::from_value(meta), + _ => Err(Box::from(format!("Unknown meta version {version}"))), + } + } +} + +impl TryFrom for Meta { + type Error = Box; + fn try_from(meta: Value) -> Result { + // Make sure it's valid. + let mut validator = crate::valid::Validator::new(); + let version = match validator.validate(&meta) { + Err(e) => return Err(Box::from(e.to_string())), + Ok(v) => v, + }; + Meta::from_version(version, meta) + } +} + +impl TryFrom<&PathBuf> for Meta { + type Error = Box; + fn try_from(file: &PathBuf) -> Result { + let meta: Value = serde_json::from_reader(File::open(file)?)?; + Meta::try_from(meta) + } +} + +impl TryFrom<&String> for Meta { + type Error = Box; + fn try_from(str: &String) -> Result { + let meta: Value = serde_json::from_str(str)?; + Meta::try_from(meta) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/meta/tests.rs b/src/meta/tests.rs new file mode 100644 index 0000000..4455d6b --- /dev/null +++ b/src/meta/tests.rs @@ -0,0 +1,82 @@ +use super::*; +use serde_json::{json, Value}; +use std::{error::Error, fs, fs::File, path::PathBuf}; +use wax::Glob; + +#[test] +fn test_corpus() -> Result<(), Box> { + for v_dir in ["v2"] { + let dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "corpus", v_dir] + .iter() + .collect(); + let glob = Glob::new("*.json")?; + + for path in glob.walk(dir) { + let path = path?.into_path(); + + // Test try_from path. + if let Err(e) = Meta::try_from(&path) { + panic!("{v_dir}/{:?} failed: {e}", path.file_name().unwrap()); + } + + // 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()); + } + + // 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()); + } + + println!("Example {v_dir}/{:?} ok", path.file_name().unwrap()); + } + } + Ok(()) +} + +#[test] +fn test_bad_corpus() -> Result<(), Box> { + let file: PathBuf = [env!("CARGO_MANIFEST_DIR"), "corpus", "invalid.json"] + .iter() + .collect(); + let mut meta: Value = serde_json::from_reader(File::open(&file)?)?; + + // Make sure we catch the validation failure. + match Meta::try_from(meta.clone()) { + Ok(_) => panic!( + "Should have failed on {:?} but did not", + file.file_name().unwrap() + ), + Err(e) => assert!(e.to_string().contains(" missing properties 'version")), + } + + // Make sure we fail on invalid version. + match Meta::from_version(99, meta.clone()) { + Ok(_) => panic!( + "Should have failed on {:?} but did not", + file.file_name().unwrap() + ), + Err(e) => assert_eq!("Unknown meta version 99", e.to_string()), + } + + // Should fail when no meta-spec. + meta.as_object_mut().unwrap().remove("meta-spec"); + match Meta::try_from(meta.clone()) { + Ok(_) => panic!( + "Should have failed on {:?} but did not", + file.file_name().unwrap() + ), + Err(e) => assert_eq!("Cannot determine meta-spec version", e.to_string()), + } + + // Make sure we catch a failure parsing into a Meta struct. + match v2::from_value(json!({"invalid": true})) { + Ok(_) => panic!("Should have failed on invalid meta contents but did not",), + Err(e) => assert_eq!("missing field `name`", e.to_string()), + } + + Ok(()) +} diff --git a/src/meta/v1/mod.rs b/src/meta/v1/mod.rs new file mode 100644 index 0000000..f186948 --- /dev/null +++ b/src/meta/v1/mod.rs @@ -0,0 +1,5 @@ +use super::*; + +pub fn from_value(_: Value) -> Result> { + todo!(); +} diff --git a/src/meta/v2/mod.rs b/src/meta/v2/mod.rs new file mode 100644 index 0000000..deb65e9 --- /dev/null +++ b/src/meta/v2/mod.rs @@ -0,0 +1,8 @@ +use super::*; + +pub fn from_value(meta: Value) -> Result> { + match serde_json::from_value(meta) { + Ok(m) => Ok(m), + Err(e) => Err(Box::from(e)), + } +} diff --git a/src/tests/v1.rs b/src/tests/v1.rs index df1b3e4..a8136c1 100644 --- a/src/tests/v1.rs +++ b/src/tests/v1.rs @@ -533,9 +533,13 @@ fn test_v1_meta_spec() -> Result<(), Box> { ("x key", json!({"version": "1.0.99", "x_y": true})), ("X key", json!({"version": "1.0.99", "X_x": true})), ( - "version plus URL", + "version plus https URL", json!({"version": "1.0.0", "url": "https://pgxn.org/meta/spec.txt"}), ), + ( + "version plus http URL", + json!({"version": "1.0.0", "url": "http://pgxn.org/meta/spec.txt"}), + ), ] { if let Err(e) = schemas.validate(&valid_meta_spec.1, idx) { panic!("extension {} failed: {e}", valid_meta_spec.0); diff --git a/src/valid/mod.rs b/src/valid/mod.rs index 7392ac4..ccd6655 100644 --- a/src/valid/mod.rs +++ b/src/valid/mod.rs @@ -61,8 +61,9 @@ impl Validator { /// Validates a PGXN Meta document. /// /// Load a `META.json` file into a serde_json::value::Value and pass it - /// for validation. Returns a validation error on failure. - pub fn validate<'a>(&'a mut self, meta: &'a Value) -> Result<(), Box> { + /// for validation. Returns a the Meta spec version on success and a + /// validation error on failure. + pub fn validate<'a>(&'a mut self, meta: &'a Value) -> Result> { let map = meta.as_object().ok_or(ValidationError::UnknownSpec)?; let version = map .get("meta-spec") @@ -86,7 +87,7 @@ impl Validator { let idx = compiler.compile(&id, schemas)?; schemas.validate(meta, idx)?; - Ok(()) + Ok(v) } }