stac_types/
href.rs

1use crate::{Error, Result};
2use serde::{Deserialize, Serialize};
3use std::{
4    fmt::Display,
5    path::{Path, PathBuf},
6};
7use url::Url;
8
9/// An href.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum Href {
13    /// A url href.
14    ///
15    /// This _can_ have a `file:` scheme.
16    Url(Url),
17
18    /// A string href.
19    ///
20    /// This is expected to have `/` delimiters. Windows-style `\` delimiters are not supported.
21    String(String),
22}
23
24#[derive(Debug)]
25pub enum RealizedHref {
26    /// A path buf
27    PathBuf(PathBuf),
28
29    /// A url
30    Url(Url),
31}
32
33/// Implemented by all three STAC objects, the [SelfHref] trait allows getting
34/// and setting an object's href.
35///
36/// Though the self href isn't part of the data structure, it is useful to know
37/// where a given STAC object was read from.  Objects created from scratch don't
38/// have an href.
39///
40/// # Examples
41///
42/// ```
43/// use stac::{Item, SelfHref};
44///
45/// let item = Item::new("an-id");
46/// assert!(item.self_href().is_none());
47/// let item: Item = stac::read("examples/simple-item.json").unwrap();
48/// assert!(item.self_href().is_some());
49/// ```
50pub trait SelfHref {
51    /// Gets this object's href.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use stac::{SelfHref, Item};
57    ///
58    /// let item: Item = stac::read("examples/simple-item.json").unwrap();
59    /// assert!(item.self_href().unwrap().to_string().ends_with("simple-item.json"));
60    /// ```
61    fn self_href(&self) -> Option<&Href>;
62
63    /// Returns a mutable reference to this object's self href.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use stac::{Item, SelfHref};
69    ///
70    /// let mut item = Item::new("an-id");
71    /// *item.self_href_mut() = Option::Some("./a/relative/path.json".into());
72    /// ```
73    fn self_href_mut(&mut self) -> &mut Option<Href>;
74}
75
76impl Href {
77    /// Convert this href into an absolute href using the given base.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use stac::Href;
83    ///
84    /// let href = Href::from("./a/b.json").absolute(&"/c/d/e.json".into()).unwrap();
85    /// assert_eq!(href, "/c/d/a/b.json");
86    /// ```
87    pub fn absolute(&self, base: &Href) -> Result<Href> {
88        tracing::debug!("making href={self} absolute with base={base}");
89        match base {
90            Href::Url(url) => url.join(self.as_str()).map(Href::Url).map_err(Error::from),
91            Href::String(s) => Ok(Href::String(make_absolute(self.as_str(), s))),
92        }
93    }
94
95    /// Convert this href into an relative href using to the given base.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use stac::Href;
101    ///
102    /// let href = Href::from("/a/b/c.json").relative(&"/a/d.json".into()).unwrap();
103    /// assert_eq!(href, "./b/c.json");
104    /// ```
105    pub fn relative(&self, base: &Href) -> Result<Href> {
106        tracing::debug!("making href={self} relative with base={base}");
107        match base {
108            Href::Url(base) => match self {
109                Href::Url(url) => Ok(base
110                    .make_relative(url)
111                    .map(Href::String)
112                    .unwrap_or_else(|| self.clone())),
113                Href::String(s) => {
114                    let url = s.parse()?;
115                    Ok(base
116                        .make_relative(&url)
117                        .map(Href::String)
118                        .unwrap_or_else(|| self.clone()))
119                }
120            },
121            Href::String(s) => Ok(Href::String(make_relative(self.as_str(), s))),
122        }
123    }
124
125    /// Returns true if this href is absolute.
126    ///
127    /// Urls are always absolute. Strings are absolute if they start with a `/`.
128    pub fn is_absolute(&self) -> bool {
129        match self {
130            Href::Url(_) => true,
131            Href::String(s) => s.starts_with('/'),
132        }
133    }
134
135    /// Returns this href as a str.
136    pub fn as_str(&self) -> &str {
137        match self {
138            Href::Url(url) => url.as_str(),
139            Href::String(s) => s.as_str(),
140        }
141    }
142
143    /// If the url scheme is `file`, convert it to a path string.
144    pub fn realize(self) -> RealizedHref {
145        match self {
146            Href::Url(url) => {
147                if url.scheme() == "file" {
148                    url.to_file_path()
149                        .map(RealizedHref::PathBuf)
150                        .unwrap_or_else(|_| RealizedHref::Url(url))
151                } else {
152                    RealizedHref::Url(url)
153                }
154            }
155            Href::String(s) => RealizedHref::PathBuf(PathBuf::from(s)),
156        }
157    }
158}
159
160impl Display for Href {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        match self {
163            Href::Url(url) => url.fmt(f),
164            Href::String(s) => s.fmt(f),
165        }
166    }
167}
168
169impl From<&str> for Href {
170    fn from(value: &str) -> Self {
171        if let Ok(url) = Url::parse(value) {
172            Href::Url(url)
173        } else {
174            Href::String(value.to_string())
175        }
176    }
177}
178
179impl From<String> for Href {
180    fn from(value: String) -> Self {
181        if let Ok(url) = Url::parse(&value) {
182            Href::Url(url)
183        } else {
184            Href::String(value)
185        }
186    }
187}
188
189impl From<&Path> for Href {
190    fn from(value: &Path) -> Self {
191        if cfg!(target_os = "windows") {
192            if let Ok(url) = Url::from_file_path(value) {
193                Href::Url(url)
194            } else {
195                Href::String(value.to_string_lossy().into_owned())
196            }
197        } else {
198            Href::String(value.to_string_lossy().into_owned())
199        }
200    }
201}
202
203impl From<PathBuf> for Href {
204    fn from(value: PathBuf) -> Self {
205        if cfg!(target_os = "windows") {
206            if let Ok(url) = Url::from_file_path(&value) {
207                Href::Url(url)
208            } else {
209                Href::String(value.to_string_lossy().into_owned())
210            }
211        } else {
212            Href::String(value.to_string_lossy().into_owned())
213        }
214    }
215}
216
217impl TryFrom<Href> for Url {
218    type Error = Error;
219    fn try_from(value: Href) -> Result<Self> {
220        match value {
221            Href::Url(url) => Ok(url),
222            Href::String(s) => s.parse().map_err(Error::from),
223        }
224    }
225}
226
227#[cfg(feature = "reqwest")]
228impl From<reqwest::Url> for Href {
229    fn from(value: reqwest::Url) -> Self {
230        Href::Url(value)
231    }
232}
233
234#[cfg(not(feature = "reqwest"))]
235impl From<Url> for Href {
236    fn from(value: Url) -> Self {
237        Href::Url(value)
238    }
239}
240
241impl PartialEq<&str> for Href {
242    fn eq(&self, other: &&str) -> bool {
243        self.as_str().eq(*other)
244    }
245}
246
247fn make_absolute(href: &str, base: &str) -> String {
248    // TODO if we make this interface public, make this an impl Option
249    if href.starts_with('/') {
250        href.to_string()
251    } else {
252        let (base, _) = base.split_at(base.rfind('/').unwrap_or(0));
253        if base.is_empty() {
254            normalize_path(href)
255        } else {
256            normalize_path(&format!("{}/{}", base, href))
257        }
258    }
259}
260
261fn normalize_path(path: &str) -> String {
262    let mut parts = if path.starts_with('/') {
263        Vec::new()
264    } else {
265        vec![""]
266    };
267    for part in path.split('/') {
268        match part {
269            "." => {}
270            ".." => {
271                let _ = parts.pop();
272            }
273            s => parts.push(s),
274        }
275    }
276    parts.join("/")
277}
278
279fn make_relative(href: &str, base: &str) -> String {
280    // Cribbed from `Url::make_relative`
281    let mut relative = String::new();
282
283    fn extract_path_filename(s: &str) -> (&str, &str) {
284        let last_slash_idx = s.rfind('/').unwrap_or(0);
285        let (path, filename) = s.split_at(last_slash_idx);
286        if filename.is_empty() {
287            (path, "")
288        } else {
289            (path, &filename[1..])
290        }
291    }
292
293    let (base_path, base_filename) = extract_path_filename(base);
294    let (href_path, href_filename) = extract_path_filename(href);
295
296    let mut base_path = base_path.split('/').peekable();
297    let mut href_path = href_path.split('/').peekable();
298
299    while base_path.peek().is_some() && base_path.peek() == href_path.peek() {
300        let _ = base_path.next();
301        let _ = href_path.next();
302    }
303
304    for base_path_segment in base_path {
305        if base_path_segment.is_empty() {
306            break;
307        }
308
309        if !relative.is_empty() {
310            relative.push('/');
311        }
312
313        relative.push_str("..");
314    }
315
316    for href_path_segment in href_path {
317        if relative.is_empty() {
318            relative.push_str("./");
319        } else {
320            relative.push('/');
321        }
322
323        relative.push_str(href_path_segment);
324    }
325
326    if !relative.is_empty() || base_filename != href_filename {
327        if href_filename.is_empty() {
328            relative.push('/');
329        } else {
330            if relative.is_empty() {
331                relative.push_str("./");
332            } else {
333                relative.push('/');
334            }
335            relative.push_str(href_filename);
336        }
337    }
338
339    relative
340}