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=