From 254382a396e481a1ce887fd954f33872d5ba8864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Tue, 17 Dec 2024 21:18:36 +0100 Subject: [PATCH 01/13] add parsed decorator --- configuration.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ decorators.go | 72 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 23 ++++++++++++++- go.sum | 65 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 configuration.go diff --git a/configuration.go b/configuration.go new file mode 100644 index 0000000..cde63f9 --- /dev/null +++ b/configuration.go @@ -0,0 +1,77 @@ +// Copyright © 2024 Meroxa, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ecdysis + +import ( + "reflect" + + "github.com/spf13/viper" +) + +type UserConfig struct { + Prefix string + ParsedConfig any + DefaultConfig any + ConfigPath string +} + +func setDefaults(v *viper.Viper, defaults interface{}, fieldName string) { + val := reflect.ValueOf(defaults) + typ := reflect.TypeOf(defaults) + + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return + } + val = val.Elem() + typ = typ.Elem() + } + + if val.Kind() != reflect.Struct { + return + } + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // Use the long or short tag if available + fieldName := fieldType.Tag.Get("long") + if fieldName == "" { + fieldName = fieldType.Tag.Get("short") + } + + // Skip fields without a long or short tag + if fieldName == "" { + continue + } + + switch field.Kind() { + case reflect.Struct: + // Recursively handle nested structs + setDefaults(v, field.Interface(), fieldName) + case reflect.Ptr: + // Handle pointer fields + if !field.IsNil() { + setDefaults(v, field.Interface(), fieldName) + } + default: + // Set the default value + if field.CanInterface() { + v.SetDefault(fieldName, field.Interface()) + } + } + } +} diff --git a/decorators.go b/decorators.go index d8dbc22..11d896e 100644 --- a/decorators.go +++ b/decorators.go @@ -21,17 +21,23 @@ import ( "fmt" "log/slog" "os" + "reflect" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/pflag" + "github.com/spf13/viper" ) var DefaultDecorators = []Decorator{ CommandWithLoggerDecorator{}, CommandWithAliasesDecorator{}, CommandWithFlagsDecorator{}, + + // Parsing Config need to be after Flags to make sure the flags are parsed. + CommandWithParsingConfigDecorator{}, + CommandWithDocsDecorator{}, CommandWithHiddenDecorator{}, CommandWithSubCommandsDecorator{}, @@ -242,6 +248,72 @@ func (CommandWithFlagsDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command, c Comm return nil } +// -- PARSING CONFIGURATION -------------------------------------------------------------------- + +// CommandWithParsingConfig can be implemented by a command to parsing configuration. +type CommandWithParsingConfig interface { + Command + + Config() UserConfig +} + +// CommandWithParsingConfigDecorator is a decorator that sets the command flags. +type CommandWithParsingConfigDecorator struct{} + +// Decorate parses the configuration based on flags. +func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command, c Command) error { + v, ok := c.(CommandWithParsingConfig) + if !ok { + return nil + } + + old := cmd.PreRunE + cmd.PreRunE = func(cmd *cobra.Command, args []string) error { + if old != nil { + err := old(cmd, args) + if err != nil { + return err + } + } + + usrCfg := v.Config() + + // Ensure ParsedConfig is a pointer + if reflect.ValueOf(usrCfg.ParsedConfig).Kind() != reflect.Ptr { + return fmt.Errorf("ParsedConfig must be a pointer") + } + + viper := viper.New() + + // set default values + setDefaults(viper, usrCfg.DefaultConfig, "") + + // Set environment variable handling + viper.SetEnvPrefix(usrCfg.Prefix) + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // Load configuration file + viper.SetConfigFile(usrCfg.ConfigPath) + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("fatal error config file: %w", err) + } + + // Bind flags to Viper + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if err := viper.BindPFlag(f.Name, f); err != nil { + fmt.Printf("error binding flag: %v\n", err) + } + }) + // Unmarshal the configuration into the ParsedConfig + if err := viper.Unmarshal(usrCfg.ParsedConfig); err != nil { + return fmt.Errorf("error unmarshalling config: %w", err) + } + return nil + } + return nil +} + // -- DOCS --------------------------------------------------------------------- // CommandWithDocs can be implemented by a command to provide documentation. diff --git a/go.mod b/go.mod index 8501285..b9adff6 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,28 @@ require ( github.com/google/go-cmp v0.6.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 go.uber.org/mock v0.5.0 ) -require github.com/inconshreveable/mousetrap v1.1.0 // indirect +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 3cbca28..271391b 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,79 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 9cd7bcd567c3862f89e4d90d950acb59e793db34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Tue, 17 Dec 2024 21:24:08 +0100 Subject: [PATCH 02/13] simplify --- configuration.go | 6 +++--- decorators.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configuration.go b/configuration.go index cde63f9..bb9110b 100644 --- a/configuration.go +++ b/configuration.go @@ -27,7 +27,7 @@ type UserConfig struct { ConfigPath string } -func setDefaults(v *viper.Viper, defaults interface{}, fieldName string) { +func setDefaults(v *viper.Viper, defaults interface{}) { val := reflect.ValueOf(defaults) typ := reflect.TypeOf(defaults) @@ -61,11 +61,11 @@ func setDefaults(v *viper.Viper, defaults interface{}, fieldName string) { switch field.Kind() { case reflect.Struct: // Recursively handle nested structs - setDefaults(v, field.Interface(), fieldName) + setDefaults(v, field.Interface()) case reflect.Ptr: // Handle pointer fields if !field.IsNil() { - setDefaults(v, field.Interface(), fieldName) + setDefaults(v, field.Interface()) } default: // Set the default value diff --git a/decorators.go b/decorators.go index 11d896e..43ea645 100644 --- a/decorators.go +++ b/decorators.go @@ -286,7 +286,7 @@ func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command viper := viper.New() // set default values - setDefaults(viper, usrCfg.DefaultConfig, "") + setDefaults(viper, usrCfg.DefaultConfig) // Set environment variable handling viper.SetEnvPrefix(usrCfg.Prefix) From e277debba1bb1b7febf61a7ebf0847b54d1dd970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Tue, 17 Dec 2024 21:28:49 +0100 Subject: [PATCH 03/13] rename --- configuration.go | 10 +++++----- decorators.go | 26 +++++++++++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/configuration.go b/configuration.go index bb9110b..f50fcc5 100644 --- a/configuration.go +++ b/configuration.go @@ -20,11 +20,11 @@ import ( "github.com/spf13/viper" ) -type UserConfig struct { - Prefix string - ParsedConfig any - DefaultConfig any - ConfigPath string +type Config struct { + EnvPrefix string + ParsedCfg any + DefaultCfg any + ConfigPath string } func setDefaults(v *viper.Viper, defaults interface{}) { diff --git a/decorators.go b/decorators.go index 43ea645..4b758aa 100644 --- a/decorators.go +++ b/decorators.go @@ -35,7 +35,7 @@ var DefaultDecorators = []Decorator{ CommandWithAliasesDecorator{}, CommandWithFlagsDecorator{}, - // Parsing Config need to be after Flags to make sure the flags are parsed. + // CommandWithParsingConfigDecorator need to be after CommandWithFlagsDecorator to make sure the flags are parsed. CommandWithParsingConfigDecorator{}, CommandWithDocsDecorator{}, @@ -250,11 +250,11 @@ func (CommandWithFlagsDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command, c Comm // -- PARSING CONFIGURATION -------------------------------------------------------------------- -// CommandWithParsingConfig can be implemented by a command to parsing configuration. -type CommandWithParsingConfig interface { +// CommandWithConfiguration can be implemented by a command to parsing configuration. +type CommandWithConfiguration interface { Command - Config() UserConfig + ParseConfig() Config } // CommandWithParsingConfigDecorator is a decorator that sets the command flags. @@ -262,7 +262,7 @@ type CommandWithParsingConfigDecorator struct{} // Decorate parses the configuration based on flags. func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command, c Command) error { - v, ok := c.(CommandWithParsingConfig) + v, ok := c.(CommandWithConfiguration) if !ok { return nil } @@ -276,20 +276,20 @@ func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command } } - usrCfg := v.Config() + usrCfg := v.ParseConfig() - // Ensure ParsedConfig is a pointer - if reflect.ValueOf(usrCfg.ParsedConfig).Kind() != reflect.Ptr { - return fmt.Errorf("ParsedConfig must be a pointer") + // Ensure ParsedCfg is a pointer + if reflect.ValueOf(usrCfg.ParsedCfg).Kind() != reflect.Ptr { + return fmt.Errorf("ParsedCfg must be a pointer") } viper := viper.New() // set default values - setDefaults(viper, usrCfg.DefaultConfig) + setDefaults(viper, usrCfg.DefaultCfg) // Set environment variable handling - viper.SetEnvPrefix(usrCfg.Prefix) + viper.SetEnvPrefix(usrCfg.EnvPrefix) viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) @@ -305,8 +305,8 @@ func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command fmt.Printf("error binding flag: %v\n", err) } }) - // Unmarshal the configuration into the ParsedConfig - if err := viper.Unmarshal(usrCfg.ParsedConfig); err != nil { + // Unmarshal the configuration into the ParsedCfg + if err := viper.Unmarshal(usrCfg.ParsedCfg); err != nil { return fmt.Errorf("error unmarshalling config: %w", err) } return nil From bf6138402361a01e8c4f6c58b98183a098be59d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Tue, 17 Dec 2024 21:42:21 +0100 Subject: [PATCH 04/13] update readme --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/README.md b/README.md index b2da5a0..64c5717 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,66 @@ func main() { } ``` +### Configuration + +Ecdysis provides an automatic way to parse a configuration file, environment variables, and flags using the [`viper`](https://github.com/spf13/viper) library. To use it, you need to implement the `CommandWithConfig` interface. + +The order of precedence for configuration values is: + +1. Default values +2. Configuration file +3. Environment variables +4. Flags + +```go +var ( + _ ecdysis.CommandWithConfiguration = (*RootCommand)(nil) +) + +type ConduitConfig struct { + ConduitCfgPath string `long:"config.path" usage:"global conduit configuration file" default:"./conduit.yaml"` + + Connectors struct { + Path string `long:"connectors.path" usage:"path to standalone connectors' directory"` + } + + // ... +} + +type RootFlags struct { + ConduitConfig // you can embed any configuration, and it'll use the proper tags +} + +type RootCommand struct { + flags RootFlags + cfg ConduitConfig +} + +func (c *RootCommand) ParseConfig() ecdysis.Config { + return ecdysis.Config{ + EnvPrefix: "CONDUIT", // prefix for environment variables + ParsedCfg: &c.cfg, // where configuration will be parsed + ConfigPath: c.flags.ConduitCfgPath, // where to read the configuration file + DefaultCfg: c.cfg, // where to extract default values from + } +} + +func (c *RootCommand) Execute(_ context.Context) error { + // c.cfg is now populated with the right parsed configuration + return nil +} + +func (c *RootCommand) Flags() []ecdysis.Flag { + flags := ecdysis.BuildFlags(&c.flags) + + // set a default value for each flag + flags.SetDefault("config.path", c.cfg.ConduitCfgPath) + // ... + + return flags +} +```` + ## Flags Ecdysis provides a way to define flags using field tags. Flags will be From ea6ae44254877885e3f62a5e061ea933f22b36d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Tue, 17 Dec 2024 21:49:16 +0100 Subject: [PATCH 05/13] go mod tidy --- go.mod | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go.mod b/go.mod index b9adff6..05d2bf5 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,19 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.5.0 ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect From d372dbe1874538f1cbad6629c28581e9553829ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Tue, 17 Dec 2024 21:57:06 +0100 Subject: [PATCH 06/13] no lint --- configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration.go b/configuration.go index f50fcc5..a3d4bbf 100644 --- a/configuration.go +++ b/configuration.go @@ -58,7 +58,7 @@ func setDefaults(v *viper.Viper, defaults interface{}) { continue } - switch field.Kind() { + switch field.Kind() { //nolint:exhaustive // no need to handle all cases case reflect.Struct: // Recursively handle nested structs setDefaults(v, field.Interface()) From 9b7e84c9e4d9837e5699c5688ca2b7fdc0c858fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Wed, 18 Dec 2024 10:45:25 +0100 Subject: [PATCH 07/13] chore: update comments --- configuration.go | 5 ----- decorators.go | 11 +++++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/configuration.go b/configuration.go index a3d4bbf..e73794c 100644 --- a/configuration.go +++ b/configuration.go @@ -47,28 +47,23 @@ func setDefaults(v *viper.Viper, defaults interface{}) { field := val.Field(i) fieldType := typ.Field(i) - // Use the long or short tag if available fieldName := fieldType.Tag.Get("long") if fieldName == "" { fieldName = fieldType.Tag.Get("short") } - // Skip fields without a long or short tag if fieldName == "" { continue } switch field.Kind() { //nolint:exhaustive // no need to handle all cases case reflect.Struct: - // Recursively handle nested structs setDefaults(v, field.Interface()) case reflect.Ptr: - // Handle pointer fields if !field.IsNil() { setDefaults(v, field.Interface()) } default: - // Set the default value if field.CanInterface() { v.SetDefault(fieldName, field.Interface()) } diff --git a/decorators.go b/decorators.go index 4b758aa..b704ac5 100644 --- a/decorators.go +++ b/decorators.go @@ -35,7 +35,7 @@ var DefaultDecorators = []Decorator{ CommandWithAliasesDecorator{}, CommandWithFlagsDecorator{}, - // CommandWithParsingConfigDecorator need to be after CommandWithFlagsDecorator to make sure the flags are parsed. + // CommandWithParsingConfigDecorator needs to be after CommandWithFlagsDecorator to make sure the flags are parsed. CommandWithParsingConfigDecorator{}, CommandWithDocsDecorator{}, @@ -285,27 +285,26 @@ func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command viper := viper.New() - // set default values + // Set default values setDefaults(viper, usrCfg.DefaultCfg) - // Set environment variable handling + // Handle env variables viper.SetEnvPrefix(usrCfg.EnvPrefix) viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - // Load configuration file + // Handle config file viper.SetConfigFile(usrCfg.ConfigPath) if err := viper.ReadInConfig(); err != nil { return fmt.Errorf("fatal error config file: %w", err) } - // Bind flags to Viper + // Handle flags cmd.Flags().VisitAll(func(f *pflag.Flag) { if err := viper.BindPFlag(f.Name, f); err != nil { fmt.Printf("error binding flag: %v\n", err) } }) - // Unmarshal the configuration into the ParsedCfg if err := viper.Unmarshal(usrCfg.ParsedCfg); err != nil { return fmt.Errorf("error unmarshalling config: %w", err) } From 64f3a54a01440b4e575967db926e6e61dec0e92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Wed, 18 Dec 2024 11:14:56 +0100 Subject: [PATCH 08/13] update documentation --- README.md | 2 +- configuration.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 64c5717..b3ca05f 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Ecdysis provides an automatic way to parse a configuration file, environment var The order of precedence for configuration values is: -1. Default values +1. Default values (slices and maps are not currently supported) 2. Configuration file 3. Environment variables 4. Flags diff --git a/configuration.go b/configuration.go index e73794c..da1d802 100644 --- a/configuration.go +++ b/configuration.go @@ -27,6 +27,7 @@ type Config struct { ConfigPath string } +// setDefaults sets the default values for the configuration. slices and maps are not supported. func setDefaults(v *viper.Viper, defaults interface{}) { val := reflect.ValueOf(defaults) typ := reflect.TypeOf(defaults) From 03472b66d26a5675aa4eeb02fddf6719e53efdbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Wed, 18 Dec 2024 11:20:38 +0100 Subject: [PATCH 09/13] return binding errors --- decorators.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/decorators.go b/decorators.go index b704ac5..777685c 100644 --- a/decorators.go +++ b/decorators.go @@ -299,12 +299,18 @@ func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command return fmt.Errorf("fatal error config file: %w", err) } + var errors []error + // Handle flags cmd.Flags().VisitAll(func(f *pflag.Flag) { if err := viper.BindPFlag(f.Name, f); err != nil { - fmt.Printf("error binding flag: %v\n", err) + errors = append(errors, err) } }) + if len(errors) > 0 { + return fmt.Errorf("error binding flags: %w", errors) + } + if err := viper.Unmarshal(usrCfg.ParsedCfg); err != nil { return fmt.Errorf("error unmarshalling config: %w", err) } From 4808a02dd48126a8b6521b5c6aada9ea3c34c9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Wed, 18 Dec 2024 11:24:00 +0100 Subject: [PATCH 10/13] ensure both parsedCfg and defaultCfg are same types --- decorators.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/decorators.go b/decorators.go index 777685c..c661dd9 100644 --- a/decorators.go +++ b/decorators.go @@ -283,6 +283,11 @@ func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command return fmt.Errorf("ParsedCfg must be a pointer") } + // Ensure both usrCfg.ParsedCfg and usrCfg.DefaultCfg are the same type + if reflect.TypeOf(usrCfg.ParsedCfg) != reflect.TypeOf(usrCfg.DefaultCfg) { + return fmt.Errorf("ParsedCfg and DefaultCfg must be the same type") + } + viper := viper.New() // Set default values From 35a869508d409765768d412f2fd6a1e7d8f8bea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Wed, 18 Dec 2024 11:26:39 +0100 Subject: [PATCH 11/13] fix lint --- decorators.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/decorators.go b/decorators.go index c661dd9..af9f2df 100644 --- a/decorators.go +++ b/decorators.go @@ -312,8 +312,13 @@ func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command errors = append(errors, err) } }) + if len(errors) > 0 { - return fmt.Errorf("error binding flags: %w", errors) + var errStrs []string + for _, err := range errors { + errStrs = append(errStrs, err.Error()) + } + return fmt.Errorf("error binding flags: %s", strings.Join(errStrs, "; ")) } if err := viper.Unmarshal(usrCfg.ParsedCfg); err != nil { From 3fe7c6f54a27658021bf84636a6c6d418b114773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Wed, 18 Dec 2024 14:15:14 +0100 Subject: [PATCH 12/13] small refactor --- configuration.go => config.go | 44 +++++++++++++++++++++--- decorators.go | 65 +++++++++++------------------------ 2 files changed, 60 insertions(+), 49 deletions(-) rename configuration.go => config.go (62%) diff --git a/configuration.go b/config.go similarity index 62% rename from configuration.go rename to config.go index da1d802..f771cad 100644 --- a/configuration.go +++ b/config.go @@ -15,16 +15,20 @@ package ecdysis import ( + "fmt" "reflect" + "strings" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" ) type Config struct { - EnvPrefix string - ParsedCfg any - DefaultCfg any - ConfigPath string + EnvPrefix string + Parsed any + DefaultValues any + Path string } // setDefaults sets the default values for the configuration. slices and maps are not supported. @@ -71,3 +75,35 @@ func setDefaults(v *viper.Viper, defaults interface{}) { } } } + +// parseConfig parses the configuration (from cfg and cmd) into the viper instance. +func parseConfig(v *viper.Viper, cfg Config, cmd *cobra.Command) error { + // Handle env variables + v.SetEnvPrefix(cfg.EnvPrefix) + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // Handle config file + v.SetConfigFile(cfg.Path) + if err := v.ReadInConfig(); err != nil { + return fmt.Errorf("fatal error config file: %w", err) + } + + var errors []error + + // Handle flags + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if err := v.BindPFlag(f.Name, f); err != nil { + errors = append(errors, err) + } + }) + + if len(errors) > 0 { + var errStrs []string + for _, err := range errors { + errStrs = append(errStrs, err.Error()) + } + return fmt.Errorf("error binding flags: %s", strings.Join(errStrs, "; ")) + } + return nil +} diff --git a/decorators.go b/decorators.go index af9f2df..62d85fe 100644 --- a/decorators.go +++ b/decorators.go @@ -35,8 +35,8 @@ var DefaultDecorators = []Decorator{ CommandWithAliasesDecorator{}, CommandWithFlagsDecorator{}, - // CommandWithParsingConfigDecorator needs to be after CommandWithFlagsDecorator to make sure the flags are parsed. - CommandWithParsingConfigDecorator{}, + // CommandWithConfigDecorator needs to be after CommandWithFlagsDecorator to make sure the flags are parsed. + CommandWithConfigDecorator{}, CommandWithDocsDecorator{}, CommandWithHiddenDecorator{}, @@ -250,19 +250,19 @@ func (CommandWithFlagsDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command, c Comm // -- PARSING CONFIGURATION -------------------------------------------------------------------- -// CommandWithConfiguration can be implemented by a command to parsing configuration. -type CommandWithConfiguration interface { +// CommandWithConfig can be implemented by a command to parsing configuration. +type CommandWithConfig interface { Command - ParseConfig() Config + Config() Config } -// CommandWithParsingConfigDecorator is a decorator that sets the command flags. -type CommandWithParsingConfigDecorator struct{} +// CommandWithConfigDecorator is a decorator that sets the command flags. +type CommandWithConfigDecorator struct{} // Decorate parses the configuration based on flags. -func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command, c Command) error { - v, ok := c.(CommandWithConfiguration) +func (CommandWithConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command, c Command) error { + v, ok := c.(CommandWithConfig) if !ok { return nil } @@ -276,52 +276,27 @@ func (CommandWithParsingConfigDecorator) Decorate(_ *Ecdysis, cmd *cobra.Command } } - usrCfg := v.ParseConfig() + cfg := v.Config() - // Ensure ParsedCfg is a pointer - if reflect.ValueOf(usrCfg.ParsedCfg).Kind() != reflect.Ptr { - return fmt.Errorf("ParsedCfg must be a pointer") + // Ensure Parsed is a pointer + if reflect.ValueOf(cfg.Parsed).Kind() != reflect.Ptr { + return fmt.Errorf("parsed must be a pointer") } - // Ensure both usrCfg.ParsedCfg and usrCfg.DefaultCfg are the same type - if reflect.TypeOf(usrCfg.ParsedCfg) != reflect.TypeOf(usrCfg.DefaultCfg) { - return fmt.Errorf("ParsedCfg and DefaultCfg must be the same type") + // Ensure both cfg.Parsed and cfg.DefaultValues are the same type + if reflect.TypeOf(cfg.Parsed) != reflect.TypeOf(cfg.DefaultValues) { + return fmt.Errorf("parsed and DefaultValues must be the same type") } viper := viper.New() - // Set default values - setDefaults(viper, usrCfg.DefaultCfg) + setDefaults(viper, cfg.DefaultValues) - // Handle env variables - viper.SetEnvPrefix(usrCfg.EnvPrefix) - viper.AutomaticEnv() - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - - // Handle config file - viper.SetConfigFile(usrCfg.ConfigPath) - if err := viper.ReadInConfig(); err != nil { - return fmt.Errorf("fatal error config file: %w", err) - } - - var errors []error - - // Handle flags - cmd.Flags().VisitAll(func(f *pflag.Flag) { - if err := viper.BindPFlag(f.Name, f); err != nil { - errors = append(errors, err) - } - }) - - if len(errors) > 0 { - var errStrs []string - for _, err := range errors { - errStrs = append(errStrs, err.Error()) - } - return fmt.Errorf("error binding flags: %s", strings.Join(errStrs, "; ")) + if err := parseConfig(viper, cfg, cmd); err != nil { + return fmt.Errorf("error parsing config: %w", err) } - if err := viper.Unmarshal(usrCfg.ParsedCfg); err != nil { + if err := viper.Unmarshal(cfg.Parsed); err != nil { return fmt.Errorf("error unmarshalling config: %w", err) } return nil From d38bc09e8b14ca648c2cb170639171c104303b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Wed, 18 Dec 2024 14:23:59 +0100 Subject: [PATCH 13/13] Add a note about flags --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b3ca05f..f4c9b66 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,12 @@ The order of precedence for configuration values is: 3. Environment variables 4. Flags + +> [!IMPORTANT] +> For flags, it's important to set default values to ensure that the configuration will be correctly parsed. +> Otherwise, they will be empty, and it will be considered as if the user set that intentionally. +> example: `flags.SetDefault("config.path", c.cfg.ConduitCfgPath)` + ```go var ( _ ecdysis.CommandWithConfiguration = (*RootCommand)(nil)