1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use crate::{
    error::{BundleError, MrBundleResult},
    ResourceBytes,
};
use holochain_util::ffs;
use std::path::{Path, PathBuf};

/// Where to find a Resource.
///
/// This representation, with named fields, is chosen so that in the yaml config
/// either "path", "url", or "bundled" can be specified due to this field
/// being flattened.
#[derive(Clone, Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
#[allow(missing_docs)]
pub enum Location {
    /// Expect file to be part of this bundle
    Bundled(PathBuf),

    /// Get file from local filesystem (not bundled)
    Path(PathBuf),

    /// Get file from URL
    Url(String),
}

impl Location {
    /// Make a relative Path absolute if possible, given the `root_dir`
    pub fn normalize(&self, root_dir: Option<&PathBuf>) -> MrBundleResult<Location> {
        if let Location::Path(path) = self {
            if path.is_relative() {
                if let Some(dir) = root_dir {
                    Ok(Location::Path(ffs::sync::canonicalize(dir.join(path))?))
                } else {
                    Err(BundleError::RelativeLocalPath(path.to_owned()).into())
                }
            } else {
                Ok(self.clone())
            }
        } else {
            Ok(self.clone())
        }
    }
}

#[cfg(feature = "fuzzing")]
impl proptest::arbitrary::Arbitrary for Location {
    type Parameters = ();
    type Strategy = proptest::strategy::BoxedStrategy<Self>;

    // XXX: this is a bad arbitrary impl, could be derived automatically when
    // https://github.com/proptest-rs/proptest/pull/362 lands
    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
        use proptest::strategy::Strategy;

        proptest::prelude::any::<String>()
            .prop_map(|s| Self::Path(s.into()))
            .boxed()
    }
}

pub(crate) async fn resolve_local(path: &Path) -> MrBundleResult<ResourceBytes> {
    Ok(ffs::read(path).await?.into())
}

pub(crate) async fn resolve_remote(url: &str) -> MrBundleResult<ResourceBytes> {
    Ok(reqwest::get(url)
        .await?
        .bytes()
        .await?
        .into_iter()
        .collect::<Vec<_>>()
        .into())
}

#[cfg(test)]
mod tests {

    use super::*;
    use serde::{Deserialize, Serialize};
    use serde_yaml::value::{Tag, TaggedValue};

    #[derive(Serialize, Deserialize)]
    struct TunaSalad {
        celery: Vec<Location>,

        #[serde(flatten)]
        mayo: Location,
    }

    /// Test that Location serializes in a convenient way suitable for
    /// human-readable manifests, e.g. YAML
    ///
    /// The YAML produced by this test looks like:
    /// ---
    /// celery:
    ///   - !bundled: b
    ///   - !path: p
    /// url: "http://r.co"
    #[test]
    fn location_flattening() {
        use serde_yaml::Value;

        let tuna = TunaSalad {
            celery: vec![Location::Bundled("b".into()), Location::Path("p".into())],
            mayo: Location::Url("http://r.co".into()),
        };
        let val = serde_yaml::to_value(&tuna).unwrap();
        println!("yaml produced:\n{}", serde_yaml::to_string(&tuna).unwrap());

        assert_eq!(
            val["celery"][0],
            Value::Tagged(Box::new(TaggedValue {
                tag: Tag::new("!bundled"),
                value: Value::from("b")
            }))
        );
        assert_eq!(
            val["celery"][1],
            Value::Tagged(Box::new(TaggedValue {
                tag: Tag::new("!path"),
                value: Value::from("p")
            }))
        );
        assert_eq!(val["url"], Value::from("http://r.co"));
    }
}