From d6e7b03d3557140e28553cd407a450ad64876b58 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 30 Jul 2022 17:31:05 +0300 Subject: [PATCH 1/4] add namespaces - extract domain logic - extract cli logic - cover domain logic with unit tests --- .env.example | 22 +++- .gitignore | 3 +- Cargo.toml | 2 + README.md | 15 ++- src/cli.rs | 4 +- src/cli/switch.rs | 21 ++++ src/domain.rs | 26 +++++ src/domain/switch.rs | 136 +++++++++++++++++++++++ src/main.rs | 58 +--------- test_data/all_env | 36 ++++++ test_data/base_env | 36 ++++++ test_data/should_enable_local | 36 ++++++ test_data/should_enable_staging | 36 ++++++ test_data/should_use_debug_in_staging | 36 ++++++ test_data/should_use_staging_db_in_local | 36 ++++++ 15 files changed, 439 insertions(+), 64 deletions(-) create mode 100644 src/cli/switch.rs create mode 100644 src/domain.rs create mode 100644 src/domain/switch.rs create mode 100644 test_data/all_env create mode 100644 test_data/base_env create mode 100644 test_data/should_enable_local create mode 100644 test_data/should_enable_staging create mode 100644 test_data/should_use_debug_in_staging create mode 100644 test_data/should_use_staging_db_in_local diff --git a/.env.example b/.env.example index 0e1e19c..aa5e51b 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,18 @@ 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: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..4e065f9 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,20 @@ 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 +``` + +Namespaces + +```sh +vnetod db:local debug:on ``` # 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..e9829bd --- /dev/null +++ b/src/cli/switch.rs @@ -0,0 +1,21 @@ +use crate::{cli::Args, domain}; +use std::{cell::RefCell, fs::File}; + +#[derive(Debug)] +pub enum Error { + OpenFile, + Switch(domain::switch::Error), +} + +pub fn execute(args: &Args) -> Result<(), Error> { + let reader = File::open(&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 { + reader: RefCell::new(reader), + writer, + sections: &args.sections, + }) + .map_err(Error::Switch) +} diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 0000000..cfa1899 --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,26 @@ +pub mod switch; + +#[derive(Debug, Clone, Eq)] +struct Section { + namespace: Option, + name: String, +} + +impl<'a> From<&'a str> for Section { + fn from(s: &'a str) -> Self { + let (ns, name) = s + .trim() + .split_once(':') + .map_or_else(|| (None, s), |(ns, name)| (Some(ns), name)); + Self { + namespace: ns.map(String::from), + name: String::from(name), + } + } +} + +impl PartialEq for Section { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && (self.namespace.is_none() || self.namespace == other.namespace) + } +} diff --git a/src/domain/switch.rs b/src/domain/switch.rs new file mode 100644 index 0000000..0f0089c --- /dev/null +++ b/src/domain/switch.rs @@ -0,0 +1,136 @@ +use std::cell::RefCell; +use std::io::{BufWriter, Read, Write}; + +use super::Section; + +#[derive(Debug)] +pub enum Error { + ReadData, + WriteData, +} + +pub struct Request<'args, R, W> +where + R: Read, + W: Write, +{ + pub reader: RefCell, + pub writer: W, + pub sections: &'args [String], +} + +pub fn execute(req: Request) -> Result<(), Error> +where + R: Read, + W: Write, +{ + let mut content = String::new(); + req.reader + .borrow_mut() + .read_to_string(&mut content) + .map_err(|_| Error::ReadData)?; + + let mut writer = BufWriter::new(req.writer); + + let choose_sections = req + .sections + .iter() + .map(|s| Section::from(s.as_str())) + .collect::>(); + + dbg!(&choose_sections); + + let choose_namespaces = choose_sections + .iter() + .map(|s| s.namespace.clone()) + .collect::>(); + + let mut current_sections: Option> = None; + + for line in 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::from).collect()); + line.to_string() + } else if let Some(cur_sections) = current_sections.clone() { + let trimmed_line = line.trim_start_matches(['#', ' ']); + if cur_sections.iter().any(|s| choose_sections.contains(s)) { + String::from(trimmed_line) + } else { + format!("# {}", trimmed_line) + } + } else { + line.to_string() + }; + + dbg!(line, ¤t_sections, &new_line); + + 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() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + 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 reader = RefCell::new(Cursor::new(input)); + let mut output_data = vec![]; + let writer = Cursor::new(&mut output_data); + + match execute(Request { + reader, + 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"], + ); + } +} 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= From 8fed9f979b1606e7fcb6723747722543c13441fe Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 31 Jul 2022 00:02:43 +0300 Subject: [PATCH 2/4] add utils to get enable section flag --- src/domain.rs | 29 ++++++----- src/domain/switch.rs | 113 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 117 insertions(+), 25 deletions(-) diff --git a/src/domain.rs b/src/domain.rs index cfa1899..0898b8b 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -1,26 +1,29 @@ pub mod switch; -#[derive(Debug, Clone, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] struct Section { namespace: Option, name: String, } -impl<'a> From<&'a str> for Section { - fn from(s: &'a str) -> Self { - let (ns, name) = s - .trim() - .split_once(':') - .map_or_else(|| (None, s), |(ns, name)| (Some(ns), name)); +impl Section { + fn new(name: &str) -> Self { Self { - namespace: ns.map(String::from), - name: String::from(name), + name: name.to_string(), + namespace: None, } } -} -impl PartialEq for Section { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && (self.namespace.is_none() || self.namespace == other.namespace) + 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 index 0f0089c..7c1df5e 100644 --- a/src/domain/switch.rs +++ b/src/domain/switch.rs @@ -35,14 +35,7 @@ where let choose_sections = req .sections .iter() - .map(|s| Section::from(s.as_str())) - .collect::>(); - - dbg!(&choose_sections); - - let choose_namespaces = choose_sections - .iter() - .map(|s| s.namespace.clone()) + .map(|s| Section::parse(s.as_str())) .collect::>(); let mut current_sections: Option> = None; @@ -55,11 +48,11 @@ where current_sections = section_info .split_whitespace() .next() - .map(|r| r.split(',').map(Section::from).collect()); + .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 cur_sections.iter().any(|s| choose_sections.contains(s)) { + if should_enable_variable(&choose_sections, &cur_sections) { String::from(trimmed_line) } else { format!("# {}", trimmed_line) @@ -68,8 +61,6 @@ where line.to_string() }; - dbg!(line, ¤t_sections, &new_line); - writer .write_all(new_line.as_bytes()) .map_err(|_| Error::WriteData)?; @@ -82,6 +73,30 @@ 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)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -133,4 +148,78 @@ mod tests { 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") + ] + )); + } + } } From 2a6eab45c57be8bbc05d7f63f751ea9287bfa9fd Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 31 Jul 2022 00:17:46 +0300 Subject: [PATCH 3/4] add disable variable util --- src/domain/switch.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/domain/switch.rs b/src/domain/switch.rs index 7c1df5e..4c54b5d 100644 --- a/src/domain/switch.rs +++ b/src/domain/switch.rs @@ -54,8 +54,10 @@ where let trimmed_line = line.trim_start_matches(['#', ' ']); if should_enable_variable(&choose_sections, &cur_sections) { String::from(trimmed_line) - } else { + } else if should_disable_variable(&choose_sections, &cur_sections) { format!("# {}", trimmed_line) + } else { + line.to_string() } } else { line.to_string() @@ -97,6 +99,19 @@ fn should_enable_variable(choose_sections: &[Section], current_sections: &[Secti } } +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) + }) +} + #[cfg(test)] mod tests { use super::*; @@ -221,5 +236,13 @@ mod tests { ] )); } + + #[test] + fn should_disable_variables() { + assert!(should_disable_variable( + &[Section::with_namespace("debug", "on")], + &[Section::new("local")] + )); + } } } From 832de949b6fa3338cb55cc98e94553cacf43d86d Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 31 Jul 2022 00:54:38 +0300 Subject: [PATCH 4/4] implement Display and Error traits for our error objects --- .env.example | 3 +++ README.md | 10 ++++++-- src/cli/switch.rs | 17 ++++++++++--- src/domain/switch.rs | 58 +++++++++++++++++++++++++++----------------- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index aa5e51b..23105d9 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,9 @@ GRAPHQL_PLAYGROUND=1 # 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 diff --git a/README.md b/README.md index 4e065f9..298e36b 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,16 @@ vnetod local debug # enable local and debug sections vnetod # disable all sections ``` -Namespaces +You can also use variables from namespaces ```sh -vnetod db:local debug:on +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/switch.rs b/src/cli/switch.rs index e9829bd..a48ae7c 100644 --- a/src/cli/switch.rs +++ b/src/cli/switch.rs @@ -1,5 +1,5 @@ use crate::{cli::Args, domain}; -use std::{cell::RefCell, fs::File}; +use std::fs::File; #[derive(Debug)] pub enum Error { @@ -7,13 +7,24 @@ pub enum Error { 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 reader = File::open(&args.file).map_err(|_| Error::OpenFile)?; + 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 { - reader: RefCell::new(reader), + content: &content, writer, sections: &args.sections, }) diff --git a/src/domain/switch.rs b/src/domain/switch.rs index 4c54b5d..25d4a98 100644 --- a/src/domain/switch.rs +++ b/src/domain/switch.rs @@ -1,35 +1,35 @@ -use std::cell::RefCell; -use std::io::{BufWriter, Read, Write}; +use std::io::{BufWriter, Write}; use super::Section; #[derive(Debug)] pub enum Error { - ReadData, WriteData, } -pub struct Request<'args, R, W> +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 - R: Read, W: Write, { - pub reader: RefCell, + pub content: &'args str, pub writer: W, pub sections: &'args [String], } -pub fn execute(req: Request) -> Result<(), Error> +pub fn execute(req: Request) -> Result<(), Error> where - R: Read, W: Write, { - let mut content = String::new(); - req.reader - .borrow_mut() - .read_to_string(&mut content) - .map_err(|_| Error::ReadData)?; - let mut writer = BufWriter::new(req.writer); let choose_sections = req @@ -40,7 +40,7 @@ where let mut current_sections: Option> = None; - for line in content.split_inclusive('\n') { + for line in req.content.split_inclusive('\n') { let new_line = if is_section_end(line) { current_sections = None; line.to_string() @@ -102,31 +102,29 @@ fn should_enable_variable(choose_sections: &[Section], current_sections: &[Secti 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 + || choose_sections .iter() .filter(|s| s.namespace.is_some()) .any(|s| { current_sections .iter() - .any(|s2| s.namespace == s2.namespace) + .any(|s2| s.namespace == s2.namespace && s.name != s2.name) }) } #[cfg(test)] mod tests { use super::*; - use std::cell::RefCell; 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 reader = RefCell::new(Cursor::new(input)); let mut output_data = vec![]; let writer = Cursor::new(&mut output_data); match execute(Request { - reader, + content: input, writer, sections: §ions.into_iter().map(String::from).collect::>(), }) { @@ -238,11 +236,27 @@ mod tests { } #[test] - fn should_disable_variables() { - assert!(should_disable_variable( + 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")] + )); + } } }