use super::Distribution;
use crate::error::Error;
use email_address::EmailAddress;
use serde_json::{json, Map, Value};
use std::str::FromStr;
pub fn to_v2(v1: &Value) -> Result<Value, Error> {
let mut v2 = v1_to_v2_common(v1);
v2.insert("maintainers".to_string(), v1_to_v2_maintainers(v1)?);
v2.insert("license".to_string(), v1_to_v2_license(v1)?);
v2.insert("contents".to_string(), v1_to_v2_contents(v1)?);
if let Some(val) = v1_to_v2_classifications(v1) {
v2.insert("classifications".to_string(), val);
}
if let Some(val) = v1_to_v2_ignore(v1) {
v2.insert("ignore".to_string(), val);
}
if let Some(val) = v1_to_v2_dependencies(v1) {
v2.insert("dependencies".to_string(), val);
}
if let Some(val) = v1_to_v2_resources(v1) {
v2.insert("resources".to_string(), val);
}
Ok(Value::Object(v2))
}
pub fn from_value(v1: Value) -> Result<Distribution, Error> {
to_v2(&v1)?.try_into()
}
fn v1_to_v2_common(v1: &Value) -> Map<String, Value> {
let mut v2 = Map::new();
for (k1, k2) in [
("name", "name"),
("abstract", "abstract"),
("description", "description"),
("version", "version"),
("generated_by", "producer"),
] {
if let Some(v) = v1.get(k1) {
v2.insert(k2.to_string(), v.clone());
}
}
v1_value_to_v2_custom_props(v1, &mut v2);
let mut spec = Map::new();
spec.insert("version".to_string(), json!("2.0.0"));
spec.insert(
"url".to_string(),
json!("https://rfcs.pgxn.org/0003-meta-spec-v2.html"),
);
if let Some(v1_spec) = v1.get("meta-spec") {
v1_value_to_v2_custom_props(v1_spec, &mut spec);
}
v2.insert("meta-spec".to_string(), Value::Object(spec));
v2
}
fn v1_value_to_v2_custom_props(v1: &Value, v2: &mut Map<String, Value>) {
if let Some(obj) = v1.as_object() {
v1_to_v2_custom_props(obj, v2);
}
}
fn v1_to_v2_custom_props(v1: &Map<String, Value>, v2: &mut Map<String, Value>) {
for (k, v) in v1
.into_iter()
.filter(|(key, _)| key.starts_with("x_") || key.starts_with("X_"))
{
v2.insert(k.to_string(), v.clone());
}
}
fn v1_to_v2_maintainers(v1: &Value) -> Result<Value, Error> {
if let Some(maintainer) = v1.get("maintainer") {
return match maintainer {
Value::Array(list) => parse_v1_maintainers(v1, list),
Value::String(_) => {
let list = vec![maintainer.clone()];
parse_v1_maintainers(v1, &list)
}
_ => Err(Error::Invalid("maintainer", 1, maintainer.clone())),
};
}
Err(Error::Missing("maintainer"))
}
fn parse_v1_maintainers(v1: &Value, list: &[Value]) -> Result<Value, Error> {
let mut new_list: Vec<Value> = Vec::with_capacity(list.len());
for v in list {
if let Some(str) = v.as_str() {
if let Ok(email) = EmailAddress::from_str(str) {
new_list.push(json!({
"name": match email.display_part() {
"" => str,
d => d,
},
"email": email.email(),
}));
} else {
const FALLBACK_URL: &str = "https://pgxn.org";
let url = match v1.get("resources") {
Some(Value::Object(resources)) => match resources.get("homepage") {
Some(Value::String(home)) => home.to_string(),
_ => FALLBACK_URL.to_string(),
},
_ => FALLBACK_URL.to_string(),
};
new_list.push(json!({"name": str, "url": url}));
}
} else {
return Err(Error::Invalid("maintainer", 1, v.clone()));
}
}
Ok(Value::Array(new_list))
}
fn v1_to_v2_license(v1: &Value) -> Result<Value, Error> {
if let Some(license) = v1.get("license") {
return match license {
Value::String(l) => {
if let Some(name) = license_expression_for(l.as_str()) {
return Ok(Value::String(name.to_string()));
}
Err(Error::Invalid("license", 1, license.clone()))
}
Value::Array(list) => {
let mut v = Vec::with_capacity(list.len());
for ln in list {
match ln {
Value::String(s) => {
match license_expression_for(s.as_str()) {
Some(name) => v.push(name.to_string()),
None => return Err(Error::Invalid("license", 1, ln.clone())),
};
}
_ => return Err(Error::Invalid("license", 1, ln.clone())),
};
}
return Ok(Value::String(v.join(" OR ")));
}
Value::Object(obj) => {
let mut list = Vec::with_capacity(obj.len());
for (k, v) in obj.iter() {
match (k.as_str(), v.as_str()) {
("PostgreSQL", _) => list.push(k.to_string()),
("Apache", _) => list.push("Apache-2.0".to_string()),
("ISC", _) => list.push(k.to_string()),
("mit", _) => list.push("MIT".to_string()),
("mozilla_2_0", _) => list.push("MPL-2.0".to_string()),
("gpl_3", _) => list.push("GPL-3.0-only".to_string()),
("BSD", _) => list.push("BSD-2-Clause".to_string()),
("BSD 2 Clause", _) => list.push("BSD-2-Clause".to_string()),
(
"restricted",
Some("https://github.com/diffix/pg_diffix/blob/master/LICENSE.md"),
) => list.push("BUSL-1.1".to_string()),
_ => return Err(Error::Invalid("license", 1, v.clone())),
}
}
return Ok(Value::String(list.join(" OR ")));
}
_ => Err(Error::Invalid("license", 1, license.clone())),
};
}
Err(Error::Missing("license"))
}
fn license_expression_for(name: &str) -> Option<&str> {
match name {
"agpl_3" => Some("AGPL-3.0"),
"apache_1_1" => Some("Apache-1.1"),
"apache_2_0" => Some("Apache-2.0"),
"artistic_1" => Some("Artistic-1.0"),
"artistic_2" => Some("Artistic-2.0"),
"bsd" => Some("BSD-3-Clause"),
"freebsd" => Some("BSD-2-Clause-FreeBSD"),
"gfdl_1_2" => Some("GFDL-1.2-or-later"),
"gfdl_1_3" => Some("GFDL-1.3-or-later"),
"gpl_1" => Some("GPL-1.0-only"),
"gpl_2" => Some("GPL-2.0-only"),
"gpl_3" => Some("GPL-3.0-only"),
"lgpl_2_1" => Some("LGPL-2.1"),
"lgpl_3_0" => Some("LGPL-3.0"),
"mit" => Some("MIT"),
"mozilla_1_0" => Some("MPL-1.0"),
"mozilla_1_1" => Some("MPL-1.1"),
"openssl" => Some("OpenSSL"),
"perl_5" => Some("Artistic-1.0-Perl OR GPL-1.0-or-later"),
"postgresql" => Some("PostgreSQL"),
"qpl_1_0" => Some("QPL-1.0"),
"sun" => Some("SISSL"),
"zlib" => Some("Zlib"),
_ => None,
}
}
fn v1_to_v2_contents(v1: &Value) -> Result<Value, Error> {
if let Some(provides) = v1.get("provides") {
let mut extensions = Map::new();
if let Value::Object(obj) = provides {
for (ext, spec) in obj {
match spec {
Value::Object(obj) => {
let mut v2_spec = Map::new();
v2_spec.insert(
"control".to_string(),
Value::String(ext.to_string() + ".control"),
);
if obj.contains_key("file") {
v2_spec.insert("sql".to_string(), obj["file"].clone());
} else {
v2_spec.insert("sql".to_string(), Value::String("UNKNOWN".to_string()));
}
for (v2, v1) in [("doc", "docfile"), ("abstract", "abstract")] {
if obj.contains_key(v1) {
v2_spec.insert(v2.to_string(), obj[v1].clone());
}
}
v1_to_v2_custom_props(obj, &mut v2_spec);
extensions.insert(ext.to_string(), Value::Object(v2_spec));
}
_ => return Err(Error::Invalid("extension", 1, spec.clone())),
}
}
} else {
return Err(Error::Invalid("provides", 1, provides.clone()));
}
return Ok(json!({"extensions": extensions}));
}
Err(Error::Missing("provides"))
}
fn v1_to_v2_classifications(v1: &Value) -> Option<Value> {
v1.get("tags").map(|tags| json!({"tags": tags.clone()}))
}
fn v1_to_v2_ignore(v1: &Value) -> Option<Value> {
match v1.get("no_index") {
Some(Value::Object(ni)) => {
let mut ignore: Vec<Value> = Vec::new();
for k in ["file", "directory"] {
if let Some(Value::Array(v)) = ni.get(k) {
for path in v {
if !ignore.contains(path) {
ignore.push(path.clone())
}
}
}
}
if ignore.is_empty() {
None
} else {
Some(Value::Array(ignore))
}
}
_ => None,
}
}
fn v1_to_v2_dependencies(v1: &Value) -> Option<Value> {
use semver::Version;
match v1.get("prereqs") {
Some(Value::Object(prereqs)) => {
let max_version = Version::parse("9999.0.0").unwrap();
let mut pg_version = max_version.clone();
let mut dependencies = Map::new();
let mut packages = Map::new();
for (phase1, phase2) in [
("develop", "develop"),
("configure", "configure"),
("build", "build"),
("test", "test"),
("runtime", "run"),
] {
if let Some(Value::Object(relation)) = prereqs.get(phase1) {
let mut phase = Map::new();
for rel_name in ["requires", "recommends", "suggests", "conflicts"] {
if let Some(Value::Object(spec)) = relation.get(rel_name) {
let mut deps = Map::new();
for (name, version) in spec {
let ext = name.to_lowercase();
if ext == "postgresql" {
if let Value::String(version) = version {
if let Ok(pgv) = Version::parse(version) {
if pgv < pg_version {
pg_version = pgv;
}
}
}
} else {
deps.insert(
format!("pkg:{}/{ext}", source_for(&ext)),
version.clone(),
);
}
}
if !deps.is_empty() {
phase.insert(rel_name.to_string(), Value::Object(deps));
}
}
}
v1_to_v2_custom_props(relation, &mut phase);
if !phase.is_empty() {
packages.insert(phase2.to_string(), Value::Object(phase));
}
}
}
if pg_version < max_version {
dependencies.insert("postgres".to_string(), json!({"version": pg_version}));
}
v1_to_v2_custom_props(prereqs, &mut packages);
if !packages.is_empty() {
dependencies.insert("packages".to_string(), Value::Object(packages));
return Some(Value::Object(dependencies));
}
match dependencies.is_empty() {
false => Some(Value::Object(dependencies)),
true => None,
}
}
_ => None,
}
}
fn source_for(ext: &str) -> String {
match ext {
"adminpack"
| "amcheck"
| "auth_delay"
| "auto_explain"
| "basebackup_to_shell"
| "basic_archive"
| "bloom"
| "bool_plperl"
| "btree_gin"
| "btree_gist"
| "chkpass"
| "citext"
| "cube"
| "dblink"
| "dict_int"
| "dict_xsyn"
| "earthdistance"
| "file_fdw"
| "fuzzystrmatch"
| "hstore"
| "hstore_plperl"
| "hstore_plpython"
| "intagg"
| "intarray"
| "isn"
| "jsonb_plperl"
| "jsonb_plpython"
| "lo"
| "ltree"
| "ltree_plpython"
| "oid2name"
| "old_snapshot"
| "pageinspect"
| "passwordcheck"
| "pg_buffercache"
| "pg_freespacemap"
| "pg_prewarm"
| "pg_standby"
| "pg_stat_statements"
| "pg_surgery"
| "pg_trgm"
| "pg_visibility"
| "pg_walinspect"
| "pgcrypto"
| "pgrowlocks"
| "pgstattuple"
| "plperl"
| "plperlu"
| "plpgsql"
| "plpython"
| "plpythonu"
| "plpython2u"
| "plpython3u"
| "pltcl"
| "pltclu"
| "postgres_fdw"
| "seg"
| "sepgsql"
| "spi"
| "sslinfo"
| "start-scripts"
| "tablefunc"
| "tcn"
| "test_decoding"
| "tsearch2"
| "tsm_system_rows"
| "tsm_system_time"
| "unaccent"
| "uuid-ossp"
| "vacuumlo"
| "xml2" => "postgres".to_string(),
_ => "pgxn".to_string(),
}
}
fn v1_to_v2_resources(v1: &Value) -> Option<Value> {
match v1.get("resources") {
Some(Value::Object(resources)) => {
let mut ret = Map::new();
if let Some(Value::String(home)) = resources.get("homepage") {
ret.insert("homepage".to_string(), json!(home));
}
if let Some(Value::Object(bug)) = resources.get("bugtracker") {
if let Some(Value::String(web)) = bug.get("web") {
ret.insert("issues".to_string(), json!(web));
} else if let Some(Value::String(mail)) = bug.get("mailto") {
ret.insert("issues".to_string(), json!(format!("mailto:{mail}")));
}
}
if let Some(Value::Object(repo)) = resources.get("repository") {
if let Some(Value::String(web)) = repo.get("web") {
ret.insert("repository".to_string(), json!(web));
} else if let Some(Value::String(url)) = repo.get("url") {
ret.insert("repository".to_string(), json!(url));
}
}
v1_to_v2_custom_props(resources, &mut ret);
if ret.is_empty() {
None
} else {
Some(Value::Object(ret))
}
}
_ => None,
}
}
#[cfg(test)]
mod tests;