Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop regexp for performance #74

Merged
merged 2 commits into from Aug 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
}
})
}
}