disqualified/
short_name.rs

1/// Lazily shortens a type name to remove all module paths.
2///
3/// The short name of a type is its full name as returned by
4/// [`core::any::type_name`], but with the prefix of all paths removed. For
5/// example, the short name of `alloc::vec::Vec<core::option::Option<u32>>`
6/// would be `Vec<Option<u32>>`.
7///
8/// Shortening is performed lazily without allocation.
9#[cfg_attr(
10    feature = "alloc",
11    doc = r#" To get a [`String`] from this type, use the [`to_string`](`alloc::string::ToString::to_string`) method."#
12)]
13///
14/// # Examples
15///
16/// ```rust
17/// # use bevy_reflect::ShortName;
18/// #
19/// # mod foo {
20/// #     pub mod bar {
21/// #         pub struct Baz;
22/// #     }
23/// # }
24/// // Baz
25/// let short_name = ShortName::of::<foo::bar::Baz>();
26/// ```
27#[derive(Clone, Copy)]
28pub struct ShortName<'a>(pub &'a str);
29
30impl ShortName<'static> {
31    /// Gets a shortened version of the name of the type `T`.
32    pub fn of<T: ?Sized>() -> Self {
33        Self(core::any::type_name::<T>())
34    }
35}
36
37impl<'a> ShortName<'a> {
38    /// Gets the original name before shortening.
39    pub const fn original(&self) -> &'a str {
40        self.0
41    }
42}
43
44impl<'a> From<&'a str> for ShortName<'a> {
45    fn from(value: &'a str) -> Self {
46        Self(value)
47    }
48}
49
50impl<'a> core::fmt::Debug for ShortName<'a> {
51    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52        let &ShortName(full_name) = self;
53        // Generics result in nested paths within <..> blocks.
54        // Consider "bevy_render::camera::camera::extract_cameras<bevy_render::camera::bundle::Camera3d>".
55        // To tackle this, we parse the string from left to right, collapsing as we go.
56        let mut index: usize = 0;
57        let end_of_string = full_name.len();
58
59        while index < end_of_string {
60            let rest_of_string = full_name.get(index..end_of_string).unwrap_or_default();
61
62            // Collapse everything up to the next special character,
63            // then skip over it
64            if let Some(special_character_index) = rest_of_string.find(|c: char| {
65                (c == ' ')
66                    || (c == '<')
67                    || (c == '>')
68                    || (c == '(')
69                    || (c == ')')
70                    || (c == '[')
71                    || (c == ']')
72                    || (c == ',')
73                    || (c == ';')
74            }) {
75                let segment_to_collapse = rest_of_string
76                    .get(0..special_character_index)
77                    .unwrap_or_default();
78
79                f.write_str(collapse_type_name(segment_to_collapse))?;
80
81                // Insert the special character
82                let special_character =
83                    &rest_of_string[special_character_index..=special_character_index];
84
85                f.write_str(special_character)?;
86
87                match special_character {
88                    ">" | ")" | "]"
89                        if rest_of_string[special_character_index + 1..].starts_with("::") =>
90                    {
91                        f.write_str("::")?;
92                        // Move the index past the "::"
93                        index += special_character_index + 3;
94                    }
95                    // Move the index just past the special character
96                    _ => index += special_character_index + 1,
97                }
98            } else {
99                // If there are no special characters left, we're done!
100                f.write_str(collapse_type_name(rest_of_string))?;
101                index = end_of_string;
102            }
103        }
104
105        Ok(())
106    }
107}
108
109impl<'a> core::fmt::Display for ShortName<'a> {
110    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
111        <Self as core::fmt::Debug>::fmt(self, f)
112    }
113}
114
115#[inline(always)]
116fn collapse_type_name(string: &str) -> &str {
117    // Enums types are retained.
118    // As heuristic, we assume the enum type to be uppercase.
119    let mut segments = string.rsplit("::");
120    let (last, second_last): (&str, Option<&str>) = (segments.next().unwrap(), segments.next());
121    let Some(second_last) = second_last else {
122        return last;
123    };
124
125    if second_last.starts_with(char::is_uppercase) {
126        let index = string.len() - last.len() - second_last.len() - 2;
127        &string[index..]
128    } else {
129        last
130    }
131}
132
133#[cfg(all(test, feature = "alloc"))]
134mod name_formatting_tests {
135    use super::ShortName;
136
137    #[test]
138    fn trivial() {
139        assert_eq!(ShortName("test_system").to_string(), "test_system");
140    }
141
142    #[test]
143    fn path_separated() {
144        assert_eq!(
145            ShortName("bevy_prelude::make_fun_game").to_string(),
146            "make_fun_game"
147        );
148    }
149
150    #[test]
151    fn tuple_type() {
152        assert_eq!(
153            ShortName("(String, String)").to_string(),
154            "(String, String)"
155        );
156    }
157
158    #[test]
159    fn array_type() {
160        assert_eq!(ShortName("[i32; 3]").to_string(), "[i32; 3]");
161    }
162
163    #[test]
164    fn trivial_generics() {
165        assert_eq!(ShortName("a<B>").to_string(), "a<B>");
166    }
167
168    #[test]
169    fn multiple_type_parameters() {
170        assert_eq!(ShortName("a<B, C>").to_string(), "a<B, C>");
171    }
172
173    #[test]
174    fn enums() {
175        assert_eq!(ShortName("Option::None").to_string(), "Option::None");
176        assert_eq!(ShortName("Option::Some(2)").to_string(), "Option::Some(2)");
177        assert_eq!(
178            ShortName("bevy_render::RenderSet::Prepare").to_string(),
179            "RenderSet::Prepare"
180        );
181    }
182
183    #[test]
184    fn generics() {
185        assert_eq!(
186            ShortName("bevy_render::camera::camera::extract_cameras<bevy_render::camera::bundle::Camera3d>").to_string(),
187            "extract_cameras<Camera3d>"
188        );
189    }
190
191    #[test]
192    fn nested_generics() {
193        assert_eq!(
194            ShortName("bevy::mad_science::do_mad_science<mad_science::Test<mad_science::Tube>, bavy::TypeSystemAbuse>").to_string(),
195            "do_mad_science<Test<Tube>, TypeSystemAbuse>"
196        );
197    }
198
199    #[test]
200    fn sub_path_after_closing_bracket() {
201        assert_eq!(
202            ShortName("bevy_asset::assets::Assets<bevy_scene::dynamic_scene::DynamicScene>::asset_event_system").to_string(),
203            "Assets<DynamicScene>::asset_event_system"
204        );
205        assert_eq!(
206            ShortName("(String, String)::default").to_string(),
207            "(String, String)::default"
208        );
209        assert_eq!(
210            ShortName("[i32; 16]::default").to_string(),
211            "[i32; 16]::default"
212        );
213    }
214}