1use crate::error::MrBundleError;
2use crate::manifest::ResourceIdentifier;
3use crate::{error::MrBundleResult, manifest::Manifest};
4use resource::ResourceBytes;
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6use std::collections::{BTreeMap, HashMap, HashSet};
7use std::fmt::Debug;
8use std::io::Read;
9
10pub mod resource;
11
12pub type ResourceMap = BTreeMap<ResourceIdentifier, ResourceBytes>;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Bundle<M>
21where
22 M: Debug + Serialize + DeserializeOwned,
23{
24 #[serde(bound(deserialize = "M: DeserializeOwned"))]
26 manifest: M,
27
28 resources: ResourceMap,
32}
33
34impl<M> Bundle<M>
35where
36 M: Debug + Serialize + DeserializeOwned,
37{
38 pub fn manifest(&self) -> &M {
40 &self.manifest
41 }
42
43 pub fn get_all_resources(&self) -> HashMap<&ResourceIdentifier, &ResourceBytes> {
45 self.resources.iter().collect()
46 }
47
48 pub fn get_resource(&self, resource_identifier: &ResourceIdentifier) -> Option<&ResourceBytes> {
50 self.resources.get(resource_identifier)
51 }
52
53 pub fn pack(&self) -> MrBundleResult<bytes::Bytes> {
57 crate::pack(self)
58 }
59
60 pub fn unpack(source: impl Read) -> MrBundleResult<Self> {
64 crate::unpack(source)
65 }
66}
67
68impl<M> Bundle<M>
69where
70 M: Manifest,
71{
72 pub fn new(
82 manifest: M,
83 resources: impl IntoIterator<Item = (ResourceIdentifier, ResourceBytes)>,
84 ) -> MrBundleResult<Self> {
85 Self::from_parts(manifest, resources)
86 }
87
88 fn from_parts(
89 mut manifest: M,
90 resources: impl IntoIterator<Item = (ResourceIdentifier, ResourceBytes)>,
91 ) -> MrBundleResult<Self> {
92 let resources = resources.into_iter().collect::<ResourceMap>();
93 let manifest_resource_ids: HashSet<_> =
94 manifest.generate_resource_ids().keys().cloned().collect();
95
96 let missing_resources = manifest_resource_ids
97 .difference(&resources.keys().cloned().collect())
98 .cloned()
99 .collect::<Vec<_>>();
100 if !missing_resources.is_empty() {
101 return Err(MrBundleError::MissingResources(missing_resources));
102 }
103
104 let extra_resources = resources
105 .keys()
106 .cloned()
107 .collect::<HashSet<_>>()
108 .difference(&manifest_resource_ids)
109 .cloned()
110 .collect::<Vec<_>>();
111
112 if !extra_resources.is_empty() {
113 return Err(MrBundleError::UnusedResources(extra_resources));
114 }
115
116 Ok(Self {
117 manifest,
118 resources,
119 })
120 }
121
122 pub fn update_manifest(self, manifest: M) -> MrBundleResult<Self> {
125 Self::from_parts(manifest, self.resources)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::error::MrBundleError;
133 use bytes::Buf;
134
135 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
136 struct TestManifest(Vec<ResourceIdentifier>);
137
138 impl Manifest for TestManifest {
139 fn generate_resource_ids(&mut self) -> HashMap<ResourceIdentifier, String> {
140 self.resource_ids()
141 .iter()
142 .map(|r| (r.clone(), r.clone()))
143 .collect()
144 }
145
146 fn resource_ids(&self) -> Vec<ResourceIdentifier> {
147 self.0.clone()
148 }
149
150 #[cfg(feature = "fs")]
151 #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
152 fn file_name() -> &'static str {
153 unimplemented!()
154 }
155
156 #[cfg(feature = "fs")]
157 #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
158 fn bundle_extension() -> &'static str {
159 unimplemented!()
160 }
161 }
162
163 #[test]
164 fn bundle_validation() {
165 let manifest = TestManifest(vec!["1.thing".into(), "2.thing".into()]);
166
167 Bundle::new(
168 manifest.clone(),
169 vec![
170 ("1.thing".into(), vec![1].into()),
171 ("2.thing".into(), vec![2].into()),
172 ],
173 )
174 .unwrap();
175
176 let err =
177 Bundle::new(manifest.clone(), vec![("1.thing".into(), vec![1].into())]).unwrap_err();
178 assert!(
179 matches!(err, MrBundleError::MissingResources(ref resources) if resources.contains(&"2.thing".into())),
180 "Got other error: {err:?}"
181 );
182
183 let err = Bundle::new(
184 manifest,
185 vec![
186 ("1.thing".into(), vec![1].into()),
187 ("2.thing".into(), vec![2].into()),
188 ("3.thing".into(), vec![3].into()),
189 ],
190 )
191 .unwrap_err();
192 assert!(
193 matches!(
194 err,
195 MrBundleError::UnusedResources(ref resources) if resources.contains(&"3.thing".into())
196 ),
197 "Got other error: {err:?}"
198 );
199 }
200
201 #[test]
202 fn round_trip_pack_unpack() {
203 let manifest = TestManifest(vec!["1.thing".into(), "2.thing".into()]);
204
205 let bundle = Bundle::new(
206 manifest.clone(),
207 vec![
208 ("1.thing".into(), vec![1].into()),
209 ("2.thing".into(), vec![2].into()),
210 ],
211 )
212 .unwrap();
213
214 let packed = bundle.pack().unwrap();
215 let unpacked = Bundle::unpack(packed.reader()).unwrap();
216
217 assert_eq!(bundle, unpacked);
218 }
219
220 #[test]
221 fn consistent_id_generation_in_mem() {
222 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223 struct TestManifest(String);
224
225 impl Manifest for TestManifest {
226 fn generate_resource_ids(&mut self) -> HashMap<ResourceIdentifier, String> {
227 let id = self.0.split(".").last().unwrap().to_string();
228 let original = self.0.clone();
229
230 self.0 = id.clone();
231
232 HashMap::from([(id, original)])
233 }
234
235 fn resource_ids(&self) -> Vec<ResourceIdentifier> {
236 vec![self.0.clone()]
237 }
238
239 fn file_name() -> &'static str {
240 "test.yaml"
241 }
242
243 fn bundle_extension() -> &'static str {
244 "test"
245 }
246 }
247
248 let manifest = TestManifest("test.thing".into());
249
250 let bundle = Bundle::new(manifest.clone(), vec![("thing".into(), vec![1].into())]).unwrap();
251
252 assert_eq!(vec!["thing".to_string()], bundle.manifest.resource_ids());
253 assert_eq!(
254 &ResourceBytes::from(vec![1]),
255 bundle.get_resource(&"thing".into()).unwrap()
256 );
257 }
258}