Skip to content

Commit 2e8ab9a

Browse files
committed
feat(enum): add basic enum support
Fixes: #178 Refs: #302
1 parent da9db12 commit 2e8ab9a

File tree

21 files changed

+687
-18
lines changed

21 files changed

+687
-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.2"
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: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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

Comments
 (0)