Skip to content

Commit

Permalink
Remove Reflection and Added Generics
Browse files Browse the repository at this point in the history
  • Loading branch information
K4L1Ma committed Jun 19, 2024
1 parent 983ddc1 commit 73f4414
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 345 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
go-version: 1.21

- name: Build
run: go build -v ./...
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ Bootstrap package for building Services in Go, Handle Signaling and Config comin


func main() {
kernel := snout.Kernel{
kernel := snout.Kernel[Config]{
RunE: Run,
}
kernelBootstrap := kernel.Bootstrap(
context.Background(),
new(Config),
)
if err := kernelBootstrap.Initialize(); err != nil {
if err != context.Canceled {
Expand Down
231 changes: 120 additions & 111 deletions bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package snout

import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"reflect"
"strings"
Expand All @@ -17,76 +20,124 @@ import (
"github.com/spf13/viper"
)

type Kernel struct {
RunE interface{}
var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true}))

// ServiceConfig is a generic type for service configuration.
type ServiceConfig any

// Kernel represents a service kernel with a run function.
type Kernel[T ServiceConfig] struct {
RunE func(ctx context.Context, cfg T) error
}

type env struct {
// Env represents the environment configuration.
type Env struct {
VarFile string
VarsPrefix string
}

type kernelOptions struct {
// KernelOptions contains options for configuring the kernel.
type KernelOptions struct {
ServiceName string
Env env
Env Env
}

func newKernelOptions() *kernelOptions {
return &kernelOptions{
// Options is a function type for configuring KernelOptions.
type Options func(kernel *KernelOptions)

// NewKernelOptions returns a new instance of KernelOptions with default values.
func NewKernelOptions() *KernelOptions {
return &KernelOptions{
ServiceName: "",
Env: env{
Env: Env{
VarFile: ".",
VarsPrefix: "",
},
}
}

// WithServiceName creates a profile based on the service name to look up for envVar files
// WithServiceName sets the service name in KernelOptions.
func WithServiceName(name string) Options {
return func(kernel *kernelOptions) {
return func(kernel *KernelOptions) {
kernel.ServiceName = name
}
}

// WithEnvVarPrefix strips any prefix from os EnvVars to map it into Config struct.
// WithEnvVarPrefix sets the environment variable prefix in KernelOptions.
func WithEnvVarPrefix(prefix string) Options {
return func(kernel *kernelOptions) {
return func(kernel *KernelOptions) {
kernel.Env.VarsPrefix = prefix
}
}

// WithEnvVarFolderLocation Specify where to look up form the env var file.
// WithEnvVarFolderLocation sets the folder location for environment variable files in KernelOptions.
func WithEnvVarFolderLocation(folderLocation string) Options {
return func(kernel *kernelOptions) {
return func(kernel *KernelOptions) {
kernel.Env.VarFile = folderLocation
}
}

type Options func(kernel *kernelOptions)

// Bootstrap service creating a Ctx with Signalling and fetching EnvVars from
// env, yaml or json file, or straight from envVars from the OS.
func (k *Kernel) Bootstrap(ctx context.Context, cfg interface{}, opts ...Options) kernelBootstrap {
krnlOpt := newKernelOptions()
for _, o := range opts {
o(krnlOpt)
// Bootstrap initializes the kernel with given options, setting up context and fetching configuration.
func (k *Kernel[T]) Bootstrap(ctx context.Context, opts ...Options) KernelBootstrap[T] {
kernelOpts := NewKernelOptions()
for _, opt := range opts {
opt(kernelOpts)
}

ctx = signallingContext(ctx)
ctx = setUpSignalHandling(ctx)
cfg := k.fetchVars(kernelOpts)

k.varFetching(cfg, krnlOpt)
return KernelBootstrap[T]{ctx, cfg, k.RunE}
}

return kernelBootstrap{ctx, cfg, k.RunE}
// KernelBootstrap holds the context, configuration, and run function for the kernel.
type KernelBootstrap[T ServiceConfig] struct {
context context.Context
cfg T
runE func(ctx context.Context, cfg T) error
}

func (k Kernel) varFetching(cfg interface{}, options *kernelOptions) {
// Initialize validates the configuration and runs the kernel.
func (kb KernelBootstrap[T]) Initialize() (err error) {
validate := validator.New()

if err = validate.Struct(kb.cfg); err != nil {
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
}

defer func() {
if r := recover(); r != nil {
switch pErr := r.(type) {
case string:
err = fmt.Errorf("%w: %s", ErrPanic, pErr)
case error:
err = fmt.Errorf("%w: %w", ErrPanic, pErr)
default:
err = fmt.Errorf("%w: %+v", ErrPanic, pErr)
}
}
}()

return kb.runE(kb.context, kb.cfg)
}

// ErrPanic is an error indicating a panic occurred.
var ErrPanic = errors.New("panic")

// ErrValidation is an error indicating a validation failure.
var ErrValidation = errors.New("validation error")

// fetchVars fetches the configuration using Viper from environment variables and configuration files.
func (k *Kernel[T]) fetchVars(options *KernelOptions) T {
var cfg T

viper.SetEnvPrefix(options.Env.VarsPrefix)
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

flagSet := pflag.NewFlagSet(options.ServiceName, pflag.ContinueOnError)

if err := gpflag.ParseTo(cfg, flagSet, sflags.FlagDivider("."), sflags.FlagTag("snout")); err != nil {
if err := gpflag.ParseTo(&cfg, flagSet, sflags.FlagDivider("."), sflags.FlagTag("snout")); err != nil {
panic(err)
}

Expand All @@ -98,120 +149,78 @@ func (k Kernel) varFetching(cfg interface{}, options *kernelOptions) {
viper.AddConfigPath(options.Env.VarFile)

if err := viper.ReadInConfig(); err == nil {
fmt.Printf("Using config file: %s \n", viper.ConfigFileUsed())
logger.Info("Using config file", slog.String("config file", viper.ConfigFileUsed()))
}

setDefaultValues(reflect.TypeOf(cfg).Elem(), "")
setDefaultValues(reflect.TypeOf(&cfg).Elem(), "")

if err := viper.Unmarshal(cfg, unmarshalWithStructTag("snout")); err != nil {
if err := viper.Unmarshal(&cfg, unmarshalWithStructTag("snout")); err != nil {
panic(err)
}
}

func setDefaultValues(p reflect.Type, path string) {
for i := 0; i < p.NumField(); i++ {
field := p.Field(i)
return cfg
}

PathMap := map[bool]string{
true: strings.ToUpper(fmt.Sprintf("%s.%s", path, field.Tag.Get("snout"))),
false: strings.ToUpper(field.Tag.Get("snout")),
}
// setUpSignalHandling sets up a context with signal notifications.
func setUpSignalHandling(ctx context.Context) context.Context {
ctx, _ = signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)

finalPath := PathMap[path != ""]
return ctx
}

var typ reflect.Type
// setDefaultValues sets default values recursively for configuration fields.
func setDefaultValues(t reflect.Type, path string) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
finalPath := constructFinalPath(path, field)

switch field.Type.Kind() {
case reflect.Ptr:
typ = field.Type.Elem()
default:
typ = field.Type
if field.Type.Kind() == reflect.Struct {
setDefaultValues(field.Type, finalPath)
} else {
setDefaultValue(finalPath, field)
}
}
}

if typ.Kind() != reflect.Struct {
get := field.Tag.Get("default")
viper.SetDefault(finalPath, get)
// constructFinalPath constructs the final path for a field based on its tag and the current path.
func constructFinalPath(path string, field reflect.StructField) string {
tag := field.Tag.Get("snout")
if path != "" {
return strings.ToUpper(fmt.Sprintf("%s.%s", path, tag))
}

continue
}
return strings.ToUpper(tag)
}

setDefaultValues(typ, finalPath)
// setDefaultValue sets the default value for a field in Viper.
func setDefaultValue(finalPath string, field reflect.StructField) {
if defaultValue := field.Tag.Get("default"); defaultValue != "" {
viper.SetDefault(finalPath, defaultValue)
}
}

// unmarshalWithStructTag sets the struct tag for unmarshaling configuration.
func unmarshalWithStructTag(tag string) viper.DecoderConfigOption {
return func(config *mapstructure.DecoderConfig) {
config.TagName = tag
config.DecodeHook = mapstructure.ComposeDecodeHookFunc(customUnMarshallerHookFunc)
}
}

// customUnMarshallerHookFunc is a custom unmarshal function for handling time.Duration.
func customUnMarshallerHookFunc(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if t.String() == "time.Duration" && f.Kind() == reflect.String {
var s string
var (
stringDuration string
ok bool
)

if s = data.(string); s == "" {
s = "0s"
if stringDuration, ok = data.(string); stringDuration == "" && ok {
stringDuration = "0s"
}

return time.ParseDuration(s)
return time.ParseDuration(stringDuration)
}

return data, nil
}

func signallingContext(ctx context.Context) context.Context {
ctx, _ = signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)

return ctx
}

type kernelBootstrap struct {
context context.Context
cfg interface{}
runE interface{}
}

var ErrPanic = fmt.Errorf("panic")
var ErrValidation = fmt.Errorf("validation error")

// Initialize Runs the Bootstrapped service
func (kb kernelBootstrap) Initialize() (err error) {
validate := validator.New()

if err = validate.Struct(kb.cfg); err != nil {
return fmt.Errorf("%w:%s", ErrValidation, err.Error())
}

typeOf := reflect.TypeOf(kb.runE)
if typeOf.Kind() != reflect.Func {
return fmt.Errorf("%s is not a reflect.Func", reflect.TypeOf(kb.runE))
}

defer func() {
if r := recover(); r != nil {
switch pErr := r.(type) {
case string:
err = fmt.Errorf("%w:%s", ErrPanic, pErr)
case error:
err = fmt.Errorf("%w:%v", ErrPanic, pErr)
default:
err = fmt.Errorf("%w:%+v", ErrPanic, pErr)
}
return
}
}()

var In []reflect.Value
In = append(In, reflect.ValueOf(kb.context))
In = append(In, reflect.ValueOf(kb.cfg).Elem())

call := reflect.ValueOf(kb.runE).Call(In)

err, ok := call[0].Interface().(error)
if !ok {
return nil
}

return err
}
Loading

0 comments on commit 73f4414

Please sign in to comment.