pacenotes_shared/
manifest.rs1#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
3pub struct Manifest {
4 pub packages: Vec<crate::PackageInfo>,
5 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
8 pub schemas: std::collections::BTreeMap<String, crate::GameSchema>,
9}
10
11impl Manifest {
12 pub fn visible_to(mut self, grants: &[String]) -> Self {
14 self.packages
15 .retain(|package| package.is_public() || grants.contains(&package.directory));
16 self
17 }
18
19 pub fn for_game(mut self, game: &str) -> Self {
20 self.packages.retain(|package| package.game == game);
21 self.schemas.retain(|id, _| id == game);
22 self
23 }
24
25 pub fn public_only(self) -> Self {
26 self.visible_to(&[])
27 }
28
29 pub fn get(&self, game: &str, reference: &crate::PackageRef) -> Option<&crate::PackageInfo> {
31 self.packages.iter().find(|package| {
32 package.game == game
33 && package.directory == reference.directory
34 && package.name == reference.name
35 })
36 }
37}
38
39#[cfg(test)]
40mod tests {
41 fn sample() -> super::Manifest {
42 super::Manifest {
43 packages: vec![
44 crate::fixtures::package(crate::PUBLIC_DIR, "FilipekMod_numeric_standard"),
45 crate::fixtures::package("premium-voices", "FilipekMod_premium_pack"),
46 crate::fixtures::package("beta", "FilipekMod_beta_pack"),
47 ],
48 schemas: std::collections::BTreeMap::new(),
49 }
50 }
51
52 fn schema(sha256: &str) -> crate::GameSchema {
53 crate::GameSchema {
54 sha256: sha256.into(),
55 categories: vec![crate::SchemaCategory {
56 id: crate::CATEGORY_CODRIVER.into(),
57 select: crate::SELECT_ONE.into(),
58 install_root: crate::INSTALL_ROOT_GAME.into(),
59 }],
60 }
61 }
62
63 #[test]
64 fn schemas_round_trip_and_follow_the_game_filter() {
65 let mut manifest = sample();
66 manifest
67 .schemas
68 .insert(crate::GAME_RBR.into(), schema("aa"));
69 manifest
70 .schemas
71 .insert(crate::GAME_BEAMNG.into(), schema("bb"));
72
73 let back: super::Manifest =
74 serde_json::from_str(&serde_json::to_string(&manifest).unwrap()).unwrap();
75 assert_eq!(back.schemas, manifest.schemas);
76
77 let rbr = back.for_game(crate::GAME_RBR);
78 assert_eq!(rbr.schemas.len(), 1);
79 assert_eq!(rbr.schemas[crate::GAME_RBR].sha256, "aa");
80 }
81
82 #[test]
83 fn a_manifest_without_schemas_still_parses_and_stays_lean() {
84 let old = r#"{"packages":[]}"#;
85 let manifest: super::Manifest = serde_json::from_str(old).unwrap();
86 assert!(manifest.schemas.is_empty());
87 assert_eq!(serde_json::to_string(&manifest).unwrap(), old);
88 }
89
90 #[test]
91 fn json_round_trip() {
92 let json = serde_json::to_string(&sample()).unwrap();
93 let back: super::Manifest = serde_json::from_str(&json).unwrap();
94 assert_eq!(back.packages.len(), 3);
95 assert_eq!(back.packages[1].directory, "premium-voices");
96 }
97
98 #[test]
99 fn an_older_manifest_still_deserializes() {
100 let old = r#"{"packages":[{"directory":"public","name":"basic",
101 "version":"1.0.0","size":5,"sha256":"ab","updated_at":1,
102 "category":"pacenotes","tree_path":["ignored"]}]}"#;
103 let manifest: super::Manifest = serde_json::from_str(old).unwrap();
104 assert_eq!(manifest.packages[0].game, crate::GAME_RBR);
105 }
106
107 #[test]
108 fn get_finds_by_ref() {
109 let manifest = sample();
110 let reference = crate::PackageRef::parse("beta/FilipekMod_beta_pack").unwrap();
111 assert_eq!(
112 manifest.get(crate::GAME_RBR, &reference).unwrap().name,
113 "FilipekMod_beta_pack"
114 );
115 let missing = crate::PackageRef::parse("beta/nope").unwrap();
116 assert!(manifest.get(crate::GAME_RBR, &missing).is_none());
117 }
118
119 #[test]
120 fn get_does_not_cross_games() {
121 let mut beamng = crate::fixtures::package(crate::PUBLIC_DIR, "language/polish");
122 beamng.game = crate::GAME_BEAMNG.to_string();
123 beamng.version = "beamng-version".into();
124 let mut rbr = crate::fixtures::package(crate::PUBLIC_DIR, "language/polish");
125 rbr.version = "rbr-version".into();
126 let manifest = super::Manifest {
127 packages: vec![beamng, rbr],
128 schemas: std::collections::BTreeMap::new(),
129 };
130
131 let reference = crate::PackageRef::parse("public/language/polish").unwrap();
132 assert_eq!(
133 manifest.get(crate::GAME_RBR, &reference).unwrap().version,
134 "rbr-version"
135 );
136 assert_eq!(
137 manifest
138 .get(crate::GAME_BEAMNG, &reference)
139 .unwrap()
140 .version,
141 "beamng-version"
142 );
143 assert!(manifest.get(crate::GAME_ASSETTOCORSA, &reference).is_none());
144 }
145
146 #[test]
147 fn for_game_filters_one_tab_worth() {
148 let mut manifest = sample();
149 manifest.packages[1].game = crate::GAME_BEAMNG.into();
150 assert_eq!(manifest.clone().for_game(crate::GAME_RBR).packages.len(), 2);
151 assert_eq!(
152 manifest.clone().for_game(crate::GAME_BEAMNG).packages.len(),
153 1
154 );
155 assert!(manifest.for_game("nosuchgame").packages.is_empty());
156 }
157
158 #[test]
159 fn public_only_keeps_public_dir() {
160 let manifest = sample().public_only();
161 assert_eq!(manifest.packages.len(), 1);
162 assert!(manifest.packages[0].is_public());
163 }
164
165 #[test]
166 fn visible_to_unions_grants_with_public() {
167 let manifest = sample().visible_to(&["beta".to_string()]);
168 let directories: Vec<&str> = manifest
169 .packages
170 .iter()
171 .map(|package| package.directory.as_str())
172 .collect();
173 assert_eq!(directories, vec![crate::PUBLIC_DIR, "beta"]);
174 }
175
176 #[test]
177 fn multiple_grants() {
178 let manifest = sample().visible_to(&["beta".to_string(), "premium-voices".to_string()]);
179 assert_eq!(manifest.packages.len(), 3);
180 }
181}