diff --git a/.gitignore b/.gitignore index 98de5f9..c97db96 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,5 @@ go.work .idea gob -*.sql 11.json target diff --git a/README.md b/README.md index ef0d034..8db60a6 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ flowchart TD gob.yaml --> plugin2 gob.yaml --> plugin3 ``` -You just need to tell `gob` 3W(where,when and what) +You just need to tell `gbc` 3W(where,when and what) 1. **Where** : where to download the tool 2. **When** : when to execute to command @@ -59,40 +59,40 @@ You just need to tell `gob` 3W(where,when and what) ## Quick Start 1. Install `gob` with below command ```shell - go install github.com/kcmvp/gob + go install github.com/kcmvp/gbc ``` 2. Initialize project with below command(in the project home directory) ```shell - gob init + gbc init ``` -| Make some changes and comit code | execute `gob deps` | +| Make some changes and comit code | execute `gbc deps` | |--------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| -| | | +| | | ## Commands Build Commands -- [gob init](#gob-init) -- [gob build](#gob-build) -- [gob clean](#gob-clean) -- [gob test](#gob-test) -- [gob lint](#gob-lint) -- [gob deps](#gob-deps) +- [gbc init](#gbc-init) +- [gbc build](#gbc-build) +- [gbc clean](#gbc-clean) +- [gbc test](#gbc-test) +- [gbc lint](#gbc-lint) +- [gbc deps](#gbc-deps) Plugin Commands -- [gob plugin install](#gob-plugin-install) -- [gob plugin list](#gob-plugin-list) +- [gbc plugin install](#gbc-plugin-install) +- [gbc plugin list](#gbc-plugin-list) Setup Commands -- [gob setup version](#gob-setup-version) +- [gbc setup version](#gbc-setup-version) -### gob init +### gbc init ```shell -gob init +gbc init ``` -Initialize gob for the project, it will do following initializations +Initialize gbc for the project, it will do following initializations 1. generate file `gob.yaml` 2. generate file `.golangci.yaml`, which is the configuration for [golangci-lint](https://github.com/golangci/golangci-lint) 3. setup `git hooks` if project in the source control. @@ -113,7 +113,7 @@ exec: - test plugins: golangci-lint: - alias: lint #When : when issue `gob lint` + alias: lint #When : when issue `gbc lint` args: run ./... #What: execute `golangci-lint run ./...` url: github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 #Where: where to download the plugin gotestsum: @@ -121,41 +121,41 @@ plugins: args: --format testname -- -coverprofile=target/cover.out ./... url: gotest.tools/gotestsum@v1.11.0 ``` -in most cases you don't need to edit the configuration manually. you can achieve this by [plugin commands](#gob-plugin-install) +in most cases you don't need to edit the configuration manually. you can achieve this by [plugin commands](#gbc-plugin-install) -### gob build +### gbc build ```shell -gob build +gbc build ``` This command would build all the candidate binaries(main methods in main packages) to the `target` folder. 1. Final binary name is same as go source file name which contains `main method` 2. Would fail if there are same name go main surce file -### gob clean +### gbc clean ```shell -gob clean +gbc clean ``` This command would clean `target` folder -### gob test +### gbc test ```shell -gob test +gbc test ``` This command would run all tests for the project and generate coverage report at `target/cover.html` -### gob lint +### gbc lint ```shell -gob lint +gbc lint ``` Run `golangci-lint` against project based on the configuration, a report named `target/lint.log` will be generated if there are any violations -### gob deps +### gbc deps ```shell -gob deps +gbc deps ``` List project dependencies tree and indicate there are updates for a specific dependency -### gob plugin install +### gbc plugin install ```shell -gob plugin install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 lint run ./... +gbc plugin install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 lint run ./... ``` It is an advanced version of `go install`, which supports multi-version.(eg:`golangci-lint-v1.55.2`, `golangci-lint-v1.55.1`) 1. Install the versioned tool(just the same as `go install`) diff --git a/application.yaml b/application.yaml new file mode 100644 index 0000000..92284e1 --- /dev/null +++ b/application.yaml @@ -0,0 +1,19 @@ +# "postgres://username:password@localhost:5432/database_name" +# username:password@protocol(address)/dbname?param=value +datasource: + ds1: + driver: sqlite3 + user: usera + password: passwd1 + Host: localhost + url: file:test1.db?cache=shared&mode=memory + ds2: + driver: sqlite3 + user: userb + password: passwd2 + url: file:test2.db?cache=shared&mode=memory + scripts: + - sqlite3-schema.sql + + + diff --git a/application_test.yaml b/application_test.yaml new file mode 100644 index 0000000..e7930d4 --- /dev/null +++ b/application_test.yaml @@ -0,0 +1,12 @@ +# "postgres://username:password@localhost:5432/database_name" +# username:password@protocol(address)/dbname?param=value +datasource: + ds1: + driver: sqlite3 + user: abc + password: 123 + url: file:test3.db?cache=shared&mode=memory + scripts: + - sqlite3-schema.sql + - sqlite3-init.sql + diff --git a/boot/application.go b/boot/application.go new file mode 100644 index 0000000..09fb0ba --- /dev/null +++ b/boot/application.go @@ -0,0 +1,57 @@ +package boot + +import ( + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/kcmvp/gob/internal" + "github.com/kcmvp/gob/utils" + + "github.com/samber/do/v2" + "github.com/spf13/viper" +) + +const ( + DefaultCfg = "application" +) + +var ( + cfg *viper.Viper + once sync.Once +) + +func RootDir() string { + return internal.RootDir +} + +func Container() *do.RootScope { + return internal.Container +} + +func InitApp() { + InitAppWith(DefaultCfg) +} + +func InitAppWith(cfgName string) { + if cfg == nil { + once.Do(func() { + cfg = viper.New() + cfg.SetConfigName(cfgName) // name of cfg file (without extension) + cfg.SetConfigType("yaml") // REQUIRED if the cfg file does not have the extension in the name + cfg.AddConfigPath(internal.RootDir) // optionally look for cfg in the working directory + if err := cfg.ReadInConfig(); err != nil { // Find and read the cfg file + panic(fmt.Errorf("fatal error cfg file: %w", err)) + } + if test, _ := utils.TestCaller(); test { + if testCfg, err := os.Open(filepath.Join(internal.RootDir, fmt.Sprintf("%s_test.yaml", cfgName))); err == nil { + if err = cfg.MergeConfig(testCfg); err != nil { + panic(fmt.Errorf("failed to merge test configuration file: %w", err)) + } + } + } + setupDb() + }) + } +} diff --git a/boot/db.go b/boot/db.go new file mode 100644 index 0000000..5211159 --- /dev/null +++ b/boot/db.go @@ -0,0 +1,86 @@ +package boot + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/kcmvp/gob/internal" + + //"github.com/kcmvp/gob/internal" + + "github.com/samber/do/v2" + typetostring "github.com/samber/go-type-to-string" + "github.com/samber/lo" +) + +const ( + DSKey = "datasource" + UserKey = "${user}" + PasswordKey = "${password}" + HostKey = "${host}" + DefaultDS = "DefaultDS" +) + +type dataSource struct { + Driver string `mapstructure:"driver"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + Host string `mapstructure:"host"` + URL string `mapstructure:"url"` + Scripts []string `mapstructure:"scripts"` +} + +func (ds dataSource) DSN() string { + dsn := strings.ReplaceAll(ds.URL, UserKey, ds.User) + dsn = strings.ReplaceAll(dsn, PasswordKey, ds.Password) + return strings.ReplaceAll(dsn, HostKey, ds.Host) +} + +func dsMap() map[string]dataSource { + // single data source + if v := cfg.Get(fmt.Sprintf("%s.%s", DSKey, "driver")); v != nil { + var ds dataSource + if err := cfg.UnmarshalKey(DSKey, &ds); err != nil { + panic(fmt.Errorf("failed parse datasource: %w", err)) + } + return map[string]dataSource{DefaultDS: ds} + // multiple data sources + } else if v = cfg.Get(DSKey); v != nil { + dss := v.(map[string]any) + return lo.MapValues(dss, func(_ any, key string) dataSource { + var ds dataSource + key = fmt.Sprintf("%s.%s", DSKey, key) + if err := cfg.UnmarshalKey(key, &ds); err != nil { + panic(fmt.Errorf("failed parse datasource: %w", err)) + } + return ds + }) + } + return map[string]dataSource{} +} + +func setupDb() { + for name, ds := range dsMap() { + if db, err := sql.Open(ds.Driver, ds.DSN()); err == nil { + if err = db.Ping(); err != nil { + _ = db.Close() + panic(fmt.Errorf("failed to initialize %s: %w", name, err)) + } + lo.ForEach(ds.Scripts, func(script string, _ int) { + if data, err := os.ReadFile(filepath.Join(internal.RootDir, script)); err == nil { + if _, err = db.Exec(string(data)); err != nil { + panic(fmt.Errorf("failed to execute %s: %w", script, err)) + } + } else { + panic(fmt.Errorf("failed to read %s: %w", script, err)) + } + }) + do.ProvideNamedValue[*sql.DB](internal.Container, fmt.Sprintf("%s_%s", name, typetostring.GetType[*sql.DB]()), db) + } else { + panic(fmt.Errorf("failed to connect to datasource %s: %w", name, err)) + } + } +} diff --git a/boot/db_test.go b/boot/db_test.go new file mode 100644 index 0000000..3ce8d82 --- /dev/null +++ b/boot/db_test.go @@ -0,0 +1,42 @@ +package boot + +import ( + "database/sql" + "fmt" + "github.com/kcmvp/gob/internal" + _ "github.com/mattn/go-sqlite3" + "github.com/samber/do/v2" + typetostring "github.com/samber/go-type-to-string" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "testing" +) + +type DBTestSuite struct { + suite.Suite +} + +func TestDBTestSuite(t *testing.T) { + suite.Run(t, &DBTestSuite{}) +} + +func (dbs *DBTestSuite) SetupSuite() { + InitApp() +} + +func (dbs *DBTestSuite) TestMultipleDB() { + ds := dsMap() + assert.Equal(dbs.T(), 2, len(ds)) + db := do.MustInvokeNamed[*sql.DB](internal.Container, fmt.Sprintf("%s_%s", "ds1", typetostring.GetType[*sql.DB]())) + assert.NotNil(dbs.T(), db) + rs, err := db.Exec("select * from Product") + assert.NoError(dbs.T(), err) + cnt, _ := rs.RowsAffected() + assert.Equal(dbs.T(), int64(2), cnt) + db = do.MustInvokeNamed[*sql.DB](internal.Container, fmt.Sprintf("%s_%s", "ds2", typetostring.GetType[*sql.DB]())) + assert.NotNil(dbs.T(), db) + rs, err = db.Exec("select * from Product") + assert.NoError(dbs.T(), err) + cnt, _ = rs.RowsAffected() + assert.Equal(dbs.T(), int64(0), cnt) +} diff --git a/cmd/builder.go b/cmd/builder.go deleted file mode 100644 index e138644..0000000 --- a/cmd/builder.go +++ /dev/null @@ -1,69 +0,0 @@ -// Package cmd /* -package cmd - -import ( - "context" - "embed" - "errors" - "fmt" - "github.com/fatih/color" - "github.com/kcmvp/gob/internal" - "github.com/samber/lo" - "github.com/spf13/cobra" - "os" -) - -//go:embed resources/* -var resources embed.FS - -const resourceDir = "resources" - -// builderCmd represents the base command when called without any subcommands -var builderCmd = &cobra.Command{ - Use: "gob", - Short: "Go project boot", - Long: `Go pluggable toolchain and best practice`, - ValidArgs: validBuilderArgs(), - Args: func(cmd *cobra.Command, args []string) error { - if !lo.Every(validBuilderArgs(), args) { - return fmt.Errorf(color.RedString("valid args are : %s", validBuilderArgs())) - } - if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { - return fmt.Errorf(color.RedString(err.Error())) - } - return nil - }, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - internal.CurProject().Validate() - }, - RunE: func(cmd *cobra.Command, args []string) error { - for _, arg := range lo.Uniq(args) { - if err := execute(cmd, arg); err != nil { - return errors.New(color.RedString("%s \n", err.Error())) - } - } - return nil - }, -} - -func Execute() error { - currentDir, _ := os.Getwd() - if internal.CurProject().Root() != currentDir { - return fmt.Errorf(color.RedString("Please execute the command in the project root dir")) - } - ctx := context.Background() - if err := builderCmd.ExecuteContext(ctx); err != nil { - return fmt.Errorf(color.RedString(err.Error())) - } - return nil -} - -func init() { - builderCmd.SetErrPrefix(color.RedString("Error:")) - builderCmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error { - return lo.IfF(err != nil, func() error { - return fmt.Errorf(color.RedString(err.Error())) - }).Else(nil) - }) - builderCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/builder_test.go b/cmd/builder_test.go deleted file mode 100644 index 3d46a01..0000000 --- a/cmd/builder_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package cmd - -import ( - "github.com/fatih/color" - "github.com/kcmvp/gob/internal" - "github.com/samber/lo" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "os" - "path/filepath" - "strings" - "testing" -) - -type BuilderTestSuit struct { - suite.Suite -} - -func TestBuilderTestSuit(t *testing.T) { - suite.Run(t, &BuilderTestSuit{}) -} - -func (suite *BuilderTestSuit) TearDownSuite() { - TearDownSuite("cmd_builder_test_") -} - -func (suite *BuilderTestSuit) TestValidArgs() { - assert.Equal(suite.T(), []string{"build", "clean", "test", "lint"}, builderCmd.ValidArgs) -} - -func (suite *BuilderTestSuit) TestArgs() { - tests := []struct { - name string - args []string - wantErr bool - }{ - { - name: "not in valid args list", - args: []string{"def"}, - wantErr: true, - }, - { - name: "partial valid args", - args: []string{"test", "def"}, - wantErr: true, - }, - { - name: "no args", - args: []string{}, - wantErr: true, - }, - { - name: "empty args", - args: []string{""}, - wantErr: true, - }, - { - name: "positive case", - args: []string{"clean", "test"}, - wantErr: false, - }, - } - for _, test := range tests { - err := builderCmd.Args(nil, test.args) - assert.True(suite.T(), test.wantErr == (err != nil)) - } - -} - -func (suite *BuilderTestSuit) TestExecute() { - builderCmd.SetArgs([]string{"cd"}) - os.Chdir(internal.CurProject().Root()) - err := Execute() - assert.Equal(suite.T(), "valid args are : [build clean test lint]", err.Error()) - os.Chdir(internal.GoPath()) - err = Execute() - assert.Equal(suite.T(), "Please execute the command in the project root dir", err.Error()) - builderCmd.SetArgs([]string{"build"}) - os.Chdir(internal.CurProject().Root()) - err = Execute() - assert.NoError(suite.T(), err) -} - -func (suite *BuilderTestSuit) TestBuild() { - tests := []struct { - name string - args []string - wantErr bool - }{ - { - name: "invalid", - args: []string{"cd"}, - wantErr: true, - }, - { - name: "valid", - args: []string{"build"}, - wantErr: false, - }, - } - for _, test := range tests { - builderCmd.SetArgs(test.args) - err := execute(builderCmd, test.args[0]) - assert.True(suite.T(), test.wantErr == (err != nil)) - if test.wantErr { - assert.True(suite.T(), strings.Contains(err.Error(), color.RedString(""))) - } - } -} - -func (suite *BuilderTestSuit) TestPersistentPreRun() { - builderCmd.PersistentPreRun(nil, nil) - hooks := lo.MapToSlice(internal.HookScripts(), func(key string, _ string) string { - return key - }) - for _, hook := range hooks { - _, err := os.Stat(filepath.Join(internal.CurProject().HookDir(), hook)) - assert.Error(suite.T(), err) - } - internal.CurProject().SetupHooks(true) - for _, hook := range hooks { - _, err := os.Stat(filepath.Join(internal.CurProject().HookDir(), hook)) - assert.NoError(suite.T(), err) - } -} - -func (suite *BuilderTestSuit) TestBuiltinPlugins() { - plugins := builtinPlugins() - assert.Equal(suite.T(), 2, len(plugins)) - plugin, ok := lo.Find(plugins, func(plugin internal.Plugin) bool { - return plugin.Url == "github.com/golangci/golangci-lint/cmd/golangci-lint" - }) - assert.True(suite.T(), ok) - assert.Equal(suite.T(), "v1.56.2", plugin.Version()) - assert.Equal(suite.T(), "golangci-lint", plugin.Name()) - assert.Equal(suite.T(), "github.com/golangci/golangci-lint", plugin.Module()) - assert.Equal(suite.T(), "lint", plugin.Alias) - plugin, ok = lo.Find(plugins, func(plugin internal.Plugin) bool { - return plugin.Url == "gotest.tools/gotestsum" - }) - assert.True(suite.T(), ok) - assert.Equal(suite.T(), "v1.11.0", plugin.Version()) - assert.Equal(suite.T(), "gotestsum", plugin.Name()) - assert.Equal(suite.T(), "gotest.tools/gotestsum", plugin.Module()) - assert.Equal(suite.T(), "test", plugin.Alias) -} - -func (suite *BuilderTestSuit) TestRunE() { - target := internal.CurProject().Target() - err := builderCmd.RunE(builderCmd, []string{"build"}) - assert.NoError(suite.T(), err) - _, err = os.Stat(filepath.Join(target, lo.If(internal.Windows(), "gob.exe").Else("gob"))) - assert.NoError(suite.T(), err, "binary should be generated") - err = builderCmd.RunE(builderCmd, []string{"build", "clean"}) - assert.NoError(suite.T(), err) - assert.NoFileExistsf(suite.T(), filepath.Join(target, lo.If(internal.Windows(), "gob.exe").Else("gob")), "binary should be deleted") - err = builderCmd.RunE(builderCmd, []string{"def"}) - assert.Errorf(suite.T(), err, "can not find the command def") -} diff --git a/cmd/setup_test.go b/cmd/setup_test.go deleted file mode 100644 index 631117e..0000000 --- a/cmd/setup_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/kcmvp/gob/internal" - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "testing" -) - -func TestValidSetupArgs(t *testing.T) { - assert.Equal(t, setupCmd.ValidArgs, []string{"version"}) -} - -func TestSetupVersion(t *testing.T) { - version := filepath.Join(internal.CurProject().Root(), "infra", "version.go") - os.Remove(version) - _, err := os.Stat(version) - assert.Error(t, err) - builderCmd.SetArgs([]string{"setup", "version"}) - err = builderCmd.Execute() - assert.NoError(t, err) - _, err = os.Stat(version) - assert.NoError(t, err) -} diff --git a/dbx/clause.go b/dbx/clause.go new file mode 100644 index 0000000..50c9e81 --- /dev/null +++ b/dbx/clause.go @@ -0,0 +1,108 @@ +package dbx + +import ( + "fmt" + + "github.com/kcmvp/gob/internal" + "github.com/samber/lo" +) + +type IEntity interface { + Table() string +} + +type Key interface { + string | int64 +} + +type SQL interface { + SQLStr() string +} + +type Order string + +const ( + ASC Order = "ASC" + DESC Order = "DESC" +) + +type Attr internal.Mapper + +func (a Attr) SQLStr() string { + return a.B +} + +type Set lo.Tuple2[Attr, any] + +// Criteria attribute predicate +type Criteria struct { + expression string +} + +func (criteria Criteria) SQLStr() string { + return criteria.expression +} + +func (criteria Criteria) Or(rp Criteria) Criteria { + return Criteria{ + expression: fmt.Sprintf("(%s or %s)", criteria.expression, rp.SQLStr()), + } +} + +func (criteria Criteria) Add(rp Criteria) Criteria { + return Criteria{ + expression: fmt.Sprintf("(%s and %s)", criteria.expression, rp.SQLStr()), + } +} + +func LT(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s < ?", attr.SQLStr())} +} + +func LTE(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s <= ?", attr.SQLStr())} +} + +func GT(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s > ?", attr.SQLStr())} +} + +func GTE(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s >= ?", attr.SQLStr())} +} + +func Null(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s is null", attr.SQLStr())} +} + +func NotNull(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s is not null", attr.SQLStr())} +} + +func Like(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s like '%%?%%'", attr.SQLStr())} +} + +func NotLike(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s not like '%%?%%'", attr.SQLStr())} +} + +func Prefix(attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s like '?%%'", attr.SQLStr())} +} + +func Suffix[E IEntity](attr Attr) Criteria { + return Criteria{expression: fmt.Sprintf("%s like '%%?'", attr.SQLStr())} +} + +type OrderBy lo.Tuple2[Attr, Order] + +func (orderBy OrderBy) SQLStr() string { + return fmt.Sprintf("%s %s", orderBy.A.SQLStr(), orderBy.B) +} + +var ( + _ SQL = (*Attr)(nil) + _ SQL = (*Criteria)(nil) + _ SQL = (*OrderBy)(nil) +) diff --git a/dbx/clause_test.go b/dbx/clause_test.go new file mode 100644 index 0000000..f7e054a --- /dev/null +++ b/dbx/clause_test.go @@ -0,0 +1,51 @@ +package dbx + +import ( + "database/sql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "testing" + "time" +) + +type Base struct { + CreatedAt time.Time `ds:"autoUpdateTime"` + CreatedBy string `ds:"createdBy"` + UpdatedAt time.Time `ds:"autoCreateTime"` + UpdatedBy string `ds:"updatedBy"` +} + +type Product struct { + Base + Id string `ds:"pk;"` + Name string `ds:"column=name"` + FullName string `ds:"ignore"` + Grade sql.NullInt32 + Address sql.NullString + ProductDate time.Time + comment string +} + +func (p Product) Table() string { + return "product" +} + +func TestName(t *testing.T) { + //assert.Equal(t, 1, 1) +} + +type BuilderTestSuit struct { + //builder *SqlBuilder + suite.Suite +} + +func TestBuilderSuite(t *testing.T) { + suite.Run(t, &BuilderTestSuit{}) +} + +// func (suite *BuilderTestSuit) SetupSuite() { +// //suite.builder = do.MustInvoke[*SqlBuilder](boot.Container()) +// } +func (suite *BuilderTestSuit) TestHappyFlow() { + assert.Equal(suite.T(), 1, 1) +} diff --git a/dbx/dbx.go b/dbx/dbx.go new file mode 100644 index 0000000..f00286e --- /dev/null +++ b/dbx/dbx.go @@ -0,0 +1,115 @@ +// nolint +package dbx + +import ( + "context" + "database/sql" + "strings" + + "github.com/kcmvp/gob/internal" + "github.com/samber/do/v2" + "github.com/samber/lo" +) + +const DefaultDS = "defaultDS" + +// DBX database adapter +type DBX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row + Close() error +} + +type Hook func(sql string) string + +type DBXImpl struct { //nolint + ds DBX + beforeQueryHooks []Hook + beforeExecHooks []Hook +} + +func (dbxImpl *DBXImpl) PoolSize() int32 { + // TODO implement me + panic("implement me") +} + +func (dbxImpl *DBXImpl) TotalConns() int32 { + // TODO implement me + panic("implement me") +} + +func (dbxImpl *DBXImpl) IdleConns() int32 { + // TODO implement me + panic("implement me") +} + +func (dbxImpl *DBXImpl) MaxIdleDestroyCount() int32 { + // TODO implement me + panic("implement me") +} + +func (dbxImpl *DBXImpl) Close() error { + return dbxImpl.ds.Close() +} + +func (dbxImpl *DBXImpl) Shutdown() { + dbxImpl.Close() +} + +func (dbxImpl *DBXImpl) HealthCheck(_ context.Context) error { + panic("print pool status") +} + +func (dbxImpl *DBXImpl) PrepareContext(_ context.Context, s string) (*sql.Stmt, error) { + // TODO implement me + panic("implement me") +} + +func (dbxImpl *DBXImpl) ExecContext(_ context.Context, s string, i ...interface{}) (sql.Result, error) { + // TODO implement me + for _, hook := range dbxImpl.beforeExecHooks { + s = hook(s) + } + panic("implement me") +} + +func (dbxImpl *DBXImpl) QueryContext(ctx context.Context, s string, i ...interface{}) (*sql.Rows, error) { + for _, hook := range dbxImpl.beforeQueryHooks { + s = hook(s) + } + panic("implement me") +} + +func (dbxImpl *DBXImpl) QueryRowContext(ctx context.Context, s string, i ...interface{}) *sql.Row { + for _, hook := range dbxImpl.beforeQueryHooks { + s = hook(s) + } + panic("implement me") +} + +func (dbxImpl *DBXImpl) AddQueryHook(hook Hook) { + dbxImpl.beforeQueryHooks = append(dbxImpl.beforeQueryHooks, hook) +} + +func (dbxImpl *DBXImpl) AddExecHooks(hook Hook) { + dbxImpl.beforeExecHooks = append(dbxImpl.beforeExecHooks, hook) +} + +func init() { + lo.ForEach(internal.Container.ListProvidedServices(), func(item do.EdgeService, _ int) { + if strings.HasSuffix(item.Service, "_*database/sql.DB") { + dsName := strings.TrimSuffix(item.Service, "_*database/sql.DB") + do.ProvideNamed[DBX](internal.Container, dsName, func(injector do.Injector) (DBX, error) { + return &DBXImpl{ds: do.MustInvokeNamed[*sql.DB](injector, item.Service)}, nil + }) + } + }) +} + +var ( + _ DBX = (*DBXImpl)(nil) + _ do.HealthcheckerWithContext = (*DBXImpl)(nil) + _ do.Shutdowner = (*DBXImpl)(nil) +) diff --git a/dbx/query.go b/dbx/query.go new file mode 100644 index 0000000..e02031c --- /dev/null +++ b/dbx/query.go @@ -0,0 +1,51 @@ +// nolint +package dbx + +import ( + "github.com/samber/lo" +) + +type Joint lo.Tuple3[Attr, Attr, string] + +func (joint Joint) String() string { + return "" +} + +type Result struct { + raw map[string]any + attrs []Attr //nolint +} + +func (result Result) Get(attr Attr) (any, error) { + return nil, nil +} + +func (result Result) Count() int { + return len(result.raw) +} + +type Query struct { + jointStr string +} + +func Select(attrs []Attr) Query { + //@todo validate + return Query{} +} + +func (query Query) WithJoin(joints []Joint) Query { + //@todo validate + return Query{} +} + +func (query Query) OrderBy(orders []OrderBy) Query { + return Query{} +} + +func (query Query) Where(criteria Criteria, values func() []any) Query { + return Query{} +} + +func (query Query) Rows() Result { + return Result{} +} diff --git a/dbx/repository.go b/dbx/repository.go new file mode 100644 index 0000000..a28d917 --- /dev/null +++ b/dbx/repository.go @@ -0,0 +1,88 @@ +// nolint +package dbx + +import ( + "fmt" + + "github.com/kcmvp/gob/internal" + "github.com/samber/do/v2" +) + +type Repository[E IEntity, K Key] interface { + Insert(entity E) (int64, error) + BatchInsert(entities []E) (int64, error) + Delete(Key K) (int64, error) + Update(set []Set, criteria Criteria, values func() []any) (int64, error) + Find(Key K) (E, error) + FindBy(criteria Criteria, values func() []any) (E, error) + DeleteBy(criteria Criteria, values func() []any) (int64, error) + SearchBy(criteria Criteria, values func() []any, orderBy ...OrderBy) ([]E, error) +} + +type MustBeStructError struct { + msg string +} + +func (e MustBeStructError) Error() string { + return fmt.Sprintf("must be struct %s", e.msg) +} + +// defaultRepository default Repository implementation +type defaultRepository[E IEntity, K Key] struct { + zeroK K //nolint + zeroE E //nolint + // sqlBuilder *SqlBuilder + dbx DBX +} + +func (d defaultRepository[E, K]) Insert(entity E) (int64, error) { + // TODO implement me + panic("implement me") +} + +func (d defaultRepository[E, K]) BatchInsert(entities []E) (int64, error) { + // TODO implement me + panic("implement me") +} + +func (d defaultRepository[E, K]) Delete(Key K) (int64, error) { + // TODO implement me + panic("implement me") +} + +func (d defaultRepository[E, K]) Find(Key K) (E, error) { + // TODO implement me + panic("implement me") +} + +func (d defaultRepository[E, K]) FindBy(criteria Criteria, parameters func() []any) (E, error) { + // TODO implement me + panic("implement me") +} + +func (d defaultRepository[E, K]) DeleteBy(criteria Criteria, parameters func() []any) (int64, error) { + // TODO implement me + panic("implement me") +} + +func (d defaultRepository[E, K]) Update(set []Set, criteria Criteria, parameters func() []any) (int64, error) { + // TODO implement me + panic("implement me") +} + +func (d defaultRepository[E, K]) SearchBy(criteria Criteria, parameters func() []any, orderBy ...OrderBy) ([]E, error) { + // TODO implement me + panic("implement me") +} + +func NewRepository[E IEntity, K Key]() Repository[E, K] { + return NewRepositoryWithDS[E, K](DefaultDS) +} + +func NewRepositoryWithDS[E IEntity, K Key](dsName string) Repository[E, K] { + repo := &defaultRepository[E, K]{ + dbx: do.MustInvokeNamed[DBX](internal.Container, dsName), + } + //@todo validate with Attr + return repo +} diff --git a/internal/hook.go b/gbc/artifact/hook.go similarity index 77% rename from internal/hook.go rename to gbc/artifact/hook.go index 2091268..e56e30f 100644 --- a/internal/hook.go +++ b/gbc/artifact/hook.go @@ -1,4 +1,4 @@ -package internal +package artifact import ( "bufio" @@ -10,6 +10,7 @@ import ( ) const ( + command = "gbc" execCfgKey = "exec" //hook script name CommitMsg = "commit-msg" @@ -19,9 +20,9 @@ const ( func HookScripts() map[string]string { return map[string]string{ - CommitMsg: fmt.Sprintf("gob exec %s $1", CommitMsg), - PreCommit: fmt.Sprintf("gob exec %s", PreCommit), - PrePush: fmt.Sprintf("gob exec %s $1 $2", PrePush), + CommitMsg: fmt.Sprintf("%s exec %s $1", command, CommitMsg), + PreCommit: fmt.Sprintf("%s exec %s", command, PreCommit), + PrePush: fmt.Sprintf("%s exec %s $1 $2", command, PrePush), } } @@ -58,7 +59,7 @@ func (project *Project) Executions() []Execution { } // SetupHooks setup git local hooks for project. force means always update gob.yaml -func (project *Project) SetupHooks(force bool) { +func (project *Project) SetupHooks(force bool) error { if force { hook := map[string]any{ fmt.Sprintf("%s.%s", execCfgKey, CommitMsg): "^#[0-9]+:\\s*.{10,}$", @@ -70,10 +71,11 @@ func (project *Project) SetupHooks(force bool) { } } if !InGit() { - color.Yellow("project is not in the source control") - return + return fmt.Errorf(color.RedString("project is not in the source control")) + } + if err := project.config().ReadInConfig(); err != nil { + return err } - _ = project.config().ReadInConfig() gitHook := CurProject().GitHook() var hooks []string if len(gitHook.CommitMsg) > 0 { @@ -88,9 +90,10 @@ func (project *Project) SetupHooks(force bool) { shell := lo.IfF(Windows(), func() string { return "#!/usr/bin/env pwsh\n" }).Else("#!/bin/sh\n") + hookDir := CurProject().HookDir() for name, script := range HookScripts() { if lo.Contains(hooks, name) || force { - msgHook, _ := os.OpenFile(filepath.Join(CurProject().HookDir(), name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm) + msgHook, _ := os.OpenFile(filepath.Join(hookDir, name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm) writer := bufio.NewWriter(msgHook) writer.WriteString(shell) writer.WriteString("\n") @@ -98,7 +101,8 @@ func (project *Project) SetupHooks(force bool) { writer.Flush() msgHook.Close() } else { - os.Remove(filepath.Join(CurProject().HookDir(), name)) + os.Remove(filepath.Join(hookDir, name)) } } + return nil } diff --git a/internal/hook_test.go b/gbc/artifact/hook_test.go similarity index 91% rename from internal/hook_test.go rename to gbc/artifact/hook_test.go index db83e5f..2ff715a 100644 --- a/internal/hook_test.go +++ b/gbc/artifact/hook_test.go @@ -1,6 +1,7 @@ -package internal +package artifact import ( + "github.com/kcmvp/gob/utils" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -31,7 +32,8 @@ func TearDownSuite(prefix string) { } func (suite *GitHookTestSuite) TearDownSuite() { - TearDownSuite("internal_hook_test_") + _, method := utils.TestCaller() + TearDownSuite(strings.Join(lo.DropRight(strings.Split(method, "_"), 1), "_")) } func TestGitHookSuite(t *testing.T) { diff --git a/internal/multiple_writer.go b/gbc/artifact/multiple_writer.go similarity index 99% rename from internal/multiple_writer.go rename to gbc/artifact/multiple_writer.go index b49bf82..44ed37a 100644 --- a/internal/multiple_writer.go +++ b/gbc/artifact/multiple_writer.go @@ -1,4 +1,4 @@ -package internal +package artifact import ( "bufio" diff --git a/internal/plugin.go b/gbc/artifact/plugin.go similarity index 91% rename from internal/plugin.go rename to gbc/artifact/plugin.go index bd01ed3..3814fe6 100644 --- a/internal/plugin.go +++ b/gbc/artifact/plugin.go @@ -1,4 +1,4 @@ -package internal +package artifact import ( "encoding/json" @@ -17,13 +17,14 @@ import ( const modulePattern = `^[^@]+@?[^@\s]+$` type Plugin struct { - Alias string `json:"alias" mapstructure:"alias"` - Args string `json:"args" mapstructure:"args"` - Url string `json:"url" mapstructure:"url"` //nolint - Config string `json:"config" mapstructure:"config"` - version string - name string - module string + Alias string `json:"alias" mapstructure:"alias"` + Args string `json:"args" mapstructure:"args"` + Url string `json:"url" mapstructure:"url"` //nolint + Config string `json:"config" mapstructure:"config"` + Description string `json:"description" mapstructure:"description"` + version string + name string + module string } func (plugin *Plugin) init() error { diff --git a/internal/plugin_test.go b/gbc/artifact/plugin_test.go similarity index 91% rename from internal/plugin_test.go rename to gbc/artifact/plugin_test.go index 72ff664..a91fb40 100644 --- a/internal/plugin_test.go +++ b/gbc/artifact/plugin_test.go @@ -1,14 +1,16 @@ -package internal +package artifact import ( "encoding/json" "fmt" + "github.com/kcmvp/gob/utils" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/tidwall/gjson" "os" "path/filepath" + "strings" "testing" ) @@ -17,7 +19,8 @@ type InternalPluginTestSuit struct { } func (suite *InternalPluginTestSuit) TearDownSuite() { - TearDownSuite("internal_plugin_test_") + _, method := utils.TestCaller() + TearDownSuite(strings.Trim(method, "TearDownSuite")) } func TestInternalPluginSuite(t *testing.T) { @@ -42,7 +45,7 @@ func (suite *InternalPluginTestSuit) TestNewPlugin() { url: "github.com/golangci/golangci-lint/cmd/golangci-lint", module: "github.com/golangci/golangci-lint", logName: "golangci-lint", - binary: "golangci-lint-v1.56.2", + binary: "golangci-lint-v1.57.2", wantErr: false, }, { @@ -50,7 +53,7 @@ func (suite *InternalPluginTestSuit) TestNewPlugin() { url: "github.com/golangci/golangci-lint/cmd/golangci-lint@latest", module: "github.com/golangci/golangci-lint", logName: "golangci-lint", - binary: "golangci-lint-v1.56.2", + binary: "golangci-lint-v1.57.2", wantErr: false, }, { @@ -102,7 +105,7 @@ func (suite *InternalPluginTestSuit) TestNewPlugin() { assert.True(t, test.wantErr == (err != nil)) if !test.wantErr { assert.Equal(t, test.module, plugin.module) - assert.True(t, lo.Contains([]string{"v1.56.2", "v1.1.1", "v1.11.0"}, plugin.Version())) + assert.True(t, lo.Contains([]string{"v1.57.2", "v1.1.1", "v1.11.0"}, plugin.Version())) } }) } @@ -113,7 +116,7 @@ func (suite *InternalPluginTestSuit) TestUnmarshalJSON() { defer func() { os.RemoveAll(gopath) }() - data, _ := os.ReadFile(filepath.Join(CurProject().Root(), "cmd", "resources", "config.json")) + data, _ := os.ReadFile(filepath.Join(CurProject().Root(), "gbc", "cmd", "resources", "config.json")) v := gjson.GetBytes(data, "plugins") var plugins []Plugin err := json.Unmarshal([]byte(v.Raw), &plugins) @@ -124,7 +127,7 @@ func (suite *InternalPluginTestSuit) TestUnmarshalJSON() { return plugin.Url == "github.com/golangci/golangci-lint/cmd/golangci-lint" }) assert.True(t, ok) - assert.Equal(t, "v1.56.2", plugin.Version()) + assert.Equal(t, "v1.57.2", plugin.Version()) assert.Equal(t, "golangci-lint", plugin.Name()) assert.Equal(t, "github.com/golangci/golangci-lint", plugin.Module()) assert.Equal(t, "lint", plugin.Alias) diff --git a/internal/progress.go b/gbc/artifact/progress.go similarity index 97% rename from internal/progress.go rename to gbc/artifact/progress.go index 8357d43..a2ddab7 100644 --- a/internal/progress.go +++ b/gbc/artifact/progress.go @@ -1,4 +1,4 @@ -package internal +package artifact import ( "fmt" diff --git a/internal/project.go b/gbc/artifact/project.go similarity index 71% rename from internal/project.go rename to gbc/artifact/project.go index d25b7b1..56211c8 100644 --- a/internal/project.go +++ b/gbc/artifact/project.go @@ -1,10 +1,11 @@ -package internal +package artifact import ( "bufio" "errors" "fmt" "github.com/fatih/color" //nolint + "github.com/kcmvp/gob/utils" "github.com/samber/lo" //nolint "github.com/spf13/viper" //nolint "io/fs" @@ -21,6 +22,7 @@ import ( const ( pluginCfgKey = "plugins" defaultCfgKey = "_default_" + pluginCfgFile = "plugins.yaml" ) var ( @@ -31,44 +33,18 @@ type Project struct { root string module string deps []string - cfg sync.Map // store all the configuration -} - -// TestCaller returns true when caller is from _test.go and the full method name -func TestCaller() (bool, string) { - var test bool - var file string - callers := make([]uintptr, 10) - n := runtime.Callers(0, callers) - frames := runtime.CallersFrames(callers[:n]) - for { - frame, more := frames.Next() - // fmt.Printf("%s->%s:%d\n", frame.File, frame.Function, frame.Line) - test = strings.HasSuffix(frame.File, "_test.go") && strings.HasPrefix(frame.Function, project.module) - if test || !more { - items := strings.Split(frame.File, "/") - items = lo.Map(items[len(items)-2:], func(item string, _ int) string { - return strings.ReplaceAll(item, ".go", "") - }) - uniqueNames := strings.Split(frame.Function, ".") - items = append(items, uniqueNames[len(uniqueNames)-1]) - file = strings.Join(items, "_") - break - } - } - return test, file + cfgs sync.Map // store all the configuration } func (project *Project) config() *viper.Viper { - testEnv, file := TestCaller() + testEnv, file := utils.TestCaller() key := lo.If(testEnv, file).Else(defaultCfgKey) - obj, ok := project.cfg.Load(key) + obj, ok := project.cfgs.Load(key) if ok { return obj.(*viper.Viper) } v := viper.New() path := lo.If(!testEnv, project.Root()).Else(project.Target()) - v.SetConfigFile(filepath.Join(path, "gob.yaml")) if err := v.ReadInConfig(); err != nil { var configFileNotFoundError viper.ConfigFileNotFoundError @@ -76,7 +52,7 @@ func (project *Project) config() *viper.Viper { color.Yellow("Warning: can not find configuration gob.yaml") } } - project.cfg.Store(key, v) + project.cfgs.Store(key, v) return v } @@ -89,7 +65,7 @@ func (project *Project) mergeConfig(cfg map[string]any) error { } func (project *Project) HookDir() string { - if ok, _ := TestCaller(); ok { + if ok, _ := utils.TestCaller(); ok { mock := filepath.Join(CurProject().Target(), ".git", "hooks") if _, err := os.Stat(mock); err != nil { os.MkdirAll(mock, os.ModePerm) //nolint @@ -110,7 +86,7 @@ func init() { project = Project{ root: item[0], module: item[1], - cfg: sync.Map{}, + cfgs: sync.Map{}, } cmd = exec.Command("go", "list", "-f", "{{if not .Standard}}{{.ImportPath}}{{end}}", "-deps", "./...") output, err = cmd.Output() @@ -145,7 +121,7 @@ func (project *Project) Module() string { func (project *Project) Target() string { target := filepath.Join(project.Root(), "target") - if test, method := TestCaller(); test { + if test, method := utils.TestCaller(); test { target = filepath.Join(target, method) } if _, err := os.Stat(target); err != nil { @@ -205,12 +181,13 @@ func (project *Project) MainFiles() []string { } func (project *Project) Plugins() []Plugin { - if v := project.config().Get(pluginCfgKey); v != nil { + viper := project.config() + if v := viper.Get(pluginCfgKey); v != nil { plugins := v.(map[string]any) return lo.MapToSlice(plugins, func(key string, _ any) Plugin { var plugin Plugin key = fmt.Sprintf("%s.%s", pluginCfgKey, key) - if err := project.config().UnmarshalKey(key, &plugin); err != nil { + if err := viper.UnmarshalKey(key, &plugin); err != nil { color.Yellow("failed to parse plugin %s: %s", key, err.Error()) } if err := plugin.init(); err != nil { @@ -233,7 +210,7 @@ func (project *Project) SetupPlugin(plugin Plugin) { return fmt.Sprintf("%s.%s.%s", pluginCfgKey, plugin.Name(), key), value }) if err := project.mergeConfig(values); err != nil { - color.Red("faialed to setup plugin %s", err.Error()) + color.Red("failed to setup plugin %s", err.Error()) return } _ = project.config().ReadInConfig() @@ -247,8 +224,8 @@ func (project *Project) isSetup(plugin Plugin) bool { return project.config().Get(fmt.Sprintf("plugins.%s.url", plugin.name)) != nil } -func (project *Project) Validate() { - project.SetupHooks(false) +func (project *Project) Validate() error { + return project.SetupHooks(false) } func InGit() bool { @@ -256,30 +233,14 @@ func InGit() bool { return err == nil } -var unknownVersion = "unknown" +var latestHash = []string{`log`, `-1`, `--abbrev-commit`, `--date=format-local:%Y-%m-%d %H:%M`, `--format=%h(%ad)`} func Version() string { - if output, err := exec.Command("git", "rev-parse", "HEAD").CombinedOutput(); err == nil { - hash := strings.ReplaceAll(string(output), "\n", "") - output, err = exec.Command("git", "describe", "--tag", hash).CombinedOutput() - if err != nil { - return unknownVersion - } - tag := strings.ReplaceAll(string(output), "\n", "") - output, _ = exec.Command("git", "status", "--short").CombinedOutput() - if lo.ContainsBy(strings.Split(string(output), "\n"), func(line string) bool { - line = strings.TrimSpace(strings.ToUpper(line)) - return strings.HasPrefix(line, "M ") || - strings.HasSuffix(line, "D ") || - strings.HasSuffix(line, "?? ") - }) { - return fmt.Sprintf("%s@stage", tag) - } - if output, err = exec.Command("git", "log", "--format=%ci -n 1", hash).CombinedOutput(); err == nil { - return fmt.Sprintf("%s@%s", tag, strings.ReplaceAll(string(output), "\n", "")) - } + version := "unknown" + if output, err := exec.Command("git", latestHash...).CombinedOutput(); err == nil { + version = strings.Trim(string(output), "\n") } - return unknownVersion + return version } func temporaryGoPath() string { @@ -288,7 +249,7 @@ func temporaryGoPath() string { } func GoPath() string { - if ok, method := TestCaller(); ok { + if ok, method := utils.TestCaller(); ok { dir := filepath.Join(os.TempDir(), method) _ = os.MkdirAll(dir, os.ModePerm) //nolint return dir diff --git a/internal/project_test.go b/gbc/artifact/project_test.go similarity index 83% rename from internal/project_test.go rename to gbc/artifact/project_test.go index b2e8f0c..78fb3a3 100644 --- a/internal/project_test.go +++ b/gbc/artifact/project_test.go @@ -1,7 +1,8 @@ -package internal +package artifact import ( "fmt" + "github.com/kcmvp/gob/utils" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -21,8 +22,8 @@ type ProjectTestSuite struct { } func (suite *ProjectTestSuite) BeforeTest(_, testName string) { - s, _ := os.Open(filepath.Join(CurProject().Root(), "testdata", "gob.yaml")) - root := filepath.Join(CurProject().Root(), "target", fmt.Sprintf("internal_project_test_%s", testName)) + s, _ := os.Open(filepath.Join(CurProject().Root(), "gbc", "testdata", "gob.yaml")) + root := filepath.Join(CurProject().Root(), "target", fmt.Sprintf("artifact_ProjectTestSuite_%s", testName)) os.MkdirAll(root, os.ModePerm) t, _ := os.Create(filepath.Join(root, "gob.yaml")) io.Copy(t, s) @@ -31,7 +32,9 @@ func (suite *ProjectTestSuite) BeforeTest(_, testName string) { } func (suite *ProjectTestSuite) TearDownSuite() { - TearDownSuite("internal_project_test_") + _, method := utils.TestCaller() + prefix := strings.TrimRight(method, "TearDownSuite") + TearDownSuite(prefix) } func TestProjectSuite(t *testing.T) { @@ -114,7 +117,7 @@ func (suite *ProjectTestSuite) TestValidate() { func (suite *ProjectTestSuite) TestMainFiles() { mainFiles := CurProject().MainFiles() assert.Equal(suite.T(), 1, len(mainFiles)) - assert.True(suite.T(), lo.Contains(mainFiles, filepath.Join(CurProject().Root(), "gob.go"))) + assert.True(suite.T(), lo.Contains(mainFiles, filepath.Join(CurProject().Root(), "gbc", "gbc.go"))) } func (suite *ProjectTestSuite) TestVersion() { @@ -122,14 +125,15 @@ func (suite *ProjectTestSuite) TestVersion() { } func (suite *ProjectTestSuite) Test_Callee() { - test, method := TestCaller() + test, method := utils.TestCaller() assert.True(suite.T(), test) - assert.Equal(suite.T(), "internal_project_test_Test_Callee", method) + assert.Equal(suite.T(), "artifact_ProjectTestSuite_Test_Callee", method) } func (suite *ProjectTestSuite) TestHookDir() { hookDir := CurProject().HookDir() - assert.True(suite.T(), strings.HasSuffix(hookDir, "internal_project_test_TestHookDir/.git/hooks")) + _, dir := utils.TestCaller() + assert.True(suite.T(), strings.Contains(hookDir, dir)) _, err := os.Stat(hookDir) assert.NoError(suite.T(), err) } @@ -137,8 +141,10 @@ func (suite *ProjectTestSuite) TestHookDir() { func (suite *ProjectTestSuite) TestSetupPlugin() { plugin, _ := NewPlugin(v6) project.SetupPlugin(plugin) - entry, err := os.ReadDir(GoPath()) - assert.True(suite.T(), strings.HasSuffix(GoPath(), "project_test_TestSetupPlugin")) + gopath := GoPath() + entry, err := os.ReadDir(gopath) + _, suffix := utils.TestCaller() + assert.True(suite.T(), strings.HasSuffix(gopath, suffix)) assert.NoErrorf(suite.T(), err, "GOPATH should be created") assert.True(suite.T(), len(entry) == 1, "plugin should be installed to GOPATH") } diff --git a/cmd/action.go b/gbc/cmd/build_action.go similarity index 67% rename from cmd/action.go rename to gbc/cmd/build_action.go index ec42e56..dfda14d 100644 --- a/cmd/action.go +++ b/gbc/cmd/build_action.go @@ -3,30 +3,33 @@ package cmd import ( "errors" "fmt" + "github.com/kcmvp/gob/gbc/artifact" //nolint "os" "os/exec" "path/filepath" "strings" "github.com/fatih/color" - "github.com/kcmvp/gob/internal" "github.com/samber/lo" //nolint "github.com/spf13/cobra" ) type ( Execution func(cmd *cobra.Command, args ...string) error - Action lo.Tuple2[string, Execution] + Action lo.Tuple3[string, Execution, string] ) func buildActions() []Action { return []Action{ - {A: "build", B: buildAction}, - {A: "clean", B: cleanAction}, - {A: "test", B: testAction}, + {A: "build", B: buildAction, C: "build all main methods which in main package and name the binary as file name"}, + {A: "clean", B: cleanAction, C: "clean project target folder"}, + {A: "test", B: testAction, C: "test the project and generate coverage report in target folder"}, {A: "after_test", B: coverReport}, } } +func (a Action) String() string { + return fmt.Sprintf("%s: %s", a.A, a.A) +} func setupActions() []Action { return []Action{ @@ -54,7 +57,7 @@ func afterExecution(cmd *cobra.Command, arg string) { func execute(cmd *cobra.Command, arg string) error { beforeExecution(cmd, arg) //nolint var err error - if plugin, ok := lo.Find(internal.CurProject().Plugins(), func(plugin internal.Plugin) bool { + if plugin, ok := lo.Find(artifact.CurProject().Plugins(), func(plugin artifact.Plugin) bool { return plugin.Alias == arg }); ok { err = plugin.Execute() @@ -78,7 +81,7 @@ func validBuilderArgs() []string { }), func(action Action, _ int) string { return action.A }) - lo.ForEach(internal.CurProject().Plugins(), func(item internal.Plugin, _ int) { + lo.ForEach(artifact.CurProject().Plugins(), func(item artifact.Plugin, _ int) { if !lo.Contains(builtIn, item.Alias) { builtIn = append(builtIn, item.Alias) } @@ -88,17 +91,17 @@ func validBuilderArgs() []string { func buildAction(_ *cobra.Command, _ ...string) error { bm := map[string]string{} - for _, mainFile := range internal.CurProject().MainFiles() { + for _, mainFile := range artifact.CurProject().MainFiles() { binary := strings.TrimSuffix(filepath.Base(mainFile), ".go") if f, exists := bm[binary]; exists { return fmt.Errorf("file %s has already built as %s, please rename %s", f, binary, mainFile) } - output := filepath.Join(internal.CurProject().Target(), binary) - versionFlag := fmt.Sprintf("-X '%s/infra.buildVersion=%s'", internal.CurProject().Module(), internal.Version()) - if _, err := exec.Command("go", "build", "-ldflags", versionFlag, "-o", output, mainFile).CombinedOutput(); err != nil { //nolint - return errors.New(color.RedString("failed to build the project: %s", err.Error())) + output := filepath.Join(artifact.CurProject().Target(), binary) + versionFlag := fmt.Sprintf("-X 'main.buildVersion=%s'", artifact.Version()) + if data, err := exec.Command("go", "build", "-o", output, "-ldflags", versionFlag, mainFile).CombinedOutput(); err != nil { //nolint + return errors.New(color.RedString(string(data))) } - fmt.Printf("Build %s to %s successfully\n", mainFile, output) + fmt.Printf("Build project successfully %s\n", output) bm[binary] = output } if len(bm) == 0 { @@ -109,40 +112,39 @@ func buildAction(_ *cobra.Command, _ ...string) error { func cleanAction(_ *cobra.Command, _ ...string) error { // clean target folder - os.RemoveAll(internal.CurProject().Target()) - os.Mkdir(internal.CurProject().Target(), os.ModePerm) //nolint errcheck + os.RemoveAll(artifact.CurProject().Target()) + os.Mkdir(artifact.CurProject().Target(), os.ModePerm) //nolint errcheck fmt.Println("Clean target folder successfully !") // clean cache args := []string{"clean"} _, err := exec.Command("go", args...).CombinedOutput() - if err == nil { - fmt.Println("Clean cache successfully !") + if err != nil { + color.Red("failed to clean the project cache %s", err.Error()) } return err } -func testAction(_ *cobra.Command, args ...string) error { - coverProfile := fmt.Sprintf("-coverprofile=%s/cover.out", internal.CurProject().Target()) +func testAction(_ *cobra.Command, _ ...string) error { + coverProfile := fmt.Sprintf("-coverprofile=%s/cover.out", artifact.CurProject().Target()) testCmd := exec.Command("go", []string{"test", "-v", coverProfile, "./..."}...) //nolint - return internal.StreamCmdOutput(testCmd, "test") + return artifact.StreamCmdOutput(testCmd, "test") } func coverReport(_ *cobra.Command, _ ...string) error { - target := internal.CurProject().Target() + target := artifact.CurProject().Target() _, err := os.Stat(filepath.Join(target, "cover.out")) if err == nil { if _, err = exec.Command("go", []string{"tool", "cover", fmt.Sprintf("-html=%s/cover.out", target), fmt.Sprintf("-o=%s/cover.html", target)}...).CombinedOutput(); err == nil { //nolint fmt.Printf("Coverage report is generated at %s/cover.html \n", target) return nil - } else { - return fmt.Errorf(color.RedString("Failed to generate coverage report %s", err.Error())) } + return fmt.Errorf(color.RedString("Failed to generate coverage report %s", err.Error())) } return fmt.Errorf(color.RedString("Failed to generate coverage report %s", err.Error())) } func setupVersion(_ *cobra.Command, _ ...string) error { - infra := filepath.Join(internal.CurProject().Root(), "infra") + infra := filepath.Join(artifact.CurProject().Root(), "infra") if _, err := os.Stat(infra); err != nil { os.Mkdir(infra, 0700) // nolint } diff --git a/cmd/action_test.go b/gbc/cmd/build_action_test.go similarity index 69% rename from cmd/action_test.go rename to gbc/cmd/build_action_test.go index 0e1212c..6e9e017 100644 --- a/cmd/action_test.go +++ b/gbc/cmd/build_action_test.go @@ -2,12 +2,12 @@ package cmd import ( "github.com/fatih/color" - "github.com/kcmvp/gob/internal" + "github.com/kcmvp/gob/gbc/artifact" + "github.com/kcmvp/gob/utils" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "io" - "io/fs" "os" "path/filepath" "strings" @@ -22,29 +22,15 @@ func TestActionSuite(t *testing.T) { suite.Run(t, &ActionTestSuite{}) } -func TearDownSuite(prefix string) { - filepath.WalkDir(os.TempDir(), func(path string, d fs.DirEntry, err error) error { - if d.IsDir() && strings.HasPrefix(d.Name(), prefix) { - os.RemoveAll(path) - } - return nil - }) - filepath.WalkDir(filepath.Join(internal.CurProject().Root(), "target"), func(path string, d fs.DirEntry, err error) error { - if d.IsDir() && strings.HasPrefix(d.Name(), prefix) { - os.RemoveAll(path) - } - return nil - }) -} - func (suite *ActionTestSuite) TearDownSuite() { - TearDownSuite("cmd_action_test_") + _, method := utils.TestCaller() + TearDownSuite(strings.TrimRight(method, "TearDownSuite")) } func (suite *ActionTestSuite) TestActionBuild() { err := buildAction(nil) assert.NoError(suite.T(), err) - binary := filepath.Join(internal.CurProject().Target(), lo.If(internal.Windows(), "gob.exe").Else("gob")) + binary := filepath.Join(artifact.CurProject().Target(), lo.If(artifact.Windows(), "gbc.exe").Else("gbc")) _, err = os.Stat(binary) assert.NoError(suite.T(), err) } @@ -69,10 +55,10 @@ func (suite *ActionTestSuite) TestBuiltInActions() { } func (suite *ActionTestSuite) TestExecute() { - _ = os.Chdir(internal.CurProject().Root()) - err := execute(builderCmd, "build") + _ = os.Chdir(artifact.CurProject().Root()) + err := execute(rootCmd, "build") assert.NoError(suite.T(), err) - err = execute(builderCmd, "build1") + err = execute(rootCmd, "build1") assert.Error(suite.T(), err) assert.Contains(suite.T(), err.Error(), color.RedString("can not find command %s", "build1")) } @@ -80,15 +66,15 @@ func (suite *ActionTestSuite) TestExecute() { func (suite *ActionTestSuite) TestCoverage() { err := coverReport(nil, "") assert.Errorf(suite.T(), err, "no cover.out") - s, _ := os.Open(filepath.Join(internal.CurProject().Root(), "testdata", "cover.out")) - t, _ := os.Create(filepath.Join(internal.CurProject().Target(), "cover.out")) + s, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "testdata", "cover.out")) + t, _ := os.Create(filepath.Join(artifact.CurProject().Target(), "cover.out")) io.Copy(t, s) s.Close() t.Close() - _, err = os.Stat(filepath.Join(internal.CurProject().Target(), "cover.out")) + _, err = os.Stat(filepath.Join(artifact.CurProject().Target(), "cover.out")) err = coverReport(nil, "") assert.NoError(suite.T(), err, "should generate test cover report successfully") - _, err = os.Stat(filepath.Join(internal.CurProject().Target(), "cover.html")) + _, err = os.Stat(filepath.Join(artifact.CurProject().Target(), "cover.html")) assert.NoError(suite.T(), err) } @@ -100,7 +86,7 @@ func (suite *ActionTestSuite) TestSetupActions() { func (suite *ActionTestSuite) TestSetupVersion() { err := setupVersion(nil, "") assert.NoError(suite.T(), err) - version := filepath.Join(internal.CurProject().Root(), "infra", "version.go") + version := filepath.Join(artifact.CurProject().Root(), "infra", "version.go") os.Remove(version) _, err = os.Stat(version) assert.Error(suite.T(), err) @@ -111,7 +97,7 @@ func (suite *ActionTestSuite) TestSetupVersion() { } func (suite *ActionTestSuite) TestBuildAndClean() { - target := internal.CurProject().Target() + target := artifact.CurProject().Target() err := buildAction(nil, "") assert.NoError(suite.T(), err) entry, err := os.ReadDir(target) diff --git a/cmd/deps.go b/gbc/cmd/deps.go similarity index 91% rename from cmd/deps.go rename to gbc/cmd/deps.go index 58acc04..cedf58e 100644 --- a/cmd/deps.go +++ b/gbc/cmd/deps.go @@ -8,16 +8,17 @@ import ( "path/filepath" "strings" + "github.com/kcmvp/gob/gbc/artifact" //nolint + "github.com/fatih/color" - "github.com/kcmvp/gob/internal" "github.com/samber/lo" "github.com/spf13/cobra" "github.com/xlab/treeprint" ) var ( - bold = color.New(color.Bold) - yellow = color.New(color.FgYellow, color.Bold) + green = color.New(color.FgGreen) + yellow = color.New(color.FgYellow) ) // parseMod return a tuple which the fourth element is the indicator of direct or indirect reference @@ -47,7 +48,7 @@ func parseMod(mod *os.File) (string, string, []*lo.Tuple4[string, string, string // dependencyTree build dependency tree of the project, an empty tree returns when runs into error func dependencyTree() (treeprint.Tree, error) { - mod, err := os.Open(filepath.Join(internal.CurProject().Root(), "go.mod")) + mod, err := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod")) if err != nil { return nil, fmt.Errorf(color.RedString(err.Error())) } @@ -63,7 +64,7 @@ func dependencyTree() (treeprint.Tree, error) { return nil, fmt.Errorf(err.Error()) } tree := treeprint.New() - tree.SetValue(bold.Sprintf("%s", module)) + tree.SetValue(module) direct := lo.FilterMap(dependencies, func(item *lo.Tuple4[string, string, string, int], _ int) (string, bool) { return fmt.Sprintf("%s@latest", item.A), item.D == 1 }) @@ -126,14 +127,14 @@ and indicate available updates which take an green * indicator`, yellow.Println("No dependencies !") return nil } - bold.Print("\nDependencies of the projects:\n") - yellow.Print("* indicates new versions available\n") - fmt.Println("") + green.Println("Dependencies of the projects:") fmt.Println(tree.String()) return nil }, } func init() { - builderCmd.AddCommand(depCmd) + depCmd.SetUsageTemplate(usageTemplate()) + depCmd.SetErrPrefix(color.RedString("Error:")) + rootCmd.AddCommand(depCmd) } diff --git a/cmd/deps_test.go b/gbc/cmd/deps_test.go similarity index 70% rename from cmd/deps_test.go rename to gbc/cmd/deps_test.go index 3fb816c..a9edbe0 100644 --- a/cmd/deps_test.go +++ b/gbc/cmd/deps_test.go @@ -2,7 +2,7 @@ package cmd import ( "fmt" - "github.com/kcmvp/gob/internal" + "github.com/kcmvp/gob/gbc/artifact" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/xlab/treeprint" @@ -13,20 +13,20 @@ import ( ) func TestParseMod(t *testing.T) { - os.Chdir(internal.CurProject().Root()) - mod, _ := os.Open(filepath.Join(internal.CurProject().Root(), "go.mod")) + os.Chdir(artifact.CurProject().Root()) + mod, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod")) m, _, deps, err := parseMod(mod) assert.NoError(t, err) assert.Equal(t, m, "github.com/kcmvp/gob") - assert.Equal(t, 10, len(lo.Filter(deps, func(item *lo.Tuple4[string, string, string, int], _ int) bool { + assert.Equal(t, 15, len(lo.Filter(deps, func(item *lo.Tuple4[string, string, string, int], _ int) bool { return item.D == 1 }))) - assert.Equal(t, 43, len(deps)) + assert.Equal(t, 48, len(deps)) } func TestDependency(t *testing.T) { - os.Chdir(internal.CurProject().Root()) - mod, _ := os.Open(filepath.Join(internal.CurProject().Root(), "go.mod")) + os.Chdir(artifact.CurProject().Root()) + mod, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod")) _, _, deps, _ := parseMod(mod) tree, err := dependencyTree() assert.NoError(t, err) diff --git a/cmd/exec.go b/gbc/cmd/exec.go similarity index 76% rename from cmd/exec.go rename to gbc/cmd/exec.go index a391644..f49c6cd 100644 --- a/cmd/exec.go +++ b/gbc/cmd/exec.go @@ -7,13 +7,14 @@ import ( "bufio" "errors" "fmt" - "github.com/fatih/color" - "github.com/kcmvp/gob/internal" - "github.com/samber/lo" - "github.com/spf13/cobra" "os" "regexp" "strings" + + "github.com/fatih/color" + "github.com/kcmvp/gob/gbc/artifact" + "github.com/samber/lo" + "github.com/spf13/cobra" ) const pushDeleteHash = "0000000000000000000000000000000000000000" @@ -36,13 +37,13 @@ var validateCommitMsg Execution = func(_ *cobra.Command, args ...string) error { } func execValidArgs() []string { - return lo.Map(internal.CurProject().Executions(), func(exec internal.Execution, _ int) string { + return lo.Map(artifact.CurProject().Executions(), func(exec artifact.Execution, _ int) string { return exec.CmdKey }) } func pushDelete(cmd string) bool { - if cmd == internal.PrePush { + if cmd == artifact.PrePush { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { line := scanner.Text() @@ -54,22 +55,21 @@ func pushDelete(cmd string) bool { return false } -func do(execution internal.Execution, cmd *cobra.Command, args ...string) error { - if execution.CmdKey == internal.CommitMsg { +func do(execution artifact.Execution, cmd *cobra.Command, args ...string) error { + if execution.CmdKey == artifact.CommitMsg { args = append(args, execution.Actions...) return validateCommitMsg(nil, args...) - } else { - if pushDelete(execution.CmdKey) { - return nil - } - // process hook - for _, arg := range execution.Actions { - if err := execute(cmd, arg); err != nil { - return errors.New(color.RedString("failed to %s the project \n", arg)) - } - } + } + if pushDelete(execution.CmdKey) { return nil } + // process hook + for _, arg := range execution.Actions { + if err := execute(cmd, arg); err != nil { + return errors.New(color.RedString("failed to %s the project \n", arg)) + } + } + return nil } // execCmd represents the exec command @@ -90,7 +90,7 @@ var execCmd = &cobra.Command{ return nil }, RunE: func(cmd *cobra.Command, args []string) error { - execution, _ := lo.Find(internal.CurProject().Executions(), func(exec internal.Execution) bool { + execution, _ := lo.Find(artifact.CurProject().Executions(), func(exec artifact.Execution) bool { return exec.CmdKey == args[0] }) return do(execution, cmd, args...) @@ -98,5 +98,5 @@ var execCmd = &cobra.Command{ } func init() { - builderCmd.AddCommand(execCmd) + rootCmd.AddCommand(execCmd) } diff --git a/cmd/exec_test.go b/gbc/cmd/exec_test.go similarity index 77% rename from cmd/exec_test.go rename to gbc/cmd/exec_test.go index bb8b24f..6166d6c 100644 --- a/cmd/exec_test.go +++ b/gbc/cmd/exec_test.go @@ -2,13 +2,15 @@ package cmd import ( "fmt" - "github.com/kcmvp/gob/internal" + "github.com/kcmvp/gob/gbc/artifact" + "github.com/kcmvp/gob/utils" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "io" "os" "path/filepath" + "strings" "testing" ) @@ -17,8 +19,8 @@ type ExecTestSuite struct { } func (suite *ExecTestSuite) BeforeTest(_, testName string) { - s, _ := os.Open(filepath.Join(internal.CurProject().Root(), "testdata", "gob.yaml")) - root := filepath.Join(internal.CurProject().Root(), "target", fmt.Sprintf("cmd_exec_test_%s", testName)) + s, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "gbc", "testdata", "gob.yaml")) + root := filepath.Join(artifact.CurProject().Root(), "target", fmt.Sprintf("cmd_ExecTestSuite_%s", testName)) os.MkdirAll(root, os.ModePerm) t, _ := os.Create(filepath.Join(root, "gob.yaml")) io.Copy(t, s) @@ -27,7 +29,8 @@ func (suite *ExecTestSuite) BeforeTest(_, testName string) { } func (suite *ExecTestSuite) TearDownSuite() { - TearDownSuite("cmd_exec_test_") + _, method := utils.TestCaller() + TearDownSuite(strings.TrimRight(method, "TearDownSuite")) } func TestExecSuite(t *testing.T) { @@ -36,7 +39,7 @@ func TestExecSuite(t *testing.T) { func (suite *ExecTestSuite) TestActions() { assert.Equal(suite.T(), 3, len(execValidArgs())) - assert.True(suite.T(), lo.Every(execValidArgs(), []string{internal.CommitMsg, internal.PreCommit, internal.PreCommit})) + assert.True(suite.T(), lo.Every(execValidArgs(), []string{artifact.CommitMsg, artifact.PreCommit, artifact.PreCommit})) } func (suite *ExecTestSuite) TestCmdArgs() { @@ -47,9 +50,9 @@ func (suite *ExecTestSuite) TestCmdArgs() { }{ {"no args", []string{}, true}, {"no match", []string{lo.RandomString(10, lo.LettersCharset)}, true}, - {"first match", []string{internal.CommitMsg, lo.RandomString(10, lo.LettersCharset)}, false}, + {"first match", []string{artifact.CommitMsg, lo.RandomString(10, lo.LettersCharset)}, false}, {"second match", []string{lo.RandomString(10, lo.LettersCharset), "msghook"}, true}, - {"more than 3", []string{internal.CommitMsg, lo.RandomString(10, lo.AlphanumericCharset), + {"more than 3", []string{artifact.CommitMsg, lo.RandomString(10, lo.AlphanumericCharset), lo.RandomString(10, lo.AlphanumericCharset), lo.RandomString(10, lo.AlphanumericCharset)}, true, @@ -79,7 +82,7 @@ func (suite *ExecTestSuite) TestValidateCommitMsg() { } for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { - err := do(internal.Execution{CmdKey: internal.CommitMsg}, nil, test.args...) + err := do(artifact.Execution{CmdKey: artifact.CommitMsg}, nil, test.args...) assert.True(t, test.wantErr == (err != nil)) }) } @@ -94,13 +97,13 @@ func (suite *ExecTestSuite) TestPushDelete() { }{ { name: "valid", - cmdKey: internal.PrePush, + cmdKey: artifact.PrePush, msg: fmt.Sprintf("delete %s", pushDeleteHash), result: true, }, { name: "invalid", - cmdKey: internal.PrePush, + cmdKey: artifact.PrePush, msg: pushDeleteHash, result: false, }, @@ -123,7 +126,7 @@ func (suite *ExecTestSuite) TestPushDelete() { defer writer.Close() io.WriteString(writer, test.msg) }() - rs := pushDelete(internal.PrePush) + rs := pushDelete(artifact.PrePush) assert.Equal(t, test.result, rs) }) } @@ -132,22 +135,22 @@ func (suite *ExecTestSuite) TestPushDelete() { func (suite *ExecTestSuite) TestDo() { tests := []struct { name string - execution internal.Execution + execution artifact.Execution wantErr bool }{ { name: "pre-push", - execution: internal.Execution{ - CmdKey: internal.PrePush, + execution: artifact.Execution{ + CmdKey: artifact.PrePush, Actions: []string{"build"}, }, wantErr: false, }, { name: "pre-commit", - execution: internal.Execution{ - CmdKey: internal.PreCommit, + execution: artifact.Execution{ + CmdKey: artifact.PreCommit, Actions: []string{"lint"}, }, wantErr: true, diff --git a/cmd/initializer.go b/gbc/cmd/initialize.go similarity index 64% rename from cmd/initializer.go rename to gbc/cmd/initialize.go index c311d78..300b801 100644 --- a/cmd/initializer.go +++ b/gbc/cmd/initialize.go @@ -4,28 +4,29 @@ Copyright © 2023 kcmvp package cmd import ( - _ "embed" "encoding/json" "fmt" + "os" + "path/filepath" + "github.com/fatih/color" - "github.com/kcmvp/gob/internal" + "github.com/kcmvp/gob/gbc/artifact" + "github.com/kcmvp/gob/utils" "github.com/samber/lo" "github.com/spf13/cobra" "github.com/tidwall/gjson" - "os" - "path/filepath" ) -func builtinPlugins() []internal.Plugin { +func builtinPlugins() []artifact.Plugin { var data []byte var err error - test, _ := internal.TestCaller() + test, _ := utils.TestCaller() if !test { data, err = resources.ReadFile("resources/config.json") } else { - data, err = os.ReadFile(filepath.Join(internal.CurProject().Root(), "testdata", "config.json")) + data, err = os.ReadFile(filepath.Join(artifact.CurProject().Root(), "gbc", "testdata", "config.json")) } - var plugins []internal.Plugin + var plugins []artifact.Plugin if err == nil { v := gjson.GetBytes(data, "plugins") if err = json.Unmarshal([]byte(v.Raw), &plugins); err != nil { @@ -35,14 +36,14 @@ func builtinPlugins() []internal.Plugin { return plugins } -func initializerFunc(_ *cobra.Command, _ []string) { +func initialize(_ *cobra.Command, _ []string) { fmt.Println("Initialize configuration ......") - lo.ForEach(builtinPlugins(), func(plugin internal.Plugin, index int) { - internal.CurProject().SetupPlugin(plugin) + lo.ForEach(builtinPlugins(), func(plugin artifact.Plugin, _ int) { + artifact.CurProject().SetupPlugin(plugin) if len(plugin.Config) > 0 { - if _, err := os.Stat(filepath.Join(internal.CurProject().Root(), plugin.Config)); err != nil { + if _, err := os.Stat(filepath.Join(artifact.CurProject().Root(), plugin.Config)); err != nil { if data, err := resources.ReadFile(filepath.Join(resourceDir, plugin.Config)); err == nil { - if err = os.WriteFile(filepath.Join(internal.CurProject().Root(), plugin.Config), data, os.ModePerm); err != nil { + if err = os.WriteFile(filepath.Join(artifact.CurProject().Root(), plugin.Config), data, os.ModePerm); err != nil { color.Red("failed to create configuration %s", plugin.Config) } } else { @@ -51,7 +52,7 @@ func initializerFunc(_ *cobra.Command, _ []string) { } } }) - internal.CurProject().SetupHooks(true) + artifact.CurProject().SetupHooks(true) } // initializerCmd represents the init command @@ -59,9 +60,9 @@ var initializerCmd = &cobra.Command{ Use: "init", Short: "Initialize project builder configuration", Long: `Initialize project builder configuration`, - Run: initializerFunc, + Run: initialize, } func init() { - builderCmd.AddCommand(initializerCmd) + rootCmd.AddCommand(initializerCmd) } diff --git a/cmd/initializer_test.go b/gbc/cmd/initialize_test.go similarity index 56% rename from cmd/initializer_test.go rename to gbc/cmd/initialize_test.go index 6ca9eb4..d313116 100644 --- a/cmd/initializer_test.go +++ b/gbc/cmd/initialize_test.go @@ -1,7 +1,8 @@ package cmd import ( - "github.com/kcmvp/gob/internal" + "github.com/kcmvp/gob/gbc/artifact" + "github.com/kcmvp/gob/utils" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -14,22 +15,23 @@ import ( const golangCiLinter = "github.com/golangci/golangci-lint/cmd/golangci-lint" const testsum = "gotest.tools/gotestsum" -type InitializationTestSuite struct { +type InitializeTestSuite struct { suite.Suite } -func TestInitializationTestSuit(t *testing.T) { - suite.Run(t, &InitializationTestSuite{}) +func TestInitializeTestSuit(t *testing.T) { + suite.Run(t, &InitializeTestSuite{}) } -func (suite *InitializationTestSuite) TearDownSuite() { - TearDownSuite("cmd_initializer_test_") +func (suite *InitializeTestSuite) TearDownSuite() { + _, method := utils.TestCaller() + TearDownSuite(strings.TrimRight(method, "TearDownSuite")) } -func (suite *InitializationTestSuite) TestBuiltInPlugins() { +func (suite *InitializeTestSuite) TestBuiltInPlugins() { plugins := builtinPlugins() assert.Equal(suite.T(), 2, len(plugins)) - plugin, ok := lo.Find(plugins, func(plugin internal.Plugin) bool { + plugin, ok := lo.Find(plugins, func(plugin artifact.Plugin) bool { return plugin.Module() == "github.com/golangci/golangci-lint" }) assert.True(suite.T(), ok) @@ -37,22 +39,22 @@ func (suite *InitializationTestSuite) TestBuiltInPlugins() { assert.Equal(suite.T(), "lint", plugin.Alias) } -func (suite *InitializationTestSuite) TestInitializerFunc() { - gopath := internal.GoPath() - target := internal.CurProject().Target() - initializerFunc(nil, nil) - plugins := internal.CurProject().Plugins() +func (suite *InitializeTestSuite) TestInitialization() { + gopath := artifact.GoPath() + target := artifact.CurProject().Target() + initialize(nil, nil) + plugins := artifact.CurProject().Plugins() assert.Equal(suite.T(), 2, len(plugins)) - _, ok := lo.Find(plugins, func(plugin internal.Plugin) bool { + _, ok := lo.Find(plugins, func(plugin artifact.Plugin) bool { return strings.HasPrefix(plugin.Url, golangCiLinter) }) assert.True(suite.T(), ok) - _, ok = lo.Find(plugins, func(plugin internal.Plugin) bool { + _, ok = lo.Find(plugins, func(plugin artifact.Plugin) bool { return strings.HasPrefix(plugin.Url, testsum) }) assert.True(suite.T(), ok) - lo.ForEach(plugins, func(plugin internal.Plugin, _ int) { + lo.ForEach(plugins, func(plugin artifact.Plugin, _ int) { _, err := os.Stat(filepath.Join(gopath, plugin.Binary())) assert.NoError(suite.T(), err) }) diff --git a/cmd/plugin.go b/gbc/cmd/plugin.go similarity index 64% rename from cmd/plugin.go rename to gbc/cmd/plugin.go index 0b5e69c..e642e91 100644 --- a/cmd/plugin.go +++ b/gbc/cmd/plugin.go @@ -6,14 +6,14 @@ package cmd import ( "errors" "fmt" + "strings" + "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" - "github.com/kcmvp/gob/internal" + "github.com/kcmvp/gob/gbc/artifact" "github.com/samber/lo" "github.com/spf13/cobra" - //nolint - "strings" ) // alias is the tool alias @@ -24,21 +24,21 @@ var command string // Install the specified tool as gob plugin func install(_ *cobra.Command, args ...string) error { - plugin, err := internal.NewPlugin(args[0]) + plugin, err := artifact.NewPlugin(args[0]) if err != nil { return err } plugin.Alias = alias plugin.Alias = command - internal.CurProject().SetupPlugin(plugin) + artifact.CurProject().SetupPlugin(plugin) return nil } func list(_ *cobra.Command, _ ...string) error { - plugins := internal.CurProject().Plugins() + plugins := artifact.CurProject().Plugins() ct := table.Table{} ct.SetTitle("Installed Plugins") - ct.AppendRow(table.Row{"Command", "Alias", "Method", "URL"}) + ct.AppendRow(table.Row{"Command", "Plugin"}) style := table.StyleDefault style.Options.DrawBorder = true style.Options.SeparateRows = true @@ -46,8 +46,8 @@ func list(_ *cobra.Command, _ ...string) error { style.Title.Align = text.AlignCenter style.HTML.CSSClass = table.DefaultHTMLCSSClass ct.SetStyle(style) - rows := lo.Map(plugins, func(plugin internal.Plugin, index int) table.Row { - return table.Row{plugin.Name(), plugin.Alias, plugin.Args, plugin.Url} + rows := lo.Map(plugins, func(plugin artifact.Plugin, index int) table.Row { + return table.Row{plugin.Alias, plugin.Url} }) ct.AppendRows(rows) fmt.Println(ct.Render()) @@ -58,10 +58,12 @@ var pluginCmdAction = []Action{ { A: "list", B: list, + C: "list all setup plugins", }, { A: "install", B: install, + C: "install a plugin. `gbc plugin install `", }, } @@ -69,17 +71,18 @@ var pluginCmdAction = []Action{ var pluginCmd = &cobra.Command{ Use: "plugin", Short: "Install a new plugin or list installed plugins", - Long: `Install a new plugin or list installed plugins + Long: color.BlueString(` +Install a new plugin or list installed plugins you can update the plugin by edit gob.yaml directly -`, +`), Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { + if err := MinimumNArgs(1)(cmd, args); err != nil { return err } if !lo.Contains(lo.Map(pluginCmdAction, func(item Action, _ int) string { return item.A }), args[0]) { - return fmt.Errorf("invalid argument %s", args[0]) + return errors.New(color.RedString("invalid argument %s", args[0])) } if "install" == args[0] && (len(args) < 2 || strings.TrimSpace(args[1]) == "") { return errors.New(color.RedString("miss the plugin url")) @@ -97,10 +100,24 @@ you can update the plugin by edit gob.yaml directly }, } +func pluginExample() string { + format := fmt.Sprintf(" %%-%ds %%s", pluginCmd.NamePadding()) + return strings.Join(lo.Map(pluginCmdAction, func(action Action, index_ int) string { + return fmt.Sprintf(format, action.A, action.C) + }), "\n") +} + func init() { // init pluginCmd - builderCmd.AddCommand(pluginCmd) - // init installPluginCmd + pluginCmd.Example = pluginExample() + pluginCmd.SetUsageTemplate(usageTemplate()) + pluginCmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error { + return lo.IfF(err != nil, func() error { + return fmt.Errorf(color.RedString(err.Error())) + }).Else(nil) + }) pluginCmd.Flags().StringVarP(&alias, "alias", "a", "", "alias of the tool") pluginCmd.Flags().StringVarP(&command, "command", "c", "", "default command of this tool") + + rootCmd.AddCommand(pluginCmd) } diff --git a/cmd/plugin_test.go b/gbc/cmd/plugin_test.go similarity index 82% rename from cmd/plugin_test.go rename to gbc/cmd/plugin_test.go index 739e4e0..cdb665d 100644 --- a/cmd/plugin_test.go +++ b/gbc/cmd/plugin_test.go @@ -1,11 +1,13 @@ package cmd import ( - "github.com/kcmvp/gob/internal" + "github.com/kcmvp/gob/gbc/artifact" + "github.com/kcmvp/gob/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "os" "path/filepath" + "strings" "testing" ) @@ -21,15 +23,15 @@ func TestPluginSuite(t *testing.T) { } func (suite *PluginTestSuit) TearDownSuite() { - - TearDownSuite("cmd_plugin_test_") + _, method := utils.TestCaller() + TearDownSuite(strings.TrimRight(method, "TearDownSuite")) } func (suite *PluginTestSuit) TestInstallPlugin() { err := install(nil, v6) assert.NoError(suite.T(), err) - plugins := internal.CurProject().Plugins() - _, err = os.Stat(filepath.Join(internal.GoPath(), plugins[0].Binary())) + plugins := artifact.CurProject().Plugins() + _, err = os.Stat(filepath.Join(artifact.GoPath(), plugins[0].Binary())) assert.NoError(suite.T(), err) assert.Equal(suite.T(), 1, len(plugins)) assert.Equal(suite.T(), "digraph", plugins[0].Name()) @@ -38,7 +40,7 @@ func (suite *PluginTestSuit) TestInstallPlugin() { err = install(nil, v7) assert.NoError(suite.T(), err) assert.Equal(suite.T(), 1, len(plugins)) - _, err = os.Stat(filepath.Join(internal.GoPath(), plugins[0].Binary())) + _, err = os.Stat(filepath.Join(artifact.GoPath(), plugins[0].Binary())) assert.NoError(suite.T(), err) } @@ -100,3 +102,8 @@ func (suite *PluginTestSuit) TestRunE() { err := pluginCmd.RunE(pluginCmd, []string{"list"}) assert.NoErrorf(suite.T(), err, "should list iinstalled plugin successfully") } + +func (suite *PluginTestSuit) TestPluginHelpTemplate() { + rootCmd.SetArgs([]string{"plugin", "--help"}) + rootCmd.Execute() +} diff --git a/cmd/resources/.golangci.yaml b/gbc/cmd/resources/.golangci.yaml similarity index 100% rename from cmd/resources/.golangci.yaml rename to gbc/cmd/resources/.golangci.yaml diff --git a/cmd/resources/config.json b/gbc/cmd/resources/config.json similarity index 100% rename from cmd/resources/config.json rename to gbc/cmd/resources/config.json diff --git a/gbc/cmd/resources/usage.tmpl b/gbc/cmd/resources/usage.tmpl new file mode 100644 index 0000000..d8ac719 --- /dev/null +++ b/gbc/cmd/resources/usage.tmpl @@ -0,0 +1,32 @@ +Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .ValidArgs) 0}} + {{.CommandPath}} [argument]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if gt (len .ValidArgs) 0}} + +Available Arguments: +{{.Example}}{{end}} +{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} + +Available Commands:{{range $cmds}}{{if (and (ne .Name "exec") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} + diff --git a/cmd/resources/version.tmpl b/gbc/cmd/resources/version.tmpl similarity index 100% rename from cmd/resources/version.tmpl rename to gbc/cmd/resources/version.tmpl diff --git a/gbc/cmd/root.go b/gbc/cmd/root.go new file mode 100644 index 0000000..997a63e --- /dev/null +++ b/gbc/cmd/root.go @@ -0,0 +1,100 @@ +// Package cmd /* +package cmd + +import ( + "context" + "embed" + "errors" + "fmt" + "github.com/fatih/color" + "github.com/kcmvp/gob/gbc/artifact" + "github.com/samber/lo" + "github.com/spf13/cobra" + "os" //nolint + "strings" //nolint + "sync" //nolint +) + +//go:embed resources/* +var resources embed.FS + +var ( + once sync.Once + template string +) + +func usageTemplate() string { + once.Do(func() { + bytes, _ := resources.ReadFile("resources/usage.tmpl") + template = color.YellowString(string(bytes)) + }) + return template +} + +const resourceDir = "resources" + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "gbc", + Short: color.GreenString(`Go project boot command line`), + Long: color.GreenString(`Go project boot command line`), + ValidArgs: validBuilderArgs(), + Args: func(cmd *cobra.Command, args []string) error { + if err := OnlyValidArgs(cmd, args); err != nil { + return err + } + return MinimumNArgs(1)(cmd, args) + }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + currentDir, _ := os.Getwd() + if artifact.CurProject().Root() != currentDir { + return fmt.Errorf(color.RedString("Please execute the command in the project root dir")) + } + return artifact.CurProject().Validate() + }, + RunE: func(cmd *cobra.Command, args []string) error { + for _, arg := range lo.Uniq(args) { + if err := execute(cmd, arg); err != nil { + return errors.New(color.RedString("%s \n", err.Error())) + } + } + return nil + }, +} + +func rootExample() string { + format := fmt.Sprintf(" %%-%ds %%s", rootCmd.NamePadding()) + builtIn := lo.Map(lo.Filter(buildActions(), func(item Action, _ int) bool { + return !strings.Contains(item.A, "_") + }), func(action Action, _ int) string { + return fmt.Sprintf(format, action.A, action.C) + }) + lo.ForEach(artifact.CurProject().Plugins(), func(plugin artifact.Plugin, _ int) { + if !lo.ContainsBy(builtIn, func(item string) bool { + return strings.HasPrefix(strings.TrimSpace(item), strings.TrimSpace(plugin.Alias)) + }) { + builtIn = append(builtIn, fmt.Sprintf(format, plugin.Alias, plugin.Description)) + } + }) + return strings.Join(builtIn, "\n") +} + +func Execute() error { + ctx := context.Background() + if err := rootCmd.ExecuteContext(ctx); err != nil { + return fmt.Errorf(color.RedString(err.Error())) + } + return nil +} + +func init() { + rootCmd.Example = rootExample() + rootCmd.SetUsageTemplate(usageTemplate()) + rootCmd.SetErrPrefix(color.RedString("Error:")) + rootCmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error { + return lo.IfF(err != nil, func() error { + return fmt.Errorf(color.RedString(err.Error())) + }).Else(nil) + }) + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/gbc/cmd/root_test.go b/gbc/cmd/root_test.go new file mode 100644 index 0000000..15aca20 --- /dev/null +++ b/gbc/cmd/root_test.go @@ -0,0 +1,199 @@ +package cmd + +import ( + "fmt" + "github.com/fatih/color" + "github.com/kcmvp/gob/gbc/artifact" + "github.com/kcmvp/gob/utils" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" +) + +type RootTestSuit struct { + suite.Suite +} + +func TestRootTestSuit(t *testing.T) { + suite.Run(t, &RootTestSuit{}) +} + +func (suite *RootTestSuit) BeforeTest(_, testName string) { + os.Chdir(artifact.CurProject().Root()) + s, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "gbc", "testdata", "gob.yaml")) + _, method := utils.TestCaller() + root := filepath.Join(artifact.CurProject().Root(), "target", strings.ReplaceAll(method, "_BeforeTest", fmt.Sprintf("_%s", testName))) + os.MkdirAll(root, os.ModePerm) + t, _ := os.Create(filepath.Join(root, "gob.yaml")) + io.Copy(t, s) + t.Close() + s.Close() + os.Stat(root) +} + +func (suite *RootTestSuit) TearDownSuite() { + _, method := utils.TestCaller() + TearDownSuite(strings.TrimRight(method, "TearDownSuite")) +} + +func (suite *RootTestSuit) TestValidArgs() { + args := rootCmd.ValidArgs + assert.Equal(suite.T(), 4, len(args)) + assert.True(suite.T(), lo.Every(args, []string{"build", "clean", "test", "lint"})) +} + +func (suite *RootTestSuit) TestArgs() { + tests := []struct { + name string + args []string + wantErr bool + }{ + { + name: "not in valid args list", + args: []string{"def"}, + wantErr: true, + }, + { + name: "partial valid args", + args: []string{"build", "def"}, + wantErr: true, + }, + { + name: "no args", + args: []string{}, + wantErr: true, + }, + { + name: "positive case", + args: []string{"clean", "build"}, + wantErr: false, + }, + } + for _, test := range tests { + rootCmd.SetArgs(test.args) + err := Execute() + assert.True(suite.T(), test.wantErr == (err != nil)) + } + +} + +func (suite *RootTestSuit) TestExecute() { + os.Chdir(artifact.CurProject().Target()) + rootCmd.SetArgs([]string{"build"}) + err := Execute() + assert.Equal(suite.T(), "Please execute the command in the project root dir", err.Error()) + rootCmd.SetArgs([]string{"cd"}) + err = Execute() + lo.ForEach([]string{"build", "clean", "test", "lint", "depth"}, func(item string, _ int) { + assert.Equal(suite.T(), "invalid argument \"cd\" for gbc", err.Error()) + }) + os.Chdir(artifact.CurProject().Root()) + rootCmd.SetArgs([]string{"build"}) + err = Execute() + assert.NoError(suite.T(), err) +} + +func (suite *RootTestSuit) TestBuild() { + tests := []struct { + name string + args []string + wantErr bool + }{ + { + name: "no args", + wantErr: true, + }, + { + name: "invalid", + args: []string{"cd"}, + wantErr: true, + }, + { + name: "valid", + args: []string{"build"}, + wantErr: false, + }, + } + for _, test := range tests { + rootCmd.SetArgs(test.args) + err := rootCmd.Execute() + assert.True(suite.T(), test.wantErr == (err != nil)) + if test.wantErr { + assert.True(suite.T(), strings.Contains(err.Error(), color.RedString(""))) + } + } +} + +func (suite *RootTestSuit) TestPersistentPreRun() { + rootCmd.SetArgs([]string{"build"}) + Execute() + hooks := lo.MapToSlice(artifact.HookScripts(), func(key string, _ string) string { + return key + }) + for _, hook := range hooks { + _, err := os.Stat(filepath.Join(artifact.CurProject().HookDir(), hook)) + assert.NoError(suite.T(), err) + } +} + +func (suite *RootTestSuit) TestBuiltinPlugins() { + plugins := builtinPlugins() + assert.Equal(suite.T(), 2, len(plugins)) + plugin, ok := lo.Find(plugins, func(plugin artifact.Plugin) bool { + return plugin.Url == "github.com/golangci/golangci-lint/cmd/golangci-lint" + }) + assert.True(suite.T(), ok) + assert.Equal(suite.T(), "v1.57.2", plugin.Version()) + assert.Equal(suite.T(), "golangci-lint", plugin.Name()) + assert.Equal(suite.T(), "github.com/golangci/golangci-lint", plugin.Module()) + assert.Equal(suite.T(), "lint", plugin.Alias) + plugin, ok = lo.Find(plugins, func(plugin artifact.Plugin) bool { + return plugin.Url == "gotest.tools/gotestsum" + }) + assert.True(suite.T(), ok) + assert.Equal(suite.T(), "v1.11.0", plugin.Version()) + assert.Equal(suite.T(), "gotestsum", plugin.Name()) + assert.Equal(suite.T(), "gotest.tools/gotestsum", plugin.Module()) + assert.Equal(suite.T(), "test", plugin.Alias) +} + +func (suite *RootTestSuit) TestRunE() { + target := artifact.CurProject().Target() + err := rootCmd.RunE(rootCmd, []string{"build"}) + assert.NoError(suite.T(), err) + _, err = os.Stat(filepath.Join(target, lo.If(artifact.Windows(), "gbc.exe").Else("gbc"))) + assert.NoError(suite.T(), err, "binary should be generated") + err = rootCmd.RunE(rootCmd, []string{"build", "clean"}) + assert.NoError(suite.T(), err) + assert.NoFileExistsf(suite.T(), filepath.Join(target, lo.If(artifact.Windows(), "gob.exe").Else("gob")), "binary should be deleted") + err = rootCmd.RunE(rootCmd, []string{"def"}) + assert.Errorf(suite.T(), err, "can not find the command def") +} + +func (suite *RootTestSuit) TestOutOfRoot() { + os.Chdir(artifact.CurProject().Target()) + err := Execute() + assert.Error(suite.T(), err) + assert.True(suite.T(), strings.Contains(err.Error(), "Please execute the command in the project root dir")) +} + +func TearDownSuite(prefix string) { + filepath.WalkDir(os.TempDir(), func(path string, d fs.DirEntry, err error) error { + if d.IsDir() && strings.HasPrefix(d.Name(), prefix) { + os.RemoveAll(path) + } + return nil + }) + filepath.WalkDir(filepath.Join(artifact.CurProject().Root(), "target"), func(path string, d fs.DirEntry, err error) error { + if d.IsDir() && strings.HasPrefix(d.Name(), prefix) { + os.RemoveAll(path) + } + return nil + }) +} diff --git a/cmd/setup.go b/gbc/cmd/setup.go similarity index 95% rename from cmd/setup.go rename to gbc/cmd/setup.go index a1b1626..4c407cb 100644 --- a/cmd/setup.go +++ b/gbc/cmd/setup.go @@ -29,5 +29,5 @@ var setupCmd = &cobra.Command{ } func init() { - builderCmd.AddCommand(setupCmd) + rootCmd.AddCommand(setupCmd) } diff --git a/gbc/cmd/setup_test.go b/gbc/cmd/setup_test.go new file mode 100644 index 0000000..e588827 --- /dev/null +++ b/gbc/cmd/setup_test.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestValidSetupArgs(t *testing.T) { + assert.Equal(t, setupCmd.ValidArgs, []string{"version"}) +} + +// +//func TestSetupVersion(t *testing.T) { +// version := filepath.Join(artifact.CurProject().Root(), "infra", "version.go") +// os.Remove(version) +// _, err := os.Stat(version) +// assert.Error(t, err) +// rootCmd.SetArgs([]string{"setup", "version"}) +// err = rootCmd.Execute() +// assert.NoError(t, err) +// _, err = os.Stat(version) +// assert.NoError(t, err) +//} diff --git a/gbc/cmd/validator.go b/gbc/cmd/validator.go new file mode 100644 index 0000000..ef1d364 --- /dev/null +++ b/gbc/cmd/validator.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "errors" + + "github.com/fatih/color" + "github.com/samber/lo" + "github.com/spf13/cobra" +) + +func MinimumNArgs(n int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) < n { + return errors.New(color.RedString("requires at least %d arg(s), only received %d", n, len(args))) + } + return nil + } +} + +func OnlyValidArgs(cmd *cobra.Command, args []string) error { + for _, arg := range args { + if !lo.Contains(cmd.ValidArgs, arg) { + return errors.New(color.RedString("invalid argument %q for %s", arg, cmd.CommandPath())) + } + } + return nil +} diff --git a/gob.go b/gbc/gbc.go similarity index 64% rename from gob.go rename to gbc/gbc.go index a97e0c9..f8249d4 100644 --- a/gob.go +++ b/gbc/gbc.go @@ -4,12 +4,12 @@ Copyright © 2023 kcheng.mvp@gmail.com package main import ( - "github.com/kcmvp/gob/cmd" + "github.com/kcmvp/gob/gbc/cmd" "os" //nolint ) func main() { - if cmd.Execute() != nil { + if err := cmd.Execute(); err != nil { os.Exit(1) } os.Exit(0) diff --git a/testdata/config.json b/gbc/testdata/config.json similarity index 100% rename from testdata/config.json rename to gbc/testdata/config.json diff --git a/testdata/cover.out b/gbc/testdata/cover.out similarity index 100% rename from testdata/cover.out rename to gbc/testdata/cover.out diff --git a/testdata/gob.yaml b/gbc/testdata/gob.yaml similarity index 100% rename from testdata/gob.yaml rename to gbc/testdata/gob.yaml diff --git a/go.mod b/go.mod index 5930fb9..609b890 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,12 @@ go 1.21.4 require ( github.com/creack/pty v1.1.21 github.com/fatih/color v1.16.0 + github.com/iancoleman/strcase v0.3.0 github.com/jedib0t/go-pretty/v6 v6.5.4 + github.com/kcmvp/structs v1.1.0 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/samber/do/v2 v2.0.0-beta.5 + github.com/samber/go-type-to-string v1.2.0 github.com/samber/lo v1.39.0 github.com/schollz/progressbar/v3 v3.14.1 github.com/spf13/cobra v1.8.0 diff --git a/go.sum b/go.sum index bbc0d05..bffb49b 100644 --- a/go.sum +++ b/go.sum @@ -15,11 +15,15 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.5.4 h1:gOGo0613MoqUcf0xCj+h/V3sHDaZasfv152G6/5l91s= github.com/jedib0t/go-pretty/v6 v6.5.4/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/kcmvp/structs v1.1.0 h1:SwJ8JQ2CWnPIJ3ncn19zE7nBQmPG5oxVTw2eKY64g2U= +github.com/kcmvp/structs v1.1.0/go.mod h1:JBFThRiqYYNjAgfWKaFJGXHecQEwaSQfyp+ACFfRJSY= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -36,6 +40,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -55,6 +61,10 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/samber/do/v2 v2.0.0-beta.5 h1:KpQhVkkzDlsLSDC5WXgyCL8Q3SqOYoInFJIbvntPazM= +github.com/samber/do/v2 v2.0.0-beta.5/go.mod h1:FNMy1RSKMX11Ag8v4KW95n9k+ZkCXn8GuvDKufVKN9E= +github.com/samber/go-type-to-string v1.2.0 h1:Pvdqx3r/EHn9/DTKoW6RoHz/850s5yV1vA6MqKKG5Ys= +github.com/samber/go-type-to-string v1.2.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/schollz/progressbar/v3 v3.14.1 h1:VD+MJPCr4s3wdhTc7OEJ/Z3dAeBzJ7yKH/P4lC5yRTI= @@ -93,6 +103,8 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= diff --git a/gob.yaml b/gob.yaml index 94819c5..6292f79 100644 --- a/gob.yaml +++ b/gob.yaml @@ -10,7 +10,9 @@ plugins: alias: lint args: run ./... url: github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 + description: run golangci-lint against project gotestsum: alias: test args: --format testname -- -coverprofile=target/cover.out ./... url: gotest.tools/gotestsum@v1.11.0 + description: test project with gotest diff --git a/internal/container.go b/internal/container.go new file mode 100644 index 0000000..2104942 --- /dev/null +++ b/internal/container.go @@ -0,0 +1,33 @@ +package internal + +import ( + "fmt" + "log" + "os" + "os/exec" + + "github.com/kcmvp/gob/utils" + "github.com/samber/do/v2" +) + +var ( + Container *do.RootScope + RootDir string +) + +func init() { + Container = do.NewWithOpts(&do.InjectorOpts{ + HookAfterRegistration: func(scope *do.Scope, serviceName string) { + fmt.Printf("scope is %s, name is %s \n", scope.Name(), serviceName) + //@todo, parse the mapping once + }, + Logf: func(format string, args ...any) { + log.Printf(format, args...) + }, + }) + if output, err := exec.Command("go", "list", "-f", "{{.Root}}").CombinedOutput(); err == nil { + RootDir = utils.CleanStr(string(output)) + } else { + RootDir, _ = os.Executable() + } +} diff --git a/internal/parser.go b/internal/parser.go new file mode 100644 index 0000000..a1cb571 --- /dev/null +++ b/internal/parser.go @@ -0,0 +1,72 @@ +package internal + +import ( + "fmt" + "strings" + + "github.com/iancoleman/strcase" + "github.com/kcmvp/structs" + "github.com/samber/lo" +) + +type Mapper lo.Tuple3[string, string, []string] + +func (mapper Mapper) String() string { + return fmt.Sprintf("%s_%s_%s", mapper.A, mapper.B, strings.Join(mapper.C, "_")) +} + +func (mapper Mapper) IsPK() bool { + return lo.ContainsBy(mapper.C, func(item string) bool { + return string(PK) == item + }) +} + +type Attribute string + +const ( + // DBTag struct tag name + DBTag = "db" + // AutoUpdateTime attribute tag with `aut` will be set to time.Now() for update + AutoUpdateTime Attribute = "aut" + // AutoCreateTime attribute tag with `act` will be set to time.Now() for creation + AutoCreateTime Attribute = "act" + // PK identify a column is primary key + PK Attribute = "pk" + // Ignore identify this attribute would not map to database column + Ignore Attribute = "ignore" + // Name database column name + Name Attribute = "name" + // Type database column type + Type Attribute = "type" +) + +func Parse(str any) []Mapper { + var mappers []Mapper + return append(mappers, lo.FilterMap(structs.Fields(str), func(f *structs.Field, _ int) (Mapper, bool) { + if !f.IsExported() { + return Mapper{}, false + } + if f.IsEmbedded() && f.Kind().String() == "struct" { + mappers = append(mappers, Parse(f.Value())...) + return Mapper{}, false + } + colName := strcase.ToSnake(f.Name()) + var attrs []string + ignore := false + if tag := f.Tag(DBTag); tag != "" { + prefix := fmt.Sprintf("%s=", Name) + attrs = strings.Split(tag, ";") + ignore = lo.ContainsBy(attrs, func(attr string) bool { + return string(Ignore) == strings.TrimSpace(attr) + }) + if !ignore { + lo.ForEach(attrs, func(item string, _ int) { + if strings.HasPrefix(item, prefix) { + colName = strings.TrimLeft(item, prefix) + } + }) + } + } + return Mapper{A: f.Name(), B: colName, C: attrs}, !ignore + })...) +} diff --git a/internal/parser_test.go b/internal/parser_test.go new file mode 100644 index 0000000..69bb2a0 --- /dev/null +++ b/internal/parser_test.go @@ -0,0 +1,83 @@ +package internal + +import ( + "database/sql" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +type Base struct { + CreatedAt time.Time `db:"act;name=ct_time"` + CreatedBy string `db:"type=varchar(20)"` + UpdatedAt time.Time `db:"aut"` + UpdatedBy string `db:"type=varchar(20)"` +} +type Base1 struct { + Id int64 `db:"pk;type=integer"` + CreatedAt time.Time + UpdatedAt time.Time `db:"ignore"` +} +type Base2 struct { + CreatedAt time.Time + UpdatedAt time.Time `db:"ignore;name=abc;type=varchar(20)"` +} + +type Product struct { + Base1 + Address sql.NullString `db:"name=full_address"` + comment string +} + +func TestParse(t *testing.T) { + + tests := []struct { + name string + arg any + want []string + }{ + { + name: "Simple", + arg: Base{}, + want: []string{ + Mapper{"CreatedAt", "ct_time", []string{"act", "name=ct_time"}}.String(), + Mapper{"CreatedBy", "created_by", []string{"type=varchar(20)"}}.String(), + Mapper{"UpdatedAt", "updated_at", []string{"aut"}}.String(), + Mapper{"UpdatedBy", "updated_by", []string{"type=varchar(20)"}}.String(), + }, + }, + { + name: "Ignore_case1", + arg: Base1{}, + want: []string{ + Mapper{A: "CreatedAt", B: "created_at"}.String(), + Mapper{A: "Id", B: "id", C: []string{"pk", "type=integer"}}.String(), + }, + }, + { + name: "Ignore_case2", + arg: Base2{}, + want: []string{ + Mapper{A: "CreatedAt", B: "created_at"}.String(), + }, + }, + { + name: "embedded", + arg: Product{}, + want: []string{ + Mapper{A: "CreatedAt", B: "created_at"}.String(), + Mapper{A: "Id", B: "id", C: []string{"pk", "type=integer"}}.String(), + Mapper{A: "Address", B: "full_address", C: []string{"name=full_address"}}.String(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := lo.Map(Parse(tt.arg), func(item Mapper, _ int) string { + return item.String() + }) + assert.True(t, lo.Every(got, tt.want)) + }) + } +} diff --git a/internal/type.go b/internal/type.go new file mode 100644 index 0000000..de2c31b --- /dev/null +++ b/internal/type.go @@ -0,0 +1,136 @@ +package internal + +import ( + "fmt" + "strings" + + "github.com/samber/lo" +) + +const ( + MySQL = "mysql" + PostgreSQL = "postgreSQL" + SQLite = "sqlite" +) + +type ( + GoType string + DBType lo.Tuple3[string, []string, []string] +) + +func ParseDBType() (DBType, error) { + var dbName string + db, ok := lo.Find([]DBType{ + {A: MySQL, B: []string{"auto_increment"}, C: []string{"github.com/go-sql-driver/mysql"}}, + {A: PostgreSQL, B: []string{"smallserial", "serial", "bigserial"}, C: []string{"github.com/lib/pq", "github.com/jackc/pgx"}}, + {A: SQLite, B: []string{"autoincrement"}, C: []string{"github.com/mattn/go-sqlite3"}}, + }, func(item DBType) bool { + return strings.Contains(dbName, item.A) + }) + if !ok { + return DBType{}, fmt.Errorf("can not find database driver in go.mod") + } + return db, nil +} + +func (p DBType) PrimaryStr() []string { + return p.B +} + +var TypeMappings = []lo.Tuple4[GoType, string, string, string]{ + { + A: "string", + B: "varchar", // MySQL + C: "varchar", // PostgreSQL + D: "text", // SQLite + }, + { + A: "bool", + B: "boolean", + C: "boolean", + D: "integer", + }, + { + A: "int8", + B: "tinyint", + C: "int8", + D: "integer", + }, + // unsigned, not a SQL standard + { + A: "uint8", + B: "tinyint unsigned", + C: "int8", + D: "integer", + }, + // unsigned, not a SQL standard + { + A: "byte", + B: "tinyint unsigned", + C: "int8", + D: "integer", + }, + { + A: "int16", + B: "smallint", + C: "smallint", + D: "integer", + }, + // unsigned, not a SQL standard + { + A: "uint16", + B: "smallint", + C: "smallint", + D: "integer", + }, + { + A: "int32", + B: "int", + C: "integer", + D: "integer", + }, + { + A: "rune", + B: "int", + C: "integer", + D: "integer", + }, + // unsigned, not a SQL standard + { + A: "uint32", + B: "int", + C: "integer", + D: "integer", + }, + { + A: "int64", + B: "bigint", + C: "bigint", + D: "integer", + }, + // unsigned, not a SQL standard + { + A: "uint64", + B: "bigint", + C: "bigint", + D: "integer", + }, + { + A: "float32", + B: "float", + C: "double precision", + D: "double precision", + }, + { + A: "float64", + B: "decimal", + C: "decimal", + D: "decimal", + }, + { + A: "time", + B: "timestamp", + C: "timestamp", + D: "datetime", + }, +} diff --git a/sqlite3-init.sql b/sqlite3-init.sql new file mode 100644 index 0000000..1244f3a --- /dev/null +++ b/sqlite3-init.sql @@ -0,0 +1 @@ +insert into Product(Name) values ('Apple'),('Peach'); diff --git a/sqlite3-schema.sql b/sqlite3-schema.sql new file mode 100644 index 0000000..64c5093 --- /dev/null +++ b/sqlite3-schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE Product +( + id INTEGER PRIMARY KEY, + Name text +); diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..23f402a --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,46 @@ +package utils + +import ( + "regexp" + "runtime" + "strings" + + "github.com/samber/lo" +) + +// CleanStr Function to remove non-printable characters +func CleanStr(str string) string { + cleanStr := func(r rune) rune { + if r >= 32 && r != 127 { + return r + } + return -1 + } + return strings.Map(cleanStr, str) +} + +func TestCaller() (bool, string) { + var test bool + var frame runtime.Frame + more := true + callers := make([]uintptr, 20) + for { + size := runtime.Callers(0, callers) + if size == len(callers) { + callers = make([]uintptr, 2*len(callers)) + continue + } + frames := runtime.CallersFrames(callers[:size]) + for !test && more { + frame, more = frames.Next() + // fmt.Printf("%s: %s\size", frame.Function, frame.File) + test = strings.HasSuffix(frame.File, "_test.go") + } + break + } + fqn, _ := lo.Last(strings.Split(frame.Function, "/")) + re := regexp.MustCompile(`\(\*|\)`) + fqn = re.ReplaceAllString(fqn, "") + fqn = strings.ReplaceAll(fqn, ".", "_") + return test, fqn +}