Skip to content

Commit fd9fae5

Browse files
✨ Introduce optional strict mode for config (#252)
This adds a strict mode using toml marshalling to ensure that all fields in a config file are present and correctly typed. This is being introduced as an opt-in feature using the `strict_mode` flag. Co-authored-by: Copilot <[email protected]>
1 parent cc84818 commit fd9fae5

File tree

4 files changed

+57
-12
lines changed

4 files changed

+57
-12
lines changed

configurator/configurator.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package configurator
22

33
import (
4+
"errors"
45
"fmt"
56
"strings"
67

@@ -12,7 +13,7 @@ import (
1213
)
1314

1415
type Configurator interface {
15-
GetConfig() (model.Config, error)
16+
GetConfig() (model.Config, error) // Since error is an interface, we use it here to return a single variable instead of multiple variables (configError holds 2 strings, human and err)
1617
}
1718

1819
type RealConfigurator struct {
@@ -21,6 +22,20 @@ type RealConfigurator struct {
2122
runtime runtimewrap.Runtime
2223
}
2324

25+
// Helper for consolidation of error into a single structure
26+
type ConfigError struct {
27+
Err string // Load the (DecodeError/StrictMissingError).Error() into this
28+
HumanDetails string // Load the (DecodeError/StrictMissingError).String() into this
29+
}
30+
31+
func (ce *ConfigError) Error() string {
32+
return ce.Err // Return the error
33+
}
34+
35+
func (ce *ConfigError) Human() string {
36+
return ce.HumanDetails // Return the string
37+
}
38+
2439
func NewConfigurator(os oswrap.Os, path pathwrap.Path, runtime runtimewrap.Runtime) Configurator {
2540
return &RealConfigurator{os, path, runtime}
2641
}
@@ -39,20 +54,39 @@ func (c *RealConfigurator) fullImportPath(homeDir, importPath string) (string, e
3954

4055
func (c *RealConfigurator) getConfigFileFromUserConfigDir() (model.Config, error) {
4156
config := model.Config{}
57+
evaluation := model.Evaluation{}
4258
userHomeDir, err := c.os.UserHomeDir()
4359
if err != nil {
4460
return config, fmt.Errorf("couldn't get user config dir: %q", err)
4561
}
4662
userConfigDir := c.path.Join(userHomeDir, ".config")
4763
configFilePath := c.configFilePath(userConfigDir)
4864
file, _ := c.os.ReadFile(configFilePath)
49-
// TODO: add to debugging logs
65+
// TODO: add to debugging logs (Update, added details string)
5066
// if err != nil {
51-
// return config, fmt.Errorf("couldn't read config file: %q", err)
67+
// return config, "", fmt.Errorf("couldn't read config file: %q", err)
5268
// }
53-
err = toml.Unmarshal(file, &config)
54-
if err != nil {
55-
return config, fmt.Errorf("couldn't unmarshal config file: %q", err)
69+
_ = toml.Unmarshal(file, &evaluation)
70+
if evaluation.StrictMode {
71+
reader := strings.NewReader(string(file))
72+
d := toml.NewDecoder(reader)
73+
d.DisallowUnknownFields() // enable the strict mode
74+
err = d.Decode(&config)
75+
if err != nil {
76+
var details *toml.StrictMissingError
77+
if errors.As(err, &details) {
78+
return config, &ConfigError{Err: err.Error(), HumanDetails: details.String()}
79+
}
80+
return config, err
81+
}
82+
} else {
83+
err = toml.Unmarshal(file, &config)
84+
if err != nil {
85+
var derr *toml.DecodeError
86+
if errors.As(err, &derr) {
87+
return config, &ConfigError{Err: err.Error(), HumanDetails: derr.String()}
88+
}
89+
}
5690
}
5791

5892
for _, importPath := range config.ImportPaths {

main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ func createLoggerFile(userTempDir string) (*os.File, error) {
8888
now := time.Now()
8989
date := fmt.Sprintf("%s.log", now.Format("2006-01-02"))
9090

91-
if err := os.MkdirAll(path.Join(userTempDir, ".seshtmp"), 0755); err != nil {
91+
if err := os.MkdirAll(path.Join(userTempDir, ".seshtmp"), 0o755); err != nil {
9292
return nil, err
9393
}
9494

9595
fileFullPath := path.Join(userTempDir, ".seshtmp", date)
96-
file, err := os.OpenFile(fileFullPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
96+
file, err := os.OpenFile(fileFullPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
9797
if err != nil {
9898
return nil, err
9999
}

model/config.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package model
22

33
type (
44
Config struct {
5+
StrictMode bool `toml:"strict_mode"`
56
ImportPaths []string `toml:"import"`
67
DefaultSessionConfig DefaultSessionConfig `toml:"default_session"`
78
Blacklist []string `toml:"blacklist"`
89
SessionConfigs []SessionConfig `toml:"session"`
9-
SortOrder []string `toml:"sort_order"`
10+
SortOrder []string `toml:"sort_order"`
1011
WindowConfigs []WindowConfig `toml:"window"`
1112
}
13+
Evaluation struct {
14+
StrictMode bool `toml:"strict_mode"`
15+
}
1216

1317
DefaultSessionConfig struct {
1418
// TODO: mention breaking change in v2 release notes
@@ -28,8 +32,8 @@ type (
2832
}
2933

3034
WindowConfig struct {
31-
Name string `toml:"name"`
32-
StartupScript string `toml:"startup_script"`
33-
Path string `toml:"path"`
35+
Name string `toml:"name"`
36+
StartupScript string `toml:"startup_script"`
37+
Path string `toml:"path"`
3438
}
3539
)

seshcli/seshcli.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package seshcli
22

33
import (
4+
"errors"
5+
"fmt"
46
"log/slog"
57

68
"github.com/urfave/cli/v2"
@@ -53,6 +55,11 @@ func App(version string) cli.App {
5355
config, err := configurator.NewConfigurator(os, path, runtime).GetConfig()
5456
// TODO: make sure to ignore the error if the config doesn't exist
5557
if err != nil {
58+
var human *configurator.ConfigError
59+
if errors.As(err, &human) {
60+
// No panic here because it leads to panic in the end of the root branch anyway.
61+
fmt.Printf("Couldn't parse config, err: %v\n details:\n %s\n", err.Error(), human.Human())
62+
}
5663
slog.Error("seshcli/seshcli.go: App", "error", err)
5764
panic(err)
5865
}

0 commit comments

Comments
 (0)