1pub const BASE_IMAGE: &str = "qcode/base";
14
15pub const PROFILE_LABEL: &str = "qcode.profile";
22
23pub const HOSTNAME: &str = "qcode";
39
40#[must_use]
42pub fn profile_image(profile: &str) -> String {
43 format!("qcode/profile/{profile}")
44}
45
46#[must_use]
48pub fn profile_container(workspace: &str, profile: &str) -> String {
49 format!("qcode-{workspace}-{profile}")
50}
51
52#[must_use]
54pub fn base_container(workspace: &str) -> String {
55 format!("qcode-{workspace}-base")
56}
57
58#[must_use]
63pub fn sound_container(workspace: &str, tab: u64) -> String {
64 format!("qcode-{workspace}.play-{tab}")
65}
66
67#[must_use]
74pub fn desktop_container(workspace: &str, profile: &str) -> String {
75 format!("qcode-{workspace}-{profile}.desk")
76}
77
78#[must_use]
84pub fn settings_container(workspace: &str, profile: &str) -> String {
85 format!("qcode-{workspace}-{profile}.mcp")
86}
87
88#[must_use]
90pub fn credential_volume(profile: &str) -> String {
91 format!("qcode-cred-{profile}")
92}
93
94#[must_use]
97pub fn home_volume(workspace: &str, profile: &str) -> String {
98 format!("qcode-home-{workspace}-{profile}")
99}
100
101#[cfg(test)]
102mod tests {
103 use super::{
104 BASE_IMAGE, HOSTNAME, PROFILE_LABEL, base_container, credential_volume, desktop_container, home_volume,
105 profile_container, profile_image, settings_container, sound_container,
106 };
107 use crate::profile::SafeName;
108
109 fn is_object_name(name: &str) -> bool {
112 let mut chars = name.chars();
113 chars.next().is_some_and(|first| first.is_ascii_alphanumeric())
114 && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-'))
115 }
116
117 fn is_image_name(name: &str) -> bool {
120 name.split('/').all(|component| {
121 !component.is_empty()
122 && component.starts_with(|c: char| c.is_ascii_lowercase() || c.is_ascii_digit())
123 && component.ends_with(|c: char| c.is_ascii_lowercase() || c.is_ascii_digit())
124 && component
125 .chars()
126 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '_' | '-'))
127 && !component.contains("--")
128 })
129 }
130
131 fn safe(text: &str) -> SafeName {
132 SafeName::from_display(text).expect("the text has usable characters")
133 }
134
135 #[test]
136 fn turkish_workspace_and_profile_names_still_give_valid_object_names() {
137 let workspace = safe("İstanbul Şubesi");
138 let profile = safe("Günlük Çalışma");
139 assert!(is_image_name(&profile_image(profile.as_str())), "{}", profile_image(profile.as_str()));
140 assert!(is_image_name(BASE_IMAGE));
141 for object in [
142 profile_container(workspace.as_str(), profile.as_str()),
143 base_container(workspace.as_str()),
144 credential_volume(profile.as_str()),
145 home_volume(workspace.as_str(), profile.as_str()),
146 ] {
147 assert!(is_object_name(&object), "{object}");
148 }
149 }
150
151 #[test]
152 fn every_name_a_safe_name_can_produce_is_accepted_by_the_engine() {
153 for text in ["a", "9lives", "a-b-c", &"ab ".repeat(50), "v2.0/build", "IŞIK", "_x_"] {
154 let single = safe(text);
155 let name = single.as_str();
156 assert!(is_image_name(&profile_image(name)), "{text:?}");
157 assert!(is_object_name(&profile_container(name, name)), "{text:?}");
158 assert!(is_object_name(&home_volume(name, name)), "{text:?}");
159 assert!(is_object_name(&credential_volume(name)), "{text:?}");
160 assert!(is_object_name(&base_container(name)), "{text:?}");
161 assert!(is_object_name(&sound_container(name, 7)), "{text:?}");
162 assert!(is_object_name(&desktop_container(name, name)), "{text:?}");
163 assert!(is_object_name(&settings_container(name, name)), "{text:?}");
164 assert!(!profile_container(name, name).contains('.'), "{text:?}");
167 assert_ne!(desktop_container(name, name), profile_container(name, name), "{text:?}");
168 assert_ne!(settings_container(name, name), desktop_container(name, name), "{text:?}");
169 assert_ne!(settings_container(name, name), profile_container(name, name), "{text:?}");
170 }
171 }
172
173 #[test]
174 fn the_machine_name_is_one_the_engines_and_a_resolver_take() {
175 assert!(is_object_name(HOSTNAME) && !HOSTNAME.contains(['_', '.']) && !HOSTNAME.ends_with('-'));
177 }
178
179 #[test]
180 fn the_names_are_the_ones_the_design_settled_on() {
181 assert_eq!(BASE_IMAGE, "qcode/base");
182 assert_eq!(HOSTNAME, "qcode");
183 assert_eq!(PROFILE_LABEL, "qcode.profile");
184 assert_eq!(profile_image("claude-sub"), "qcode/profile/claude-sub");
185 assert_eq!(profile_container("my-app", "claude-sub"), "qcode-my-app-claude-sub");
186 assert_eq!(base_container("my-app"), "qcode-my-app-base");
187 assert_eq!(credential_volume("claude-sub"), "qcode-cred-claude-sub");
188 assert_eq!(home_volume("my-app", "claude-sub"), "qcode-home-my-app-claude-sub");
189 assert_eq!(sound_container("my-app", 3), "qcode-my-app.play-3");
190 assert_eq!(desktop_container("my-app", "antigravity"), "qcode-my-app-antigravity.desk");
191 }
192}