use darling::FromMeta;
use inflector::Inflector;
use proc_macro_error::abort;
use quote::quote;
use syn::DeriveInput;
const MODEL_HELPER_ATTR: &str = "model";
const DUPLICATE_ATTR_SPEC: &str = "duplicate attr specification";
const META_MUST_BE_KV_PAIR: &str = "this attribute must be specified as a `key=value` pair";
pub(crate) struct MetaModel<'a> {
ident: &'a syn::Ident,
attrs: &'a [syn::Attribute],
fields: Vec<FieldWithFilteredAttrs<'a>>,
collection_name: Option<String>,
skip_serde_checks: Option<()>,
indexes: Vec<IndexModelTokens>,
pub read_concern: Option<ReadConcern>,
pub write_concern: Option<WriteConcern>,
pub selection_criteria: Option<syn::Path>,
}
impl<'a> MetaModel<'a> {
pub fn new(input: &'a DeriveInput) -> Self {
let ident = &input.ident;
let fields = match &input.data {
syn::Data::Struct(struct_data) => match &struct_data.fields {
syn::Fields::Named(named_fields) => named_fields,
_ => abort!(&input, "wither models must have named fields"),
},
_ => abort!(&input, "only structs can be used as wither models"),
};
let mut inst = Self {
ident,
attrs: input.attrs.as_slice(),
fields: vec![],
indexes: vec![],
collection_name: None,
skip_serde_checks: None,
read_concern: None,
write_concern: None,
selection_criteria: None,
};
inst.extract_model_attrs();
inst.extract_model_fields(fields);
inst.check_id_field();
inst
}
pub fn expand(&self) -> proc_macro2::TokenStream {
let name = self.ident;
let collection_name = self.get_collection_name();
let read_concern = OptionReadConcern(&self.read_concern);
let write_concern = OptionWriteConcern(&self.write_concern);
let selection_criteria = OptionSelectionCriteria(&self.selection_criteria);
let indexes = &self.indexes;
quote! {
#[wither::async_trait]
impl wither::Model for #name {
const COLLECTION_NAME: &'static str = #collection_name;
fn id(&self) -> ::std::option::Option<wither::bson::oid::ObjectId> {
self.id.clone()
}
fn set_id(&mut self, oid: wither::bson::oid::ObjectId) {
self.id = Some(oid);
}
fn read_concern() -> Option<wither::mongodb::options::ReadConcern> {
#read_concern
}
fn write_concern() -> Option<wither::mongodb::options::WriteConcern> {
#write_concern
}
fn selection_criteria() -> Option<wither::mongodb::options::SelectionCriteria> {
#selection_criteria
}
fn indexes() -> Vec<wither::IndexModel> {
vec![#(#indexes),*]
}
}
}
}
fn extract_model_attrs(&mut self) {
let attrs = Self::parse_attrs(&self.attrs, MODEL_HELPER_ATTR);
for attr_meta in attrs {
let ident = attr_meta
.path()
.get_ident()
.unwrap_or_else(|| abort!(attr_meta, "malformed wither model attribute, please review the wither docs"));
let ident_str = ident.to_string();
match ident_str.as_str() {
"collection_name" => self.extract_collection_name(&attr_meta),
"index" => self.extract_index(&attr_meta),
"read_concern" => self.extract_read_concern(&attr_meta),
"selection_criteria" => self.extract_selection_criteria(&attr_meta),
"skip_serde_checks" => self.extract_skip_serde_checks(&attr_meta),
"write_concern" => self.extract_write_concern(&attr_meta),
_ => abort!(ident, "unrecognized wither model attribute"),
}
}
}
fn extract_collection_name(&mut self, meta: &syn::Meta) {
let name = match meta {
syn::Meta::NameValue(val) => match &val.lit {
syn::Lit::Str(inner) => inner.value(),
lit => abort!(lit, "this must be a string literal"),
},
_ => abort!(meta, META_MUST_BE_KV_PAIR),
};
if name.is_empty() {
abort!(meta, "wither model collection names must be at least one character in length");
}
if self.collection_name.is_some() {
abort!(meta, DUPLICATE_ATTR_SPEC);
}
self.collection_name = Some(name);
}
fn extract_index(&mut self, meta: &syn::Meta) {
let idx = match RawIndexModel::from_meta(meta) {
Ok(idx) => idx,
Err(err) => abort!(meta, "malformed wither model index specification"; hint=err),
};
self.indexes.push(IndexModelTokens::from(idx));
}
fn extract_read_concern(&mut self, meta: &syn::Meta) {
let rc = match ReadConcern::from_meta(meta) {
Ok(rc) => rc,
Err(err) => abort!(meta, "malformed wither model read concern attribute"; hint=err),
};
if self.read_concern.is_some() {
abort!(meta, DUPLICATE_ATTR_SPEC);
}
self.read_concern = Some(rc);
}
fn extract_selection_criteria(&mut self, meta: &syn::Meta) {
let fnpath = match meta {
syn::Meta::NameValue(val) => match syn::Path::from_value(&val.lit) {
Ok(path) => path,
Err(err) => abort!(val, "this must be a string literal"; hint=err),
},
_ => abort!(meta, META_MUST_BE_KV_PAIR),
};
if self.selection_criteria.is_some() {
abort!(meta, DUPLICATE_ATTR_SPEC);
}
self.selection_criteria = Some(fnpath);
}
fn extract_skip_serde_checks(&mut self, meta: &syn::Meta) {
match meta {
syn::Meta::Path(path) if path.is_ident("skip_serde_checks") => (),
_ => abort!(meta, "this attribute must be specified simply as `#[model(skip_serde_checks)]`"),
}
if self.skip_serde_checks.is_some() {
abort!(meta, DUPLICATE_ATTR_SPEC);
}
self.skip_serde_checks = Some(());
}
fn extract_write_concern(&mut self, meta: &syn::Meta) {
let wc = match WriteConcern::from_meta(meta) {
Ok(wc) => wc,
Err(err) => abort!(meta, "malformed wither model write concern attribute"; hint=err),
};
if self.write_concern.is_some() {
abort!(meta, DUPLICATE_ATTR_SPEC);
}
self.write_concern = Some(wc);
}
fn extract_model_fields(&mut self, fields: &'a syn::FieldsNamed) {
self.fields = fields.named.iter()
.map(|field| {
let serde_attrs = Self::parse_attrs(&field.attrs, "serde");
FieldWithFilteredAttrs{serde_attrs, field}
})
.collect();
}
fn get_collection_name(&self) -> String {
self.collection_name
.as_ref()
.cloned()
.unwrap_or_else(|| self.ident.to_string().to_table_case().to_plural())
}
fn parse_attrs(attrs: &[syn::Attribute], container_name: &str) -> Vec<syn::Meta> {
attrs.iter()
.filter(|attr| attr.path.is_ident(container_name))
.filter_map(|attr| match attr.parse_meta() {
Ok(meta) => Some(meta),
Err(err) => abort!(attr, "malformed attribute"; hint=err),
})
.map(|meta| match meta {
syn::Meta::List(inner) => inner.nested,
_ => abort!(meta, format!("wither expected this attribute to be formatted as a meta list, eg: `#[{}(...)]`", container_name)),
})
.fold(vec![], |mut acc, nested| {
for inner in nested {
match inner {
syn::NestedMeta::Meta(meta) => acc.push(meta),
syn::NestedMeta::Lit(lit) => abort!(lit, "unexpected literal value"),
}
}
acc
})
}
fn check_id_field(&self) {
let id_field = self
.fields
.iter()
.find(|field| match &field.field.ident {
Some(ident) => ident == "id",
None => false,
})
.unwrap_or_else(|| abort!(self.ident, "wither models must have a field `id` of type `Option<bson::oid::ObjectId>`"));
if self.skip_serde_checks.is_none() {
self.check_id_serde_attrs(id_field);
}
}
fn check_id_serde_attrs(&self, id_field: &FieldWithFilteredAttrs<'a>) {
let mut found_rename = false;
let mut found_skip = false;
for attr in &id_field.serde_attrs {
if attr.path().is_ident("rename") {
let model = SerdeIdRename::from_meta(attr).unwrap_or_else(|err| abort!(attr, "failed to parse serde rename attr"; hint=err));
if model.0 != "_id" {
abort!(attr, r#"the serde `rename` attr for wither::Model ID fields should be `rename="_id"`"#);
}
found_rename = true;
}
if attr.path().is_ident("skip_serializing_if") {
let model =
SerdeIdSkip::from_meta(attr).unwrap_or_else(|err| abort!(attr, "failed to parse serde skip_serializing_if attr"; hint=err));
if model.0 != "Option::is_none" {
abort!(
attr,
r#"the serde `skip_serializing_if` attr for wither::Model ID fields should be `skip_serializing_if="Option::is_none"`"#
);
}
found_skip = true
}
if found_rename && found_skip {
break;
}
}
if !(found_rename && found_skip) {
abort!(
id_field.field.ident,
r#"the ID field of wither::Models must have the attribute `#[serde(rename="_id", skip_serializing_if="Option::is_none")]`"#
)
}
}
}
pub struct FieldWithFilteredAttrs<'a> {
serde_attrs: Vec<syn::Meta>,
field: &'a syn::Field,
}
#[derive(FromMeta)]
pub struct SerdeIdRename(pub String);
#[derive(FromMeta)]
pub struct SerdeIdSkip(pub String);
#[derive(FromMeta)]
pub enum ReadConcern {
Local,
Majority,
Linearizable,
Available,
Custom(String),
}
pub struct OptionReadConcern<'a>(&'a Option<ReadConcern>);
impl quote::ToTokens for OptionReadConcern<'_> {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self.0 {
None => tokens.extend(quote!(None)),
Some(ReadConcern::Local) => tokens.extend(quote!(Some(wither::mongodb::options::ReadConcern::local()))),
Some(ReadConcern::Majority) => tokens.extend(quote!(Some(wither::mongodb::options::ReadConcern::majority()))),
Some(ReadConcern::Linearizable) => tokens.extend(quote!(Some(wither::mongodb::options::ReadConcern::linearizable()))),
Some(ReadConcern::Available) => tokens.extend(quote!(Some(wither::mongodb::options::ReadConcern::available()))),
Some(ReadConcern::Custom(val)) => tokens.extend(quote!(Some(wither::mongodb::options::ReadConcern::custom(String::from(#val))))),
}
}
}
#[derive(FromMeta)]
pub struct WriteConcern {
#[darling(default)]
pub w: Option<Acknowledgment>,
#[darling(default)]
pub w_timeout: Option<u64>,
#[darling(default)]
pub journal: Option<bool>,
}
#[derive(FromMeta)]
pub enum Acknowledgment {
Nodes(i32),
Majority,
Custom(String),
}
pub struct OptionWriteConcern<'a>(&'a Option<WriteConcern>);
impl quote::ToTokens for OptionWriteConcern<'_> {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self.0 {
None => tokens.extend(quote!(None)),
Some(wc) => {
let w = match &wc.w {
Some(ack) => match ack {
Acknowledgment::Nodes(val) => quote!(Some(wither::mongodb::options::Acknowledgment::Nodes(#val))),
Acknowledgment::Majority => quote!(Some(wither::mongodb::options::Acknowledgment::Majority)),
Acknowledgment::Custom(val) => quote!(Some(wither::mongodb::options::Acknowledgment::Custom(String::from(#val)))),
},
None => quote!(None),
};
let w_timeout = match &wc.w_timeout {
Some(val) => quote!(Some(::std::time::Duration::from_secs(#val))),
None => quote!(None),
};
let journal = match &wc.journal {
Some(val) => quote!(Some(#val)),
None => quote!(None),
};
tokens.extend(quote!(Some(wither::mongodb::options::WriteConcern::builder().w(#w).w_timeout(#w_timeout).journal(#journal).build())));
}
}
}
}
pub struct OptionSelectionCriteria<'a>(&'a Option<syn::Path>);
impl quote::ToTokens for OptionSelectionCriteria<'_> {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self.0 {
None => tokens.extend(quote!(None)),
Some(path) => tokens.extend(quote!(Some(#path()))),
}
}
}
#[derive(Debug, FromMeta)]
pub struct RawIndexModel {
pub keys: darling::util::SpannedValue<String>,
#[darling(default)]
pub options: darling::util::SpannedValue<Option<String>>,
}
impl From<RawIndexModel> for IndexModelTokens {
fn from(src: RawIndexModel) -> Self {
let keys = syn::parse_str(&src.keys).unwrap_or_else(|err| abort!(src.keys.span(), "error parsing keys, must be valid Rust code"; hint=err));
let options = src.options.as_ref().as_ref().map(|opts| {
syn::parse_str(opts.as_ref()).unwrap_or_else(|err| abort!(src.options.span(), "error parsing options, must be valid Rust code"; hint=err))
});
Self { keys, options }
}
}
pub struct IndexModelTokens {
pub keys: proc_macro2::TokenStream,
pub options: Option<proc_macro2::TokenStream>,
}
impl quote::ToTokens for IndexModelTokens {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let options = match &self.options {
Some(opts) => quote!(Some(#opts)),
None => quote!(None),
};
let keys = &self.keys;
tokens.extend(quote!(wither::IndexModel::new(#keys, #options)));
}
}