mcvm_pkg/
declarative.rs

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/// Structure for a declarative / JSON package
21#[derive(Deserialize, Serialize, Debug, Default, Clone)]
22#[cfg_attr(feature = "schema", derive(JsonSchema))]
23#[serde(default)]
24pub struct DeclarativePackage {
25	/// Metadata for the package
26	#[serde(skip_serializing_if = "PackageMetadata::is_empty")]
27	pub meta: PackageMetadata,
28	/// Properties for the package
29	#[serde(skip_serializing_if = "PackageProperties::is_empty")]
30	pub properties: PackageProperties,
31	/// Addons that the package installs
32	#[serde(skip_serializing_if = "HashMap::is_empty")]
33	pub addons: HashMap<String, DeclarativeAddon>,
34	/// Relationships with other packages
35	#[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
36	pub relations: DeclarativePackageRelations,
37	/// Changes to conditionally apply to the package
38	#[serde(skip_serializing_if = "Vec::is_empty")]
39	pub conditional_rules: Vec<DeclarativeConditionalRule>,
40}
41
42/// Package relationships for declarative packages
43#[derive(Deserialize, Serialize, Debug, Default, Clone)]
44#[cfg_attr(feature = "schema", derive(JsonSchema))]
45#[serde(default)]
46pub struct DeclarativePackageRelations {
47	/// Package dependencies
48	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
49	pub dependencies: DeserListOrSingle<String>,
50	/// Explicit dependencies
51	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
52	pub explicit_dependencies: DeserListOrSingle<String>,
53	/// Package conflicts
54	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
55	pub conflicts: DeserListOrSingle<String>,
56	/// Package extensions
57	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
58	pub extensions: DeserListOrSingle<String>,
59	/// Bundled packages
60	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
61	pub bundled: DeserListOrSingle<String>,
62	/// Package compats
63	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
64	pub compats: DeserListOrSingle<(String, String)>,
65	/// Package recommendations
66	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
67	pub recommendations: DeserListOrSingle<RecommendedPackage>,
68}
69
70impl DeclarativePackageRelations {
71	/// Merges this struct and another struct's relations
72	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	/// Checks if the relations are empty
84	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/// Properties that are used for choosing the best addon version
96/// from a declarative package and conditional rules
97#[derive(Deserialize, Serialize, Debug, Default, Clone)]
98#[cfg_attr(feature = "schema", derive(JsonSchema))]
99#[serde(default)]
100pub struct DeclarativeConditionSet {
101	/// Minecraft versions to allow
102	#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
103	pub minecraft_versions: Option<DeserListOrSingle<VersionPattern>>,
104	/// What side to allow
105	#[serde(skip_serializing_if = "Option::is_none")]
106	pub side: Option<Side>,
107	/// What modloaders to allow
108	#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
109	pub modloaders: Option<DeserListOrSingle<ModloaderMatch>>,
110	/// What plugin loaders to allow
111	#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
112	pub plugin_loaders: Option<DeserListOrSingle<PluginLoaderMatch>>,
113	/// What stability setting to allow
114	#[serde(skip_serializing_if = "Option::is_none")]
115	pub stability: Option<PackageStability>,
116	/// What features to allow
117	#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
118	pub features: Option<DeserListOrSingle<String>>,
119	/// What content versions to allow
120	#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
121	pub content_versions: Option<DeserListOrSingle<String>>,
122	/// What operating systems to allow
123	#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
124	pub operating_systems: Option<DeserListOrSingle<OSCondition>>,
125	/// What system architectures to allow
126	#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
127	pub architectures: Option<DeserListOrSingle<ArchCondition>>,
128	/// What languages to allow
129	#[serde(skip_serializing_if = "DeserListOrSingle::is_option_empty")]
130	pub languages: Option<DeserListOrSingle<Language>>,
131}
132
133/// Conditional rule to apply changes to a declarative package
134#[derive(Deserialize, Serialize, Debug, Default, Clone)]
135#[cfg_attr(feature = "schema", derive(JsonSchema))]
136#[serde(default)]
137pub struct DeclarativeConditionalRule {
138	/// Conditions for this rule
139	#[serde(skip_serializing_if = "Vec::is_empty")]
140	pub conditions: Vec<DeclarativeConditionSet>,
141	/// Properties to apply if this rule succeeds
142	pub properties: DeclarativeConditionalRuleProperties,
143}
144
145/// Properties that can be applied conditionally
146#[derive(Deserialize, Serialize, Debug, Default, Clone)]
147#[cfg_attr(feature = "schema", derive(JsonSchema))]
148#[serde(default)]
149pub struct DeclarativeConditionalRuleProperties {
150	/// Relations to append
151	#[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
152	pub relations: DeclarativePackageRelations,
153	/// Notices to raise
154	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
155	pub notices: DeserListOrSingle<String>,
156}
157
158/// Addon in a declarative package
159#[derive(Deserialize, Serialize, Debug, Clone)]
160#[cfg_attr(feature = "schema", derive(JsonSchema))]
161pub struct DeclarativeAddon {
162	/// What kind of addon this is
163	pub kind: AddonKind,
164	/// The available versions of this addon
165	#[serde(default)]
166	#[serde(skip_serializing_if = "Vec::is_empty")]
167	pub versions: Vec<DeclarativeAddonVersion>,
168	/// Conditions for this addon to be considered
169	#[serde(default)]
170	#[serde(skip_serializing_if = "Vec::is_empty")]
171	pub conditions: Vec<DeclarativeConditionSet>,
172	/// Whether this addon should be considered optional and not throw an error if it
173	/// does not match any versions
174	#[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/// Version for an addon in a declarative package
184#[derive(Deserialize, Serialize, Debug, Clone, Default)]
185#[cfg_attr(feature = "schema", derive(JsonSchema))]
186#[serde(default)]
187pub struct DeclarativeAddonVersion {
188	/// Conditional properties for this version
189	#[serde(flatten)]
190	pub conditional_properties: DeclarativeConditionSet,
191	/// Additional relations that this version imposes
192	#[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
193	pub relations: DeclarativePackageRelations,
194	/// Notices that this version raises
195	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
196	pub notices: DeserListOrSingle<String>,
197	/// Filename for the addon file
198	#[serde(skip_serializing_if = "Option::is_none")]
199	pub filename: Option<String>,
200	/// Path to the version file
201	#[serde(skip_serializing_if = "Option::is_none")]
202	pub path: Option<String>,
203	/// URL to the version file
204	#[serde(skip_serializing_if = "Option::is_none")]
205	pub url: Option<String>,
206	/// Version identifier for this version
207	#[serde(skip_serializing_if = "Option::is_none")]
208	pub version: Option<String>,
209	/// Hashes for this version file
210	#[serde(skip_serializing_if = "PackageAddonOptionalHashes::is_empty")]
211	pub hashes: PackageAddonOptionalHashes,
212}
213
214/// Properties for declarative addon versions that can be changed with patches
215#[derive(Deserialize, Serialize, Debug, Default, Clone)]
216#[cfg_attr(feature = "schema", derive(JsonSchema))]
217#[serde(default)]
218pub struct DeclarativeAddonVersionPatchProperties {
219	/// Relations to append
220	#[serde(skip_serializing_if = "DeclarativePackageRelations::is_empty")]
221	pub relations: DeclarativePackageRelations,
222	/// A filename to change
223	pub filename: Option<String>,
224}
225
226/// Deserialize a declarative package
227pub fn deserialize_declarative_package(text: &str) -> anyhow::Result<DeclarativePackage> {
228	// SAFETY: The modified, possibly invalid string is a copy that is never used again
229	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
237/// Validate a declarative package
238pub 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	/// Improve a generated package by inferring certain fields
249	pub fn improve_generation(&mut self) {
250		// Infer issues link from a GitHub source link
251		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	/// Optimize the package by removing redundancies. This might break some packages
263	/// so it is recommended to use it only on simple generated ones
264	pub fn optimize(&mut self) {
265		// Move common relations in every version of an addon to the package scope
266		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}