diff --git a/cmd/builder/command.go b/cmd/builder/command.go index bd0c2cb6..b7c82a42 100644 --- a/cmd/builder/command.go +++ b/cmd/builder/command.go @@ -29,10 +29,16 @@ type Runner interface { LookPath() error } -// Command interface comprehends bot Runner and Builder interfaces +// Parser holds available methods for parse commands +type Parser interface { + Parse(string) error +} + +// Command interface comprehends bot Runner, Builder, Parser interfaces type Command interface { Builder Runner + Parser } // NewCommand Create a new command. @@ -51,7 +57,6 @@ func ParseCommand(line string) (command *DefaultCommand, err error) { } command = &DefaultCommand{parsed[0], parsed[1:]} - return } @@ -94,3 +99,12 @@ func (c *DefaultCommand) Exec(args ...string) (outStr string, err error) { outStr, err = shell.Exec(c.command, finalArgs...) return } + +// Parse calls the ParseCommand function +func (c *DefaultCommand) Parse(line string) (err error) { + if parsed, err := ParseCommand(line); err == nil { + *c = *parsed + } + + return +} diff --git a/cmd/builder/command_test.go b/cmd/builder/command_test.go index f2147bf6..aa12a90b 100644 --- a/cmd/builder/command_test.go +++ b/cmd/builder/command_test.go @@ -204,3 +204,19 @@ func TestInteractiveArgs(t *testing.T) { t.Errorf("Interactive failed; expected output 'x', got '%s'", output) } } + +func TestParse(t *testing.T) { + line := "echo 'xxx'" + + cmd := NewCommand("") + err := cmd.Parse(line) + + if err != nil { + t.Errorf("failed to parse proper command line onto Command; error: %s", err) + return + } + + if len(cmd.args) != 1 || cmd.command != "echo" || cmd.args[0] != "xxx" { + t.Errorf("ParseCommand failed; given %s got %v", line, cmd.String()) + } +} diff --git a/cmd/builder/fake_command.go b/cmd/builder/fake_command.go index af4ed6e7..40c75774 100644 --- a/cmd/builder/fake_command.go +++ b/cmd/builder/fake_command.go @@ -2,14 +2,15 @@ package builder // FakeCommand implements the Command interface and is used for mocking on testing scenarios type FakeCommand struct { - ArgsAppend []string - ArgsInteractive []string - ArgsExec []string - CalledAppendArgs bool - CalledString bool - CalledLookPath bool - CalledInteractive bool - CalledExec bool + ArgsAppend []string + ArgsInteractive []string + ArgsExec []string + CalledAppendArgs bool + CalledString bool + CalledLookPath bool + CalledInteractive bool + CalledExec bool + CalledParseCommand bool MockExecOut string MockError error @@ -51,3 +52,10 @@ func (f *FakeCommand) Exec(args ...string) (outStr string, err error) { err = f.MockError return } + +// Parse call the ParseCommand function +func (f *FakeCommand) Parse(line string) (err error) { + f.CalledParseCommand = true + err = f.MockError + return +} diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 00000000..c2d63215 --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "fmt" + "kool-dev/kool/cmd/builder" + "kool-dev/kool/cmd/presets" + "os" + + "github.com/spf13/cobra" +) + +// KoolCreate holds handlers and functions to implement the preset command logic +type KoolCreate struct { + DefaultKoolService + parser presets.Parser + createCommand builder.Command + KoolPreset +} + +func init() { + var ( + create = NewKoolCreate() + createCmd = NewCreateCommand(create) + ) + + rootCmd.AddCommand(createCmd) +} + +// NewKoolCreate creates a new handler for create logic +func NewKoolCreate() *KoolCreate { + return &KoolCreate{ + *newDefaultKoolService(), + &presets.DefaultParser{}, + &builder.DefaultCommand{}, + *NewKoolPreset(), + } +} + +// Execute runs the create logic with incoming arguments. +func (c *KoolCreate) Execute(originalArgs []string) (err error) { + preset := originalArgs[0] + dir := originalArgs[1] + + c.parser.LoadPresets(presets.GetAll()) + + if !c.parser.Exists(preset) { + err = fmt.Errorf("Unknown preset %s", preset) + return + } + + createCmd, err := c.parser.GetCreateCommand(preset) + + if err != nil { + return + } + + err = c.createCommand.Parse(createCmd) + + if err != nil { + return + } + + err = c.createCommand.Interactive(dir) + + if err != nil { + return + } + + _ = os.Chdir(dir) + + err = c.KoolPreset.Execute([]string{preset}) + + return +} + +// NewCreateCommand initializes new kool create command +func NewCreateCommand(create *KoolCreate) (createCmd *cobra.Command) { + createCmd = &cobra.Command{ + Use: "create [preset] [project]", + Short: "Create a new project using preset", + Args: cobra.ExactArgs(2), + Run: DefaultCommandRunFunction(create), + } + + return +} diff --git a/cmd/create_test.go b/cmd/create_test.go new file mode 100644 index 00000000..068fccbe --- /dev/null +++ b/cmd/create_test.go @@ -0,0 +1,126 @@ +package cmd + +import ( + "bytes" + "kool-dev/kool/cmd/builder" + "kool-dev/kool/cmd/presets" + "kool-dev/kool/cmd/shell" + "testing" +) + +func newFakeKoolCreate() *KoolCreate { + return &KoolCreate{ + *newFakeKoolService(), + &presets.FakeParser{}, + &builder.FakeCommand{}, + *newFakeKoolPreset(), + } +} + +func TestNewKoolCreate(t *testing.T) { + k := NewKoolCreate() + + if _, ok := k.DefaultKoolService.out.(*shell.DefaultOutputWriter); !ok { + t.Errorf("unexpected shell.OutputWriter on default KoolCreate instance") + } + + if _, ok := k.DefaultKoolService.exiter.(*shell.DefaultExiter); !ok { + t.Errorf("unexpected shell.Exiter on default KoolCreate instance") + } + + if _, ok := k.DefaultKoolService.in.(*shell.DefaultInputReader); !ok { + t.Errorf("unexpected shell.InputReader on default KoolCreate instance") + } + + if _, ok := k.createCommand.(*builder.DefaultCommand); !ok { + t.Errorf("unexpected builder.Command on default KoolCreate instance") + } + + if _, ok := k.parser.(*presets.DefaultParser); !ok { + t.Errorf("unexpected presets.Parser on default KoolCreate instance") + } +} + +func TestNewKoolCreateCommand(t *testing.T) { + f := newFakeKoolCreate() + + f.parser.(*presets.FakeParser).MockExists = true + f.KoolPreset.parser.(*presets.FakeParser).MockExists = true + f.parser.(*presets.FakeParser).MockCreateCommand = "kool docker create command" + + cmd := NewCreateCommand(f) + cmd.SetArgs([]string{"laravel", "my-app"}) + + if err := cmd.Execute(); err != nil { + t.Errorf("unexpected error executing create command; error: %v", err) + } + + if !f.parser.(*presets.FakeParser).CalledLoadPresets { + t.Error("did not call parser.LoadPresets") + } + + if !f.parser.(*presets.FakeParser).CalledExists { + t.Error("did not call parser.Exists") + } + + if !f.parser.(*presets.FakeParser).CalledGetCreateCommand { + t.Error("did not call parser.GetCreateCommand") + } + + if !f.createCommand.(*builder.FakeCommand).CalledParseCommand { + t.Error("did not call Parse on KoolCreate.createCommand Command") + } + + if !f.createCommand.(*builder.FakeCommand).CalledInteractive { + t.Error("did not call Interactive on KoolCreate.createCommand Command") + } + + if !f.out.(*shell.FakeOutputWriter).CalledSetWriter { + t.Error("did not call SetWriter") + } +} + +func TestInvalidPresetCreateCommand(t *testing.T) { + f := newFakeKoolCreate() + cmd := NewCreateCommand(f) + + cmd.SetArgs([]string{"invalid", "my-app"}) + + if err := cmd.Execute(); err != nil { + t.Errorf("unexpected error executing preset command; error: %v", err) + } + + if !f.parser.(*presets.FakeParser).CalledLoadPresets { + t.Error("did not call parser.LoadPresets") + } + + if !f.parser.(*presets.FakeParser).CalledExists { + t.Error("did not call parser.Exists") + } + + if !f.out.(*shell.FakeOutputWriter).CalledError { + t.Error("did not call Error") + } + + expected := "Unknown preset invalid" + output := f.out.(*shell.FakeOutputWriter).Err.Error() + + if expected != output { + t.Errorf("expecting error '%s', got '%s'", expected, output) + } + + if !f.exiter.(*shell.FakeExiter).Exited() { + t.Error("did not call Exit") + } +} + +func TestNoArgsNewCreateCommand(t *testing.T) { + f := newFakeKoolCreate() + + cmd := NewCreateCommand(f) + cmd.SetOut(bytes.NewBufferString("")) + + if err := cmd.Execute(); err == nil { + t.Error("expecting no arguments error executing create command") + } +} diff --git a/cmd/preset.go b/cmd/preset.go index 3111294c..47ce1e48 100644 --- a/cmd/preset.go +++ b/cmd/preset.go @@ -40,7 +40,7 @@ func NewKoolPreset() *KoolPreset { return &KoolPreset{ *newDefaultKoolService(), &KoolPresetFlags{false}, - &presets.DefaultParser{Presets: presets.GetAll()}, + &presets.DefaultParser{}, shell.NewTerminalChecker(), shell.NewPromptSelect(), } @@ -67,6 +67,8 @@ func (p *KoolPreset) Execute(args []string) (err error) { preset = args[0] } + p.parser.LoadPresets(presets.GetAll()) + if !p.parser.Exists(preset) { err = fmt.Errorf("Unknown preset %s", preset) return diff --git a/cmd/presets/fake_parser.go b/cmd/presets/fake_parser.go index 2064c219..0cca39d3 100644 --- a/cmd/presets/fake_parser.go +++ b/cmd/presets/fake_parser.go @@ -2,14 +2,16 @@ package presets // FakeParser implements all fake behaviors for using parser in tests. type FakeParser struct { - CalledExists, CalledLookUpFiles, CalledWriteFiles, CalledGetPresets, CalledGetLanguages bool + CalledExists, CalledLookUpFiles, CalledWriteFiles, CalledGetCreateCommand, CalledGetPresets, CalledGetLanguages, CalledLoadPresets bool - MockExists bool - MockFoundFiles []string - MockFileError string - MockError error - MockLanguages []string - MockPresets []string + MockExists bool + MockFoundFiles []string + MockFileError string + MockError error + MockCreateCommand string + MockLanguages []string + MockPresets []string + MockAllPresets map[string]map[string]string } // Exists check if preset exists @@ -19,6 +21,13 @@ func (f *FakeParser) Exists(preset string) (exists bool) { return } +// GetCreateCommand gets the command to create a new project +func (f *FakeParser) GetCreateCommand(preset string) (cmd string, err error) { + f.CalledGetCreateCommand = true + cmd = f.MockCreateCommand + return +} + // GetLanguages get all presets languages func (f *FakeParser) GetLanguages() (languages []string) { f.CalledGetLanguages = true @@ -47,3 +56,9 @@ func (f *FakeParser) WriteFiles(preset string) (fileError string, err error) { err = f.MockError return } + +//LoadPresets loads all presets +func (f *FakeParser) LoadPresets(presets map[string]map[string]string) { + f.CalledLoadPresets = true + f.MockAllPresets = presets +} diff --git a/cmd/presets/fake_parser_test.go b/cmd/presets/fake_parser_test.go index 505065f6..d6f29104 100644 --- a/cmd/presets/fake_parser_test.go +++ b/cmd/presets/fake_parser_test.go @@ -43,4 +43,11 @@ func TestFakeParser(t *testing.T) { if !f.CalledGetLanguages || len(languages) != 1 || languages[0] != "php" { t.Error("failed to use mocked GetPresets function on FakeParser") } + + f.MockCreateCommand = "create" + createCommand, _ := f.GetCreateCommand("") + + if !f.CalledGetCreateCommand || createCommand == "" || createCommand != "create" { + t.Error("failed to use mocked GetCreateCommand function on FakeParser") + } } diff --git a/cmd/presets/parser.go b/cmd/presets/parser.go index 4eb7ad03..b4338ce4 100644 --- a/cmd/presets/parser.go +++ b/cmd/presets/parser.go @@ -17,10 +17,12 @@ type DefaultParser struct { // Parser holds presets parsing logic type Parser interface { Exists(string) bool + GetCreateCommand(string) (string, error) GetLanguages() []string GetPresets(string) []string LookUpFiles(string) []string WriteFiles(string) (string, error) + LoadPresets(map[string]map[string]string) } // Exists check if preset exists @@ -29,6 +31,20 @@ func (p *DefaultParser) Exists(preset string) (exists bool) { return } +// ErrCreateCommandtNotFoundOrEmpty error throwed when did not find the preset create command or it's empty +var ErrCreateCommandtNotFoundOrEmpty = errors.New("create command not found or empty") + +// GetCreateCommand gets the command to create a new project +func (p *DefaultParser) GetCreateCommand(preset string) (cmd string, err error) { + cmd = p.Presets[preset]["preset_create"] + + if cmd == "" { + err = ErrCreateCommandtNotFoundOrEmpty + } + + return +} + // GetLanguages get all presets languages func (p *DefaultParser) GetLanguages() (languages []string) { if len(p.Presets) == 0 { @@ -123,3 +139,8 @@ func (p *DefaultParser) WriteFiles(preset string) (fileError string, err error) return } + +// LoadPresets loads the presets +func (p *DefaultParser) LoadPresets(allPresets map[string]map[string]string) { + p.Presets = allPresets +} diff --git a/cmd/presets/parser_test.go b/cmd/presets/parser_test.go index 3ad7257d..4efacb29 100644 --- a/cmd/presets/parser_test.go +++ b/cmd/presets/parser_test.go @@ -2,6 +2,7 @@ package presets import ( "os" + "reflect" "testing" ) @@ -12,9 +13,8 @@ func TestExistsParser(t *testing.T) { laravelPreset["kool.yml"] = "" presets["laravel"] = laravelPreset - p := &DefaultParser{ - Presets: presets, - } + p := &DefaultParser{} + p.LoadPresets(presets) exists := p.Exists("laravel") @@ -40,9 +40,8 @@ func TestGetAllParser(t *testing.T) { presets["laravel"] = laravelPreset presets["symfony"] = symfonyPreset - p := &DefaultParser{ - Presets: presets, - } + p := &DefaultParser{} + p.LoadPresets(presets) allPresets := p.GetPresets("") @@ -65,9 +64,8 @@ func TestGetLanguagesParser(t *testing.T) { presets["laravel"] = laravelPreset presets["symfony"] = symfonyPreset - p := &DefaultParser{ - Presets: presets, - } + p := &DefaultParser{} + p.LoadPresets(presets) allLanguages := p.GetLanguages() @@ -90,9 +88,8 @@ func TestGetPresetByLanguageParser(t *testing.T) { presets["php_language"] = phpPreset presets["javascript_language"] = jsPreset - p := &DefaultParser{ - Presets: presets, - } + p := &DefaultParser{} + p.LoadPresets(presets) phpPresets := p.GetPresets("php") @@ -101,6 +98,46 @@ func TestGetPresetByLanguageParser(t *testing.T) { } } +func TestGetCreateCommandParser(t *testing.T) { + presets := make(map[string]map[string]string) + + laravelPreset := make(map[string]string) + + laravelPreset["preset_create"] = "command" + laravelPreset["kool.yml"] = "" + + presets["laravel"] = laravelPreset + + p := &DefaultParser{} + p.LoadPresets(presets) + + laravelCmd, _ := p.GetCreateCommand("laravel") + + if laravelCmd != laravelPreset["preset_create"] { + t.Error("failed to get command") + } +} + +func TestFailGetCreateCommandParser(t *testing.T) { + presets := make(map[string]map[string]string) + + laravelPreset := make(map[string]string) + + laravelPreset["preset_create"] = "" + laravelPreset["kool.yml"] = "" + + presets["laravel"] = laravelPreset + + p := &DefaultParser{} + p.LoadPresets(presets) + + _, err := p.GetCreateCommand("laravel") + + if err != ErrCreateCommandtNotFoundOrEmpty { + t.Errorf("failed, expected to get error %v got %v", ErrCreateCommandtNotFoundOrEmpty, err.Error()) + } +} + func TestIgnorePresetMetaKeysParser(t *testing.T) { originalStat := osStat @@ -119,9 +156,8 @@ func TestIgnorePresetMetaKeysParser(t *testing.T) { presets["preset"] = testingPreset - p := &DefaultParser{ - Presets: presets, - } + p := &DefaultParser{} + p.LoadPresets(presets) foundFiles := p.LookUpFiles("preset") @@ -129,3 +165,16 @@ func TestIgnorePresetMetaKeysParser(t *testing.T) { t.Errorf("expecting to find only 'kool.yml', found %v", foundFiles) } } + +func TestLoadPresetsParser(t *testing.T) { + presets := map[string]map[string]string{ + "laravel": {"preset_create": "command"}, + } + + p := &DefaultParser{} + p.LoadPresets(presets) + + if ok := reflect.DeepEqual(p.Presets, presets); !ok { + t.Error("did not load the presets correctly") + } +} diff --git a/cmd/presets/presets.go b/cmd/presets/presets.go index 09f3cc06..858d36b9 100644 --- a/cmd/presets/presets.go +++ b/cmd/presets/presets.go @@ -9,6 +9,7 @@ func GetAll() map[string]map[string]string { ".dockerignore": `/node_modules `, "preset_language": "javascript", + "preset_create": "kool docker kooldev/node:14-adonis adonis new", "Dockerfile.build": `FROM kooldev/node:14-adonis AS build COPY . /app @@ -104,6 +105,7 @@ networks: /vendor `, "preset_language": "php", + "preset_create": "kool docker kooldev/php:7.4 composer create-project --prefer-dist laravel/laravel", "Dockerfile.build": `FROM kooldev/php:7.4 AS composer COPY . /app @@ -193,6 +195,7 @@ networks: ".dockerignore": `/node_modules `, "preset_language": "javascript", + "preset_create": "kool docker kooldev/node:14-nest nest new", "Dockerfile.build": `FROM kooldev/node:14-nest AS build COPY . /app @@ -286,6 +289,7 @@ networks: /node_modules `, "preset_language": "javascript", + "preset_create": "kool docker kooldev/node:14 yarn create next-app", "Dockerfile.build": `FROM kooldev/node:14 AS build COPY . /app @@ -339,6 +343,7 @@ networks: /node_modules `, "preset_language": "javascript", + "preset_create": "kool docker kooldev/node:14 yarn create next-app", "Dockerfile.build": `FROM kooldev/node:14 AS node COPY . /app @@ -389,6 +394,7 @@ networks: /node_modules `, "preset_language": "javascript", + "preset_create": "kool docker kooldev/node:14 yarn create nuxt-app", "Dockerfile.build": `FROM kooldev/node:14 AS build COPY . /app @@ -447,6 +453,7 @@ networks: /node_modules `, "preset_language": "javascript", + "preset_create": "kool docker kooldev/node:14 yarn create nuxt-app", "Dockerfile.build": `FROM kooldev/node:14 AS node COPY . /app @@ -502,6 +509,7 @@ networks: /vendor `, "preset_language": "php", + "preset_create": "kool docker kooldev/php:7.4 composer create-project --prefer-dist symfony/website-skeleton", "Dockerfile.build": `FROM kooldev/php:7.4 AS composer COPY . /app diff --git a/docs/4-Commands/0-kool.md b/docs/4-Commands/0-kool.md index f7ea17ae..2ba110d2 100644 --- a/docs/4-Commands/0-kool.md +++ b/docs/4-Commands/0-kool.md @@ -16,6 +16,8 @@ Complete documentation is available at https://kool.dev/docs ``` ### SEE ALSO + +* [kool create](kool-create.md) - Create a new project using preset * [kool docker](kool-docker.md) - Creates a new container and runs the command in it. * [kool exec](kool-exec.md) - Execute a command within a running service container * [kool info](kool-info.md) - Prints out information about kool setup (like environment variables) diff --git a/docs/4-Commands/kool-create.md b/docs/4-Commands/kool-create.md new file mode 100644 index 00000000..6bcb335d --- /dev/null +++ b/docs/4-Commands/kool-create.md @@ -0,0 +1,24 @@ +## kool create + +Create a new project using preset + +``` +kool create [preset] [project] [flags] +``` + +### Options + +``` + -h, --help help for create +``` + +### Options inherited from parent commands + +``` + --verbose increases output verbosity +``` + +### SEE ALSO + +* [kool](kool.md) - kool - Kool stuff + diff --git a/presets/adonis/.preset b/presets/adonis/.preset index 708b460a..b7546900 100644 --- a/presets/adonis/.preset +++ b/presets/adonis/.preset @@ -1 +1,2 @@ language=javascript +create=kool docker kooldev/node:14-adonis adonis new diff --git a/presets/laravel/.preset b/presets/laravel/.preset index 361165aa..4bedead0 100644 --- a/presets/laravel/.preset +++ b/presets/laravel/.preset @@ -1 +1,2 @@ language=php +create=kool docker kooldev/php:7.4 composer create-project --prefer-dist laravel/laravel diff --git a/presets/nestjs/.preset b/presets/nestjs/.preset index 708b460a..5f7c252e 100644 --- a/presets/nestjs/.preset +++ b/presets/nestjs/.preset @@ -1 +1,2 @@ language=javascript +create=kool docker kooldev/node:14-nest nest new diff --git a/presets/nextjs-static/.preset b/presets/nextjs-static/.preset index 708b460a..f527916d 100644 --- a/presets/nextjs-static/.preset +++ b/presets/nextjs-static/.preset @@ -1 +1,2 @@ language=javascript +create=kool docker kooldev/node:14 yarn create next-app diff --git a/presets/nextjs/.preset b/presets/nextjs/.preset index 708b460a..f527916d 100644 --- a/presets/nextjs/.preset +++ b/presets/nextjs/.preset @@ -1 +1,2 @@ language=javascript +create=kool docker kooldev/node:14 yarn create next-app diff --git a/presets/nuxtjs-static/.preset b/presets/nuxtjs-static/.preset index 708b460a..9addafdd 100644 --- a/presets/nuxtjs-static/.preset +++ b/presets/nuxtjs-static/.preset @@ -1 +1,2 @@ language=javascript +create=kool docker kooldev/node:14 yarn create nuxt-app diff --git a/presets/nuxtjs/.preset b/presets/nuxtjs/.preset index 708b460a..9addafdd 100644 --- a/presets/nuxtjs/.preset +++ b/presets/nuxtjs/.preset @@ -1 +1,2 @@ language=javascript +create=kool docker kooldev/node:14 yarn create nuxt-app diff --git a/presets/symfony/.preset b/presets/symfony/.preset index 361165aa..e6e6fdba 100644 --- a/presets/symfony/.preset +++ b/presets/symfony/.preset @@ -1 +1,2 @@ language=php +create=kool docker kooldev/php:7.4 composer create-project --prefer-dist symfony/website-skeleton