use crate::type_map::PhpMapper;
use ahash::AHashSet;
use alef_adapters::AdapterBodies;
use alef_codegen::builder::ImplBuilder;
use alef_codegen::generators::{self, RustBindingConfig};
use alef_codegen::shared::partition_methods;
use alef_codegen::type_mapper::TypeMapper;
use alef_core::ir::{EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
use super::functions::{
gen_async_instance_method, gen_async_static_method, gen_instance_method, gen_instance_method_non_opaque,
gen_static_method,
};
fn is_php_prop_scalar_with_enums(ty: &TypeRef, enum_names: &AHashSet<String>) -> bool {
match ty {
TypeRef::Primitive(_) | TypeRef::String | TypeRef::Char | TypeRef::Duration | TypeRef::Path => true,
TypeRef::Optional(inner) => is_php_prop_scalar_with_enums(inner, enum_names),
TypeRef::Vec(inner) => {
matches!(inner.as_ref(), TypeRef::Primitive(_) | TypeRef::String | TypeRef::Char)
|| matches!(inner.as_ref(), TypeRef::Named(n) if enum_names.contains(n))
}
TypeRef::Named(n) if enum_names.contains(n) => true,
TypeRef::Named(_) | TypeRef::Map(_, _) | TypeRef::Json | TypeRef::Bytes | TypeRef::Unit => false,
}
}
fn is_php_copy_type(ty: &TypeRef) -> bool {
match ty {
TypeRef::Primitive(_) => true,
TypeRef::Optional(inner) => matches!(inner.as_ref(), TypeRef::Primitive(_)),
_ => false,
}
}
pub(crate) fn gen_opaque_struct_methods(
typ: &TypeDef,
mapper: &PhpMapper,
opaque_types: &AHashSet<String>,
core_import: &str,
adapter_bodies: &AdapterBodies,
) -> String {
let mut impl_builder = ImplBuilder::new(&typ.name);
impl_builder.add_attr("php_impl");
let (instance, statics) = partition_methods(&typ.methods);
for method in &instance {
if method.is_async {
impl_builder.add_method(&gen_async_instance_method(
method,
mapper,
true,
&typ.name,
opaque_types,
adapter_bodies,
));
} else {
impl_builder.add_method(&gen_instance_method(
method,
mapper,
true,
&typ.name,
opaque_types,
core_import,
adapter_bodies,
));
}
}
for method in &statics {
if method.is_async {
impl_builder.add_method(&gen_async_static_method(method, mapper, opaque_types));
} else {
impl_builder.add_method(&gen_static_method(method, mapper, opaque_types, typ, core_import));
}
}
impl_builder.build()
}
pub(crate) fn gen_php_struct(
typ: &TypeDef,
mapper: &PhpMapper,
cfg: &RustBindingConfig<'_>,
php_namespace: Option<&str>,
enum_names: &AHashSet<String>,
) -> String {
let php_name_attr: String;
let struct_attrs_override: Vec<&str>;
let effective_struct_attrs: &[&str] = if let Some(ns) = php_namespace {
let ns_escaped = ns.replace('\\', "\\\\");
php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
struct_attrs_override = vec!["php_class", php_name_attr.as_str()];
&struct_attrs_override
} else {
cfg.struct_attrs
};
let field_attrs_fn = |field: &FieldDef| -> Vec<String> {
let mut attrs = if is_php_prop_scalar_with_enums(&field.ty, enum_names) {
let php_name = alef_codegen::naming::to_php_name(&field.name);
vec![format!("php(prop, name = \"{}\")", php_name)]
} else {
vec![]
};
if cfg.has_serde && matches!(field.ty, TypeRef::Duration) && !field.optional {
attrs.push("serde(skip_serializing_if = \"Option::is_none\")".to_string());
}
if cfg.has_serde
&& typ.has_default
&& matches!(
field.typed_default,
Some(alef_core::ir::DefaultValue::BoolLiteral(true))
)
{
attrs.push("serde(default = \"crate::serde_defaults::bool_true\")".to_string());
}
if cfg.has_serde {
let enum_backed_string = match &field.ty {
TypeRef::Named(n) if enum_names.contains(n) => true,
TypeRef::Optional(inner) => matches!(inner.as_ref(), TypeRef::Named(n) if enum_names.contains(n)),
_ => false,
};
if enum_backed_string {
if field.optional {
attrs.push("serde(skip_serializing_if = \"Option::is_none\")".to_string());
} else {
attrs.push("serde(skip_serializing_if = \"String::is_empty\")".to_string());
}
}
}
if cfg.has_serde && typ.name == "SecurityLimits" && !field.optional {
match field.name.as_str() {
"max_archive_size"
| "max_compression_ratio"
| "max_files_in_archive"
| "max_nesting_depth"
| "max_entity_length"
| "max_content_size"
| "max_iterations"
| "max_xml_depth"
| "max_table_cells" => {
let serde_attr = format!("serde(default = \"crate::serde_defaults::{}\")", field.name);
attrs.push(serde_attr);
}
_ => {}
}
}
attrs
};
if cfg.has_serde {
let mut extra_derives: Vec<&str> = cfg.struct_derives.to_vec();
extra_derives.push("serde::Serialize");
extra_derives.push("serde::Deserialize");
let mut serde_struct_attrs: Vec<&str> = effective_struct_attrs.to_vec();
serde_struct_attrs.push("serde(default)");
let modified_cfg = RustBindingConfig {
struct_attrs: &serde_struct_attrs,
field_attrs: cfg.field_attrs,
struct_derives: &extra_derives,
method_block_attr: cfg.method_block_attr,
constructor_attr: cfg.constructor_attr,
static_attr: cfg.static_attr,
function_attr: cfg.function_attr,
enum_attrs: cfg.enum_attrs,
enum_derives: cfg.enum_derives,
needs_signature: cfg.needs_signature,
signature_prefix: cfg.signature_prefix,
signature_suffix: cfg.signature_suffix,
core_import: cfg.core_import,
async_pattern: cfg.async_pattern,
has_serde: cfg.has_serde,
type_name_prefix: cfg.type_name_prefix,
option_duration_on_defaults: cfg.option_duration_on_defaults,
opaque_type_names: cfg.opaque_type_names,
skip_impl_constructor: cfg.skip_impl_constructor,
cast_uints_to_i32: cfg.cast_uints_to_i32,
cast_large_ints_to_f64: cfg.cast_large_ints_to_f64,
named_non_opaque_params_by_ref: cfg.named_non_opaque_params_by_ref,
lossy_skip_types: cfg.lossy_skip_types,
serializable_opaque_type_names: cfg.serializable_opaque_type_names,
};
generators::gen_struct_with_per_field_attrs(typ, mapper, &modified_cfg, field_attrs_fn)
} else {
let modified_cfg = RustBindingConfig {
struct_attrs: effective_struct_attrs,
..*cfg
};
generators::gen_struct_with_per_field_attrs(typ, mapper, &modified_cfg, field_attrs_fn)
}
}
#[allow(dead_code)]
pub(crate) fn gen_struct_methods(
typ: &TypeDef,
mapper: &PhpMapper,
has_serde: bool,
core_import: &str,
opaque_types: &AHashSet<String>,
enum_names: &AHashSet<String>,
enums: &[EnumDef],
) -> String {
gen_struct_methods_impl(
typ,
mapper,
has_serde,
core_import,
opaque_types,
enum_names,
enums,
&[], &AHashSet::new(), )
}
#[allow(clippy::too_many_arguments)]
pub fn gen_struct_methods_with_exclude(
typ: &TypeDef,
mapper: &PhpMapper,
has_serde: bool,
core_import: &str,
opaque_types: &AHashSet<String>,
enum_names: &AHashSet<String>,
enums: &[EnumDef],
exclude_functions: &[String],
bridge_type_aliases: &AHashSet<String>,
) -> String {
gen_struct_methods_impl(
typ,
mapper,
has_serde,
core_import,
opaque_types,
enum_names,
enums,
exclude_functions,
bridge_type_aliases,
)
}
#[allow(clippy::too_many_arguments)]
fn gen_struct_methods_impl(
typ: &TypeDef,
mapper: &PhpMapper,
has_serde: bool,
core_import: &str,
opaque_types: &AHashSet<String>,
enum_names: &AHashSet<String>,
enums: &[EnumDef],
exclude_functions: &[String],
bridge_type_aliases: &AHashSet<String>,
) -> String {
let mut impl_builder = ImplBuilder::new(&typ.name);
impl_builder.add_attr("php_impl");
if !typ.fields.is_empty() {
let has_named_params = typ
.fields
.iter()
.any(|f| !is_php_prop_scalar_with_enums(&f.ty, enum_names));
let use_from_json = has_serde && (has_named_params || typ.has_default);
if use_from_json {
let constructor = "#[php(name = \"from_json\")]\npub fn from_json(json: String) -> PhpResult<Self> {\n \
serde_json::from_str(&json)\n \
.map_err(|e| PhpException::default(e.to_string()))\n\
}"
.to_string();
impl_builder.add_method(&constructor);
fn field_can_be_param(
ty: &alef_core::ir::TypeRef,
enum_names: &AHashSet<String>,
opaque_types: &AHashSet<String>,
) -> bool {
match ty {
alef_core::ir::TypeRef::Vec(inner) => {
match inner.as_ref() {
alef_core::ir::TypeRef::Named(name) => {
opaque_types.contains(name.as_str()) || enum_names.contains(name.as_str())
}
alef_core::ir::TypeRef::Json => false,
_ => true, }
}
alef_core::ir::TypeRef::Bytes => true,
alef_core::ir::TypeRef::Optional(inner) => {
field_can_be_param(inner, enum_names, opaque_types)
}
_ => is_php_prop_scalar_with_enums(ty, enum_names),
}
}
let has_representable_required = typ
.fields
.iter()
.any(|f| !f.optional && field_can_be_param(&f.ty, enum_names, opaque_types));
if has_representable_required {
let param_defs: Vec<alef_core::ir::ParamDef> = typ
.fields
.iter()
.filter(|f| field_can_be_param(&f.ty, enum_names, opaque_types))
.map(|f| {
let php_param_name = alef_codegen::naming::to_php_name(&f.name);
let optional =
f.optional || (has_serde && typ.has_default && matches!(f.ty, TypeRef::Duration));
alef_core::ir::ParamDef {
name: php_param_name,
ty: f.ty.clone(),
optional,
default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
sanitized: false,
original_type: None,
typed_default: None,
}
})
.collect();
let param_lines =
super::helpers::gen_php_function_params(¶m_defs, mapper, opaque_types, &AHashSet::new());
let mut let_bindings = String::new();
for f in typ
.fields
.iter()
.filter(|f| field_can_be_param(&f.ty, enum_names, opaque_types))
{
if let TypeRef::Vec(inner) = &f.ty {
if let TypeRef::Named(name) = inner.as_ref() {
if !opaque_types.contains(name.as_str()) && !enum_names.contains(name.as_str()) {
let php_param_name = alef_codegen::naming::to_php_name(&f.name);
if f.optional {
let_bindings.push_str(&format!(
"let {}_core: Option<Vec<{}::{}>> = if let Some(ht) = {} {{\n \
let mut result = Vec::new();\n \
for (_, item) in ht.iter() {{\n \
if let Some(parsed) = <&{} as ext_php_rs::convert::FromZval>::from_zval(item) {{\n \
result.push(parsed.clone().into());\n \
}} else {{\n \
return Err(ext_php_rs::exception::PhpException::default(\"Failed to convert array element to {}\".to_string()));\n \
}}\n \
}}\n \
Some(result)\n \
}} else {{\n \
None\n \
}};\n ",
php_param_name, core_import, name, php_param_name, name, name
));
} else {
let_bindings.push_str(&format!(
"let mut {}_core_result: Vec<{}::{}> = Vec::new();\n \
for (_, item) in {}.iter() {{\n \
if let Some(parsed) = <&{} as ext_php_rs::convert::FromZval>::from_zval(item) {{\n \
{}_core_result.push(parsed.clone().into());\n \
}} else {{\n \
return Err(ext_php_rs::exception::PhpException::default(\"Failed to convert array element to {}\".to_string()));\n \
}}\n \
}}\n \
let {}_core: Vec<{}::{}> = {}_core_result;\n ",
php_param_name, core_import, name, php_param_name, name,
php_param_name, name, php_param_name, core_import, name, php_param_name
));
}
}
}
}
}
let param_init = typ
.fields
.iter()
.map(|f| {
let php_param_name = alef_codegen::naming::to_php_name(&f.name);
if field_can_be_param(&f.ty, enum_names, opaque_types) {
if let TypeRef::Vec(inner) = &f.ty {
if let TypeRef::Named(name) = inner.as_ref() {
if !opaque_types.contains(name.as_str()) && !enum_names.contains(name.as_str()) {
return format!("{}: {}_core", f.name, php_param_name);
}
}
}
let is_bytes = matches!(&f.ty, TypeRef::Bytes)
|| matches!(&f.ty, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Bytes));
if is_bytes {
if f.optional {
return format!("{}: {}.map(|b| b.0)", f.name, php_param_name);
}
return format!("{}: {}.0", f.name, php_param_name);
}
format!("{}: {}", f.name, php_param_name)
} else {
format!("{}: Default::default()", f.name)
}
})
.collect::<Vec<_>>()
.join(", ");
let named_constructor = format!(
"#[php(constructor)]\npub fn new(\n{param_lines}\n) -> Self {{\n \
{let_bindings}Self {{ {param_init} }}\n\
}}"
);
impl_builder.add_method(&named_constructor);
}
} else if has_named_params {
let constructor = format!(
"pub fn __construct() -> PhpResult<Self> {{\n \
Err(PhpException::default(\"Not implemented: constructor for {} requires complex params\".to_string()))\n\
}}",
typ.name
);
impl_builder.add_method(&constructor);
} else {
let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
if typ.has_default {
let config_method = alef_codegen::config_gen::gen_php_kwargs_constructor(typ, &map_fn);
impl_builder.add_method(&config_method);
} else {
let param_defs: Vec<alef_core::ir::ParamDef> = typ
.fields
.iter()
.map(|f| alef_core::ir::ParamDef {
name: f.name.clone(),
ty: f.ty.clone(),
optional: f.optional,
default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
sanitized: false,
original_type: None,
typed_default: None,
})
.collect();
let param_lines =
super::helpers::gen_php_function_params(¶m_defs, mapper, opaque_types, &AHashSet::new());
let mut let_bindings = String::new();
for f in typ.fields.iter() {
if let TypeRef::Vec(inner) = &f.ty {
if let TypeRef::Named(name) = inner.as_ref() {
if !opaque_types.contains(name.as_str()) && !enum_names.contains(name.as_str()) {
if f.optional {
let_bindings.push_str(&format!(
"let {}_core: Option<Vec<{}::{}>> = if let Some(ht) = {} {{\n \
let mut result = Vec::new();\n \
for (_, item) in ht.iter() {{\n \
if let Some(parsed) = <&{} as ext_php_rs::convert::FromZval>::from_zval(item) {{\n \
result.push(parsed.clone().into());\n \
}} else {{\n \
return Err(ext_php_rs::exception::PhpException::default(\"Failed to convert array element to {}\".to_string()));\n \
}}\n \
}}\n \
Some(result)\n \
}} else {{\n \
None\n \
}};\n ",
f.name, core_import, name, f.name, name, name
));
} else {
let_bindings.push_str(&format!(
"let mut {}_core_result: Vec<{}::{}> = Vec::new();\n \
for (_, item) in {}.iter() {{\n \
if let Some(parsed) = <&{} as ext_php_rs::convert::FromZval>::from_zval(item) {{\n \
{}_core_result.push(parsed.clone().into());\n \
}} else {{\n \
return Err(ext_php_rs::exception::PhpException::default(\"Failed to convert array element to {}\".to_string()));\n \
}}\n \
}}\n \
let {}_core: Vec<{}::{}> = {}_core_result;\n ",
f.name, core_import, name, f.name, name,
f.name, name, f.name, core_import, name, f.name
));
}
}
}
}
}
let param_init = typ
.fields
.iter()
.map(|f| {
let php_param_name = alef_codegen::naming::to_php_name(&f.name);
if let TypeRef::Vec(inner) = &f.ty {
if let TypeRef::Named(name) = inner.as_ref() {
if !opaque_types.contains(name.as_str()) && !enum_names.contains(name.as_str()) {
return format!("{}: {}_core", f.name, php_param_name);
}
}
}
format!("{}: {}", f.name, php_param_name)
})
.collect::<Vec<_>>()
.join(", ");
let constructor = format!(
"#[php(constructor)]\npub fn new(\n{param_lines}\n) -> Self {{\n \
{let_bindings}Self {{ {param_init} }}\n\
}}"
);
impl_builder.add_method(&constructor);
}
}
}
for field in &typ.fields {
if field.cfg.is_some() {
continue;
}
let effective_ty = &field.ty;
if !is_php_prop_scalar_with_enums(effective_ty, enum_names) {
let php_field_name = alef_codegen::naming::to_php_name(&field.name);
let getter_ident = format!("get_{php_field_name}");
fn ty_is_or_wraps_json(t: &TypeRef) -> bool {
match t {
TypeRef::Json => true,
TypeRef::Optional(inner) | TypeRef::Vec(inner) => ty_is_or_wraps_json(inner),
TypeRef::Map(_, v) => matches!(v.as_ref(), TypeRef::Json),
_ => false,
}
}
let is_json_field = ty_is_or_wraps_json(&field.ty);
if ty_references_untagged_data_enum(&field.ty, &mapper.untagged_data_enum_names) || is_json_field {
let body = if field.optional {
format!(
"self.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok())",
name = field.name
)
} else {
format!("serde_json::to_string(&self.{name}).ok()", name = field.name)
};
let getter_method =
format!("#[php(getter)]\npub fn {getter_ident}(&self) -> Option<String> {{\n {body}\n}}");
impl_builder.add_method(&getter_method);
continue;
}
let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
let rust_return_type = if field.optional {
mapper.optional(&mapper.map_type(&field.ty))
} else {
map_fn(&field.ty)
};
let getter_method = format!(
"#[php(getter)]\npub fn {getter_ident}(&self) -> {ret} {{\n self.{field_name}.clone()\n}}",
field_name = field.name,
ret = rust_return_type,
);
impl_builder.add_method(&getter_method);
}
}
let empty_adapter_bodies: alef_adapters::AdapterBodies = Default::default();
let (instance, statics) = partition_methods(&typ.methods);
for method in &instance {
if method.is_async {
impl_builder.add_method(&gen_async_instance_method(
method,
mapper,
false,
&typ.name,
opaque_types,
&empty_adapter_bodies,
));
} else {
impl_builder.add_method(&gen_instance_method_non_opaque(
method,
mapper,
typ,
core_import,
opaque_types,
enums,
bridge_type_aliases,
));
}
}
for method in &statics {
if exclude_functions.contains(&method.name) {
continue;
}
if method.is_async {
impl_builder.add_method(&gen_async_static_method(method, mapper, opaque_types));
} else {
impl_builder.add_method(&gen_static_method(method, mapper, opaque_types, typ, core_import));
}
}
impl_builder.build()
}
pub(crate) fn gen_enum_constants(enum_def: &EnumDef) -> String {
let mut lines = vec![format!("// {} enum values", enum_def.name)];
for variant in &enum_def.variants {
let const_name = format!("{}_{}", enum_def.name.to_uppercase(), variant.name.to_uppercase());
lines.push(format!("pub const {}: &str = \"{}\";", const_name, variant.name));
}
lines.join("\n")
}
pub(crate) fn is_tagged_data_enum(enum_def: &EnumDef) -> bool {
enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty())
}
pub(crate) fn is_untagged_data_enum(enum_def: &EnumDef) -> bool {
enum_def.serde_untagged && enum_def.variants.iter().any(|v| !v.fields.is_empty())
}
pub(crate) fn ty_references_untagged_data_enum(ty: &TypeRef, untagged_data_enum_names: &AHashSet<String>) -> bool {
match ty {
TypeRef::Named(n) => untagged_data_enum_names.contains(n.as_str()),
TypeRef::Optional(inner) | TypeRef::Vec(inner) => {
ty_references_untagged_data_enum(inner, untagged_data_enum_names)
}
_ => false,
}
}
fn flat_field_name(variant: &EnumVariant, field_index: usize) -> String {
use heck::ToSnakeCase as _;
if alef_codegen::conversions::is_tuple_variant(&variant.fields) {
let base = variant.name.to_snake_case();
if variant.fields.len() == 1 {
base
} else {
format!("{base}_{field_index}")
}
} else {
variant.fields[field_index].name.clone()
}
}
pub(crate) fn gen_flat_data_enum(enum_def: &EnumDef, mapper: &PhpMapper, php_namespace: Option<&str>) -> String {
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let php_attrs: String = if let Some(ns) = php_namespace {
let ns_escaped = ns.replace('\\', "\\\\");
let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, enum_def.name);
format!("#[php_class]\n#[{php_name_attr}]")
} else {
"#[php_class]".to_string()
};
let mut out = String::new();
out.push_str(&crate::template_env::render(
"php_flat_enum_struct_start.jinja",
minijinja::context! {
php_attrs => &php_attrs,
enum_name => &enum_def.name,
},
));
out.push_str(&crate::template_env::render(
"php_flat_enum_tag_field.jinja",
minijinja::context! {
tag_field => tag_field,
},
));
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for variant in &enum_def.variants {
for (idx, field) in variant.fields.iter().enumerate() {
let flat_name = flat_field_name(variant, idx);
if seen.insert(flat_name.clone()) {
let mapped = mapper.map_type(&field.ty).to_string();
let field_ty = format!("Option<{mapped}>");
out.push_str(&crate::template_env::render(
"php_flat_enum_option_field.jinja",
minijinja::context! {
flat_name => &flat_name,
field_ty => &field_ty,
},
));
}
}
}
out.push_str(&crate::template_env::render(
"php_flat_enum_struct_end.jinja",
minijinja::Value::default(),
));
out
}
pub(crate) fn gen_flat_data_enum_methods(enum_def: &EnumDef, mapper: &PhpMapper) -> String {
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let mut impl_builder = ImplBuilder::new(&enum_def.name);
impl_builder.add_attr("php_impl");
let from_json = "#[php(name = \"from_json\")]\npub fn from_json(json: String) -> PhpResult<Self> {\n \
serde_json::from_str(&json)\n \
.map_err(|e| PhpException::default(e.to_string()))\n\
}"
.to_string();
impl_builder.add_method(&from_json);
let tag_getter =
format!("#[php(getter)]\npub fn get_{tag_field}_tag(&self) -> String {{\n self.{tag_field}_tag.clone()\n}}");
impl_builder.add_method(&tag_getter);
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for variant in &enum_def.variants {
for (idx, field) in variant.fields.iter().enumerate() {
let flat_name = flat_field_name(variant, idx);
if seen.insert(flat_name.clone()) {
let mapped = mapper.map_type(&field.ty).to_string();
let field_ty = format!("Option<{mapped}>");
let body_expr = if is_php_copy_type(&field.ty) {
format!("self.{flat_name}")
} else {
format!("self.{flat_name}.clone()")
};
let getter_body =
format!("#[php(getter)]\npub fn get_{flat_name}(&self) -> {field_ty} {{\n {body_expr}\n}}",);
impl_builder.add_method(&getter_body);
}
}
}
let mut out = String::new();
let impl_code = impl_builder.build();
out.push_str(&impl_code);
out.push('\n');
out
}
fn variant_tag_value(variant: &EnumVariant, enum_def: &EnumDef) -> String {
if let Some(rename) = &variant.serde_rename {
return rename.clone();
}
if let Some(rename_all) = &enum_def.serde_rename_all {
return apply_rename_all(&variant.name, rename_all);
}
variant.name.clone()
}
fn apply_rename_all(name: &str, strategy: &str) -> String {
use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
match strategy {
"lowercase" => name.to_lowercase(),
"UPPERCASE" => name.to_uppercase(),
"camelCase" => name.to_lower_camel_case(),
"PascalCase" => name.to_upper_camel_case(),
"snake_case" => name.to_snake_case(),
"SCREAMING_SNAKE_CASE" => name.to_shouty_snake_case(),
"kebab-case" => name.to_kebab_case(),
_ => name.to_string(),
}
}
pub(crate) fn gen_flat_data_enum_from_impls(enum_def: &EnumDef, core_import: &str) -> String {
use alef_core::ir::{PrimitiveType, TypeRef};
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
let binding_name = &enum_def.name;
let all_flat_fields: std::collections::BTreeSet<String> = {
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for variant in &enum_def.variants {
for (idx, _) in variant.fields.iter().enumerate() {
seen.insert(flat_field_name(variant, idx));
}
}
seen
};
let mut out = String::new();
out.push_str(&crate::template_env::render(
"php_flat_enum_impl_from_start.jinja",
minijinja::context! {
core_path => &core_path,
binding_name => &binding_name,
},
));
for variant in &enum_def.variants {
let tag_val = variant_tag_value(variant, enum_def);
if variant.fields.is_empty() {
out.push_str(&crate::template_env::render(
"php_flat_enum_variant_match_empty.jinja",
minijinja::context! {
core_path => &core_path,
variant_name => &variant.name,
tag_field => tag_field,
tag_val => &tag_val,
needs_default => !all_flat_fields.is_empty(),
},
));
} else {
let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
let pattern = if is_tuple {
let names: Vec<String> = variant
.fields
.iter()
.map(|f| {
if f.sanitized {
format!("_{}", f.name)
} else {
f.name.clone()
}
})
.collect();
names.join(", ")
} else {
let bindings: Vec<String> = variant
.fields
.iter()
.map(|f| {
if f.sanitized {
format!("{}: _{}", f.name, f.name)
} else {
f.name.clone()
}
})
.collect();
bindings.join(", ")
};
let pattern_start = if is_tuple {
format!(" {core_path}::{}({pattern}) => Self {{", variant.name)
} else {
format!(" {core_path}::{}{{ {pattern} }} => Self {{", variant.name)
};
out.push_str(&pattern_start);
out.push_str(&crate::template_env::render(
"php_flat_enum_tag_assignment.jinja",
minijinja::context! {
tag_field => tag_field,
tag_val => &tag_val,
},
));
for (idx, f) in variant.fields.iter().enumerate() {
let flat_name = flat_field_name(variant, idx);
let bound_var = if f.sanitized {
format!("_{}", f.name)
} else {
f.name.clone()
};
let expr = flat_enum_core_to_binding_field_expr(f, &bound_var);
out.push_str(&crate::template_env::render(
"php_flat_enum_variant_field.jinja",
minijinja::context! {
flat_name => &flat_name,
expr => &expr,
},
));
}
let variant_flat_names: std::collections::BTreeSet<String> =
(0..variant.fields.len()).map(|i| flat_field_name(variant, i)).collect();
if variant_flat_names == all_flat_fields {
out.push_str(" },\n");
} else {
out.push_str(" ..Default::default() },\n");
}
}
}
out.push_str(&crate::template_env::render(
"php_flat_enum_impl_match_end.jinja",
minijinja::Value::default(),
));
out.push_str(&crate::template_env::render(
"php_flat_enum_impl_into_start.jinja",
minijinja::context! {
binding_name => &binding_name,
core_path => &core_path,
tag_field => tag_field,
},
));
for variant in &enum_def.variants {
let tag_val = variant_tag_value(variant, enum_def);
if variant.fields.is_empty() {
out.push_str(&crate::template_env::render(
"php_flat_enum_variant_match_into_empty.jinja",
minijinja::context! {
tag_val => &tag_val,
core_path => &core_path,
variant_name => &variant.name,
},
));
} else {
let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
let pattern_start = if is_tuple {
format!(" \"{tag_val}\" => {core_path}::{}(", variant.name)
} else {
format!(" \"{tag_val}\" => {core_path}::{}{{", variant.name)
};
out.push_str(&pattern_start);
if is_tuple {
let exprs: Vec<String> = variant
.fields
.iter()
.enumerate()
.map(|(idx, f)| flat_enum_binding_to_core_field_expr(f, &flat_field_name(variant, idx)))
.collect();
out.push_str(&crate::template_env::render(
"php_flat_enum_tuple_exprs.jinja",
minijinja::context! {
exprs_joined => exprs.join(", "),
},
));
out.push_str(" ),\n");
} else {
for (idx, f) in variant.fields.iter().enumerate() {
let flat_name = flat_field_name(variant, idx);
let expr = flat_enum_binding_to_core_field_expr(f, &flat_name);
out.push_str(&crate::template_env::render(
"php_flat_enum_variant_field.jinja",
minijinja::context! {
flat_name => &flat_name,
expr => &expr,
},
));
}
out.push_str(" },\n");
}
}
}
if let Some(first) = enum_def.variants.first() {
if first.fields.is_empty() {
out.push_str(&crate::template_env::render(
"php_flat_enum_fallback_variant_empty.jinja",
minijinja::context! {
core_path => &core_path,
variant_name => &first.name,
},
));
} else if alef_codegen::conversions::is_tuple_variant(&first.fields) {
out.push_str(&crate::template_env::render(
"php_flat_enum_fallback_variant_tuple_start.jinja",
minijinja::context! {
core_path => &core_path,
variant_name => &first.name,
},
));
let parts: Vec<String> = first
.fields
.iter()
.map(|f| {
if f.is_boxed {
"Box::new(Default::default())".to_string()
} else {
"Default::default()".to_string()
}
})
.collect();
out.push_str(&crate::template_env::render(
"php_flat_enum_tuple_exprs.jinja",
minijinja::context! {
exprs_joined => parts.join(", "),
},
));
out.push_str(" ),\n");
} else {
out.push_str(&crate::template_env::render(
"php_flat_enum_fallback_variant_struct_start.jinja",
minijinja::context! {
core_path => &core_path,
variant_name => &first.name,
},
));
for f in &first.fields {
let default_expr = if f.is_boxed {
"Box::new(Default::default())".to_string()
} else {
"Default::default()".to_string()
};
out.push_str(&crate::template_env::render(
"php_flat_enum_fallback_variant_field.jinja",
minijinja::context! {
field_name => &f.name,
default_expr => &default_expr,
},
));
}
out.push_str(" },\n");
}
}
out.push_str(&crate::template_env::render(
"php_flat_enum_impl_match_end.jinja",
minijinja::Value::default(),
));
let _ = TypeRef::Unit;
let _ = PrimitiveType::Bool;
out
}
fn flat_enum_core_to_binding_field_expr(f: &alef_core::ir::FieldDef, bound_var: &str) -> String {
use alef_core::ir::{PrimitiveType, TypeRef};
if f.sanitized {
return "None".to_string();
}
let wrap_some = |inner: String| -> String { format!("Some({inner})") };
match &f.ty {
TypeRef::Path => {
if f.optional {
format!("{bound_var}.map(|p| p.to_string_lossy().into_owned())")
} else {
wrap_some(format!("{bound_var}.to_string_lossy().into_owned()"))
}
}
TypeRef::Primitive(PrimitiveType::Usize | PrimitiveType::U64 | PrimitiveType::Isize) => {
if f.optional {
format!("{bound_var}.map(|v| v as i64)")
} else {
wrap_some(format!("{bound_var} as i64"))
}
}
TypeRef::Named(_) if f.is_boxed => {
if f.optional {
format!("{bound_var}.map(|v| (*v).into())")
} else {
wrap_some(format!("(*{bound_var}).into()"))
}
}
TypeRef::Primitive(_) | TypeRef::String => {
if f.optional {
bound_var.to_string()
} else {
wrap_some(bound_var.to_string())
}
}
_ => {
if f.optional {
format!("{bound_var}.map(Into::into)")
} else {
wrap_some(format!("{bound_var}.into()"))
}
}
}
}
fn flat_enum_binding_to_core_field_expr(f: &alef_core::ir::FieldDef, flat_name: &str) -> String {
use alef_core::ir::{PrimitiveType, TypeRef};
if f.sanitized {
return if f.is_boxed {
"Box::new(Default::default())".to_string()
} else {
"Default::default()".to_string()
};
}
let expr = match &f.ty {
TypeRef::Path => {
if f.optional {
format!("val.{flat_name}.map(std::path::PathBuf::from)")
} else {
format!("val.{flat_name}.map(std::path::PathBuf::from).unwrap_or_default()")
}
}
TypeRef::Primitive(p @ (PrimitiveType::Usize | PrimitiveType::U64 | PrimitiveType::Isize)) => {
let core_ty = match p {
PrimitiveType::Usize => "usize",
PrimitiveType::U64 => "u64",
PrimitiveType::Isize => "isize",
_ => unreachable!(),
};
if f.optional {
format!("val.{flat_name}.map(|v| v as {core_ty})")
} else {
format!("val.{flat_name}.map(|v| v as {core_ty}).unwrap_or_default()")
}
}
TypeRef::Primitive(_) | TypeRef::String => {
if f.optional {
format!("val.{flat_name}")
} else {
format!("val.{flat_name}.unwrap_or_default()")
}
}
_ => {
if f.optional {
format!("val.{flat_name}.map(Into::into)")
} else {
format!("val.{flat_name}.map(Into::into).unwrap_or_default()")
}
}
};
if f.is_boxed { format!("Box::new({expr})") } else { expr }
}