mr_bundle/
location.rs

1use crate::{
2    error::{BundleError, MrBundleResult},
3    ResourceBytes,
4};
5use holochain_util::ffs;
6use std::path::{Path, PathBuf};
7
8/// Where to find a Resource.
9///
10/// This representation, with named fields, is chosen so that in the yaml config
11/// either "path", "url", or "bundled" can be specified due to this field
12/// being flattened.
13#[derive(Clone, Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[serde(rename_all = "snake_case")]
15#[allow(missing_docs)]
16pub enum Location {
17    /// Expect file to be part of this bundle
18    Bundled(PathBuf),
19
20    /// Get file from local filesystem (not bundled)
21    Path(PathBuf),
22
23    /// Get file from URL
24    Url(String),
25}
26
27impl Location {
28    /// Make a relative Path absolute if possible, given the `root_dir`
29    pub fn normalize(&self, root_dir: Option<&PathBuf>) -> MrBundleResult<Location> {
30        if let Location::Path(path) = self {
31            if path.is_relative() {
32                if let Some(dir) = root_dir {
33                    Ok(Location::Path(ffs::sync::canonicalize(dir.join(path))?))
34                } else {
35                    Err(BundleError::RelativeLocalPath(path.to_owned()).into())
36                }
37            } else {
38                Ok(self.clone())
39            }
40        } else {
41            Ok(self.clone())
42        }
43    }
44}
45
46#[cfg(feature = "fuzzing")]
47impl proptest::arbitrary::Arbitrary for Location {
48    type Parameters = ();
49    type Strategy = proptest::strategy::BoxedStrategy<Self>;
50
51    // XXX: this is a bad arbitrary impl, could be derived automatically when
52    // https://github.com/proptest-rs/proptest/pull/362 lands
53    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
54        use proptest::strategy::Strategy;
55
56        proptest::prelude::any::<String>()
57            .prop_map(|s| Self::Path(s.into()))
58            .boxed()
59    }
60}
61
62pub(crate) async fn resolve_local(path: &Path) -> MrBundleResult<ResourceBytes> {
63    Ok(ffs::read(path).await?.into())
64}
65
66pub(crate) async fn resolve_remote(url: &str) -> MrBundleResult<ResourceBytes> {
67    Ok(reqwest::get(url)
68        .await?
69        .bytes()
70        .await?
71        .into_iter()
72        .collect::<Vec<_>>()
73        .into())
74}
75
76#[cfg(test)]
77mod tests {
78
79    use super::*;
80    use serde::{Deserialize, Serialize};
81    use serde_yaml::value::{Tag, TaggedValue};
82
83    #[derive(Serialize, Deserialize)]
84    struct TunaSalad {
85        celery: Vec<Location>,
86
87        #[serde(flatten)]
88        mayo: Location,
89    }
90
91    /// Test that Location serializes in a convenient way suitable for
92    /// human-readable manifests, e.g. YAML
93    ///
94    /// The YAML produced by this test looks like:
95    /// ```yaml
96    /// ---
97    /// celery:
98    ///   - !bundled: b
99    ///   - !path: p
100    /// url: "http://r.co"
101    /// ```
102    #[test]
103    fn location_flattening() {
104        use serde_yaml::Value;
105
106        let tuna = TunaSalad {
107            celery: vec![Location::Bundled("b".into()), Location::Path("p".into())],
108            mayo: Location::Url("http://r.co".into()),
109        };
110        let val = serde_yaml::to_value(&tuna).unwrap();
111        println!("yaml produced:\n{}", serde_yaml::to_string(&tuna).unwrap());
112
113        assert_eq!(
114            val["celery"][0],
115            Value::Tagged(Box::new(TaggedValue {
116                tag: Tag::new("!bundled"),
117                value: Value::from("b")
118            }))
119        );
120        assert_eq!(
121            val["celery"][1],
122            Value::Tagged(Box::new(TaggedValue {
123                tag: Tag::new("!path"),
124                value: Value::from("p")
125            }))
126        );
127        assert_eq!(val["url"], Value::from("http://r.co"));
128    }
129}