languages_rs/
value.rs

1//! The Value enum, a loosely typed way of representing any valid language text value.
2//!
3//! # Valid language texts
4//! Language texts only can be in JSON or TOML format.
5//!
6//! ## JSON
7//! ```json
8//! {
9//!     "hello_world": "Hello, world!",
10//!     "pages": {
11//!         "home": {
12//!             "title": "Home page",
13//!             "description": "This is the home page."
14//!         },
15//!     },
16//!     "data": [
17//!         "Message 1",
18//!         "Message 2"
19//!     ]
20//! }
21//! ```
22//!
23//! ## TOML
24//! ```toml
25//! hello_world = "Hello, world!"
26//!
27//! [pages]
28//!     [pages.home]
29//!     title = "Home page"
30//!     description = "This is the home page."
31//!
32//! data = [
33//!     "Message 1",
34//!     "Message 2"
35//! ]
36//! ```
37
38use std::{collections::HashMap, fmt};
39
40#[cfg(feature = "with-json")]
41use serde_json::Value as JsonValue;
42
43#[cfg(feature = "with-toml")]
44use toml::Value as TomlValue;
45
46#[derive(Clone, Debug, PartialEq)]
47pub enum Value {
48    String(String),
49    Array(Vec<Value>),
50    Object(HashMap<String, Value>),
51}
52
53impl Value {
54    #[cfg(feature = "with-json")]
55    fn from_value(value: JsonValue) -> anyhow::Result<Self> {
56        if value.is_string() {
57            return Ok(Self::String(String::from(value.as_str().unwrap())));
58        } else if value.is_array() {
59            return Ok(Self::Array(
60                value
61                    .as_array()
62                    .unwrap()
63                    .iter()
64                    .map(|e| Self::from_value(e.clone()).expect("Invalid format."))
65                    .collect(),
66            ));
67        } else if value.is_object() {
68            let mut new_data: HashMap<String, Value> = HashMap::new();
69            for (key, value) in value.as_object().unwrap().iter() {
70                new_data.insert(key.clone(), Self::from_value(value.clone())?);
71            }
72
73            return Ok(Self::Object(new_data));
74        }
75
76        Err(anyhow::Error::msg(format!(
77            "Cannot parse `{}` as a language text value.",
78            value
79        )))
80    }
81
82    #[cfg(feature = "with-toml")]
83    pub fn from_value(value: TomlValue) -> anyhow::Result<Self> {
84        if value.is_str() {
85            return Ok(Self::String(String::from(value.as_str().unwrap())));
86        } else if value.is_array() {
87            return Ok(Self::Array(
88                value
89                    .as_array()
90                    .unwrap()
91                    .iter()
92                    .map(|e| Self::from_value(e.clone()).expect("Invalid format."))
93                    .collect(),
94            ));
95        } else if value.is_table() {
96            let mut new_data: HashMap<String, Value> = HashMap::new();
97            for (key, value) in value.as_table().unwrap().iter() {
98                new_data.insert(key.clone(), Self::from_value(value.clone())?);
99            }
100
101            return Ok(Self::Object(new_data));
102        }
103
104        Err(anyhow::Error::msg(format!(
105            "Cannot parse `{}` as a language text value.",
106            value
107        )))
108    }
109
110    /// Get the texts from a JSON string.
111    ///
112    /// # Example
113    /// ```rust
114    /// use languages_rs::Value;
115    ///
116    /// let value = Value::from_string(String::from("\"Hi\""));
117    /// assert!(value.is_ok());
118    /// assert_eq!(value.unwrap(), Value::String(String::from("Hi")));
119    /// ```
120    #[cfg(feature = "with-json")]
121    pub fn from_string(text: String) -> anyhow::Result<Self> {
122        Self::from_value(serde_json::from_str(&text)?)
123    }
124
125    /// Get the texts from a JSON string or TOML string.
126    ///
127    /// # Example
128    /// ```rust
129    /// use languages_rs::Value;
130    ///
131    /// use std::collections::HashMap;
132    ///
133    /// let value = Value::from_string(String::from("hi = \"Hi\""));
134    /// assert!(value.is_ok());
135    ///
136    /// let mut data: HashMap<String, Value> = HashMap::new();
137    /// data.insert(String::from("hi"), Value::String(String::from("Hi")));
138    ///
139    /// assert_eq!(value.unwrap(), Value::Object(data));
140    /// ```
141    #[cfg(feature = "with-toml")]
142    pub fn from_string(text: String) -> anyhow::Result<Self> {
143        Self::from_value(text.parse()?)
144    }
145
146    #[cfg(all(not(feature = "with-json"), not(feature = "with-toml")))]
147    pub fn from_string(_text: String) -> anyhow::Result<Self> {
148        Err(anyhow::Error::msg("You must define the parse feature."))
149    }
150
151    /// Check if the current value is a string.
152    ///
153    /// # Example
154    /// ```rust
155    /// use languages_rs::Value;
156    ///
157    /// #[cfg(feature = "with-json")]
158    /// fn main() {
159    ///     let value = Value::from_string(String::from("\"Hi\""));
160    ///     assert!(value.is_ok());
161    ///     assert!(value.unwrap().is_string());
162    /// }
163    ///
164    /// #[cfg(feature = "with-toml")]
165    /// fn main() {
166    ///     let value = Value::from_string(String::from("hi = \"Hi\""));
167    ///     assert!(value.is_ok());
168    ///
169    ///     let table = value.unwrap().get_object();
170    ///     assert!(table.is_some());
171    ///
172    ///     let table = table.unwrap();
173    ///     let text = table.get("hi");
174    ///     assert!(text.is_some());
175    ///     assert!(text.unwrap().is_string());
176    /// }
177    ///
178    /// #[cfg(all(not(feature = "with-json"), not(feature = "with-toml")))]
179    /// fn main() {}
180    /// ```
181    pub fn is_string(&self) -> bool {
182        matches!(self, Self::String(_))
183    }
184
185    /// Get the string value.
186    ///
187    /// # Example
188    /// ```rust
189    /// use languages_rs::Value;
190    ///
191    /// #[cfg(feature = "with-json")]
192    /// fn main() {
193    ///     let value = Value::from_string(String::from("\"Hi\""));
194    ///     assert!(value.is_ok());
195    ///     assert_eq!(value.unwrap().get_string(), Some(String::from("Hi")));
196    /// }
197    ///
198    /// #[cfg(feature = "with-toml")]
199    /// fn main() {
200    ///     let value = Value::from_string(String::from("hi = \"Hi\""));
201    ///     assert!(value.is_ok());
202    ///
203    ///     let table = value.unwrap().get_object();
204    ///     assert!(table.is_some());
205    ///
206    ///     let table = table.unwrap();
207    ///     let text = table.get("hi");
208    ///     assert!(text.is_some());
209    ///     assert_eq!(text.unwrap().get_string(), Some(String::from("Hi")));
210    /// }
211    ///
212    /// #[cfg(all(not(feature = "with-json"), not(feature = "with-toml")))]
213    /// fn main() {}
214    /// ```
215    pub fn get_string(&self) -> Option<String> {
216        match self {
217            Self::String(value) => Some(value.clone()),
218            _ => None,
219        }
220    }
221
222    /// Check if the current value is an array.
223    ///
224    /// # Example
225    /// ```rust
226    /// use languages_rs::Value;
227    ///
228    /// #[cfg(feature = "with-json")]
229    /// fn main() {
230    ///     let value = Value::from_string(String::from("[\"1\", \"2\"]"));
231    ///     assert!(value.is_ok());
232    ///     assert!(value.unwrap().is_array());
233    /// }
234    ///
235    /// #[cfg(feature = "with-toml")]
236    /// fn main() {
237    ///     let value = Value::from_string(String::from("numbers = [\"1\", \"2\"]"));
238    ///     assert!(value.is_ok());
239    ///
240    ///     let table = value.unwrap().get_object();
241    ///     assert!(table.is_some());
242    ///
243    ///     let table = table.unwrap();
244    ///     let values = table.get("numbers");
245    ///     assert!(values.is_some());
246    ///     assert!(values.unwrap().is_array());
247    /// }
248    ///
249    /// #[cfg(all(not(feature = "with-json"), not(feature = "with-toml")))]
250    /// fn main() {}
251    /// ```
252    pub fn is_array(&self) -> bool {
253        matches!(self, Self::Array(_))
254    }
255
256    /// Get the array value.
257    ///
258    /// # Example
259    /// ```rust
260    /// use languages_rs::Value;
261    ///
262    /// #[cfg(feature = "with-json")]
263    /// fn main() {
264    ///     let value = Value::from_string(String::from("[\"1\", \"2\"]"));
265    ///     assert!(value.is_ok());
266    ///     assert_eq!(
267    ///         value.unwrap().get_array(),
268    ///         Some(vec![Value::String(String::from("1")), Value::String(String::from("2"))]),
269    ///     );
270    /// }
271    ///
272    /// #[cfg(feature = "with-toml")]
273    /// fn main() {
274    ///     let value = Value::from_string(String::from("numbers = [\"1\", \"2\"]"));
275    ///     assert!(value.is_ok());
276    ///
277    ///     let table = value.unwrap().get_object();
278    ///     assert!(table.is_some());
279    ///
280    ///     let table = table.unwrap();
281    ///     let values = table.get("numbers");
282    ///     assert!(values.is_some());
283    ///     assert_eq!(
284    ///         values.unwrap().get_array(),
285    ///         Some(vec![Value::String(String::from("1")), Value::String(String::from("2"))]),
286    ///     );
287    /// }
288    ///
289    /// #[cfg(all(not(feature = "with-json"), not(feature = "with-toml")))]
290    /// fn main() {}
291    /// ```
292    pub fn get_array(&self) -> Option<Vec<Value>> {
293        match self {
294            Self::Array(data) => Some(data.clone()),
295            _ => None,
296        }
297    }
298
299    /// Check if the current value is an object.
300    ///
301    /// # Example JSON
302    /// ```rust
303    /// use languages_rs::Value;
304    ///
305    /// #[cfg(feature = "with-json")]
306    /// fn main() {
307    ///     let value = Value::from_string(String::from("{\"home\":{\"title\":\"Home page\"}}"));
308    ///     assert!(value.is_ok());
309    ///     assert!(value.unwrap().is_object());
310    /// }
311    ///
312    /// #[cfg(feature = "with-toml")]
313    /// fn main() {
314    ///     let value = Value::from_string(String::from("[home]\r\ntitle = \"Home page\""));
315    ///     assert!(value.is_ok());
316    ///     assert!(value.unwrap().is_object());
317    /// }
318    ///
319    /// #[cfg(all(not(feature = "with-json"), not(feature = "with-toml")))]
320    /// fn main() {}
321    /// ```
322    pub fn is_object(&self) -> bool {
323        matches!(self, Self::Object(_))
324    }
325
326    /// Get the object value.
327    ///
328    /// # Example JSON
329    /// ```rust
330    /// use std::collections::HashMap;
331    ///
332    /// use languages_rs::Value;
333    ///
334    /// #[cfg(feature = "with-json")]
335    /// fn main() {
336    ///     let value = Value::from_string(String::from("{ \"title\": \"Home page\" }"));
337    ///     assert!(value.is_ok());
338    ///
339    ///     let mut data: HashMap<String, Value> = HashMap::new();
340    ///     data.insert(String::from("title"), Value::String(String::from("Home page")));
341    ///
342    ///     assert_eq!(value.unwrap().get_object(), Some(data));
343    /// }
344    ///
345    /// #[cfg(feature = "with-toml")]
346    /// fn main() {
347    ///     let value = Value::from_string(String::from("title = \"Home page\""));
348    ///     assert!(value.is_ok());
349    ///
350    ///     let mut data: HashMap<String, Value> = HashMap::new();
351    ///     data.insert(String::from("title"), Value::String(String::from("Home page")));
352    ///
353    ///     assert_eq!(value.unwrap().get_object(), Some(data));
354    /// }
355    ///
356    /// #[cfg(all(not(feature = "with-json"), not(feature = "with-toml")))]
357    /// fn main() {}
358    /// ```
359    pub fn get_object(&self) -> Option<HashMap<String, Value>> {
360        match self {
361            Self::Object(data) => Some(data.clone()),
362            _ => None,
363        }
364    }
365}
366
367impl fmt::Display for Value {
368    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369        match self {
370            Self::String(value) => write!(f, "{}", value),
371            Self::Array(value) => write!(
372                f,
373                "[{}]",
374                value
375                    .iter()
376                    .map(|e| format!("{}", e))
377                    .collect::<Vec<String>>()
378                    .join(", ")
379            ),
380            Self::Object(value) => {
381                write!(
382                    f,
383                    "{{ {} }}",
384                    value
385                        .iter()
386                        .map(|(key, value)| format!("{}: {}", key, value))
387                        .collect::<Vec<String>>()
388                        .join(", ")
389                )
390            }
391        }
392    }
393}