add namespaces

- extract domain logic
- extract cli logic
- cover domain logic with unit tests
This commit is contained in:
Dmitriy Pleshevskiy 2022-07-30 17:31:05 +03:00
parent 4cd7087f13
commit d6e7b03d35
Signed by: pleshevskiy
GPG key ID: 1B59187B161C0215
15 changed files with 439 additions and 64 deletions

View file

@ -1,8 +1,17 @@
VITE_STRIPE_PK= VITE_STRIPE_PK=
VITE_SENTRY_ENABLED= VITE_SENTRY_ENABLED=
VITE_SENTRY_DSN= VITE_SENTRY_DSN=
# staging / production ### local,staging
VITE_SENTRY_ENV=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_EMAIL='owner@cypress.test'
CYPRESS_CRED_PASSWORD='12345678' CYPRESS_CRED_PASSWORD='12345678'
@ -10,13 +19,18 @@ CYPRESS_TEST_PRIVATE_TOKEN='12345678'
### local ### local
VITE_API_BASE_URL='http://localhost:1337/api' 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' CYPRESS_API_BASE_URL='http://localhost:1337/api'
# this variable will not switch # this variable will not switch
DEBUG=1 GRAPHQL_PLAYGROUND=1
### staging ### staging
# VITE_API_BASE_URL='https://app.develop.staging.example.com/api' # 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' # CYPRESS_API_BASE_URL='https://app.develop.staging.example.com/api'
### debug:on,local
# DEBUG=1
### debug:off,staging
# DEBUG=

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target /target
.env .env*
!.env.example

View file

@ -12,3 +12,5 @@ categories = ["command-line-interface", "config", "development-tools"]
[dependencies] [dependencies]
clap = { version = "3.2.15", features = ["derive"] } clap = { version = "3.2.15", features = ["derive"] }
[features]

View file

@ -15,11 +15,20 @@ You can see the [full example].
# Usage # Usage
Basic usage
```sh ```sh
cp .env.example .env cp .env.example .env
vnetod local # choose local state vnetod local # enable local section
vnetod staging # choose staging state vnetod staging # enable staging section
vnetod - # disable all states vnetod local debug # enable local and debug sections
vnetod # disable all sections
```
Namespaces
```sh
vnetod db:local debug:on
``` ```
# License # License

View file

@ -14,6 +14,8 @@
//! along with vnetod. If not, see <https://www.gnu.org/licenses/>. //! along with vnetod. If not, see <https://www.gnu.org/licenses/>.
//! //!
pub mod switch;
use std::path::PathBuf; use std::path::PathBuf;
use clap::Parser; use clap::Parser;
@ -40,5 +42,5 @@ pub struct Args {
pub output: Option<PathBuf>, pub output: Option<PathBuf>,
#[clap(value_parser)] #[clap(value_parser)]
pub section_names: Vec<String>, pub sections: Vec<String>,
} }

21
src/cli/switch.rs Normal file
View file

@ -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)
}

26
src/domain.rs Normal file
View file

@ -0,0 +1,26 @@
pub mod switch;
#[derive(Debug, Clone, Eq)]
struct Section {
namespace: Option<String>,
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)
}
}

136
src/domain/switch.rs Normal file
View file

@ -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<R>,
pub writer: W,
pub sections: &'args [String],
}
pub fn execute<R, W>(req: Request<R, W>) -> 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::<Vec<_>>();
dbg!(&choose_sections);
let choose_namespaces = choose_sections
.iter()
.map(|s| s.namespace.clone())
.collect::<Vec<_>>();
let mut current_sections: Option<Vec<Section>> = 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, &current_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: &sections.into_iter().map(String::from).collect::<Vec<_>>(),
}) {
Ok(()) => {
let output = String::from_utf8(output_data).unwrap();
assert_eq!(
output.lines().collect::<Vec<_>>(),
expected_output.lines().collect::<Vec<_>>()
);
}
_ => 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"],
);
}
}

View file

@ -16,66 +16,14 @@
#![deny(clippy::all, clippy::pedantic)] #![deny(clippy::all, clippy::pedantic)]
mod cli; mod cli;
mod domain;
use std::{
fs::File,
io::{BufWriter, Write},
};
use clap::Parser; use clap::Parser;
fn main() -> Result<(), Error> { fn main() -> Result<(), cli::switch::Error> {
let cli = cli::Args::parse(); let cli = cli::Args::parse();
change_env_layout(&cli)?; cli::switch::execute(&cli)?;
Ok(()) 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<Vec<String>> = 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,
}

36
test_data/all_env Normal file
View file

@ -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=

36
test_data/base_env Normal file
View file

@ -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=

View file

@ -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=

View file

@ -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=

View file

@ -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=

View file

@ -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=