-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: extract env file handling (#32)
### Description Move env file handling to own module so that we can better test its behaviour and have it contained. ### Note We need the service name on the env file handling just so that we can use that while building the `CliError`. It feels that maybe we can return another type of error by the env handlers and leave the translation into a `CliError` as a responsibility of the caller. But maybe we can do that on another PR.
- Loading branch information
1 parent
bc48b4b
commit b7b41e7
Showing
4 changed files
with
291 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,282 @@ | ||
use std::io::Write; | ||
use std::{ | ||
fs::{self, OpenOptions}, | ||
path::PathBuf, | ||
}; | ||
|
||
use crate::{CliError, Result}; | ||
|
||
const LINKUP_ENV_SEPARATOR: &str = "##### Linkup environment - DO NOT EDIT #####"; | ||
|
||
pub fn write_to_env_file(service: &str, dev_env_path: &PathBuf, env_path: &PathBuf) -> Result<()> { | ||
if let Ok(env_content) = fs::read_to_string(env_path) { | ||
if env_content.contains(LINKUP_ENV_SEPARATOR) { | ||
return Ok(()); | ||
} | ||
} | ||
|
||
let mut dev_env_content = fs::read_to_string(dev_env_path).map_err(|e| { | ||
CliError::SetServiceEnv( | ||
service.to_string(), | ||
format!("could not read dev env file: {}", e), | ||
) | ||
})?; | ||
|
||
if dev_env_content.ends_with('\n') { | ||
dev_env_content.pop(); | ||
} | ||
|
||
let mut env_file = OpenOptions::new() | ||
.create(true) | ||
.append(true) | ||
.open(env_path) | ||
.map_err(|e| { | ||
CliError::SetServiceEnv( | ||
service.to_string(), | ||
format!("Failed to open .env file: {}", e), | ||
) | ||
})?; | ||
|
||
let content = vec![ | ||
format!("\n{}", LINKUP_ENV_SEPARATOR), | ||
format!("\n{}", dev_env_content), | ||
format!("\n{}", LINKUP_ENV_SEPARATOR), | ||
]; | ||
|
||
writeln!(env_file, "{}", content.concat()).map_err(|e| { | ||
CliError::SetServiceEnv( | ||
service.to_string(), | ||
format!("could not write to env file: {}", e), | ||
) | ||
})?; | ||
|
||
Ok(()) | ||
} | ||
|
||
pub fn clear_env_file(service: &str, env_path: &PathBuf) -> Result<()> { | ||
let mut file_content = fs::read_to_string(env_path).map_err(|e| { | ||
CliError::RemoveServiceEnv( | ||
service.to_string(), | ||
format!("could not read dev env file: {}", e), | ||
) | ||
})?; | ||
|
||
let start_idx = file_content.find(LINKUP_ENV_SEPARATOR); | ||
let end_idx = file_content.rfind(LINKUP_ENV_SEPARATOR); | ||
|
||
if let (Some(mut start), Some(mut end)) = (start_idx, end_idx) { | ||
if start < end { | ||
let new_line_above_start = | ||
start > 0 && file_content.chars().nth(start - 1) == Some('\n'); | ||
let new_line_bellow_end = file_content.chars().nth(end + 1) == Some('\n'); | ||
|
||
if new_line_above_start { | ||
start -= 1; | ||
} | ||
|
||
if new_line_bellow_end { | ||
end += 1; | ||
} | ||
|
||
file_content.drain(start..=end + LINKUP_ENV_SEPARATOR.len()); | ||
} | ||
|
||
if file_content.ends_with('\n') { | ||
file_content.pop(); | ||
} | ||
|
||
// Write the updated content back to the file | ||
let mut file = OpenOptions::new() | ||
.write(true) | ||
.truncate(true) | ||
.open(env_path) | ||
.map_err(|e| { | ||
CliError::RemoveServiceEnv( | ||
service.to_string(), | ||
format!("Failed to open .env file for writing: {}", e), | ||
) | ||
})?; | ||
file.write_all(file_content.as_bytes()).map_err(|e| { | ||
CliError::RemoveServiceEnv( | ||
service.to_string(), | ||
format!("Failed to write .env file: {}", e), | ||
) | ||
})?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use rand::distributions::Alphanumeric; | ||
use rand::Rng; | ||
use std::fs::{self, OpenOptions}; | ||
use std::io::Write; | ||
use std::path::PathBuf; | ||
|
||
use crate::env_files::write_to_env_file; | ||
|
||
use super::clear_env_file; | ||
|
||
#[test] | ||
fn write_to_env_file_empty_target() { | ||
let source = TestFile::create("SOURCE_1=VALUE_1"); | ||
let target = TestFile::create(""); | ||
|
||
write_to_env_file("service_1", &source.path, &target.path).unwrap(); | ||
|
||
let file_content = fs::read_to_string(&target.path).unwrap(); | ||
let expected_content = format!( | ||
"\n{}\n{}\n{}\n", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
"SOURCE_1=VALUE_1", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
); | ||
|
||
assert_eq!(file_content, expected_content); | ||
} | ||
|
||
#[test] | ||
fn write_to_env_file_filled_target() { | ||
let source = TestFile::create("SOURCE_1=VALUE_1"); | ||
let target = TestFile::create("EXISTING_1=VALUE_1\nEXISTING_2=VALUE_2"); | ||
|
||
write_to_env_file("service_1", &source.path, &target.path).unwrap(); | ||
|
||
let file_content = fs::read_to_string(&target.path).unwrap(); | ||
let expected_content = format!( | ||
"{}\n{}\n{}\n{}\n{}\n", | ||
"EXISTING_1=VALUE_1", | ||
"EXISTING_2=VALUE_2", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
"SOURCE_1=VALUE_1", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
); | ||
|
||
assert_eq!(file_content, expected_content); | ||
} | ||
|
||
#[test] | ||
fn clear_env_file_only_linkup_env() { | ||
let content = format!( | ||
"\n{}\n{}\n{}\n", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
"SOURCE_1=VALUE_1", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
); | ||
let env_file = TestFile::create(&content); | ||
|
||
clear_env_file("service_1", &env_file.path).unwrap(); | ||
|
||
let file_content = fs::read_to_string(&env_file.path).unwrap(); | ||
assert_eq!("", file_content); | ||
} | ||
|
||
#[test] | ||
fn clear_env_file_existing_env_before_linkup() { | ||
let content = format!( | ||
"{}\n{}\n{}\n\n{}\n{}\n", | ||
"EXISTING_1=VALUE_1", | ||
"EXISTING_2=VALUE_2", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
"SOURCE_1=VALUE_1", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
); | ||
let env_file = TestFile::create(&content); | ||
|
||
clear_env_file("service_1", &env_file.path).unwrap(); | ||
|
||
let file_content = fs::read_to_string(&env_file.path).unwrap(); | ||
let expected_content = format!("{}\n{}", "EXISTING_1=VALUE_1", "EXISTING_2=VALUE_2",); | ||
assert_eq!(expected_content, file_content); | ||
} | ||
|
||
#[test] | ||
fn clear_env_file_existing_env_before_and_after_linkup() { | ||
let content = format!( | ||
"{}\n{}\n{}\n\n{}\n{}\n\n{}\n{}", | ||
"EXISTING_1=VALUE_1", | ||
"EXISTING_2=VALUE_2", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
"SOURCE_1=VALUE_1", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
"EXISTING_3=VALUE_3", | ||
"EXISTING_4=VALUE_4", | ||
); | ||
let env_file = TestFile::create(&content); | ||
|
||
clear_env_file("service_1", &env_file.path).unwrap(); | ||
|
||
let file_content = fs::read_to_string(&env_file.path).unwrap(); | ||
let expected_content = format!( | ||
"{}\n{}\n{}\n{}", | ||
"EXISTING_1=VALUE_1", "EXISTING_2=VALUE_2", "EXISTING_3=VALUE_3", "EXISTING_4=VALUE_4", | ||
); | ||
assert_eq!(expected_content, file_content); | ||
} | ||
|
||
#[test] | ||
fn write_and_clear() { | ||
let source = TestFile::create("SOURCE_1=VALUE_1\nSOURCE_2=VALUE_2"); | ||
let target = TestFile::create("EXISTING_1=VALUE_1\nEXISTING_2=VALUE_2"); | ||
|
||
write_to_env_file("service_1", &source.path, &target.path).unwrap(); | ||
|
||
// Check post write content | ||
let file_content = fs::read_to_string(&target.path).unwrap(); | ||
let expected_content = format!( | ||
"{}\n{}\n{}\n{}\n{}\n{}\n", | ||
"EXISTING_1=VALUE_1", | ||
"EXISTING_2=VALUE_2", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
"SOURCE_1=VALUE_1", | ||
"SOURCE_2=VALUE_2", | ||
"##### Linkup environment - DO NOT EDIT #####", | ||
); | ||
assert_eq!(file_content, expected_content); | ||
|
||
clear_env_file("service_1", &target.path).unwrap(); | ||
|
||
// Check post clear content | ||
let file_content = fs::read_to_string(&target.path).unwrap(); | ||
let expected_content = format!("{}\n{}", "EXISTING_1=VALUE_1", "EXISTING_2=VALUE_2",); | ||
assert_eq!(file_content, expected_content); | ||
} | ||
|
||
struct TestFile { | ||
pub path: PathBuf, | ||
} | ||
|
||
impl TestFile { | ||
fn create(content: &str) -> Self { | ||
let file_name: String = rand::thread_rng() | ||
.sample_iter(&Alphanumeric) | ||
.take(12) | ||
.map(char::from) | ||
.collect(); | ||
|
||
let mut test_file = std::env::temp_dir(); | ||
test_file.push(file_name); | ||
|
||
let mut env_file = OpenOptions::new() | ||
.create(true) | ||
.write(true) | ||
.truncate(true) | ||
.open(&test_file) | ||
.unwrap(); | ||
|
||
write!(&mut env_file, "{}", content).unwrap(); | ||
|
||
Self { path: test_file } | ||
} | ||
} | ||
|
||
impl Drop for TestFile { | ||
fn drop(&mut self) { | ||
if let Err(err) = std::fs::remove_file(&self.path) { | ||
println!("failed to remove file {}: {}", &self.path.display(), err); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.