Skip to content

Commit

Permalink
feat(flag): cli flag support. (#13)
Browse files Browse the repository at this point in the history
* feat(flag/stdflags): initial cli flag support.

* chore(_example): updates example.

* feat(diag): adds support for diagnosing flag specified config file path.

* chore(stdflag): renames `flag/flag` to `flag/stdflag` to avoid polluting stdlib imports.

* feat(configurator): initial cli flag support.

* chore(_example): reorder imports.

* refactor(configurator): moves flag processing into a function.

* refactor(configurator): migrates core processing functions to methods.
  • Loading branch information
matthewhartstonge authored Sep 13, 2024
1 parent bb33806 commit 692ceef
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 97 deletions.
6 changes: 5 additions & 1 deletion _example/envconfig.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"strconv"

"github.com/matthewhartstonge/configurator"
"github.com/matthewhartstonge/configurator/diag"
)
Expand All @@ -15,7 +17,9 @@ type ExampleEnvConfig struct {
func (e *ExampleEnvConfig) Validate(_ diag.Component) diag.Diagnostics {
var diags diag.Diagnostics
if e.Port < 0 || e.Port > 65535 {
diags.Env("PORT").Error("Unable to parse port", "Port must be between 0 and 65535")
diags.Env("PORT").Error("Unable to parse port",
"Port must be between 0 and 65535, but instead got "+strconv.Itoa(e.Port))
e.Port = 0
}

return diags
Expand Down
15 changes: 12 additions & 3 deletions _example/fileconfig.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"strconv"
"time"

"github.com/matthewhartstonge/configurator"
Expand All @@ -21,11 +22,19 @@ type ExampleFileConfig struct {
func (e *ExampleFileConfig) Validate(component diag.Component) diag.Diagnostics {
var diags diag.Diagnostics
if e.MyApp.Port < 0 || e.MyApp.Port > 65535 {
diags.FromComponent(component, "PORT").
Error("Unable to parse port", "Port must be between 0 and 65535")
diags.FromComponent(component, "myapp.port").
Error("Unable to parse port",
"Port must be between 0 and 65535, but instead got "+strconv.Itoa(e.MyApp.Port))
e.MyApp.Port = 0
}
if e.MyApp.BackupFrequency < 0 {
diags.FromComponent(component, "myapp.backupFrequency").
Error("Unable to parse backup frequency",
"Backup frequency should be provided in hours and should be non-negative, but got "+strconv.Itoa(e.MyApp.BackupFrequency))
e.MyApp.BackupFrequency = 0
}

return nil
return diags
}

func (e *ExampleFileConfig) Merge(d any) any {
Expand Down
58 changes: 58 additions & 0 deletions _example/flagconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"flag"
"strconv"
"time"

"github.com/matthewhartstonge/configurator"
"github.com/matthewhartstonge/configurator/diag"
"github.com/matthewhartstonge/configurator/flag/stdflag"
)

var _ configurator.ConfigParser = (*ExampleFlagConfig)(nil)
var _ configurator.ConfigImplementer = (*ExampleFlagConfig)(nil)
var _ configurator.ConfigFlagImplementer = (*ExampleFlagConfig)(nil)

type ExampleFlagConfig struct {
stdflag.Flag

Port int
BackupFrequency int
}

func (f *ExampleFlagConfig) Init() {
flag.IntVar(&f.Port, "port", 0, "path to config file")
flag.IntVar(&f.BackupFrequency, "backup-frequency", 0, "path to config file")
}

func (f *ExampleFlagConfig) Validate(component diag.Component) diag.Diagnostics {
var diags diag.Diagnostics
if f.Port < 0 || f.Port > 65535 {
diags.FromComponent(component, "-port").
Error("Unable to parse port",
"Port must be between 0 and 65535, but instead got "+strconv.Itoa(f.Port))
f.Port = 0
}
if f.BackupFrequency < 0 {
diags.FromComponent(component, "-backup-frequency").
Error("Unable to parse backup frequency",
"Backup frequency should be provided in hours and be non-negative, but instead got "+strconv.Itoa(f.BackupFrequency))
f.BackupFrequency = 0
}

return diags
}

func (f *ExampleFlagConfig) Merge(d any) any {
cfg := d.(*DomainConfig)

if f.Port != 0 {
cfg.Port = uint16(f.Port)
}
if f.BackupFrequency != 0 {
cfg.BackupFrequency = time.Duration(f.BackupFrequency) * time.Hour
}

return cfg
}
5 changes: 3 additions & 2 deletions _example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/matthewhartstonge/configurator/file/json"
"github.com/matthewhartstonge/configurator/file/toml"
"github.com/matthewhartstonge/configurator/file/yaml"
"github.com/matthewhartstonge/configurator/flag/stdflag"
)

type DomainConfig struct {
Expand All @@ -31,14 +32,14 @@ func main() {
cfg := &configurator.Config{
AppName: "ExampleApp",
Domain: defaults,
File: []configurator.ConfigTypeable{
File: []configurator.ConfigFileTypeable{
yaml.New(&ExampleFileConfig{}),
toml.New(&ExampleFileConfig{}),
json.New(&ExampleFileConfig{}),
hcl.New(&ExampleFileConfig{}),
},
Env: envconfig.New(&ExampleEnvConfig{}),
Flag: nil,
Flag: stdflag.New(&ExampleFlagConfig{}),
}

// Calling `New` implicitly parses the configuration. If you want to
Expand Down
2 changes: 2 additions & 0 deletions _example/specific-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
myapp:
version: 1.0.1
58 changes: 48 additions & 10 deletions config_file_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ var (
// type. If manual unmarshaling is required, the Unmarshaler can be provided.
func NewConfigFileType(
config ConfigImplementer,
fileExtensions []string,
fileTypes []string,
unmarshaler Unmarshaler,
) ConfigFileType {
return ConfigFileType{
unmarshaler: unmarshaler,
Extensions: fileExtensions,
Types: fileTypes,
ConfigType: ConfigType{
Config: config,
},
Expand All @@ -43,8 +43,9 @@ type ConfigFileType struct {
unmarshaler Unmarshaler
// Path is the path to the file that will be read and unmarshaled.
Path string
// Extensions is a list of file extensions that the provider will look for.
Extensions []string
// Types is a list of file types or dot extensions that the provider
// is able to process.
Types []string

// ConfigType is the embedded configurator.ConfigType.
ConfigType
Expand All @@ -57,21 +58,58 @@ func (f *ConfigFileType) Type() string {

// Stat checks if the file exists and computes the platform specific Path and
// directly writes to the provided diagnostics.
func (f *ConfigFileType) Stat(diags *diag.Diagnostics, component diag.Component, cfg *Config, dirPath string) bool {
for _, ext := range f.Extensions {
cfgFilePath := dirPath + string(filepath.Separator) + cfg.FileName + "." + ext
func (f *ConfigFileType) Stat(diags *diag.Diagnostics, component diag.Component, cfg *Config, filePath string) bool {
// todo: tidy `ConfigFileType.Stat` implementation. there should be a better way.
filename := filepath.Base(filePath)
fileExt := filepath.Ext(filePath)
for _, fileType := range f.Types {
// stat for full paths, if provided.
if fileExt != "" {
if fileExt != "."+fileType {
// full file path provided, ext does match provider file type - skip.
diags.FromComponent(component, filePath).
Trace("Skipping File Type",
"The file type does not match "+fileType)
continue
}

// stat full path!
info, err := os.Stat(filePath)
if err != nil {
diags.FromComponent(component, filePath).
Trace("Config File Not Found",
"No config file was found at the specified path, error: "+err.Error())
return false
}

if info.IsDir() {
// y u disguised as file...
continue
}

// specified config file exists for the given file parser!
f.Path = filePath
diags.FromComponent(component, filePath).
Trace("Config File Found",
fmt.Sprintf("Will attempt to parse %s", filename))
return true
}

// Dynamically build the expected config file path that can be parsed
// with this provider to check for files existence.
cfgFilePath := filePath + string(filepath.Separator) + cfg.FileName + "." + filePath
if _, err := os.Stat(cfgFilePath); err == nil {
f.Path = cfgFilePath
diags.FromComponent(component, dirPath).
diags.FromComponent(component, filePath).
Trace("Config File Found",
fmt.Sprintf("Will attempt to parse %s", cfgFilePath))
return true
}
}

diags.FromComponent(component, dirPath).
diags.FromComponent(component, filePath).
Trace("Config File Not Found",
fmt.Sprintf("Unable to find config file for extensions {%s} at %s", strings.Join(f.Extensions, ", "), dirPath))
fmt.Sprintf("Unable to find config file for extensions {%s} at %s", strings.Join(f.Types, ", "), filePath))
return false
}

Expand Down
16 changes: 16 additions & 0 deletions config_file_typeable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package configurator

import (
"github.com/matthewhartstonge/configurator/diag"
)

type ConfigFileTypeable interface {
ConfigParser
ConfigFileParser
ConfigImplementer
}

type ConfigFileParser interface {
// Stat returns false if a file can't be found by the parser.
Stat(diags *diag.Diagnostics, component diag.Component, cfg *Config, dirPath string) bool
}
13 changes: 13 additions & 0 deletions config_flag_typeable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package configurator

type ConfigFlagTypeable interface {
ConfigParser
ConfigFlagImplementer
}

type ConfigFlagImplementer interface {
ConfigImplementer

// Init is called to configure and set up the flag environment.
Init()
}
5 changes: 0 additions & 5 deletions config_typeable.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ type ConfigTypeable interface {
ConfigImplementer
}

type ConfigFileParser interface {
// Stat returns false if a file can't be found by the parser.
Stat(diags *diag.Diagnostics, component diag.Component, cfg *Config, dirPath string) bool
}

type ConfigParser interface {
// Type informs the user as to which parser is being used.
Type() string
Expand Down
Loading

0 comments on commit 692ceef

Please sign in to comment.