From 61168ad968b6499f03501992d96f033c9ebcae6d Mon Sep 17 00:00:00 2001 From: Wenbo han Date: Fri, 2 Jun 2023 18:52:20 +0800 Subject: [PATCH] Feat: [#49] Support Package Development --- contracts/foundation/application.go | 9 + contracts/foundation/mocks/Application.go | 67 +++++- foundation/application.go | 68 +++++- foundation/application_test.go | 67 ++++++ foundation/console/vendor_publish_command.go | 168 ++++++++++++++ .../console/vendor_publish_command_test.go | 210 ++++++++++++++++++ foundation/service_provider.go | 15 -- support/file/file_test.go | 1 + 8 files changed, 584 insertions(+), 21 deletions(-) create mode 100644 foundation/application_test.go create mode 100644 foundation/console/vendor_publish_command.go create mode 100644 foundation/console/vendor_publish_command_test.go delete mode 100644 foundation/service_provider.go diff --git a/contracts/foundation/application.go b/contracts/foundation/application.go index a81a54b70..b82b7d81e 100644 --- a/contracts/foundation/application.go +++ b/contracts/foundation/application.go @@ -1,7 +1,16 @@ package foundation +import ( + "github.com/goravel/framework/contracts/console" +) + //go:generate mockery --name=Application type Application interface { Container Boot() + Commands([]console.Command) + ConfigPath(path string) string + DatabasePath(path string) string + PublicPath(path string) string + Publishes(packageName string, paths map[string]string, groups ...string) } diff --git a/contracts/foundation/mocks/Application.go b/contracts/foundation/mocks/Application.go index e670494ab..6217a4706 100644 --- a/contracts/foundation/mocks/Application.go +++ b/contracts/foundation/mocks/Application.go @@ -18,6 +18,8 @@ import ( filesystem "github.com/goravel/framework/contracts/filesystem" + foundation "github.com/goravel/framework/contracts/foundation" + grpc "github.com/goravel/framework/contracts/grpc" hash "github.com/goravel/framework/contracts/hash" @@ -47,12 +49,12 @@ type Application struct { } // Bind provides a mock function with given fields: key, callback -func (_m *Application) Bind(key interface{}, callback func() (interface{}, error)) { +func (_m *Application) Bind(key interface{}, callback func(foundation.Application) (interface{}, error)) { _m.Called(key, callback) } // BindWith provides a mock function with given fields: key, callback -func (_m *Application) BindWith(key interface{}, callback func(map[string]interface{}) (interface{}, error)) { +func (_m *Application) BindWith(key interface{}, callback func(foundation.Application, map[string]interface{}) (interface{}, error)) { _m.Called(key, callback) } @@ -61,6 +63,39 @@ func (_m *Application) Boot() { _m.Called() } +// Commands provides a mock function with given fields: _a0 +func (_m *Application) Commands(_a0 []console.Command) { + _m.Called(_a0) +} + +// ConfigPath provides a mock function with given fields: path +func (_m *Application) ConfigPath(path string) string { + ret := _m.Called(path) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(path) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// DatabasePath provides a mock function with given fields: path +func (_m *Application) DatabasePath(path string) string { + ret := _m.Called(path) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(path) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // Instance provides a mock function with given fields: key, instance func (_m *Application) Instance(key interface{}, instance interface{}) { _m.Called(key, instance) @@ -400,8 +435,34 @@ func (_m *Application) MakeWith(key interface{}, parameters map[string]interface return r0, r1 } +// PublicPath provides a mock function with given fields: path +func (_m *Application) PublicPath(path string) string { + ret := _m.Called(path) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(path) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Publishes provides a mock function with given fields: packageName, paths, groups +func (_m *Application) Publishes(packageName string, paths map[string]string, groups ...string) { + _va := make([]interface{}, len(groups)) + for _i := range groups { + _va[_i] = groups[_i] + } + var _ca []interface{} + _ca = append(_ca, packageName, paths) + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + // Singleton provides a mock function with given fields: key, callback -func (_m *Application) Singleton(key interface{}, callback func() (interface{}, error)) { +func (_m *Application) Singleton(key interface{}, callback func(foundation.Application) (interface{}, error)) { _m.Called(key, callback) } diff --git a/foundation/application.go b/foundation/application.go index d9d8aeb20..51a470ff6 100644 --- a/foundation/application.go +++ b/foundation/application.go @@ -1,20 +1,30 @@ package foundation import ( + "fmt" "os" + "path/filepath" "strings" "github.com/goravel/framework/config" + consolecontract "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/foundation" + "github.com/goravel/framework/foundation/console" "github.com/goravel/framework/support" ) -var App foundation.Application +var ( + App foundation.Application +) func init() { setEnv() - app := &Application{Container: NewContainer()} + app := &Application{ + Container: NewContainer(), + publishes: make(map[string]map[string]string), + publishGroups: make(map[string]map[string]string), + } app.registerBaseServiceProviders() app.bootBaseServiceProviders() App = app @@ -22,6 +32,8 @@ func init() { type Application struct { foundation.Container + publishes map[string]map[string]string + publishGroups map[string]map[string]string } func NewApplication() foundation.Application { @@ -32,11 +44,57 @@ func NewApplication() foundation.Application { func (app *Application) Boot() { app.registerConfiguredServiceProviders() app.bootConfiguredServiceProviders() - + app.registerCommands([]consolecontract.Command{ + console.NewVendorPublishCommand(app.publishes, app.publishGroups), + }) app.bootArtisan() setRootPath() } +func (app *Application) Commands(commands []consolecontract.Command) { + app.registerCommands(commands) +} + +func (app *Application) ConfigPath(path string) string { + return fmt.Sprintf("config%s%s", string(filepath.Separator), path) +} + +func (app *Application) DatabasePath(path string) string { + return fmt.Sprintf("database%s%s", string(filepath.Separator), path) +} + +func (app *Application) PublicPath(path string) string { + return fmt.Sprintf("public%s%s", string(filepath.Separator), path) +} + +func (app *Application) Publishes(packageName string, paths map[string]string, groups ...string) { + app.ensurePublishArrayInitialized(packageName) + + for key, value := range paths { + app.publishes[packageName][key] = value + } + + for _, group := range groups { + app.addPublishGroup(group, paths) + } +} + +func (app *Application) ensurePublishArrayInitialized(packageName string) { + if _, exist := app.publishes[packageName]; !exist { + app.publishes[packageName] = make(map[string]string) + } +} + +func (app *Application) addPublishGroup(group string, paths map[string]string) { + if _, exist := app.publishGroups[group]; !exist { + app.publishGroups[group] = make(map[string]string) + } + + for key, value := range paths { + app.publishGroups[group][key] = value + } +} + //bootArtisan Boot artisan command. func (app *Application) bootArtisan() { app.MakeArtisan().Run(os.Args, true) @@ -88,6 +146,10 @@ func (app *Application) bootServiceProviders(serviceProviders []foundation.Servi } } +func (app *Application) registerCommands(commands []consolecontract.Command) { + app.MakeArtisan().Register(commands) +} + func setEnv() { args := os.Args if strings.HasSuffix(os.Args[0], ".test") { diff --git a/foundation/application_test.go b/foundation/application_test.go new file mode 100644 index 000000000..9ce34b2bd --- /dev/null +++ b/foundation/application_test.go @@ -0,0 +1,67 @@ +package foundation + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type ApplicationTestSuite struct { + suite.Suite + app *Application +} + +func TestApplicationTestSuite(t *testing.T) { + suite.Run(t, new(ApplicationTestSuite)) +} + +func (s *ApplicationTestSuite) SetupTest() { + s.app = &Application{ + publishes: make(map[string]map[string]string), + publishGroups: make(map[string]map[string]string), + } +} + +func (s *ApplicationTestSuite) TestConfigPath() { + s.Equal("config/goravel.go", s.app.ConfigPath("goravel.go")) +} + +func (s *ApplicationTestSuite) TestDatabasePath() { + s.Equal("database/goravel.go", s.app.DatabasePath("goravel.go")) +} + +func (s *ApplicationTestSuite) TestPublicPath() { + s.Equal("public/goravel.go", s.app.PublicPath("goravel.go")) +} + +func (s *ApplicationTestSuite) TestPublishes() { + s.app.Publishes("github.com/goravel/sms", map[string]string{ + "config.go": "config.go", + }) + s.Equal(1, len(s.app.publishes["github.com/goravel/sms"])) + s.Equal(0, len(s.app.publishGroups)) + + s.app.Publishes("github.com/goravel/sms", map[string]string{ + "config.go": "config1.go", + "config1.go": "config1.go", + }, "public", "private") + s.Equal(2, len(s.app.publishes["github.com/goravel/sms"])) + s.Equal("config1.go", s.app.publishes["github.com/goravel/sms"]["config.go"]) + s.Equal(2, len(s.app.publishGroups["public"])) + s.Equal("config1.go", s.app.publishes["public"]["config.go"]) + s.Equal(2, len(s.app.publishGroups["private"])) +} + +func (s *ApplicationTestSuite) TestAddPublishGroup() { + s.app.addPublishGroup("public", map[string]string{ + "config.go": "config.go", + }) + s.Equal(1, len(s.app.publishGroups["public"])) + + s.app.addPublishGroup("public", map[string]string{ + "config.go": "config1.go", + "config1.go": "config1.go", + }) + s.Equal(2, len(s.app.publishGroups["public"])) + s.Equal("config1.go", s.app.publishGroups["public"]["config.go"]) +} diff --git a/foundation/console/vendor_publish_command.go b/foundation/console/vendor_publish_command.go new file mode 100644 index 000000000..dc49f09c3 --- /dev/null +++ b/foundation/console/vendor_publish_command.go @@ -0,0 +1,168 @@ +package console + +import ( + "errors" + "go/build" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/gookit/color" + "github.com/spf13/cast" + + "github.com/goravel/framework/contracts/console" + "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/support/file" +) + +type VendorPublishCommand struct { + publishes map[string]map[string]string + publishGroups map[string]map[string]string +} + +func NewVendorPublishCommand(publishes, publishGroups map[string]map[string]string) *VendorPublishCommand { + return &VendorPublishCommand{ + publishes: publishes, + publishGroups: publishGroups, + } +} + +// Signature The name and signature of the console command. +func (receiver *VendorPublishCommand) Signature() string { + return "vendor:publish" +} + +// Description The console command description. +func (receiver *VendorPublishCommand) Description() string { + return "Publish any publishable assets from vendor packages" +} + +// Extend The console command extend. +func (receiver *VendorPublishCommand) Extend() command.Extend { + return command.Extend{ + Category: "vendor", + Flags: []command.Flag{ + { + Name: "existing", + Value: "", + Aliases: []string{"e"}, + Usage: "Publish and overwrite only the files that have already been published", + }, + { + Name: "force", + Value: "", + Aliases: []string{"f"}, + Usage: "Overwrite any existing files", + }, + { + Name: "package", + Value: "", + Aliases: []string{"p"}, + Usage: "Package name to publish", + }, + { + Name: "tag", + Value: "", + Aliases: []string{"t"}, + Usage: "One tag that have assets you want to publish", + }, + }, + } +} + +// Handle Execute the console command. +func (receiver *VendorPublishCommand) Handle(ctx console.Context) error { + packageName := ctx.Option("package") + paths := receiver.pathsForPackageOrGroup(packageName, ctx.Option("tag")) + if len(paths) == 0 { + return errors.New("no vendor found") + } + + packageDir, err := receiver.packageDir(packageName) + if err != nil { + return err + } + + for key, value := range paths { + value = strings.TrimPrefix(strings.TrimPrefix(value, "/"), "./") + content, err := ioutil.ReadFile(filepath.Join(packageDir, key)) + if err != nil { + return err + } + + if receiver.publish(value, string(content), cast.ToBool(ctx.Option("existing")), cast.ToBool(ctx.Option("force"))) { + color.Greenp("Copied Directory ") + color.Yellowf("[%s/%s]", strings.TrimSuffix(packageName, "/"), strings.TrimPrefix(key, "/")) + color.Greenp(" To ") + color.Yellowf("/%s\n", value) + } + } + + color.Greenln("Publishing complete") + + return nil +} + +func (receiver *VendorPublishCommand) pathsForPackageOrGroup(packageName, group string) map[string]string { + if packageName != "" && group != "" { + return receiver.pathsForProviderAndGroup(packageName, group) + } else if group != "" { + if paths, exist := receiver.publishGroups[group]; exist { + return paths + } + } else if packageName != "" { + if paths, exist := receiver.publishes[packageName]; exist { + return paths + } + } + + return nil +} + +func (receiver *VendorPublishCommand) pathsForProviderAndGroup(packageName, group string) map[string]string { + packagePaths, exist := receiver.publishes[packageName] + if !exist { + return nil + } + + groupPaths, exist := receiver.publishGroups[group] + if !exist { + return nil + } + + paths := make(map[string]string) + for key, path := range packagePaths { + if _, exist := groupPaths[key]; exist { + paths[key] = path + } + } + + return paths +} + +func (receiver *VendorPublishCommand) packageDir(packageName string) (string, error) { + var srcDir string + if build.IsLocalImport(packageName) { + srcDir = "./" + } + + pkg, err := build.Import(packageName, srcDir, build.FindOnly) + if err != nil { + return "", err + } + + return pkg.Dir, nil +} + +func (receiver *VendorPublishCommand) publish(path, content string, existing, force bool) bool { + if !file.Exists(path) && existing { + return false + } + if file.Exists(path) && !force && !existing { + return false + } + + file.Create(path, content) + + return true +} diff --git a/foundation/console/vendor_publish_command_test.go b/foundation/console/vendor_publish_command_test.go new file mode 100644 index 000000000..cc7276fad --- /dev/null +++ b/foundation/console/vendor_publish_command_test.go @@ -0,0 +1,210 @@ +package console + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/goravel/framework/support/file" +) + +type VendorPublishCommandTestSuite struct { + suite.Suite +} + +func TestVendorPublishCommandTestSuite(t *testing.T) { + suite.Run(t, new(VendorPublishCommandTestSuite)) +} + +func (s *VendorPublishCommandTestSuite) SetupTest() { + +} + +func (s *VendorPublishCommandTestSuite) TestPathsForPackageOrGroup() { + tests := []struct { + name string + packageName string + group string + publishes map[string]map[string]string + publishGroups map[string]map[string]string + expectPaths map[string]string + }{ + { + name: "packageName and group are empty", + }, + { + name: "packageName and group are not empty, and have same path", + packageName: "github.com/goravel/sms", + group: "public", + publishes: map[string]map[string]string{ + "github.com/goravel/sms": { + "config.go": "config.go", + }, + }, + publishGroups: map[string]map[string]string{ + "public": { + "config.go": "config.go", + }, + }, + expectPaths: map[string]string{ + "config.go": "config.go", + }, + }, + { + name: "packageName and group are not empty, but doesn't have same path", + packageName: "github.com/goravel/sms", + group: "public", + publishes: map[string]map[string]string{ + "github.com/goravel/sms": { + "config.go": "config.go", + }, + }, + publishGroups: map[string]map[string]string{ + "public": { + "config1.go": "config.go", + }, + }, + expectPaths: map[string]string{}, + }, + { + name: "packageName is empty, group is not empty", + group: "public", + publishes: map[string]map[string]string{ + "github.com/goravel/sms": { + "config.go": "config.go", + }, + }, + publishGroups: map[string]map[string]string{ + "public": { + "config1.go": "config.go", + }, + }, + expectPaths: map[string]string{ + "config1.go": "config.go", + }, + }, + { + name: "packageName is not empty, group is empty", + packageName: "github.com/goravel/sms", + publishes: map[string]map[string]string{ + "github.com/goravel/sms": { + "config.go": "config.go", + }, + }, + publishGroups: map[string]map[string]string{ + "public": { + "config1.go": "config.go", + }, + }, + expectPaths: map[string]string{ + "config.go": "config.go", + }, + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + command := NewVendorPublishCommand(test.publishes, test.publishGroups) + s.Equal(test.expectPaths, command.pathsForPackageOrGroup(test.packageName, test.group)) + }) + } +} + +func (s *VendorPublishCommandTestSuite) TestPathsForProviderAndGroup() { + tests := []struct { + name string + packageName string + group string + publishes map[string]map[string]string + publishGroups map[string]map[string]string + expectPaths map[string]string + }{ + { + name: "not found packageName", + packageName: "github.com/goravel/sms1", + group: "public", + publishes: map[string]map[string]string{ + "github.com/goravel/sms": { + "config.go": "config.go", + }, + }, + publishGroups: map[string]map[string]string{ + "public": { + "config1.go": "config.go", + }, + }, + }, + { + name: "not found group", + packageName: "github.com/goravel/sms", + group: "public1", + publishes: map[string]map[string]string{ + "github.com/goravel/sms": { + "config.go": "config.go", + }, + }, + publishGroups: map[string]map[string]string{ + "public": { + "config1.go": "config.go", + }, + }, + }, + { + name: "does not have Intersection", + packageName: "github.com/goravel/sms", + group: "public", + publishes: map[string]map[string]string{ + "github.com/goravel/sms": { + "config.go": "config.go", + }, + }, + publishGroups: map[string]map[string]string{ + "public": { + "config1.go": "config.go", + }, + }, + expectPaths: map[string]string{}, + }, + { + name: "have Intersection", + packageName: "github.com/goravel/sms", + group: "public", + publishes: map[string]map[string]string{ + "github.com/goravel/sms": { + "config.go": "config.go", + }, + }, + publishGroups: map[string]map[string]string{ + "public": { + "config.go": "config.go", + }, + }, + expectPaths: map[string]string{ + "config.go": "config.go", + }, + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + command := NewVendorPublishCommand(test.publishes, test.publishGroups) + s.Equal(test.expectPaths, command.pathsForProviderAndGroup(test.packageName, test.group)) + }) + } +} + +func (s *VendorPublishCommandTestSuite) TestPublish() { + command := &VendorPublishCommand{} + + s.False(command.publish("test.go", "123", true, false)) + s.True(command.publish("test.go", "123", false, false)) + s.True(file.Contain("test.go", "123")) + s.False(command.publish("test.go", "123", false, false)) + s.True(command.publish("test.go", "111", false, true)) + s.True(file.Contain("test.go", "111")) + s.True(command.publish("test.go", "222", true, false)) + s.True(file.Contain("test.go", "222")) + s.True(command.publish("test.go", "333", true, true)) + s.True(file.Contain("test.go", "333")) + s.True(file.Remove("test.go")) +} diff --git a/foundation/service_provider.go b/foundation/service_provider.go deleted file mode 100644 index 7f0e333b2..000000000 --- a/foundation/service_provider.go +++ /dev/null @@ -1,15 +0,0 @@ -package foundation - -import ( - "reflect" -) - -var Publishes = make(map[string]map[string]string) - -type ServiceProvider struct { -} - -func (receiver ServiceProvider) Publishes(paths map[string]string, groups ...string) { - a := reflect.TypeOf(receiver) - Publishes[a.PkgPath()] = paths -} diff --git a/support/file/file_test.go b/support/file/file_test.go index defc09008..6f79b3fc2 100644 --- a/support/file/file_test.go +++ b/support/file/file_test.go @@ -55,6 +55,7 @@ func TestRemove(t *testing.T) { Create(path, `goravel`) assert.True(t, Remove(path)) + assert.True(t, Remove(pwd+"/goravel")) } func TestSize(t *testing.T) {