feattle_core/
feattle_value.rs

1use crate::definition::{SerializedFormat, StringFormat};
2use crate::json_reading::{
3    extract_array, extract_bool, extract_f64, extract_i64, extract_object, extract_str,
4    FromJsonError,
5};
6use crate::{SerializedFormatKind, StringFormatKind};
7use serde_json::{Number, Value};
8use std::collections::{BTreeMap, BTreeSet};
9use std::convert::TryInto;
10use std::error::Error;
11use std::fmt::Debug;
12use std::fmt::Write;
13use std::str::FromStr;
14#[cfg(feature = "uuid")]
15use uuid::Uuid;
16
17/// The base trait for types that can be used for feattles.
18///
19/// This lib already implements this trait for many base types from the std lib, but the user can
20/// make their own types compatible by providing their own logic.
21///
22/// For types that are string based, it suffices to implement the somewhat simpler
23/// [`FeattleStringValue`] trait.
24pub trait FeattleValue: Debug + Sized {
25    /// Convert the value to its JSON representation.
26    fn as_json(&self) -> Value;
27
28    /// Return a short overview of the current value. This is meant to give an overall idea of the
29    /// actual value. For example, it can choose to display only the first 3 items of a large list.
30    fn overview(&self) -> String;
31
32    /// Parse from a JSON representation of the value, if possible.
33    fn try_from_json(value: &Value) -> Result<Self, FromJsonError>;
34
35    /// Return a precise description of a feattle type. This will be consumed, for example, by the
36    /// UI code to show an appropriate HTML form in the admin panel.
37    fn serialized_format() -> SerializedFormat;
38}
39
40/// The base trait for string-types that can be used for feattles.
41///
42/// This trait should be used for types that behave like string. A blanked implementation of
43/// [`FeattleValue`] for types that implement this trait will provide the necessary compatibility
44/// to use them as feattles.
45///
46/// Note that this trait also requires that the type implements:
47/// * [`Debug`]
48/// * [`ToString`]
49/// * [`FromStr`], with a compatible error
50pub trait FeattleStringValue: FromStr + ToString + Debug
51where
52    <Self as FromStr>::Err: Error + Send + Sync + 'static,
53{
54    /// Return a precise description of a feattle type. This will be consumed, for example, by the
55    /// UI code to show an appropriate HTML form in the admin panel.
56    fn serialized_string_format() -> StringFormat;
57}
58
59impl<T> FeattleValue for T
60where
61    T: FeattleStringValue,
62    <T as FromStr>::Err: Error + Send + Sync + 'static,
63{
64    fn as_json(&self) -> Value {
65        Value::String(self.to_string())
66    }
67    fn overview(&self) -> String {
68        self.to_string()
69    }
70    fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
71        extract_str(value)?.parse().map_err(FromJsonError::parsing)
72    }
73    fn serialized_format() -> SerializedFormat {
74        let f = Self::serialized_string_format();
75        SerializedFormat {
76            kind: SerializedFormatKind::String(f.kind),
77            tag: f.tag,
78        }
79    }
80}
81
82impl FeattleValue for bool {
83    fn as_json(&self) -> Value {
84        Value::Bool(*self)
85    }
86    fn overview(&self) -> String {
87        self.to_string()
88    }
89    fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
90        extract_bool(value)
91    }
92    fn serialized_format() -> SerializedFormat {
93        SerializedFormat {
94            kind: SerializedFormatKind::Bool,
95            tag: "bool".to_owned(),
96        }
97    }
98}
99
100macro_rules! impl_try_from_value_i64 {
101    ($kind:ty) => {
102        impl FeattleValue for $kind {
103            fn as_json(&self) -> Value {
104                serde_json::to_value(*self).unwrap()
105            }
106            fn overview(&self) -> String {
107                self.to_string()
108            }
109            fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
110                extract_i64(value)?
111                    .try_into()
112                    .map_err(FromJsonError::parsing)
113            }
114            fn serialized_format() -> SerializedFormat {
115                SerializedFormat {
116                    kind: SerializedFormatKind::Integer,
117                    tag: stringify!($kind).to_owned(),
118                }
119            }
120        }
121    };
122}
123
124impl_try_from_value_i64! {u8}
125impl_try_from_value_i64! {i8}
126impl_try_from_value_i64! {u16}
127impl_try_from_value_i64! {i16}
128impl_try_from_value_i64! {u32}
129impl_try_from_value_i64! {i32}
130impl_try_from_value_i64! {u64}
131impl_try_from_value_i64! {i64}
132impl_try_from_value_i64! {usize}
133impl_try_from_value_i64! {isize}
134
135impl FeattleValue for f32 {
136    fn as_json(&self) -> Value {
137        Value::Number(Number::from_f64(*self as f64).unwrap())
138    }
139    fn overview(&self) -> String {
140        self.to_string()
141    }
142    fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
143        let n_64 = extract_f64(value)?;
144        let n_32 = n_64 as f32;
145        if (n_64 - n_32 as f64).abs() > 1e-6 {
146            Err(FromJsonError::WrongKind {
147                actual: "Number::f64",
148                expected: "Number::f32",
149            })
150        } else {
151            Ok(n_32)
152        }
153    }
154    fn serialized_format() -> SerializedFormat {
155        SerializedFormat {
156            kind: SerializedFormatKind::Float,
157            tag: "f32".to_owned(),
158        }
159    }
160}
161
162impl FeattleValue for f64 {
163    fn as_json(&self) -> Value {
164        Value::Number(Number::from_f64(*self).unwrap())
165    }
166    fn overview(&self) -> String {
167        self.to_string()
168    }
169    fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
170        extract_f64(value)
171    }
172    fn serialized_format() -> SerializedFormat {
173        SerializedFormat {
174            kind: SerializedFormatKind::Float,
175            tag: "f64".to_owned(),
176        }
177    }
178}
179
180#[cfg(feature = "uuid")]
181impl FeattleStringValue for Uuid {
182    fn serialized_string_format() -> StringFormat {
183        StringFormat {
184            kind: StringFormatKind::Pattern(
185                "[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}",
186            ),
187            tag: "Uuid".to_owned(),
188        }
189    }
190}
191
192impl FeattleStringValue for String {
193    fn serialized_string_format() -> StringFormat {
194        StringFormat {
195            kind: StringFormatKind::Any,
196            tag: "String".to_owned(),
197        }
198    }
199}
200
201impl<T: FeattleValue> FeattleValue for Vec<T> {
202    fn as_json(&self) -> Value {
203        Value::Array(self.iter().map(|item| item.as_json()).collect())
204    }
205    fn overview(&self) -> String {
206        format!("[{}]", iter_overview(self.iter()))
207    }
208    fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
209        let mut list = Vec::new();
210        for item in extract_array(value)? {
211            list.push(T::try_from_json(item)?);
212        }
213        Ok(list)
214    }
215    fn serialized_format() -> SerializedFormat {
216        let f = T::serialized_format();
217        SerializedFormat {
218            kind: SerializedFormatKind::List(Box::new(f.kind)),
219            tag: format!("Vec<{}>", f.tag),
220        }
221    }
222}
223
224impl<T: FeattleValue + Ord> FeattleValue for BTreeSet<T> {
225    fn as_json(&self) -> Value {
226        Value::Array(self.iter().map(|item| item.as_json()).collect())
227    }
228    fn overview(&self) -> String {
229        format!("[{}]", iter_overview(self.iter()))
230    }
231    fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
232        let mut set = BTreeSet::new();
233        for item in extract_array(value)? {
234            set.insert(T::try_from_json(item)?);
235        }
236        Ok(set)
237    }
238    fn serialized_format() -> SerializedFormat {
239        let f = T::serialized_format();
240        SerializedFormat {
241            kind: SerializedFormatKind::Set(Box::new(f.kind)),
242            tag: format!("Set<{}>", f.tag),
243        }
244    }
245}
246
247impl<K: FeattleStringValue + Ord, V: FeattleValue> FeattleValue for BTreeMap<K, V>
248where
249    <K as FromStr>::Err: Error + Send + Sync + 'static,
250{
251    fn as_json(&self) -> Value {
252        Value::Object(
253            self.iter()
254                .map(|(item_key, item_value)| (item_key.to_string(), item_value.as_json()))
255                .collect(),
256        )
257    }
258    fn overview(&self) -> String {
259        // Group by value
260        let mut keys_by_value: BTreeMap<_, Vec<_>> = BTreeMap::new();
261        for (key, value) in self {
262            keys_by_value.entry(value.overview()).or_default().push(key);
263        }
264
265        let overview_by_value: Vec<_> = keys_by_value
266            .into_iter()
267            .map(|(value, keys)| format!("{}: {}", iter_overview(keys.into_iter()), value))
268            .collect();
269
270        format!("{{{}}}", iter_overview(overview_by_value.iter()))
271    }
272    fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
273        let mut map = BTreeMap::new();
274        for (item_key, item_value) in extract_object(value)? {
275            map.insert(
276                item_key.parse().map_err(FromJsonError::parsing)?,
277                V::try_from_json(item_value)?,
278            );
279        }
280        Ok(map)
281    }
282    fn serialized_format() -> SerializedFormat {
283        let fk = K::serialized_string_format();
284        let fv = V::serialized_format();
285        SerializedFormat {
286            kind: SerializedFormatKind::Map(fk.kind, Box::new(fv.kind)),
287            tag: format!("Map<{}, {}>", fk.tag, fv.tag),
288        }
289    }
290}
291
292impl<T: FeattleValue> FeattleValue for Option<T> {
293    fn as_json(&self) -> Value {
294        match self {
295            None => Value::Null,
296            Some(inner) => inner.as_json(),
297        }
298    }
299    fn overview(&self) -> String {
300        match self {
301            None => "None".to_owned(),
302            Some(s) => format!("Some({})", s.overview()),
303        }
304    }
305    fn try_from_json(value: &Value) -> Result<Self, FromJsonError> {
306        match value {
307            Value::Null => Ok(None),
308            other => T::try_from_json(other).map(Some),
309        }
310    }
311    fn serialized_format() -> SerializedFormat {
312        let f = T::serialized_format();
313        SerializedFormat {
314            kind: SerializedFormatKind::Optional(Box::new(f.kind)),
315            tag: format!("Option<{}>", f.tag),
316        }
317    }
318}
319
320fn iter_overview<'a, T: FeattleValue + 'a>(iter: impl Iterator<Item = &'a T>) -> String {
321    const MAX_ITEMS: usize = 3;
322    let mut overview = String::new();
323    let mut iter = iter.enumerate();
324
325    while let Some((i, value)) = iter.next() {
326        if i == MAX_ITEMS {
327            write!(overview, ", ... {} more", iter.count() + 1).unwrap();
328            break;
329        } else if i > 0 {
330            overview += ", ";
331        }
332        overview += &value.overview();
333    }
334
335    overview
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use serde_json::json;
342
343    fn converts<T: FeattleValue + PartialEq>(value: Value, parsed: T, overview: &str) {
344        converts2(value.clone(), parsed, overview, value);
345    }
346
347    fn converts2<T: FeattleValue + PartialEq>(
348        value: Value,
349        parsed: T,
350        overview: &str,
351        converted: Value,
352    ) {
353        assert_eq!(parsed.as_json(), converted);
354        assert_eq!(parsed.overview(), overview);
355        assert_eq!(T::try_from_json(&value).ok(), Some(parsed));
356    }
357
358    fn fails<T: FeattleValue + PartialEq>(value: Value) {
359        assert_eq!(T::try_from_json(&value).ok(), None);
360    }
361
362    #[test]
363    fn bool() {
364        converts(json!(true), true, "true");
365        converts(json!(false), false, "false");
366
367        fails::<bool>(json!(0));
368        fails::<bool>(json!(null));
369
370        assert_eq!(bool::serialized_format().kind, SerializedFormatKind::Bool);
371    }
372
373    #[test]
374    fn int() {
375        fn basic<T: FeattleValue + PartialEq>(parsed: T) {
376            converts(json!(17), parsed, "17");
377            fails::<T>(json!(17.5));
378            fails::<T>(json!(null));
379            assert_eq!(T::serialized_format().kind, SerializedFormatKind::Integer);
380        }
381
382        basic(17u8);
383        basic(17i8);
384        basic(17u16);
385        basic(17i16);
386        basic(17u32);
387        basic(17i32);
388        basic(17u64);
389        basic(17i64);
390        basic(17usize);
391        basic(17isize);
392
393        fails::<u8>(json!(-17));
394        converts(json!(-17), -17i8, "-17");
395        fails::<u16>(json!(-17));
396        converts(json!(-17), -17i16, "-17");
397        fails::<u32>(json!(-17));
398        converts(json!(-17), -17i32, "-17");
399        fails::<u64>(json!(-17));
400        converts(json!(-17), -17i64, "-17");
401        fails::<usize>(json!(-17));
402        converts(json!(-17), -17isize, "-17");
403
404        let overview = u32::MAX.to_string();
405        fails::<u8>(json!(u32::MAX));
406        fails::<i8>(json!(u32::MAX));
407        fails::<u16>(json!(u32::MAX));
408        fails::<i16>(json!(u32::MAX));
409        converts(json!(u32::MAX), u32::MAX, &overview);
410        fails::<i32>(json!(u32::MAX));
411        converts(json!(u32::MAX), u32::MAX as u64, &overview);
412        converts(json!(u32::MAX), u32::MAX as i64, &overview);
413        converts(json!(u32::MAX), u32::MAX as usize, &overview);
414        converts(json!(u32::MAX), u32::MAX as isize, &overview);
415    }
416
417    #[test]
418    fn float() {
419        converts2(json!(17), 17f32, "17", json!(17.0));
420        converts2(json!(17), 17f64, "17", json!(17.0));
421        converts(json!(17.5), 17.5f32, "17.5");
422        converts(json!(17.5), 17.5f64, "17.5");
423
424        fails::<bool>(json!(null));
425
426        assert_eq!(f32::serialized_format().kind, SerializedFormatKind::Float);
427        assert_eq!(f64::serialized_format().kind, SerializedFormatKind::Float);
428    }
429
430    #[test]
431    #[cfg(feature = "uuid")]
432    fn uuid() {
433        converts(
434            json!("8886fc87-93e1-4d08-9722-9fc1411b6b96"),
435            Uuid::parse_str("8886fc87-93e1-4d08-9722-9fc1411b6b96").unwrap(),
436            "8886fc87-93e1-4d08-9722-9fc1411b6b96",
437        );
438
439        fails::<Uuid>(json!("yadayada"));
440        let kind = Uuid::serialized_format().kind;
441        match kind {
442            SerializedFormatKind::String(StringFormatKind::Pattern(_)) => {}
443            _ => panic!("invalid serialized format kind: {:?}", kind),
444        }
445    }
446
447    #[test]
448    fn string() {
449        converts(json!("17"), "17".to_owned(), "17");
450        converts(json!(""), "".to_owned(), "");
451        fails::<String>(json!(17));
452        fails::<String>(json!(null));
453        assert_eq!(
454            String::serialized_format().kind,
455            SerializedFormatKind::String(StringFormatKind::Any)
456        );
457    }
458
459    #[test]
460    fn vec() {
461        converts(json!([3, 14, 15]), vec![3i32, 14, 15], "[3, 14, 15]");
462        converts(
463            json!([3, 14, 15, 92]),
464            vec![3i32, 14, 15, 92],
465            "[3, 14, 15, ... 1 more]",
466        );
467        converts(
468            json!([3, 14, 15, 92, 65, 35]),
469            vec![3i32, 14, 15, 92, 65, 35],
470            "[3, 14, 15, ... 3 more]",
471        );
472        fails::<Vec<i32>>(json!([3, 14, "15", 92]));
473        assert_eq!(
474            Vec::<i32>::serialized_format().kind,
475            SerializedFormatKind::List(Box::new(SerializedFormatKind::Integer))
476        )
477    }
478
479    #[test]
480    fn set() {
481        converts(
482            json!([3, 14, 15]),
483            vec![3, 14, 15].into_iter().collect::<BTreeSet<i32>>(),
484            "[3, 14, 15]",
485        );
486        converts2(
487            json!([1, 2, 4, 4, 3]),
488            vec![1, 2, 3, 4].into_iter().collect::<BTreeSet<i32>>(),
489            "[1, 2, 3, ... 1 more]",
490            json!([1, 2, 3, 4]),
491        );
492        fails::<BTreeSet<i32>>(json!([3, 14, "15", 92]));
493        assert_eq!(
494            BTreeSet::<i32>::serialized_format().kind,
495            SerializedFormatKind::Set(Box::new(SerializedFormatKind::Integer))
496        )
497    }
498
499    #[test]
500    fn map() {
501        converts(
502            json!({
503                "a": 1,
504                "b": 2,
505                "x": 1,
506            }),
507            vec![
508                ("a".to_owned(), 1),
509                ("b".to_owned(), 2),
510                ("x".to_owned(), 1),
511            ]
512            .into_iter()
513            .collect::<BTreeMap<_, _>>(),
514            "{a, x: 1, b: 2}",
515        );
516        fails::<BTreeMap<String, String>>(json!({
517            "a": "1",
518            "b": 2,
519            "x": 1,
520        }));
521        assert_eq!(
522            BTreeMap::<String, i32>::serialized_format().kind,
523            SerializedFormatKind::Map(
524                StringFormatKind::Any,
525                Box::new(SerializedFormatKind::Integer)
526            )
527        )
528    }
529
530    #[test]
531    fn option() {
532        converts(json!(17), Some(17), "Some(17)");
533        converts(json!(null), None::<i32>, "None");
534        fails::<Option<i32>>(json!(17.5));
535        assert_eq!(
536            Option::<i32>::serialized_format().kind,
537            SerializedFormatKind::Optional(Box::new(SerializedFormatKind::Integer))
538        )
539    }
540
541    #[test]
542    fn choices() {
543        use crate::feattle_enum;
544        feattle_enum! {enum Choices { Red, Green, Blue }};
545
546        converts(json!("Red"), Choices::Red, "Red");
547        fails::<Choices>(json!("Black"));
548        assert_eq!(
549            Choices::serialized_format().kind,
550            SerializedFormatKind::String(StringFormatKind::Choices(&["Red", "Green", "Blue"]))
551        )
552    }
553}