use std::{borrow::Borrow, collections::HashMap, fs::File, path::Path};
use crate::{error::Error, util};
use relative_path::{RelativePath, RelativePathBuf};
use semver::Version;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
pub(crate) mod v1;
mod v2;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Spec {
version: Version,
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Spec {
pub fn version(&self) -> &Version {
self.version.borrow()
}
pub fn url(&self) -> Option<&str> {
self.url.as_deref()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Maintainer {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Maintainer {
pub fn name(&self) -> &str {
self.name.as_str()
}
pub fn email(&self) -> Option<&str> {
self.email.as_deref()
}
pub fn url(&self) -> Option<&str> {
self.url.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Extension {
control: RelativePathBuf,
#[serde(rename = "abstract")]
#[serde(skip_serializing_if = "Option::is_none")]
abs_tract: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tle: Option<bool>,
sql: RelativePathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
doc: Option<RelativePathBuf>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Extension {
pub fn control(&self) -> &RelativePathBuf {
self.control.borrow()
}
pub fn abs_tract(&self) -> Option<&str> {
self.abs_tract.as_deref()
}
pub fn tle(&self) -> bool {
self.tle.unwrap_or(false)
}
pub fn sql(&self) -> &RelativePathBuf {
self.sql.borrow()
}
pub fn doc(&self) -> Option<&RelativePath> {
self.doc.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum ModuleType {
#[serde(rename = "extension")]
Extension,
#[serde(rename = "hook")]
Hook,
#[serde(rename = "bgw")]
Bgw,
}
impl std::fmt::Display for ModuleType {
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"),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum Preload {
#[serde(rename = "server")]
Server,
#[serde(rename = "session")]
Session,
}
impl std::fmt::Display for Preload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Preload::Server => write!(f, "server"),
Preload::Session => write!(f, "session"),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Module {
#[serde(rename = "type")]
kind: ModuleType,
#[serde(rename = "abstract")]
#[serde(skip_serializing_if = "Option::is_none")]
abs_tract: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
preload: Option<Preload>,
lib: RelativePathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
doc: Option<RelativePathBuf>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Module {
pub fn kind(&self) -> &ModuleType {
self.kind.borrow()
}
pub fn abs_tract(&self) -> Option<&str> {
self.abs_tract.as_deref()
}
pub fn preload(&self) -> Option<&Preload> {
self.preload.as_ref()
}
pub fn lib(&self) -> &RelativePathBuf {
self.lib.borrow()
}
pub fn doc(&self) -> Option<&RelativePath> {
self.doc.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct App {
#[serde(skip_serializing_if = "Option::is_none")]
lang: Option<String>,
#[serde(rename = "abstract")]
#[serde(skip_serializing_if = "Option::is_none")]
abs_tract: Option<String>,
bin: RelativePathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
doc: Option<RelativePathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
lib: Option<RelativePathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
man: Option<RelativePathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
html: Option<RelativePathBuf>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl App {
pub fn lang(&self) -> Option<&str> {
self.lang.as_deref()
}
pub fn abs_tract(&self) -> Option<&str> {
self.abs_tract.as_deref()
}
pub fn bin(&self) -> &RelativePathBuf {
self.bin.borrow()
}
pub fn lib(&self) -> Option<&RelativePath> {
self.lib.as_deref()
}
pub fn doc(&self) -> Option<&RelativePath> {
self.doc.as_deref()
}
pub fn man(&self) -> Option<&RelativePath> {
self.man.as_deref()
}
pub fn html(&self) -> Option<&RelativePath> {
self.html.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Contents {
#[serde(skip_serializing_if = "Option::is_none")]
extensions: Option<HashMap<String, Extension>>,
#[serde(skip_serializing_if = "Option::is_none")]
modules: Option<HashMap<String, Module>>,
#[serde(skip_serializing_if = "Option::is_none")]
apps: Option<HashMap<String, App>>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Contents {
pub fn extensions(&self) -> Option<&HashMap<String, Extension>> {
self.extensions.as_ref()
}
pub fn modules(&self) -> Option<&HashMap<String, Module>> {
self.modules.as_ref()
}
pub fn apps(&self) -> Option<&HashMap<String, App>> {
self.apps.as_ref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Classifications {
#[serde(skip_serializing_if = "Option::is_none")]
tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
categories: Option<Vec<String>>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Classifications {
pub fn tags(&self) -> Option<&[String]> {
self.tags.as_deref()
}
pub fn categories(&self) -> Option<&[String]> {
self.categories.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Postgres {
version: String,
#[serde(skip_serializing_if = "Option::is_none")]
with: Option<Vec<String>>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Postgres {
pub fn version(&self) -> &str {
self.version.as_str()
}
pub fn with(&self) -> Option<&[String]> {
self.with.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum Pipeline {
#[serde(rename = "pgxs")]
Pgxs,
#[serde(rename = "meson")]
Meson,
#[serde(rename = "pgrx")]
Pgrx,
#[serde(rename = "autoconf")]
Autoconf,
#[serde(rename = "cmake")]
Cmake,
}
impl std::fmt::Display for Pipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Pipeline::Pgxs => write!(f, "pgxs"),
Pipeline::Meson => write!(f, "meson"),
Pipeline::Pgrx => write!(f, "pgrx"),
Pipeline::Autoconf => write!(f, "autoconf"),
Pipeline::Cmake => write!(f, "cmake"),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(untagged)]
pub enum VersionRange {
Integer(u8),
String(String),
}
impl std::fmt::Display for VersionRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VersionRange::Integer(int) => write!(f, "{int}"),
VersionRange::String(str) => write!(f, "{str}"),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Phase {
#[serde(skip_serializing_if = "Option::is_none")]
requires: Option<HashMap<String, VersionRange>>,
#[serde(skip_serializing_if = "Option::is_none")]
recommends: Option<HashMap<String, VersionRange>>,
#[serde(skip_serializing_if = "Option::is_none")]
suggests: Option<HashMap<String, VersionRange>>,
#[serde(skip_serializing_if = "Option::is_none")]
conflicts: Option<HashMap<String, VersionRange>>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Phase {
pub fn requires(&self) -> Option<&HashMap<String, VersionRange>> {
self.requires.as_ref()
}
pub fn recommends(&self) -> Option<&HashMap<String, VersionRange>> {
self.recommends.as_ref()
}
pub fn suggests(&self) -> Option<&HashMap<String, VersionRange>> {
self.suggests.as_ref()
}
pub fn conflicts(&self) -> Option<&HashMap<String, VersionRange>> {
self.conflicts.as_ref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Packages {
#[serde(skip_serializing_if = "Option::is_none")]
configure: Option<Phase>,
#[serde(skip_serializing_if = "Option::is_none")]
build: Option<Phase>,
#[serde(skip_serializing_if = "Option::is_none")]
test: Option<Phase>,
#[serde(skip_serializing_if = "Option::is_none")]
run: Option<Phase>,
#[serde(skip_serializing_if = "Option::is_none")]
develop: Option<Phase>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Packages {
pub fn configure(&self) -> Option<&Phase> {
self.configure.as_ref()
}
pub fn build(&self) -> Option<&Phase> {
self.build.as_ref()
}
pub fn test(&self) -> Option<&Phase> {
self.test.as_ref()
}
pub fn run(&self) -> Option<&Phase> {
self.run.as_ref()
}
pub fn develop(&self) -> Option<&Phase> {
self.develop.as_ref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Variations {
#[serde(rename = "where")]
wheres: Dependencies,
dependencies: Dependencies,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Variations {
pub fn wheres(&self) -> &Dependencies {
self.wheres.borrow()
}
pub fn dependencies(&self) -> &Dependencies {
self.dependencies.borrow()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Dependencies {
#[serde(skip_serializing_if = "Option::is_none")]
platforms: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
postgres: Option<Postgres>,
#[serde(skip_serializing_if = "Option::is_none")]
pipeline: Option<Pipeline>,
#[serde(skip_serializing_if = "Option::is_none")]
packages: Option<Packages>,
#[serde(skip_serializing_if = "Option::is_none")]
variations: Option<Vec<Variations>>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Dependencies {
pub fn platforms(&self) -> Option<&[String]> {
self.platforms.as_deref()
}
pub fn postgres(&self) -> Option<&Postgres> {
self.postgres.as_ref()
}
pub fn pipeline(&self) -> Option<&Pipeline> {
self.pipeline.as_ref()
}
pub fn packages(&self) -> Option<&Packages> {
self.packages.as_ref()
}
pub fn variations(&self) -> Option<&[Variations]> {
self.variations.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Badge {
src: String,
alt: String,
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Badge {
pub fn src(&self) -> &str {
self.src.as_str()
}
pub fn alt(&self) -> &str {
self.alt.as_str()
}
pub fn url(&self) -> Option<&str> {
self.url.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Resources {
#[serde(skip_serializing_if = "Option::is_none")]
homepage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
issues: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
repository: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
docs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
support: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
badges: Option<Vec<Badge>>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Resources {
pub fn homepage(&self) -> Option<&str> {
self.homepage.as_deref()
}
pub fn issues(&self) -> Option<&str> {
self.issues.as_deref()
}
pub fn repository(&self) -> Option<&str> {
self.repository.as_deref()
}
pub fn docs(&self) -> Option<&str> {
self.docs.as_deref()
}
pub fn support(&self) -> Option<&str> {
self.support.as_deref()
}
pub fn badges(&self) -> Option<&[Badge]> {
self.badges.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Artifact {
url: String,
#[serde(rename = "type")]
kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
platform: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
sha512: Option<String>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
impl Artifact {
pub fn url(&self) -> &str {
self.url.as_str()
}
pub fn kind(&self) -> &str {
self.kind.as_str()
}
pub fn platform(&self) -> Option<&str> {
self.platform.as_deref()
}
pub fn sha256(&self) -> Option<&str> {
self.sha256.as_deref()
}
pub fn sha512(&self) -> Option<&str> {
self.sha512.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Distribution {
name: String,
version: Version,
#[serde(rename = "abstract")]
abs_tract: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
producer: Option<String>,
license: String, #[serde(rename = "meta-spec")]
spec: Spec,
maintainers: Vec<Maintainer>,
#[serde(skip_serializing_if = "Option::is_none")]
classifications: Option<Classifications>,
contents: Contents,
#[serde(skip_serializing_if = "Option::is_none")]
ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
dependencies: Option<Dependencies>,
#[serde(skip_serializing_if = "Option::is_none")]
resources: Option<Resources>,
#[serde(skip_serializing_if = "Option::is_none")]
artifacts: Option<Vec<Artifact>>,
#[serde(flatten)]
#[serde(deserialize_with = "deserialize_custom_properties")]
custom_props: HashMap<String, Value>,
}
pub fn deserialize_custom_properties<'de, D>(
deserializer: D,
) -> Result<HashMap<String, Value>, D::Error>
where
D: Deserializer<'de>,
{
let map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
Ok(map
.into_iter()
.filter(|(key, _value)| key.starts_with("x_") || key.starts_with("X_"))
.collect())
}
impl Distribution {
fn from_version(version: u8, meta: Value) -> Result<Self, Error> {
match version {
1 => v1::from_value(meta),
2 => v2::from_value(meta),
_ => Err(Error::UnknownSpec),
}
}
pub fn load<P: AsRef<Path>>(file: P) -> Result<Self, Error> {
let meta: Value = serde_json::from_reader(File::open(file)?)?;
meta.try_into()
}
pub fn name(&self) -> &str {
self.name.as_str()
}
pub fn version(&self) -> &Version {
self.version.borrow()
}
pub fn abs_tract(&self) -> &str {
self.abs_tract.as_str()
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn producer(&self) -> Option<&str> {
self.producer.as_deref()
}
pub fn license(&self) -> &str {
self.license.as_str()
}
pub fn spec(&self) -> &Spec {
self.spec.borrow()
}
pub fn maintainers(&self) -> &[Maintainer] {
self.maintainers.borrow()
}
pub fn classifications(&self) -> Option<&Classifications> {
self.classifications.as_ref()
}
pub fn contents(&self) -> &Contents {
self.contents.borrow()
}
pub fn ignore(&self) -> Option<&[String]> {
self.ignore.as_deref()
}
pub fn dependencies(&self) -> Option<&Dependencies> {
self.dependencies.as_ref()
}
pub fn resources(&self) -> Option<&Resources> {
self.resources.as_ref()
}
pub fn artifacts(&self) -> Option<&[Artifact]> {
self.artifacts.as_deref()
}
pub fn custom_props(&self) -> &HashMap<String, Value> {
self.custom_props.borrow()
}
}
impl TryFrom<Value> for Distribution {
type Error = Error;
fn try_from(meta: Value) -> Result<Self, Self::Error> {
let mut validator = crate::valid::Validator::new();
let version = validator.validate(&meta)?;
Distribution::from_version(version, meta)
}
}
impl TryFrom<&[Value]> for Distribution {
type Error = Error;
fn try_from(meta: &[Value]) -> Result<Self, Self::Error> {
if meta.is_empty() {
return Err(Error::Param("meta contains no values"));
}
let version = util::get_version(&meta[0]).ok_or(Error::UnknownSpec)?;
let mut v2 = match version {
1 => v1::to_v2(&meta[0])?,
2 => meta[0].clone(),
_ => unreachable!(),
};
for patch in meta[1..].iter() {
json_patch::merge(&mut v2, patch)
}
let mut validator = crate::valid::Validator::new();
validator.validate(&v2)?;
Distribution::from_version(2, v2)
}
}
impl TryFrom<Distribution> for Value {
type Error = Error;
fn try_from(meta: Distribution) -> Result<Self, Self::Error> {
let val = serde_json::to_value(meta)?;
Ok(val)
}
}
impl TryFrom<&String> for Distribution {
type Error = Error;
fn try_from(str: &String) -> Result<Self, Self::Error> {
let meta: Value = serde_json::from_str(str)?;
meta.try_into()
}
}
impl TryFrom<Distribution> for String {
type Error = Error;
fn try_from(meta: Distribution) -> Result<Self, Self::Error> {
let val = serde_json::to_string(&meta)?;
Ok(val)
}
}
#[cfg(test)]
mod tests;