bern_target_api/names.rs
1//! Identifiers generators derive from Bern's `snake_case` names. Each
2//! target escapes the results that are keywords of its own language.
3
4/// `main_window` → `MainWindow`. A hyphen separates words too.
5pub fn pascal(snake: &str) -> String {
6 snake
7 .split(['_', '-'])
8 .filter(|part| !part.is_empty())
9 .map(|part| {
10 let mut chars = part.chars();
11 match chars.next() {
12 Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
13 None => String::new(),
14 }
15 })
16 .collect()
17}
18
19/// The name an application value type (a `#[value]` enum or record)
20/// has in generated Swift and Kotlin: `Layout` → `App_Layout`.
21///
22/// Those types sit at the top of the module the generated code shares
23/// with the platform's frameworks and Bern's runtime, so a bare name like
24/// SwiftUI's `Layout` or `View`, or Compose's `Modifier`, would hide the
25/// framework's type from every file of the module. No type of those
26/// frameworks and none of Bern's (`Bern…`) begins with `App_`, and the
27/// prefix keeps distinct names distinct.
28pub fn value_type(name: &str) -> String {
29 format!("App_{name}")
30}
31
32/// The class of an application session (a `#[session]` struct) in
33/// generated Swift and Kotlin: `URL` → `App_URL`.
34///
35/// The declared name with `Session` appended was Foundation's
36/// `URLSession` for a session named `URL`. Bern type names are unique
37/// per application, so sessions share the prefix of value types.
38pub fn session_type(name: &str) -> String {
39 value_type(name)
40}
41
42/// The variable through which a view of generated Swift and Kotlin reads
43/// a session: `Window` → `session_Window`.
44///
45/// `bern` and the declared name gave the views' own `bernWindow` and
46/// `bernEngine` for sessions named `Window` and `Engine`. The names
47/// generated for parameters and bindings have no underscore.
48pub fn session_variable(name: &str) -> String {
49 format!("session_{name}")
50}
51
52/// The view of a component in generated Swift and Kotlin: `empty` →
53/// `AppView_empty`.
54///
55/// The name in Pascal case with `View` appended was SwiftUI's
56/// `EmptyView`, `AnyView` or `ScrollView`, or a runtime's
57/// `BernPlayerView`, for components named `empty`, `any`, `scroll` or
58/// `bern_player`. No type or function of those frameworks or of the
59/// runtimes begins with `AppView_`, none of the types' `App_` names
60/// does, and the declared name follows unchanged, so distinct components
61/// keep distinct names.
62pub fn component(name: &str) -> String {
63 format!("AppView_{name}")
64}
65
66/// `last_action` → `lastAction`.
67pub fn camel(snake: &str) -> String {
68 let mut out = String::new();
69 let mut upper = false;
70 for (index, c) in snake.chars().enumerate() {
71 if c == '_' {
72 upper = index > 0;
73 continue;
74 }
75 if upper {
76 out.extend(c.to_uppercase());
77 upper = false;
78 } else {
79 out.push(c);
80 }
81 }
82 out
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn snake_case_becomes_pascal_and_camel_case() {
91 assert_eq!(pascal("main_window"), "MainWindow");
92 assert_eq!(pascal("svg-demo"), "SvgDemo");
93 assert_eq!(pascal("a__b_"), "AB");
94 assert_eq!(camel("last_action"), "lastAction");
95 assert_eq!(camel("_private"), "private");
96 assert_eq!(camel("volume_2"), "volume2");
97 assert_eq!(value_type("Layout"), "App_Layout");
98 assert_eq!(session_type("URL"), "App_URL");
99 assert_eq!(session_variable("Window"), "session_Window");
100 assert_eq!(
101 component("content_unavailable"),
102 "AppView_content_unavailable"
103 );
104 }
105}