facti_lib/modinfo.rs
1use std::{
2 fmt::{self, Display, Formatter},
3 path::PathBuf,
4};
5
6use serde::{Deserialize, Serialize};
7use url::Url;
8
9use super::{dependency::Dependency, version::Version, FactorioVersion};
10
11/// The info.json file identifies the mod and defines its version.
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct ModInfo {
14 /// The internal name of mod.
15 ///
16 /// The game accepts anything as a mod name, however the mod portal
17 /// restricts mod names to only consist of alphanumeric characters,
18 /// dashes and underscores. Note that the mod folder or mod zip file
19 /// name has to contain the mod name, where the restrictions of the
20 /// file system apply.
21 ///
22 /// The game accepts mod names with a maximum length of 100 characters.
23 /// The mod portal only accepts mods with names that are longer than
24 /// 3 characters and shorter than 50 characters.
25 pub name: String,
26
27 /// Defines the version of the mod.
28 pub version: Version,
29
30 /// The display name of the mod, so it is not recommended to use
31 /// someUgly_pRoGrAmMeR-name here.
32 ///
33 /// Can be overwritten with a locale entry in the `mod-name` category,
34 /// using the internal mod name as the key.
35 ///
36 /// The game will reject a title field that is longer than 100 characters.
37 /// However, this can be worked around by using the locale entry.
38 /// The mod portal does not restrict mod title length.
39 pub title: String,
40
41 /// The author of the mod.
42 ///
43 /// This field does not have restrictions, it can also be a list of
44 /// authors etc. The mod portal ignores this field, it will simply display
45 /// the uploader's name as the author.
46 pub author: String,
47
48 /// How the mod author can be contacted, for example an email address.
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub contact: Option<String>,
51
52 /// Where the mod can be found on the internet.
53 ///
54 /// Note that the in-game mod browser shows the mod portal link additionally
55 /// to this field. Please don't put the string `"None"` here,
56 /// it makes the field on the mod portal website look ugly.
57 /// Just leave the field empty if the mod doesn't have
58 /// a website/forum thread/discord.
59 ///
60 /// **Note:** The [`None`] variant is perfectly valid to use.
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub homepage: Option<Url>,
63
64 /// A short description of what your mod does.
65 ///
66 /// This is all that people get to see in-game.
67 /// Can be overwritten with a locale entry in the `mod-description` category,
68 /// using the internal mod name as the key.
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub description: Option<String>,
71
72 /// The Factorio version that this mod supports.
73 ///
74 /// This can only be one Factorio version, not multiple.
75 /// However, it includes all `.sub` (`.patch`) versions.
76 /// While the field is optional, usually mods are developed for versions
77 /// higher than the default 0.12, so the field has to be added anyway.
78 ///
79 /// Adding a sub (patch) part, e.g. "0.18.27" will make the mod portal
80 /// reject the mod and the game act weirdly.
81 /// That means this shouldn't be done;
82 /// use only the major and minor components "major.minor", for example "1.0".
83 ///
84 /// Mods with the factorio_version "0.18" can also be loaded in 1.0
85 /// and the mod portal will return them
86 /// when queried for factorio_version 1.0 mods.
87 #[serde(default)]
88 pub factorio_version: FactorioVersion,
89
90 /// Mods that this mod depends on or is incompatible with.
91 ///
92 /// If this mod depends on another, the other mod will load first,
93 /// see [Data-Lifecycle][].
94 /// An empty [`Vec`] allows to work around the default
95 /// and have no dependencies at all.
96 ///
97 /// [data-lifecycle]: https://lua-api.factorio.com/latest/Data-Lifecycle.html
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub dependencies: Vec<Dependency>,
100
101 /// Unofficial extensions to the `info.json` format.
102 ///
103 /// Used by third party packaging tools (such as [Facti][]).
104 ///
105 /// [facti]: https://facti.rs
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub package: Option<ModPackageInfo>,
108}
109
110/// Contains unofficial extensions to the `info.json` format.
111///
112/// Used by third party packaging tools.
113#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
114pub struct ModPackageInfo {
115 /// The path to a file that should be used as the content for
116 /// the "information" page on the mod portal.
117 ///
118 /// The mod portal supports markdown formatting, so it's recommended
119 /// to specify the path to a markdown file here.
120 ///
121 /// Note that this path is considered relative to the `info.json` file
122 /// (unless an absolute path is specified).
123 #[serde(rename = "information", skip_serializing_if = "Option::is_none")]
124 pub readme_path: Option<PathBuf>,
125
126 /// The path to a file that should be used as the content for
127 /// the "FAQ" page on the mod portal.
128 ///
129 /// The mod portal supports markdown formatting, so it's recommended
130 /// to specify the path to a markdown file here.
131 ///
132 /// Note that this path is considered relative to the `info.json` file
133 /// (unless an absolute path is specified).
134 #[serde(rename = "faq", skip_serializing_if = "Option::is_none")]
135 pub faq_path: Option<PathBuf>,
136
137 /// Paths to images that should be displayed on the mod portal.
138 ///
139 /// If mod details are updated with Facti, the images will be displayed
140 /// on the mod portal in the same order that they are specified in this
141 /// [`Vec`].
142 ///
143 /// Note that these paths are considered relative to the `info.json` file
144 /// (unless absolute paths are specified).
145 #[serde(default, rename = "gallery", skip_serializing_if = "Vec::is_empty")]
146 pub gallery_paths: Vec<PathBuf>,
147}
148
149impl ModInfo {
150 /// Creates a builder to more conveniently construct a [`ModInfo`] struct.
151 pub fn builder<T, U, V>(name: T, version: Version, title: U, author: V) -> ModInfoBuilder
152 where
153 T: Into<String>,
154 U: Into<String>,
155 V: Into<String>,
156 {
157 ModInfoBuilder::new(name, version, title, author)
158 }
159}
160
161impl Display for ModInfo {
162 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
163 write!(f, "{} v{} by {}", self.name, self.version, self.author)
164 }
165}
166
167/// Contains an internal [`ModInfo`] value that is used to later construct a
168/// finished [`ModInfo`]`.
169pub struct ModInfoBuilder {
170 info: ModInfo,
171}
172
173/// Contains methods to successively build up a [`ModInfo`] struct.
174impl ModInfoBuilder {
175 fn new<T, V, U, X>(name: T, version: V, title: U, author: X) -> Self
176 where
177 T: Into<String>,
178 V: Into<Version>,
179 U: Into<String>,
180 X: Into<String>,
181 {
182 Self {
183 info: ModInfo {
184 name: name.into(),
185 version: version.into(),
186 title: title.into(),
187 author: author.into(),
188 contact: None,
189 homepage: None,
190 description: None,
191 factorio_version: Default::default(),
192 dependencies: Vec::new(),
193 package: None,
194 },
195 }
196 }
197
198 /// Sets the [`contact`][ModInfo::contact] field.
199 pub fn contact<T: Into<String>>(&mut self, contact: T) -> &mut Self {
200 self.info.contact = Some(contact.into());
201 self
202 }
203
204 /// Sets the [`homepage`][ModInfo::homepage] field.
205 pub fn homepage(&mut self, homepage: Url) -> &mut Self {
206 self.info.homepage = Some(homepage);
207 self
208 }
209
210 /// Sets the [`description`][ModInfo::description] field.
211 pub fn description<T: Into<String>>(&mut self, description: T) -> &mut Self {
212 self.info.description = Some(description.into());
213 self
214 }
215
216 /// Sets the [`factorio_version`][ModInfo::factorio_version] field.
217 pub fn factorio_version(&mut self, factorio_version: FactorioVersion) -> &mut Self {
218 self.info.factorio_version = factorio_version;
219 self
220 }
221
222 /// Adds a dependency to [`dependencies`][ModInfo::dependencies].
223 pub fn dependency(&mut self, dependency: Dependency) -> &mut Self {
224 self.info.dependencies.push(dependency);
225 self
226 }
227
228 /// Adds multiple dependencies at once to [`dependencies`][ModInfo::dependencies].
229 pub fn dependencies(&mut self, dependencies: &[Dependency]) -> &mut Self {
230 self.info.dependencies.extend_from_slice(dependencies);
231 self
232 }
233
234 /// Sets a path to the readme file that should be displayed
235 /// on the mod portal.
236 ///
237 /// The path is relative to where the `info.json` file is located.
238 ///
239 /// **Note:** This is an unofficial extension to the `info.json` format.
240 pub fn information_path<T: Into<PathBuf>>(&mut self, path: T) -> &mut Self {
241 self.info
242 .package
243 .get_or_insert_with(Default::default)
244 .readme_path = Some(path.into());
245 self
246 }
247
248 /// Sets a path to the FAQ file that should be displayed
249 /// on the mod portal.
250 ///
251 /// The path is relative to where the `info.json` file is located.
252 ///
253 ///
254 pub fn faq_path<T: Into<PathBuf>>(&mut self, path: T) -> &mut Self {
255 self.info
256 .package
257 .get_or_insert_with(Default::default)
258 .faq_path = Some(path.into());
259 self
260 }
261
262 /// Adds a path to the collection of gallery images.
263 pub fn gallery<T: Into<PathBuf>>(&mut self, gallery: T) -> &mut Self {
264 self.info
265 .package
266 .get_or_insert_with(Default::default)
267 .gallery_paths
268 .push(gallery.into());
269 self
270 }
271
272 /// Builds a finished [`ModInfo`] from the builder.
273 pub fn build(&mut self) -> ModInfo {
274 self.info.clone()
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use crate::{modinfo::ModPackageInfo, version::VersionReq};
281
282 use super::*;
283
284 #[test]
285 fn test_builder() {
286 let expected = ModInfo {
287 name: "boblibrary".to_string(),
288 version: Version::parse("0.17.0").unwrap(),
289 title: "Bob's Library".to_string(),
290 author: "Bob".to_string(),
291 contact: None,
292 homepage: None,
293 description: None,
294 factorio_version: Default::default(),
295 dependencies: vec![Dependency::required(
296 "angel".to_string(),
297 VersionReq::Latest,
298 )],
299 package: Some(ModPackageInfo {
300 readme_path: Some(PathBuf::from("README.md")),
301 ..Default::default()
302 }),
303 };
304
305 let mut builder = ModInfoBuilder::new(
306 "boblibrary".to_string(),
307 Version::parse("0.17.0").unwrap(),
308 "Bob's Library".to_string(),
309 "Bob".to_string(),
310 );
311 builder.dependency(Dependency::required(
312 "angel".to_string(),
313 VersionReq::Latest,
314 ));
315 builder.information_path("README.md");
316 let built = builder.build();
317
318 assert_eq!(built, expected);
319 }
320}