app API

app

package

API reference for the app package.

F
function

TestRunDoctorDisabled

Parameters

app/doctor_disabled_test.go:7-13
func TestRunDoctorDisabled(t *testing.T)

{
	a := New()

	if err := a.runDoctor(); err != nil {
		t.Fatalf("runDoctor() error = %v", err)
	}
}
F
function

TestRunDoctorEnabled

Parameters

app/doctor_enabled_test.go:7-19
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)
	}
}
F
function

TestRunDoctorEnabledFails

Parameters

app/doctor_enabled_test.go:21-28
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")
	}
}
T
type

Handler

Handler is the interface for declarative struct-tagged endpoints.

app/app.go:18-18
type Handler web.Handler
S
struct

App

App orchestrates DI, HTTP, dispatching, and scheduling into a single entrypoint.

app/app.go:21-32
type App struct

Methods

runDoctor
Method

Returns

error
func (*App) runDoctor() error
{
	return nil
}
runDoctor
Method

Returns

error
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
Method

Log sets the application logger.

Parameters

logger *slog.Logger

Returns

*App
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
Method

Provide registers a named dependency for injection into handler structs.

Parameters

name string
instance any

Returns

*App
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
Method

RegisterHTTP registers a struct-tagged HTTP handler.

Parameters

Returns

*App
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

*App
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

defs ...web.HandlerDefinition

Returns

*App
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

name string
handler func(ctx context.Context, payload ...any) (any, error)

Returns

*App
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

*App
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

*App
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

defs ...actions.Definition

Returns

*App
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
Method

Dispatch calls a named action handler.

Parameters

name string
payload ...any

Returns

any
error
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
Method

DispatchKey calls a struct-tagged action handler by key binding.

Parameters

key string
payload ...any

Returns

any
error
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

bus *events.Bus

Returns

*App
func (*App) UseActionEvents(bus *events.Bus) *App
{
	a.actions.UseEvents(bus)
	return a
}

UseAsyncActionEvents emits action instances asynchronously after dispatch.

Parameters

bus *events.Bus

Returns

*App
func (*App) UseAsyncActionEvents(bus *events.Bus) *App
{
	a.actions.UseAsyncEvents(bus)
	return a
}
Actions
Method

Actions returns registered struct-tagged action names.

Returns

[]string
func (*App) Actions() []string
{
	return a.actions.Actions()
}
KeyBindings
Method

KeyBindings returns struct-tagged action key bindings.

Returns

map[string]string
func (*App) KeyBindings() map[string]string
{
	return a.actions.KeyBindings()
}
Schedule
Method

Schedule registers a cron job.

Parameters

name string
cronExpr string
handler func(ctx context.Context) error

Returns

*App
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
Method

Use adds middleware to the HTTP server.

Parameters

Returns

*App
func (*App) Use(mw web.Middleware) *App
{
	a.server.Use(mw)
	return a
}
Configure
Method

Configure allows direct customization of the underlying web.Server.

Parameters

fn func(*web.Server)

Returns

*App
func (*App) Configure(fn func(*web.Server)) *App
{
	fn(a.server)
	return a
}
Build
Method

Build constructs the DI container and registers all handlers.

Returns

error
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
Method

Listen starts the HTTP server and scheduler, then blocks until shutdown.

Parameters

addr string

Returns

error
func (*App) Listen(addr string) error
{
	return a.listen(addr, "", "")
}
ListenTLS
Method

ListenTLS starts HTTPS using the given certificate and key files.

Parameters

addr string
certFile string
keyFile string

Returns

error
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)
}
listen
Method

Parameters

addr string
certFile string
keyFile string

Returns

error
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
F
function

New

New creates a new App with default components.

Returns

app/app.go:35-44
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())
}
S
struct

schedulerHost

app/app.go:314-316
type schedulerHost struct

Methods

Start
Method

Parameters

Returns

error
func (*schedulerHost) Start(ctx context.Context) error
{
	return s.sched.Start(ctx)
}
Stop
Method

Parameters

Returns

error
func (*schedulerHost) Stop(ctx context.Context) error
{
	return s.sched.Stop(ctx)
}
Completion
Method

Returns

<-chan error
func (*schedulerHost) Completion() <-chan error
{
	return s.sched.Completion()
}

Fields

Name Type Description
sched *scheduler.Scheduler
S
struct

greetEndpoint

app/app_test.go:18-21
type greetEndpoint struct

Methods

Handle
Method

Parameters

Returns

any
error
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"
S
struct

greetResponse

app/app_test.go:23-25
type greetResponse struct

Fields

Name Type Description
Message string json:"message"
F
function

TestApp_New

Parameters

app/app_test.go:31-36
func TestApp_New(t *testing.T)

{
	a := New()
	if a == nil {
		t.Fatal("New() returned nil")
	}
}
F
function

TestApp_LogConfiguresScheduler

Parameters

app/app_test.go:38-50
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())
	}
}
F
function

TestApp_Provide

Parameters

app/app_test.go:52-55
func TestApp_Provide(t *testing.T)

{
	a := New()
	a.Provide("db", "fake-connection")
}
F
function

TestApp_RegisterHTTP

Parameters

app/app_test.go:57-60
func TestApp_RegisterHTTP(t *testing.T)

{
	a := New()
	a.RegisterHTTP(&greetEndpoint{})
}
F
function

TestApp_RegisterAction

Parameters

app/app_test.go:62-67
func TestApp_RegisterAction(t *testing.T)

{
	a := New()
	a.RegisterAction("test", func(ctx context.Context, payload ...any) (any, error) {
		return "ok", nil
	})
}
F
function

TestApp_Dispatch

Parameters

app/app_test.go:69-81
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)
	}
}
S
struct

saveAction

app/app_test.go:83-86
type saveAction struct

Methods

Handle
Method

Parameters

Returns

any
error
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"
F
function

TestApp_RegisterActionHandler

Parameters

app/app_test.go:92-103
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)
	}
}
F
function

TestApp_DispatchKey

Parameters

app/app_test.go:105-116
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)
	}
}
F
function

TestApp_Schedule

Parameters

app/app_test.go:118-123
func TestApp_Schedule(t *testing.T)

{
	a := New()
	a.Schedule("cleanup", "0 0 * * *", func(ctx context.Context) error {
		return nil
	})
}
F
function

TestApp_BuildIsIdempotent

Parameters

app/app_test.go:125-138
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")
	}
}
F
function

TestApp_RegistersStaticDefinitionsAfterBuild

Parameters

app/app_test.go:140-157
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)
	}
}
S
struct

actionWithDependency

app/app_test.go:159-161
type actionWithDependency struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*actionWithDependency) Handle(context.Context) (any, error)
{
	return a.Value, nil
}

Fields

Name Type Description
Value string inject:"value"
F
function

TestApp_BuildValidatesActionDependencies

Parameters

app/app_test.go:167-177
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")
	}
}
S
struct

endpointWithDependency

app/app_test.go:179-182
type endpointWithDependency struct

Methods

Handle
Method

Parameters

Returns

any
error
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"
F
function

TestApp_BuildCanRetryWithoutPartialRouteRegistration

Parameters

app/app_test.go:188-216
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())
	}
}
S
struct

appCloser

app/app_test.go:218-220
type appCloser struct

Methods

Close
Method

Returns

error
func (*appCloser) Close() error
{
	c.closed = true
	return nil
}

Fields

Name Type Description
closed bool
F
function

TestApp_FailedBuildDoesNotCloseProvidedDependencies

Parameters

app/app_test.go:227-257
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")
	}
}
F
function

TestApp_PostBuildHTTPDefinitionsAreTransactional

Parameters

app/app_test.go:259-290
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")
		}
	}
}
F
function

TestApp_BuildRejectsCrossRouterActionDuplicate

Parameters

app/app_test.go:292-306
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{} },
	})
}
F
function

TestApp_RejectsCrossRouterActionDuplicateAfterBuild

Parameters

app/app_test.go:308-362
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{})
	})
}