patchpanel

package module
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: MIT Imports: 9 Imported by: 0

README

patchpanel

Go Reference Go Report Card CI Build Status

patchpanel provides a thread-safe utility for coercing configuration values into strongly typed values on a struct, which is especially useful for loading up configuration data or complex arguments for a function.

No external dependencies are used, but patchpanel is built to easily integrate with other libraries.

Metadata / struct tags

Support for evaluating metadata (such as default values or specific configuration tags) is built-in for converting string representations to corresponding native types, performed automatically to the type of the struct field to which they are attached (e.g., string, int, bool, time.Duration, time.Time).

Secondary struct tags are also supported for influencing how the primary tag is parsed. For example, a timeFormat tag dictates how a default tag representing a timestamp should be interpreted.

Custom parsers

Consumers can register user-defined types and parsing logic at runtime.

See: type Parser func(value string, parserHints map[string]any) (any, error)

and

func (pc *PatchPanel) AddParser(typ reflect.Type, parser Parser) {
	pc.Lock()
	defer pc.Unlock()
	pc.parsers[typ] = parser
}

when parsing a type, patchpanel will call the registered parser (if available) along with any parser hints that are provided.

Functionality
  • Getting values via struct tags
  • Type coercions / deserializers
Examples
Simple usage:
type ServerConfig struct {
    Port int `env:"PORT" default:"8080"`
    TLS  bool `default:"true"`
}

func main() {
    panel := patchpanel.NewPatchPanel(patchpanel.TokenSeparator, patchpanel.KeyValueSeparator)
    cfgType := patchpanel.ToReflectType(ServerConfig{})

    // extract the "default" tag from the "Port" field
    _, val, err := panel.GetFieldTag("Port", "default", cfgType, nil)
    if err != nil {
        panic(err)
    }

    // the Port val is now strongly typed as int(8080)
    fmt.Printf("Type: %T, Value: %v\n", val, val)
}
Usage of the GetDefault() convenience function:
type RateLimit struct {
    MaxRequests int `default:"100"`
}

func main() {
    panel := patchpanel.NewPatchPanel("·", ":")
    rlType := patchpanel.ToReflectType(RateLimit{})

    val, err := panel.GetDefault("MaxRequests", rlType, nil)
    if err != nil {
        // Handle specific errors such as missing values
        if errors.As(err, &patchpanel.NoValueError{}) {
            fmt.Println("No default value provided.")
        }
    }
}
Adding a custom parser and using a default value:
type IPv4Address string

func main() {
    panel := patchpanel.NewPatchPanel(patchpanel.TokenSeparator, patchpanel.KeyValueSeparator)

    // Define the parser for the custom type IPv4Address
    ipv4Parser := func(v string, hints map[string]any) (any, error) {
        parts := strings.Split(v, ".")
        if len(parts) != 4 {
            return nil, errors.New("invalid ipv4 format")
        }
        return IPv4Address(v), nil
    }

    // Register the parser against the reflect.Type of IPv4Address
    panel.AddParser(patchpanel.ToReflectType(IPv4Address("")), ipv4Parser)

    type NetworkConfig struct {
        BindIP IPv4Address `default:"127.0.0.1"`
    }

    cfgType := patchpanel.ToReflectType(NetworkConfig{})
    val, err := panel.GetDefault("BindIP", cfgType, nil)
    if err != nil {
        panic(err)
    }

    // val is now strongly typed as IPv4Address("127.0.0.1")
}
Integrating spf13/viper with patchpanel:
package main

import (
	"context"
	"errors"
	"fmt"
	"github.com/spf13/viper"
	"github.com/tristanfisher/patchpanel"
	"os"
	"reflect"
	"time"
)

type Config struct {
	Runtime time.Duration `default:"5s"`
}

func ParseConfig(configPath string, configStruct Config) (*Config, error) {
	viperConf := viper.New()
	patch := patchpanel.NewPatchPanel(patchpanel.TokenSeparator, patchpanel.KeyValueSeparator)

	// get defaults off of our struct using patchpanel
	confType := reflect.TypeOf(configStruct)
	for i := 0; i < confType.NumField(); i++ {
		fieldVal, err := patch.GetDefault(confType.Field(i).Name, confType, []string{})
		if err != nil {
			return &Config{}, err
		}
		viperConf.SetDefault(confType.Field(i).Name, fieldVal)
	}

	// check configuration file for more values
	if configPath != "" {
		viperConf.SetConfigFile(configPath)
		err := viperConf.ReadInConfig()
		if err != nil {
			var configFileNotFoundError viper.ConfigFileNotFoundError
			if errors.As(err, &configFileNotFoundError) {
				return nil, fmt.Errorf("file not found: %s", err)
			}
			return &Config{}, err
		}
	}

	viperConf.AutomaticEnv()
	err := viperConf.Unmarshal(&configStruct)
	if err != nil {
		return &Config{}, err
	}

	return &configStruct, nil
}

func main() {
	// grab a values/configuration file path from our environment using patchpanel
	valuesFile := patchpanel.GetFileEnvOrPath(patchpanel.ENV_CONFIG_FILE, patchpanel.FLAG_CONFIG_FILE)

	conf, err := ParseConfig(valuesFile, Config{})
	if err != nil {
		_, _ = os.Stderr.WriteString(fmt.Sprintf("error parsing configuration: %s\n", err.Error()))
		os.Exit(1)
	}

	_, _  = os.Stdout.WriteString(fmt.Sprintf(":)\n"))
	ctx, cancel := context.WithTimeout(context.Background(), conf.Runtime)
	defer cancel()
	select {
		case <-ctx.Done():
			fmt.Println("have a good day")
	}
}

Documentation

Index

Constants

View Source
const ENV_CONFIG_FILE = "CONFIG_FILE"
View Source
const FLAG_CONFIG_FILE = "config_file"
View Source
const KeyValueSeparator = `:`

KeyValueSeparator is used to split key/value combinations on a given tag e.g. for `entries:"a:b·c:d"`, KeyValueSeparator is used to create:

{
  "a": "b",
  "c": "d"
}

with TokenSeparator set to · The default is a colon due to common usage.

Reminder that multiple hints can be provided for parsing, such as `ports:"http:80·https:443" allowed_ciphers:"1.2,1.3"`

Use of key/value is optional and provided for custom parsers.

View Source
const TokenSeparator = `·`

TokenSeparator separates entries inside a given tag, typically used in conjunction with KeyValueSeparator The default is a middot due to low likelihood of collision.

Variables

This section is empty.

Functions

func FieldNameById

func FieldNameById(obj reflect.Type, idx int) string

FieldNameById is a convenience func to get named field off of a struct. This is useful for being able to loop over a struct object. Field() lookups on structs use zero-based counting

func GetFileEnvOrPath

func GetFileEnvOrPath(envKey string, flagKey string) string

func ToReflectType

func ToReflectType(input any) reflect.Type

ToReflectType is a shallow wrapper around reflect.TypeOf, placed in this library for reasons of code-flow This library operates on types that are understood by the `reflect` library

Types

type KindParser added in v1.0.2

type KindParser func(value string, toType reflect.Type, parserHints map[string]any) (any, error)

KindParser is like Parser but also receives the target reflect.Type, enabling it to handle all types matching a given reflect.Kind (e.g. all struct types). This is necessary because a single KindParser must be able to construct a zero value of any type of that kind.

type NoFieldError

type NoFieldError struct {
	Msg string
}

NoFieldError allows for differentiating no named field vs parsing errors

func (NoFieldError) Error

func (nfe NoFieldError) Error() string

type NoValueError

type NoValueError struct {
	Msg string
}

func (NoValueError) Error

func (nve NoValueError) Error() string

type Parser

type Parser func(value string, parserHints map[string]any) (any, error)

Parser handles parsing a string input based on an arbitrary function body. parserHints are provided at call time for interpretation within a given function (see time.Time's parser in NewPatchPanel for an example implementation)

type PatchPanel

type PatchPanel struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

func NewPatchPanel

func NewPatchPanel(tokenSeparator string, keyValueSeparator string) *PatchPanel

NewPatchPanel instantiates a PatchPanel

func (*PatchPanel) AddKindParser added in v1.0.2

func (pc *PatchPanel) AddKindParser(kind reflect.Kind, parser KindParser)

AddKindParser adds a KindParser that is used as a fallback for all types matching the given reflect.Kind when no exact reflect.Type parser is registered. The ability to overwrite is intentional.

func (*PatchPanel) AddParser

func (pc *PatchPanel) AddParser(typ reflect.Type, parser Parser)

AddParser adds a parser configuration. The ability to overwrite is intentional.

func (*PatchPanel) GetDefault

func (pc *PatchPanel) GetDefault(fieldName string, t reflect.Type, parserHints []string) (any, error)

GetDefault retrieves the field tag called 'default' and extracts the value

func (*PatchPanel) GetFieldTag

func (pc *PatchPanel) GetFieldTag(fieldName string, tagName string, t reflect.Type, parserHints []string) (reflect.StructField, any, error)

GetFieldTag loads a tag off of a given field in a struct. In an example struct of { A int `x:"y"` }, the fieldName is A, the tagName is x.

The struct field, the tag value, and any error is returned.

type UnhandledParserTypeError

type UnhandledParserTypeError struct {
	Msg string
}

UnhandledParserTypeError allows for a user to handle his/her type or just use as is (e.g. if plumbing an any)

func (UnhandledParserTypeError) Error

func (u UnhandledParserTypeError) Error() string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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