diff --git a/command/command.go b/command/command.go index fa86c5e9..7e40f8f4 100644 --- a/command/command.go +++ b/command/command.go @@ -36,60 +36,6 @@ func Exec(conf string) error { return commands.Exec() } -// ParseConfigFile parse the config file to a command list -func ParseConfigFile(path string) (commands *Commands, err error) { - data, err := os.ReadFile(path) - if err != nil { - return - } - var c Config - err = yaml.Unmarshal(data, &c) - if err != nil { - return - } - return ParseConfig(c) -} - -// ParseConfig parse the config to a command list -func ParseConfig(conf Config) (commands *Commands, err error) { - commands = &Commands{ - Name: conf.Name, - } - commands.Init, err = parseCommands(conf.Init) - if err != nil { - return nil, err - } - commands.Actions, err = parseCommands(conf.Actions) - if err != nil { - return nil, err - } - commands.Clear, err = parseCommands(conf.Clear) - if err != nil { - return nil, err - } - return commands, nil -} - -func parseCommands(actions []Action) (commands []Command, err error) { - for _, action := range actions { - var c Command - for name, fn := range allCommands { - if _, ok := action[name]; ok { - c, err = fn(action) - break - } - } - if err != nil { - return nil, err - } - if c == nil { - return nil, errUnsupportedCommand - } - commands = append(commands, c) - } - return commands, nil -} - func parse[T Command](a Action) (c T, err error) { out, err := yaml.Marshal(a) if err != nil { diff --git a/command/command_test.go b/command/command_test.go index ac3766f4..dd76d351 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -49,54 +49,6 @@ func TestExec_ConfigFileNotExist(t *testing.T) { } } -func TestParseConfigFile_ConfigFileNotExist(t *testing.T) { - _, err := ParseConfigFile("./example/notexist.yaml") - if !os.IsNotExist(err) { - t.Errorf("ParseConfigFile expect to get a not exist error, but get %v", err) - } -} - -func TestParseConfigFile_InvalidConfigFile(t *testing.T) { - _, err := ParseConfigFile("./command_test.go") - if err == nil { - t.Errorf("ParseConfigFile expect get an error, but get nil") - } -} - -func TestParseConfig_UnsupportedCommand(t *testing.T) { - testCases := []struct { - name string - conf Config - }{ - {"unsupported command in init", Config{Init: []Action{{"unsupported-command": ""}}}}, - {"unsupported command in actions", Config{Actions: []Action{{"unsupported-command": ""}}}}, - {"unsupported command in clear", Config{Clear: []Action{{"unsupported-command": ""}}}}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := ParseConfig(tc.conf) - if !errors.Is(err, errUnsupportedCommand) { - t.Errorf("ParseConfig expect get error => %v, but get %v", errUnsupportedCommand, err) - } - }) - } -} - -func TestParseConfig_WithIllegalField(t *testing.T) { - conf := Config{ - Name: "invalid command", - } - action := make(Action) - action["cp"] = "" - action["source"] = errMarshaler{} - conf.Actions = append(conf.Actions, action) - _, err := ParseConfig(conf) - if !errors.Is(err, errMarshalYamlMock) { - t.Errorf("ParseConfig expect get error => %v, but get %v", errMarshalYamlMock, err) - } -} - var ( errMarshalYamlMock = errors.New("marshal yaml error mock") diff --git a/command/config.go b/command/config.go index 7493dedd..fd1289bd 100644 --- a/command/config.go +++ b/command/config.go @@ -2,10 +2,13 @@ package command // Config the config structure defined a series of commands type Config struct { - Name string `yaml:"name"` - Init []Action `yaml:"init"` - Actions []Action `yaml:"actions"` - Clear []Action `yaml:"clear"` + Name string `yaml:"name"` + // IncludePath the root directory of the include files, include path is the directory of current config file by default + IncludePath string `yaml:"include_path"` + Include []string `yaml:"include"` + Init []Action `yaml:"init"` + Actions []Action `yaml:"actions"` + Clear []Action `yaml:"clear"` } // Action contain the command action name and some parameters that current command needed diff --git a/command/example/include/actions.yaml b/command/example/include/actions.yaml new file mode 100644 index 00000000..7d2d401a --- /dev/null +++ b/command/example/include/actions.yaml @@ -0,0 +1,13 @@ +name: actions step +include: + - actions_step1.yaml + - actions_step2.yaml +init: + - print: + input: call init from actions.yaml +actions: + - print: + input: call actions from actions.yaml +clear: + - print: + input: call clear from actions.yaml \ No newline at end of file diff --git a/command/example/include/actions_step1.yaml b/command/example/include/actions_step1.yaml new file mode 100644 index 00000000..66042364 --- /dev/null +++ b/command/example/include/actions_step1.yaml @@ -0,0 +1,10 @@ +name: actions step 1 +init: + - print: + input: call init from actions_step1.yaml +actions: + - print: + input: call actions from actions_step1.yaml +clear: + - print: + input: call clear from actions_step1.yaml \ No newline at end of file diff --git a/command/example/include/actions_step2.yaml b/command/example/include/actions_step2.yaml new file mode 100644 index 00000000..28d7d639 --- /dev/null +++ b/command/example/include/actions_step2.yaml @@ -0,0 +1,10 @@ +name: actions step 2 +init: + - print: + input: call init from actions_step2.yaml +actions: + - print: + input: call actions from actions_step2.yaml +clear: + - print: + input: call clear from actions_step2.yaml \ No newline at end of file diff --git a/command/example/include/clear.yaml b/command/example/include/clear.yaml new file mode 100644 index 00000000..54352dd9 --- /dev/null +++ b/command/example/include/clear.yaml @@ -0,0 +1,17 @@ +name: clear step +include: + - clear_step1.yaml + - clear_step2.yaml +init: + - print: + input: call init from clear.yaml +actions: + - print: + input: call actions from clear.yaml +clear: + - print: + input: call clear from clear.yaml + - rm: + source: ./source + - rm: + source: ./dest \ No newline at end of file diff --git a/command/example/include/clear_step1.yaml b/command/example/include/clear_step1.yaml new file mode 100644 index 00000000..b28963c8 --- /dev/null +++ b/command/example/include/clear_step1.yaml @@ -0,0 +1,10 @@ +name: clear step 1 +init: + - print: + input: call init from clear_step1.yaml +actions: + - print: + input: call actions from clear_step1.yaml +clear: + - print: + input: call clear from clear_step1.yaml \ No newline at end of file diff --git a/command/example/include/clear_step2.yaml b/command/example/include/clear_step2.yaml new file mode 100644 index 00000000..1a6da921 --- /dev/null +++ b/command/example/include/clear_step2.yaml @@ -0,0 +1,10 @@ +name: clear step 2 +init: + - print: + input: call init from clear_step2.yaml +actions: + - print: + input: call actions from clear_step2.yaml +clear: + - print: + input: call clear from clear_step2.yaml \ No newline at end of file diff --git a/command/example/include/include.yaml b/command/example/include/include.yaml new file mode 100644 index 00000000..45a0f59c --- /dev/null +++ b/command/example/include/include.yaml @@ -0,0 +1,15 @@ +name: command examples with include +include: + - init_source.yaml + - init_dest.yaml + - actions.yaml + - clear.yaml +init: + - print: + input: call init from include.yaml +actions: + - print: + input: call actions from include.yaml +clear: + - print: + input: call clear from include.yaml \ No newline at end of file diff --git a/command/example/include/infinite_recursion.yaml b/command/example/include/infinite_recursion.yaml new file mode 100644 index 00000000..7df7ae98 --- /dev/null +++ b/command/example/include/infinite_recursion.yaml @@ -0,0 +1,3 @@ +name: command examples of infinite recursion +include: + - infinite_recursion.yaml \ No newline at end of file diff --git a/command/example/include/init_dest.yaml b/command/example/include/init_dest.yaml new file mode 100644 index 00000000..54ba944c --- /dev/null +++ b/command/example/include/init_dest.yaml @@ -0,0 +1,6 @@ +name: init dest path +init: + - print: + input: call init from init_dest.yaml + - mkdir: + source: ./dest \ No newline at end of file diff --git a/command/example/include/init_source.yaml b/command/example/include/init_source.yaml new file mode 100644 index 00000000..f010d62b --- /dev/null +++ b/command/example/include/init_source.yaml @@ -0,0 +1,6 @@ +name: init source path +init: + - print: + input: call init from init_source.yaml + - mkdir: + source: ./source \ No newline at end of file diff --git a/command/parser.go b/command/parser.go new file mode 100644 index 00000000..19241719 --- /dev/null +++ b/command/parser.go @@ -0,0 +1,121 @@ +package command + +import ( + "errors" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +var ( + errInfiniteRecursion = errors.New("find an infinite recursion") +) + +const ( + maxRecursionDepth = 10000 +) + +type parser struct { + c int // check infinite recursion +} + +func newParser() *parser { + return &parser{} +} + +func (p *parser) ParseConfigFile(path string) (commands *Commands, err error) { + data, err := os.ReadFile(path) + if err != nil { + return + } + var c Config + err = yaml.Unmarshal(data, &c) + if err != nil { + return + } + if len(c.IncludePath) == 0 { + c.IncludePath = filepath.Dir(path) + } + return p.ParseConfig(c) +} + +func (p *parser) ParseConfig(conf Config) (commands *Commands, err error) { + commands, err = p.parseConfig(conf) + if err != nil { + return nil, err + } + + var ( + init []Command + actions []Command + clear []Command + ) + for _, f := range conf.Include { + baseCommands, err := p.ParseConfigFile(filepath.Join(conf.IncludePath, f)) + if err != nil { + return nil, err + } + init = append(init, baseCommands.Init...) + actions = append(actions, baseCommands.Actions...) + clear = append(clear, baseCommands.Clear...) + } + + commands.Init = append(init, commands.Init...) + commands.Actions = append(actions, commands.Actions...) + commands.Clear = append(clear, commands.Clear...) + return commands, nil +} + +func (p *parser) parseConfig(conf Config) (commands *Commands, err error) { + p.c++ + if p.c > maxRecursionDepth { + return nil, errInfiniteRecursion + } + commands = &Commands{ + Name: conf.Name, + } + commands.Init, err = p.parseCommands(conf.Init) + if err != nil { + return nil, err + } + commands.Actions, err = p.parseCommands(conf.Actions) + if err != nil { + return nil, err + } + commands.Clear, err = p.parseCommands(conf.Clear) + if err != nil { + return nil, err + } + return commands, nil +} + +func (p *parser) parseCommands(actions []Action) (commands []Command, err error) { + for _, action := range actions { + var c Command + for name, fn := range allCommands { + if _, ok := action[name]; ok { + c, err = fn(action) + break + } + } + if err != nil { + return nil, err + } + if c == nil { + return nil, errUnsupportedCommand + } + commands = append(commands, c) + } + return commands, nil +} + +// ParseConfigFile parse the config file to a command list +func ParseConfigFile(path string) (commands *Commands, err error) { + return newParser().ParseConfigFile(path) +} + +// ParseConfig parse the config to a command list +func ParseConfig(conf Config) (commands *Commands, err error) { + return newParser().ParseConfig(conf) +} diff --git a/command/parser_test.go b/command/parser_test.go new file mode 100644 index 00000000..10effa2d --- /dev/null +++ b/command/parser_test.go @@ -0,0 +1,99 @@ +package command + +import ( + "errors" + "fmt" + "os" + "testing" +) + +func TestParseConfigFile_ConfigFileNotExist(t *testing.T) { + _, err := ParseConfigFile("./example/notexist.yaml") + if !os.IsNotExist(err) { + t.Errorf("ParseConfigFile expect to get a not exist error, but get %v", err) + } +} + +func TestParseConfigFile_InvalidConfigFile(t *testing.T) { + _, err := ParseConfigFile("./command_test.go") + if err == nil { + t.Errorf("ParseConfigFile expect get an error, but get nil") + } +} + +func TestParseConfig_UnsupportedCommand(t *testing.T) { + testCases := []struct { + name string + conf Config + }{ + {"unsupported command in init", Config{Init: []Action{{"unsupported-command": ""}}}}, + {"unsupported command in actions", Config{Actions: []Action{{"unsupported-command": ""}}}}, + {"unsupported command in clear", Config{Clear: []Action{{"unsupported-command": ""}}}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := ParseConfig(tc.conf) + if !errors.Is(err, errUnsupportedCommand) { + t.Errorf("ParseConfig expect get error => %v, but get %v", errUnsupportedCommand, err) + } + }) + } +} + +func TestParseConfig_WithIllegalField(t *testing.T) { + conf := Config{ + Name: "invalid command", + } + action := make(Action) + action["cp"] = "" + action["source"] = errMarshaler{} + conf.Actions = append(conf.Actions, action) + _, err := ParseConfig(conf) + if !errors.Is(err, errMarshalYamlMock) { + t.Errorf("ParseConfig expect get error => %v, but get %v", errMarshalYamlMock, err) + } +} + +func TestParseConfigFile_InfiniteRecursion(t *testing.T) { + _, err := ParseConfigFile("./example/include/infinite_recursion.yaml") + if !errors.Is(err, errInfiniteRecursion) { + t.Errorf("ParseConfigFile expect to get %v, but get %v", errInfiniteRecursion, err) + } +} + +func ExampleParseConfigFile_WithInclude() { + commands, err := ParseConfigFile("./example/include/include.yaml") + if err != nil { + panic(fmt.Sprintf("ParseConfigFile expect to get an nil error, but get %v", err)) + } + err = commands.Exec() + if err != nil { + panic(fmt.Sprintf("Exec expect to get an nil error, but get %v", err)) + } + + // Output: + //call init from init_source.yaml + //call init from init_dest.yaml + //call init from actions_step1.yaml + //call init from actions_step2.yaml + //call init from actions.yaml + //call init from clear_step1.yaml + //call init from clear_step2.yaml + //call init from clear.yaml + //call init from include.yaml + //call actions from actions_step1.yaml + //call actions from actions_step2.yaml + //call actions from actions.yaml + //call actions from clear_step1.yaml + //call actions from clear_step2.yaml + //call actions from clear.yaml + //call actions from include.yaml + //call clear from actions_step1.yaml + //call clear from actions_step2.yaml + //call clear from actions.yaml + //call clear from clear_step1.yaml + //call clear from clear_step2.yaml + //call clear from clear.yaml + //call clear from include.yaml +}