This is somewhat related to #4031, but I would like full type support for enums, mainly named field variants
enum Birthday {
Custom(String),
// ? currently unsupported
Date {
day: u8,
month: u8,
year: Option<u16>
}
}
as well as tuple variants
enum Birthday {
Date(u8, u8, Option<u16>),
...
}
both currently error with must be a unit variant or a newtype variant
You can get around this by creating a struct and wrapping it as a newtype variant, but for types that will only ever be used as a single enum variant, that's not ideal.
In my testing manually implementing the derive macro, sats seems to support named field variants just fine, and with only that change you can deploy to maincloud without issue, reads and writes all work, only the module codegen step currently fails.
I haven't tested tuple variants, as I don't personally need them, they're just listed here to complete the type support.
you can already do that with
#[derive(SpacetimeType)]
pub struct Date {
day: u8,
month: u8,
year: Option<u16>
}
enum Birthday {
Date(Date),
}
the requirement is just that the custom struct derives SpacetimeType
Yes that was mentioned, but it does require that new struct, Date, be created, imported, and used downstream, any time you need to create the enum, or match on it's contents. In some cases, that would be the singular purpose of the date struct, and it wouldn't be used elsewhere.
Since these types are supported by sats via nesting AlgebraicType::product within AlgebraicType::sum, the macro would only need to silently create that date struct, solely for the purpose of (de)serialization
It is a relatively small thing, but since it's supported by sats, it should probably be implemented eventually just to be feature complete. I'm just unsure of what it would be like to create/codegen these types in other languages, since I mostly only know rust and a little typescript
With the example below, provided you don't try to also generate code, this will deploy to a spacetime server without issue. A codegen attempt will error.
Example, loosely based on the macro expansionuse core::any::TypeId;
use std::marker::PhantomData;
use spacetimedb::{
Serialize,
sats::{
de::{
FieldNameVisitor,
NamedProductAccess,
ProductVisitor,
SeqProductAccess
},
ser::Serializer
},
spacetimedb_lib::{
SpacetimeType,
de::{
Deserialize,
Deserializer,
Error as DeError,
SumAccess,
SumVisitor,
VariantAccess,
VariantVisitor
},
sats::{AlgebraicType, typespace::TypespaceBuilder},
ser::SerializeNamedProduct
}
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Birthday {
Custom(String),
Date {
day: u8,
month: u8,
year: Option<u16>
}
}
const _: () = {
struct __DateDePayload {
day: u8,
month: u8,
year: Option<u16>
}
#[allow(non_camel_case_types)]
enum __DateFieldIdent {
day,
month,
year
}
struct __DateProductVisitor {
_marker: PhantomData<fn() -> __DateDePayload>
}
struct __DateSerPayload<'date> {
day: &'date u8,
month: &'date u8,
year: &'date Option<u16>
}
struct __SumVisitor {
_marker: PhantomData<fn() -> Birthday>
}
enum __Variant {
Custom,
Date
}
impl<'de> Deserialize<'de> for Birthday {
fn deserialize<D: Deserializer<'de>>(
deserializer: D
) -> Result<Self, D::Error> {
deserializer.deserialize_sum(__SumVisitor {
_marker: PhantomData::<fn() -> Self>
})
}
fn validate<D: Deserializer<'de>>(
deserializer: D
) -> Result<(), D::Error> {
deserializer.validate_sum(__SumVisitor {
_marker: PhantomData::<fn() -> Self>
})
}
}
impl Serialize for Birthday {
fn serialize<S: Serializer>(
&self,
__serializer: S
) -> Result<S::Ok, S::Error> {
match self {
Self::Custom(__variant) => __serializer
.serialize_variant::<String>(0, Some("Custom"), __variant),
Self::Date { day, month, year } => __serializer
.serialize_variant(1, Some("Date"), &__DateSerPayload {
day,
month,
year
})
}
}
}
impl SpacetimeType for Birthday {
fn make_type<S: TypespaceBuilder>(
__typespace: &mut S
) -> AlgebraicType {
TypespaceBuilder::add(
__typespace,
TypeId::of::<Self>(),
Some("Birthday"),
|__typespace| {
AlgebraicType::sum::<[(&str, AlgebraicType); 2]>([
(
"Custom",
<String as SpacetimeType>::make_type(__typespace)
),
(
"Date",
AlgebraicType::product::<
[(Option<&str>, AlgebraicType); 3]
>([
(
Some("day"),
<u8 as SpacetimeType>::make_type(
__typespace
)
),
(
Some("month"),
<u8 as SpacetimeType>::make_type(
__typespace
)
),
(
Some("year"),
<Option<u16> as SpacetimeType>::make_type(
__typespace
)
)
])
)
])
}
)
}
}
impl<'de> Deserialize<'de> for __DateDePayload {
fn deserialize<D: Deserializer<'de>>(
deserializer: D
) -> Result<Self, D::Error> {
deserializer.deserialize_product(__DateProductVisitor {
_marker: PhantomData::<fn() -> Self>
})
}
fn validate<D: Deserializer<'de>>(
deserializer: D
) -> Result<(), D::Error> {
deserializer.validate_product(__DateProductVisitor {
_marker: PhantomData::<fn() -> Self>
})
}
}
impl FieldNameVisitor<'_> for __DateProductVisitor {
type Output = __DateFieldIdent;
fn field_names(&self) -> impl '_ + Iterator<Item = Option<&str>> {
["day", "month", "year"].into_iter().map(Some)
}
fn visit<E: DeError>(self, __name: &str) -> Result<Self::Output, E> {
match __name {
"day" => Ok(__DateFieldIdent::day),
"month" => Ok(__DateFieldIdent::month),
"year" => Ok(__DateFieldIdent::year),
_ => Err(DeError::unknown_field_name(__name, &self))
}
}
fn visit_seq(self, __index: usize) -> Self::Output {
match __index {
0 => __DateFieldIdent::day,
1 => __DateFieldIdent::month,
2 => __DateFieldIdent::year,
_ => core::unreachable!()
}
}
}
impl<'de> ProductVisitor<'de> for __DateProductVisitor {
type Output = __DateDePayload;
fn product_len(&self) -> usize {
3
}
fn product_name(&self) -> Option<&str> {
Some("Date")
}
fn validate_named_product<A: NamedProductAccess<'de>>(
self,
mut __prod: A
) -> Result<(), A::Error> {
let mut day = false;
let mut month = false;
let mut year = false;
while let Some(__field) =
NamedProductAccess::get_field_ident(&mut __prod, Self {
_marker: PhantomData
})?
{
match __field {
__DateFieldIdent::day => {
if day {
return Err(DeError::duplicate_field(
0,
Some("day"),
&self
));
}
NamedProductAccess::validate_field_value::<u8>(
&mut __prod
)?;
day = true;
}
__DateFieldIdent::month => {
if month {
return Err(DeError::duplicate_field(
1,
Some("month"),
&self
));
}
NamedProductAccess::validate_field_value::<u8>(
&mut __prod
)?;
month = true;
}
__DateFieldIdent::year => {
if year {
return Err(DeError::duplicate_field(
2,
Some("year"),
&self
));
}
NamedProductAccess::validate_field_value::<Option<u16>>(
&mut __prod
)?;
year = true;
}
}
}
if !day {
return Err(DeError::missing_field(0, Some("day"), &self));
}
if !month {
return Err(DeError::missing_field(1, Some("month"), &self));
}
if !year {
return Err(DeError::missing_field(2, Some("year"), &self));
}
Ok(())
}
fn validate_seq_product<A: SeqProductAccess<'de>>(
self,
mut tup: A
) -> Result<(), A::Error> {
tup.validate_next_element::<u8>()?
.ok_or_else(|| DeError::invalid_product_length(0, &self))?;
tup.validate_next_element::<u8>()?
.ok_or_else(|| DeError::invalid_product_length(1, &self))?;
tup.validate_next_element::<Option<u16>>()?
.ok_or_else(|| DeError::invalid_product_length(2, &self))?;
Ok(())
}
fn visit_named_product<A: NamedProductAccess<'de>>(
self,
mut __prod: A
) -> Result<Self::Output, A::Error> {
let mut day = None;
let mut month = None;
let mut year = None;
while let Some(__field) =
NamedProductAccess::get_field_ident(&mut __prod, Self {
_marker: PhantomData
})?
{
match __field {
__DateFieldIdent::day => {
if day.is_some() {
return Err(DeError::duplicate_field(
0,
Some("day"),
&self
));
}
day = Some(NamedProductAccess::get_field_value::<u8>(
&mut __prod
)?);
}
__DateFieldIdent::month => {
if month.is_some() {
return Err(DeError::duplicate_field(
1,
Some("month"),
&self
));
}
month =
Some(NamedProductAccess::get_field_value::<u8>(
&mut __prod
)?);
}
__DateFieldIdent::year => {
if year.is_some() {
return Err(DeError::duplicate_field(
2,
Some("year"),
&self
));
}
year = Some(NamedProductAccess::get_field_value::<
Option<u16>
>(&mut __prod)?);
}
}
}
Ok(__DateDePayload {
day: day.ok_or_else(|| {
DeError::missing_field(0, Some("day"), &self)
})?,
month: month.ok_or_else(|| {
DeError::missing_field(1, Some("month"), &self)
})?,
year: year.ok_or_else(|| {
DeError::missing_field(2, Some("year"), &self)
})?
})
}
fn visit_seq_product<A: SeqProductAccess<'de>>(
self,
mut tup: A
) -> Result<Self::Output, A::Error> {
Ok(__DateDePayload {
day: tup
.next_element::<u8>()?
.ok_or_else(|| DeError::invalid_product_length(0, &self))?,
month: tup
.next_element::<u8>()?
.ok_or_else(|| DeError::invalid_product_length(1, &self))?,
year: tup
.next_element::<Option<u16>>()?
.ok_or_else(|| DeError::invalid_product_length(2, &self))?
})
}
}
impl Serialize for __DateSerPayload<'_> {
fn serialize<S: Serializer>(
&self,
__serializer: S
) -> Result<S::Ok, S::Error> {
let mut __prod = __serializer.serialize_named_product(3)?;
__prod.serialize_element::<u8>(Some("day"), self.day)?;
__prod.serialize_element::<u8>(Some("month"), self.month)?;
__prod.serialize_element::<Option<u16>>(Some("year"), self.year)?;
__prod.end()
}
}
impl<'de> SumVisitor<'de> for __SumVisitor {
type Output = Birthday;
fn sum_name(&self) -> Option<&str> {
Some("Birthday")
}
fn validate_sum<A: SumAccess<'de>>(
self,
__data: A
) -> Result<(), A::Error> {
let (__variant, __access) = __data.variant(self)?;
match __variant {
__Variant::Custom => {
VariantAccess::validate::<String>(__access)?;
}
__Variant::Date => {
VariantAccess::validate::<__DateDePayload>(__access)?;
}
}
Ok(())
}
fn visit_sum<A: SumAccess<'de>>(
self,
__data: A
) -> Result<Self::Output, A::Error> {
let (__variant, __access) = __data.variant(self)?;
match __variant {
__Variant::Custom => Ok(Birthday::Custom(
VariantAccess::deserialize::<String>(__access)?
)),
__Variant::Date => {
let __payload = VariantAccess::deserialize::<
__DateDePayload
>(__access)?;
Ok(Birthday::Date {
day: __payload.day,
month: __payload.month,
year: __payload.year
})
}
}
}
}
impl VariantVisitor<'_> for __SumVisitor {
type Output = __Variant;
fn variant_names(&self) -> impl '_ + Iterator<Item = &str> {
["Custom", "Date"].into_iter()
}
fn visit_name<E: DeError>(
self,
__name: &str
) -> Result<Self::Output, E> {
match __name {
"Custom" => Ok(__Variant::Custom),
"Date" => Ok(__Variant::Date),
_ => Err(DeError::unknown_variant_name(__name, &self))
}
}
fn visit_tag<E: DeError>(self, __tag: u8) -> Result<Self::Output, E> {
match __tag {
0 => Ok(__Variant::Custom),
1 => Ok(__Variant::Date),
_ => Err(DeError::unknown_variant_tag(__tag, &self))
}
}
}
};
spacetimedb::spacetimedb_lib::__make_register_reftype!(Birthday, "Birthday");