Skip to main content

ocpi_tariffs/
enumeration.rs

1//! Types to parse all OCPI enumerations.
2
3use crate::{json, warning};
4
5/// A type used to deserialize a JSON string value into a structured Rust enum.
6///
7/// The deserialized value may not map to a `Known` variant in the enum and therefore be `Unknown`.
8/// The caller can then decide what to do with the `Unknown` variant.
9#[derive(Clone, Debug)]
10pub(crate) enum Enum<T> {
11    Known(T),
12    Unknown(String),
13}
14
15/// Create an `Enum<T>` from a `&str`.
16///
17/// This is used in conjunction with `FromJson`.
18pub(crate) trait IntoEnum: Sized {
19    fn enum_from_str(s: &str) -> Enum<Self>;
20}
21
22/// A wrapper around `WarningKind` that associates the `enum` name with the warning.
23#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
24pub struct Warning {
25    /// The type name of the enum that raised this warning.
26    type_name: &'static str,
27
28    /// The kind of warning raised.
29    kind: WarningKind,
30}
31
32#[cfg(test)]
33impl Warning {
34    pub(crate) fn type_name(&self) -> &'static str {
35        self.type_name
36    }
37
38    pub(crate) fn kind(&self) -> &WarningKind {
39        &self.kind
40    }
41}
42
43impl std::fmt::Display for Warning {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match &self.kind {
46            WarningKind::ContainsEscapeCodes => write!(
47                f,
48                "The {} value contains escape codes but it does not need them.",
49                self.type_name
50            ),
51            WarningKind::Decode(warning) => std::fmt::Display::fmt(&warning, f),
52            WarningKind::PreferUpperCase => {
53                write!(f, "The `{}` should be uppercase.", self.type_name)
54            }
55            WarningKind::InvalidVariant => {
56                write!(f, "The value is not a valid `{}`.", self.type_name)
57            }
58            WarningKind::InvalidType { type_found } => {
59                write!(
60                    f,
61                    "A {} value should be a string but `{}` was found.",
62                    self.type_name, type_found
63                )
64            }
65        }
66    }
67}
68
69impl crate::Warning for Warning {
70    fn id(&self) -> warning::Id {
71        match &self.kind {
72            WarningKind::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
73            WarningKind::Decode(warning) => warning.id(),
74            WarningKind::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
75            WarningKind::InvalidVariant => warning::Id::from_static("invalid_variant"),
76
77            WarningKind::InvalidType { type_found } => {
78                warning::Id::from_string(format!("invalid_type({type_found})"))
79            }
80        }
81    }
82}
83
84#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
85pub enum WarningKind {
86    /// The enum variant doesn't need to have escape-codes.
87    ContainsEscapeCodes,
88
89    /// The field at the path could not be decoded.
90    Decode(json::decode::Warning),
91
92    /// Each enum should be uppercase.
93    PreferUpperCase,
94
95    /// The value is not a valid.
96    InvalidVariant,
97
98    /// The JSON value given is not a string.
99    InvalidType { type_found: json::ValueKind },
100}
101
102impl WarningKind {
103    pub(crate) fn invalid_type(elem: &json::Element<'_>) -> Self {
104        Self::InvalidType {
105            type_found: elem.value().kind(),
106        }
107    }
108
109    /// Convert a `WarningKind` into a `Warning` by supplying a type name.
110    pub(crate) fn into_warning(self, type_name: &'static str) -> Warning {
111        Warning {
112            type_name,
113            kind: self,
114        }
115    }
116}
117
118/// This macro defines the `FromJson` impl for the given enum.
119///
120/// Note: The `IntoEnum` should already be defined for the enum.
121#[macro_export]
122macro_rules! define_enum_from_json {
123    ($kind:ident, display_name: $name:literal, warning_id: $warning:literal) => {
124        impl $crate::json::FromJson<'_> for $kind {
125            type Warning = $crate::enumeration::Warning;
126
127            fn from_json(elem: &$crate::json::Element<'_>) -> $crate::Verdict<Self, Self::Warning> {
128                use $crate::warning::IntoCaveat as _;
129
130                let value = Enum::<$kind>::from_json(elem)?;
131                let (value, warnings) = value.into_parts();
132
133                let value = match value {
134                    $crate::Enum::Known(v) => v,
135                    $crate::Enum::Unknown(_) => {
136                        return warnings.bail(
137                            $crate::enumeration::WarningKind::InvalidVariant
138                                .into_warning(stringify!($kind)),
139                            elem,
140                        );
141                    }
142                };
143
144                Ok(value.into_caveat(warnings))
145            }
146        }
147
148        impl $crate::json::FromJson<'_> for $crate::Enum<$kind> {
149            type Warning = $crate::enumeration::Warning;
150
151            fn from_json(elem: &$crate::json::Element<'_>) -> $crate::Verdict<Self, Self::Warning> {
152                use $crate::warning::IntoCaveat as _;
153
154                let mut warnings = $crate::warning::Set::new();
155                let value = elem.as_value();
156
157                let Some(s) = value.to_raw_str() else {
158                    return warnings.bail(
159                        $crate::enumeration::WarningKind::invalid_type(elem)
160                            .into_warning(stringify!($kind)),
161                        elem,
162                    );
163                };
164
165                // We can't use `fn gather_warnings_into` here as we want to convert the warnings
166                // using the type name from the macro instead of the `impl From<WarningA> for WarningB`.
167                // This requires a manual conversion from `decode::Warning` to `Warning`.
168                let (s, inner_warnings) = s.has_escapes(elem).into_parts();
169                let inner_warnings =
170                    inner_warnings
171                        .into_inner()
172                        .into_iter()
173                        .map(|(elem_id, group)| {
174                            (
175                                elem_id,
176                                group.map(|w| {
177                                    $crate::enumeration::WarningKind::Decode(w)
178                                        .into_warning(stringify!($kind))
179                                }),
180                            )
181                        });
182
183                warnings.extend(inner_warnings);
184
185                let s = match s {
186                    $crate::json::decode::PendingStr::NoEscapes(s) => s,
187                    $crate::json::decode::PendingStr::HasEscapes(_) => {
188                        return warnings.bail(
189                            $crate::enumeration::WarningKind::ContainsEscapeCodes
190                                .into_warning(stringify!($kind)),
191                            elem,
192                        );
193                    }
194                };
195
196                if !s.chars().all(|c| c.is_uppercase() || c == '_') {
197                    warnings.insert(
198                        $crate::enumeration::WarningKind::PreferUpperCase
199                            .into_warning(stringify!($kind)),
200                        elem,
201                    );
202                }
203
204                let value = $kind::enum_from_str(s);
205
206                Ok(value.into_caveat(warnings))
207            }
208        }
209    };
210}