diff --git a/cmd/create.go b/cmd/create.go index fc2a265..d1f80a0 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -1,4 +1,19 @@ package cmd +/* +Copyright © 2021 Edwin Vautier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ import ( "os" diff --git a/cmd/install.go b/cmd/install.go index df01f49..37cdf19 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -1,6 +1,6 @@ package cmd /* -Copyright © 2021 NAME HERE +Copyright © 2021 Edwin Vautier Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/cmd/make.go b/cmd/make.go new file mode 100644 index 0000000..7bb2c7c --- /dev/null +++ b/cmd/make.go @@ -0,0 +1,46 @@ +package cmd + +/* +Copyright © 2021 Edwin Vautier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import ( + "github.com/edwinvautier/go-cli/services/makeCommand" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// makeCmd represents the install command +var makeCmd = &cobra.Command{ + Use: "make", + Short: "make is used to create new files, for example for models", + Long: `make is used to create new files, for example for models, it creates your model file after prompting you for fields`, + Run: func(cmd *cobra.Command, args []string) { + if !isAMakeCommand(args[0]) { + log.Fatal(args[0], " is not a make command!") + } + if err := makeCommand.MakeEntity(args[1]); err != nil { + log.Fatal(err) + } + }, +} + +func init() { + rootCmd.AddCommand(makeCmd) +} + +func isAMakeCommand(commandName string) bool { + return commandName == "entity" +} diff --git a/config/create_command.go b/config/create_command.go index 974fd16..880ffe1 100644 --- a/config/create_command.go +++ b/config/create_command.go @@ -18,7 +18,7 @@ func InitCreateCmdConfig(config *CreateCmdConfig) { config.DBMS = getDBMS() config.UseDocker = chooseToUseDocker() config.GoPackageFullPath = "github.com/" + strings.TrimSuffix(config.GitUserName, "\n") + "/" + config.AppName - config.Box = packr.New("My Box", "../templates") + config.Box = packr.New("My Box", "../templates/newProject") config.AuthModule = viper.GetBool("auth-module") } diff --git a/config/make_command.go b/config/make_command.go new file mode 100644 index 0000000..e79191a --- /dev/null +++ b/config/make_command.go @@ -0,0 +1,62 @@ +package config + +import ( + "os" + + "github.com/edwinvautier/go-cli/prompt/entity" + "github.com/edwinvautier/go-cli/helpers" + "github.com/gobuffalo/packr/v2" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +// InitMakeCmdConfig creates the needed config for the create command by prompting user and doing other actions +func InitMakeCmdConfig(config *MakeCmdConfig) error { + workdir, err := os.Getwd() + if err != nil { + return err + } + + viper.AddConfigPath(workdir) + viper.SetConfigName(".go-cli-config") + if err := viper.ReadInConfig(); err != nil { + return err + } + + config.GoPackageFullPath = viper.GetString("package") + config.Box = packr.New("makeEntityBox", "../templates/makeEntity") + config.Entity.NameLowerCase = helpers.LowerCase(config.Entity.Name) + config.Entity.NamePascalCase = helpers.PascalCase(config.Entity.Name) + return entity.PromptUserForEntityFields(&config.Entity) +} + +// AddModelToConfig set the new bundle to true in config after install +func AddModelToConfig(newEntity entity.NewEntity) error { + workdir, err := os.Getwd() + if err != nil { + return err + } + + viper.AddConfigPath(workdir) + viper.SetConfigName(".go-cli-config") + viper.SetDefault("models", map[string]map[string]string{}) + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err != nil { + return err + } + + models := viper.GetStringMap("models") + models[newEntity.Name] = newEntity + viper.Set("models", models) + log.Info("Using config file : ", viper.ConfigFileUsed()) + viper.WriteConfig() + + return nil +} + +// MakeCmdConfig is the struct used to configure make command +type MakeCmdConfig struct { + GoPackageFullPath string + Box *packr.Box + Entity entity.NewEntity +} diff --git a/helpers/strings.go b/helpers/strings.go index 7d5adcf..03d8c27 100644 --- a/helpers/strings.go +++ b/helpers/strings.go @@ -2,6 +2,7 @@ package helpers import ( "strings" + "unicode" ) // JoinString takes a pointer to a string and modify this string in order to remove spaces @@ -11,16 +12,20 @@ func JoinString(str string) string { } // GetFilePartsFromName tries to get path and name from the file name, also try to find desired extension -func GetFilePartsFromName(name string) FileParts { +func GetFilePartsFromName(name string, outputName string) FileParts { var fileParts FileParts slices := strings.Split(name, "/") - fileParts.Name = slices[len(slices)-1] fileParts.Path = strings.Join(slices[:len(slices)-1], "/") + "/" - + fileParts.Name = slices[len(slices)-1] slices = strings.Split(fileParts.Name, ".") - fileParts.OutputName = strings.Join(slices[:len(slices)-1], ".") - + + if outputName == "" { + fileParts.OutputName = strings.Join(slices[:len(slices)-1], ".") + } else { + fileParts.OutputName = outputName + } + return fileParts } @@ -30,3 +35,20 @@ type FileParts struct { Path string OutputName string } + +// UpperCaseFirstChar returns the input string with first letter capitalized +func UpperCaseFirstChar(word string) string { + a := []rune(word) + a[0] = unicode.ToUpper(a[0]) + return string(a) +} + +// LowerCase returns input string lowercased +func LowerCase(name string) string { + return strings.ToLower(name) +} + +// PascalCase returns the string with uppercased first char +func PascalCase(name string) string { + return UpperCaseFirstChar(name) +} \ No newline at end of file diff --git a/prompt/entity/entity.go b/prompt/entity/entity.go new file mode 100644 index 0000000..5ac0e59 --- /dev/null +++ b/prompt/entity/entity.go @@ -0,0 +1,135 @@ +package entity + +import ( + "io/ioutil" + "os" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/edwinvautier/go-cli/helpers" + log "github.com/sirupsen/logrus" +) + + +// EntityField represents a single field from an entity +type EntityField struct { + Type string + Name string + IsSlice bool + SliceType string +} + +// NewEntity represents the full entity that user wants to create +type NewEntity struct { + Name string + NamePascalCase string + NameLowerCase string + HasDate bool + HasCustomTypes bool + Fields []EntityField +} + +// PromptUserForEntityFields prompts user in the CLI to choose entity fields wanted +func PromptUserForEntityFields(entity *NewEntity) error{ + for { + fieldName := "" + if err := promptForFieldName(&fieldName); err != nil { + return err + } + + // If field name is empty then stop the function + if fieldName == "" { + break + } + + var fieldType string + if err := promptForFieldType(&fieldType); err != nil { + return err + } + + field := EntityField{ + Name: helpers.UpperCaseFirstChar(fieldName), + Type: fieldType, + IsSlice: false, + } + + if field.Type == "date" { + entity.HasDate = true + } else if field.Type == "slice" { + field.IsSlice = true + + sliceTypePrompt := &survey.Select{ + Message: "Slice type :", + Options: GetTypeOptions(), + } + survey.AskOne(sliceTypePrompt, &field.SliceType) + + if choosedCustomType(field.SliceType) { + entity.HasCustomTypes = true + } + } + + if choosedCustomType(field.Type) { + entity.HasCustomTypes = true + } + + entity.Fields = append(entity.Fields, field) + } + + return nil +} + +// promptForFieldName asks the user for a string used as the app name +func promptForFieldName(fieldName *string) error { + prompt := &survey.Input{ + Message: "Choose new field name (Press enter to stop adding fields)", + } + return survey.AskOne(prompt, fieldName) +} + +func promptForFieldType(fieldType *string) error { + typePrompt := &survey.Select{ + Message: "Choose type :", + Options: GetTypeOptions(), + } + return survey.AskOne(typePrompt, fieldType) +} + +// GetTypeOptions returns a list of strings for user prompt of data types when creating new models +func GetTypeOptions() []string { + entitiesList := GetEntitiesList() + options := []string{"string", "boolean", "int", "uint", "float32", "float64", "date", "slice"} + options = append(options, entitiesList...) + + return options +} + +// GetEntitiesList returns a slice of strings with all the entities names found in the models/ dir +func GetEntitiesList() []string { + workdir, err := os.Getwd() + if err != nil { + log.Error(err) + } + files, err := ioutil.ReadDir(workdir + "/api/models") + if err != nil { + log.Fatal(err) + } + entities := make([]string, 0) + for _, file := range files { + name := helpers.UpperCaseFirstChar(strings.Split(file.Name(), ".go")[0]) + entities = append(entities, name) + } + + return entities +} + +func choosedCustomType(cType string) bool{ + entitiesList := GetEntitiesList() + for _, entityName := range entitiesList { + if entityName == cType { + return true + } + } + + return false +} \ No newline at end of file diff --git a/services/createCommand/generate_structure.go b/services/createCommand/generate_structure.go index 338654f..d7907b2 100644 --- a/services/createCommand/generate_structure.go +++ b/services/createCommand/generate_structure.go @@ -11,7 +11,7 @@ func generateTemplates(config config.CreateCmdConfig) error { config.Box.Walk(func(path string, f packd.File) error { fInfo, _ := f.FileInfo() - fileParts := helpers.GetFilePartsFromName(fInfo.Name()) + fileParts := helpers.GetFilePartsFromName(fInfo.Name(), "") GenerateFile(fileParts.Path, fileParts.Name, fileParts.OutputName, config) return nil }) diff --git a/services/installCommand/template.go b/services/installCommand/template.go index 2cdd6d7..6067c2a 100644 --- a/services/installCommand/template.go +++ b/services/installCommand/template.go @@ -16,7 +16,7 @@ func executeTemplates(name string, installCmdConfig config.InstallCmdConfig) err box.Walk(func(path string, f packd.File) error { fInfo, _ := f.FileInfo() - fileParts := helpers.GetFilePartsFromName(fInfo.Name()) + fileParts := helpers.GetFilePartsFromName(fInfo.Name(), "") generateFile(fileParts.Path, fileParts.Name, fileParts.OutputName, installCmdConfig, box) return nil }) diff --git a/services/makeCommand/make.go b/services/makeCommand/make.go new file mode 100644 index 0000000..a8180fb --- /dev/null +++ b/services/makeCommand/make.go @@ -0,0 +1,18 @@ +package makeCommand + +import "github.com/edwinvautier/go-cli/config" + +// MakeEntity creates the config and execute templates in order to create a new entity +func MakeEntity(entityName string) error { + var makeCmdConfig config.MakeCmdConfig + makeCmdConfig.Entity.Name = entityName + if err := config.InitMakeCmdConfig(&makeCmdConfig); err != nil { + return err + } + + if err := executeTemplates(makeCmdConfig); err != nil { + return err + } + + return config.AddModelToConfig(makeCmdConfig.Entity) +} \ No newline at end of file diff --git a/services/makeCommand/template.go b/services/makeCommand/template.go new file mode 100644 index 0000000..8e14c33 --- /dev/null +++ b/services/makeCommand/template.go @@ -0,0 +1,61 @@ +package makeCommand + +import ( + "os" + + "text/template" + + "github.com/edwinvautier/go-cli/config" + "github.com/edwinvautier/go-cli/helpers" + "github.com/gobuffalo/packd" + log "github.com/sirupsen/logrus" +) + +func executeTemplates(makeCmdConfig config.MakeCmdConfig) error { + + makeCmdConfig.Box.Walk(func(path string, f packd.File) error { + fInfo, _ := f.FileInfo() + fileParts := helpers.GetFilePartsFromName(fInfo.Name(), makeCmdConfig.Entity.NameLowerCase + ".go") + generateFile(fileParts.Path, fileParts.Name, fileParts.OutputName, makeCmdConfig) + return nil + }) + + return nil +} + +func generateFile(path string, name string, outputName string, makeCmdConfig config.MakeCmdConfig) { + // Get template content as string + templateString, err := makeCmdConfig.Box.FindString(path + name) + if err != nil { + log.Error(err) + return + } + workdir, err := os.Getwd() + if err != nil { + log.Error(err) + } + // Create the directory if not exist + if _, err := os.Stat(workdir + "/" + path); os.IsNotExist(err) { + os.MkdirAll(workdir+"/"+path, os.ModePerm) + } + + err = executeTemplate(makeCmdConfig, outputName, workdir+"/"+path, templateString) + if err != nil { + log.Error(err) + return + } +} + +func executeTemplate(makeCmdConfig config.MakeCmdConfig, outputName string, path string, templateString string) error { + // Create the file + file, err := os.Create(path + outputName) + if err != nil { + log.Error(err) + return err + } + // Execute template and write file + parsedTemplate := template.Must(template.New("template").Parse(templateString)) + err = parsedTemplate.Execute(file, makeCmdConfig) + + return nil +} diff --git a/templates/makeEntity/api/models/models_template.go.template b/templates/makeEntity/api/models/models_template.go.template new file mode 100644 index 0000000..cf89dd5 --- /dev/null +++ b/templates/makeEntity/api/models/models_template.go.template @@ -0,0 +1,32 @@ +package models + +import ( + "github.com/asaskevich/govalidator"{{if .Entity.HasDate}} + "time"{{end}} +) + +// {{.Entity.NamePascalCase}} is our struct for users +type {{.Entity.NamePascalCase}} struct { + ID uint64 `gorm:"primary_key"`{{range .Entity.Fields}} + {{.Name}} {{if (eq .Type "date")}}time.Time{{else}}{{if eq .Type "slice"}}[]{{.SliceType}}{{else}}{{.Type}}{{end}}{{end}}{{end}} +} + +// {{.Entity.NamePascalCase}}Form is our struct to handle new users requests +type {{.Entity.NamePascalCase}}Form struct { {{range .Entity.Fields}} + {{.Name}} {{if (eq .Type "date")}}time.Time{{else}}{{if eq .Type "slice"}}[]{{.SliceType}}{{else}}{{.Type}}{{end}}{{end}}{{end}} +} + +// {{.Entity.NamePascalCase}}JSON is the struct to return {{.Entity.NameLowerCase}} in json +type {{.Entity.NamePascalCase}}JSON struct { + ID uint64{{range .Entity.Fields}} + {{.Name}} {{if (eq .Type "date")}}time.Time{{else}}{{if eq .Type "slice"}}[]{{.SliceType}}{{else}}{{.Type}}{{end}}{{end}}{{end}} +} + +// Validate{{.Entity.NamePascalCase}} takes a {{.Entity.NameLowerCase}} form as parameter and check if its properties are valid +func Validate{{.Entity.NamePascalCase}}({{.Entity.NameLowerCase}} *{{.Entity.NamePascalCase}}Form) error { + _, err := govalidator.ValidateStruct({{.Entity.NameLowerCase}}) + + return err +} + + diff --git a/templates/makeEntity/api/repositories/repositories_template.go.template b/templates/makeEntity/api/repositories/repositories_template.go.template new file mode 100644 index 0000000..63ce9d8 --- /dev/null +++ b/templates/makeEntity/api/repositories/repositories_template.go.template @@ -0,0 +1,66 @@ +package repositories + +import ( + "{{.GoPackageFullPath}}/shared/database" + "{{.GoPackageFullPath}}/api/models" + "errors" + "github.com/jinzhu/gorm" +) + +func Create{{.Entity.NamePascalCase}}({{.Entity.NameLowerCase}} *models.{{.Entity.NamePascalCase}}) (*models.{{.Entity.NamePascalCase}}, error) { + var err error + + err = database.Db.Debug().Create({{.Entity.NameLowerCase}}).Error + if err != nil { + return &models.{{.Entity.NamePascalCase}}{}, err + } + + return {{.Entity.NameLowerCase}}, nil +} + +func Edit{{.Entity.NamePascalCase}}ByID({{.Entity.NameLowerCase}} *models.{{.Entity.NamePascalCase}}, id uint64) error { + var err error + var old models.{{.Entity.NamePascalCase}} + err = database.Db.Debug().Where("id = ?", id).First(&old).Error + if gorm.IsRecordNotFoundError(err) { + return errors.New("{{.Entity.NameLowerCase}} Not Found") + } + {{.Entity.NameLowerCase}}.ID = id + + err = database.Db.Debug().Save(&{{.Entity.NameLowerCase}}).Error + if err != nil { + return errors.New("Could'nt update {{.Entity.NameLowerCase}}") + } + + return nil +} + +func Delete{{.Entity.NamePascalCase}}ByID(id uint64) (models.{{.Entity.NamePascalCase}}, error) { + + var err error + var {{.Entity.NameLowerCase}} models.{{.Entity.NamePascalCase}} + + err = database.Db.Debug().Delete(&{{.Entity.NameLowerCase}}, id).Error + if err != nil { + return models.{{.Entity.NamePascalCase}}{}, err + } + if gorm.IsRecordNotFoundError(err) { + return models.{{.Entity.NamePascalCase}}{}, errors.New("{{.Entity.NameLowerCase}} Not Found") + } + + return {{.Entity.NameLowerCase}}, err +} + +func Find{{.Entity.NamePascalCase}}ByID(id uint64) (*models.{{.Entity.NamePascalCase}}, error) { + var err error + var {{.Entity.NameLowerCase}} models.{{.Entity.NamePascalCase}} + err = database.Db.Debug().Model(models.{{.Entity.NamePascalCase}}{}).Where("id = ?", id).Take(&{{.Entity.NameLowerCase}}).Error + if err != nil { + return &models.{{.Entity.NamePascalCase}}{}, err + } + if gorm.IsRecordNotFoundError(err) { + return &models.{{.Entity.NamePascalCase}}{}, errors.New("{{.Entity.NameLowerCase}} Not Found") + } + + return &{{.Entity.NameLowerCase}}, err +} \ No newline at end of file diff --git a/templates/.env.dist.template b/templates/newProject/.env.dist.template similarity index 100% rename from templates/.env.dist.template rename to templates/newProject/.env.dist.template diff --git a/templates/.github/ISSUE_TEMPLATE/bug_report.md.template b/templates/newProject/.github/ISSUE_TEMPLATE/bug_report.md.template similarity index 100% rename from templates/.github/ISSUE_TEMPLATE/bug_report.md.template rename to templates/newProject/.github/ISSUE_TEMPLATE/bug_report.md.template diff --git a/templates/.github/ISSUE_TEMPLATE/feature_request.md.template b/templates/newProject/.github/ISSUE_TEMPLATE/feature_request.md.template similarity index 100% rename from templates/.github/ISSUE_TEMPLATE/feature_request.md.template rename to templates/newProject/.github/ISSUE_TEMPLATE/feature_request.md.template diff --git a/templates/.github/workflows/ci.yml.template b/templates/newProject/.github/workflows/ci.yml.template similarity index 100% rename from templates/.github/workflows/ci.yml.template rename to templates/newProject/.github/workflows/ci.yml.template diff --git a/templates/.github/workflows/codeql-analysis.yml.template b/templates/newProject/.github/workflows/codeql-analysis.yml.template similarity index 100% rename from templates/.github/workflows/codeql-analysis.yml.template rename to templates/newProject/.github/workflows/codeql-analysis.yml.template diff --git a/templates/.gitignore.template b/templates/newProject/.gitignore.template similarity index 100% rename from templates/.gitignore.template rename to templates/newProject/.gitignore.template diff --git a/templates/CODE_OF_CONDUCT.md.template b/templates/newProject/CODE_OF_CONDUCT.md.template similarity index 100% rename from templates/CODE_OF_CONDUCT.md.template rename to templates/newProject/CODE_OF_CONDUCT.md.template diff --git a/templates/COMMIT_CONVENTIONS.md.template b/templates/newProject/COMMIT_CONVENTIONS.md.template similarity index 100% rename from templates/COMMIT_CONVENTIONS.md.template rename to templates/newProject/COMMIT_CONVENTIONS.md.template diff --git a/templates/CONTRIBUTING.md.template b/templates/newProject/CONTRIBUTING.md.template similarity index 100% rename from templates/CONTRIBUTING.md.template rename to templates/newProject/CONTRIBUTING.md.template diff --git a/templates/Makefile.template b/templates/newProject/Makefile.template similarity index 100% rename from templates/Makefile.template rename to templates/newProject/Makefile.template diff --git a/templates/README.md.template b/templates/newProject/README.md.template similarity index 100% rename from templates/README.md.template rename to templates/newProject/README.md.template diff --git a/templates/api/controllers/customers.go.template b/templates/newProject/api/controllers/customers.go.template similarity index 100% rename from templates/api/controllers/customers.go.template rename to templates/newProject/api/controllers/customers.go.template diff --git a/templates/api/models/customers.go.template b/templates/newProject/api/models/customers.go.template similarity index 100% rename from templates/api/models/customers.go.template rename to templates/newProject/api/models/customers.go.template diff --git a/templates/api/repositories/customers.go.template b/templates/newProject/api/repositories/customers.go.template similarity index 100% rename from templates/api/repositories/customers.go.template rename to templates/newProject/api/repositories/customers.go.template diff --git a/templates/api/routes/routes.go.template b/templates/newProject/api/routes/routes.go.template similarity index 100% rename from templates/api/routes/routes.go.template rename to templates/newProject/api/routes/routes.go.template diff --git a/templates/docker-compose.yml.template b/templates/newProject/docker-compose.yml.template similarity index 100% rename from templates/docker-compose.yml.template rename to templates/newProject/docker-compose.yml.template diff --git a/templates/docker/go/Dockerfile.txt b/templates/newProject/docker/go/Dockerfile.txt similarity index 100% rename from templates/docker/go/Dockerfile.txt rename to templates/newProject/docker/go/Dockerfile.txt diff --git a/templates/main.go.template b/templates/newProject/main.go.template similarity index 100% rename from templates/main.go.template rename to templates/newProject/main.go.template diff --git a/templates/shared/database/connector.go.template b/templates/newProject/shared/database/connector.go.template similarity index 100% rename from templates/shared/database/connector.go.template rename to templates/newProject/shared/database/connector.go.template diff --git a/templates/shared/database/migrations.go.template b/templates/newProject/shared/database/migrations.go.template similarity index 100% rename from templates/shared/database/migrations.go.template rename to templates/newProject/shared/database/migrations.go.template diff --git a/templates/shared/env/env.go.template b/templates/newProject/shared/env/env.go.template similarity index 100% rename from templates/shared/env/env.go.template rename to templates/newProject/shared/env/env.go.template diff --git a/templates/shared/helpers/errors.go.template b/templates/newProject/shared/helpers/errors.go.template similarity index 100% rename from templates/shared/helpers/errors.go.template rename to templates/newProject/shared/helpers/errors.go.template diff --git a/templates/shared/services/password_hasher.go.template b/templates/newProject/shared/services/password_hasher.go.template similarity index 100% rename from templates/shared/services/password_hasher.go.template rename to templates/newProject/shared/services/password_hasher.go.template diff --git a/templates/shared/services/token.go.template b/templates/newProject/shared/services/token.go.template similarity index 100% rename from templates/shared/services/token.go.template rename to templates/newProject/shared/services/token.go.template diff --git a/templates/tests/customers_test.go.template b/templates/newProject/tests/customers_test.go.template similarity index 100% rename from templates/tests/customers_test.go.template rename to templates/newProject/tests/customers_test.go.template