Compare commits

...

51 Commits
v0.1.0 ... main

Author SHA1 Message Date
Dmitriy Pleshevskiy be61316444
nix: fix overlays 2022-11-14 14:12:45 +03:00
Dmitriy Pleshevskiy 9eebd41db0
nix: use colored version by default 2022-11-12 17:07:01 +03:00
Dmitriy Pleshevskiy f4d4d46390
doc: update rules in readme 2022-11-12 17:06:43 +03:00
Dmitriy Pleshevskiy 4f583c160d
doc: update docker readme 2022-11-12 17:06:19 +03:00
Dmitriy Pleshevskiy 1f9e1d0e9e
bump vnetod version 2022-11-12 16:56:08 +03:00
Dmitriy Pleshevskiy c4e30fb179
cli: polish help output 2022-11-12 16:53:32 +03:00
Dmitriy Pleshevskiy 542d7f1d5c
nix: use colored version for docker 2022-11-12 16:37:16 +03:00
Dmitriy Pleshevskiy 8f81bddeaa
cli: add message for dry-run option 2022-11-12 16:36:53 +03:00
Dmitriy Pleshevskiy bbd1d47ba5
cli: add color option 2022-11-12 16:25:35 +03:00
Dmitriy Pleshevskiy 4e9ea6c683
add colors 2022-11-10 00:29:28 +03:00
Dmitriy Pleshevskiy 2be25a330f
domain: add optional on_line callback 2022-11-08 00:07:21 +03:00
Dmitriy Pleshevskiy 25d28531a1
cli: fallback to environment variable 2022-11-07 23:41:52 +03:00
Dmitriy Pleshevskiy 6eddd4c2a0
nix: use overlays instead of deprecated overlay 2022-11-07 23:38:51 +03:00
Dmitriy Pleshevskiy 7983beaea3
chore: change nix version format 2022-11-07 23:36:54 +03:00
Dmitriy Pleshevskiy 5f0c94af77
chore: change email 2022-11-07 23:29:25 +03:00
Dmitriy Pleshevskiy dc66d1a643
nix: move overlay to the top level 2022-09-25 17:02:16 +03:00
Dmitriy Pleshevskiy 1f57ca0c97
chore: update lock 2022-09-18 23:37:35 +03:00
Dmitriy Pleshevskiy 97b2897edc
bump version 2022-09-18 23:36:25 +03:00
Dmitriy Pleshevskiy ac3f245a1c
build docker via nix 2022-09-18 23:34:38 +03:00
Dmitriy Pleshevskiy 21e9e89de2
nix: add overlays to flake 2022-09-18 17:35:14 +03:00
Dmitriy Pleshevskiy 38be5079ff
nix: don't use naersik to build package 2022-09-18 17:23:59 +03:00
Dmitriy Pleshevskiy c0b010e279
deps/clap: disable default features...
- color - moved to a feature . To enable install vnetod with
- suggestions
2022-08-02 15:53:29 +03:00
Dmitriy Pleshevskiy 570a39734b
doc: update header 2022-08-02 15:41:38 +03:00
Dmitriy Pleshevskiy 6095b4cd3c
doc: add contact information 2022-08-02 15:13:50 +03:00
Dmitriy Pleshevskiy b67662f6ac
bump version 2022-08-02 11:56:56 +03:00
Dmitriy Pleshevskiy ca299ad81d
add help for each argument 2022-08-02 11:43:42 +03:00
Dmitriy Pleshevskiy 1a6dfb8d46
add dry-run option
Closes #2
2022-08-02 11:32:12 +03:00
Dmitriy Pleshevskiy 146c0697f2
add section info struct with computed values
Closes #13
2022-08-02 11:10:03 +03:00
Dmitriy Pleshevskiy 8f786cfd36
add support of comments
Closes #5
2022-08-02 00:07:44 +03:00
Dmitriy Pleshevskiy c6a7217ebd
docker: add cache to dockerfile
Closes #11
2022-08-01 23:26:44 +03:00
Dmitriy Pleshevskiy 6b23168d84
doc: fix copying url 2022-08-01 16:35:34 +03:00
Dmitriy Pleshevskiy e8f17add36
add docker hub readme 2022-08-01 16:28:04 +03:00
Dmitriy Pleshevskiy 73dd2d4883
doc: add missed -f flag to the command 2022-08-01 12:49:52 +03:00
Dmitriy Pleshevskiy 925f446cb1
add missed GPL header to each file 2022-08-01 00:26:38 +03:00
Dmitriy Pleshevskiy 5bd659ab46
bump version 2022-08-01 00:03:34 +03:00
Dmitriy Pleshevskiy d46da89a47
doc: add many rules to readme 2022-08-01 00:03:07 +03:00
Dmitriy Pleshevskiy 53b99905f8
doc: add install section to the readme 2022-07-31 23:55:58 +03:00
Dmitriy Pleshevskiy 5736fe2ea8
add docker and publish the current version to hub
Closes #10
2022-07-31 23:46:43 +03:00
Dmitriy Pleshevskiy ddfe229f9d
chore: cleanup github workflow after flake template 2022-07-31 23:29:48 +03:00
Dmitriy Pleshevskiy 34744cb5c8
add support of nix
Closes #9
2022-07-31 23:24:46 +03:00
Dmitriy Pleshevskiy c599e624f3
doc: add info about options to the readme 2022-07-31 13:14:11 +03:00
Dmitriy Pleshevskiy 00c1fc2954
bump version 2022-07-31 00:57:55 +03:00
Dmitriy Pleshevskiy 9a7f3bde5a
Merge pull request 'add namespaces' (#8) from namespace into main
Reviewed-on: #8
2022-07-30 21:55:30 +00:00
Dmitriy Pleshevskiy 832de949b6
implement Display and Error traits for our error objects 2022-07-31 00:54:38 +03:00
Dmitriy Pleshevskiy 2a6eab45c5
add disable variable util 2022-07-31 00:17:46 +03:00
Dmitriy Pleshevskiy 8fed9f979b
add utils to get enable section flag 2022-07-31 00:02:43 +03:00
Dmitriy Pleshevskiy d6e7b03d35
add namespaces
- extract domain logic
- extract cli logic
- cover domain logic with unit tests
2022-07-30 17:31:05 +03:00
Dmitriy Pleshevskiy 4cd7087f13
add output option
Closes #6
2022-07-29 19:02:17 +03:00
Dmitriy Pleshevskiy e94a9ca351
add support of multi-name in section
Closes #3
2022-07-29 19:00:09 +03:00
Dmitriy Pleshevskiy dd8f120152
add support of change dotenv file
Closes #1
2022-07-29 18:51:54 +03:00
Dmitriy Pleshevskiy 3972001328
add support of switch many states
deps: add clippy

Closes #7
2022-07-29 18:44:46 +03:00
21 changed files with 1291 additions and 86 deletions

View File

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

10
.gitignore vendored
View File

@ -1,3 +1,11 @@
# build
/target
/result
.env
# environments
.env*
!.env.example
# direnv
.envrc
.direnv

228
Cargo.lock generated
View File

@ -2,6 +2,232 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "clap"
version = "3.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44bbe24bbd31a185bc2c4f7c2abe80bea13a20d57ee4e55be70ac512bdc76417"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"once_cell",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "indexmap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "libc"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "once_cell"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]]
name = "os_str_bytes"
version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "unicode-ident"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vnetod"
version = "0.1.0"
version = "0.4.0"
dependencies = [
"atty",
"clap",
"termcolor",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@ -1,9 +1,10 @@
[package]
name = "vnetod"
description = "Dotenv state switcher"
version = "0.1.0"
description = "Are you still switching sections in your dotenv file manually? Try this dotenv section switcher"
version = "0.4.0"
edition = "2021"
license = "GPL-3.0+"
homepage = "https://github.com/pleshevskiy/vnetod/discussions"
repository = "https://git.pleshevski.ru/pleshevskiy/vnetod.git"
keywords = ["env", "dotenv", "switcher", "change"]
categories = ["command-line-interface", "config", "development-tools"]
@ -11,3 +12,9 @@ categories = ["command-line-interface", "config", "development-tools"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "3.2.15", default-features = false, features = ["std", "env", "derive"] }
atty = { version = "0.2.14", optional = true }
termcolor = { version = "1.1.3", optional = true }
[features]
color = ["clap/color", "dep:atty", "dep:termcolor"]

View File

@ -1,13 +1,20 @@
# vnetod
# vnetod*
Dotenv state switcher
<small><strong>*</strong> inverted word "dotenv"</small>
You can create many states in your `.env` and switch between them.
Are you still switching sections in your dotenv file manually? Try this dotenv
section switcher!
You can create many sections in your `.env` and switch between them.
Rules:
- State name starts on a new line with `###` symbols (Ex. `### local`)
- State ends if line is empty or contains a new state name.
- Section name starts on a new line with `###` symbols (Ex. `### local`)
- Section can contain multiple comma-separated names (Ex.
`### local,staging`)
- Each section name may specify a namespace (Ex. `### debug:on,dev:on`). If a
section doesn't contain a namespace, it's a global namespace.
- Section ends if line is empty or contains a new section name.
You can see the [full example].
@ -15,17 +22,80 @@ 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
```
This tool uses `.env` from your current location, but you can change this
behavior with the `-f` (`--file`) flag.
```sh
cp .env.example .env.properties
vnetod -f .env.properties local
```
And you can also change the output file with the `-o` (`--output`) flag, if you
don't want to overwrite the input file.
```sh
vnetod -f .env.example -o .env local
```
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
```
For more information, see the help.
```sh
vnetod --help
```
# Install
## Cargo
```sh
cargo install vnetod
```
## Docker
```sh
docker run --rm -it -v $PWD:/data pleshevskiy/vnetod --help
```
## Nix
```sh
nix run git+https://git.pleshevski.ru/pleshevskiy/vnetod -- --help
```
# Contact me
- [send feedback](https://github.com/pleshevskiy/vnetod/discussions)
- [make an issue](https://github.com/pleshevskiy/vnetod/issues)
- matrix: @pleshevskiy:matrix.org
- email: dmitriy@pleshevski.ru
# License
GNU General Public License v3.0 or later
See [COPYING](./COPYING) to see the full text.
See [COPYING] to see the full text.
[COPYING]: https://git.pleshevski.ru/pleshevskiy/vnetod/src/branch/main/COPYING

7
default.nix Normal file
View File

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).defaultNix

38
docker/README.md Normal file
View File

@ -0,0 +1,38 @@
# Supported tags
- latest
- 0.4
# vnetod*
<small><strong>*</strong> inverted word "dotenv"</small>
Are you still switching sections in your dotenv file manually? Try this dotenv
section switcher!
You can create many sections in your `.env` and switch between them.
Rules:
- Section name starts on a new line with `###` symbols (Ex. `### local`)
- Section can contain multiple comma-separated names (Ex.
`### local,staging`)
- Each section name may specify a namespace (Ex. `### debug:on,dev:on`). If a
section doesn't contain a namespace, it's a global namespace.
- Section ends if line is empty or contains a new section name.
# Usage
```sh
docker run --rm -it -v $PWD:/data pleshevskiy/vnetod -f .env.example -o .env staging db:local debug:on
```
For more details see [the repo](https://git.pleshevski.ru/pleshevskiy/vnetod)
# License
GNU General Public License v3.0 or later
See [COPYING] to see the full text.
[COPYING]: https://git.pleshevski.ru/pleshevskiy/vnetod/src/branch/main/COPYING

43
flake.lock Normal file
View File

@ -0,0 +1,43 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1659190188,
"narHash": "sha256-LudYrDFPFaQMW0l68TYkPWRPKmqpxIFU1nWfylIp9AQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a3fddd46a7f3418d7e3940ded94701aba569161d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

77
flake.nix Normal file
View File

@ -0,0 +1,77 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
let
inherit (builtins) fromTOML readFile substring;
cargoToml = fromTOML (readFile ./Cargo.toml);
version = "${cargoToml.package.version}+${substring 0 8 self.lastModifiedDate}.${self.shortRev or "dirty"}";
mkVnetod = { lib, rustPlatform, vnetodFeatures ? [ ], ... }:
rustPlatform.buildRustPackage {
name = "vnetod-${version}";
src = lib.cleanSource ./.;
cargoLock.lockFile = ./Cargo.lock;
buildFeatures = vnetodFeatures;
doCheck = true;
};
in
{
overlays = {
minimal = final: prev: {
vnetod = final.callPackage mkVnetod { };
};
default = final: prev: {
vnetod = final.callPackage mkVnetod {
vnetodFeatures = [ "color" ];
};
};
};
}
// flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
vnetod = pkgs.callPackage mkVnetod { vnetodFeatures = [ "color" ]; };
minimalVnetod = pkgs.callPackage mkVnetod { };
docker = pkgs.dockerTools.buildLayeredImage {
name = "pleshevskiy/vnetod";
tag = cargoToml.package.version;
config = {
Volumes."/data" = { };
WorkingDir = "/data";
Entrypoint = [ "${vnetod}/bin/vnetod" ];
};
};
mkApp = prog: {
type = "app";
program = "${vnetod}/bin/vnetod";
};
in
{
apps = {
default = mkApp vnetod;
minimal = mkApp minimalVnetod;
};
packages = {
inherit docker vnetod;
default = vnetod;
minimal = minimalVnetod;
};
devShell = pkgs.mkShell {
packages = with pkgs; [ cargo rustc rustfmt clippy rust-analyzer ];
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
};
});
}

7
shell.nix Normal file
View File

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).shellNix

124
src/cli.rs Normal file
View File

@ -0,0 +1,124 @@
//! Copyright (C) 2022, Dmitriy Pleshevskiy <dmitriy@pleshevski.ru>
//!
//! vnetod is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! vnetod is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with vnetod. If not, see <https://www.gnu.org/licenses/>.
//!
pub mod switch;
use std::path::PathBuf;
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(
author,
version,
about = "\
Dotenv state switcher
---------------------------------------------------------------------
vnetod Copyright (C) 2022 Dmitriy Pleshevskiy <dmitriy@pleshevski.ru>
This program comes with ABSOLUTELY NO WARRANTY;
This is free software, and you are welcome to redistribute it
under certain conditions;
---------------------------------------------------------------------
"
)]
pub struct Args {
#[clap(
short = 'f',
long,
default_value = ".env",
env = "VNETOD_FILE",
help = "Change source file with environment variables."
)]
pub file: PathBuf,
#[clap(
short = 'o',
long,
help = "Change output file with modified environment variables. It uses `file` argument by default if the output is not specified."
)]
pub output: Option<PathBuf>,
#[clap(
long,
help = "Writes modified environment variables to stdout instead of a file. `Output` argument will be ignored."
)]
pub dry_run: bool,
#[clap(
value_parser,
help = "Environment varible sections that will be enabled."
)]
pub sections: Vec<String>,
#[cfg(feature = "color")]
#[clap(
long,
value_enum,
default_value = "auto",
help = "This flag controls when to use colors.",
long_help = "
This flag controls when to use colors. The default setting is 'auto', which
means vnetod will try to guess when to use colors. For example, if vnetod is
printing to a terminal, then it will use colors, but if it is redirected to a
file or a pipe, then it will suppress color output. vnetod will suppress color
output in some other circumstances as well. For example, if the TERM
environment variable is not set or set to 'dumb', then vnetod will not use
colors.
The possible values for this flag are:
never Colors will never be used.
auto The default. vnetod tries to be smart.
always Colors will always be used regardless of where output is sent.
ansi Like 'always', but emits ANSI escapes (even in a Windows console).
"
)]
pub color: ColorVariant,
}
#[cfg(feature = "color")]
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum ColorVariant {
Auto,
Always,
Ansi,
Never,
}
#[cfg(feature = "color")]
impl From<ColorVariant> for termcolor::ColorChoice {
fn from(col: ColorVariant) -> Self {
match col {
ColorVariant::Never => Self::Never,
ColorVariant::Always => Self::Always,
ColorVariant::Ansi => Self::AlwaysAnsi,
ColorVariant::Auto => {
if atty::is(atty::Stream::Stdout) {
// Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
// - If `NO_COLOR` is set to any value, then colors will be suppressed.
// - If `TERM` is set to dumb, then colors will be suppressed.
// - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
//
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
Self::Auto
} else {
// Colors should be deactivated if the terminal is not a tty.
Self::Never
}
}
}
}
}

91
src/cli/switch.rs Normal file
View File

@ -0,0 +1,91 @@
//! Copyright (C) 2022, Dmitriy Pleshevskiy <dmitriy@pleshevski.ru>
//!
//! vnetod is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! vnetod is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with vnetod. If not, see <https://www.gnu.org/licenses/>.
//!
#[cfg(feature = "color")]
use termcolor::{Color, ColorSpec, StandardStream, WriteColor};
use crate::{cli::Args, domain};
use std::fs::File;
use std::io::Write;
#[derive(Debug)]
pub enum Error {
OpenFile,
WriteFile,
}
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::WriteFile => f.write_str("Cannot write file"),
}
}
}
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)?;
if args.dry_run {
println!("Your file will be changed to the following")
}
let fs_writer = (!args.dry_run)
.then(|| {
File::create(args.output.as_ref().unwrap_or(&args.file)).map_err(|_| Error::OpenFile)
})
.transpose()?;
#[cfg(feature = "color")]
let color = args.color.clone();
println!();
let new_content = domain::switch::execute(domain::switch::Request {
content: &content.trim(),
sections: &args.sections,
on_line: Some(Box::new(move |line| {
#[cfg(feature = "color")]
print_line(line, color.clone());
#[cfg(not(feature = "color"))]
print!("{}", line)
})),
});
println!();
if let Some(mut fs_writer) = fs_writer {
fs_writer
.write_all(new_content.as_bytes())
.map_err(|_| Error::WriteFile)?;
}
Ok(())
}
#[cfg(feature = "color")]
fn print_line(line: &String, color: crate::cli::ColorVariant) {
let mut stdout = StandardStream::stdout(color.into());
let color = line
.starts_with("###")
.then_some(Color::Yellow)
.or_else(|| (!line.starts_with("#")).then_some(Color::Green));
stdout.set_color(ColorSpec::new().set_fg(color)).ok();
write!(&mut stdout, "{}", line).unwrap();
stdout.reset().ok();
}

50
src/domain.rs Normal file
View File

@ -0,0 +1,50 @@
//! Copyright (C) 2022, Dmitriy Pleshevskiy <dmitriy@pleshevski.ru>
//!
//! vnetod is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! vnetod is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with vnetod. If not, see <https://www.gnu.org/licenses/>.
//!
pub mod switch;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Section {
namespace: Option<String>,
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))
}
}
pub struct SectionInfo {
enable_variable: bool,
disable_variable: bool,
}

276
src/domain/switch.rs Normal file
View File

@ -0,0 +1,276 @@
//! Copyright (C) 2022, Dmitriy Pleshevskiy <dmitriy@pleshevski.ru>
//!
//! vnetod is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! vnetod is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with vnetod. If not, see <https://www.gnu.org/licenses/>.
//!
use super::{Section, SectionInfo};
pub type OnLineFn = Box<dyn Fn(&String)>;
pub struct Request<'args> {
pub content: &'args str,
pub sections: &'args [String],
pub on_line: Option<OnLineFn>,
}
pub fn execute(req: Request) -> String {
let choose_sections = req
.sections
.iter()
.map(|s| Section::parse(s.as_str()))
.collect::<Vec<_>>();
let mut current_section: Option<SectionInfo> = None;
let mut new_content = String::new();
for line in req.content.split_inclusive('\n') {
let new_line = if is_section_end(line) {
current_section = None;
line.to_string()
} else if let Some(section_info) = line.strip_prefix("### ") {
current_section = section_info.split_whitespace().next().map(|r| {
let current_sections = r.split(',').map(Section::parse).collect::<Vec<_>>();
SectionInfo {
enable_variable: should_enable_variable(&choose_sections, &current_sections),
disable_variable: should_disable_variable(&choose_sections, &current_sections),
}
});
line.to_string()
} else if let Some(section_info) = current_section.as_ref() {
let trimmed_line = line.trim_start_matches(['#', ' ']);
let is_var = is_variable(trimmed_line);
if is_var && section_info.enable_variable {
String::from(trimmed_line)
} else if is_var && section_info.disable_variable {
format!("# {}", trimmed_line)
} else {
line.to_string()
}
} else {
line.to_string()
};
new_content.push_str(&new_line);
if let Some(on_line) = req.on_line.as_ref() {
on_line(&new_line);
}
}
new_content
}
fn is_variable(trimmed_line: &str) -> bool {
trimmed_line
.chars()
.filter(|ch| (*ch != '_' && ch.is_ascii_punctuation()) || ch.is_whitespace())
.enumerate()
.find(|(_, ch)| *ch == '=')
.map_or(false, |(i, _)| i == 0)
}
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::<Vec<_>>();
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::*;
const BASE_ENV: &str = include_str!("../../test_data/base_env");
fn make_test(input: &str, expected_output: &str, sections: Vec<&str>) {
let output_data = execute(Request {
content: input,
sections: &sections.into_iter().map(String::from).collect::<Vec<_>>(),
on_line: None,
});
assert_eq!(
output_data.lines().collect::<Vec<_>>(),
expected_output.lines().collect::<Vec<_>>()
);
}
#[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")]
));
}
#[test]
fn should_check_line_on_variable() {
let test_cases = [
("VAR=10", true),
("THIS_IS_MY_VAR='hello world'", true),
("hello world", false),
("staging/production", false),
("staging/production=value", false),
];
for (input, expected) in test_cases {
assert_eq!(is_variable(input), expected);
}
}
}
}

View File

@ -1,4 +1,4 @@
//! Copyright (C) 2022, Dmitriy Pleshevskiy <dmitriy@ideascup.me>
//! Copyright (C) 2022, Dmitriy Pleshevskiy <dmitriy@pleshevski.ru>
//!
//! vnetod is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
@ -13,75 +13,17 @@
//! You should have received a copy of the GNU General Public License
//! along with vnetod. If not, see <https://www.gnu.org/licenses/>.
//!
#![deny(clippy::all, clippy::pedantic)]
use std::{
fs::File,
io::{BufWriter, Write},
};
mod cli;
mod domain;
use clap::Parser;
fn main() -> Result<(), cli::switch::Error> {
let cli = cli::Args::parse();
cli::switch::execute(&cli)?;
fn main() -> Result<(), Error> {
match std::env::args().nth(1) {
Some(name) => change_env_layout(&name)?,
_ => print_help(),
};
Ok(())
}
fn change_env_layout(name: &str) -> Result<(), Error> {
let content = std::fs::read_to_string(".env").map_err(|_| Error::OpenFile)?;
let mut writer = File::create(".env")
.map_err(|_| Error::OpenFile)
.map(BufWriter::new)?;
let mut current_section_name: Option<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(String::from);
line.to_string()
} else if let Some(cur_name) = current_section_name.clone() {
if cur_name == name {
String::from(line.trim_start_matches(['#', ' ']))
} else {
format!("# {}", line.trim_start_matches(['#', ' ']))
}
} 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 print_help() {
eprintln!(
"\
USAGE
vnetod [STATE_NAME] - switch environment variable state by name
---
vnetod Copyright (C) 2022 Dmitriy Pleshevskiy <dmitriy@ideascup.me>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
"
)
}
#[derive(Debug)]
enum Error {
OpenFile,
WriteData,
}

37
test_data/all_env Normal file
View File

@ -0,0 +1,37 @@
VITE_STRIPE_PK=
VITE_SENTRY_ENABLED=
VITE_SENTRY_DSN=
### local,staging
# staging/production
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=

37
test_data/base_env Normal file
View File

@ -0,0 +1,37 @@
VITE_STRIPE_PK=
VITE_SENTRY_ENABLED=
VITE_SENTRY_DSN=
### local,staging
# staging/production
# 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,37 @@
VITE_STRIPE_PK=
VITE_SENTRY_ENABLED=
VITE_SENTRY_DSN=
### local,staging
# staging/production
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,37 @@
VITE_STRIPE_PK=
VITE_SENTRY_ENABLED=
VITE_SENTRY_DSN=
### local,staging
# staging/production
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,37 @@
VITE_STRIPE_PK=
VITE_SENTRY_ENABLED=
VITE_SENTRY_DSN=
### local,staging
# staging/production
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,37 @@
VITE_STRIPE_PK=
VITE_SENTRY_ENABLED=
VITE_SENTRY_DSN=
### local,staging
# staging/production
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=