Skip to content

Commit

Permalink
story(issue-358): migrate the lifecycle middleware from app to appbui…
Browse files Browse the repository at this point in the history
…lder (#359)
  • Loading branch information
Zaba505 authored Feb 9, 2025
1 parent d1172e2 commit cfc49dd
Show file tree
Hide file tree
Showing 12 changed files with 568 additions and 244 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
[![Go Reference](https://pkg.go.dev/badge/github.com/z5labs/bedrock.svg)](https://pkg.go.dev/github.com/z5labs/bedrock)
[![Go Report Card](https://goreportcard.com/badge/github.com/z5labs/bedrock)](https://goreportcard.com/report/github.com/z5labs/bedrock)
![Coverage](https://img.shields.io/badge/Coverage-96.2%25-brightgreen)
![Coverage](https://img.shields.io/badge/Coverage-96.1%25-brightgreen)
[![build](https://github.com/z5labs/bedrock/actions/workflows/build.yaml/badge.svg)](https://github.com/z5labs/bedrock/actions/workflows/build.yaml)

**bedrock provides a minimal, modular and composable foundation for
Expand Down
74 changes: 16 additions & 58 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/z5labs/bedrock"
"github.com/z5labs/bedrock/internal/try"
"github.com/z5labs/bedrock/lifecycle"
)

type runFunc func(context.Context) error
Expand All @@ -34,10 +35,10 @@ func Recover(app bedrock.App) bedrock.App {
})
}

// WithSignalNotifications wraps a given [bedrock.App] in an implementation
// InterruptOn wraps a given [bedrock.App] in an implementation
// that cancels the [context.Context] that's passed to app.Run if an [os.Signal]
// is received by the running process.
func WithSignalNotifications(app bedrock.App, signals ...os.Signal) bedrock.App {
func InterruptOn(app bedrock.App, signals ...os.Signal) bedrock.App {
return runFunc(func(ctx context.Context) error {
sigCtx, cancel := signal.NotifyContext(ctx, signals...)
defer cancel()
Expand All @@ -46,68 +47,25 @@ func WithSignalNotifications(app bedrock.App, signals ...os.Signal) bedrock.App
})
}

// LifecycleHook represents functionality that needs to be performed
// at a specific "time" relative to the execution of [bedrock.App.Run].
type LifecycleHook interface {
Run(context.Context) error
}

// LifecycleHookFunc is a convenient helper type for implementing a [LifecycleHook]
// from just a regular func.
type LifecycleHookFunc func(context.Context) error

// Run implements the [LifecycleHook] interface.
func (f LifecycleHookFunc) Run(ctx context.Context) error {
return f(ctx)
}

// ComposeLifecycleHooks combines multiple [LifecycleHook]s into a single hook.
// Each hook is called sequentially and each hook is called irregardless if a
// previous hook returned an error or not. Any and all errors are then returned
// after all hooks have been ran.
func ComposeLifecycleHooks(hooks ...LifecycleHook) LifecycleHook {
return LifecycleHookFunc(func(ctx context.Context) error {
errs := make([]error, 0, len(hooks))
for _, hook := range hooks {
err := hook.Run(ctx)
if err == nil {
continue
}
errs = append(errs, err)
}
if len(errs) == 0 {
return nil
}
return errors.Join(errs...)
})
}

// Lifecycle
type Lifecycle struct {
// PostRun is always executed regardless if the underlying [bedrock.App]
// returns an error or panics.
PostRun LifecycleHook
}

// WithLifecycleHooks wraps a given [bedrock.App] in an implementation
// that runs [LifecycleHook]s around the execution of app.Run.
func WithLifecycleHooks(app bedrock.App, lifecycle Lifecycle) bedrock.App {
// PostRun defers the execution of the given [lifecycle.Hook] until
// after the given [bedrock.App] returns from its Run method. Since
// the [lifecycle.Hook] execution is deferred it will always execute
// even if the [bedrock.App.Run] panics.
func PostRun(app bedrock.App, hook lifecycle.Hook) bedrock.App {
return runFunc(func(ctx context.Context) (err error) {
// Always run PostRun hook regardless if app returns an error or panics.
defer runPostRunHook(ctx, lifecycle.PostRun, &err)

defer runPostHook(&err, ctx, hook)
return app.Run(ctx)
})
}

func runPostRunHook(ctx context.Context, hook LifecycleHook, err *error) {
if hook == nil {
func runPostHook(err *error, ctx context.Context, hook lifecycle.Hook) {
hookErr := hook.Run(ctx)
if hookErr == nil {
return
}
if *err == nil {
*err = hookErr
return
}

hookErr := hook.Run(ctx)

// errors.Join will not return an error if both
// *err and hookErr are nil.
*err = errors.Join(*err, hookErr)
}
84 changes: 7 additions & 77 deletions app/app_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"context"
"fmt"

"github.com/z5labs/bedrock"
"github.com/z5labs/bedrock/lifecycle"
)

func ExampleRecover() {
Expand All @@ -24,91 +24,21 @@ func ExampleRecover() {
// Output: recovered from panic: hello world
}

func ExampleWithLifecycleHooks() {
var app bedrock.App = runFunc(func(ctx context.Context) error {
return nil
})

postRun := LifecycleHookFunc(func(ctx context.Context) error {
fmt.Println("ran post run hook")
return nil
})

app = WithLifecycleHooks(app, Lifecycle{
PostRun: postRun,
})

err := app.Run(context.Background())
if err != nil {
fmt.Println(err)
return
}

// Output: ran post run hook
}

func ExampleWithLifecycleHooks_unrecoveredPanic() {
var app bedrock.App = runFunc(func(ctx context.Context) error {
panic("hello world")
return nil
})

postRun := LifecycleHookFunc(func(ctx context.Context) error {
fmt.Println("ran post run hook")
func ExamplePostRun() {
app := runFunc(func(ctx context.Context) error {
return nil
})

app = WithLifecycleHooks(app, Lifecycle{
PostRun: postRun,
})

run := func(ctx context.Context) error {
// recover here so the panic doesn't crash the example
defer func() {
recover()
}()

return app.Run(ctx)
}

err := run(context.Background())
if err != nil {
fmt.Println(err)
return
}

// Output: ran post run hook
}

func ExampleComposeLifecycleHooks() {
var app bedrock.App = runFunc(func(ctx context.Context) error {
hook := lifecycle.HookFunc(func(ctx context.Context) error {
fmt.Println("hook ran")
return nil
})

app = WithLifecycleHooks(app, Lifecycle{
PostRun: ComposeLifecycleHooks(
LifecycleHookFunc(func(ctx context.Context) error {
fmt.Println("one")
return nil
}),
LifecycleHookFunc(func(ctx context.Context) error {
fmt.Println("two")
return nil
}),
LifecycleHookFunc(func(ctx context.Context) error {
fmt.Println("three")
return nil
}),
),
})

err := app.Run(context.Background())
err := PostRun(app, hook).Run(context.Background())
if err != nil {
fmt.Println(err)
return
}

// Output: one
// two
// three
// Output: hook ran
}
Loading

0 comments on commit cfc49dd

Please sign in to comment.