1use std::collections::HashMap;
2
3use anyhow::Context;
4use mcvm_parse::conditions::{ArchCondition, OSCondition};
5use mcvm_shared::addon::AddonKind;
6use mcvm_shared::lang::Language;
7use mcvm_shared::modifications::{ModloaderMatch, PluginLoaderMatch};
8use mcvm_shared::pkg::{PackageAddonOptionalHashes, PackageStability};
9use mcvm_shared::util::DeserListOrSingle;
10use mcvm_shared::versions::VersionPattern;
11use mcvm_shared::Side;
12#[cfg(feature = "schema")]
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16use crate::metadata::PackageMetadata;
17use crate::properties::PackageProperties;
18use crate::RecommendedPackage;
19
20#[derive(Deserialize, Serialize, Debug, Default, Clone)]
22#[cfg_attr(feature = "schema", derive(JsonSchema))]
23#[serde(default)]
24pub struct DeclarativePackage {
25 #[serde(skip_serializing_if = "PackageMetadata::is_empty")]
27 pub meta: PackageMetadata,
28 #[serde(skip_serializing_if = "PackageProperties::is_empty")]
30 pub properties: PackageProperties,
31 #[serde(skip_serializing_if = "HashMap::is_empty")]
33 pub addons: HashMap<String, DeclarativeAddon>,
34 #[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
36 pub relations: DeclarativePackageRelations,
37 #[serde(skip_serializing_if = "Vec::is_empty")]
39 pub conditional_rules: Vec<DeclarativeConditionalRule>,
40}
41
42#[derive(Deserialize, Serialize, Debug, Default, Clone)]
44#[cfg_attr(feature = "schema", derive(JsonSchema))]
45#[serde(default)]
46pub struct DeclarativePackageRelations {
47 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
49 pub dependencies: DeserListOrSingle<String>,
50 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
52 pub explicit_dependencies: DeserListOrSingle<String>,
53 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
55 pub conflicts: DeserListOrSingle<String>,
56 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
58 pub extensions: DeserListOrSingle<String>,
59 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
61 pub bundled: DeserListOrSingle<String>,
62 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
64 pub compats: DeserListOrSingle<(String, String)>,
65 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
67 pub recommendations: DeserListOrSingle<RecommendedPackage>,
68}
69
70impl DeclarativePackageRelations {
71 pub fn merge(&mut self, other: Self) {
73 self.dependencies.merge(other.dependencies);
74 self.explicit_dependencies
75 .merge(other.explicit_dependencies);
76 self.conflicts.merge(other.conflicts);
77 self.extensions.merge(other.extensions);
78 self.bundled.merge(other.bundled);
79 self.compats.merge(other.compats);
80 self.recommendations.merge(other.recommendations);
81 }
82
83 pub fn is_empty(&self) -> bool {
85 self.dependencies.is_empty()
86 && self.explicit_dependencies.is_empty()
87 && self.conflicts.is_empty()
88 && self.extensions.is_empty()
89 && self.bundled.is_empty()
90 && self.compats.is_empty()
91 && self.recommendations.is_empty()
92 }
93}
94
95#[derive(Deserialize, Serialize, Debug, Default, Clone)]
98#[cfg_attr(feature = "schema", derive(JsonSchema))]
99#[serde(default)]
100pub struct DeclarativeConditionSet {
101 #[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
103 pub minecraft_versions: Option<DeserListOrSingle<VersionPattern>>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub side: Option<Side>,
107 #[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
109 pub modloaders: Option<DeserListOrSingle<ModloaderMatch>>,
110 #[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
112 pub plugin_loaders: Option<DeserListOrSingle<PluginLoaderMatch>>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub stability: Option<PackageStability>,
116 #[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
118 pub features: Option<DeserListOrSingle<String>>,
119 #[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
121 pub content_versions: Option<DeserListOrSingle<String>>,
122 #[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
124 pub operating_systems: Option<DeserListOrSingle<OSCondition>>,
125 #[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
127 pub architectures: Option<DeserListOrSingle<ArchCondition>>,
128 #[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
130 pub languages: Option<DeserListOrSingle<Language>>,
131}
132
133#[derive(Deserialize, Serialize, Debug, Default, Clone)]
135#[cfg_attr(feature = "schema", derive(JsonSchema))]
136#[serde(default)]
137pub struct DeclarativeConditionalRule {
138 #[serde(skip_serializing_if = "Vec::is_empty")]
140 pub conditions: Vec<DeclarativeConditionSet>,
141 pub properties: DeclarativeConditionalRuleProperties,
143}
144
145#[derive(Deserialize, Serialize, Debug, Default, Clone)]
147#[cfg_attr(feature = "schema", derive(JsonSchema))]
148#[serde(default)]
149pub struct DeclarativeConditionalRuleProperties {
150 #[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
152 pub relations: DeclarativePackageRelations,
153 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
155 pub notices: DeserListOrSingle<String>,
156}
157
158#[derive(Deserialize, Serialize, Debug, Clone)]
160#[cfg_attr(feature = "schema", derive(JsonSchema))]
161pub struct DeclarativeAddon {
162 pub kind: AddonKind,
164 #[serde(default)]
166 #[serde(skip_serializing_if = "Vec::is_empty")]
167 pub versions: Vec<DeclarativeAddonVersion>,
168 #[serde(default)]
170 #[serde(skip_serializing_if = "Vec::is_empty")]
171 pub conditions: Vec<DeclarativeConditionSet>,
172 #[serde(default)]
175 #[serde(skip_serializing_if = "is_false")]
176 pub optional: bool,
177}
178
179fn is_false(v: &bool) -> bool {
180 !v
181}
182
183#[derive(Deserialize, Serialize, Debug, Clone, Default)]
185#[cfg_attr(feature = "schema", derive(JsonSchema))]
186#[serde(default)]
187pub struct DeclarativeAddonVersion {
188 #[serde(flatten)]
190 pub conditional_properties: DeclarativeConditionSet,
191 #[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
193 pub relations: DeclarativePackageRelations,
194 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
196 pub notices: DeserListOrSingle<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub filename: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub path: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub url: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub version: Option<String>,
209 #[serde(skip_serializing_if = "PackageAddonOptionalHashes::is_empty")]
211 pub hashes: PackageAddonOptionalHashes,
212}
213
214#[derive(Deserialize, Serialize, Debug, Default, Clone)]
216#[cfg_attr(feature = "schema", derive(JsonSchema))]
217#[serde(default)]
218pub struct DeclarativeAddonVersionPatchProperties {
219 #[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
221 pub relations: DeclarativePackageRelations,
222 pub filename: Option<String>,
224}
225
226pub fn deserialize_declarative_package(text: &str) -> anyhow::Result<DeclarativePackage> {
228 let out = unsafe {
230 let mut text = text.to_string();
231 let text = text.as_bytes_mut();
232 simd_json::from_slice(text)?
233 };
234 Ok(out)
235}
236
237pub fn validate_declarative_package(pkg: &DeclarativePackage) -> anyhow::Result<()> {
239 pkg.meta.check_validity().context("Metadata was invalid")?;
240 pkg.properties
241 .check_validity()
242 .context("Properties were invalid")?;
243
244 Ok(())
245}
246
247impl DeclarativePackage {
248 pub fn improve_generation(&mut self) {
250 if self.meta.issues.is_none() {
252 if let Some(source) = &self.meta.source {
253 if source.contains("://github.com/") {
254 let issues = source.clone();
255 let issues = issues.trim_end_matches('/');
256 self.meta.issues = Some(issues.to_string() + "issues");
257 }
258 }
259 }
260 }
261
262 pub fn optimize(&mut self) {
265 for addon in self.addons.values_mut() {
267 if addon.versions.is_empty() {
268 return;
269 }
270 let mut first = None;
271 let mut all_same = true;
272 for version in &addon.versions {
273 if let Some(first) = first {
274 if &version.relations.dependencies != first {
275 all_same = false;
276 break;
277 }
278 } else {
279 first = Some(&version.relations.dependencies);
280 }
281 }
282
283 if all_same && addon.conditions.is_empty() {
284 self.relations
285 .dependencies
286 .extend(first.expect("Length of versions is > 0").iter().cloned());
287
288 for version in &mut addon.versions {
289 version.relations.dependencies = DeserListOrSingle::List(Vec::new());
290 }
291 }
292 }
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_declarative_package_deser() {
302 let contents = r#"
303 {
304 "meta": {
305 "name": "Test Package",
306 "long_description": "Blah blah blah"
307 },
308 "properties": {
309 "modrinth_id": "2E4b7"
310 },
311 "addons": {
312 "test": {
313 "kind": "mod",
314 "versions": [
315 {
316 "url": "example.com"
317 }
318 ]
319 }
320 },
321 "relations": {
322 "compats": [[ "foo", "bar" ]]
323 }
324 }
325 "#;
326
327 let pkg = deserialize_declarative_package(contents).unwrap();
328
329 assert_eq!(pkg.meta.name, Some("Test Package".into()));
330 }
331}