use std::convert::TryFrom;
use darling::{FromAttributes, util::Flag};
use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{Fields, Ident, ItemEnum, Lit};
use crate::{
helpers::get_docs,
parsing::{
PhpNameContext, PhpRename, RenameRule, Visibility, ident_to_php_name, validate_php_name,
},
prelude::*,
};
#[derive(FromAttributes, Default, Debug)]
#[darling(default, attributes(php), forward_attrs(doc))]
struct PhpEnumAttribute {
#[darling(flatten)]
rename: PhpRename,
#[darling(default)]
allow_native_discriminants: Flag,
rename_cases: Option<RenameRule>,
vis: Option<Visibility>,
attrs: Vec<syn::Attribute>,
}
#[derive(FromAttributes, Default, Debug)]
#[darling(default, attributes(php), forward_attrs(doc))]
struct PhpEnumVariantAttribute {
#[darling(flatten)]
rename: PhpRename,
#[darling(rename = "value")]
discriminant: Option<Lit>,
attrs: Vec<syn::Attribute>,
}
pub fn parser(mut input: ItemEnum) -> Result<TokenStream> {
let php_attr = PhpEnumAttribute::from_attributes(&input.attrs)?;
input.attrs.retain(|attr| !attr.path().is_ident("php"));
let docs = get_docs(&php_attr.attrs)?;
let mut cases = vec![];
let mut discriminant_type = DiscriminantType::None;
for variant in &mut input.variants {
if variant.fields != Fields::Unit {
bail!("Enum cases must be unit variants, found: {:?}", variant);
}
if !php_attr.allow_native_discriminants.is_present() && variant.discriminant.is_some() {
bail!(variant => "Native discriminants are currently not exported to PHP. To set a discriminant, use the `#[php(allow_native_discriminants)]` attribute on the enum. To export discriminants, set the #[php(value = ...)] attribute on the enum case.");
}
let variant_attr = PhpEnumVariantAttribute::from_attributes(&variant.attrs)?;
variant.attrs.retain(|attr| !attr.path().is_ident("php"));
let docs = get_docs(&variant_attr.attrs)?;
let discriminant = variant_attr
.discriminant
.as_ref()
.map(TryInto::try_into)
.transpose()?;
if let Some(d) = &discriminant {
match d {
Discriminant::String(_) => {
if discriminant_type == DiscriminantType::Integer {
bail!(variant => "Mixed discriminants are not allowed in enums, found string and integer discriminants");
}
discriminant_type = DiscriminantType::String;
}
Discriminant::Integer(_) => {
if discriminant_type == DiscriminantType::String {
bail!(variant => "Mixed discriminants are not allowed in enums, found string and integer discriminants");
}
discriminant_type = DiscriminantType::Integer;
}
}
} else if discriminant_type != DiscriminantType::None {
bail!(variant => "Discriminant must be specified for all enum cases, found: {:?}", variant);
}
let case_name = variant_attr.rename.rename(
ident_to_php_name(&variant.ident),
php_attr.rename_cases.unwrap_or(RenameRule::Pascal),
);
validate_php_name(&case_name, PhpNameContext::EnumCase, variant.ident.span())?;
cases.push(EnumCase {
ident: variant.ident.clone(),
name: case_name,
attrs: variant_attr,
discriminant,
docs,
});
if !cases
.iter()
.filter_map(|case| case.discriminant.as_ref())
.all_unique()
{
bail!(variant => "Enum cases must have unique discriminants, found duplicates in: {:?}", cases);
}
}
let enum_props = Enum::new(
&input.ident,
&php_attr,
docs,
cases,
None, discriminant_type,
)?;
Ok(quote! {
#[allow(dead_code)]
#input
#enum_props
})
}
#[derive(Debug)]
pub struct Enum<'a> {
ident: &'a Ident,
name: String,
discriminant_type: DiscriminantType,
docs: Vec<String>,
cases: Vec<EnumCase>,
flags: Option<String>,
}
impl<'a> Enum<'a> {
fn new(
ident: &'a Ident,
attrs: &PhpEnumAttribute,
docs: Vec<String>,
cases: Vec<EnumCase>,
flags: Option<String>,
discriminant_type: DiscriminantType,
) -> Result<Self> {
let name = attrs
.rename
.rename(ident_to_php_name(ident), RenameRule::Pascal);
validate_php_name(&name, PhpNameContext::Enum, ident.span())?;
Ok(Self {
ident,
name,
discriminant_type,
docs,
cases,
flags,
})
}
fn registered_class(&self) -> TokenStream {
let ident = &self.ident;
let name = &self.name;
let flags = self
.flags
.as_ref()
.map(|f| quote! { | #f })
.unwrap_or_default();
let flags = quote! { ::ext_php_rs::flags::ClassFlags::Enum #flags };
let docs = &self.docs;
quote! {
impl ::ext_php_rs::class::RegisteredClass for #ident {
const CLASS_NAME: &'static str = #name;
const BUILDER_MODIFIER: ::std::option::Option<
fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder
> = None;
const EXTENDS: ::std::option::Option<
::ext_php_rs::class::ClassEntryInfo
> = None;
const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[];
const FLAGS: ::ext_php_rs::flags::ClassFlags = #flags;
const DOC_COMMENTS: &'static [&'static str] = &[
#(#docs,)*
];
fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata<Self> {
static METADATA: ::ext_php_rs::class::ClassMetadata<#ident> =
::ext_php_rs::class::ClassMetadata::new(&[]);
&METADATA
}
#[inline]
fn method_builders() -> ::std::vec::Vec<
(::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags)
> {
use ::ext_php_rs::internal::class::PhpClassImpl;
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_methods()
}
#[inline]
fn constructor() -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<Self>> {
None
}
#[inline]
fn constants() -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] {
use ::ext_php_rs::internal::class::PhpClassImpl;
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_constants()
}
}
}
}
fn registered_enum(&self) -> TokenStream {
let ident = &self.ident;
let cases = &self.cases;
let case_from_names = self.cases.iter().map(|case| {
let ident = &case.ident;
let name = &case.name;
quote! {
#name => Ok(Self::#ident)
}
});
let case_to_names = self.cases.iter().map(|case| {
let ident = &case.ident;
let name = &case.name;
quote! {
Self::#ident => #name
}
});
quote! {
impl ::ext_php_rs::enum_::RegisteredEnum for #ident {
const CASES: &'static [::ext_php_rs::enum_::EnumCase] = &[
#(#cases,)*
];
fn from_name(name: &str) -> ::ext_php_rs::error::Result<Self> {
match name {
#(#case_from_names,)*
_ => Err(::ext_php_rs::error::Error::InvalidProperty),
}
}
fn to_name(&self) -> &'static str {
match self {
#(#case_to_names,)*
}
}
}
}
}
pub fn impl_try_from(&self) -> TokenStream {
if self.discriminant_type == DiscriminantType::None {
return quote! {};
}
let discriminant_type = match self.discriminant_type {
DiscriminantType::Integer => quote! { i64 },
DiscriminantType::String => quote! { &str },
DiscriminantType::None => unreachable!("Discriminant type should not be None here"),
};
let ident = &self.ident;
let cases = self.cases.iter().map(|case| {
let ident = &case.ident;
match case
.discriminant
.as_ref()
.expect("Discriminant should be set")
{
Discriminant::String(s) => quote! { #s => Ok(Self::#ident) },
Discriminant::Integer(i) => quote! { #i => Ok(Self::#ident) },
}
});
quote! {
impl TryFrom<#discriminant_type> for #ident {
type Error = ::ext_php_rs::error::Error;
fn try_from(value: #discriminant_type) -> ::ext_php_rs::error::Result<Self> {
match value {
#(
#cases,
)*
_ => Err(::ext_php_rs::error::Error::InvalidProperty),
}
}
}
}
}
pub fn impl_into(&self) -> TokenStream {
if self.discriminant_type == DiscriminantType::None {
return quote! {};
}
let discriminant_type = match self.discriminant_type {
DiscriminantType::Integer => quote! { i64 },
DiscriminantType::String => quote! { &'static str },
DiscriminantType::None => unreachable!("Discriminant type should not be None here"),
};
let ident = &self.ident;
let cases = self.cases.iter().map(|case| {
let ident = &case.ident;
match case
.discriminant
.as_ref()
.expect("Discriminant should be set")
{
Discriminant::String(s) => quote! { Self::#ident => #s },
Discriminant::Integer(i) => quote! { Self::#ident => #i },
}
});
quote! {
impl Into<#discriminant_type> for #ident {
fn into(self) -> #discriminant_type {
match self {
#(
#cases,
)*
}
}
}
}
}
}
impl ToTokens for Enum<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let class = self.registered_class();
let enum_impl = self.registered_enum();
let impl_try_from = self.impl_try_from();
let impl_into = self.impl_into();
tokens.extend(quote! {
#class
#enum_impl
#impl_try_from
#impl_into
});
}
}
#[derive(Debug)]
struct EnumCase {
ident: Ident,
name: String,
#[allow(dead_code)]
attrs: PhpEnumVariantAttribute,
discriminant: Option<Discriminant>,
docs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Discriminant {
String(String),
Integer(i64),
}
impl TryFrom<&Lit> for Discriminant {
type Error = syn::Error;
fn try_from(lit: &Lit) -> Result<Self> {
match lit {
Lit::Str(s) => Ok(Discriminant::String(s.value())),
Lit::Int(i) => i
.base10_parse::<i64>()
.map(Discriminant::Integer)
.map_err(|_| err!(lit => "Invalid integer literal for enum case: {:?}", lit)),
_ => bail!(lit => "Unsupported discriminant type: {:?}", lit),
}
}
}
impl ToTokens for Discriminant {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(match self {
Discriminant::String(s) => {
quote! { ::ext_php_rs::enum_::Discriminant::String(#s) }
}
Discriminant::Integer(i) => {
quote! { ::ext_php_rs::enum_::Discriminant::Int(#i) }
}
});
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum DiscriminantType {
None,
String,
Integer,
}
impl ToTokens for EnumCase {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ident = &self.name;
let discriminant = self
.discriminant
.as_ref()
.map_or_else(|| quote! { None }, |v| quote! { Some(#v) });
let docs = &self.docs;
tokens.extend(quote! {
::ext_php_rs::enum_::EnumCase {
name: #ident,
discriminant: #discriminant,
docs: &[#(#docs,)*],
}
});
}
}