proteus

package module
v0.0.7 Latest Latest
Warning

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

Go to latest
Published: Dec 12, 2023 License: GPL-2.0 Imports: 16 Imported by: 0

README

Proteus

Go Report Card

About

Proteus is a package for defining the configuration of an Go application in a struct and loading it from different sources. Application can also opt-in to getting updates when the configuration changes.

Project Status

This project is in pre-release stage and backwards compatibility is not guaranteed.

How to Get

go get github.com/simplesurance/proteus@latest

How to Use

Specify the parameters and from what sources they can be provided:

func main() {
	params := struct {
		Server string
		Port   uint16
	}{}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("Server: %s:%d\n", params.Server, params.Port)
}

Provide the parameters using one of the configured providers:

go run main.go -server a -port 123
# Output: Server: a:123

CFG__SERVER=b CFG__PORT=42 go run *.go
# Output: Server: b:42

In the default configuration, as shown above, command-line flags have priority over environment variables.

Sets of Parameters

Parameters can be organized in parameter sets. Two parameters with the same name can exist, as long as they belong to different sets.

package main

func main() {
	params := struct {
		HTTP httpParams
		DB   dbParams
	}{}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	connectDB(params.DB)
	startHTTP(params.HTTP)
}

func connectDB(dbP dbParams) {
	fmt.Printf("Connecting to %s on db %s with user=%s\n",
		dbP.Server, dbP.Database, dbP.Username)
}

func startHTTP(httpP httpParams) {
	fmt.Printf("Starting HTTP server on :%d\n", httpP.BindPort)
}

type httpParams struct {
	BindPort uint16 `param:"bind_port"`
	Password string `param:"pwd,secret"`
}

type dbParams struct {
	Server   string
	Username string `param:"user"`
	Password string `param:"pwd,secret"`
	Database string
}
CFG__DB__SERVER=localhost go run *.go \
  db \
    -database library \
    -user sa \
    -pwd sa \
  http \
    -bind_port 5432 \
    -pwd secret-token

# Output:
# Connecting to localhost on db library with user=sa
# Starting HTTP server on :5432

Note that one parameter was provided as environment variable and the others as command-line flags.

Struct Tags and Defaults

Some struct tags are supported to allow specifying some details about the parameter. Default values can also be provided:

func main() {
	params := struct {
		Enabled bool   `param:"is_enabled,optional" param_desc:"Allows enabling or disabling the HTTP server"`
		Port    uint16 `param:",optional"           param_desc:"Port to bind for the HTTP server"`
		Token   string `param:",secret"             param_desc:"Token clients must provide for authentication"`
	}{
		Enabled: true,
		Port:    8080,
	}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	if params.Enabled {
		fmt.Printf("Starting HTTP server on :%d\n", params.Port)
	}
}

Only -token is mandatory:

go run *.go -token my-secret-token

# Output: Starting HTTP server on :8080

The token is marked as a secret, which is important to avoid leaking its value.

XTypes

XTypes are types provided by proteus to handle complex types and to provide more additional functionality.

package main

func main() {
	params := struct {
		BindPort   uint16
		PrivKey    *xtypes.RSAPrivateKey
		RequestLog *xtypes.OneOf
	}{
		RequestLog: &xtypes.OneOf{
			Choices: []string{"none", "basic", "full"},
			UpdateFn: func(s string) {
				fmt.Printf("Log level changed to %s", s)
			},
		},
	}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("Starting HTTP server on :%d with an RSA key of size %d, log level %s\n",
		params.BindPort,
		params.PrivKey.Value().Size(),
		params.RequestLog.Value())
}
Auto-Generated Usage (a.k.a --help)

To have usage information include the WithAutoUsage option:

func main() {
	params := struct {
		Database    dbParams
		Environment *xtypes.OneOf `param_desc:"Which environment the app is running on"`
		Port        uint16
	}{
		Database: defaultDBParams(),
		Environment: &xtypes.OneOf{
			Choices: []string{"dev", "stg", "prd"},
		},
	}

	parsed, err := proteus.MustParse(&params,
		proteus.WithAutoUsage(os.Stderr, func() { os.Exit(0) }))
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("Running in %s mode\n", params.Environment.Value())
}

type dbParams struct {
	Server   string `param:",optional"    param_desc:"Name of the database server"`
	Port     uint16 `param:",optional"    param_desc:"TCP port number of the database server"`
	User     string `                     param_desc:"Username for authentication"`
	Password string `                     param_desc:"Password for authentication"`
}

func defaultDBParams() dbParams {
	return dbParams{
		Server: "localhost",
		Port:   5432,
	}
}
go run main.go --help

# Output:
# Usage: x [-help] -environment <dev|stg|prd> -port <uint16>
#          database -password <string> -user <string> [-port <uint16>]
#                   [-server <string>]
#
# PARAMETERS
# - help default=false
#   Prints information about how to use this application
# - environment
#   Which environment the app is running on
# - port
#
# PARAMETER SET: DATABASE
# - password
#   Password for authentication
# - user
#   Username for authentication
# - port default=5432
#   TCP port number of the database server
# - server default=localhost
#   Name of the database server
#
#
# PARAMETERS
# - help default=false
#   Prints information about how to use this application
# - environment
#   Which environment the app is running on
# - port
#
# PARAMETER SET: DATABASE
# - password
#   Password for authentication
# - user
#   Username for authentication
# - port default=5432
#   TCP port number of the database server
# - server default=localhost
#   Name of the database server

Supported Providers

Documentation

Overview

Package proteus is a package for defining the configuration of an Go application in a struct and loading it from different sources. Application can also opt-in to getting updates when the configuration changes.

Example
package main

import (
	"fmt"
	"os"

	"github.com/simplesurance/proteus"
)

func main() {
	params := struct {
		Server string
		Port   uint16 `param:",optional"`
	}{
		Port: 5432,
	}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("Server: %s:%d\n", params.Server, params.Port)
}
Output:

Example (ParamSet)
package main

import (
	"fmt"
	"os"

	"github.com/simplesurance/proteus"
)

func main() {
	params := struct {
		HTTP httpParams
		DB   dbParams
	}{}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	connectDB(params.DB)
	startHTTP(params.HTTP)
}

type httpParams struct {
	BindPort uint16 `param:"bind_port"`
	Password string `param:"pwd,secret"`
}

type dbParams struct {
	Server   string
	Username string `param:"user"`
	Password string `param:"pwd,secret"`
	Database string
}

func connectDB(dbP dbParams) {
	fmt.Printf("Connecting to DB server %s (db=%q, user=%q)",
		dbP.Server, dbP.Database, dbP.Username)
}

func startHTTP(httpP httpParams) {
	fmt.Printf("Starting HTTP server on :%d\n", httpP.BindPort)
}
Output:

Example (TagsAndDefaults)
package main

import (
	"fmt"
	"os"

	"github.com/simplesurance/proteus"
)

func main() {
	params := struct {
		Enabled bool   `param:"is_enabled,optional" param_desc:"Allows enabling or disabling the HTTP server"`
		Port    uint16 `param:",optional"           param_desc:"Port to bind for the HTTP server"`
		Token   string `param:",secret"             param_desc:"Client authentication token"`
	}{
		Enabled: true,
		Port:    8080,
	}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	if params.Enabled {
		fmt.Printf("Starting HTTP server on :%d\n", params.Port)
	}
}
Output:

Example (Xtypes)
package main

import (
	"fmt"
	"os"

	"github.com/simplesurance/proteus"
	"github.com/simplesurance/proteus/xtypes"
)

func main() {
	params := struct {
		BindPort   uint16
		PrivKey    *xtypes.RSAPrivateKey
		RequestLog *xtypes.OneOf
	}{
		RequestLog: &xtypes.OneOf{
			Choices: []string{"none", "basic", "full"},
			UpdateFn: func(s string) {
				fmt.Printf("Log level changed to %s", s)
			},
		},
	}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("Starting HTTP server on :%d with an RSA key of size %d, log level %s\n",
		params.BindPort,
		params.PrivKey.Value().Size(),
		params.RequestLog.Value())
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option func(*settings)

Option specifes options when creating a configuration parser.

func WithAutoUsage

func WithAutoUsage(writer io.Writer, exitFn func()) Option

WithAutoUsage will change how the --help parameter is parsed, allowing to specify a writer, for the usage information and an "exit function". If not specified, proteus will use stdout for writer and will use os.Exit(0) as exit function.

func WithLogger

func WithLogger(l plog.Logger) Option

WithLogger provides a custom logger. By default logs are suppressed.

Warning: the "Logger" interface is expected to change in the stable release.

func WithPrintfLogger added in v0.0.3

func WithPrintfLogger(logFn func(format string, v ...any)) Option

WithPrintfLogger use the printf-style logFn function as logger.

func WithProviders added in v0.0.2

func WithProviders(s ...sources.Provider) Option

WithProviders specifies from where the configuration should be read. If not specified, proteus will use the equivalent to:

WithEnv(cfgflags.New(), cfgenv.New("CFG"))

Providing this option override any previous configuration for providers.

func WithShortDescription added in v0.0.2

func WithShortDescription(oneline string) Option

WithShortDescription species a short one-line description for the application. Is used when generating help information.

func WithValueFormatting added in v0.0.4

func WithValueFormatting(o ValueFormattingOptions) Option

WithValueFormatting specifies options for pre-processing values before using them. See ValueFormattingOptions for more details.

func WithVersion added in v0.0.6

func WithVersion(version string) Option

type Parsed

type Parsed struct {
	// contains filtered or unexported fields
}

Parsed holds information about all parameters supported by the application, and their options, allowing interacting with them.

func MustParse

func MustParse(config any, options ...Option) (*Parsed, error)

MustParse receives on "config" a pointer to a struct that defines the expected application parameters and loads the parameters values into it. An example of a configuration is struct is as follows:

params := struct{
	Name      string                                       // simple parameter
	IsEnabled bool   `param:"is_enabled"`                  // rename parameter
	Password  string `param:"pwd,secret"`                  // rename and mark parameter as secret
	Port      uint16 `param:",optional"`                   // keep the name, mark as optional
	LogLevel  string `param_desc:"Cut-off level for logs"` // describes the parameter
	X         string `param:"-"`                           // ignore this field
}{
	Port: 8080, // default value for optional parameter
}

The tag "param" has the format "name[,option]*", where name is either empty, "-" or a lowercase arbitrary string containing a-z, 0-9, _ or -, starting with a-z and terminating not with - or _. The value "-" for the name result in the field being ignored. The empty string value indicates to infer the parameter name from the struct name. The inferred parameter name is the struct name in lowercase. Option can be either "secret" or "optional". An option can be provided without providing the name of the parameter by using an empty value for the name, resulting in the "param" tag starting with ",".

The tag "param_desc" is an arbitrary string describing what the parameter is for. This will be shown to the user when usage information is requested.

The provided struct can have any level of embedded structs. Embedded structs are handled as if they were "flat":

type httpParams struct {
	Server string
	Port   uint16
}

parmas := struct{
	httpParams
	LogLevel string
}{}

Is the same as:

params := struct {
	Server   string
	Port     uint16
	LogLevel string
}{}

Configuration structs can also have "xtypes". Xtypes provide support for getting updates when parameter values change and other types-specific optons.

params := struct{
	LogLevel *xtypes.OneOf
}{
	OneOf: &xtypes.OneOf{
		Choices: []string{"debug", "info", "error"},
		Default: "info",
		UpdateFn: func(newVal string) {
			fmt.Printf("new log level: %s\n", newVal)
		}
	}
}

The "options" parameter provides further customization. The option WithProviders() must be specified to define from what sources the parameters must be read.

The configuration struct can have named sub-structs (in opposition to named, or embedded sub-structs, already mentioned above). The sub-structs can be up to 1 level deep, and can be used to represent "parameter sets". Two parameters can have the same name, as long as they belong to different parameter sets. Example:

params := struct{
	Database struct {
		Host     string
		Username string
		Password string `param:,secret`
	}
	Tracing struct {
		Host     string
		Username string
		Password string `param:,secret`
	}
}{}

Complete usage example:

func main() {
	params := struct {
		X int
	}{}

	parsed, err := proteus.MustParse(&params,
		proteus.WithAutoUsage(os.Stdout, "My Application", func() { os.Exit(0) }),
		proteus.WithProviders(
			cfgflags.New(),
			cfgenv.New("CFG"),
		))
	if err != nil {
		parsed.ErrUsage(os.Stderr, err)
		os.Exit(1)
	}

	// "parsed" now have the parameter values
}

See godoc for more examples.

A Parsed object is guaranteed to be always returned, even in case of error, allowing the creation of useful error messages.

Example
package main

import (
	"fmt"
	"os"

	"github.com/simplesurance/proteus"
)

func main() {
	params := struct {
		Server string
		Port   uint16
	}{}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("Server: %s:%d\n", params.Server, params.Port)
}
Output:

Example (Providers)

ExampleMustParse_providers changes how and from where proteus reads configuration.

package main

import (
	"fmt"
	"os"

	"github.com/simplesurance/proteus"
	"github.com/simplesurance/proteus/sources/cfgenv"
	"github.com/simplesurance/proteus/sources/cfgflags"
)

func main() {
	params := struct {
		Server string
		Port   uint16
	}{}

	parsed, err := proteus.MustParse(&params,
		proteus.WithProviders(
			cfgenv.New("CONFIG"), // change env var prefix to CONFIG
			cfgflags.New()))      // flags are used, but priority is to env vars
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("Server: %s:%d\n", params.Server, params.Port)
}
Output:

Example (TrimSpaces)

ExampleMustParse_trimSpaces instructs proteus to trim values of parameters, removing leading and trailing spaces. This also removes trailing new lines.

package main

import (
	"fmt"
	"os"

	"github.com/simplesurance/proteus"
)

func main() {
	params := struct {
		Server string
		Port   uint16
	}{}

	parsed, err := proteus.MustParse(&params,
		proteus.WithValueFormatting(proteus.ValueFormattingOptions{
			TrimSpace: true,
		}))
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Printf("Server: %s:%d\n", params.Server, params.Port)

	// Calling with:
	//
	//   ./app -server "localhost" -port "8080"
	//
	// is the same as:
	//
	//   ./app -server " localhost \n" -port "8080\n"
}
Output:

Example (WithTags)
package main

import (
	"fmt"
	"os"

	"github.com/simplesurance/proteus"
)

func main() {
	params := struct {
		Enabled bool   `param:"is_enabled,optional" param_desc:"Allows enabling or disabling the HTTP server"`
		Port    uint16 `param:",optional"           param_desc:"Port to bind for the HTTP server"`
		Token   string `param:",secret"             param_desc:"Client authentication token"`
	}{
		Enabled: true,
		Port:    8080,
	}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	if params.Enabled {
		fmt.Printf("Starting HTTP server on :%d\n", params.Port)
	}
}
Output:

func (*Parsed) Dump

func (p *Parsed) Dump(w io.Writer)

Dump prints the names and values of the parameters.

func (*Parsed) Stop added in v0.0.2

func (p *Parsed) Stop()

Stop release resources being used. Proteus itself does not use any resource that need to be released, but some providers might.

func (*Parsed) Usage

func (p *Parsed) Usage(w io.Writer)

Usage prints usage and detailed help output to the provided writer.

Example
package main

import (
	"os"

	"github.com/simplesurance/proteus"
	"github.com/simplesurance/proteus/sources/cfgenv"
)

func main() {
	params := struct {
		Server string `param_desc:"Name of the server to connect"`
		Port   uint16 `param:",optional" param_desc:"Port to conect"`
	}{
		Port: 5432,
	}

	parsed, _ := proteus.MustParse(&params, proteus.WithProviders(cfgenv.New("TEST")))

	parsed.Usage(os.Stdout)

}
Output:

Usage: proteus.test [-help] -server <string> [-port <uint16>]

PARAMETERS
- help default=false
  Prints information about how to use this application
- server
  Name of the server to connect
- port default=5432
  Port to conect

func (*Parsed) Valid

func (p *Parsed) Valid() error

Valid allows determining if the provided application parameters are valid.

func (*Parsed) WriteError added in v0.0.5

func (p *Parsed) WriteError(w io.Writer, err error)

WriteError writes the strings representation of err to w. The line is prefixed with "ERROR: " .

Example
package main

import (
	"os"

	"github.com/simplesurance/proteus"
)

func main() {
	params := struct {
		Latitude  float64
		Longitude float64
	}{}

	parsed, err := proteus.MustParse(&params)
	if err != nil {
		// "parsed" is never nil; it can be used to parse the error
		parsed.WriteError(os.Stderr, err)
		os.Exit(1)
	}

	// use parameters
}
Output:

type ValueFormattingOptions added in v0.0.4

type ValueFormattingOptions struct {
	// TrimSpace instructs proteus to trim leading and trailing spaces from
	// values of parameters.
	TrimSpace bool
}

ValueFormattingOptions specifies how values of parameters are "trimmed".

Directories

Path Synopsis
internal
Package plog has the types and code used for logs on proteus.
Package plog has the types and code used for logs on proteus.
Package sources defines the interface for configuration source providers.
Package sources defines the interface for configuration source providers.
cfgenv
Package cfgenv is a parameter provider that reads values from environment variables.
Package cfgenv is a parameter provider that reads values from environment variables.
cfgflags
Package cfgflags implements a configuration reader that reads from command-line flags.
Package cfgflags implements a configuration reader that reads from command-line flags.
cfgtest
Package cfgtest is a configuration provider to be used on tests.
Package cfgtest is a configuration provider to be used on tests.
Package types defines types used by proteus and by code using it.
Package types defines types used by proteus and by code using it.
Package xtypes provide additional types that can be used for configuration.
Package xtypes provide additional types that can be used for configuration.

Jump to

Keyboard shortcuts

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