|
| 1 | +use std::convert::TryFrom; |
| 2 | + |
| 3 | +use darling::FromAttributes; |
| 4 | +use itertools::Itertools; |
| 5 | +use proc_macro2::TokenStream; |
| 6 | +use quote::{quote, ToTokens}; |
| 7 | +use syn::{Expr, Fields, Ident, ItemEnum, Lit}; |
| 8 | + |
| 9 | +use crate::{ |
| 10 | + helpers::get_docs, |
| 11 | + parsing::{PhpRename, RenameRule, Visibility}, |
| 12 | + prelude::*, |
| 13 | +}; |
| 14 | + |
| 15 | +#[derive(FromAttributes, Default, Debug)] |
| 16 | +#[darling(default, attributes(php), forward_attrs(doc))] |
| 17 | +struct PhpEnumAttribute { |
| 18 | + #[darling(flatten)] |
| 19 | + rename: PhpRename, |
| 20 | + #[darling(default)] |
| 21 | + allow_discriminants: bool, |
| 22 | + rename_cases: Option<RenameRule>, |
| 23 | + vis: Option<Visibility>, |
| 24 | + attrs: Vec<syn::Attribute>, |
| 25 | +} |
| 26 | + |
| 27 | +#[derive(FromAttributes, Default, Debug)] |
| 28 | +#[darling(default, attributes(php), forward_attrs(doc))] |
| 29 | +struct PhpEnumVariantAttribute { |
| 30 | + #[darling(flatten)] |
| 31 | + rename: PhpRename, |
| 32 | + discriminant: Option<Expr>, |
| 33 | + // TODO: Implement doc support for enum variants |
| 34 | + #[allow(dead_code)] |
| 35 | + attrs: Vec<syn::Attribute>, |
| 36 | +} |
| 37 | + |
| 38 | +pub fn parser(mut input: ItemEnum) -> Result<TokenStream> { |
| 39 | + let php_attr = PhpEnumAttribute::from_attributes(&input.attrs)?; |
| 40 | + input.attrs.retain(|attr| !attr.path().is_ident("php")); |
| 41 | + |
| 42 | + let docs = get_docs(&php_attr.attrs)?; |
| 43 | + let mut cases = vec![]; |
| 44 | + let mut discriminant_type = DiscriminantType::None; |
| 45 | + |
| 46 | + for variant in &mut input.variants { |
| 47 | + if variant.fields != Fields::Unit { |
| 48 | + bail!("Enum cases must be unit variants, found: {:?}", variant); |
| 49 | + } |
| 50 | + if !php_attr.allow_discriminants && variant.discriminant.is_some() { |
| 51 | + bail!(variant => "Native discriminants are currently not exported to PHP. To set a discriminant, use the `#[php(allow_discriminants)]` attribute on the enum. To export discriminants, set the #[php(discriminant = ...)] attribute on the enum case."); |
| 52 | + } |
| 53 | + |
| 54 | + let variant_attr = PhpEnumVariantAttribute::from_attributes(&variant.attrs)?; |
| 55 | + variant.attrs.retain(|attr| !attr.path().is_ident("php")); |
| 56 | + let docs = get_docs(&variant_attr.attrs)?; |
| 57 | + let discriminant = variant_attr |
| 58 | + .discriminant |
| 59 | + .as_ref() |
| 60 | + .map(TryInto::try_into) |
| 61 | + .transpose()?; |
| 62 | + |
| 63 | + if let Some(d) = &discriminant { |
| 64 | + match d { |
| 65 | + Discriminant::String(_) => { |
| 66 | + if discriminant_type == DiscriminantType::Integer { |
| 67 | + bail!(variant => "Mixed discriminants are not allowed in enums, found string and integer discriminants"); |
| 68 | + } |
| 69 | + |
| 70 | + discriminant_type = DiscriminantType::String; |
| 71 | + } |
| 72 | + Discriminant::Integer(_) => { |
| 73 | + if discriminant_type == DiscriminantType::String { |
| 74 | + bail!(variant => "Mixed discriminants are not allowed in enums, found string and integer discriminants"); |
| 75 | + } |
| 76 | + |
| 77 | + discriminant_type = DiscriminantType::Integer; |
| 78 | + } |
| 79 | + } |
| 80 | + } else if discriminant_type != DiscriminantType::None { |
| 81 | + bail!(variant => "Discriminant must be specified for all enum cases, found: {:?}", variant); |
| 82 | + } |
| 83 | + |
| 84 | + cases.push(EnumCase { |
| 85 | + ident: variant.ident.clone(), |
| 86 | + name: variant_attr.rename.rename( |
| 87 | + variant.ident.to_string(), |
| 88 | + php_attr.rename_cases.unwrap_or(RenameRule::Pascal), |
| 89 | + ), |
| 90 | + attrs: variant_attr, |
| 91 | + discriminant, |
| 92 | + docs, |
| 93 | + }); |
| 94 | + |
| 95 | + if !cases |
| 96 | + .iter() |
| 97 | + .filter_map(|case| case.discriminant.as_ref()) |
| 98 | + .all_unique() |
| 99 | + { |
| 100 | + bail!(variant => "Enum cases must have unique discriminants, found duplicates in: {:?}", cases); |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + let enum_props = Enum { |
| 105 | + ident: &input.ident, |
| 106 | + attrs: php_attr, |
| 107 | + docs, |
| 108 | + cases, |
| 109 | + flags: None, // TODO: Implement flags support |
| 110 | + }; |
| 111 | + |
| 112 | + Ok(quote! { |
| 113 | + #[allow(dead_code)] |
| 114 | + #input |
| 115 | + |
| 116 | + #enum_props |
| 117 | + }) |
| 118 | +} |
| 119 | + |
| 120 | +#[derive(Debug)] |
| 121 | +pub struct Enum<'a> { |
| 122 | + ident: &'a Ident, |
| 123 | + attrs: PhpEnumAttribute, |
| 124 | + docs: Vec<String>, |
| 125 | + cases: Vec<EnumCase>, |
| 126 | + // TODO: Implement flags support |
| 127 | + #[allow(dead_code)] |
| 128 | + flags: Option<String>, |
| 129 | +} |
| 130 | + |
| 131 | +impl ToTokens for Enum<'_> { |
| 132 | + fn to_tokens(&self, tokens: &mut TokenStream) { |
| 133 | + let ident = &self.ident; |
| 134 | + let enum_name = self |
| 135 | + .attrs |
| 136 | + .rename |
| 137 | + .rename(ident.to_string(), RenameRule::Pascal); |
| 138 | + let flags = quote! { ::ext_php_rs::flags::ClassFlags::Enum }; |
| 139 | + let docs = &self.docs; |
| 140 | + let cases = &self.cases; |
| 141 | + |
| 142 | + let class = quote! { |
| 143 | + impl ::ext_php_rs::class::RegisteredClass for #ident { |
| 144 | + const CLASS_NAME: &'static str = #enum_name; |
| 145 | + const BUILDER_MODIFIER: ::std::option::Option< |
| 146 | + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder |
| 147 | + > = None; |
| 148 | + const EXTENDS: ::std::option::Option< |
| 149 | + ::ext_php_rs::class::ClassEntryInfo |
| 150 | + > = None; |
| 151 | + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; |
| 152 | + const FLAGS: ::ext_php_rs::flags::ClassFlags = #flags; |
| 153 | + const DOC_COMMENTS: &'static [&'static str] = &[ |
| 154 | + #(#docs,)* |
| 155 | + ]; |
| 156 | + |
| 157 | + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata<Self> { |
| 158 | + static METADATA: ::ext_php_rs::class::ClassMetadata<#ident> = |
| 159 | + ::ext_php_rs::class::ClassMetadata::new(); |
| 160 | + &METADATA |
| 161 | + } |
| 162 | + |
| 163 | + #[inline] |
| 164 | + fn get_properties<'a>() -> ::std::collections::HashMap< |
| 165 | + &'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self> |
| 166 | + > { |
| 167 | + ::std::collections::HashMap::new() |
| 168 | + } |
| 169 | + |
| 170 | + #[inline] |
| 171 | + fn method_builders() -> ::std::vec::Vec< |
| 172 | + (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) |
| 173 | + > { |
| 174 | + use ::ext_php_rs::internal::class::PhpClassImpl; |
| 175 | + ::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_methods() |
| 176 | + } |
| 177 | + |
| 178 | + #[inline] |
| 179 | + fn constructor() -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<Self>> { |
| 180 | + None |
| 181 | + } |
| 182 | + |
| 183 | + #[inline] |
| 184 | + fn constants() -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] { |
| 185 | + use ::ext_php_rs::internal::class::PhpClassImpl; |
| 186 | + ::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_constants() |
| 187 | + } |
| 188 | + } |
| 189 | + }; |
| 190 | + let enum_impl = quote! { |
| 191 | + impl ::ext_php_rs::enum_::PhpEnum for #ident { |
| 192 | + const CASES: &'static [::ext_php_rs::enum_::EnumCase] = &[ |
| 193 | + #(#cases,)* |
| 194 | + ]; |
| 195 | + } |
| 196 | + }; |
| 197 | + |
| 198 | + tokens.extend(quote! { |
| 199 | + #class |
| 200 | + #enum_impl |
| 201 | + }); |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +#[derive(Debug)] |
| 206 | +struct EnumCase { |
| 207 | + #[allow(dead_code)] |
| 208 | + ident: Ident, |
| 209 | + name: String, |
| 210 | + #[allow(dead_code)] |
| 211 | + attrs: PhpEnumVariantAttribute, |
| 212 | + discriminant: Option<Discriminant>, |
| 213 | + docs: Vec<String>, |
| 214 | +} |
| 215 | + |
| 216 | +#[derive(Debug, Clone, PartialEq, Eq, Hash)] |
| 217 | +enum Discriminant { |
| 218 | + String(String), |
| 219 | + Integer(i64), |
| 220 | +} |
| 221 | + |
| 222 | +impl TryFrom<&Expr> for Discriminant { |
| 223 | + type Error = syn::Error; |
| 224 | + |
| 225 | + fn try_from(expr: &Expr) -> Result<Self> { |
| 226 | + match expr { |
| 227 | + Expr::Lit(expr) => match &expr.lit { |
| 228 | + Lit::Str(s) => Ok(Discriminant::String(s.value())), |
| 229 | + Lit::Int(i) => i.base10_parse::<i64>().map(Discriminant::Integer).map_err( |
| 230 | + |_| err!(expr => "Invalid integer literal for enum case: {:?}", expr.lit), |
| 231 | + ), |
| 232 | + _ => bail!(expr => "Unsupported discriminant type: {:?}", expr.lit), |
| 233 | + }, |
| 234 | + _ => { |
| 235 | + bail!(expr => "Unsupported discriminant type, expected a literal of type string or i64, found: {:?}", expr); |
| 236 | + } |
| 237 | + } |
| 238 | + } |
| 239 | +} |
| 240 | + |
| 241 | +impl ToTokens for Discriminant { |
| 242 | + fn to_tokens(&self, tokens: &mut TokenStream) { |
| 243 | + tokens.extend(match self { |
| 244 | + Discriminant::String(s) => { |
| 245 | + quote! { ::ext_php_rs::enum_::Discriminant::String(#s.to_string()) } |
| 246 | + } |
| 247 | + Discriminant::Integer(i) => { |
| 248 | + quote! { ::ext_php_rs::enum_::Discriminant::Int(#i) } |
| 249 | + } |
| 250 | + }); |
| 251 | + } |
| 252 | +} |
| 253 | + |
| 254 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 255 | +enum DiscriminantType { |
| 256 | + None, |
| 257 | + String, |
| 258 | + Integer, |
| 259 | +} |
| 260 | + |
| 261 | +impl ToTokens for EnumCase { |
| 262 | + fn to_tokens(&self, tokens: &mut TokenStream) { |
| 263 | + let ident = &self.name; |
| 264 | + let discriminant = self |
| 265 | + .discriminant |
| 266 | + .as_ref() |
| 267 | + .map_or_else(|| quote! { None }, |v| quote! { Some(#v) }); |
| 268 | + let docs = &self.docs; |
| 269 | + |
| 270 | + tokens.extend(quote! { |
| 271 | + ::ext_php_rs::enum_::EnumCase { |
| 272 | + name: #ident, |
| 273 | + discriminant: #discriminant, |
| 274 | + docs: &[#(#docs,)*], |
| 275 | + } |
| 276 | + }); |
| 277 | + } |
| 278 | +} |
0 commit comments