mr_bundle/
bundle.rs

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
12/// A map from resource identifiers to their value as byte arrays.
13pub type ResourceMap = BTreeMap<ResourceIdentifier, ResourceBytes>;
14
15/// A [Manifest], bundled with the Resources that it describes.
16///
17/// This is meant to be serialized for standalone distribution, and deserialized
18/// by the receiver.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Bundle<M>
21where
22    M: Debug + Serialize + DeserializeOwned,
23{
24    /// The manifest describing the resources that compose this bundle.
25    #[serde(bound(deserialize = "M: DeserializeOwned"))]
26    manifest: M,
27
28    /// The full or partial resource data. Each entry must correspond to one
29    /// of the Bundled Locations specified by the Manifest. Bundled Locations
30    /// are always relative paths (relative to the root_dir).
31    resources: ResourceMap,
32}
33
34impl<M> Bundle<M>
35where
36    M: Debug + Serialize + DeserializeOwned,
37{
38    /// Accessor for the Manifest
39    pub fn manifest(&self) -> &M {
40        &self.manifest
41    }
42
43    /// Accessor for the map of resources included in this bundle
44    pub fn get_all_resources(&self) -> HashMap<&ResourceIdentifier, &ResourceBytes> {
45        self.resources.iter().collect()
46    }
47
48    /// Retrieve the bytes for a single resource.
49    pub fn get_resource(&self, resource_identifier: &ResourceIdentifier) -> Option<&ResourceBytes> {
50        self.resources.get(resource_identifier)
51    }
52
53    /// Pack this bundle into a byte array.
54    ///
55    /// Uses [`pack`](fn@crate::pack) to produce the byte array.
56    pub fn pack(&self) -> MrBundleResult<bytes::Bytes> {
57        crate::pack(self)
58    }
59
60    /// Unpack bytes produced by [`pack`](Bundle::pack) into a new [Bundle].
61    ///
62    /// Uses [`unpack`](crate::unpack) to produce the new Bundle.
63    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    /// Creates a bundle containing a manifest and a collection of resources to
73    /// be bundled together with the manifest.
74    ///
75    /// The paths paired with each resource must correspond to the set of
76    /// `Location::Bundle`s specified in the `Manifest::location()`, or else
77    /// this is not a valid bundle.
78    ///
79    /// A base directory must also be supplied so that relative paths can be
80    /// resolved into absolute ones.
81    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    /// Return a new Bundle with an updated manifest, subject to the same
123    /// validation constraints as creating a new Bundle from scratch.
124    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}