Skip to content

Commit 6dcb528

Browse files
Add YAML config loading functionality
1 parent 9ae1f5c commit 6dcb528

File tree

7 files changed

+221
-193
lines changed

7 files changed

+221
-193
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# ignore the local captainhook.json configuration
55
/captainhook.json
66
/captainhook.config.json
7+
/captainhook.yml
8+
/captainhook.config.yml
79

810
# ignore goreleaser build directory
911
/dist/

commands/setup.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package commands
22

33
import (
44
"errors"
5+
"fmt"
56
"github.com/captainhook-go/captainhook/configuration"
67
"github.com/captainhook-go/captainhook/info"
78
"github.com/spf13/cobra"
9+
"path/filepath"
10+
"strings"
811
)
912

1013
// configurationAware is for all commands that need to read or write a configuration
@@ -25,7 +28,7 @@ func repositoryAware(cmd *cobra.Command) {
2528
// This is important since the command line options should supersede all other ways of
2629
// configuring the Cap'n.
2730
func setUpConfig(cmd *cobra.Command, fileRequired bool) (*configuration.Configuration, error) {
28-
nullableSettings := &configuration.JsonAppSettings{}
31+
nullableSettings := &configuration.NullableAppSettings{}
2932

3033
detectColor(cmd, nullableSettings)
3134
detectGitDir(cmd, nullableSettings)
@@ -36,8 +39,17 @@ func setUpConfig(cmd *cobra.Command, fileRequired bool) (*configuration.Configur
3639
if len(confOption) > 0 {
3740
confPath = confOption
3841
}
42+
var factory configuration.ConfigFactory
43+
ext := strings.ToLower(filepath.Ext(confPath))
44+
switch ext {
45+
case ".yaml", ".yml":
46+
factory = configuration.NewYamlFactory()
47+
case ".json":
48+
factory = configuration.NewJsonFactory()
49+
default:
50+
return nil, errors.New(fmt.Sprintf("Unknown configuration format: %s", ext))
51+
}
3952

40-
factory := configuration.NewFactory()
4153
conf, confErr := factory.CreateConfig(confPath, nullableSettings)
4254
if confErr != nil {
4355
return nil, confErr
@@ -49,7 +61,7 @@ func setUpConfig(cmd *cobra.Command, fileRequired bool) (*configuration.Configur
4961
}
5062

5163
// detectColor is checking the `--no-color` option and sets the configuration accordingly
52-
func detectColor(cmd *cobra.Command, settings *configuration.JsonAppSettings) {
64+
func detectColor(cmd *cobra.Command, settings *configuration.NullableAppSettings) {
5365
noColor := getNoColor(cmd)
5466
if noColor {
5567
falsePointer := false
@@ -63,7 +75,7 @@ func getNoColor(cmd *cobra.Command) bool {
6375
}
6476

6577
// detectGitDir is checking the `--git-directory` option and sets the configuration accordingly
66-
func detectGitDir(cmd *cobra.Command, settings *configuration.JsonAppSettings) {
78+
func detectGitDir(cmd *cobra.Command, settings *configuration.NullableAppSettings) {
6779
repoOption := getGitDir(cmd)
6880
if len(repoOption) > 0 {
6981
settings.GitDirectory = &repoOption
@@ -80,7 +92,7 @@ func getGitDir(cmd *cobra.Command) string {
8092
}
8193

8294
// detectVerbosity is checking the `--verbose` and `--debug` options and sets the configuration accordingly
83-
func detectVerbosity(cmd *cobra.Command, settings *configuration.JsonAppSettings) {
95+
func detectVerbosity(cmd *cobra.Command, settings *configuration.NullableAppSettings) {
8496
v := getVerbosity(cmd)
8597
if v != "normal" {
8698
settings.Verbosity = &v

configuration/factory.go

Lines changed: 5 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,16 @@
11
package configuration
22

33
import (
4-
"encoding/json"
5-
"errors"
64
"fmt"
7-
"github.com/captainhook-go/captainhook/info"
8-
"github.com/captainhook-go/captainhook/io"
95
"os"
10-
"path/filepath"
116
)
127

13-
type Factory struct {
14-
includeLevel int
15-
maxIncludeLevel int
8+
type ConfigFactory interface {
9+
// CreateConfig creates a CaptainHook configuration
10+
CreateConfig(path string, cliSettings *NullableAppSettings) (*Configuration, error)
1611
}
1712

18-
// CreateConfig creates a default configuration in case the file exists it is loaded
19-
func (f *Factory) CreateConfig(path string, cliSettings *JsonAppSettings) (*Configuration, error) {
20-
c, err := f.setupConfig(path)
21-
// load the local config "captainhook.config.json"
22-
decodeErr := f.loadSettingsFile(c)
23-
if decodeErr != nil {
24-
return c, decodeErr
25-
}
26-
// everything provided from the command line should overwrite any loaded configuration
27-
// this works even if there is an error because then you have a default configuration
28-
c.overwriteSettings(cliSettings)
29-
return c, err
30-
}
31-
32-
// setupConfig creates a new configuration and loads the json file if it exists
33-
func (f *Factory) setupConfig(path string) (*Configuration, error) {
34-
var err error
35-
c := NewConfiguration(path, io.FileExists(path))
36-
if c.fileExists {
37-
err = f.loadFromFile(c)
38-
}
39-
return c, err
40-
}
41-
42-
func (f *Factory) loadFromFile(c *Configuration) error {
43-
jsonBytes, readError := f.readConfigFile(c.path)
44-
if readError != nil {
45-
return readError
46-
}
47-
configurationJson, decodeErr := f.decodeConfigJson(jsonBytes)
48-
if decodeErr != nil {
49-
return fmt.Errorf("unable to parse json: %s %s", c.path, decodeErr.Error())
50-
}
51-
c.overwriteSettings(configurationJson.Settings)
52-
53-
if configurationJson.Hooks == nil {
54-
return errors.New("no hooks config found")
55-
}
56-
includeErr := f.appendIncludedConfiguration(c)
57-
if includeErr != nil {
58-
return includeErr
59-
}
60-
61-
for hookName, hookConfigJson := range *configurationJson.Hooks {
62-
hookConfig := c.HookConfig(hookName)
63-
hookConfig.isEnabled = true
64-
for _, actionJson := range hookConfigJson.Actions {
65-
if !f.isValidAction(actionJson) {
66-
return fmt.Errorf("invalid action config in %s", hookName)
67-
}
68-
hookConfig.AddAction(CreateActionFromJson(actionJson))
69-
}
70-
}
71-
return nil
72-
}
73-
74-
func (f *Factory) loadSettingsFile(c *Configuration) error {
75-
directory := filepath.Dir(c.path)
76-
filePath := directory + "/captainhook.config.json"
77-
78-
// no local config file to load just exit
79-
if !io.FileExists(filePath) {
80-
return nil
81-
}
82-
83-
jsonBytes, readError := f.readConfigFile(filePath)
84-
if readError != nil {
85-
return readError
86-
}
87-
appSettingJson, decodeErr := f.decodeSettingJson(jsonBytes)
88-
if decodeErr != nil {
89-
return fmt.Errorf("unable to parse json: %s %s", filePath, decodeErr.Error())
90-
}
91-
// overwrite current settings
92-
c.overwriteSettings(appSettingJson)
93-
return nil
94-
}
95-
96-
func (f *Factory) appendIncludedConfiguration(c *Configuration) error {
97-
f.detectMaxIncludeLevel(c)
98-
if f.includeLevel < f.maxIncludeLevel {
99-
f.includeLevel++
100-
includes, err := f.loadIncludedConfigs(c.Includes(), c.path)
101-
if err != nil {
102-
return err
103-
}
104-
for _, configToInclude := range includes {
105-
f.mergeHookConfigs(configToInclude, c)
106-
}
107-
f.includeLevel--
108-
}
109-
return nil
110-
}
111-
112-
func (f *Factory) mergeHookConfigs(from, to *Configuration) {
113-
for _, hook := range info.GetValidHooks() {
114-
// This `Enable` is solely to overwrite the main configuration in the special case that the hook
115-
// is not configured at all. In this case the empty config is disabled by default, and adding an
116-
// empty hook config just to enable the included actions feels a bit dull.
117-
// Since the main hook is processed last (if one is configured) the enabled flag will be overwritten
118-
// once again by the main config value. This is to make sure that if somebody disables a hook in its
119-
// main configuration no actions will get executed, even if we have enabled hooks in any include file.
120-
targetHookConfig := to.HookConfig(hook)
121-
targetHookConfig.Enable()
122-
f.copyActionsFromTo(from.HookConfig(hook), targetHookConfig)
123-
}
124-
}
125-
126-
func (f *Factory) readConfigFile(path string) ([]byte, error) {
13+
func readConfigFile(path string) ([]byte, error) {
12714
fileInfo, err := os.Stat(path)
12815
if err != nil {
12916
return nil, err
@@ -138,76 +25,8 @@ func (f *Factory) readConfigFile(path string) ([]byte, error) {
13825
return jsonData, nil
13926
}
14027

141-
func (f *Factory) decodeConfigJson(jsonInBytes []byte) (JsonConfiguration, error) {
142-
var jConfig JsonConfiguration
143-
if !json.Valid(jsonInBytes) {
144-
return jConfig, fmt.Errorf("json configuration is invalid")
145-
}
146-
marshalError := json.Unmarshal(jsonInBytes, &jConfig)
147-
if marshalError != nil {
148-
return jConfig, fmt.Errorf("could not load json to struct: %s", marshalError.Error())
149-
}
150-
return jConfig, nil
151-
}
152-
153-
func (f *Factory) decodeSettingJson(jsonInBytes []byte) (*JsonAppSettings, error) {
154-
var jSettings JsonAppSettings
155-
if !json.Valid(jsonInBytes) {
156-
return nil, fmt.Errorf("json configuration is invalid")
157-
}
158-
marshalError := json.Unmarshal(jsonInBytes, &jSettings)
159-
if marshalError != nil {
160-
return nil, fmt.Errorf("could not load json to struct: %s", marshalError.Error())
161-
}
162-
return &jSettings, nil
163-
}
164-
165-
func (f *Factory) detectMaxIncludeLevel(c *Configuration) {
166-
// read the include-level setting only for the actual configuration not any included ones
167-
if f.includeLevel == 0 {
168-
f.maxIncludeLevel = c.MaxIncludeLevel()
169-
}
170-
}
171-
172-
func (f *Factory) loadIncludedConfigs(includes []string, path string) ([]*Configuration, error) {
173-
var configs []*Configuration
174-
directory := filepath.Dir(path)
175-
176-
for _, file := range includes {
177-
config, err := f.includeConfig(directory + "/" + file)
178-
if err != nil {
179-
return nil, err
180-
}
181-
configs = append(configs, config)
182-
}
183-
return configs, nil
184-
}
185-
186-
func (f *Factory) includeConfig(path string) (*Configuration, error) {
187-
if !io.FileExists(path) {
188-
return nil, fmt.Errorf("config to include not found: %s", path)
189-
}
190-
return f.setupConfig(path)
191-
}
192-
193-
func (f *Factory) copyActionsFromTo(from *Hook, to *Hook) {
28+
func copyActionsFromTo(from *Hook, to *Hook) {
19429
for _, action := range from.GetActions() {
19530
to.AddAction(action)
19631
}
19732
}
198-
199-
func (f *Factory) isValidAction(actionJson *JsonAction) bool {
200-
if len(actionJson.Run) == 0 {
201-
return false
202-
}
203-
for _, condition := range actionJson.Conditions {
204-
if len(condition.Run) == 0 {
205-
return false
206-
}
207-
}
208-
return true
209-
}
210-
211-
func NewFactory() *Factory {
212-
return &Factory{includeLevel: 0}
213-
}

0 commit comments

Comments
 (0)