reflectapi_schema/
rename.rs

1use std::ops::ControlFlow;
2
3use crate::Visitor;
4
5#[cfg(feature = "glob")]
6pub use self::glob::Glob;
7
8pub(crate) struct Renamer<'a, P> {
9    pattern: P,
10    replacer: &'a str,
11}
12
13impl<'a, P: Pattern> Renamer<'a, P> {
14    pub fn new(pattern: P, replacer: &'a str) -> Self {
15        Self { pattern, replacer }
16    }
17}
18
19impl<'a, P: Pattern> Visitor for Renamer<'a, P> {
20    type Output = usize;
21
22    fn visit_top_level_name(
23        &mut self,
24        name: &mut String,
25    ) -> ControlFlow<Self::Output, Self::Output> {
26        ControlFlow::Continue(
27            self.pattern
28                .rename(name, self.replacer)
29                .map_or(0, |new_name| {
30                    *name = new_name;
31                    1
32                }),
33        )
34    }
35}
36
37#[allow(private_bounds)]
38pub trait Pattern: Sealed + Copy {
39    fn rename(self, name: &str, with: &str) -> Option<String>;
40}
41
42impl Sealed for &str {}
43
44impl Pattern for &str {
45    fn rename(self, name: &str, replacer: &str) -> Option<String> {
46        if self.ends_with("::") {
47            // replacing module name
48            if let Some(name) = name.strip_prefix(self) {
49                let replacer = replacer.trim_end_matches("::");
50                if replacer.is_empty() {
51                    return Some(name.into());
52                }
53
54                Some(format!("{replacer}::{name}"))
55            } else {
56                None
57            }
58        } else {
59            // replacing type name
60            if name == self {
61                Some(replacer.into())
62            } else {
63                None
64            }
65        }
66    }
67}
68
69#[cfg(feature = "glob")]
70mod glob {
71    use core::fmt;
72    use std::str::FromStr;
73
74    use super::{Pattern, Sealed};
75
76    /// Bulk renamer of modules using glob patterns.
77    #[derive(Clone)]
78    pub struct Glob(glob::Pattern);
79
80    impl fmt::Display for Glob {
81        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82            write!(f, "{}", self.0.to_string().replace('/', "::"))
83        }
84    }
85
86    impl fmt::Debug for Glob {
87        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88            write!(f, "{self}")
89        }
90    }
91
92    impl FromStr for Glob {
93        type Err = glob::PatternError;
94
95        fn from_str(s: &str) -> Result<Self, Self::Err> {
96            let s = s.replace("::", "/");
97            Ok(Self(glob::Pattern::new(&s)?))
98        }
99    }
100
101    impl Sealed for &Glob {}
102
103    impl Pattern for &Glob {
104        fn rename(self, name: &str, replacer: &str) -> Option<String> {
105            let path = std::path::PathBuf::from(name.replace("::", "/"));
106            let file_name = path.file_name().expect("non-empty name");
107
108            if path.components().count() <= 1 {
109                // This means the `name` has no module, so we don't replace it.
110                return None;
111            }
112
113            if self.0.matches_path_with(
114                &path,
115                glob::MatchOptions {
116                    case_sensitive: true,
117                    require_literal_separator: true,
118                    require_literal_leading_dot: false,
119                },
120            ) {
121                let replacer = replacer.trim_end_matches("::");
122                let name = file_name
123                    .to_str()
124                    .expect("is utf-8 as it was converted from str");
125                if replacer.is_empty() {
126                    return Some(name.into());
127                }
128
129                Some(format!("{replacer}::{name}"))
130            } else {
131                None
132            }
133        }
134    }
135}
136
137trait Sealed {}
138
139#[cfg(test)]
140mod tests {
141    use super::Pattern;
142
143    #[track_caller]
144    fn check(matcher: impl Pattern, name: &str, replacer: &str, expected: Option<&str>) {
145        assert_eq!(matcher.rename(name, replacer).as_deref(), expected);
146    }
147
148    // Confusing to read if calling function directly
149    macro_rules! rename {
150        ($matcher:literal to $replacer:literal on $name:expr => $expected:expr) => {
151            check($matcher, $name, $replacer, $expected);
152        };
153    }
154
155    macro_rules! glob_rename {
156        ($matcher:literal to $replacer:literal on $name:expr => $expected:expr) => {
157            #[cfg(feature = "glob")]
158            check(
159                &$matcher.parse::<super::Glob>().unwrap(),
160                $name,
161                $replacer,
162                $expected,
163            );
164        };
165    }
166
167    #[test]
168    fn rename_str() {
169        rename!("foo" to "bar" on "foo" => Some("bar"));
170        rename!("foo" to "bar" on "baz" => None);
171
172        rename!("foo::" to "" on "foo::bar" => Some("bar"));
173
174        rename!("foo::" to "bar::" on "foo::a" => Some("bar::a"));
175        // Optional trailing `::` on replacer
176        rename!("foo::" to "bar" on "foo::a" => Some("bar::a"));
177    }
178
179    #[test]
180    fn rename_glob() {
181        // globs should only match modules
182        glob_rename!("foo*" to "bar" on "foo" => None);
183        glob_rename!("foo::*" to "bar::" on "foo::a" => Some("bar::a"));
184        // Optional trailing `::` on replacer
185        glob_rename!("foo::*" to "bar" on "foo::a" => Some("bar::a"));
186        glob_rename!("foo::*" to "" on "foo::a" => Some("a"));
187        glob_rename!("foo::*" to "bar" on "foo::a" => Some("bar::a"));
188        glob_rename!("foo::**" to "bar" on "foo::a::b::c" => Some("bar::c"));
189
190        // Must be a full match
191        glob_rename!("fo*" to "bar" on "foo::a" => None);
192    }
193}