use proc_macro_error2::{ResultExt as _, abort};
use proc_macro2::{Span, TokenStream};
use crate::{abort_unsupported, parse_repr, reject_duplicate};
pub struct FieldModel {
pub member: syn::Member,
pub doc: String,
pub ty: syn::Type,
pub since: u32,
pub default: Option<syn::Expr>,
pub with: Option<syn::Path>,
pub rename_from: Option<String>,
pub span: Span,
}
pub enum VariantShape {
Unit,
Tuple,
Struct,
}
pub struct VariantFieldModel {
pub name: Option<syn::Ident>,
pub ty: syn::Type,
}
pub struct VariantModel {
pub ident: syn::Ident,
pub doc: String,
pub kind: VariantShape,
pub fields: Vec<VariantFieldModel>,
}
pub struct StructModel {
pub ident: syn::Ident,
pub doc: String,
pub generics: syn::Generics,
pub version: u32,
pub migrate_from: Option<syn::Type>,
pub repr: TokenStream,
pub fields: Vec<FieldModel>,
pub variants: Vec<VariantModel>,
pub removed: Vec<String>,
}
impl StructModel {
pub fn analyze(input: &syn::DeriveInput) -> Self {
match &input.data {
syn::Data::Struct(data) => Self::analyze_struct(input, &data.fields),
syn::Data::Enum(data) => Self::analyze_enum(input, data),
syn::Data::Union(_) => abort_unsupported(input),
}
}
fn analyze_struct(input: &syn::DeriveInput, struct_fields: &syn::Fields) -> Self {
let (version, migrate_from, removed) = parse_struct_attrs(&input.attrs);
let repr = parse_repr(&input.attrs);
let fields: Vec<FieldModel> = struct_fields
.iter()
.enumerate()
.map(|(i, field)| FieldModel::analyze(field, i, version))
.collect();
if !removed.is_empty() && migrate_from.is_none() {
abort!(
input.ident,
"`removed(..)` records fields dropped from a predecessor, but no \
`migrate_from` is declared";
help = "add `#[ix(version = .., migrate_from = PrevType)]`"
);
}
for name in &removed {
if fields.iter().any(|f| f.name_string() == *name) {
abort!(
input.ident,
"`removed(\"{}\")` names a field that is still present",
name;
help = "delete the field, or drop it from `removed(..)`"
);
}
}
StructModel {
ident: input.ident.clone(),
doc: doc_of(&input.attrs),
generics: input.generics.clone(),
version,
migrate_from,
repr,
fields,
variants: Vec::new(),
removed,
}
}
fn analyze_enum(input: &syn::DeriveInput, data: &syn::DataEnum) -> Self {
let (version, migrate_from, removed) = parse_struct_attrs(&input.attrs);
if migrate_from.is_some() || !removed.is_empty() {
abort!(
input.ident,
"ix-schema: schema migration is not yet supported on enums";
help = "derive `Ix` on an enum without `migrate_from`/`removed`"
);
}
let repr = parse_repr(&input.attrs);
let variants: Vec<VariantModel> = data
.variants
.iter()
.map(|variant| {
let (kind, fields) = match &variant.fields {
syn::Fields::Unit => (VariantShape::Unit, Vec::new()),
syn::Fields::Unnamed(unnamed) => (
VariantShape::Tuple,
unnamed
.unnamed
.iter()
.map(|f| VariantFieldModel {
name: None,
ty: f.ty.clone(),
})
.collect(),
),
syn::Fields::Named(named) => (
VariantShape::Struct,
named
.named
.iter()
.map(|f| VariantFieldModel {
name: f.ident.clone(),
ty: f.ty.clone(),
})
.collect(),
),
};
VariantModel {
ident: variant.ident.clone(),
doc: doc_of(&variant.attrs),
kind,
fields,
}
})
.collect();
StructModel {
ident: input.ident.clone(),
doc: doc_of(&input.attrs),
generics: input.generics.clone(),
version,
migrate_from: None,
repr,
fields: Vec::new(),
variants,
removed: Vec::new(),
}
}
}
fn doc_of(attrs: &[syn::Attribute]) -> String {
let mut lines = Vec::new();
for attr in attrs {
if !attr.path().is_ident("doc") {
continue;
}
if let syn::Meta::NameValue(nv) = &attr.meta
&& let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
lines.push(s.value().trim().to_string());
}
}
lines.join("\n")
}
impl FieldModel {
pub fn name_string(&self) -> String {
match &self.member {
syn::Member::Named(id) => id.to_string(),
syn::Member::Unnamed(idx) => idx.index.to_string(),
}
}
fn analyze(field: &syn::Field, index: usize, struct_version: u32) -> Self {
let member = match &field.ident {
Some(ident) => syn::Member::Named(ident.clone()),
None => syn::Member::Unnamed(syn::Index {
index: index as u32,
span: Span::call_site(),
}),
};
let span = field
.ident
.as_ref()
.map_or_else(Span::call_site, syn::Ident::span);
let mut since: Option<u32> = None;
let mut default: Option<syn::Expr> = None;
let mut with: Option<syn::Path> = None;
let mut rename_from: Option<String> = None;
for attr in &field.attrs {
if !attr.path().is_ident("ix") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("since") {
reject_duplicate(since.is_some(), &meta)?;
let lit: syn::LitInt = meta.value()?.parse()?;
since = Some(lit.base10_parse()?);
} else if meta.path.is_ident("default") {
reject_duplicate(default.is_some(), &meta)?;
default = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("with") {
reject_duplicate(with.is_some(), &meta)?;
with = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("rename_from") {
reject_duplicate(rename_from.is_some(), &meta)?;
let lit: syn::LitStr = meta.value()?.parse()?;
rename_from = Some(lit.value());
} else {
return Err(meta.error(
"unknown field attribute; expected `since`, `default`, `with` or `rename_from`",
));
}
Ok(())
})
.unwrap_or_abort();
}
let since = since.unwrap_or(1);
if since > struct_version {
abort!(
span,
"field declares `since = {}` but the struct is only version {}",
since,
struct_version;
help = "raise the struct's `#[ix(version = ..)]` or lower `since`"
);
}
FieldModel {
member,
doc: doc_of(&field.attrs),
ty: field.ty.clone(),
since,
default,
with,
rename_from,
span,
}
}
}
fn parse_struct_attrs(attrs: &[syn::Attribute]) -> (u32, Option<syn::Type>, Vec<String>) {
let mut version: Option<u32> = None;
let mut migrate_from: Option<syn::Type> = None;
let mut removed: Vec<String> = Vec::new();
for attr in attrs {
if !attr.path().is_ident("ix") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("version") {
reject_duplicate(version.is_some(), &meta)?;
let lit: syn::LitInt = meta.value()?.parse()?;
version = Some(lit.base10_parse()?);
} else if meta.path.is_ident("migrate_from") {
reject_duplicate(migrate_from.is_some(), &meta)?;
migrate_from = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("removed") {
let content;
syn::parenthesized!(content in meta.input);
let names = content
.parse_terminated(<syn::LitStr as syn::parse::Parse>::parse, syn::Token![,])?;
for name in names {
removed.push(name.value());
}
} else {
return Err(meta.error(
"unknown struct attribute; expected `version`, `migrate_from` or `removed`",
));
}
Ok(())
})
.unwrap_or_abort();
}
(version.unwrap_or(1), migrate_from, removed)
}