diff --git a/src/meta/mod.rs b/src/meta/mod.rs index f2472b2..312af27 100644 --- a/src/meta/mod.rs +++ b/src/meta/mod.rs @@ -8,7 +8,7 @@ Spec `META.json` files. It supports both the [v1] and [v2] specs. [v2]: https://github.com/pgxn/rfcs/pull/3 */ -use std::{collections::HashMap, error::Error, fs::File, path::PathBuf}; +use std::{borrow::Borrow, collections::HashMap, error::Error, fs::File, path::PathBuf}; use crate::util; use relative_path::RelativePathBuf; @@ -22,11 +22,23 @@ mod v2; /// Represents the `meta-spec` object in [`Meta`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Spec { - version: String, + version: Version, #[serde(skip_serializing_if = "Option::is_none")] url: Option, } +impl Spec { + /// Borrows the Spec version. + pub fn version(&self) -> &Version { + self.version.borrow() + } + + /// Borrows the Spec URL. + pub fn url(&self) -> Option<&String> { + self.url.as_ref() + } +} + /// Maintainer represents an object in the list of `maintainers` in [`Meta`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Maintainer { @@ -37,6 +49,23 @@ pub struct Maintainer { url: Option, } +impl Maintainer { + /// Borrows the Maintainer name. + pub fn name(&self) -> &str { + self.name.as_str() + } + + /// Borrows the Maintainer email. + pub fn email(&self) -> Option<&String> { + self.email.as_ref() + } + + /// Borrows the Maintainer URL. + pub fn url(&self) -> Option<&String> { + self.url.as_ref() + } +} + /// Describes an extension in under `extensions` in [`Contents`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Extension { @@ -51,26 +80,82 @@ pub struct Extension { doc: Option, } +impl Extension { + /// Borrows the Extension control file location. + pub fn control(&self) -> &RelativePathBuf { + self.control.borrow() + } + + /// Borrows the Extension abstract. + pub fn abs_tract(&self) -> Option<&String> { + self.abs_tract.as_ref() + } + + /// Returns true if the Extension is marked as a trusted language + /// extension. + pub fn tle(&self) -> bool { + self.tle.unwrap_or(false) + } + + /// Borrows the Extension sql file location. + pub fn sql(&self) -> &RelativePathBuf { + self.sql.borrow() + } + + /// Borrows the Extension doc file location. + pub fn doc(&self) -> Option<&RelativePathBuf> { + self.doc.as_ref() + } +} + /// Defines a type of module in [`Module`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] -enum ModuleType { +pub enum ModuleType { + /// Indicates an extension shared library module. #[serde(rename = "extension")] Extension, + /// Indicates a hook shared library module. #[serde(rename = "hook")] Hook, + /// Indicates a background worker shared library module. #[serde(rename = "bgw")] Bgw, } +impl std::fmt::Display for ModuleType { + /// fmt writes the sting representation of the ModuleType to f. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModuleType::Extension => write!(f, "extension"), + ModuleType::Hook => write!(f, "hook"), + ModuleType::Bgw => write!(f, "bgw"), + } + } +} + /// Defines the values for the `preload` value in [`Module`]s. #[derive(Serialize, Deserialize, PartialEq, Debug)] -enum Preload { +pub enum Preload { + /// Indicates a module that should be included in + /// `shared_preload_libraries` and requires a service restart. #[serde(rename = "server")] Server, + /// Indicates a module that can be loaded in a session via + /// `session_preload_libraries` or `local_preload_libraries`. #[serde(rename = "session")] Session, } +impl std::fmt::Display for Preload { + /// fmt writes the sting representation of the ModuleType to f. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Preload::Server => write!(f, "server"), + Preload::Session => write!(f, "session"), + } + } +} + /// Represents a loadable module under `modules` in [`Contents`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Module { @@ -86,6 +171,33 @@ pub struct Module { doc: Option, } +impl Module { + /// Borrows the Module type. + pub fn kind(&self) -> &ModuleType { + self.kind.borrow() + } + + /// Borrows the Module abstract. + pub fn abs_tract(&self) -> Option<&String> { + self.abs_tract.as_ref() + } + + /// Borrows the Module preload value. + pub fn preload(&self) -> Option<&Preload> { + self.preload.as_ref() + } + + /// Borrows the Module library file location. + pub fn lib(&self) -> &RelativePathBuf { + self.lib.borrow() + } + + /// Borrows the Module doc file location. + pub fn doc(&self) -> Option<&RelativePathBuf> { + self.doc.as_ref() + } +} + /// Represents an app under `apps` in [`Contents`]. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct App { diff --git a/src/meta/tests.rs b/src/meta/tests.rs index 06337cf..2a0e57b 100644 --- a/src/meta/tests.rs +++ b/src/meta/tests.rs @@ -419,3 +419,193 @@ fn test_try_merge_partman() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_spec() { + for (name, json) in [ + ( + "both", + json!({"version": "2.0.0", "url": "https://example.com"}), + ), + ("version only", json!({"version": "2.0.4"})), + ] { + let spec: Spec = serde_json::from_value(json.clone()).unwrap(); + assert_eq!( + json.get("version").unwrap().as_str().unwrap(), + spec.version().to_string(), + "{name}", + ); + match json.get("url") { + None => assert!(spec.url().is_none()), + Some(url) => assert_eq!(url.as_str().unwrap(), spec.url().unwrap()), + } + } +} + +#[test] +fn test_maintainer() { + for (name, json) in [ + ( + "all fields", + json!({ + "name": "Barrack Obama", + "email": "potus@example.com", + "url": "https://potus.example.com", + }), + ), + ( + "name and email", + json!({ + "name": "Barrack Obama", + "email": "potus@example.com", + }), + ), + ( + "name and url", + json!({ + "name": "Barrack Obama", + "url": "https://potus.example.com", + }), + ), + ] { + let maintainer: Maintainer = serde_json::from_value(json.clone()).unwrap(); + assert_eq!( + json.get("name").unwrap().as_str().unwrap(), + maintainer.name().to_string(), + "{name}", + ); + match json.get("email") { + None => assert!(maintainer.email().is_none()), + Some(email) => assert_eq!(email.as_str().unwrap(), maintainer.email().unwrap()), + } + match json.get("url") { + None => assert!(maintainer.url().is_none()), + Some(url) => assert_eq!(url.as_str().unwrap(), maintainer.url().unwrap()), + } + } +} + +#[test] +fn test_extension() { + for (name, json) in [ + ( + "all fields", + json!({ + "control": "pair.control", + "abstract": "We have assumed control", + "tle": true, + "sql": "pair.sql", + "doc": "doc/pair.md", + }), + ), + ( + "minimal", + json!({ + "control": "pair.control", + "sql": "pair.sql", + }), + ), + ( + "false tle", + json!({ + "control": "pair.control", + "tle": false, + "sql": "pair.sql", + }), + ), + ] { + let extension: Extension = serde_json::from_value(json.clone()).unwrap(); + assert_eq!( + json.get("control").unwrap().as_str().unwrap(), + extension.control().to_string(), + "{name} control", + ); + assert_eq!( + json.get("sql").unwrap().as_str().unwrap(), + extension.sql().to_string(), + "{name} sql", + ); + let tle = match json.get("tle") { + None => false, + Some(tle) => tle.as_bool().unwrap(), + }; + assert_eq!(tle, extension.tle()); + match json.get("abstract") { + None => assert!(extension.abs_tract().is_none()), + Some(abs) => assert_eq!(abs.as_str().unwrap(), extension.abs_tract().unwrap()), + } + match json.get("doc") { + None => assert!(extension.doc().is_none()), + Some(doc) => assert_eq!(doc.as_str().unwrap(), extension.doc().unwrap()), + } + } +} + +#[test] +fn test_module_type() { + for (name, mod_type) in [ + ("extension", ModuleType::Extension), + ("hook", ModuleType::Hook), + ("bgw", ModuleType::Bgw), + ] { + let mt: ModuleType = serde_json::from_value(json!(name)).unwrap(); + assert_eq!(mod_type, mt); + assert_eq!(name, mt.to_string()) + } +} + +#[test] +fn test_preload() { + for (name, preload) in [("server", Preload::Server), ("session", Preload::Session)] { + let pre: Preload = serde_json::from_value(json!(name)).unwrap(); + assert_eq!(preload, pre); + assert_eq!(name, pre.to_string()) + } +} + +#[test] +fn test_module() { + for (name, json) in [ + ( + "all fields", + json!({ + "type": "hook", + "lib": "lib/my_hook", + "doc": "doc/my_hook.md", + "preload": "session", + "abstract": "My hook" + }), + ), + ( + "minimal", + json!({ + "type": "extension", + "lib": "lib/my_hook", + }), + ), + ] { + let module: Module = serde_json::from_value(json.clone()).unwrap(); + assert_eq!( + json.get("type").unwrap().as_str().unwrap(), + module.kind().to_string(), + "{name} type", + ); + assert_eq!( + json.get("lib").unwrap().as_str().unwrap(), + module.lib().to_string(), + "{name} lib", + ); + match json.get("preload") { + None => assert!(module.preload().is_none()), + Some(pre) => assert_eq!(pre.as_str().unwrap(), module.preload().unwrap().to_string()), + } + match json.get("abstract") { + None => assert!(module.abs_tract().is_none()), + Some(abs) => assert_eq!(abs.as_str().unwrap(), module.abs_tract().unwrap()), + } + match json.get("doc") { + None => assert!(module.doc().is_none()), + Some(doc) => assert_eq!(doc.as_str().unwrap(), module.doc().unwrap()), + } + } +}