diff --git a/.env.example b/.env.example index 0e1e19c..23105d9 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,17 @@ VITE_STRIPE_PK= VITE_SENTRY_ENABLED= VITE_SENTRY_DSN= -# staging / production +### local,staging VITE_SENTRY_ENV=staging +### prod +# VITE_SENTRY_ENV=production + +### db:local +DATABASE_URL='postgres://user:password@localhost:5432/core' +### db:staging +# DATABASE_URL='postgres://user:password@localhost:5555/core' +### db:prod +# DATABASE_URL='postgres://user:password@localhost:6666/core' CYPRESS_CRED_EMAIL='owner@cypress.test' CYPRESS_CRED_PASSWORD='12345678' @@ -10,13 +19,21 @@ CYPRESS_TEST_PRIVATE_TOKEN='12345678' ### local VITE_API_BASE_URL='http://localhost:1337/api' -VITE_API_BASE_URL='http://localhost:3000' +VITE_SITE_URL='http://localhost:3000' CYPRESS_API_BASE_URL='http://localhost:1337/api' # this variable will not switch -DEBUG=1 +GRAPHQL_PLAYGROUND=1 ### staging # VITE_API_BASE_URL='https://app.develop.staging.example.com/api' -# VITE_BMM_SITE_URL='https://www.develop.staging.example.com' +# VITE_SITE_URL='https://www.develop.staging.example.com' # CYPRESS_API_BASE_URL='https://app.develop.staging.example.com/api' + +### debug +# DEBUG_VAR=1 + +### debug:on,local +# DEBUG=1 +### debug:off,staging +# DEBUG= diff --git a/.gitignore b/.gitignore index 83a00ee..cb094d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target -.env +.env* +!.env.example diff --git a/Cargo.toml b/Cargo.toml index a714bd2..6e71447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,5 @@ categories = ["command-line-interface", "config", "development-tools"] [dependencies] clap = { version = "3.2.15", features = ["derive"] } + +[features] diff --git a/README.md b/README.md index fe351b3..298e36b 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,26 @@ You can see the [full example]. # Usage +Basic usage + ```sh cp .env.example .env -vnetod local # choose local state -vnetod staging # choose staging state -vnetod - # disable all states +vnetod local # enable local section +vnetod staging # enable staging section +vnetod local debug # enable local and debug sections +vnetod # disable all sections +``` + +You can also use variables from namespaces + +```sh +vnetod db:staging debug:off +``` + +You can switch between states and overwrite from namespaces at the same time. + +```sh +vnetod local db:staging debug:off ``` # License diff --git a/src/cli.rs b/src/cli.rs index 46d599f..e236cf0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,6 +14,8 @@ //! along with vnetod. If not, see . //! +pub mod switch; + use std::path::PathBuf; use clap::Parser; @@ -40,5 +42,5 @@ pub struct Args { pub output: Option, #[clap(value_parser)] - pub section_names: Vec, + pub sections: Vec, } diff --git a/src/cli/switch.rs b/src/cli/switch.rs new file mode 100644 index 0000000..a48ae7c --- /dev/null +++ b/src/cli/switch.rs @@ -0,0 +1,32 @@ +use crate::{cli::Args, domain}; +use std::fs::File; + +#[derive(Debug)] +pub enum Error { + OpenFile, + Switch(domain::switch::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::OpenFile => f.write_str("Cannot open file"), + Error::Switch(inner) => write!(f, "Cannot switch between states: {}", inner), + } + } +} + +impl std::error::Error for Error {} + +pub fn execute(args: &Args) -> Result<(), Error> { + let content = std::fs::read_to_string(&args.file).map_err(|_| Error::OpenFile)?; + let writer = + File::create(args.output.as_ref().unwrap_or(&args.file)).map_err(|_| Error::OpenFile)?; + + domain::switch::execute(domain::switch::Request { + content: &content, + writer, + sections: &args.sections, + }) + .map_err(Error::Switch) +} diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 0000000..0898b8b --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,29 @@ +pub mod switch; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Section { + namespace: Option, + name: String, +} + +impl Section { + fn new(name: &str) -> Self { + Self { + name: name.to_string(), + namespace: None, + } + } + + fn with_namespace(namespace: &str, name: &str) -> Self { + Self { + name: name.to_string(), + namespace: Some(namespace.to_string()), + } + } + + fn parse(s: &str) -> Self { + let s = s.trim(); + s.split_once(':') + .map_or_else(|| Self::new(s), |(ns, name)| Self::with_namespace(ns, name)) + } +} diff --git a/src/domain/switch.rs b/src/domain/switch.rs new file mode 100644 index 0000000..25d4a98 --- /dev/null +++ b/src/domain/switch.rs @@ -0,0 +1,262 @@ +use std::io::{BufWriter, Write}; + +use super::Section; + +#[derive(Debug)] +pub enum Error { + WriteData, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::WriteData => f.write_str("Cannot write data"), + } + } +} + +impl std::error::Error for Error {} + +pub struct Request<'args, W> +where + W: Write, +{ + pub content: &'args str, + pub writer: W, + pub sections: &'args [String], +} + +pub fn execute(req: Request) -> Result<(), Error> +where + W: Write, +{ + let mut writer = BufWriter::new(req.writer); + + let choose_sections = req + .sections + .iter() + .map(|s| Section::parse(s.as_str())) + .collect::>(); + + let mut current_sections: Option> = None; + + for line in req.content.split_inclusive('\n') { + let new_line = if is_section_end(line) { + current_sections = None; + line.to_string() + } else if let Some(section_info) = line.strip_prefix("### ") { + current_sections = section_info + .split_whitespace() + .next() + .map(|r| r.split(',').map(Section::parse).collect()); + line.to_string() + } else if let Some(cur_sections) = current_sections.clone() { + let trimmed_line = line.trim_start_matches(['#', ' ']); + if should_enable_variable(&choose_sections, &cur_sections) { + String::from(trimmed_line) + } else if should_disable_variable(&choose_sections, &cur_sections) { + format!("# {}", trimmed_line) + } else { + line.to_string() + } + } else { + line.to_string() + }; + + writer + .write_all(new_line.as_bytes()) + .map_err(|_| Error::WriteData)?; + } + + writer.flush().map_err(|_| Error::WriteData) +} + +fn is_section_end(line: &str) -> bool { + line.trim().is_empty() +} + +fn should_enable_variable(choose_sections: &[Section], current_sections: &[Section]) -> bool { + let cross_sections = choose_sections + .iter() + .filter(|s| { + s.namespace.is_some() + && current_sections + .iter() + .any(|s2| s.namespace == s2.namespace) + }) + .collect::>(); + + if cross_sections.is_empty() { + choose_sections.iter().any(|s| { + if s.namespace.is_none() { + current_sections.iter().any(|s2| s.name == s2.name) + } else { + current_sections.contains(s) + } + }) + } else { + cross_sections.iter().any(|s| current_sections.contains(s)) + } +} + +fn should_disable_variable(choose_sections: &[Section], current_sections: &[Section]) -> bool { + choose_sections.is_empty() + || choose_sections.iter().any(|s| s.namespace.is_none()) + || choose_sections + .iter() + .filter(|s| s.namespace.is_some()) + .any(|s| { + current_sections + .iter() + .any(|s2| s.namespace == s2.namespace && s.name != s2.name) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + const BASE_ENV: &str = include_str!("../../test_data/base_env"); + + fn make_test(input: &str, expected_output: &str, sections: Vec<&str>) { + let mut output_data = vec![]; + let writer = Cursor::new(&mut output_data); + + match execute(Request { + content: input, + writer, + sections: §ions.into_iter().map(String::from).collect::>(), + }) { + Ok(()) => { + let output = String::from_utf8(output_data).unwrap(); + assert_eq!( + output.lines().collect::>(), + expected_output.lines().collect::>() + ); + } + _ => unreachable!(), + } + } + + #[test] + fn should_disable_all_sections() { + make_test(include_str!("../../test_data/all_env"), BASE_ENV, vec![]); + } + + #[test] + fn should_enable_local_sections() { + make_test( + BASE_ENV, + include_str!("../../test_data/should_enable_local"), + vec!["local"], + ); + } + + #[test] + fn should_enable_staging_sections() { + make_test( + BASE_ENV, + include_str!("../../test_data/should_enable_staging"), + vec!["staging"], + ); + } + + #[test] + fn should_use_debug_in_staging_section() { + make_test( + BASE_ENV, + include_str!("../../test_data/should_use_debug_in_staging"), + vec!["staging", "debug:on"], + ); + } + + #[test] + fn should_use_staging_db_in_local_section() { + make_test( + BASE_ENV, + include_str!("../../test_data/should_use_staging_db_in_local"), + vec!["local", "db:staging"], + ); + } + + mod utils { + use super::*; + + #[test] + fn should_not_enable_variables() { + assert!(!should_enable_variable( + &[Section::new("local")], + &[Section::new("staging")] + )); + assert!(!should_enable_variable( + &[Section::with_namespace("db", "local")], + &[Section::new("local")] + )); + assert!(!should_enable_variable( + &[Section::with_namespace("db", "local")], + &[Section::with_namespace("db", "staging")] + )); + assert!(!should_enable_variable( + &[ + Section::new("staging"), + Section::with_namespace("debug", "on") + ], + &[ + Section::with_namespace("debug", "off"), + Section::new("staging") + ] + )); + } + + #[test] + fn should_enable_variables() { + assert!(should_enable_variable( + &[Section::new("local")], + &[Section::new("local")] + )); + assert!(should_enable_variable( + &[Section::new("local")], + &[Section::with_namespace("db", "local")] + )); + assert!(should_enable_variable( + &[Section::with_namespace("db", "local")], + &[Section::with_namespace("db", "local")] + )); + assert!(should_enable_variable( + &[ + Section::new("local"), + Section::with_namespace("debug", "on") + ], + &[ + Section::with_namespace("debug", "on"), + Section::new("staging") + ] + )); + } + + #[test] + fn should_not_disable_variables() { + assert!(!should_disable_variable( + &[Section::with_namespace("debug", "on")], + &[Section::new("local")] + )); + } + + #[test] + fn should_disable_variables() { + assert!(should_disable_variable( + &[Section::new("local")], + &[Section::with_namespace("debug", "off")] + )); + assert!(should_disable_variable( + &[], + &[Section::with_namespace("debug", "off")] + )); + assert!(should_disable_variable( + &[Section::with_namespace("debug", "on")], + &[Section::with_namespace("debug", "off")] + )); + } + } +} diff --git a/src/main.rs b/src/main.rs index a9029f6..8c2fc7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,66 +16,14 @@ #![deny(clippy::all, clippy::pedantic)] mod cli; - -use std::{ - fs::File, - io::{BufWriter, Write}, -}; +mod domain; use clap::Parser; -fn main() -> Result<(), Error> { +fn main() -> Result<(), cli::switch::Error> { let cli = cli::Args::parse(); - change_env_layout(&cli)?; + cli::switch::execute(&cli)?; Ok(()) } - -fn change_env_layout(args: &cli::Args) -> Result<(), Error> { - let content = std::fs::read_to_string(&args.file).map_err(|_| Error::OpenFile)?; - - let mut writer = File::create(args.output.as_ref().unwrap_or(&args.file)) - .map_err(|_| Error::OpenFile) - .map(BufWriter::new)?; - - let mut current_section_name: Option> = None; - - for line in content.split_inclusive('\n') { - let new_line = if is_section_end(line) { - current_section_name = None; - line.to_string() - } else if let Some(section_info) = line.strip_prefix("### ") { - current_section_name = section_info - .split_whitespace() - .next() - .map(|r| r.split(',').map(str::trim).map(String::from).collect()); - line.to_string() - } else if let Some(cur_name) = current_section_name.clone() { - let trimmed_line = line.trim_start_matches(['#', ' ']); - if args.section_names.iter().any(|sn| cur_name.contains(sn)) { - String::from(trimmed_line) - } else { - format!("# {}", trimmed_line) - } - } else { - line.to_string() - }; - - writer - .write_all(new_line.as_bytes()) - .map_err(|_| Error::WriteData)?; - } - - writer.flush().map_err(|_| Error::WriteData) -} - -fn is_section_end(line: &str) -> bool { - line.trim().is_empty() -} - -#[derive(Debug)] -enum Error { - OpenFile, - WriteData, -} diff --git a/test_data/all_env b/test_data/all_env new file mode 100644 index 0000000..1583d32 --- /dev/null +++ b/test_data/all_env @@ -0,0 +1,36 @@ +VITE_STRIPE_PK= +VITE_SENTRY_ENABLED= +VITE_SENTRY_DSN= +### local,staging +VITE_SENTRY_ENV=staging +### prod +VITE_SENTRY_ENV=production + +### db:local +DATABASE_URL='postgres://user:password@localhost:5432/core' +### db:staging +DATABASE_URL='postgres://user:password@localhost:5555/core' +### db:prod +DATABASE_URL='postgres://user:password@localhost:6666/core' + +CYPRESS_CRED_EMAIL='owner@cypress.test' +CYPRESS_CRED_PASSWORD='12345678' +CYPRESS_TEST_PRIVATE_TOKEN='12345678' + +### local +VITE_API_BASE_URL='http://localhost:1337/api' +VITE_SITE_URL='http://localhost:3000' +CYPRESS_API_BASE_URL='http://localhost:1337/api' + +# this variable will not switch +GRAPHQL_PLAYGROUND=1 + +### staging +VITE_API_BASE_URL='https://app.develop.staging.example.com/api' +VITE_SITE_URL='https://www.develop.staging.example.com' +CYPRESS_API_BASE_URL='https://app.develop.staging.example.com/api' + +### debug:on,local +DEBUG=1 +### debug:off,staging +DEBUG= diff --git a/test_data/base_env b/test_data/base_env new file mode 100644 index 0000000..6888eb6 --- /dev/null +++ b/test_data/base_env @@ -0,0 +1,36 @@ +VITE_STRIPE_PK= +VITE_SENTRY_ENABLED= +VITE_SENTRY_DSN= +### local,staging +# VITE_SENTRY_ENV=staging +### prod +# VITE_SENTRY_ENV=production + +### db:local +# DATABASE_URL='postgres://user:password@localhost:5432/core' +### db:staging +# DATABASE_URL='postgres://user:password@localhost:5555/core' +### db:prod +# DATABASE_URL='postgres://user:password@localhost:6666/core' + +CYPRESS_CRED_EMAIL='owner@cypress.test' +CYPRESS_CRED_PASSWORD='12345678' +CYPRESS_TEST_PRIVATE_TOKEN='12345678' + +### local +# VITE_API_BASE_URL='http://localhost:1337/api' +# VITE_SITE_URL='http://localhost:3000' +# CYPRESS_API_BASE_URL='http://localhost:1337/api' + +# this variable will not switch +GRAPHQL_PLAYGROUND=1 + +### staging +# VITE_API_BASE_URL='https://app.develop.staging.example.com/api' +# VITE_SITE_URL='https://www.develop.staging.example.com' +# CYPRESS_API_BASE_URL='https://app.develop.staging.example.com/api' + +### debug:on,local +# DEBUG=1 +### debug:off,staging +# DEBUG= diff --git a/test_data/should_enable_local b/test_data/should_enable_local new file mode 100644 index 0000000..d6e4df7 --- /dev/null +++ b/test_data/should_enable_local @@ -0,0 +1,36 @@ +VITE_STRIPE_PK= +VITE_SENTRY_ENABLED= +VITE_SENTRY_DSN= +### local,staging +VITE_SENTRY_ENV=staging +### prod +# VITE_SENTRY_ENV=production + +### db:local +DATABASE_URL='postgres://user:password@localhost:5432/core' +### db:staging +# DATABASE_URL='postgres://user:password@localhost:5555/core' +### db:prod +# DATABASE_URL='postgres://user:password@localhost:6666/core' + +CYPRESS_CRED_EMAIL='owner@cypress.test' +CYPRESS_CRED_PASSWORD='12345678' +CYPRESS_TEST_PRIVATE_TOKEN='12345678' + +### local +VITE_API_BASE_URL='http://localhost:1337/api' +VITE_SITE_URL='http://localhost:3000' +CYPRESS_API_BASE_URL='http://localhost:1337/api' + +# this variable will not switch +GRAPHQL_PLAYGROUND=1 + +### staging +# VITE_API_BASE_URL='https://app.develop.staging.example.com/api' +# VITE_SITE_URL='https://www.develop.staging.example.com' +# CYPRESS_API_BASE_URL='https://app.develop.staging.example.com/api' + +### debug:on,local +DEBUG=1 +### debug:off,staging +# DEBUG= diff --git a/test_data/should_enable_staging b/test_data/should_enable_staging new file mode 100644 index 0000000..a6b48fc --- /dev/null +++ b/test_data/should_enable_staging @@ -0,0 +1,36 @@ +VITE_STRIPE_PK= +VITE_SENTRY_ENABLED= +VITE_SENTRY_DSN= +### local,staging +VITE_SENTRY_ENV=staging +### prod +# VITE_SENTRY_ENV=production + +### db:local +# DATABASE_URL='postgres://user:password@localhost:5432/core' +### db:staging +DATABASE_URL='postgres://user:password@localhost:5555/core' +### db:prod +# DATABASE_URL='postgres://user:password@localhost:6666/core' + +CYPRESS_CRED_EMAIL='owner@cypress.test' +CYPRESS_CRED_PASSWORD='12345678' +CYPRESS_TEST_PRIVATE_TOKEN='12345678' + +### local +# VITE_API_BASE_URL='http://localhost:1337/api' +# VITE_SITE_URL='http://localhost:3000' +# CYPRESS_API_BASE_URL='http://localhost:1337/api' + +# this variable will not switch +GRAPHQL_PLAYGROUND=1 + +### staging +VITE_API_BASE_URL='https://app.develop.staging.example.com/api' +VITE_SITE_URL='https://www.develop.staging.example.com' +CYPRESS_API_BASE_URL='https://app.develop.staging.example.com/api' + +### debug:on,local +# DEBUG=1 +### debug:off,staging +DEBUG= diff --git a/test_data/should_use_debug_in_staging b/test_data/should_use_debug_in_staging new file mode 100644 index 0000000..6681dda --- /dev/null +++ b/test_data/should_use_debug_in_staging @@ -0,0 +1,36 @@ +VITE_STRIPE_PK= +VITE_SENTRY_ENABLED= +VITE_SENTRY_DSN= +### local,staging +VITE_SENTRY_ENV=staging +### prod +# VITE_SENTRY_ENV=production + +### db:local +# DATABASE_URL='postgres://user:password@localhost:5432/core' +### db:staging +DATABASE_URL='postgres://user:password@localhost:5555/core' +### db:prod +# DATABASE_URL='postgres://user:password@localhost:6666/core' + +CYPRESS_CRED_EMAIL='owner@cypress.test' +CYPRESS_CRED_PASSWORD='12345678' +CYPRESS_TEST_PRIVATE_TOKEN='12345678' + +### local +# VITE_API_BASE_URL='http://localhost:1337/api' +# VITE_SITE_URL='http://localhost:3000' +# CYPRESS_API_BASE_URL='http://localhost:1337/api' + +# this variable will not switch +GRAPHQL_PLAYGROUND=1 + +### staging +VITE_API_BASE_URL='https://app.develop.staging.example.com/api' +VITE_SITE_URL='https://www.develop.staging.example.com' +CYPRESS_API_BASE_URL='https://app.develop.staging.example.com/api' + +### debug:on,local +DEBUG=1 +### debug:off,staging +# DEBUG= diff --git a/test_data/should_use_staging_db_in_local b/test_data/should_use_staging_db_in_local new file mode 100644 index 0000000..8cb508a --- /dev/null +++ b/test_data/should_use_staging_db_in_local @@ -0,0 +1,36 @@ +VITE_STRIPE_PK= +VITE_SENTRY_ENABLED= +VITE_SENTRY_DSN= +### local,staging +VITE_SENTRY_ENV=staging +### prod +# VITE_SENTRY_ENV=production + +### db:local +# DATABASE_URL='postgres://user:password@localhost:5432/core' +### db:staging +DATABASE_URL='postgres://user:password@localhost:5555/core' +### db:prod +# DATABASE_URL='postgres://user:password@localhost:6666/core' + +CYPRESS_CRED_EMAIL='owner@cypress.test' +CYPRESS_CRED_PASSWORD='12345678' +CYPRESS_TEST_PRIVATE_TOKEN='12345678' + +### local +VITE_API_BASE_URL='http://localhost:1337/api' +VITE_SITE_URL='http://localhost:3000' +CYPRESS_API_BASE_URL='http://localhost:1337/api' + +# this variable will not switch +GRAPHQL_PLAYGROUND=1 + +### staging +# VITE_API_BASE_URL='https://app.develop.staging.example.com/api' +# VITE_SITE_URL='https://www.develop.staging.example.com' +# CYPRESS_API_BASE_URL='https://app.develop.staging.example.com/api' + +### debug:on,local +DEBUG=1 +### debug:off,staging +# DEBUG=