diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 229bdd31..638e7851 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -238,6 +238,13 @@ Please check the following rules when naming projects, stages and functions: return } + var ane *controller.ApiNewError + if errors.As(err, &ane) { + ui.Errorf("function New for api %s does not have proper type", ane.Api) + ui.Info("Function should have no parameters and only one return value of type struct or pointer to struct.") + return + } + ui.Error(err) } diff --git a/cli/controller/deploy.go b/cli/controller/deploy.go index 76147683..9d0593a3 100644 --- a/cli/controller/deploy.go +++ b/cli/controller/deploy.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "path/filepath" "time" "github.com/mantil-io/mantil/cli/log" @@ -11,7 +12,7 @@ import ( ) const ( - FunctionsDir = "functions" + ApiDir = "api" PublicDir = "public" BuildDir = "build" BinaryName = "bootstrap" @@ -19,6 +20,10 @@ const ( HashCharacters = 8 ) +var ( + FunctionsPath = filepath.Join(BuildDir, "functions") +) + type DeployArgs struct { Stage string } @@ -154,6 +159,9 @@ func (d *Deploy) HasUpdates() bool { } func (d *Deploy) buildAndFindDiffs() error { + if err := d.createMains(); err != nil { + return log.Wrap(err) + } lf, err := d.localFunctions() if err != nil { return log.Wrap(err) diff --git a/cli/controller/deploy_functions.go b/cli/controller/deploy_functions.go index dbbbba17..8e9ec7b0 100644 --- a/cli/controller/deploy_functions.go +++ b/cli/controller/deploy_functions.go @@ -17,14 +17,37 @@ import ( "github.com/mantil-io/mantil/kit/shell" ) +func (d *Deploy) createMains() error { + apis, err := d.localDirs(ApiDir) + if err != nil { + return log.Wrap(err) + } + for _, api := range apis { + dir := d.apiDir(api) + mainDest := filepath.Join(d.apiMainDir(api), "main.go") + if err := generateMain(api, dir, mainDest); err != nil { + return log.Wrap(err) + } + } + return nil +} + +func (d *Deploy) apiDir(api string) string { + return filepath.Join(d.store.ProjectRoot(), ApiDir, api) +} + +func (d *Deploy) apiMainDir(api string) string { + return filepath.Join(d.store.ProjectRoot(), FunctionsPath, api) +} + func (d *Deploy) localFunctions() ([]domain.Resource, error) { - localFuncNames, err := d.localDirs(FunctionsDir) + localFuncNames, err := d.localDirs(FunctionsPath) if err != nil { return nil, log.Wrap(err) } var localFuncs []domain.Resource for _, n := range localFuncNames { - funcDir := path.Join(d.store.ProjectRoot(), FunctionsDir, n) + funcDir := d.apiMainDir(n) if err := d.buildTimer(func() error { return d.buildFunction(BinaryName, funcDir) }); err != nil { return nil, log.Wrap(err) } @@ -85,7 +108,7 @@ func (d *Deploy) uploadFunctions() error { if f == nil { continue } - path := filepath.Join(d.store.ProjectRoot(), FunctionsDir, n, BinaryName) + path := filepath.Join(d.apiMainDir(n), BinaryName) ui.Info("\t%s", n) if err := d.uploadBinaryToS3(f.S3Key, path); err != nil { return log.Wrap(err, "failed to upload file %s to s3", path) diff --git a/cli/controller/generate.go b/cli/controller/generate.go index 1568ec84..00bb3f22 100644 --- a/cli/controller/generate.go +++ b/cli/controller/generate.go @@ -18,6 +18,14 @@ import ( "golang.org/x/mod/modfile" ) +type ApiNewError struct { + Api string +} + +func (e ApiNewError) Error() string { + return fmt.Sprintf("function New for api %s doesn't have proper type", e.Api) +} + type GenerateApiArgs struct { Name string Methods []string @@ -42,12 +50,6 @@ func GenerateApi(a GenerateApiArgs) error { if err != nil { return log.Wrap(err) } - if err := generateFunctionMain(a.Name, importPath, projectPath); err != nil { - return log.Wrap(err) - } - if err := generateFunctionGitignore(a.Name, projectPath); err != nil { - return log.Wrap(err) - } if err := generateFunctionTest(importPath, projectPath, a.Name, a.Methods); err != nil { return log.Wrap(err) } @@ -63,42 +65,14 @@ func findPackageImportPath(projectPath string) (string, error) { return modfile.ModulePath(buf), nil } -func generateFunctionMain(functionName, importPath, projectPath string) error { - functionPath := filepath.Join(FunctionsDir, functionName) - root := filepath.Join(projectPath, functionPath) - mainFile := filepath.Join(root, "main.go") - if fileExists(mainFile) { - ui.Info("%s already exists", relativePath(projectPath, mainFile)) - return nil - } - ui.Info("Generating %s...", relativePath(projectPath, mainFile)) - if err := generateFromTemplate( - apiFunctionMainTemplate, - &function{ - Name: functionName, - ImportPath: importPath, - }, - mainFile, - ); err != nil { - return log.Wrap(err) - } - return nil -} - -func generateFunctionGitignore(functionName, projectPath string) error { - functionPath := filepath.Join(FunctionsDir, functionName) - gitignoreFile := filepath.Join(projectPath, functionPath, ".gitignore") - if fileExists(gitignoreFile) { - ui.Info("%s already exists", relativePath(projectPath, gitignoreFile)) - return nil - } - ui.Info("Generating %s...", relativePath(projectPath, gitignoreFile)) - f, err := os.Create(gitignoreFile) +func generateGitignore(path, content string) error { + gitignoreFile := filepath.Join(path, ".gitignore") + f, err := os.OpenFile(gitignoreFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return log.Wrap(err) } defer f.Close() - _, err = f.WriteString(fmt.Sprintf("%s\n", BinaryName)) + _, err = f.WriteString(fmt.Sprintf("%s\n", content)) return log.Wrap(err) } @@ -259,28 +233,61 @@ func saveFile(in []byte, path string) error { return nil } -func extractApiStructName(api, dir string) (string, error) { +func generateMain(api, apiDir, destination string) error { + if err := isNewValid(api, apiDir); err != nil { + return log.Wrap(err) + } + projectPath, err := domain.FindProjectRoot(".") + if err != nil { + return log.Wrap(err) + } + importPath, err := findPackageImportPath(projectPath) + if err != nil { + return log.Wrap(err) + } + if err := generateFromTemplate( + apiFunctionMainTemplate, + &function{ + Name: api, + ImportPath: importPath, + }, + destination, + ); err != nil { + return log.Wrap(err) + } + return nil +} + +// isNewValid checks whether function New in api is of proper type +// function should have no parameters and only one return value - struct or pointer to the struct +func isNewValid(api, dir string) error { pkgs, err := parser.ParseDir(token.NewFileSet(), dir, nil, parser.AllErrors) if err != nil { - return "", log.Wrap(err) + return log.Wrap(err) } pkg, ok := pkgs[api] if !ok { - return "", log.Wrapf("package %s doesn't exist in folder %s", api, dir) + return log.Wrapf("package %s doesn't exist in folder %s", api, dir) } for _, v := range pkg.Files { for _, o := range v.Scope.Objects { if o.Name == "New" && o.Kind.String() == "func" { decl, ok := o.Decl.(*ast.FuncDecl) if !ok { - return "", log.Wrapf("could not find declaration of function New") + return log.Wrap(&ApiNewError{api}) + } + // is not a function + if decl.Recv != nil { + continue } + // has no parameters if len(decl.Type.Params.List) > 0 { - return "", log.Wrapf("function New should have no parameters") + return log.Wrap(&ApiNewError{api}) } rl := decl.Type.Results.List + // has only one return value which is either struct or pointer to struct if len(rl) > 1 { - return "", log.Wrapf("too many return values for New") + return log.Wrap(&ApiNewError{api}) } var idExpr ast.Expr expr, ok := rl[0].Type.(*ast.StarExpr) @@ -295,13 +302,13 @@ func extractApiStructName(api, dir string) (string, error) { if ok { _, ok := ft.Type.(*ast.StructType) if ok { - return ident.Name, nil + return nil } } } - return "", log.Wrapf("return value of New is not struct or pointer to struct") + return log.Wrap(&ApiNewError{api}) } } } - return "", log.Wrapf("could not find function New for api %s", api) + return log.Wrap(&ApiNewError{api}) } diff --git a/cli/controller/generate_test.go b/cli/controller/generate_test.go index 1ccaeabb..5e452085 100644 --- a/cli/controller/generate_test.go +++ b/cli/controller/generate_test.go @@ -1,51 +1,47 @@ package controller import ( - "fmt" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestExtractApiStructNamePtr(t *testing.T) { - stc, err := extractApiStructName("ping", "testdata/generate/ping_ptr") +func TestIsNewValidPtr(t *testing.T) { + err := isNewValid("ping", "testdata/generate/ping_ptr") require.NoError(t, err) - assert.Equal(t, "Ping", stc) } -func TestExtractApiStructNameStruct(t *testing.T) { - stc, err := extractApiStructName("ping", "testdata/generate/ping_struct") +func TestIsNewValidStruct(t *testing.T) { + err := isNewValid("ping", "testdata/generate/ping_struct") require.NoError(t, err) - assert.Equal(t, "Ping", stc) } -func TestExtractApiStructNameNoNew(t *testing.T) { - _, err := extractApiStructName("ping", "testdata/generate/no_new") - fmt.Println(err) +func TestIsNewValidNoNew(t *testing.T) { + err := isNewValid("ping", "testdata/generate/no_new") require.Error(t, err) } -func TestExtractApiStructNameNewWithParameters(t *testing.T) { - _, err := extractApiStructName("ping", "testdata/generate/parameters") - fmt.Println(err) +func TestIsNewValidWithParameters(t *testing.T) { + err := isNewValid("ping", "testdata/generate/parameters") require.Error(t, err) } -func TestExtractApiStructNameTooManyValues(t *testing.T) { - _, err := extractApiStructName("ping", "testdata/generate/return_values") - fmt.Println(err) +func TestIsNewValidTooManyReturnValues(t *testing.T) { + err := isNewValid("ping", "testdata/generate/return_values") require.Error(t, err) } -func TestExtractApiStructNameWrongValue(t *testing.T) { - _, err := extractApiStructName("ping", "testdata/generate/return_value") - fmt.Println(err) +func TestIsNewValidWrongReturnValue(t *testing.T) { + err := isNewValid("ping", "testdata/generate/return_value") require.Error(t, err) } -func TestExtractApiStructNameNewWrongType(t *testing.T) { - _, err := extractApiStructName("ping", "testdata/generate/new_wrong_type") - fmt.Println(err) +func TestIsNewValidNewWrongType(t *testing.T) { + err := isNewValid("ping", "testdata/generate/new_wrong_type") + require.Error(t, err) +} + +func TestIsNewValidMethod(t *testing.T) { + err := isNewValid("ping", "testdata/generate/new_method") require.Error(t, err) } diff --git a/cli/controller/new.go b/cli/controller/new.go index 8bbeabf7..239a499a 100644 --- a/cli/controller/new.go +++ b/cli/controller/new.go @@ -1,6 +1,7 @@ package controller import ( + "errors" "fmt" "os" "path/filepath" @@ -10,7 +11,6 @@ import ( "github.com/mantil-io/mantil/cli/ui" "github.com/mantil-io/mantil/domain" "github.com/mantil-io/mantil/kit/git" - "github.com/pkg/errors" ) var TemplateRepos = map[string]string{ @@ -60,10 +60,14 @@ func createProject(name, from, moduleName string) error { return log.Wrap(err, "could not initialize repository from source %s: %v", repo, err) } + // delete LICENSE from template repositories if !isExternalRepo(from) { os.Remove(filepath.Join(projectPath, LicenseFile)) } + // create .gitignore with BuildDir + generateGitignore(projectPath, BuildDir) + fs, err := newStore() if err != nil { return log.Wrap(err) diff --git a/cli/controller/testdata/generate/new_method/ping.go b/cli/controller/testdata/generate/new_method/ping.go new file mode 100644 index 00000000..38873c08 --- /dev/null +++ b/cli/controller/testdata/generate/new_method/ping.go @@ -0,0 +1,5 @@ +package ping + +type Ping struct{} + +func (p Ping) New() {}