1use crate::{
2 error::{BundleError, MrBundleResult},
3 location::Location,
4 manifest::Manifest,
5 resource::ResourceBytes,
6};
7use holochain_util::ffs;
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9use std::{
10 borrow::Cow,
11 collections::{BTreeMap, HashMap, HashSet},
12 path::{Path, PathBuf},
13};
14
15pub type ResourceMap = BTreeMap<PathBuf, ResourceBytes>;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Bundle<M>
24where
25 M: Manifest,
26{
27 #[serde(bound(deserialize = "M: DeserializeOwned"))]
29 manifest: M,
30
31 resources: ResourceMap,
35
36 #[serde(skip)]
47 root_dir: Option<PathBuf>,
48}
49
50impl<M> Bundle<M>
51where
52 M: Manifest,
53{
54 pub fn new<R: IntoIterator<Item = (PathBuf, ResourceBytes)>>(
64 manifest: M,
65 resources: R,
66 root_dir: PathBuf,
67 ) -> MrBundleResult<Self> {
68 Self::from_parts(manifest, resources, Some(root_dir))
69 }
70
71 pub fn new_unchecked<R: IntoIterator<Item = (PathBuf, ResourceBytes)>>(
73 manifest: M,
74 resources: R,
75 ) -> MrBundleResult<Self> {
76 Self::from_parts(manifest, resources, None)
77 }
78
79 fn from_parts<R: IntoIterator<Item = (PathBuf, ResourceBytes)>>(
80 manifest: M,
81 resources: R,
82 root_dir: Option<PathBuf>,
83 ) -> MrBundleResult<Self> {
84 let resources: ResourceMap = resources.into_iter().collect();
85 let manifest_paths: HashSet<_> = manifest
86 .locations()
87 .into_iter()
88 .filter_map(|loc| match loc {
89 Location::Bundled(path) => Some(path),
90 _ => None,
91 })
92 .collect();
93
94 for (resource_path, _) in resources.iter() {
96 if !manifest_paths.contains(resource_path) {
97 return Err(BundleError::BundledPathNotInManifest(resource_path.clone()).into());
98 }
99 }
100
101 let resources = resources.into_iter().collect();
102 Ok(Self {
103 manifest,
104 resources,
105 root_dir,
106 })
107 }
108
109 pub fn manifest(&self) -> &M {
111 &self.manifest
112 }
113
114 pub fn update_manifest(self, manifest: M) -> MrBundleResult<Self> {
117 Self::from_parts(manifest, self.resources, self.root_dir)
118 }
119
120 pub async fn read_from_file(path: &Path) -> MrBundleResult<Self> {
122 Self::decode(&ffs::read(path).await?)
123 }
124
125 pub async fn write_to_file(&self, path: &Path) -> MrBundleResult<()> {
127 Ok(ffs::write(path, &self.encode()?).await?)
128 }
129
130 pub async fn resolve(&self, location: &Location) -> MrBundleResult<Cow<'_, ResourceBytes>> {
133 let bytes = match &location.normalize(self.root_dir.as_ref())? {
134 Location::Bundled(path) => Cow::Borrowed(
135 self.resources
136 .get(path)
137 .ok_or_else(|| BundleError::BundledResourceMissing(path.clone()))?,
138 ),
139 Location::Path(path) => Cow::Owned(crate::location::resolve_local(path).await?),
140 Location::Url(url) => Cow::Owned(crate::location::resolve_remote(url).await?),
141 };
142 Ok(bytes)
143 }
144
145 pub async fn resolve_all(&self) -> MrBundleResult<HashMap<Location, Cow<'_, ResourceBytes>>> {
149 futures::future::join_all(
150 self.manifest.locations().into_iter().map(|loc| async move {
151 MrBundleResult::Ok((loc.clone(), self.resolve(&loc).await?))
152 }),
153 )
154 .await
155 .into_iter()
156 .collect::<MrBundleResult<HashMap<Location, Cow<'_, ResourceBytes>>>>()
157 }
158
159 pub async fn resolve_all_cloned(&self) -> MrBundleResult<HashMap<Location, ResourceBytes>> {
161 Ok(self
162 .resolve_all()
163 .await?
164 .into_iter()
165 .map(|(k, v)| (k, v.into_owned()))
166 .collect())
167 }
168
169 pub fn bundled_resources(&self) -> &ResourceMap {
173 &self.resources
174 }
175
176 pub fn encode(&self) -> MrBundleResult<Vec<u8>> {
178 crate::encode(self)
179 }
180
181 pub fn decode(bytes: &[u8]) -> MrBundleResult<Self> {
183 crate::decode(bytes)
184 }
185
186 #[cfg(feature = "packing")]
199 pub fn find_root_dir(&self, path: &Path) -> MrBundleResult<PathBuf> {
200 crate::util::prune_path(path.into(), M::path()).map_err(Into::into)
201 }
202}
203
204#[derive(Debug, PartialEq, Eq, Deserialize)]
208pub struct RawBundle<M> {
209 #[serde(bound(deserialize = "M: DeserializeOwned"))]
211 pub manifest: M,
212
213 pub resources: ResourceMap,
217}
218
219impl<M: serde::de::DeserializeOwned> RawBundle<M> {
220 pub async fn read_from_file(path: &Path) -> MrBundleResult<Self> {
222 crate::decode(&ffs::read(path).await?)
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use crate::error::MrBundleError;
229
230 use super::*;
231
232 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
233 struct TestManifest(Vec<Location>);
234
235 impl Manifest for TestManifest {
236 fn locations(&self) -> Vec<Location> {
237 self.0.clone()
238 }
239
240 #[cfg(feature = "packing")]
241 fn path() -> PathBuf {
242 unimplemented!()
243 }
244
245 #[cfg(feature = "packing")]
246 fn bundle_extension() -> &'static str {
247 unimplemented!()
248 }
249 }
250
251 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252 struct Thing(u32);
253
254 #[tokio::test]
255 async fn bundle_validation() {
256 let manifest = TestManifest(vec![
257 Location::Bundled("1.thing".into()),
258 Location::Bundled("2.thing".into()),
259 ]);
260 assert!(
261 Bundle::new_unchecked(manifest.clone(), vec![("1.thing".into(), vec![1].into())])
262 .is_ok()
263 );
264
265 matches::assert_matches!(
266 Bundle::new_unchecked(manifest, vec![("3.thing".into(), vec![3].into())]),
267 Err(MrBundleError::BundleError(BundleError::BundledPathNotInManifest(path))) if path == PathBuf::from("3.thing")
268 );
269 }
270}