Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add support for internally-tagged fat enums
  • Loading branch information
Veetaha committed Sep 12, 2020
1 parent 3c998ac commit 4d7f9e6
Show file tree
Hide file tree
Showing 12 changed files with 599 additions and 80 deletions.
167 changes: 124 additions & 43 deletions dynomite-derive/src/attr.rs
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 {
/// The name of the tag field for an internally-tagged enum
Tag(LitStr),
/* 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), */
}

impl DynomiteAttr for EnumAttrKind {
const KVS: Kvs<Self> = &[("tag", EnumAttrKind::Tag)];
}
#[derive(Clone)]
pub(crate) enum VariantAttrKind {
// TODO: add Default, maybe flatten?
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 mao 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()?,
})
}
}

0 comments on commit 4d7f9e6

Please sign in to comment.