Skip to content

Commit f052e2a

Browse files
committed
feat(enum): add basic enum support
Fixes: #178 Refs: #302
1 parent e15de17 commit f052e2a

File tree

21 files changed

+633
-18
lines changed

21 files changed

+633
-18
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ native-tls = "0.2"
3838
zip = "4.0"
3939

4040
[features]
41+
default = ["enum"]
4142
closure = []
4243
embed = []
4344
anyhow = ["dep:anyhow"]
45+
enum = []
4446

4547
[workspace]
4648
members = [

allowed_bindings.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ bind! {
8787
zend_declare_class_constant,
8888
zend_declare_property,
8989
zend_do_implement_interface,
90+
zend_enum_add_case,
91+
zend_enum_new,
9092
zend_execute_data,
9193
zend_function_entry,
9294
zend_hash_clean,
@@ -114,6 +116,7 @@ bind! {
114116
zend_register_bool_constant,
115117
zend_register_double_constant,
116118
zend_register_ini_entries,
119+
zend_register_internal_enum,
117120
zend_ini_entry_def,
118121
zend_register_internal_class_ex,
119122
zend_register_long_constant,
@@ -191,6 +194,7 @@ bind! {
191194
ZEND_ACC_DEPRECATED,
192195
ZEND_ACC_DONE_PASS_TWO,
193196
ZEND_ACC_EARLY_BINDING,
197+
ZEND_ACC_ENUM,
194198
ZEND_ACC_FAKE_CLOSURE,
195199
ZEND_ACC_FINAL,
196200
ZEND_ACC_GENERATOR,

build.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<S
261261
Ok(bindings)
262262
}
263263

264-
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
264+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
265265
enum ApiVersion {
266266
Php80 = 2020_09_30,
267267
Php81 = 2021_09_02,
@@ -272,8 +272,15 @@ enum ApiVersion {
272272

273273
impl ApiVersion {
274274
/// Returns the minimum API version supported by ext-php-rs.
275-
pub const fn min() -> Self {
276-
ApiVersion::Php80
275+
pub fn min() -> Self {
276+
[
277+
ApiVersion::Php80,
278+
#[cfg(feature = "enum")]
279+
ApiVersion::Php81,
280+
]
281+
.into_iter()
282+
.max()
283+
.unwrap_or(Self::max())
277284
}
278285

279286
/// Returns the maximum API version supported by ext-php-rs.

crates/macros/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ homepage = "https://github.com/davidcole1340/ext-php-rs"
66
license = "MIT OR Apache-2.0"
77
version = "0.11.0"
88
authors = ["David Cole <[email protected]>"]
9-
edition = "2018"
9+
edition = "2021"
1010

1111
[lib]
1212
proc-macro = true
@@ -19,6 +19,7 @@ proc-macro2 = "1.0.26"
1919
lazy_static = "1.4.0"
2020
anyhow = "1.0"
2121
convert_case = "0.8.0"
22+
itertools = "0.14.0"
2223

2324
[lints.rust]
2425
missing_docs = "warn"

crates/macros/src/enum_.rs

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

0 commit comments

Comments
 (0)