tokio_dbus_codegen/naming.rs
1//! Turning D-Bus names into Rust names.
2
3/// Words which cannot be used as an identifier, and which are escaped with the
4/// raw identifier syntax instead.
5///
6/// `crate`, `self`, `super` and `Self` cannot be raw identifiers, so they are
7/// suffixed with an underscore instead.
8const KEYWORDS: &[&str] = &[
9 "as", "async", "await", "break", "const", "continue", "dyn", "else", "enum", "extern", "false",
10 "fn", "for", "gen", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
11 "ref", "return", "static", "struct", "trait", "true", "type", "union", "unsafe", "use",
12 "where", "while", "abstract", "become", "box", "do", "final", "macro", "override", "priv",
13 "try", "typeof", "unsized", "virtual", "yield",
14];
15
16/// Keywords which the raw identifier syntax cannot express.
17const RESERVED: &[&str] = &["crate", "self", "Self", "super"];
18
19/// Convert a D-Bus name such as `GetLayout` or `icon-name` into a snake case
20/// Rust identifier, escaping it if it collides with a keyword.
21///
22/// # Examples
23///
24/// ```
25/// use tokio_dbus_codegen::snake_case;
26///
27/// assert_eq!(snake_case("GetLayout"), "get_layout");
28/// assert_eq!(snake_case("WindowId"), "window_id");
29/// assert_eq!(snake_case("ProvideXdgActivationToken"), "provide_xdg_activation_token");
30/// assert_eq!(snake_case("icon-name"), "icon_name");
31/// assert_eq!(snake_case("type"), "r#type");
32/// assert_eq!(snake_case("Self"), "self_");
33/// ```
34pub fn snake_case(name: &str) -> String {
35 let mut out = String::with_capacity(name.len() + 4);
36 let mut chars = name.chars().peekable();
37 let mut previous: Option<char> = None;
38
39 while let Some(c) = chars.next() {
40 if c == '-' || c == '.' || c == ' ' {
41 if !out.ends_with('_') && !out.is_empty() {
42 out.push('_');
43 }
44
45 previous = None;
46 continue;
47 }
48
49 if c.is_ascii_uppercase() {
50 let starts_word = match previous {
51 // `getLayout` and `id2Name` start a word at the capital.
52 Some(p) if p.is_ascii_lowercase() || p.is_ascii_digit() => true,
53 // The last capital of a run starts a word when a lowercase
54 // follows it, as in the `T` of `XdgToken`.
55 Some(p) if p.is_ascii_uppercase() => {
56 chars.peek().is_some_and(|n| n.is_ascii_lowercase())
57 }
58 _ => false,
59 };
60
61 if starts_word && !out.is_empty() && !out.ends_with('_') {
62 out.push('_');
63 }
64
65 out.extend(c.to_lowercase());
66 } else {
67 out.push(c);
68 }
69
70 previous = Some(c);
71 }
72
73 escape(out)
74}
75
76/// Escape an identifier which collides with a keyword.
77fn escape(name: String) -> String {
78 if name.is_empty() {
79 return String::from("_");
80 }
81
82 if RESERVED.contains(&name.as_str()) {
83 return format!("{name}_");
84 }
85
86 if KEYWORDS.contains(&name.as_str()) {
87 return format!("r#{name}");
88 }
89
90 if name.starts_with(|c: char| c.is_ascii_digit()) {
91 return format!("_{name}");
92 }
93
94 name
95}
96
97/// Convert a D-Bus name into a Pascal case Rust identifier, which is what enum
98/// variants and generated types are named after.
99///
100/// # Examples
101///
102/// ```
103/// use tokio_dbus_codegen::pascal_case;
104///
105/// assert_eq!(pascal_case("NewIcon"), "NewIcon");
106/// assert_eq!(pascal_case("com.canonical.dbusmenu"), "Dbusmenu");
107/// assert_eq!(pascal_case("icon-name"), "IconName");
108/// ```
109pub fn pascal_case(name: &str) -> String {
110 let last = name.rsplit('.').next().unwrap_or(name);
111 let mut out = String::with_capacity(last.len());
112 let mut capitalize = true;
113
114 for c in last.chars() {
115 if c == '-' || c == '_' || c == ' ' {
116 capitalize = true;
117 continue;
118 }
119
120 if capitalize {
121 out.extend(c.to_uppercase());
122 capitalize = false;
123 } else {
124 out.push(c);
125 }
126 }
127
128 if out.is_empty() {
129 out.push('_');
130 }
131
132 out
133}
134
135/// The name of the module generated for an interface, derived from its last
136/// segment.
137///
138/// # Examples
139///
140/// ```
141/// use tokio_dbus_codegen::module_name;
142///
143/// assert_eq!(module_name("org.freedesktop.Notifications"), "notifications");
144/// assert_eq!(module_name("org.kde.StatusNotifierItem"), "status_notifier_item");
145/// assert_eq!(module_name("com.canonical.dbusmenu"), "dbusmenu");
146/// ```
147pub fn module_name(interface: &str) -> String {
148 snake_case(interface.rsplit('.').next().unwrap_or(interface))
149}
150
151/// The name of an argument, falling back to a positional name when the
152/// interface file does not give one.
153pub fn argument_name(name: Option<&str>, index: usize) -> String {
154 match name {
155 Some(name) if !name.is_empty() => {
156 let name = snake_case(name);
157
158 // NB: Generated code uses a `__` prefix for its own locals, so an
159 // argument is never allowed to take one.
160 if name.starts_with("__") {
161 format!("arg_{name}")
162 } else {
163 name
164 }
165 }
166 _ => format!("arg{index}"),
167 }
168}