Skip to content

Add initial support for fat enums (internally-tagged representation) #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 124 additions & 43 deletions dynomite-derive/src/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@ use syn::{
Ident, LitStr, Token,
};

/// Represents a parsed attribute that appears in `#[dynomite(...)]`.
#[derive(Clone)]
pub(crate) struct Attr {
pub(crate) struct Attr<Kind> {
/// The identifier part of the attribute (e.g. `rename` in `#[dynomite(rename = "foo"`))
pub(crate) ident: Ident,
pub(crate) kind: AttrKind,
/// More specific information about the metadata entry.
pub(crate) kind: Kind,
}

/// Attribute that appears on record fields (struct fields and enum record variant fields)
pub(crate) type FieldAttr = Attr<FieldAttrKind>;
/// Attribute that appears on the top level of an enum
pub(crate) type EnumAttr = Attr<EnumAttrKind>;
/// Attribute that appears on enum varinats
pub(crate) type VariantAttr = Attr<VariantAttrKind>;

#[derive(Clone)]
pub(crate) enum AttrKind {
pub(crate) enum FieldAttrKind {
/// Denotes field should be replaced with Default impl when absent in ddb
Default,
/// Denotes field should be renamed to value of ListStr
Expand All @@ -26,51 +36,122 @@ pub(crate) enum AttrKind {
Flatten,
}

impl Attr {
fn new(
ident: Ident,
kind: AttrKind,
) -> Self {
Self { ident, kind }
impl DynomiteAttr for FieldAttrKind {
const KVS: Kvs<Self> = &[("rename", FieldAttrKind::Rename)];
const KEYS: Keys<Self> = &[
("default", FieldAttrKind::Default),
("partition_key", FieldAttrKind::PartitionKey),
("sort_key", FieldAttrKind::SortKey),
("flatten", FieldAttrKind::Flatten),
];
}

#[derive(Clone)]
pub(crate) enum EnumAttrKind {
// FIXME: implement content attribute to support non-map values in enum variants
// (adjacently tagged enums: https://serde.rs/enum-representations.html#adjacently-tagged)
// Content(LitStr),
/// The name of the tag field for an internally-tagged enum
Tag(LitStr),
}

impl DynomiteAttr for EnumAttrKind {
const KVS: Kvs<Self> = &[("tag", EnumAttrKind::Tag)];
}
#[derive(Clone)]
pub(crate) enum VariantAttrKind {
// TODO: add default for enum variants?
Rename(LitStr),
}

impl DynomiteAttr for VariantAttrKind {
const KVS: Kvs<Self> = &[("rename", VariantAttrKind::Rename)];
}

type Kvs<T> = &'static [(&'static str, fn(syn::LitStr) -> T)];
type Keys<T> = &'static [(&'static str, T)];

/// Helper to ease defining `#[dynomite(key)` and `#[dynomite(key = "val")` attributes
pub(crate) trait DynomiteAttr: Clone + Sized + 'static {
/// List of `("attr_name", enum_variant_constructor)` to define attributes
/// that require a value string literal (e.g. `rename = "foo"`)
const KVS: Kvs<Self> = &[];
/// List of `("attr_name", enum_variant_value)` entires to define attributes
/// that should not have any value (e.g. `default` or `flatten`)
const KEYS: Keys<Self> = &[];
}

impl<A: DynomiteAttr> Parse for Attr<A> {
fn parse(input: ParseStream) -> syn::Result<Self> {
let entry: MetadataEntry = input.parse()?;
let kind = entry
.try_attr_with_val(A::KVS)
.or_else(|| entry.try_attr_without_val(A::KEYS))
.unwrap_or_else(|| abort!(entry.key, "unexpected dynomite attribute: {}", entry.key));
Ok(Attr {
ident: entry.key,
kind,
})
}
}

struct MetadataEntry {
key: Ident,
val: Option<LitStr>,
}

impl MetadataEntry {
/// Attempt to map the parsed entry to an identifier-only attribute from the list
fn try_attr_without_val<T: Clone>(
&self,
mappings: Keys<T>,
) -> Option<T> {
let Self { key, val } = self;
let key_str = key.to_string();
mappings
.iter()
.find(|(key_pat, _)| *key_pat == key_str)
.map(|(_, enum_val)| match val {
Some(_) => abort!(key, "expected no value for dynomite attribute `{}`", key),
None => enum_val.clone(),
})
}

/// Attempt to map the parsed entry to a key-value attribute from the list
fn try_attr_with_val<T>(
&self,
mappings: Kvs<T>,
) -> Option<T> {
let Self { key, val } = self;
let key_str = key.to_string();
mappings
.iter()
.find(|(key_pat, _)| *key_pat == key_str)
.map(|(_, to_enum)| match val {
Some(it) => to_enum(it.clone()),
None => abort!(
key,
"expected a value for dynomite attribute: `{} = \"foo\"`",
key
),
})
}
}

impl Parse for Attr {
impl Parse for MetadataEntry {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name: Ident = input.parse()?;
let name_str = name.to_string();
if input.peek(Token![=]) {
// `name = value` attributes.
let assign = input.parse::<Token![=]>()?; // skip '='
if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
match &*name_str {
"rename" => Ok(Attr::new(name, AttrKind::Rename(lit))),
unsupported => abort! {
name,
"unsupported dynomite {} attribute",
unsupported
},
}
} else {
abort! {
assign,
"expected `string literal` after `=`"
};
}
} else if input.peek(syn::token::Paren) {
let key: Ident = input.parse()?;
if input.peek(syn::token::Paren) {
// `name(...)` attributes.
abort!(name, "unexpected dynomite attribute: {}", name_str);
} else {
// Attributes represented with a sole identifier.
let kind = match name_str.as_ref() {
"default" => AttrKind::Default,
"partition_key" => AttrKind::PartitionKey,
"sort_key" => AttrKind::SortKey,
"flatten" => AttrKind::Flatten,
_ => abort!(name, "unexpected dynomite attribute: {}", name_str),
};
Ok(Attr::new(name, kind))
abort!(key, "unexpected paren in dynomite attribute: {}", key);
}
Ok(Self {
key,
val: input
.parse::<Token![=]>()
.ok()
.map(|_| input.parse())
.transpose()?,
})
}
}
Loading