Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add as_str and godot_name to non-bitfield enums #898

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 116 additions & 19 deletions godot-codegen/src/generator/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub fn make_enum_definition_with(
quote! { #[doc(hidden)] pub }
});

let debug_impl = make_enum_debug_impl(enum_);
let debug_impl = make_enum_debug_impl(enum_, define_traits && !enum_.is_bitfield);
quote! {
#[repr(transparent)]
#[derive( #( #derives ),* )]
Expand Down Expand Up @@ -146,11 +146,8 @@ fn make_enum_index_impl(enum_: &Enum) -> Option<TokenStream> {
})
}

/// Implement `Debug` trait for the enum.
fn make_enum_debug_impl(enum_: &Enum) -> TokenStream {
let enum_name = &enum_.name;
let enum_name_str = enum_name.to_string();

// Creates the match cases to return the enumerator name as &str.
fn make_enum_to_str_cases(enum_: &Enum) -> TokenStream {
let enumerators = enum_.enumerators.iter().map(|enumerator| {
let Enumerator { name, .. } = enumerator;
let name_str = name.to_string();
Expand All @@ -159,22 +156,55 @@ fn make_enum_debug_impl(enum_: &Enum) -> TokenStream {
}
});

quote! {
#( #enumerators )*
}
}

/// Implement `Debug` trait for the enum.
fn make_enum_debug_impl(enum_: &Enum, use_as_str: bool) -> TokenStream {
let enum_name = &enum_.name;
let enum_name_str = enum_name.to_string();

// Print the ord if no matching enumerator can be found.
let enumerator_not_found = quote! {
f.debug_struct(#enum_name_str)
.field("ord", &self.ord)
.finish()?;

return Ok(());
};

// Reuse `as_str` if traits are defined and not a bitfield.
let function_body = if use_as_str {
quote! {
use crate::obj::EngineEnum;

let enumerator = self.as_str();
if enumerator.is_empty() {
#enumerator_not_found
}
}
} else {
let enumerators = make_enum_to_str_cases(enum_);

quote! {
// Many enums have duplicates, thus allow unreachable.
// In the future, we could print sth like "ONE|TWO" instead (at least for unstable Debug).
#[allow(unreachable_patterns)]
let enumerator = match *self {
#enumerators
_ => {
#enumerator_not_found
}
};
}
};

quote! {
impl std::fmt::Debug for #enum_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Many enums have duplicates, thus allow unreachable.
// In the future, we could print sth like "ONE|TWO" instead (at least for unstable Debug).
#[allow(unreachable_patterns)]
let enumerator = match *self {
#( #enumerators )*
_ => {
f.debug_struct(#enum_name_str)
.field("ord", &self.ord)
.finish()?;
return Ok(());
}
};

#function_body
f.write_str(enumerator)
}
}
Expand Down Expand Up @@ -222,6 +252,8 @@ fn make_enum_engine_trait_impl(enum_: &Enum) -> TokenStream {
}
});

let str_functions = make_enum_str_functions(enum_);

quote! {
impl #engine_trait for #name {
fn try_from_ord(ord: i32) -> Option<Self> {
Expand All @@ -234,10 +266,13 @@ fn make_enum_engine_trait_impl(enum_: &Enum) -> TokenStream {
fn ord(self) -> i32 {
self as i32
}

#str_functions
}
}
} else {
let unique_ords = enum_.unique_ords().expect("self is an enum");
let str_functions = make_enum_str_functions(enum_);

quote! {
impl #engine_trait for #name {
Expand All @@ -251,8 +286,70 @@ fn make_enum_engine_trait_impl(enum_: &Enum) -> TokenStream {
fn ord(self) -> i32 {
self.ord
}

#str_functions
}
}
}
}

/// Creates the `as_str` and `godot_name` implementations for the enum.
fn make_enum_str_functions(enum_: &Enum) -> TokenStream {
let as_str_enumerators = make_enum_to_str_cases(enum_);

// Only enumerations with different godot names are specified.
// `as_str` is called for the rest of them.
let godot_different_cases = {
let enumerators = enum_
.enumerators
.iter()
.filter(|enumerator| enumerator.name != enumerator.godot_name)
.map(|enumerator| {
let Enumerator {
name, godot_name, ..
} = enumerator;
let godot_name_str = godot_name.to_string();
quote! {
Self::#name => #godot_name_str,
}
});

quote! {
#( #enumerators )*
}
};

let godot_name_match = if godot_different_cases.is_empty() {
// If empty, all the Rust names match the Godot ones.
// Remove match statement to avoid `clippy::match_single_binding`.
quote! {
self.as_str()
}
} else {
quote! {
// Many enums have duplicates, thus allow unreachable.
#[allow(unreachable_patterns)]
match *self {
#godot_different_cases
_ => self.as_str(),
}
}
};

quote! {
#[inline]
fn as_str(&self) -> &'static str {
// Many enums have duplicates, thus allow unreachable.
#[allow(unreachable_patterns)]
match *self {
#as_str_enumerators
_ => "",
}
}

fn godot_name(&self) -> &'static str {
#godot_name_match
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions godot-core/src/builtin/vectors/vector_axis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ macro_rules! impl_vector_axis_enum {
fn ord(self) -> i32 {
self as i32
}

fn as_str(&self) -> &'static str {
match *self {
$(
Self::$axis => stringify!($axis),
)+
}
}

fn godot_name(&self) -> &'static str {
match *self {
$(
Self::$axis => concat!("AXIS_", stringify!($axis)),
)+
}
}
}

impl GodotConvert for $AxisEnum {
Expand Down
10 changes: 10 additions & 0 deletions godot-core/src/obj/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ pub trait EngineEnum: Copy {
Self::try_from_ord(ord)
.unwrap_or_else(|| panic!("ordinal {ord} does not map to any enumerator"))
}

// The name of the enumerator, as it appears in Rust.
//
// If the value does not match one of the known enumerators, the empty string is returned.
fn as_str(&self) -> &'static str;

// The equivalent name of the enumerator, as specified in Godot.
//
// If the value does not match one of the known enumerators, the empty string is returned.
fn godot_name(&self) -> &'static str;
}

/// Auto-implemented for all engine-provided bitfields.
Expand Down
27 changes: 27 additions & 0 deletions itest/rust/src/object_tests/enum_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use godot::builtin::varray;
use godot::classes::input::CursorShape;
use godot::classes::mesh::PrimitiveType;
use godot::classes::{time, ArrayMesh};
use godot::global::{Orientation, Key};
use std::collections::HashSet;

#[itest]
Expand Down Expand Up @@ -70,3 +71,29 @@ fn add_surface_from_arrays() {
let mut mesh = ArrayMesh::new();
mesh.add_surface_from_arrays(PrimitiveType::PRIMITIVE_TRIANGLES, varray![]);
}

#[itest]
fn enum_as_str_correct() {
use godot::obj::EngineEnum;
assert_eq!(Orientation::Vertical.as_str(), "VERTICAL");
assert_eq!(Orientation::Horizontal.as_str(), "HORIZONTAL");

assert_eq!(Key::NONE.as_str(), "NONE");
assert_eq!(Key::SPECIAL.as_str(), "SPECIAL");
assert_eq!(Key::ESCAPE.as_str(), "ESCAPE");
assert_eq!(Key::TAB.as_str(), "TAB");
assert_eq!(Key::A.as_str(), "A");
}

#[itest]
fn enum_godot_name_correct() {
use godot::obj::EngineEnum;
assert_eq!(Orientation::Vertical.godot_name(), Orientation::Vertical.as_str());
assert_eq!(Orientation::Horizontal.godot_name(), Orientation::Vertical.as_str());

assert_eq!(Key::NONE.godot_name(), "KEY_NONE");
assert_eq!(Key::SPECIAL.godot_name(), "KEY_SPECIAL");
assert_eq!(Key::ESCAPE.godot_name(), "KEY_ESCAPE");
assert_eq!(Key::TAB.godot_name(), "KEY_TAB");
assert_eq!(Key::A.godot_name(), "KEY_A");
}
Loading