envy

package module
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 9, 2024 License: MIT Imports: 14 Imported by: 1

README

ENVY 👑 💅

Envy is a Go package that provides a flexible way to unmarshal environment variables into struct types using struct tags for validation, type conversion, and more. It offers powerful, customizable tag-based validations like checking for required fields, ensuring values match certain patterns, and verifying they fall within predefined options.


Features

Feature Description Availability
Environment Variable Mapping Easily map environment variables to struct fields using the env tag. Ready ✅
Defaults Define default values for your struct fields using the default tag. Ready ✅
Options Validation Validate the contents of an environment variable against a set of allowed options using the options tag. Ready ✅
Regex Matching Validate the contents of an environment variable using regular expressions with the matches tag. Ready ✅
Required Fields Mark certain environment variables as required using the required tag. Ready ✅
Type Conversion Define your struct with the type you need and envy will take care of converting the environment variable for you. Ready ✅
Customizable Tag Parsing Middleware Alter the order of tag parsing, add new custom tag parsers, or replace existing ones for enhanced flexibility in the unmarshalling process. Ready ✅
Full Feature Test Suite Feature tests have been written for Unmarshalling from the env tag 🧑🏻‍💻 In Progress
Marshalling from struct to environment variables This features is on the roadmap, but hadn't been implemented. Open a PR if you'd like to get this feature implemented in a future release! Not Available 👎

There's a lot of Envious People Hearing That Story

Installation:

go get -u github.com/dubbikins/envy

Basic Usage

  1. Define Your Struct:

    Use the env,default,options,matches,and required tags when defining your struct types.

    type Config struct {
       Port     int    `env:"PORT" default:"8080" required:"true"`
       Hostname int    `env:"HOSTNAME" default:"localhost" options:"localhost,my-domain-1.subdomain.com,my-domain-1.subdomain.com"`
       Mode     string `env:"MODE" options:"debug,release,test"`
    }
    
  2. Unmarshal Environment Variables:

    Use the Unmarshal function to unmarshal a struct you've already declared and initialized, just be sure to pass a pointer to the struct, not the struct value. (Note: if you pass an uninitialized value or non-pointer value to Unmarshal, it will return an error)

    //HOSTNAME: my-domain-1.subdomain.com
    //Mode: debug
    
    cfg := &Config{}
    err := envy.Unmarshal(&cfg)
    if err != nil {
       log.Fatal(err)
    }
    ...
    

    Or use the generic New function in conjuction with the FromEnvironmentAs function which will return an instance of a type, and unmarshal it with the appropriate environment variables.

    if config, err := envy.New(envy.FromEnvironmentAs[Config]);err != nil {
       ...
    }
    ...
    
  3. Access the Struct Fields:

    Then you can utilize the freshly set fields of your struct in the rest of your code! (just be sure to check for errors beforehand 😅)

    fmt.Printf("%s:%d [mode=%s]", cfg.Hostname, cfg.Port, cfg.Mode)
    // prints "my-domain-1.subdomain.com:8080 [mode=debug]"
    

Available Type Convertions 💥

Envy can unmarshall most primitive go types out of the box. Check out the following table of types to which ones are supported out of the box.

Type Valid Values Parser Provided?
[]byte string All utf-8 encoded string values (.*) N/A
int int8 int16 int32 int64 positive and negative numbers, optional commas as thousands separators, and no leading zeros: ^([+-]?)(?:[1-9][0-9]{0,2}(?:,[0-9]{3})*|0)$ strconv.ParseInt
uint uint8``uint16 uint32 uint64 positive numbers, optional commas, as thousands separators and no leading zeros: ^(?:[1-9][0-9]{0,2}(?:,[0-9]{3})*|0)$ strconv.ParseUint
float32 float64 positive decimal values, commas allowed in the thousands separators, no leading zeros strcov.ParseFloat
bool true,false,yes,no,1,0,on,off,t,f,T,F strconv.ParseBool (yes,no,on,off pre-parsed to true or false)
embedded structs embedded struct pointers N/A envy.Unmarshal
encoding.TextUnmarshaler Whatever you say goes! Your Custom Implementation
slices maps time.Time time.Duration chan N/A require custom
Custom Type unmarshaller

Didn't find the type you were looking for? 😦 Not to worry, it's simple to create a custom unmarshaller for any struct type! To provide custom unmarshalling for specific types in your struct, implement the TextUnmarshaler inteface by defining the UnmarshalText([]byte) error method for your type. It's that easy! This interface is a standard go interface from the encoding package, so you'll find that a lot of custom types already implement this interface.

type CustomType struct {
   // your type definition here
}

func (ct *CustomType) UnmarshalText(data []byte) error {
   // custom parsing logic here
   return nil // or an error
}
Example: Custom Parsing for Durations ⏲

The below example shows how you can wrap existing types like Duration (which is actually just a wrapper for int64), from the time package in the go standard library and implement the custom unmarshalling logic to convert the []byte value into a duration value.


import "time"

// Custom implementation for Parsing Durations from a string 
type Duration struct {
   time.Duration
}

func (d *Duration) UnmarshalText(data []byte) (err error) {
   if len(data) == 0 {
      return nil
   }
   d.Duration, err = time.ParseDuration(string(data))
   return
}

Use the custom type in your struct. The below example will convert the environment variable TIMEOUT to a time.Duration object as long as the value is in 1,2,5,10, or 25 minutes. If no value is provided, 5 minutes will be used as the defaul duration.

type Config struct {
   Timeout Duration `env:"TIMEOUT" default:"5m" options:"[1m,2m,5m,10m,15m]"`
}
if cfg, err := envy.New(envy.FromEnvironmentAs[Config]);err != nil {
   panic("oh no! something went wrong... 😢")
}
select { 
    case <-time.After(cfg.Timeout.Duration): 
        fmt.Println("Time Out!") 
} 

Reordering the Middleware Stack 🔁

Envy lets you customize the order of middleware to ensure tag parsers are processed in a sequence of your choice. By utilizing the Pop and Push methods, you can extract existing middleware and re-insert them or even replace them with custom middleware.

Here's an example:

package main

import (
   "context"
   "fmt"
   "reflect"

   "github.com/your-username/envy"
)

type Config struct {
   // your struct fields with tags here
}

func main() {
   config := &Config{}
   err := envy.Unmarshal(config, func(mw envy.TagMiddleware) {
      // Pop existing middleware
      existingMiddleware1 := mw.Pop()
      existingMiddleware2 := mw.Pop()

      // Push a custom middleware at the start
      mw.Push(
         func(next envy.TagHandler) envy.TagHandler {
            return envy.TagHandlerFunc(func(ctx context.Context, field reflect.StructField) error {
               fmt.Println("I'm the first custom tag parser")
               return next.UnmarshalField(ctx, field)
            })
         })

      // Re-insert the existing middleware
      mw.Push(existingMiddleware1, existingMiddleware2)

      // Add another custom middleware at the end
      mw.Push(
         func(next envy.TagHandler) envy.TagHandler {
            return envy.TagHandlerFunc(func(ctx context.Context, field reflect.StructField) error {
               fmt.Println("I'm the last custom tag parser")
               return next.UnmarshalField(ctx, field)
            })
         })
   })

   if err != nil {
      // handle the error
   }
}

Contributions 📣

If you find a bug or think of a new feature, please open an issue or submit a pull request! Go ahead, make the world envious! 😈

Help Wanted

Documentation

Index

Constants

This section is empty.

Variables

View Source
var INVALID_FIELD_ERROR = errors.New("invalid field value")
View Source
var INVALID_TAG_SYNTAX_ERROR = errors.New("invalid tag definition; tag syntax {tagvalue} (default={value}|options=[...comma seperated values]|required) ")
View Source
var NOT_UNMARSHALLABLE_ERROR = errors.New("field is not unmarshallable")
View Source
var TAG_VALIDATION_ERROR = errors.New("invalid field definition; no tag value or default")

Functions

func DoesNotMatchError added in v0.0.3

func DoesNotMatchError(field_name, match_expression, actual string) error

func FromEnvironment added in v0.0.3

func FromEnvironment[T any](t *T) error

func InvalidOptionError added in v0.0.3

func InvalidOptionError(value string, options []string) error

func New

func New[T any](options ...OptionsFunc[*T]) (*T, error)

func RequiredError added in v0.0.3

func RequiredError(tagname, tag_details string) error

func Unmarshal

func Unmarshal(s any, options ...func(*Options)) (err error)

func WithOptionsContext added in v0.0.4

func WithOptionsContext(ctx context.Context, options *Options) context.Context

func WithTagContext added in v0.0.3

func WithTagContext(ctx context.Context, tag *Tag) context.Context

Types

type ContextKey added in v0.0.3

type ContextKey contextKey

type Duration

type Duration struct {
	time.Duration
}

Custom implementation for Parsing Durations from a string instead of parsing it as an int64

func (*Duration) UnmarshalText added in v0.0.3

func (d *Duration) UnmarshalText(data []byte) (err error)

type Middleware added in v0.0.3

type Middleware func(next TagHandler) TagHandler

func DefaultMiddleware added in v0.0.4

func DefaultMiddleware() []Middleware

type Options added in v0.0.4

type Options struct {
	Middleware []Middleware
}

func GetOptionsContext added in v0.0.4

func GetOptionsContext(ctx context.Context) (*Options, error)

func GetShouldSkip added in v0.0.4

func GetShouldSkip(ctx context.Context) (*Options, error)

func (*Options) Unmarshal added in v0.0.4

func (opts *Options) Unmarshal(s any)

type OptionsFunc

type OptionsFunc[Options any] func(Options) error

type Tag

type Tag struct {
	FieldType string
	FieldName string
	Value     reflect.Value
	Parent    reflect.Value

	Name      string
	Default   string
	Content   string
	Raw       string
	Options   []string
	Required  bool
	Matcher   *regexp.Regexp
	IgnoreNil bool

	Skip bool
	// contains filtered or unexported fields
}

func GetTagContext added in v0.0.3

func GetTagContext(ctx context.Context) (*Tag, error)

func MustGetTagContext added in v0.0.3

func MustGetTagContext(ctx context.Context) *Tag

func NewTag added in v0.0.3

func NewTag(value reflect.Value, parent reflect.Value) (t *Tag, err error)

unmarshalling order 1. env 2. default 3. options 4. matches 5. required 6. zero values (default unmarshaller)

func (*Tag) Bytes added in v0.0.4

func (t *Tag) Bytes() []byte

func (*Tag) Contents added in v0.0.4

func (t *Tag) Contents() string

func (*Tag) DefaultMiddleware added in v0.0.4

func (t *Tag) DefaultMiddleware() []Middleware

func (*Tag) GetState added in v0.0.4

func (t *Tag) GetState() map[string]interface{}

func (*Tag) GetStateValue added in v0.0.4

func (t *Tag) GetStateValue(key string) interface{}

func (*Tag) Push added in v0.0.4

func (t *Tag) Push(us ...Middleware)

func (*Tag) Read added in v0.0.5

func (t *Tag) Read(p []byte) (n int, err error)

func (*Tag) UnmarshalField added in v0.0.3

func (tag *Tag) UnmarshalField(ctx context.Context, field reflect.StructField) (err error)

func (*Tag) UnmarshalText added in v0.0.4

func (t *Tag) UnmarshalText(text []byte) (err error)

func (*Tag) Write added in v0.0.5

func (t *Tag) Write(p []byte) (n int, err error)

type TagHandler added in v0.0.3

type TagHandler interface {
	UnmarshalField(context.Context, reflect.StructField) error
}

func WithDefaultTag added in v0.0.3

func WithDefaultTag(next TagHandler) TagHandler

func WithEnvTag added in v0.0.3

func WithEnvTag(next TagHandler) TagHandler

func WithEnvyGlobalTag added in v0.0.4

func WithEnvyGlobalTag(next TagHandler) TagHandler

func WithMatchesTag added in v0.0.3

func WithMatchesTag(next TagHandler) TagHandler

func WithOptionsTag added in v0.0.3

func WithOptionsTag(next TagHandler) TagHandler

func WithRequiredTag added in v0.0.3

func WithRequiredTag(next TagHandler) TagHandler

type TagHandlerFunc added in v0.0.3

type TagHandlerFunc func(ctx context.Context, field reflect.StructField) error

func (TagHandlerFunc) UnmarshalField added in v0.0.3

func (f TagHandlerFunc) UnmarshalField(ctx context.Context, field reflect.StructField) error

type TagParser added in v0.0.3

type TagParser interface {
	TagName() string
	Handler() TagHandler
}

type TagUnmarshaler added in v0.0.4

type TagUnmarshaler interface {
	UnmarshalField(context.Context, reflect.StructField) error
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL