#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnknownVariant {
enum_name: &'static str,
value: String,
allowed: &'static [&'static str],
}
impl UnknownVariant {
#[must_use]
pub fn new(enum_name: &'static str, value: impl Into<String>, allowed: &'static [&'static str]) -> Self {
Self { enum_name, value: value.into(), allowed }
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[must_use]
pub const fn enum_name(&self) -> &'static str {
self.enum_name
}
#[must_use]
pub const fn allowed(&self) -> &'static [&'static str] {
self.allowed
}
}
impl core::fmt::Display for UnknownVariant {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?} is not a valid {}; expected one of ", self.value, self.enum_name)?;
for (i, a) in self.allowed.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{a}")?;
}
Ok(())
}
}
impl std::error::Error for UnknownVariant {}
#[macro_export]
macro_rules! ocpi_enum {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$(
$(#[$vmeta:meta])*
$variant:ident = $wire:literal
),* $(,)?
}
) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
$vis enum $name {
$(
$(#[$vmeta])*
#[doc = concat!("\n\nWire value: `", $wire, "`")]
$variant,
)*
}
impl $name {
pub const ALL: &'static [Self] = &[ $( Self::$variant ),* ];
pub const ALL_WIRE: &'static [&'static str] = &[ $( $wire ),* ];
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self { $( Self::$variant => $wire, )* }
}
#[must_use]
pub fn from_str_ignore_case(s: &str) -> Option<Self> {
$( if s.eq_ignore_ascii_case($wire) { return Some(Self::$variant); } )*
None
}
}
impl core::fmt::Display for $name {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl core::str::FromStr for $name {
type Err = $crate::types::UnknownVariant;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$( $wire => Ok(Self::$variant), )*
other => Err($crate::types::UnknownVariant::new(
stringify!($name), other, Self::ALL_WIRE,
)),
}
}
}
impl $crate::types::Validate for $name {
fn validate_in(&self, _v: &mut $crate::types::Validator) {}
}
impl serde::Serialize for $name {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl serde::de::Visitor<'_> for V {
type Value = $name;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "one of the {} values of {}", $name::ALL_WIRE.len(), stringify!($name))
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<$name, E> {
<$name as core::str::FromStr>::from_str(v).map_err(E::custom)
}
}
d.deserialize_str(V)
}
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for $name {
fn schema_name() -> std::borrow::Cow<'static, str> { stringify!($name).into() }
fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "string", "enum": Self::ALL_WIRE })
}
}
};
}
#[macro_export]
macro_rules! ocpi_open_enum {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$(
$(#[$vmeta:meta])*
$variant:ident = $wire:literal
),* $(,)?
}
) => {
$crate::__ocpi_open_enum_impl! {
@policy $crate::types::validate_open_enum_value;
$(#[$meta])*
$vis enum $name { $( $(#[$vmeta])* $variant = $wire, )* }
}
};
}
#[macro_export]
macro_rules! ocpi_lenient_enum {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$(
$(#[$vmeta:meta])*
$variant:ident = $wire:literal
),* $(,)?
}
) => {
$crate::__ocpi_open_enum_impl! {
@policy $crate::types::validate_closed_enum_value;
$(#[$meta])*
$vis enum $name { $( $(#[$vmeta])* $variant = $wire, )* }
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __ocpi_open_enum_impl {
(
@policy $policy:path;
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$(
$(#[$vmeta:meta])*
$variant:ident = $wire:literal
),* $(,)?
}
) => {
$(#[$meta])*
#[derive(Clone, Debug)]
#[non_exhaustive]
$vis enum $name {
$(
$(#[$vmeta])*
#[doc = concat!("\n\nWire value: `", $wire, "`")]
$variant,
)*
Custom(String),
}
impl $name {
pub const ALL_KNOWN: &'static [Self] = &[ $( Self::$variant ),* ];
pub const ALL_KNOWN_WIRE: &'static [&'static str] = &[ $( $wire ),* ];
#[must_use]
pub fn as_str(&self) -> &str {
match self {
$( Self::$variant => $wire, )*
Self::Custom(v) => v.as_str(),
}
}
#[must_use]
pub const fn is_known(&self) -> bool {
!matches!(self, Self::Custom(_))
}
#[must_use]
pub fn from_str_ignore_case(s: &str) -> Self {
$( if s.eq_ignore_ascii_case($wire) { return Self::$variant; } )*
Self::Custom(s.to_owned())
}
}
impl core::fmt::Display for $name {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl core::str::FromStr for $name {
type Err = core::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
$( $wire => Self::$variant, )*
other => Self::Custom(other.to_owned()),
})
}
}
impl From<&str> for $name {
fn from(s: &str) -> Self {
<Self as core::str::FromStr>::from_str(s).unwrap_or_else(|e| match e {})
}
}
impl PartialEq for $name {
fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() }
}
impl Eq for $name {}
impl PartialOrd for $name {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> { Some(self.cmp(other)) }
}
impl Ord for $name {
fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.as_str().cmp(other.as_str()) }
}
impl core::hash::Hash for $name {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) { self.as_str().hash(state); }
}
impl $crate::types::Validate for $name {
fn validate_in(&self, v: &mut $crate::types::Validator) {
if let Self::Custom(value) = self {
$policy(stringify!($name), value, v);
}
}
}
impl serde::Serialize for $name {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl serde::de::Visitor<'_> for V {
type Value = $name;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "a {} value", stringify!($name))
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<$name, E> {
Ok(<$name as core::convert::From<&str>>::from(v))
}
}
d.deserialize_str(V)
}
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for $name {
fn schema_name() -> std::borrow::Cow<'static, str> { stringify!($name).into() }
fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "string", "examples": Self::ALL_KNOWN_WIRE })
}
}
};
}
#[doc(hidden)]
pub fn validate_closed_enum_value(enum_name: &'static str, value: &str, v: &mut super::validate::Validator) {
use super::validate::ViolationCode;
validate_open_enum_value(enum_name, value, v);
v.report(
ViolationCode::Inconsistent,
format!(
"{value:?} is not one of the values this version of the specification defines for \
{enum_name}, which it declares as a closed enum; the value was kept rather than \
dropped, but a conformant peer would not have sent it"
),
);
}
#[doc(hidden)]
pub fn validate_open_enum_value(enum_name: &'static str, value: &str, v: &mut super::validate::Validator) {
use super::validate::ViolationCode;
if value.is_empty() {
v.report(ViolationCode::IllegalCharacter, format!("{enum_name} value is empty"));
return;
}
if value.chars().any(char::is_control) {
v.report(
ViolationCode::IllegalCharacter,
format!("{enum_name} value {value:?} contains a control character"),
);
}
}
#[cfg(test)]
#[allow(dead_code, reason = "the generated enums expose more API than each test exercises")]
mod tests {
use crate::types::Validate;
use core::str::FromStr;
crate::ocpi_enum! {
pub enum Closed {
Alpha = "ALPHA",
Beta = "BETA",
}
}
crate::ocpi_open_enum! {
pub enum Open {
Alpha = "ALPHA",
}
}
#[test]
fn closed_enum_rejects_unknown_values() {
assert_eq!(Closed::from_str("ALPHA").unwrap(), Closed::Alpha);
let err = serde_json::from_str::<Closed>("\"GAMMA\"").unwrap_err().to_string();
assert!(err.contains("GAMMA") && err.contains("ALPHA"), "{err}");
assert_eq!(Closed::ALL.len(), 2);
}
#[test]
fn open_enum_preserves_unknown_values_verbatim() {
let v: Open = serde_json::from_str("\"nltnm-CUSTOM\"").unwrap();
assert!(!v.is_known());
assert_eq!(serde_json::to_string(&v).unwrap(), "\"nltnm-CUSTOM\"");
}
#[test]
fn open_enum_equality_goes_through_the_wire_value() {
use std::collections::HashSet;
assert_eq!(Open::Custom("ALPHA".into()), Open::Alpha);
let mut set = HashSet::new();
set.insert(Open::Custom("ALPHA".into()));
assert!(set.contains(&Open::Alpha), "Hash must agree with Eq");
}
#[test]
fn case_insensitive_parsing_is_opt_in() {
assert_eq!(Open::from_str("alpha").unwrap(), Open::Custom("alpha".into()));
assert_eq!(Open::from_str_ignore_case("alpha"), Open::Alpha);
assert_eq!(Closed::from_str_ignore_case("beta"), Some(Closed::Beta));
}
crate::ocpi_lenient_enum! {
pub enum ClosedInSpec {
Alpha = "ALPHA",
}
}
#[test]
fn a_closed_in_spec_enum_decodes_an_unknown_value_and_reports_it() {
let v: ClosedInSpec = serde_json::from_str("\"MCS\"").unwrap();
assert!(!v.is_known());
assert_eq!(serde_json::to_string(&v).unwrap(), "\"MCS\"");
let err = v.validate().unwrap_err();
assert_eq!(err.as_slice()[0].code, crate::types::ViolationCode::Inconsistent);
assert!(ClosedInSpec::Alpha.validate().is_ok());
}
#[test]
fn open_enum_other_payload_is_validated() {
assert!(Open::Custom("fine".into()).validate().is_ok());
assert!(Open::Custom(String::new()).validate().is_err());
assert!(Open::Custom("bad\nvalue".into()).validate().is_err());
}
}