Skip to content

Commit

Permalink
Drop regexp for performance (#74)
Browse files Browse the repository at this point in the history
* Update comment syntax to Go 1.19

* Drop regexp for performance

Fixes GH-72
  • Loading branch information
sethvargo committed Aug 7, 2022
1 parent cbe83d6 commit 00d8c81
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 40 deletions.
106 changes: 66 additions & 40 deletions envconfig.go
Expand Up @@ -14,55 +14,54 @@

// Package envconfig populates struct fields based on environment variable
// values (or anything that responds to "Lookup"). Structs declare their
// environment dependencies using the `env` tag with the key being the name of
// environment dependencies using the "env" tag with the key being the name of
// the environment variable, case sensitive.
//
// type MyStruct struct {
// A string `env:"A"` // resolves A to $A
// B string `env:"B,required"` // resolves B to $B, errors if $B is unset
// C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
// type MyStruct struct {
// A string `env:"A"` // resolves A to $A
// B string `env:"B,required"` // resolves B to $B, errors if $B is unset
// C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
//
// D string `env:"D,required,default=foo"` // error, cannot be required and default
// E string `env:""` // error, must specify key
// }
// D string `env:"D,required,default=foo"` // error, cannot be required and default
// E string `env:""` // error, must specify key
// }
//
// All built-in types are supported except Func and Chan. If you need to define
// a custom decoder, implement Decoder:
//
// type MyStruct struct {
// field string
// }
// type MyStruct struct {
// field string
// }
//
// func (v *MyStruct) EnvDecode(val string) error {
// v.field = fmt.Sprintf("PREFIX-%s", val)
// return nil
// }
// func (v *MyStruct) EnvDecode(val string) error {
// v.field = fmt.Sprintf("PREFIX-%s", val)
// return nil
// }
//
// In the environment, slices are specified as comma-separated values:
//
// export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}
// export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}
//
// In the environment, maps are specified as comma-separated key:value pairs:
//
// export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}
// export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}
//
// If you need to modify environment variable values before processing, you can
// specify a custom mutator:
//
// type Config struct {
// Password `env:"PASSWORD_SECRET"`
// }
// type Config struct {
// Password `env:"PASSWORD_SECRET"`
// }
//
// func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
// if strings.HasPrefix(value, "secret://") {
// return secretmanager.Resolve(ctx, value) // example
// }
// return value, nil
// }
//
// var config Config
// ProcessWith(&config, OsLookuper(), resolveSecretFunc)
// func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
// if strings.HasPrefix(value, "secret://") {
// return secretmanager.Resolve(ctx, value) // example
// }
// return value, nil
// }
//
// var config Config
// ProcessWith(&config, OsLookuper(), resolveSecretFunc)
package envconfig

import (
Expand All @@ -74,7 +73,6 @@ import (
"fmt"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"time"
Expand All @@ -95,8 +93,6 @@ const (
defaultSeparator = ":"
)

var envvarNameRe = regexp.MustCompile(`\A[a-zA-Z_][a-zA-Z0-9_]*\z`)

// Error is a custom error type for errors returned by envconfig.
type Error string

Expand Down Expand Up @@ -138,7 +134,7 @@ func (o *osLookuper) Lookup(key string) (string, bool) {
return os.LookupEnv(key)
}

// OsLookuper returns a lookuper that uses the environment (os.LookupEnv) to
// OsLookuper returns a lookuper that uses the environment ([os.LookupEnv]) to
// resolve values.
func OsLookuper() Lookuper {
return new(osLookuper)
Expand Down Expand Up @@ -203,12 +199,11 @@ func MultiLookuper(lookupers ...Lookuper) Lookuper {
// Decoder is an interface that custom types/fields can implement to control how
// decoding takes place. For example:
//
// type MyType string
//
// func (mt MyType) EnvDecode(val string) error {
// return "CUSTOM-"+val
// }
// type MyType string
//
// func (mt MyType) EnvDecode(val string) error {
// return "CUSTOM-"+val
// }
type Decoder interface {
EnvDecode(val string) error
}
Expand All @@ -229,7 +224,7 @@ type options struct {
Required bool
}

// Process processes the struct using the environment. See ProcessWith for a
// Process processes the struct using the environment. See [ProcessWith] for a
// more customizable version.
func Process(ctx context.Context, i interface{}) error {
return ProcessWith(ctx, i, OsLookuper())
Expand Down Expand Up @@ -427,7 +422,7 @@ func keyAndOpts(tag string) (string, *options, error) {
parts := strings.Split(tag, ",")
key, tagOpts := strings.TrimSpace(parts[0]), parts[1:]

if key != "" && !envvarNameRe.MatchString(key) {
if key != "" && !validateEnvName(key) {
return "", nil, fmt.Errorf("%q: %w ", key, ErrInvalidEnvvarName)
}

Expand Down Expand Up @@ -689,3 +684,34 @@ func processField(v string, ef reflect.Value, delimiter, separator string, noIni

return nil
}

// validateEnvName validates the given string conforms to being a valid
// environment variable.
//
// Per IEEE Std 1003.1-2001 environment variables consist solely of uppercase
// letters, digits, and _, and do not begin with a digit.
func validateEnvName(s string) bool {
if s == "" {
return false
}

for i, r := range s {
if (i == 0 && !isLetter(r)) || (!isLetter(r) && !isNumber(r) && r != '_') {
return false
}
}

return true
}

// isLetter returns true if the given rune is a letter between a-z,A-Z. This is
// different than unicode.IsLetter which includes all L character cases.
func isLetter(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}

// isNumber returns true if the given run is a number between 0-9. This is
// different than unicode.IsNumber in that it only allows 0-9.
func isNumber(r rune) bool {
return r >= '0' && r <= '9'
}
73 changes: 73 additions & 0 deletions envconfig_test.go
Expand Up @@ -2436,3 +2436,76 @@ func TestProcessWith(t *testing.T) {
})
}
}

func TestValidateEnvName(t *testing.T) {
t.Parallel()

cases := []struct {
name string
in string
exp bool
}{
{
name: "empty",
in: "",
exp: false,
},
{
name: "space",
in: " ",
exp: false,
},
{
name: "digit_start",
in: "1FOO",
exp: false,
},
{
name: "emoji_start",
in: "🚀",
exp: false,
},
{
name: "lowercase_start",
in: "f",
exp: true,
},
{
name: "lowercase",
in: "foo",
exp: true,
},
{
name: "uppercase_start",
in: "F",
exp: true,
},
{
name: "uppercase",
in: "FOO",
exp: true,
},
{
name: "emoji_middle",
in: "FOO🚀",
exp: false,
},
{
name: "space_middle",
in: "FOO BAR",
exp: false,
},
}

for _, tc := range cases {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

if got, want := validateEnvName(tc.in), tc.exp; got != want {
t.Errorf("expected %q to be %t (got %t)", tc.in, want, got)
}
})
}
}

0 comments on commit 00d8c81

Please sign in to comment.