1#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct UnknownVariant {
24 enum_name: &'static str,
25 value: String,
26 allowed: &'static [&'static str],
27}
28
29impl UnknownVariant {
30 #[must_use]
32 pub fn new(enum_name: &'static str, value: impl Into<String>, allowed: &'static [&'static str]) -> Self {
33 Self { enum_name, value: value.into(), allowed }
34 }
35
36 #[must_use]
38 pub fn value(&self) -> &str {
39 &self.value
40 }
41
42 #[must_use]
44 pub const fn enum_name(&self) -> &'static str {
45 self.enum_name
46 }
47
48 #[must_use]
50 pub const fn allowed(&self) -> &'static [&'static str] {
51 self.allowed
52 }
53}
54
55impl core::fmt::Display for UnknownVariant {
56 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57 write!(f, "{:?} is not a valid {}; expected one of ", self.value, self.enum_name)?;
58 for (i, a) in self.allowed.iter().enumerate() {
59 if i > 0 {
60 f.write_str(", ")?;
61 }
62 write!(f, "{a}")?;
63 }
64 Ok(())
65 }
66}
67
68impl std::error::Error for UnknownVariant {}
69
70#[macro_export]
96macro_rules! ocpi_enum {
97 (
98 $(#[$meta:meta])*
99 $vis:vis enum $name:ident {
100 $(
101 $(#[$vmeta:meta])*
102 $variant:ident = $wire:literal
103 ),* $(,)?
104 }
105 ) => {
106 $(#[$meta])*
107 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
108 $vis enum $name {
109 $(
110 $(#[$vmeta])*
111 #[doc = concat!("\n\nWire value: `", $wire, "`")]
112 $variant,
113 )*
114 }
115
116 impl $name {
117 pub const ALL: &'static [Self] = &[ $( Self::$variant ),* ];
119 pub const ALL_WIRE: &'static [&'static str] = &[ $( $wire ),* ];
121
122 #[must_use]
124 pub const fn as_str(&self) -> &'static str {
125 match self { $( Self::$variant => $wire, )* }
126 }
127
128 #[must_use]
133 pub fn from_str_ignore_case(s: &str) -> Option<Self> {
134 $( if s.eq_ignore_ascii_case($wire) { return Some(Self::$variant); } )*
135 None
136 }
137 }
138
139 impl core::fmt::Display for $name {
140 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141 f.write_str(self.as_str())
142 }
143 }
144
145 impl core::str::FromStr for $name {
146 type Err = $crate::types::UnknownVariant;
147 fn from_str(s: &str) -> Result<Self, Self::Err> {
148 match s {
149 $( $wire => Ok(Self::$variant), )*
150 other => Err($crate::types::UnknownVariant::new(
151 stringify!($name), other, Self::ALL_WIRE,
152 )),
153 }
154 }
155 }
156
157 impl $crate::types::Validate for $name {
158 fn validate_in(&self, _v: &mut $crate::types::Validator) {}
159 }
160
161 impl serde::Serialize for $name {
162 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
163 s.serialize_str(self.as_str())
164 }
165 }
166
167 impl<'de> serde::Deserialize<'de> for $name {
168 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
169 struct V;
170 impl serde::de::Visitor<'_> for V {
171 type Value = $name;
172 fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
173 write!(f, "one of the {} values of {}", $name::ALL_WIRE.len(), stringify!($name))
174 }
175 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<$name, E> {
176 <$name as core::str::FromStr>::from_str(v).map_err(E::custom)
177 }
178 }
179 d.deserialize_str(V)
180 }
181 }
182
183 #[cfg(feature = "schema")]
184 impl schemars::JsonSchema for $name {
185 fn schema_name() -> std::borrow::Cow<'static, str> { stringify!($name).into() }
186 fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
187 schemars::json_schema!({ "type": "string", "enum": Self::ALL_WIRE })
188 }
189 }
190 };
191}
192
193#[macro_export]
215macro_rules! ocpi_open_enum {
216 (
217 $(#[$meta:meta])*
218 $vis:vis enum $name:ident {
219 $(
220 $(#[$vmeta:meta])*
221 $variant:ident = $wire:literal
222 ),* $(,)?
223 }
224 ) => {
225 $crate::__ocpi_open_enum_impl! {
226 @policy $crate::types::validate_open_enum_value;
227 $(#[$meta])*
228 $vis enum $name { $( $(#[$vmeta])* $variant = $wire, )* }
233 }
234 };
235}
236
237#[macro_export]
251macro_rules! ocpi_lenient_enum {
252 (
253 $(#[$meta:meta])*
254 $vis:vis enum $name:ident {
255 $(
256 $(#[$vmeta:meta])*
257 $variant:ident = $wire:literal
258 ),* $(,)?
259 }
260 ) => {
261 $crate::__ocpi_open_enum_impl! {
262 @policy $crate::types::validate_closed_enum_value;
263 $(#[$meta])*
264 $vis enum $name { $( $(#[$vmeta])* $variant = $wire, )* }
269 }
270 };
271}
272
273#[doc(hidden)]
274#[macro_export]
275macro_rules! __ocpi_open_enum_impl {
276 (
277 @policy $policy:path;
278 $(#[$meta:meta])*
279 $vis:vis enum $name:ident {
280 $(
281 $(#[$vmeta:meta])*
282 $variant:ident = $wire:literal
283 ),* $(,)?
284 }
285 ) => {
286 $(#[$meta])*
287 #[derive(Clone, Debug)]
288 #[non_exhaustive]
289 $vis enum $name {
290 $(
291 $(#[$vmeta])*
292 #[doc = concat!("\n\nWire value: `", $wire, "`")]
293 $variant,
294 )*
295 Custom(String),
300 }
301
302 impl $name {
303 pub const ALL_KNOWN: &'static [Self] = &[ $( Self::$variant ),* ];
305 pub const ALL_KNOWN_WIRE: &'static [&'static str] = &[ $( $wire ),* ];
307
308 #[must_use]
310 pub fn as_str(&self) -> &str {
311 match self {
312 $( Self::$variant => $wire, )*
313 Self::Custom(v) => v.as_str(),
314 }
315 }
316
317 #[must_use]
319 pub const fn is_known(&self) -> bool {
320 !matches!(self, Self::Custom(_))
321 }
322
323 #[must_use]
327 pub fn from_str_ignore_case(s: &str) -> Self {
328 $( if s.eq_ignore_ascii_case($wire) { return Self::$variant; } )*
329 Self::Custom(s.to_owned())
330 }
331 }
332
333 impl core::fmt::Display for $name {
334 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
335 f.write_str(self.as_str())
336 }
337 }
338
339 impl core::str::FromStr for $name {
340 type Err = core::convert::Infallible;
342 fn from_str(s: &str) -> Result<Self, Self::Err> {
343 Ok(match s {
344 $( $wire => Self::$variant, )*
345 other => Self::Custom(other.to_owned()),
346 })
347 }
348 }
349
350 impl From<&str> for $name {
351 fn from(s: &str) -> Self {
352 <Self as core::str::FromStr>::from_str(s).unwrap_or_else(|e| match e {})
353 }
354 }
355
356 impl PartialEq for $name {
359 fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() }
360 }
361 impl Eq for $name {}
362 impl PartialOrd for $name {
363 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> { Some(self.cmp(other)) }
364 }
365 impl Ord for $name {
366 fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.as_str().cmp(other.as_str()) }
367 }
368 impl core::hash::Hash for $name {
369 fn hash<H: core::hash::Hasher>(&self, state: &mut H) { self.as_str().hash(state); }
370 }
371
372 impl $crate::types::Validate for $name {
373 fn validate_in(&self, v: &mut $crate::types::Validator) {
374 if let Self::Custom(value) = self {
375 $policy(stringify!($name), value, v);
376 }
377 }
378 }
379
380 impl serde::Serialize for $name {
381 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
382 s.serialize_str(self.as_str())
383 }
384 }
385
386 impl<'de> serde::Deserialize<'de> for $name {
387 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
388 struct V;
389 impl serde::de::Visitor<'_> for V {
390 type Value = $name;
391 fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
392 write!(f, "a {} value", stringify!($name))
393 }
394 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<$name, E> {
395 Ok(<$name as core::convert::From<&str>>::from(v))
396 }
397 }
398 d.deserialize_str(V)
399 }
400 }
401
402 #[cfg(feature = "schema")]
403 impl schemars::JsonSchema for $name {
404 fn schema_name() -> std::borrow::Cow<'static, str> { stringify!($name).into() }
405 fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
406 schemars::json_schema!({ "type": "string", "examples": Self::ALL_KNOWN_WIRE })
408 }
409 }
410 };
411}
412
413#[doc(hidden)]
418pub fn validate_closed_enum_value(enum_name: &'static str, value: &str, v: &mut super::validate::Validator) {
419 use super::validate::ViolationCode;
420 validate_open_enum_value(enum_name, value, v);
421 v.report(
422 ViolationCode::Inconsistent,
423 format!(
424 "{value:?} is not one of the values this version of the specification defines for \
425 {enum_name}, which it declares as a closed enum; the value was kept rather than \
426 dropped, but a conformant peer would not have sent it"
427 ),
428 );
429}
430
431#[doc(hidden)]
439pub fn validate_open_enum_value(enum_name: &'static str, value: &str, v: &mut super::validate::Validator) {
440 use super::validate::ViolationCode;
441 if value.is_empty() {
442 v.report(ViolationCode::IllegalCharacter, format!("{enum_name} value is empty"));
443 return;
444 }
445 if value.chars().any(char::is_control) {
446 v.report(
447 ViolationCode::IllegalCharacter,
448 format!("{enum_name} value {value:?} contains a control character"),
449 );
450 }
451}
452
453#[cfg(test)]
454#[allow(dead_code, reason = "the generated enums expose more API than each test exercises")]
455mod tests {
456 use crate::types::Validate;
457 use core::str::FromStr;
458
459 crate::ocpi_enum! {
460 pub enum Closed {
462 Alpha = "ALPHA",
464 Beta = "BETA",
466 }
467 }
468
469 crate::ocpi_open_enum! {
470 pub enum Open {
472 Alpha = "ALPHA",
474 }
475 }
476
477 #[test]
478 fn closed_enum_rejects_unknown_values() {
479 assert_eq!(Closed::from_str("ALPHA").unwrap(), Closed::Alpha);
480 let err = serde_json::from_str::<Closed>("\"GAMMA\"").unwrap_err().to_string();
481 assert!(err.contains("GAMMA") && err.contains("ALPHA"), "{err}");
482 assert_eq!(Closed::ALL.len(), 2);
483 }
484
485 #[test]
486 fn open_enum_preserves_unknown_values_verbatim() {
487 let v: Open = serde_json::from_str("\"nltnm-CUSTOM\"").unwrap();
488 assert!(!v.is_known());
489 assert_eq!(serde_json::to_string(&v).unwrap(), "\"nltnm-CUSTOM\"");
490 }
491
492 #[test]
493 fn open_enum_equality_goes_through_the_wire_value() {
494 use std::collections::HashSet;
495 assert_eq!(Open::Custom("ALPHA".into()), Open::Alpha);
496 let mut set = HashSet::new();
497 set.insert(Open::Custom("ALPHA".into()));
498 assert!(set.contains(&Open::Alpha), "Hash must agree with Eq");
499 }
500
501 #[test]
502 fn case_insensitive_parsing_is_opt_in() {
503 assert_eq!(Open::from_str("alpha").unwrap(), Open::Custom("alpha".into()));
504 assert_eq!(Open::from_str_ignore_case("alpha"), Open::Alpha);
505 assert_eq!(Closed::from_str_ignore_case("beta"), Some(Closed::Beta));
506 }
507
508 crate::ocpi_lenient_enum! {
509 pub enum ClosedInSpec {
511 Alpha = "ALPHA",
513 }
514 }
515
516 #[test]
517 fn a_closed_in_spec_enum_decodes_an_unknown_value_and_reports_it() {
518 let v: ClosedInSpec = serde_json::from_str("\"MCS\"").unwrap();
519 assert!(!v.is_known());
520 assert_eq!(serde_json::to_string(&v).unwrap(), "\"MCS\"");
522 let err = v.validate().unwrap_err();
524 assert_eq!(err.as_slice()[0].code, crate::types::ViolationCode::Inconsistent);
525 assert!(ClosedInSpec::Alpha.validate().is_ok());
526 }
527
528 #[test]
529 fn open_enum_other_payload_is_validated() {
530 assert!(Open::Custom("fine".into()).validate().is_ok());
531 assert!(Open::Custom(String::new()).validate().is_err());
532 assert!(Open::Custom("bad\nvalue".into()).validate().is_err());
533 }
534}