|
| 1 | +//! A new enum representation for serde built on generics. Using it requires some extra attributes and impls. |
| 2 | +//! |
| 3 | +//! This new representation of enum variants makes it much simpler to |
| 4 | +//! deserialize variant from a string and fill in the missing fields using e.g. |
| 5 | +//! [`Default::default`] or to deserialize from a struct which is tagged and |
| 6 | +//! allows overrifing the default values. |
| 7 | +//! |
| 8 | +//! It is built on the `VariantRepr` type. It is not recommended to use this |
| 9 | +//! type directly. Instead for a selected type that appears within a "newtype" |
| 10 | +//! variant of an enum (a variant which wraps a single type) certain traits |
| 11 | +//! should be implemented. |
| 12 | +//! |
| 13 | +//! The traits one should implement before using this module are |
| 14 | +//! - `IsEnumVariant<&str, ENUM>` for `VARIANT`, |
| 15 | +//! - `Into<VariantRepr<&'static str, ENUM, VARIANT>>` for `VARIANT`, |
| 16 | +//! - `TryFrom<VariantRepr<&'static str, ENUM, VARIANT>>` for `VARIANT`, |
| 17 | +//! where __`ENUM`__ is the __enum type__ containing the variant which |
| 18 | +//! serialization we would like to change and __`VARIANT`__ is the type |
| 19 | +//! __wrapped by the variant__. |
| 20 | +//! |
| 21 | +//! Once those are implemented and the module in which this struct resides is |
| 22 | +//! used in serde's attribute as follows: |
| 23 | +//! ```rust,ignore |
| 24 | +//! #[derive(Serialize, Deserialize, JsonSchema)] |
| 25 | +//! #[serde(untagged)] |
| 26 | +//! pub enum Source { |
| 27 | +//! /// `Source` is the __ENUM__ and `GitSource` is the __VARIANT__ type |
| 28 | +//! #[serde(with = "compact_enum_variant")] |
| 29 | +//! #[schemars(schema_with = "compact_enum_variant::schema::<Source, GitSource>")] |
| 30 | +//! Git(GitSource), |
| 31 | +//! } |
| 32 | +//! ``` |
| 33 | +//! |
| 34 | +//! Changing a unit variant of an enum to wrap a type and use this module for |
| 35 | +//! the serialization can be made to be a backwards compatible change. |
| 36 | +
|
| 37 | +use serde::{Deserialize, Deserializer, Serialize, Serializer}; |
| 38 | +use std::{convert::TryFrom, fmt::Display, marker::PhantomData}; |
| 39 | + |
| 40 | +/// Marks a string or other type that can be converted to a string as a label |
| 41 | +/// for an variant of type `ENUM`. |
| 42 | +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] |
| 43 | +#[serde(transparent)] |
| 44 | +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] |
| 45 | +#[cfg_attr(feature = "schema", schemars(bound = "S: schemars::JsonSchema"))] |
| 46 | +pub struct EnumVariant<S: Into<String>, ENUM>(S, PhantomData<fn() -> ENUM>); |
| 47 | + |
| 48 | +/// Establishes a relation with the implementing type and an enum `ENUM`. |
| 49 | +pub trait IsEnumVariant<S: Into<String>, ENUM> { |
| 50 | + /// Returns a label identifying the type as belonging to one of possible |
| 51 | + /// types stored in the enum `ENUM`. |
| 52 | + fn variant() -> EnumVariant<S, ENUM>; |
| 53 | +} |
| 54 | + |
| 55 | +impl<S: Into<String>, E> EnumVariant<S, E> { |
| 56 | + pub fn new(variant_tag: S) -> Self { |
| 57 | + Self(variant_tag, PhantomData) |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +impl<S: Into<String>, E> From<EnumVariant<S, E>> for String { |
| 62 | + fn from(value: EnumVariant<S, E>) -> Self { |
| 63 | + value.0.into() |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +impl<E> From<&'static str> for EnumVariant<&'static str, E> { |
| 68 | + fn from(value: &'static str) -> Self { |
| 69 | + EnumVariant::new(value) |
| 70 | + } |
| 71 | +} |
| 72 | +impl<E> From<&str> for EnumVariant<String, E> { |
| 73 | + fn from(value: &str) -> Self { |
| 74 | + EnumVariant::new(value.to_owned()) |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)] |
| 79 | +#[serde( |
| 80 | + untagged, |
| 81 | + bound( |
| 82 | + serialize = "INNER: Serialize, S: 'static + Serialize", |
| 83 | + deserialize = "INNER: Deserialize<'de>, S: Deserialize<'de>" |
| 84 | + ) |
| 85 | +)] |
| 86 | +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] |
| 87 | +pub enum VariantRepr<S: Into<String>, ENUM, INNER> { |
| 88 | + // Short stringly-typed representation of the enum variant - a label - |
| 89 | + // which assumes all fields are set to their defualts. |
| 90 | + Kind(EnumVariant<S, ENUM>), |
| 91 | + // Longer representation that describes changes to default contents. |
| 92 | + Struct { |
| 93 | + kind: EnumVariant<S, ENUM>, |
| 94 | + #[serde(flatten)] |
| 95 | + strct: INNER, |
| 96 | + }, |
| 97 | +} |
| 98 | + |
| 99 | +pub fn serialize<S, ENUM, VARIANT>(inner: &VARIANT, serializer: S) -> Result<S::Ok, S::Error> |
| 100 | +where |
| 101 | + S: Serializer, |
| 102 | + VARIANT: ToOwned + IsEnumVariant<&'static str, ENUM> + Serialize, |
| 103 | + <VARIANT as ToOwned>::Owned: Into<VariantRepr<&'static str, ENUM, VARIANT>>, |
| 104 | +{ |
| 105 | + let compact: VariantRepr<&'static str, ENUM, VARIANT> = inner.to_owned().into(); |
| 106 | + |
| 107 | + Serialize::serialize(&compact, serializer) |
| 108 | +} |
| 109 | + |
| 110 | +pub fn deserialize<'de, 's, D, ENUM, VARIANT>(deserializer: D) -> Result<VARIANT, D::Error> |
| 111 | +where |
| 112 | + D: Deserializer<'de>, |
| 113 | + VARIANT: IsEnumVariant<&'s str, ENUM> |
| 114 | + + TryFrom<VariantRepr<&'s str, ENUM, VARIANT>> |
| 115 | + + Deserialize<'de>, |
| 116 | + <VARIANT as TryFrom<VariantRepr<&'s str, ENUM, VARIANT>>>::Error: Display, |
| 117 | + 'de: 's, |
| 118 | +{ |
| 119 | + let compact: VariantRepr<&'s str, ENUM, VARIANT> = Deserialize::deserialize(deserializer)?; |
| 120 | + let variant = VARIANT::try_from(compact).map_err(serde::de::Error::custom)?; |
| 121 | + |
| 122 | + Ok(variant) |
| 123 | +} |
| 124 | + |
| 125 | +/// Enriches the schema generated for `VariantRepr` with const values adequate |
| 126 | +/// to the selected variant of an enum. |
| 127 | +#[cfg(feature = "schema")] |
| 128 | +pub fn schema< |
| 129 | + 'a, |
| 130 | + ENUM: schemars::JsonSchema, |
| 131 | + VARIANT: Into<VariantRepr<&'static str, ENUM, VARIANT>> |
| 132 | + + IsEnumVariant<&'a str, ENUM> |
| 133 | + + schemars::JsonSchema, |
| 134 | +>( |
| 135 | + gen: &mut schemars::gen::SchemaGenerator, |
| 136 | +) -> schemars::schema::Schema { |
| 137 | + use schemars::JsonSchema; |
| 138 | + |
| 139 | + let mut schema = |
| 140 | + <VariantRepr<&'static str, ENUM, VARIANT> as JsonSchema>::json_schema(gen).into_object(); |
| 141 | + |
| 142 | + schema |
| 143 | + .subschemas |
| 144 | + .as_mut() |
| 145 | + .and_then(|subschemas| subschemas.any_of.as_mut()) |
| 146 | + .map(|subschemas| { |
| 147 | + let new_subschemas = subschemas.iter_mut().map(|schema| { |
| 148 | + let mut schema = schema.clone().into_object(); |
| 149 | + let typ = &schema |
| 150 | + .instance_type |
| 151 | + .as_ref() |
| 152 | + .and_then(|instance_type| match instance_type { |
| 153 | + schemars::schema::SingleOrVec::Single(typ) => Some(**typ), |
| 154 | + schemars::schema::SingleOrVec::Vec(_) => None, |
| 155 | + }) |
| 156 | + .unwrap(); |
| 157 | + match typ { |
| 158 | + schemars::schema::InstanceType::Object => { |
| 159 | + let object_schema = schema.object(); |
| 160 | + let kind_property = object_schema.properties.get_mut("kind").unwrap(); |
| 161 | + let mut kind_property_object = kind_property.clone().into_object(); |
| 162 | + kind_property_object.const_value = Some(serde_json::Value::String(VARIANT::variant().into())); |
| 163 | + *kind_property = schemars::schema::Schema::Object(kind_property_object); |
| 164 | + |
| 165 | + schemars::schema::Schema::Object(schema) |
| 166 | + }, |
| 167 | + schemars::schema::InstanceType::String => { |
| 168 | + schema.const_value = Some(serde_json::Value::String(VARIANT::variant().into())); |
| 169 | + schema.string = None; |
| 170 | + |
| 171 | + schemars::schema::Schema::Object(schema) |
| 172 | + }, |
| 173 | + _ => panic!("the schema using compact enum variant representation should allow only string or object instances"), |
| 174 | + } |
| 175 | + }).collect(); |
| 176 | + *subschemas = new_subschemas; |
| 177 | + subschemas |
| 178 | + }); |
| 179 | + |
| 180 | + schemars::schema::Schema::Object(schema) |
| 181 | +} |
0 commit comments