gcloud_ctx/
configuration.rs1use crate::{properties::Properties, Error, Result};
2use fs::File;
3use lazy_static::lazy_static;
4use regex::Regex;
5use std::{cmp::Ordering, collections::HashMap, fs, io::BufReader, path::PathBuf};
6
7lazy_static! {
8 static ref NAME_REGEX: Regex = Regex::new("^[a-z][-a-z0-9]*$").unwrap();
9}
10
11#[derive(Debug, Clone)]
12pub struct Configuration {
14 name: String,
16
17 path: PathBuf,
19}
20
21impl Configuration {
22 pub fn name(&self) -> &str {
24 &self.name
25 }
26
27 pub fn is_valid_name(name: &str) -> bool {
32 NAME_REGEX.is_match(name)
33 }
34}
35
36impl Ord for Configuration {
37 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
38 self.name.cmp(&other.name)
39 }
40}
41
42impl PartialOrd for Configuration {
43 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
44 Some(self.cmp(other))
45 }
46}
47
48impl PartialEq for Configuration {
49 fn eq(&self, other: &Self) -> bool {
50 self.name == other.name
51 }
52}
53
54impl Eq for Configuration {}
55
56#[derive(Copy, Clone, Debug, PartialEq)]
58pub enum ConflictAction {
59 Abort,
61
62 Overwrite,
64}
65
66impl From<bool> for ConflictAction {
67 fn from(value: bool) -> Self {
68 if value {
69 ConflictAction::Overwrite
70 } else {
71 ConflictAction::Abort
72 }
73 }
74}
75
76#[derive(Debug)]
77pub struct ConfigurationStore {
79 location: PathBuf,
81
82 configurations_path: PathBuf,
84
85 configurations: HashMap<String, Configuration>,
87
88 active: String,
90}
91
92impl ConfigurationStore {
93 pub fn with_default_location() -> Result<Self> {
104 let gcloud_path: PathBuf = if let Ok(value) = std::env::var("CLOUDSDK_CONFIG") {
105 value.into()
106 } else {
107 let gcloud_path = if cfg!(target_os = "macos") {
108 dirs::home_dir()
109 .ok_or(Error::ConfigurationDirectoryNotFound)?
110 .join(".config")
111 } else {
112 dirs::config_dir().ok_or(Error::ConfigurationDirectoryNotFound)?
113 };
114
115 gcloud_path.join("gcloud")
116 };
117
118 Self::with_location(gcloud_path)
119 }
120
121 pub fn with_location(gcloud_path: PathBuf) -> Result<Self> {
123 if !gcloud_path.is_dir() {
124 return Err(Error::ConfigurationStoreNotFound(gcloud_path));
125 }
126
127 let configurations_path = gcloud_path.join("configurations");
128
129 if !configurations_path.is_dir() {
130 return Err(Error::ConfigurationStoreNotFound(configurations_path));
131 }
132
133 let mut configurations: HashMap<String, Configuration> = HashMap::new();
134
135 for file in fs::read_dir(&configurations_path)? {
136 if file.is_err() {
137 continue;
139 }
140
141 let file = file.unwrap();
142 let name = file.file_name();
143 let name = match name.to_str() {
144 Some(name) => name,
145 None => continue, };
147 let name = name.trim_start_matches("config_");
148
149 if !Configuration::is_valid_name(name) {
150 continue;
151 }
152
153 configurations.insert(
154 name.to_owned(),
155 Configuration {
156 name: name.to_owned(),
157 path: file.path(),
158 },
159 );
160 }
161
162 if configurations.is_empty() {
163 return Err(Error::NoConfigurationsFound(configurations_path));
164 }
165
166 let active = gcloud_path.join("active_config");
167 let active = fs::read_to_string(active)?;
168
169 Ok(ConfigurationStore {
170 location: gcloud_path,
171 configurations_path,
172 configurations,
173 active,
174 })
175 }
176
177 pub fn active(&self) -> &str {
179 &self.active
180 }
181
182 pub fn configurations(&self) -> Vec<&Configuration> {
184 let mut value: Vec<&Configuration> = self.configurations.values().collect();
185 value.sort();
186 value
187 }
188
189 pub fn is_active(&self, configuration: &Configuration) -> bool {
191 configuration.name == self.active
192 }
193
194 pub fn activate(&mut self, name: &str) -> Result<()> {
196 let configuration = self
197 .find_by_name(name)
198 .ok_or_else(|| Error::UnknownConfiguration(name.to_owned()))?;
199
200 let path = self.location.join("active_config");
201 std::fs::write(path, &configuration.name)?;
202
203 self.active = configuration.name.to_owned();
204
205 Ok(())
206 }
207
208 pub fn copy(&mut self, src_name: &str, dest_name: &str, conflict: ConflictAction) -> Result<()> {
210 let src = self
211 .configurations
212 .get(src_name)
213 .ok_or_else(|| Error::UnknownConfiguration(src_name.to_owned()))?;
214
215 if !Configuration::is_valid_name(dest_name) {
216 return Err(Error::InvalidName(dest_name.to_owned()));
217 }
218
219 if conflict == ConflictAction::Abort && self.configurations.contains_key(dest_name) {
220 return Err(Error::ExistingConfiguration(dest_name.to_owned()));
221 }
222
223 let filename = self.configurations_path.join(format!("config_{}", dest_name));
225 fs::copy(&src.path, &filename)?;
226
227 let dest = Configuration {
228 name: dest_name.to_owned(),
229 path: filename,
230 };
231
232 self.configurations.insert(dest_name.to_owned(), dest);
233
234 Ok(())
235 }
236
237 pub fn create(&mut self, name: &str, properties: &Properties, conflict: ConflictAction) -> Result<()> {
239 if !Configuration::is_valid_name(name) {
240 return Err(Error::InvalidName(name.to_owned()));
241 }
242
243 if conflict == ConflictAction::Abort && self.configurations.contains_key(name) {
244 return Err(Error::ExistingConfiguration(name.to_owned()));
245 }
246
247 let filename = self.configurations_path.join(format!("config_{}", name));
248 let file = File::create(&filename)?;
249 properties.to_writer(file)?;
250
251 self.configurations.insert(
252 name.to_owned(),
253 Configuration {
254 name: name.to_owned(),
255 path: filename,
256 },
257 );
258
259 Ok(())
260 }
261
262 pub fn delete(&mut self, name: &str) -> Result<()> {
264 let configuration = self
265 .find_by_name(name)
266 .ok_or_else(|| Error::UnknownConfiguration(name.to_owned()))?;
267
268 if self.is_active(configuration) {
269 return Err(Error::DeleteActiveConfiguration);
270 }
271
272 let path = &configuration.path;
273 fs::remove_file(&path)?;
274
275 self.configurations.remove(name);
276
277 Ok(())
278 }
279
280 pub fn describe(&self, name: &str) -> Result<Properties> {
282 let configuration = self
283 .find_by_name(name)
284 .ok_or_else(|| Error::UnknownConfiguration(name.to_owned()))?;
285
286 let path = &configuration.path;
287 let handle = File::open(path)?;
288 let reader = BufReader::new(handle);
289
290 let properties = Properties::from_reader(reader)?;
291
292 Ok(properties)
293 }
294
295 pub fn rename(&mut self, old_name: &str, new_name: &str, conflict: ConflictAction) -> Result<()> {
297 let src = self
298 .configurations
299 .get(old_name)
300 .ok_or_else(|| Error::UnknownConfiguration(old_name.to_owned()))?;
301
302 let active = self.is_active(src);
303
304 if !Configuration::is_valid_name(new_name) {
305 return Err(Error::InvalidName(new_name.to_owned()));
306 }
307
308 if conflict == ConflictAction::Abort && self.configurations.contains_key(new_name) {
309 return Err(Error::ExistingConfiguration(new_name.to_owned()));
310 }
311
312 let new_value = Configuration {
313 name: new_name.to_owned(),
314 path: src.path.with_file_name(format!("config_{}", new_name)),
315 };
316
317 std::fs::rename(&src.path, &new_value.path)?;
318
319 self.configurations.remove(old_name);
320 self.configurations.insert(new_name.to_owned(), new_value);
321
322 if active {
324 self.activate(new_name)?;
325 }
326
327 Ok(())
328 }
329
330 pub fn find_by_name(&self, name: &str) -> Option<&Configuration> {
332 self.configurations.get(&name.to_owned())
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 pub fn test_is_valid_name_with_valid_name() {
342 assert!(Configuration::is_valid_name("foo"));
343 assert!(Configuration::is_valid_name("f"));
344 assert!(Configuration::is_valid_name("f123"));
345 assert!(Configuration::is_valid_name("foo-bar"));
346 assert!(Configuration::is_valid_name("foo-123"));
347 assert!(Configuration::is_valid_name("foo-a1b2c3"));
348 }
349
350 #[test]
351 pub fn test_is_valid_name_with_invalid_name() {
352 assert!(!Configuration::is_valid_name(""));
354
355 assert!(!Configuration::is_valid_name("F"));
357 assert!(!Configuration::is_valid_name("1"));
358 assert!(!Configuration::is_valid_name("-"));
359
360 assert!(!Configuration::is_valid_name("foo_bar"));
362 assert!(!Configuration::is_valid_name("foo.bar"));
363 assert!(!Configuration::is_valid_name("foo|bar"));
364 assert!(!Configuration::is_valid_name("foo$bar"));
365 assert!(!Configuration::is_valid_name("foo#bar"));
366 assert!(!Configuration::is_valid_name("foo@bar"));
367 assert!(!Configuration::is_valid_name("foo;bar"));
368 assert!(!Configuration::is_valid_name("foo?bar"));
369
370 assert!(!Configuration::is_valid_name("camelCase"));
372 }
373}