use std::collections::HashMap;
use anyhow::Context;
use nitro_parse::conditions::{ArchCondition, OSCondition};
use nitro_shared::lang::Language;
use nitro_shared::loaders::LoaderMatch;
use nitro_shared::pkg::{PackageAddonOptionalHashes, PackageKind, PackageStability};
use nitro_shared::util::DeserListOrSingle;
use nitro_shared::versions::VersionPattern;
use nitro_shared::Side;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::metadata::PackageMetadata;
use crate::properties::PackageProperties;
use crate::RecommendedPackage;
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct DeclarativePackage {
#[serde(skip_serializing_if = "PackageMetadata::is_empty")]
pub meta: PackageMetadata,
#[serde(skip_serializing_if = "PackageProperties::is_empty")]
pub properties: PackageProperties,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub addons: HashMap<String, DeclarativeAddon>,
#[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
pub relations: DeclarativePackageRelations,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub conditional_rules: Vec<DeclarativeConditionalRule>,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct DeclarativePackageRelations {
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub dependencies: DeserListOrSingle<String>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub explicit_dependencies: DeserListOrSingle<String>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub conflicts: DeserListOrSingle<String>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub extensions: DeserListOrSingle<String>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub bundled: DeserListOrSingle<String>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub compats: DeserListOrSingle<(String, String)>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub recommendations: DeserListOrSingle<RecommendedPackage>,
}
impl DeclarativePackageRelations {
pub fn merge(&mut self, other: Self) {
self.dependencies.merge(other.dependencies);
self.explicit_dependencies
.merge(other.explicit_dependencies);
self.conflicts.merge(other.conflicts);
self.extensions.merge(other.extensions);
self.bundled.merge(other.bundled);
self.compats.merge(other.compats);
self.recommendations.merge(other.recommendations);
}
pub fn is_empty(&self) -> bool {
self.dependencies.is_empty()
&& self.explicit_dependencies.is_empty()
&& self.conflicts.is_empty()
&& self.extensions.is_empty()
&& self.bundled.is_empty()
&& self.compats.is_empty()
&& self.recommendations.is_empty()
}
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct DeclarativeConditionSet {
#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
pub minecraft_versions: Option<DeserListOrSingle<VersionPattern>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub side: Option<Side>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
pub loaders: Option<DeserListOrSingle<LoaderMatch>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stability: Option<PackageStability>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
pub features: Option<DeserListOrSingle<String>>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
pub content_versions: Option<DeserListOrSingle<String>>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
pub operating_systems: Option<DeserListOrSingle<OSCondition>>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
pub architectures: Option<DeserListOrSingle<ArchCondition>>,
#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
pub languages: Option<DeserListOrSingle<Language>>,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct DeclarativeConditionalRule {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub conditions: Vec<DeclarativeConditionSet>,
pub properties: DeclarativeConditionalRuleProperties,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct DeclarativeConditionalRuleProperties {
#[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
pub relations: DeclarativePackageRelations,
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub notices: DeserListOrSingle<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct DeclarativeAddon {
pub kind: PackageKind,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub versions: Vec<DeclarativeAddonVersion>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub conditions: Vec<DeclarativeConditionSet>,
#[serde(default)]
#[serde(skip_serializing_if = "is_false")]
pub optional: bool,
}
fn is_false(v: &bool) -> bool {
!v
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct DeclarativeAddonVersion {
#[serde(flatten)]
pub conditional_properties: DeclarativeConditionSet,
#[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
pub relations: DeclarativePackageRelations,
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub notices: DeserListOrSingle<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "PackageAddonOptionalHashes::is_empty")]
pub hashes: PackageAddonOptionalHashes,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct DeclarativeAddonVersionPatchProperties {
#[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
pub relations: DeclarativePackageRelations,
pub filename: Option<String>,
}
pub fn deserialize_declarative_package(text: &str) -> anyhow::Result<DeclarativePackage> {
let out = unsafe {
let mut text = text.to_string();
let text = text.as_bytes_mut();
simd_json::from_slice(text)?
};
Ok(out)
}
pub fn validate_declarative_package(pkg: &DeclarativePackage) -> anyhow::Result<()> {
pkg.meta.check_validity().context("Metadata was invalid")?;
pkg.properties
.check_validity()
.context("Properties were invalid")?;
Ok(())
}
impl DeclarativePackage {
pub fn improve_generation(&mut self) {
if self.meta.issues.is_none() {
if let Some(source) = &self.meta.source {
if source.contains("://github.com/") {
let issues = source.clone();
let issues = issues.trim_end_matches('/');
self.meta.issues = Some(issues.to_string() + "issues");
}
}
}
}
pub fn optimize(&mut self) {
for addon in self.addons.values_mut() {
if addon.versions.is_empty() {
return;
}
let mut first = None;
let mut all_same = true;
for version in &addon.versions {
if let Some(first) = first {
if &version.relations.dependencies != first {
all_same = false;
break;
}
} else {
first = Some(&version.relations.dependencies);
}
}
if all_same && addon.conditions.is_empty() && !addon.optional {
self.relations
.dependencies
.extend(first.expect("Length of versions is > 0").iter().cloned());
for version in &mut addon.versions {
version.relations.dependencies = DeserListOrSingle::List(Vec::new());
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_declarative_package_deser() {
let contents = r#"
{
"meta": {
"name": "Test Package",
"long_description": "Blah blah blah"
},
"properties": {
"modrinth_id": "2E4b7"
},
"addons": {
"test": {
"kind": "mod",
"versions": [
{
"url": "example.com"
}
]
}
},
"relations": {
"compats": [[ "foo", "bar" ]]
}
}
"#;
let pkg = deserialize_declarative_package(contents).unwrap();
assert_eq!(pkg.meta.name, Some("Test Package".into()));
}
}