Skip to content

Commit

Permalink
Add export/dotenv mode.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmalloc committed Mar 11, 2023
1 parent 3fa763b commit b95a691
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 9 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ The format is based on [Keep a Changelog], and this project adheres to

## Unreleased

### Added

- Added `export/dotenv` mode, which renders environment variables and their
current values in [dotenv] format

<!-- references -->

[dotenv]: https://github.com/motdotla/dotenv

### Fixed

- Fixed issue where durations were rendered with tailing zero-valued minute components
Expand Down
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,37 @@ defer cancel()
// do HTTP request ...
```

### Automatic Usage Documentation
## Modes of Operation

Ferrite can automatically generate Markdown documentation for the declared
environment variables by executing the application with the `FERRITE_MODE`
environment variable set to `usage/markdown`.
By default, calling `Init()` operates in "validation" mode. There are several
other modes that can be used to gain insight into the application's use of
environment variables.

This causes the `ferrite.Init()` function to print the Markdown to `STDOUT`, and
then exit the process before the application code is executed.
Modes are selected by setting the `FERRITE_MODE` environment variable.

### Other Implementations
### `validate` mode

This is the default mode. If one or more environment variables are invalid, this
mode renders a description of all declared environment variables and their
associated values and validation failures to `STDERR`, then exits the process
with a non-zero exit code.

It also shows warnings if deprecated environment variables are used.

### `usage/markdown` mode

This mode renders Markdown documentation about the environment variables to
`STDOUT`. The output is designed to be included in the application's `README.md`
file or a similar file.

### `export/dotenv` mode

This mode renders environment variables to `STDOUT` in a format suitable for use
with tools like [`dotenv`](https://github.com/motdotla/dotenv) and the
[`env_file`](https://docs.docker.com/compose/compose-file/#env_file) directive
in Docker compose files.

## Other Implementations

[Austenite](https://github.com/eloquent/austenite) is a TypeScript
library with similar features to Ferrite.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/dogmatiq/ferrite
go 1.19

require (
github.com/dogmatiq/iago v0.4.0
github.com/jmalloc/gomegax v0.0.0-20200507221434-64fca4c0e03a
github.com/mattn/go-runewidth v0.0.14
github.com/onsi/ginkgo/v2 v2.9.0
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dogmatiq/iago v0.4.0 h1:57nZqVT34IZxtCZEW/RFif7DNUEjMXgevfr/Mmd0N8I=
github.com/dogmatiq/iago v0.4.0/go.mod h1:fishMWBtzYcjgis6d873VTv9kFm/wHYLOzOyO9ECBDc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand All @@ -24,15 +26,19 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jmalloc/gomegax v0.0.0-20200507221434-64fca4c0e03a h1:Gk7Gkwl1KUJII/FiAjvBjRgEz/lpvTV8kNYp+9jdpuk=
github.com/jmalloc/gomegax v0.0.0-20200507221434-64fca4c0e03a/go.mod h1:TZpc8ObQEKqTuy1/VXpPRfcMU80QFDU4zK3nchXts/k=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.10.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.27.3 h1:5VwIwnBY3vbBDOJrNtA4rVdiTZCsq9B5F12pvy1Drmk=
github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw=
Expand Down Expand Up @@ -71,8 +77,11 @@ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
Expand Down
7 changes: 5 additions & 2 deletions init.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/dogmatiq/ferrite/internal/mode"
"github.com/dogmatiq/ferrite/internal/mode/export/dotenv"
"github.com/dogmatiq/ferrite/internal/mode/usage/markdown"
"github.com/dogmatiq/ferrite/internal/mode/validate"
)
Expand All @@ -25,10 +26,12 @@ func Init(options ...InitOption) {
}

switch m := os.Getenv("FERRITE_MODE"); m {
case "usage/markdown":
markdown.Run(opts)
case "validate", "":
validate.Run(opts)
case "usage/markdown":
markdown.Run(opts)
case "export/dotenv":
dotenv.Run(opts)
default:
fmt.Fprintf(opts.Err, "unrecognized FERRITE_MODE (%s)\n", m)
opts.Exit(1)
Expand Down
3 changes: 3 additions & 0 deletions internal/mode/export/dotenv/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package dotenv is a Ferrite mode that exports the environment variables to a
// file suitable use as a .env file.
package dotenv
71 changes: 71 additions & 0 deletions internal/mode/export/dotenv/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dotenv

import (
"strings"

"github.com/dogmatiq/ferrite/internal/mode"
"github.com/dogmatiq/iago/must"
)

// Run generates and env file describing the environment variables and their
// current values.
func Run(opts mode.Options) {
for i, v := range opts.Registry.Variables() {
s := v.Spec()

if i > 0 {
must.Fprintf(opts.Out, "\n")
}

must.Fprintf(opts.Out, "# %s (", s.Description())

if def, ok := s.Default(); ok {
x := def.Quote()
if s.IsSensitive() {
x = strings.Repeat("*", len(def.String))
}
must.Fprintf(opts.Out, "default: %s", x)
} else if s.IsDeprecated() {
must.Fprintf(opts.Out, "deprecated")
} else if s.IsRequired() {
must.Fprintf(opts.Out, "required")
} else {
must.Fprintf(opts.Out, "optional")
}

if s.IsSensitive() {
must.Fprintf(opts.Out, ", sensitive")
}

must.Fprintf(opts.Out, ")\n")
must.Fprintf(opts.Out, "%s=", s.Name())

value, ok, err := v.Value()
if err != nil {
must.Fprintf(
opts.Out,
" # %s is invalid: %s",
err.Literal().Quote(),
err.Unwrap(),
)
} else if ok && v.IsExplicit() && !s.IsSensitive() {
must.Fprintf(
opts.Out,
"%s",
value.Verbatim().Quote(),
)

if value.Verbatim() != value.Canonical() {
must.Fprintf(
opts.Out,
" # equivalent to %s",
value.Canonical().Quote(),
)
}
}

must.Fprintf(opts.Out, "\n")
}

opts.Exit(0)
}
112 changes: 112 additions & 0 deletions modedotenvexample_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package ferrite_test

import (
"os"
"time"

"github.com/dogmatiq/ferrite"
)

func ExampleInit_exportDotEnvFile() {
defer example()()

os.Setenv("FERRITE_BOOL", "true")
ferrite.
Bool("FERRITE_BOOL", "example bool").
Required()

os.Setenv("FERRITE_DURATION", "620s")
ferrite.
Duration("FERRITE_DURATION", "example duration").
WithDefault(1 * time.Hour).
Required()

ferrite.
Enum("FERRITE_ENUM", "example enum").
WithMembers("foo", "bar", "baz").
WithDefault("bar").
Required()

os.Setenv("FERRITE_NETWORK_PORT", "8080")
ferrite.
NetworkPort("FERRITE_NETWORK_PORT", "example network port").
Optional()

ferrite.
Float[float32]("FERRITE_NUM_FLOAT", "example floating-point").
Required()

ferrite.
Signed[int16]("FERRITE_NUM_SIGNED", "example signed integer").
Required()

ferrite.
Unsigned[uint16]("FERRITE_NUM_UNSIGNED", "example unsigned integer").
Required()

os.Setenv("FERRITE_STRING", "hello, world!")
ferrite.
String("FERRITE_STRING", "example string").
Required()

os.Setenv("FERRITE_STRING_SENSITIVE", "hunter2")
ferrite.
String("FERRITE_STRING_SENSITIVE", "example sensitive string").
WithDefault("password").
WithSensitiveContent().
Required()

os.Setenv("FERRITE_SVC_SERVICE_HOST", "host.example.org")
os.Setenv("FERRITE_SVC_SERVICE_PORT", "443")
ferrite.
KubernetesService("ferrite-svc").
Deprecated()

os.Setenv("FERRITE_URL", "https//example.org")
ferrite.
URL("FERRITE_URL", "example URL").
Required()

// Tell ferrite to export an env file containing the environment variables.
os.Setenv("FERRITE_MODE", "export/dotenv")

ferrite.Init()

// Output:
// # example bool (required)
// FERRITE_BOOL=true
//
// # example duration (default: 1h)
// FERRITE_DURATION=620s # equivalent to 10m20s
//
// # example enum (default: bar)
// FERRITE_ENUM=
//
// # example network port (optional)
// FERRITE_NETWORK_PORT=8080
//
// # example floating-point (required)
// FERRITE_NUM_FLOAT=
//
// # example signed integer (required)
// FERRITE_NUM_SIGNED=
//
// # example unsigned integer (required)
// FERRITE_NUM_UNSIGNED=
//
// # example string (required)
// FERRITE_STRING='hello, world!'
//
// # example sensitive string (default: ********, sensitive)
// FERRITE_STRING_SENSITIVE=
//
// # kubernetes "ferrite-svc" service host (deprecated)
// FERRITE_SVC_SERVICE_HOST=host.example.org
//
// # kubernetes "ferrite-svc" service port (deprecated)
// FERRITE_SVC_SERVICE_PORT=443
//
// # example URL (required)
// FERRITE_URL= # https//example.org is invalid: URL must have a scheme
// <process exited successfully>
}

0 comments on commit b95a691

Please sign in to comment.