Skip to main content

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}