app
packageAPI reference for the app
package.
Imports
(17)github.com/mirkobrombin/go-foundation/v2/app/hosting
INT
github.com/mirkobrombin/go-foundation/v2/core/contracts
STD
testing
INT
github.com/mirkobrombin/go-foundation/v2/app/doctor
STD
context
STD
fmt
STD
log/slog
INT
github.com/mirkobrombin/go-foundation/v2/app/actions
INT
github.com/mirkobrombin/go-foundation/v2/app/di
INT
github.com/mirkobrombin/go-foundation/v2/app/dispatcher
INT
github.com/mirkobrombin/go-foundation/v2/app/web
INT
github.com/mirkobrombin/go-foundation/v2/core/events
INT
github.com/mirkobrombin/go-foundation/v2/core/scheduler
STD
bytes
STD
net/http
STD
net/http/httptest
STD
strings
TestRunDoctorDisabled
Parameters
func TestRunDoctorDisabled(t *testing.T)
{
a := New()
if err := a.runDoctor(); err != nil {
t.Fatalf("runDoctor() error = %v", err)
}
}
TestRunDoctorEnabled
Parameters
func TestRunDoctorEnabled(t *testing.T)
{
t.Setenv("FOUNDATION_DOCTOR", "fail")
a := New()
a.RegisterHTTP(&greetEndpoint{})
if _, err := a.Build(); err != nil {
t.Fatalf("Build() error = %v", err)
}
if err := a.runDoctor(); err != nil {
t.Fatalf("runDoctor() error = %v", err)
}
}
TestRunDoctorEnabledFails
Parameters
func TestRunDoctorEnabledFails(t *testing.T)
{
t.Setenv("FOUNDATION_DOCTOR", "fail")
a := New()
if err := a.runDoctor(); err == nil {
t.Fatal("runDoctor() error = nil, want error")
}
}
Handler
Handler is the interface for declarative struct-tagged endpoints.
type Handler web.Handler
App
App orchestrates DI, HTTP, dispatching, and scheduling into a single entrypoint.
type App struct
Methods
Returns
func (*App) runDoctor() error
{
routes := a.server.Routes()
doctorRoutes := make([]doctor.Route, 0, len(routes))
for _, route := range routes {
doctorRoutes = append(doctorRoutes, doctor.Route{
Method: route.Method,
Path: route.Path,
})
}
return doctor.Run(doctor.Source{Routes: doctorRoutes})
}
Log sets the application logger.
Parameters
Returns
func (*App) Log(logger *slog.Logger) *App
{
if logger == nil {
logger = slog.Default()
}
a.logger = logger
a.sched.SetLogger(func(message string) {
logger.Info(message)
})
return a
}
Provide registers a named dependency for injection into handler structs.
Parameters
Returns
func (*App) Provide(name string, instance any) *App
{
if a.container != nil {
a.container.Provide(name, instance)
return a
}
a.builder.Provide(name, instance)
a.actions.Provide(name, instance)
return a
}
RegisterHTTP registers a struct-tagged HTTP handler.
func (*App) RegisterHTTP(h Handler) *App
{
if a.container != nil {
if err := a.server.RegisterHandler(h, a.container); err != nil {
panic(err)
}
return a
}
a.handlerReg = append(a.handlerReg, h)
return a
}
RegisterHTTPDefinition registers a statically described HTTP handler.
Parameters
Returns
func (*App) RegisterHTTPDefinition(def web.HandlerDefinition) *App
{
if a.container != nil {
if err := a.server.RegisterDefinition(def, a.container); err != nil {
panic(err)
}
return a
}
a.handlerDef = append(a.handlerDef, def)
return a
}
RegisterHTTPDefinitions registers statically described HTTP handlers.
Parameters
Returns
func (*App) RegisterHTTPDefinitions(defs ...web.HandlerDefinition) *App
{
if a.container != nil {
if err := a.server.RegisterDefinitions(a.container, defs...); err != nil {
panic(err)
}
return a
}
a.handlerDef = append(a.handlerDef, defs...)
return a
}
RegisterAction registers a named action handler for dispatch.
Parameters
Returns
func (*App) RegisterAction(name string, handler func(ctx context.Context, payload ...any) (any, error)) *App
{
if a.actions.Has(name) {
panic(fmt.Sprintf("app: action %q is registered in both action routers", name))
}
a.dispatch.Register(name, handler)
return a
}
RegisterActionHandler registers a struct-tagged action handler.
Parameters
Returns
func (*App) RegisterActionHandler(h actions.Handler) *App
{
name, err := actions.HandlerName(h)
if err != nil {
panic(err)
}
if a.dispatch.Has(name) {
panic(fmt.Sprintf("app: action %q is registered in both action routers", name))
}
a.actions.Register(h)
return a
}
RegisterActionDefinition registers a statically described action.
Parameters
Returns
func (*App) RegisterActionDefinition(def actions.Definition) *App
{
if a.dispatch.Has(def.Name) {
panic(fmt.Sprintf("app: action %q is registered in both action routers", def.Name))
}
if a.container != nil {
if err := a.actions.RegisterDefinition(def); err != nil {
panic(err)
}
return a
}
a.actionDef = append(a.actionDef, def)
return a
}
RegisterActionDefinitions registers statically described actions.
Parameters
Returns
func (*App) RegisterActionDefinitions(defs ...actions.Definition) *App
{
for _, def := range defs {
if a.dispatch.Has(def.Name) {
panic(fmt.Sprintf("app: action %q is registered in both action routers", def.Name))
}
}
if a.container != nil {
if err := a.actions.RegisterDefinitions(defs...); err != nil {
panic(err)
}
return a
}
a.actionDef = append(a.actionDef, defs...)
return a
}
Dispatch calls a named action handler.
Parameters
Returns
func (*App) Dispatch(ctx context.Context, name string, payload ...any) (any, error)
{
if a.dispatch.Has(name) {
return a.dispatch.Dispatch(ctx, name, payload...)
}
if a.container == nil {
if _, err := a.Build(); err != nil {
return nil, fmt.Errorf("app: build failed: %w", err)
}
}
return a.actions.Dispatch(ctx, name, payload...)
}
DispatchKey calls a struct-tagged action handler by key binding.
Parameters
Returns
func (*App) DispatchKey(ctx context.Context, key string, payload ...any) (any, error)
{
if a.container == nil {
if _, err := a.Build(); err != nil {
return nil, fmt.Errorf("app: build failed: %w", err)
}
}
return a.actions.DispatchKey(ctx, key, payload...)
}
UseActionEvents emits action instances after dispatch.
Parameters
Returns
func (*App) UseActionEvents(bus *events.Bus) *App
{
a.actions.UseEvents(bus)
return a
}
UseAsyncActionEvents emits action instances asynchronously after dispatch.
Parameters
Returns
func (*App) UseAsyncActionEvents(bus *events.Bus) *App
{
a.actions.UseAsyncEvents(bus)
return a
}
Actions returns registered struct-tagged action names.
Returns
func (*App) Actions() []string
{
return a.actions.Actions()
}
KeyBindings returns struct-tagged action key bindings.
Returns
func (*App) KeyBindings() map[string]string
{
return a.actions.KeyBindings()
}
Schedule registers a cron job.
Parameters
Returns
func (*App) Schedule(name, cronExpr string, handler func(ctx context.Context) error) *App
{
if err := a.sched.Register(scheduler.Job{Name: name, Cron: cronExpr, Handler: handler}); err != nil {
panic(err)
}
return a
}
Use adds middleware to the HTTP server.
Parameters
Returns
func (*App) Use(mw web.Middleware) *App
{
a.server.Use(mw)
return a
}
Configure allows direct customization of the underlying web.Server.
Parameters
Returns
func (*App) Configure(fn func(*web.Server)) *App
{
fn(a.server)
return a
}
Build constructs the DI container and registers all handlers.
Returns
func (*App) Build() (*di.Container, error)
{
if a.container != nil {
return a.container, nil
}
container, err := a.builder.Build()
if err != nil {
return nil, err
}
for _, name := range a.actions.Actions() {
if a.dispatch.Has(name) {
return nil, fmt.Errorf("app: action %q is registered in both action routers", name)
}
}
for _, definition := range a.actionDef {
if a.dispatch.Has(definition.Name) {
return nil, fmt.Errorf("app: action %q is registered in both action routers", definition.Name)
}
}
if err := a.actions.Validate(container); err != nil {
return nil, err
}
if err := a.actions.ValidateDefinitions(container, a.actionDef...); err != nil {
return nil, err
}
httpDefinitions := append([]web.HandlerDefinition(nil), a.handlerDef...)
for _, prototype := range a.handlerReg {
definition, err := web.DefinitionFromHandler(prototype)
if err != nil {
return nil, err
}
httpDefinitions = append(httpDefinitions, definition)
}
if err := a.server.ValidateDefinitions(container, httpDefinitions...); err != nil {
return nil, err
}
a.actions.UseContainer(container)
if err := a.actions.RegisterDefinitions(a.actionDef...); err != nil {
return nil, err
}
if err := a.server.RegisterDefinitions(container, httpDefinitions...); err != nil {
return nil, err
}
a.actionDef = nil
a.handlerReg = nil
a.handlerDef = nil
a.container = container
return container, nil
}
Listen starts the HTTP server and scheduler, then blocks until shutdown.
Parameters
Returns
func (*App) Listen(addr string) error
{
return a.listen(addr, "", "")
}
ListenTLS starts HTTPS using the given certificate and key files.
Parameters
Returns
func (*App) ListenTLS(addr, certFile, keyFile string) error
{
if certFile == "" || keyFile == "" {
return fmt.Errorf("app: TLS certificate and key files are required")
}
return a.listen(addr, certFile, keyFile)
}
Parameters
Returns
func (*App) listen(addr, certFile, keyFile string) error
{
if a.container == nil {
if _, err := a.Build(); err != nil {
return fmt.Errorf("app: build failed: %w", err)
}
}
if addr == "" {
addr = "127.0.0.1:8080"
}
builder := hosting.NewBuilder().
WithAddr(addr).
UseContainer(a.container).
UseWeb(a.server).
AddHostedService(&schedulerHost{sched: a.sched})
if certFile != "" {
builder.WithTLS(certFile, keyFile)
}
h := builder.Build()
return h.Run(context.Background())
}
Fields
| Name | Type | Description |
|---|---|---|
| container | *di.Container | |
| server | *web.Server | |
| actions | *actions.Router | |
| dispatch | *dispatcher.Dispatcher | |
| sched | *scheduler.Scheduler | |
| builder | *di.Builder | |
| logger | *slog.Logger | |
| handlerReg | []Handler | |
| handlerDef | []web.HandlerDefinition | |
| actionDef | []actions.Definition |
New
New creates a new App with default components.
Returns
func New() *App
{
app := &App{
builder: di.NewBuilder(),
server: web.New(),
actions: actions.New(),
dispatch: dispatcher.New(),
sched: scheduler.New(),
}
return app.Log(slog.Default())
}
schedulerHost
type schedulerHost struct
Methods
Parameters
Returns
func (*schedulerHost) Start(ctx context.Context) error
{
return s.sched.Start(ctx)
}
Parameters
Returns
func (*schedulerHost) Stop(ctx context.Context) error
{
return s.sched.Stop(ctx)
}
Returns
func (*schedulerHost) Completion() <-chan error
{
return s.sched.Completion()
}
Fields
| Name | Type | Description |
|---|---|---|
| sched | *scheduler.Scheduler |
greetEndpoint
type greetEndpoint struct
Methods
Parameters
Returns
func (*greetEndpoint) Handle(_ context.Context) (any, error)
{
return greetResponse{Message: "hello " + e.Name}, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | method:"GET" path:"/greet" |
| Name | string | query:"name" default:"world" |
greetResponse
type greetResponse struct
Fields
| Name | Type | Description |
|---|---|---|
| Message | string | json:"message" |
TestApp_New
Parameters
func TestApp_New(t *testing.T)
{
a := New()
if a == nil {
t.Fatal("New() returned nil")
}
}
TestApp_LogConfiguresScheduler
Parameters
func TestApp_LogConfiguresScheduler(t *testing.T)
{
var output bytes.Buffer
application := New().Log(slog.New(slog.NewTextHandler(&output, nil)))
if err := application.sched.Start(context.Background()); err != nil {
t.Fatal(err)
}
if err := application.sched.Stop(context.Background()); err != nil {
t.Fatal(err)
}
if !strings.Contains(output.String(), "scheduler: started") {
t.Fatalf("scheduler log was not routed through App.Log(): %s", output.String())
}
}
TestApp_Provide
Parameters
func TestApp_Provide(t *testing.T)
{
a := New()
a.Provide("db", "fake-connection")
}
TestApp_RegisterHTTP
Parameters
func TestApp_RegisterHTTP(t *testing.T)
{
a := New()
a.RegisterHTTP(&greetEndpoint{})
}
TestApp_RegisterAction
Parameters
func TestApp_RegisterAction(t *testing.T)
{
a := New()
a.RegisterAction("test", func(ctx context.Context, payload ...any) (any, error) {
return "ok", nil
})
}
TestApp_Dispatch
Parameters
func TestApp_Dispatch(t *testing.T)
{
a := New()
a.RegisterAction("ping", func(ctx context.Context, payload ...any) (any, error) {
return "pong", nil
})
result, err := a.Dispatch(context.Background(), "ping")
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
if result != "pong" {
t.Errorf("result = %v, want pong", result)
}
}
saveAction
type saveAction struct
Methods
Parameters
Returns
func (*saveAction) Handle(_ context.Context) (any, error)
{
return "saved " + a.Name, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | action:"file.save" keys:"ctrl+s" |
| Name | string | json:"name" |
TestApp_RegisterActionHandler
Parameters
func TestApp_RegisterActionHandler(t *testing.T)
{
a := New()
a.RegisterActionHandler(&saveAction{})
result, err := a.Dispatch(context.Background(), "file.save", map[string]any{"name": "notes.md"})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
if result != "saved notes.md" {
t.Errorf("result = %v, want saved notes.md", result)
}
}
TestApp_DispatchKey
Parameters
func TestApp_DispatchKey(t *testing.T)
{
a := New()
a.RegisterActionHandler(&saveAction{})
result, err := a.DispatchKey(context.Background(), "ctrl+s", map[string]any{"name": "book.md"})
if err != nil {
t.Fatalf("DispatchKey: %v", err)
}
if result != "saved book.md" {
t.Errorf("result = %v, want saved book.md", result)
}
}
TestApp_Schedule
Parameters
func TestApp_Schedule(t *testing.T)
{
a := New()
a.Schedule("cleanup", "0 0 * * *", func(ctx context.Context) error {
return nil
})
}
TestApp_BuildIsIdempotent
Parameters
func TestApp_BuildIsIdempotent(t *testing.T)
{
application := New()
first, err := application.Build()
if err != nil {
t.Fatalf("Build() error = %v", err)
}
second, err := application.Build()
if err != nil {
t.Fatalf("second Build() error = %v", err)
}
if first != second {
t.Fatal("Build() returned a different container")
}
}
TestApp_RegistersStaticDefinitionsAfterBuild
Parameters
func TestApp_RegistersStaticDefinitionsAfterBuild(t *testing.T)
{
application := New()
if _, err := application.Build(); err != nil {
t.Fatal(err)
}
application.RegisterActionDefinition(actions.Definition{
Name: "late.action",
New: func() actions.Handler { return &saveAction{} },
})
result, err := application.Dispatch(context.Background(), "late.action", map[string]any{"name": "late.md"})
if err != nil {
t.Fatal(err)
}
if result != "saved late.md" {
t.Fatalf("Dispatch() = %v", result)
}
}
actionWithDependency
type actionWithDependency struct
Methods
Parameters
Returns
func (*actionWithDependency) Handle(context.Context) (any, error)
{
return a.Value, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| Value | string | inject:"value" |
TestApp_BuildValidatesActionDependencies
Parameters
func TestApp_BuildValidatesActionDependencies(t *testing.T)
{
application := New()
application.RegisterActionDefinition(actions.Definition{
Name: "requires.value",
New: func() actions.Handler { return &actionWithDependency{} },
})
if _, err := application.Build(); err == nil {
t.Fatal("Build() accepted an action with a missing dependency")
}
}
endpointWithDependency
type endpointWithDependency struct
Methods
Parameters
Returns
func (*endpointWithDependency) Handle(context.Context) (any, error)
{
return e.Value, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | method:"GET" path:"/requires-value" |
| Value | string | inject:"value" |
TestApp_BuildCanRetryWithoutPartialRouteRegistration
Parameters
func TestApp_BuildCanRetryWithoutPartialRouteRegistration(t *testing.T)
{
application := New()
application.RegisterHTTP(&endpointWithDependency{})
if _, err := application.Build(); err == nil {
t.Fatal("first Build() accepted a missing HTTP dependency")
}
before := httptest.NewRecorder()
application.server.ServeHTTP(
before,
httptest.NewRequest(http.MethodGet, "/requires-value", nil),
)
if before.Code != http.StatusNotFound {
t.Fatalf("failed Build() registered a route with status %d", before.Code)
}
application.Provide("value", "ready")
if _, err := application.Build(); err != nil {
t.Fatalf("second Build() error = %v", err)
}
after := httptest.NewRecorder()
application.server.ServeHTTP(
after,
httptest.NewRequest(http.MethodGet, "/requires-value", nil),
)
if after.Code != http.StatusOK {
t.Fatalf("second Build() route status = %d, body = %s", after.Code, after.Body.String())
}
}
appCloser
type appCloser struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| closed | bool |
TestApp_FailedBuildDoesNotCloseProvidedDependencies
Parameters
func TestApp_FailedBuildDoesNotCloseProvidedDependencies(t *testing.T)
{
closer := &appCloser{}
application := New().
Provide("db", closer).
RegisterActionDefinition(actions.Definition{
Name: "requires.value",
New: func() actions.Handler { return &actionWithDependency{} },
})
if _, err := application.Build(); err == nil {
t.Fatal("first Build() accepted a missing action dependency")
}
if closer.closed {
t.Fatal("failed Build() closed a dependency owned by the reusable builder")
}
application.Provide("value", "ready")
container, err := application.Build()
if err != nil {
t.Fatalf("second Build() error = %v", err)
}
if closer.closed {
t.Fatal("successful retry received an already-closed dependency")
}
if err := container.Close(); err != nil {
t.Fatal(err)
}
if !closer.closed {
t.Fatal("container Close() did not close the provided dependency")
}
}
TestApp_PostBuildHTTPDefinitionsAreTransactional
Parameters
func TestApp_PostBuildHTTPDefinitionsAreTransactional(t *testing.T)
{
application := New()
if _, err := application.Build(); err != nil {
t.Fatal(err)
}
func() {
defer func() {
if recover() == nil {
t.Fatal("RegisterHTTPDefinitions() accepted an invalid batch")
}
}()
application.RegisterHTTPDefinitions(
web.HandlerDefinition{
Method: http.MethodGet,
Path: "/batch-first",
New: func() web.Handler { return &greetEndpoint{} },
},
web.HandlerDefinition{
Method: http.MethodGet,
Path: "",
New: func() web.Handler { return &greetEndpoint{} },
},
)
}()
for _, route := range application.server.Routes() {
if route.Path == "/batch-first" {
t.Fatal("invalid batch partially registered its first route")
}
}
}
TestApp_BuildRejectsCrossRouterActionDuplicate
Parameters
func TestApp_BuildRejectsCrossRouterActionDuplicate(t *testing.T)
{
application := New()
application.RegisterAction("file.save", func(context.Context, ...any) (any, error) {
return nil, nil
})
defer func() {
if recover() == nil {
t.Fatal("RegisterActionDefinition() accepted a cross-router duplicate")
}
}()
application.RegisterActionDefinition(actions.Definition{
Name: "file.save",
New: func() actions.Handler { return &saveAction{} },
})
}
TestApp_RejectsCrossRouterActionDuplicateAfterBuild
Parameters
func TestApp_RejectsCrossRouterActionDuplicateAfterBuild(t *testing.T)
{
t.Run("dispatcher first", func(t *testing.T) {
application := New()
if _, err := application.Build(); err != nil {
t.Fatal(err)
}
application.RegisterAction("duplicate", func(context.Context, ...any) (any, error) {
return nil, nil
})
defer func() {
if recover() == nil {
t.Fatal("RegisterActionDefinition() accepted a dispatcher duplicate")
}
}()
application.RegisterActionDefinition(actions.Definition{
Name: "duplicate",
New: func() actions.Handler { return &saveAction{} },
})
})
t.Run("declarative first", func(t *testing.T) {
application := New()
if _, err := application.Build(); err != nil {
t.Fatal(err)
}
application.RegisterActionDefinition(actions.Definition{
Name: "duplicate",
New: func() actions.Handler { return &saveAction{} },
})
defer func() {
if recover() == nil {
t.Fatal("RegisterAction() accepted a declarative duplicate")
}
}()
application.RegisterAction("duplicate", func(context.Context, ...any) (any, error) {
return nil, nil
})
})
t.Run("reflection second", func(t *testing.T) {
application := New()
if _, err := application.Build(); err != nil {
t.Fatal(err)
}
application.RegisterAction("file.save", func(context.Context, ...any) (any, error) {
return nil, nil
})
defer func() {
if recover() == nil {
t.Fatal("RegisterActionHandler() accepted a dispatcher duplicate")
}
}()
application.RegisterActionHandler(&saveAction{})
})
}