identity_core/common/
string_or_url.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::convert::Infallible;
5use std::fmt::Display;
6use std::str::FromStr;
7
8use serde::Deserialize;
9use serde::Serialize;
10
11use super::Url;
12
13/// A type that represents either an arbitrary string or a URL.
14#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(untagged)]
16pub enum StringOrUrl {
17  /// A well-formed URL.
18  Url(Url),
19  /// An arbitrary UTF-8 string.
20  String(String),
21}
22
23impl StringOrUrl {
24  /// Parses a [`StringOrUrl`] from a string.
25  pub fn parse(s: &str) -> Result<Self, Infallible> {
26    s.parse()
27  }
28  /// Returns a [`Url`] reference if `self` is [`StringOrUrl::Url`].
29  pub fn as_url(&self) -> Option<&Url> {
30    match self {
31      Self::Url(url) => Some(url),
32      _ => None,
33    }
34  }
35
36  /// Returns a [`str`] reference if `self` is [`StringOrUrl::String`].
37  pub fn as_string(&self) -> Option<&str> {
38    match self {
39      Self::String(s) => Some(s),
40      _ => None,
41    }
42  }
43
44  /// Returns whether `self` is a [`StringOrUrl::Url`].
45  pub fn is_url(&self) -> bool {
46    matches!(self, Self::Url(_))
47  }
48
49  /// Returns whether `self` is a [`StringOrUrl::String`].
50  pub fn is_string(&self) -> bool {
51    matches!(self, Self::String(_))
52  }
53}
54
55impl Default for StringOrUrl {
56  fn default() -> Self {
57    StringOrUrl::String(String::default())
58  }
59}
60
61impl Display for StringOrUrl {
62  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63    match self {
64      Self::Url(url) => write!(f, "{url}"),
65      Self::String(s) => write!(f, "{s}"),
66    }
67  }
68}
69
70impl FromStr for StringOrUrl {
71  // Cannot fail.
72  type Err = Infallible;
73  fn from_str(s: &str) -> Result<Self, Self::Err> {
74    Ok(
75      s.parse::<Url>()
76        .map(Self::Url)
77        .unwrap_or_else(|_| Self::String(s.to_string())),
78    )
79  }
80}
81
82impl AsRef<str> for StringOrUrl {
83  fn as_ref(&self) -> &str {
84    match self {
85      Self::String(s) => s,
86      Self::Url(url) => url.as_str(),
87    }
88  }
89}
90
91impl From<Url> for StringOrUrl {
92  fn from(value: Url) -> Self {
93    Self::Url(value)
94  }
95}
96
97impl From<String> for StringOrUrl {
98  fn from(value: String) -> Self {
99    Self::String(value)
100  }
101}
102
103impl From<StringOrUrl> for String {
104  fn from(value: StringOrUrl) -> Self {
105    match value {
106      StringOrUrl::String(s) => s,
107      StringOrUrl::Url(url) => url.into_string(),
108    }
109  }
110}
111
112#[cfg(test)]
113mod tests {
114  use super::*;
115
116  #[derive(Debug, Serialize, Deserialize)]
117  struct TestData {
118    string_or_url: StringOrUrl,
119  }
120
121  impl Default for TestData {
122    fn default() -> Self {
123      Self {
124        string_or_url: StringOrUrl::Url(TEST_URL.parse().unwrap()),
125      }
126    }
127  }
128
129  const TEST_URL: &str = "file:///tmp/file.txt";
130
131  #[test]
132  fn deserialization_works() {
133    let test_data: TestData = serde_json::from_value(serde_json::json!({ "string_or_url": TEST_URL })).unwrap();
134    let target_url: Url = TEST_URL.parse().unwrap();
135    assert_eq!(test_data.string_or_url.as_url(), Some(&target_url));
136  }
137
138  #[test]
139  fn serialization_works() {
140    assert_eq!(
141      serde_json::to_value(TestData::default()).unwrap(),
142      serde_json::json!({ "string_or_url": TEST_URL })
143    )
144  }
145
146  #[test]
147  fn parsing_works() {
148    assert!(TEST_URL.parse::<StringOrUrl>().unwrap().is_url());
149    assert!("I'm a random string :)".parse::<StringOrUrl>().unwrap().is_string());
150  }
151}