Skip to content
7 changes: 7 additions & 0 deletions contracts/foundation/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import (
"github.com/goravel/framework/contracts/validation"
)

type AboutItem struct {
Key string
Value string
}

type Application interface {
// Boot register and bootstrap configured service providers.
Boot()
Expand Down Expand Up @@ -63,6 +68,8 @@ type Application interface {
SetJson(json Json)
// GetJson get the JSON implementation.
GetJson() Json
// About add information to the application's about command.
About(section string, items []AboutItem)

// Container
// Bind registers a binding with the container.
Expand Down
5 changes: 5 additions & 0 deletions foundation/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
app.registerConfiguredServiceProviders()
app.bootConfiguredServiceProviders()
app.registerCommands([]contractsconsole.Command{
console.NewAboutCommand(app),

Check warning on line 58 in foundation/application.go

View check run for this annotation

Codecov / codecov/patch

foundation/application.go#L58

Added line #L58 was not covered by tests
console.NewTestMakeCommand(),
console.NewPackageMakeCommand(),
console.NewVendorPublishCommand(app.publishes, app.publishGroups),
Expand Down Expand Up @@ -157,6 +158,10 @@
return app.json
}

func (app *Application) About(section string, items []foundation.AboutItem) {
console.AddAboutInformation(section, items...)

Check warning on line 162 in foundation/application.go

View check run for this annotation

Codecov / codecov/patch

foundation/application.go#L161-L162

Added lines #L161 - L162 were not covered by tests
}

func (app *Application) IsLocale(ctx context.Context, locale string) bool {
return app.CurrentLocale(ctx) == locale
}
Expand Down
147 changes: 147 additions & 0 deletions foundation/console/about_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package console

import (
"fmt"
"runtime"
"sort"
"strings"

"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/contracts/foundation"
)

type AboutCommand struct {
app foundation.Application
}

type information struct {
section map[string]int
details [][]foundation.AboutItem
}

var appInformation = &information{section: make(map[string]int)}
var customInformationResolvers []func()

func NewAboutCommand(app foundation.Application) *AboutCommand {
return &AboutCommand{
app: app,
}
}

// Signature The name and signature of the console command.
func (r *AboutCommand) Signature() string {
return "about"

Check warning on line 34 in foundation/console/about_command.go

View check run for this annotation

Codecov / codecov/patch

foundation/console/about_command.go#L33-L34

Added lines #L33 - L34 were not covered by tests
}

// Description The console command description.
func (r *AboutCommand) Description() string {
return "Display basic information about your application"

Check warning on line 39 in foundation/console/about_command.go

View check run for this annotation

Codecov / codecov/patch

foundation/console/about_command.go#L38-L39

Added lines #L38 - L39 were not covered by tests
}

// Extend The console command extend.
func (r *AboutCommand) Extend() command.Extend {
return command.Extend{
Flags: []command.Flag{
&command.StringFlag{
Name: "only",
Usage: "The section to display",
},
},
}

Check warning on line 51 in foundation/console/about_command.go

View check run for this annotation

Codecov / codecov/patch

foundation/console/about_command.go#L43-L51

Added lines #L43 - L51 were not covered by tests
}

// Handle Execute the console command.
func (r *AboutCommand) Handle(ctx console.Context) error {
r.gatherApplicationInformation()
ctx.NewLine()
appInformation.Range(ctx.Option("only"), func(section string, items []foundation.AboutItem) {
ctx.TwoColumnDetail("<fg=green;op=bold>"+section+"</>", "")
for i := range items {
ctx.TwoColumnDetail(items[i].Key, items[i].Value)
}
ctx.NewLine()
})
return nil
}

// gatherApplicationInformation Gather information about the application.
func (r *AboutCommand) gatherApplicationInformation() {
configFacade := r.app.MakeConfig()
appInformation.addToSection("Environment", []foundation.AboutItem{
{Key: "Application Name", Value: configFacade.GetString("app.name")},
{Key: "Goravel Version", Value: strings.TrimPrefix(r.app.Version(), "v")},
{Key: "Go Version", Value: strings.TrimPrefix(runtime.Version(), "go")},
{Key: "Environment", Value: configFacade.GetString("app.env")},
{Key: "Debug Mode", Value: func() string {
mode := "OFF"
if configFacade.GetBool("app.debug") {
mode = "<fg=yellow;op=bold>ENABLED</>"
}
return mode
}()},
{Key: "URL", Value: configFacade.GetString("http.url")},
{Key: "HTTP Host", Value: configFacade.GetString("http.host")},
{Key: "HTTP Port", Value: configFacade.GetString("http.port")},
{Key: "GRPC Host", Value: configFacade.GetString("grpc.host")},
{Key: "GRPC Port", Value: configFacade.GetString("grpc.port")},
})
appInformation.addToSection("Drivers", []foundation.AboutItem{
{Key: "Cache", Value: configFacade.GetString("cache.default")},
{Key: "Database", Value: configFacade.GetString("database.default")},
{Key: "Hashing", Value: configFacade.GetString("hashing.driver")},
{Key: "Http", Value: configFacade.GetString("http.default")},
{Key: "Logs", Value: func() string {
logs := configFacade.GetString("logging.default")
if logChannel := logs; configFacade.GetString("logging.channels."+logChannel+".driver") == "stack" {
if secondary, ok := configFacade.Get("logging.channels." + logChannel + ".channels").([]string); ok {
logs = fmt.Sprintf("<fg=yellow;op=bold>%s</> <fg=gray;op=bold>/</> %s", logChannel, strings.Join(secondary, ", "))
}
}
return logs
}()},
{Key: "Mail", Value: configFacade.GetString("mail.default", "smtp")},
{Key: "Queue", Value: configFacade.GetString("queue.default")},
{Key: "Session", Value: configFacade.GetString("session.driver")},
})
for i := range customInformationResolvers {
customInformationResolvers[i]()
}
}

// addToSection Add a new section to the application information.
func (info *information) addToSection(section string, items []foundation.AboutItem) {
index, ok := info.section[section]
if !ok {
index = len(info.details)
info.section[section] = index
info.details = append(info.details, make([]foundation.AboutItem, 0))
}
info.details[index] = append(info.details[index], items...)
}

// Range Iterate over the application information sections.
func (info *information) Range(section string, ranger func(s string, items []foundation.AboutItem)) {
var sections []string
for s := range info.section {
if len(section) == 0 || strings.EqualFold(section, s) {
sections = append(sections, s)
}
}
if len(sections) > 1 {
sort.Slice(sections, func(i, j int) bool {
return info.section[sections[i]] < info.section[sections[j]]
})
}
for i := range sections {
ranger(sections[i], info.details[info.section[sections[i]]])
}

}

// AddAboutInformation Add custom information to the application information.
func AddAboutInformation(section string, items ...foundation.AboutItem) {
customInformationResolvers = append(customInformationResolvers, func() {
appInformation.addToSection(section, items)
})
}
58 changes: 58 additions & 0 deletions foundation/console/about_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package console

import (
"io"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"

"github.com/goravel/framework/contracts/foundation"
mocksconfig "github.com/goravel/framework/mocks/config"
consolemocks "github.com/goravel/framework/mocks/console"
mocksfoundation "github.com/goravel/framework/mocks/foundation"
"github.com/goravel/framework/support/color"
)

func TestAboutCommand(t *testing.T) {
mockApp := mocksfoundation.NewApplication(t)
mockConfig := mocksconfig.NewConfig(t)
mockApp.EXPECT().MakeConfig().Return(mockConfig).Once()
mockApp.EXPECT().Version().Return("")
mockConfig.EXPECT().GetString("logging.default").Return("stack").Once()
mockConfig.EXPECT().GetString("logging.channels.stack.driver").Return("stack").Once()
mockConfig.EXPECT().Get("logging.channels.stack.channels").Return([]string{"test"}).Once()
mockConfig.EXPECT().GetString(mock.Anything).Return("")
mockConfig.EXPECT().GetString(mock.Anything, mock.Anything).Return("")
mockConfig.EXPECT().GetBool(mock.Anything).Return(true)
aboutCommand := NewAboutCommand(mockApp)
mockContext := &consolemocks.Context{}
mockContext.EXPECT().NewLine().Return()
mockContext.EXPECT().Option("only").Return("").Once()
mockContext.EXPECT().TwoColumnDetail(mock.Anything, mock.Anything).Return()
AddAboutInformation("Custom", foundation.AboutItem{Key: "Test Info", Value: "<fg=cyan>OK</>"})
color.CaptureOutput(func(w io.Writer) {
assert.Nil(t, aboutCommand.Handle(mockContext))
})
appInformation.Range("", func(section string, details []foundation.AboutItem) {
assert.Contains(t, []string{"Environment", "Drivers", "Custom"}, section)
assert.NotEmpty(t, details)
})
}

func TestAddToSection(t *testing.T) {
appInformation = &information{section: make(map[string]int)}
appInformation.addToSection("Test", []foundation.AboutItem{{Key: "Test Info", Value: "OK"}})
assert.Equal(t, appInformation.section, map[string]int{"Test": 0})
assert.Len(t, appInformation.details, 1)
}

func TestInformationRange(t *testing.T) {
appInformation = &information{section: make(map[string]int)}
appInformation.addToSection("Test", []foundation.AboutItem{{Key: "Test Info", Value: "OK"}, {Key: "Test Info", Value: "OK"}})
appInformation.Range("Test", func(section string, details []foundation.AboutItem) {
assert.Equal(t, "Test", section)
assert.Len(t, details, 2)
assert.Subset(t, details, []foundation.AboutItem{{Key: "Test Info", Value: "OK"}})
})
}
34 changes: 34 additions & 0 deletions mocks/foundation/Application.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading