Coder Social home page Coder Social logo

kkyr / fig Goto Github PK

View Code? Open in Web Editor NEW
367.0 4.0 31.0 252 KB

A minimalist Go configuration library

License: Apache License 2.0

Makefile 0.11% Go 99.26% Shell 0.63%
golang go configuration config-file configuration-management environment-variables environment yaml json toml

fig's Introduction

fig

godoc semver tag go report card coverage status license

fig

fig is a tiny library for loading an application's configuration into a Go struct.

Why fig?

  • ๐Ÿ› ๏ธ Define your configuration, validations and defaults all within a single struct.
  • ๐ŸŒ Easily load your configuration from a file, the environment, or both.
  • โฐ Decode strings into Time, Duration, Regexp, or any custom type that satisfies the StringUnmarshaler interface.
  • ๐Ÿ—‚๏ธ Compatible with yaml, json, and toml file formats.
  • ๐Ÿงฉ Only three external dependencies.

Getting Started

$ go get -d github.com/kkyr/fig

Define your config file:

# config.yaml

build: "2020-01-09T12:30:00Z"

server:
    ports:
      - 8080
    cleanup: 1h

logger:
    level: "warn"
    trace: true

Define your struct along with validations or defaults:

package main

import (
  "fmt"

  "github.com/kkyr/fig"
)

type Config struct {
  Build  time.Time `fig:"build" validate:"required"`
  Server struct {
    Host    string        `fig:"host" default:"127.0.0.1"`
    Ports   []int         `fig:"ports" default:"[80,443]"`
    Cleanup time.Duration `fig:"cleanup" default:"30m"`
  }
  Logger struct {
    Level   string         `fig:"level" default:"info"`
    Pattern *regexp.Regexp `fig:"pattern" default:".*"`
    Trace   bool           `fig:"trace"`
  }
}

func main() {
  var cfg Config
  err := fig.Load(&cfg)
  // error handling omitted
  
  fmt.Printf("%+v\n", cfg)
  // {Build:2019-12-25T00:00:00Z Server:{Host:127.0.0.1 Ports:[8080] Cleanup:1h0m0s} Logger:{Level:warn Pattern:.* Trace:true}}
}

Fields marked as required are checked to ensure they're not empty, and default values are applied to fill in those that are empty.

Environment

By default, fig will only look for values in a config file. To also include values from the environment, use the UseEnv option:

fig.Load(&cfg, fig.UseEnv("APP_PREFIX"))

In case of conflicts, values from the environment take precedence.

Usage

See usage examples.

Documentation

For detailed documentation, visit go.dev.

Contributing

PRs are welcome! Please explain your motivation for the change in your PR and ensure your change is properly tested and documented.

fig's People

Contributors

arieltorti avatar joshkaplinsky avatar jparise avatar kkyr avatar rsjethani avatar wneessen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

fig's Issues

Fields of type time.Duration should fail when the input is an integer

The duration parser provided by the standard library fails when it's input doesn't include a time unit. As expected, fig also fails in such a case, but it doesn't when the input is not wrapped in quotes because it's interpreted as an integer and not a string.

For example, given:

// config.yaml
timeout: 1
// main.go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/kkyr/fig"
)

type Config struct {
	Timeout time.Duration `fig:"timeout" validate:"required"`
}

func main() {
	var cfg Config
	if err := fig.Load(&cfg); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", cfg)
}

I would expect it to fail because timeout: 1 doesn't have a time unit, instead the config is parsed without errors and the set value is 1ns.

Maybe this could be fixed by adding a check of Int (and maybe float?) values in StringToTimeDurationHookFunc, although I may be missing some other cases.

Default bool items not being set correctly when not present in config

Howdy,

According to the documentation:

A default key in the field tag makes fig fill the field with the value specified when the field is not otherwise set.

Given the following config.yaml:

itemone:
  enabled: false

With the following program:

package main

import (
    "fmt"
    "github.com/kkyr/fig"
)

type Config struct {
    ItemOne  ItemOne   `default:{}`
    ItemTwo  ItemTwo   `default:{}`

}

type ItemOne struct {
    Enabled     bool    `fig:"enabled" default:true`
}

type ItemTwo struct {
    Enabled     bool    `fig:"enabled" default:true`
}

func main() {
    var conf Config
    err := fig.Load(&conf)
    if err != nil {
        panic(err)
    }

    fmt.Printf("Config %+v", conf)
}

What was expected:
Field ItemTwo.Enabled would be set to boolean true.

What actually happened:
Field ItemTwo.Enabled is set to boolean false:

Config {ItemOne:{Enabled:false} ItemTwo:{Enabled:false}}

I've slimmed down an existing program to this most basic PoC to rule out other code in my application being a problem, though my usage is perhaps still incorrect.

Make it possible to use only environment variables (with no config file)

Currently, environment variables can't be used as a full replacement for a config file, only in addition to one. If no config file exists, I think it should just use the defaults from the struct, check environment variables if UseEnv is set, and then return an error if required stuff is not set (e.g. port: required validation failed (using only environment as no config file could be found)).

feat: being able to add env name for a specific field

Hello and thanks for your great package.
I have a specific use case in which env var is using another names which is different with the name generated by the fig for that field.

Can we add a new feature to achieve this.

Something like this:

type Config struct {
    Field int `fig:"field" env:"DIFFERENT_KEY"`	
}

This way, the config is loaded from a file using the current approach. But if the DIFFERENT_KEY env is set, its value will be used for this field.

validate a values of map

I use fig to initialize a map of structs.
The fields in the struct have a "required" validation tag. However, fig does not enforce this validation.
E.g.

type Config struct {
Elements map[string]Element fig:"chartConfigs" validate:"required"
}

type Element struct {
Name string fig:"name" validate:"required"
}

var cfg Config
fig.Load(&cfg)

Setting config value to "zero" in file takes default value

I really enjoy fig for my configuration, because it is simpler than other solutions, but I found a problem with it that is more than a nuance.

Imagine you have a config value set like below, and then in the config file you want to set it to 0: log_level: 0

LogLevel    int    `fig:"log_level" default:"1"` // -1:trace 0: debug, 1: info, 2: warn, 3: error, 4: fatal, 5: panic

The default value will be used, I assume because at the point it looks if it is set, it thinks it is not because it is set to the "zero" value of the type

Feature request: Updating configuration files

It would be nice to have an API to create/update a configuration file, keeping constraints in mind.

The call would be something like:

// do something with the settings' attributes
// ....
// Update the configuration file and check the constraints
fig.Update(&cfg, fig.File("settings.json"), fig.Dir("/app/conf"))

Feature request: custom type unmarshalling

First of all, I love fig. It's such a versatile tool, thanks for this!

I was wondering if there would be a posibility to add config parsing/unmarshaling for custom types, like encode/json allows to provide a UnmarshalJSON() method. This would help i. e. with custom "enums" type that are set up via iota.

Here is an example:
Let's say I have a custom type ListenerType and I pre-define 3 types via const:

// ListenerType is an enumeration wrapper for the different listener types
type ListenerType uint

const (
        ListenerUnix ListenerType = iota
        ListenerTCP
        ListenerTLS
)

In my config struct, I would then define my config setting with the corresponding ListenerType and a default value:

// Config holds all the global configuration settings that are parsed by fig
type Config struct {
        // Server holds server specific configuration values
        Server struct {
                PIDFile      string               `fig:"pid_file" default:"/var/run/app.pid"`
                ListenerType ListenerType `fig:"listener_type" default:"unix"`
        }
}

By default this would of course not work, as the default or provided setting in the config file would be a string, not an uint. But here comes my request into play. fig defines an interface like this:

type Unmarshaler interface {
        UnmarshalType(string) error
}

and checks if the corresponding type provides a method that satisfies this interface. For my example it could look like this:

func (l *ListenerType) UnmarshalType(v string) error {
        switch strings.ToLower(v) {
        case "unix":
                *l = ListenerUnix
        case "tcp":
                *l = ListenerTCP
        case "tls":
                *l = ListenerTLS
        default:
                return fmt.Errorf("unknown listener type: %s", v)
        }
        return nil
}

This way, I can keep my iota type but fill the values via fig with the actual string values instead of the user having to provide "0", "1" or "2".

Hope this makes sense and is something you would consider adding.

environment variable overrides are processed after the config struct is already initialized

consider a config struct like

type Config struct {
	Foo *CustomFoo `fig:"foo_config_file"`
}

where CustomFoo:

type CustomFoo struct {
	Bar string `json:"bar"`
	Baz string `json:"baz"`
}

func (c *CustomFoo) UnmarshalString(s string) error {
	fmt.Println(s)
	file, err := os.ReadFile(s)
	if err != nil {
		return err
	}
	err = json.Unmarshal(file, &c)
	if err != nil {
		return err
	}
	return nil
}

if you attempt to fill CustomFoo by passing a filename to UnmarshalString, it should read that file, then unmarshal into the CustomFoo struct. However, if you attempt to override the config with environment variables, the struct will attempt to be built twice, the first time with the value from config.yaml and the second time with the environment variable. if UnmarshalString returns an error it fails the config loading process. I'm attaching a runnable reproducer at the bottom, but IMO fig should wait until it has merged the different sources of config before it attempts to actually construct the final struct it will return to the caller.

expected result:

the Config struct is generated with Foo.Bar and Foo.Baz populated

actual result:

the foo.json file referenced in config.yaml is opened (and doesn't exist), rather than the foo_custom.json file referenced in the environment variables

error: 1 error(s) decoding:

* error decoding 'foo_config_file': open foo.json: no such file or directory
exit status 1
full example
package main

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/kkyr/fig"
)

type CustomFoo struct {
	Bar string `json:"bar"`
	Baz string `json:"baz"`
}

func (c *CustomFoo) UnmarshalString(s string) error {
	fmt.Println(s)
	file, err := os.ReadFile(s)
	if err != nil {
		return err
	}
	err = json.Unmarshal(file, &c)
	if err != nil {
		return err
	}
	return nil
}

type Config struct {
	Foo *CustomFoo `fig:"foo_config_file"`
}

func main() {
	err := os.Setenv("MYAPP_FOO_CONFIG_FILE", "custom_foo.json")
	if err != nil {
		fmt.Printf("error: %v\n", err)
		os.Exit(1)
	}
	var cfg Config
	err = fig.Load(&cfg, fig.UseEnv("MYAPP"))
	if err != nil {
		fmt.Printf("error: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("%v\n", cfg)
}

config.yaml:

foo_config_file: "foo.json"

foo.json: non-existent

custom_foo.json:

{
  "bar": "hello",
  "baz": "world"
}

fig should be more customizable via hooks

fig uses the very powerful mapstructure under the hood just like viper. But unlike viper or uconfig it does not expose some sort of hook/middleware mechanism so that user defined logic can be executed as part of loading config

Introducing such a behavior might break backwards compatibility but it would be worth it in the long run.

Value from environment

Thank you for the library. It is something I've been planning to write myself. But I'd still hope for one more feature. Especially for projects packaged as docker images. Could you support some sort of environment variable interpolation syntax? Like ${FOOBAR} being read from the environment variable FOOBAR?

Feature request: auto create config file if not found

I am writing an app that looks for a config.yaml in

  • ".",
  • "/etc/myapp",
  • "/home/user/myapp",
  • "$HOME/.config/myapp,

What I wish to add is an option to auto-create file on the initial startup. Of course, I can make it on my own before fig.Load() but it will duplicate the code in the package (like methods findCfgFiles). Besides that, since I store all config properties under fig annotations it will be simple and more convenient.

Do you mind adding that kind of feature? I can open the PR soon

Add a strict parsing alternative which disallows unknown fields

It would be nice to have something akin to json.DisallowUnknownFields and yaml.UnmarshalStrict to catch configuration invalid configurations earlier and make them an error.

I propose to add a new flag fig.UseStrict, when set fig.Load will return an error upon encountering unknown (i.e: extra) fields.

For example. given:

---
log_level: debug
host: "0.0.0.0"
type Config struct {
	Host string `fig:"host"`
	LogLevel string `fig:"logLevel"`
}

var cfg Server
fig.Load(&cfg, UseStrict()) // Returns error because the key `log_level` is unknown

Allow for multiple possible file names

Hi,

I would like it, if it would be possible to search for different file names (same as dirs).
ex allow ./config.json ./config.yaml and ./config.toml

Thanks for the nice work.

Add an option to explicitly set which config file to use

It would be nice to have an option/function to explicitly load the config file that was provided by user.
The idea is to have smth like:

package main

var configFile = flag.String("config", "app.yaml", "Config file to use") // so it may have the value like `/usr/local/etc/name_by_user.yaml`

func main() {
    flag.Parse()
    err := fig.Load(&cfg, fig.File(*configFile))
}

In the current implementation it wouldn't work because fig will search for the file in the current directory (.) only by default. But it also can check for the file existence using only given path:

func (f *fig) findCfgFile() (path string, err error) {
	for _, dir := range f.dirs {
		path = filepath.Join(dir, f.filename)
		if fileExists(path) {
			return
		}
	}
        if path = f.filename; fileExists(path) {
            return
        }
	return "", fmt.Errorf("%s: %w", f.filename, ErrFileNotFound)
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.