Skip to main content

pacenotes_shared/
manifest.rs

1/// The body of `GET /packages`.
2#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
3pub struct Manifest {
4    pub packages: Vec<crate::PackageInfo>,
5    /// By game id, one for every game published, since a game without a
6    /// schema publishes nothing; absent from manifests written before 2026-10-25.
7    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
8    pub schemas: std::collections::BTreeMap<String, crate::GameSchema>,
9}
10
11impl Manifest {
12    /// Keeps `public` plus the directories named in `grants`.
13    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    /// Within one game, since every game may publish the same `directory/name`.
30    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}