Skip to content

Commit 9ae1f5c

Browse files
Add yaml marshal models
1 parent b8186cc commit 9ae1f5c

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed

configuration/yaml.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package configuration
2+
3+
type YamlConfiguration struct {
4+
Settings *YamlAppSettings `yaml:"config,omitempty"`
5+
Hooks *map[string]YamlHook `yaml:"hooks"`
6+
}
7+
8+
type YamlAppSettings struct {
9+
AllowFailure *bool `yaml:"allow-failure,omitempty"`
10+
AnsiColors *bool `yaml:"ansi-colors,omitempty"`
11+
Custom *map[string]string `yaml:"custom,omitempty"`
12+
FailOnFirstError *bool `yaml:"fail-on-first-error,omitempty"`
13+
GitDirectory *string `yaml:"git-directory,omitempty"`
14+
Includes *[]string `yaml:"includes,omitempty"`
15+
IncludeLevel *int `yaml:"includes-level,omitempty"`
16+
RunAsync *bool `yaml:"run-async,omitempty"`
17+
RunPath *string `yaml:"run-path,omitempty"`
18+
Verbosity *string `yaml:"verbosity,omitempty"`
19+
}
20+
21+
type YamlHook struct {
22+
Actions []YamlAction `yaml:"actions"`
23+
}
24+
25+
type YamlAction struct {
26+
Run string `yaml:"run"`
27+
Options *map[string]interface{} `yaml:"options,omitempty"`
28+
Settings *YamlActionSettings `yaml:"config,omitempty"`
29+
Conditions []YamlCondition `yaml:"conditions,omitempty"`
30+
}
31+
32+
type YamlActionSettings struct {
33+
Label *string `yaml:"label,omitempty"`
34+
AllowFailure *bool `yaml:"failure-allowed,omitempty"`
35+
RunAsync *bool `yaml:"run-async,omitempty"`
36+
WorkingDir *string `yaml:"working-directory,omitempty"`
37+
}
38+
type YamlCondition struct {
39+
Run string `yaml:"run"`
40+
Options *map[string]interface{} `yaml:"options,omitempty"`
41+
Conditions []YamlCondition `yaml:"conditions,omitempty"`
42+
}
43+
44+
func createActionFromYaml(yaml YamlAction) *Action {
45+
return &Action{
46+
run: yaml.Run,
47+
settings: createActionSettingsFromYaml(yaml.Settings),
48+
conditions: createConditionsFromYaml(yaml.Conditions),
49+
options: createOptionsFromYaml(yaml.Options),
50+
}
51+
}
52+
53+
func createActionSettingsFromYaml(yaml *YamlActionSettings) *ActionSettings {
54+
a := NewDefaultActionSettings()
55+
if yaml == nil {
56+
return a
57+
}
58+
if yaml.AllowFailure != nil {
59+
a.AllowFailure = *yaml.AllowFailure
60+
}
61+
if yaml.WorkingDir != nil {
62+
a.WorkingDir = *yaml.WorkingDir
63+
}
64+
if yaml.Label != nil {
65+
a.Label = *yaml.Label
66+
}
67+
return a
68+
}
69+
70+
func createConditionsFromYaml(yamlConditions []YamlCondition) []*Condition {
71+
var conditions []*Condition
72+
if yamlConditions == nil {
73+
return conditions
74+
}
75+
for _, condition := range yamlConditions {
76+
conditions = append(conditions, createConditionFromYaml(condition))
77+
}
78+
return conditions
79+
}
80+
81+
func createConditionFromYaml(yaml YamlCondition) *Condition {
82+
var c []*Condition
83+
84+
// default value empty options
85+
opts := map[string]interface{}{}
86+
o := NewOptions(opts)
87+
88+
if yaml.Options != nil {
89+
o = createOptionsFromJson(yaml.Options)
90+
}
91+
if yaml.Conditions != nil {
92+
c = createConditionsFromYaml(yaml.Conditions)
93+
}
94+
return NewCondition(yaml.Run, o, c)
95+
}
96+
97+
func createOptionsFromYaml(yamlOptions *map[string]interface{}) *Options {
98+
options := map[string]interface{}{}
99+
100+
if yamlOptions != nil {
101+
options = *yamlOptions
102+
}
103+
return NewOptions(options)
104+
}
105+
106+
func createNullableAppSettingsFromYaml(settings *YamlAppSettings) *NullableAppSettings {
107+
nSettings := NewNullableAppSettings()
108+
nSettings.AllowFailure = settings.AllowFailure
109+
nSettings.AnsiColors = settings.AnsiColors
110+
nSettings.Custom = settings.Custom
111+
nSettings.FailOnFirstError = settings.FailOnFirstError
112+
nSettings.GitDirectory = settings.GitDirectory
113+
nSettings.Includes = settings.Includes
114+
nSettings.IncludeLevel = settings.IncludeLevel
115+
nSettings.RunPath = settings.RunPath
116+
nSettings.RunAsync = settings.RunAsync
117+
nSettings.Verbosity = settings.Verbosity
118+
return nSettings
119+
}

configuration/yamlfactory.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package configuration
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/captainhook-go/captainhook/info"
7+
"github.com/captainhook-go/captainhook/io"
8+
"gopkg.in/yaml.v3"
9+
"log"
10+
"path/filepath"
11+
)
12+
13+
type YamlFactory struct {
14+
includeLevel int
15+
maxIncludeLevel int
16+
}
17+
18+
// CreateConfig creates a default configuration in case the file exists it is loaded
19+
func (f *YamlFactory) CreateConfig(path string, cliSettings *NullableAppSettings) (*Configuration, error) {
20+
c := NewConfiguration(path, io.FileExists(path))
21+
if c.fileExists {
22+
cErr := f.loadFromFile(c)
23+
if cErr != nil {
24+
return c, cErr
25+
}
26+
}
27+
// load the local config "captainhook.config.yml"
28+
sErr := f.loadSettingsFile(c)
29+
if sErr != nil {
30+
return c, sErr
31+
}
32+
// everything provided from the command line should overwrite any loaded configuration
33+
// this works even if there is an error because then you have a default configuration
34+
c.overwriteSettings(cliSettings)
35+
return c, nil
36+
}
37+
38+
// setupConfig creates a new configuration and loads the json file if it exists
39+
func (f *YamlFactory) setupConfig(path string) (*Configuration, error) {
40+
var err error
41+
c := NewConfiguration(path, io.FileExists(path))
42+
if c.fileExists {
43+
err = f.loadFromFile(c)
44+
}
45+
return c, err
46+
}
47+
48+
func (f *YamlFactory) loadFromFile(c *Configuration) error {
49+
yamlBytes, readError := readConfigFile(c.path)
50+
if readError != nil {
51+
return readError
52+
}
53+
configurationYaml, decodeErr := f.decodeConfigYaml(yamlBytes)
54+
if decodeErr != nil {
55+
return fmt.Errorf("unable to parse yaml: %s %s", c.path, decodeErr.Error())
56+
}
57+
c.overwriteSettings(createNullableAppSettingsFromYaml(configurationYaml.Settings))
58+
59+
if configurationYaml.Hooks == nil {
60+
return errors.New("no hooks config found")
61+
}
62+
includeErr := f.appendIncludedConfiguration(c)
63+
if includeErr != nil {
64+
return includeErr
65+
}
66+
67+
for hookName, hookConfigYaml := range *configurationYaml.Hooks {
68+
hookConfig := c.HookConfig(hookName)
69+
hookConfig.isEnabled = true
70+
for _, actionYaml := range hookConfigYaml.Actions {
71+
if !f.isValidAction(actionYaml) {
72+
return fmt.Errorf("invalid action config in %s", hookName)
73+
}
74+
hookConfig.AddAction(createActionFromYaml(actionYaml))
75+
}
76+
}
77+
return nil
78+
}
79+
80+
func (f *YamlFactory) loadSettingsFile(c *Configuration) error {
81+
directory := filepath.Dir(c.path)
82+
filePath := directory + "/captainhook.config.yaml"
83+
84+
// no local config file to load just exit
85+
if !io.FileExists(filePath) {
86+
return nil
87+
}
88+
89+
yamlBytes, readError := readConfigFile(filePath)
90+
if readError != nil {
91+
return readError
92+
}
93+
appSettingsYaml, decodeErr := f.decodeSettingYaml(yamlBytes)
94+
if decodeErr != nil {
95+
return fmt.Errorf("unable to parse json: %s %s", filePath, decodeErr.Error())
96+
}
97+
// overwrite current settings
98+
c.overwriteSettings(createNullableAppSettingsFromYaml(appSettingsYaml))
99+
return nil
100+
}
101+
102+
func (f *YamlFactory) appendIncludedConfiguration(c *Configuration) error {
103+
f.detectMaxIncludeLevel(c)
104+
if f.includeLevel < f.maxIncludeLevel {
105+
f.includeLevel++
106+
includes, err := f.loadIncludedConfigs(c.Includes(), c.path)
107+
if err != nil {
108+
return err
109+
}
110+
for _, configToInclude := range includes {
111+
f.mergeHookConfigs(configToInclude, c)
112+
}
113+
f.includeLevel--
114+
}
115+
return nil
116+
}
117+
118+
func (f *YamlFactory) mergeHookConfigs(from, to *Configuration) {
119+
for _, hook := range info.GetValidHooks() {
120+
// This `Enable` is solely to overwrite the main configuration in the special case that the hook
121+
// is not configured at all. In this case the empty config is disabled by default, and adding an
122+
// empty hook config just to enable the included actions feels a bit dull.
123+
// Since the main hook is processed last (if one is configured) the enabled flag will be overwritten
124+
// once again by the main config value. This is to make sure that if somebody disables a hook in its
125+
// main configuration no actions will get executed, even if we have enabled hooks in any include file.
126+
targetHookConfig := to.HookConfig(hook)
127+
targetHookConfig.Enable()
128+
copyActionsFromTo(from.HookConfig(hook), targetHookConfig)
129+
}
130+
}
131+
132+
func (f *YamlFactory) decodeConfigYaml(yamlInBytes []byte) (YamlConfiguration, error) {
133+
var yConfig YamlConfiguration
134+
if err := yaml.Unmarshal(yamlInBytes, &yConfig); err != nil {
135+
log.Fatalf("could not load yaml to struct: %v", err)
136+
}
137+
return yConfig, nil
138+
}
139+
140+
func (f *YamlFactory) decodeSettingYaml(yamlAsBytes []byte) (*YamlAppSettings, error) {
141+
var settings YamlAppSettings
142+
marshalError := yaml.Unmarshal(yamlAsBytes, &settings)
143+
if marshalError != nil {
144+
return nil, fmt.Errorf("could not load yaml to struct: %s", marshalError.Error())
145+
}
146+
return &settings, nil
147+
}
148+
149+
func (f *YamlFactory) detectMaxIncludeLevel(c *Configuration) {
150+
// read the include-level setting only for the actual configuration not any included ones
151+
if f.includeLevel == 0 {
152+
f.maxIncludeLevel = c.MaxIncludeLevel()
153+
}
154+
}
155+
156+
func (f *YamlFactory) loadIncludedConfigs(includes []string, path string) ([]*Configuration, error) {
157+
var configs []*Configuration
158+
directory := filepath.Dir(path)
159+
160+
for _, file := range includes {
161+
config, err := f.includeConfig(directory + "/" + file)
162+
if err != nil {
163+
return nil, err
164+
}
165+
configs = append(configs, config)
166+
}
167+
return configs, nil
168+
}
169+
170+
func (f *YamlFactory) includeConfig(path string) (*Configuration, error) {
171+
if !io.FileExists(path) {
172+
return nil, fmt.Errorf("config to include not found: %s", path)
173+
}
174+
return f.setupConfig(path)
175+
}
176+
177+
func (f *YamlFactory) isValidAction(action YamlAction) bool {
178+
if len(action.Run) == 0 {
179+
return false
180+
}
181+
for _, condition := range action.Conditions {
182+
if len(condition.Run) == 0 {
183+
return false
184+
}
185+
}
186+
return true
187+
}
188+
189+
func NewYamlFactory() *YamlFactory {
190+
return &YamlFactory{includeLevel: 0}
191+
}

0 commit comments

Comments
 (0)