Skip to content

Commit

Permalink
Merge pull request #50 from ilyakaznacheev/required-fields
Browse files Browse the repository at this point in the history
Required fields
  • Loading branch information
ilyakaznacheev committed Jun 14, 2020
2 parents fec9bfa + 47aa2e3 commit 4eefa52
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 9 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Here remote host and port may change in a distributed system architecture. Field

### Description

You can get descriptions of all environment variables to use them in help documentation.
You can get descriptions of all environment variables to use them in the help documentation.

```go
import github.com/ilyakaznacheev/cleanenv
Expand Down Expand Up @@ -169,10 +169,11 @@ Environment variables:

## Model Format

Library uses tags to configure model of configuration structure. There are following tags:
Library uses tags to configure the model of configuration structure. There are the following tags:

- `env="<name>"` - environment variable name (e.g. `env="PORT"`);
- `env-upd` - flag to mark a field as updatable. Run `UpdateEnv(&cfg)` to refresh updatable variables from environment;
- `env-required` - flag to mark a field as required. If set will return an error during environment parsing when the flagged as required field is empty (default Go value). Tag `env-default` is ignored in this case;
- `env-default="<value>"` - default value. If the field wasn't filled from the environment variable default value will be used instead;
- `env-separator="<value>"` - custom list and map separator. If not set, the default separator `,` will be used;
- `env-description="<value>"` - environment variable description;
Expand Down Expand Up @@ -249,7 +250,7 @@ There are several most popular config file formats supported:

## Integration

Package can be used with many other solutions. To make it more useful, we made some helpers.
The package can be used with many other solutions. To make it more useful, we made some helpers.

### Flag

Expand Down
42 changes: 36 additions & 6 deletions cleanenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ const (
DefaultSeparator = ","
)

// Supported tags
const (
// Name of the environment variable or a list of names
TagEnv = "env"
// Value parsing layout (for types like time.Time)
TagEnvLayout = "env-layout"
// Default value
TagEnvDefault = "env-default"
// Custom list and map separator
TagEnvSeparator = "env-separator"
// Environment variable description
TagEnvDescription = "env-description"
// Flag to mark a field as updatable
TagEnvUpd = "env-upd"
// Flag to mark a field as required
TagEnvRequired = "env-required"
)

// Setter is an interface for a custom value setter.
//
// To implement a custom value setter you need to add a SetValue function to your type that will receive a string raw value:
Expand Down Expand Up @@ -179,12 +197,14 @@ func parseENV(r io.Reader, _ interface{}) error {
// structMeta is a structure metadata entity
type structMeta struct {
envList []string
fieldName string
fieldValue reflect.Value
defValue *string
layout *string
separator string
description string
updatable bool
required bool
}

// isFieldValueZero determines if fieldValue empty or not
Expand Down Expand Up @@ -230,7 +250,7 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
continue
}
// process time.Time
if l, ok := fType.Tag.Lookup("env-layout"); ok {
if l, ok := fType.Tag.Lookup(TagEnvLayout); ok {
layout = &l
}
}
Expand All @@ -240,32 +260,36 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
continue
}

if def, ok := fType.Tag.Lookup("env-default"); ok {
if def, ok := fType.Tag.Lookup(TagEnvDefault); ok {
defValue = &def
}

if sep, ok := fType.Tag.Lookup("env-separator"); ok {
if sep, ok := fType.Tag.Lookup(TagEnvSeparator); ok {
separator = sep
} else {
separator = DefaultSeparator
}

_, upd := fType.Tag.Lookup("env-upd")
_, upd := fType.Tag.Lookup(TagEnvUpd)

_, required := fType.Tag.Lookup(TagEnvRequired)

envList := make([]string, 0)

if envs, ok := fType.Tag.Lookup("env"); ok && len(envs) != 0 {
if envs, ok := fType.Tag.Lookup(TagEnv); ok && len(envs) != 0 {
envList = strings.Split(envs, DefaultSeparator)
}

metas = append(metas, structMeta{
envList: envList,
fieldName: s.Type().Field(idx).Name,
fieldValue: s.Field(idx),
defValue: defValue,
layout: layout,
separator: separator,
description: fType.Tag.Get("env-description"),
description: fType.Tag.Get(TagEnvDescription),
updatable: upd,
required: required,
})
}

Expand Down Expand Up @@ -302,6 +326,12 @@ func readEnvVars(cfg interface{}, update bool) error {
}
}

if rawValue == nil && meta.required && meta.isFieldValueZero() {
err := fmt.Errorf("field %q is required but the value is not provided",
meta.fieldName)
return err
}

if rawValue == nil && meta.isFieldValueZero() {
rawValue = meta.defValue
}
Expand Down
12 changes: 12 additions & 0 deletions cleanenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ func TestReadEnvVars(t *testing.T) {
Time7 map[string]time.Time `env:"TEST_TIME7" env-separator:"|"`
}

type Required struct {
NotRequired int `env:"NOT_REQUIRED"`
Required int `env:"REQUIRED" env-required:"true"`
}

tests := []struct {
name string
env map[string]string
Expand Down Expand Up @@ -285,6 +290,13 @@ func TestReadEnvVars(t *testing.T) {
want: ta,
wantErr: true,
},

{
name: "required error",
cfg: &Required{},
want: &Required{},
wantErr: true,
},
}

for _, tt := range tests {
Expand Down

0 comments on commit 4eefa52

Please sign in to comment.