From 6b7b0f82a816971b5c94fdf5e355af25af9e2724 Mon Sep 17 00:00:00 2001 From: Yota Hamada Date: Sat, 17 Aug 2024 13:39:18 +0900 Subject: [PATCH] API improvements (#10) --- .gitignore | 6 +- Makefile | 11 +- README.md | 470 ++++++++---------- bus.go | 23 +- bus_test.go | 42 ++ command.go | 74 +-- context.go | 12 +- dew.go | 54 +- examples/authorization/auth.go | 12 +- examples/authorization/commands/action/org.go | 2 +- examples/authorization/handlers/org.go | 3 +- examples/authorization/main.go | 95 ++-- examples/hello-world/main.go | 45 +- mux.go | 9 +- mux_test.go | 243 ++++++--- 15 files changed, 650 insertions(+), 451 deletions(-) create mode 100644 bus_test.go diff --git a/.gitignore b/.gitignore index d739c59..41eb381 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -.idea -build/ +# Mac OS .DS_Store + +# Coverage files +coverage.* diff --git a/Makefile b/Makefile index 80073e7..2bbe989 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,12 @@ .PHONY: test test: - go clean -testcache - go test -race -v . + @go clean -testcache + @go test -race -v -coverprofile="coverage.txt" -covermode=atomic ./... + +.PHONY: test-coverage +open-coverage: + @go tool cover -html=coverage.txt bench: - go test -bench=. + @go test -bench=. + diff --git a/README.md b/README.md index e0793cc..374f86b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Dew: A Lightweight, Pragmatic Command Bus Library with Middleware System for Go +

Dew: A Lightweight, Pragmatic Command Bus Library with Middleware System for Go

[![Go Reference](https://pkg.go.dev/badge/github.com/go-dew/dew.svg)](https://pkg.go.dev/github.com/go-dew/dew) [![Go Report Card](https://goreportcard.com/badge/github.com/go-dew/dew)](https://goreportcard.com/report/github.com/go-dew/dew) @@ -6,109 +6,62 @@ dew logo -Dew frees us from the cognitive load for managing different interfaces for each operation handler or domain logic. It provides a lightweight command bus interface + Middleware System for Go. +Dew streamlines Go application development by providing a unified interface for handling operations and domain logic. It offers a lightweight command bus with an integrated middleware system, simplifying complex workflows and promoting clean, maintainable code architecture. dew overview -## Features - -- **Lightweight**: Clocks around 450 LOC with minimalistic design. -- **Pragmatic and Ergonomic**: Focused on developer experience and productivity. -- **Production Ready**: 100% test coverage. -- **Zero Dependencies**: No external dependencies. -- **Fast**: See [benchmarks](#benchmarks). - -## Installation - -```bash -go get github.com/go-dew/dew -``` - -## Example - -See [examples](examples) for more detailed examples. - -It's as easy as: - -```go -package main - -import ( - "context" - "fmt" - "github.com/go-dew/dew" -) - -// HelloAction is a simple action that greets the user. -type HelloAction struct { - Name string -} +

Table of Contents

+ +- [Features](#features) +- [Motivation](#motivation) +- [Terminology](#terminology) +- [Convention for Actions and Queries](#convention-for-actions-and-queries) +- [Installation](#installation) +- [Example](#example) +- [Usage](#usage) + - [Setting Up the Bus](#setting-up-the-bus) + - [Dispatching Actions](#dispatching-actions) + - [Executing Queries](#executing-queries) + - [Asynchronous Queries](#asynchronous-queries) + - [Middleware](#middleware) + - [Transaction Middleware Example](#transaction-middleware-example) + - [Grouping Handlers and Applying Middleware](#grouping-handlers-and-applying-middleware) +- [Testing](#testing) +- [Benchmarks](#benchmarks) +- [Contributing](#contributing) +- [License](#license) -// Validate checks if the name is valid. -func (c HelloAction) Validate(_ context.Context) error { - if c.Name == "" { - return fmt.Errorf("invalid name") - } - return nil -} - -func main() { - // Initialize the Command Bus. - bus := dew.New() +## Features - // Register the handler for the HelloAction. - bus.Register(new(HelloHandler)) +- **Lightweight**: Clocks around 450 LOC with minimalistic design. +- **Pragmatic and Ergonomic**: Focused on developer experience and productivity. +- **Production Ready**: 100% test coverage. +- **Zero Dependencies**: No external dependencies. +- **Fast**: See [benchmarks](#benchmarks). - // Alternatively, you can use the HandlerFunc to register the handler. - // bus.Register(dew.HandlerFunc[HelloAction](func(ctx context.Context, cmd *HelloAction) error { - // println(fmt.Sprintf("Hello, %s!", cmd.Name)) // Output: Hello, Dew! - // return nil - // })) +## Motivation - // Dispatch the action. - _ = dew.Dispatch(context.Background(), dew.NewAction(bus, &HelloAction{Name: "Dew"})) -} +Working on multiple complex backend applications in Go over the years, I've been seeking ways to enhance code readability, maintainability, and developer enjoyment. The Command Bus architecture emerged as a promising solution to these challenges. However, unable to find a library that met all my requirements, I created Dew. -type HelloHandler struct {} -func (h *HelloHandler) HandleHelloAction(ctx context.Context, cmd *HelloAction) error { - println(fmt.Sprintf("Hello, %s!", cmd.Name)) // Output: Hello, Dew! - return nil -} -``` +Dew is designed to be lightweight and dependency-free, facilitating easy integration into any Go project. It implements the [command-oriented interface](https://martinfowler.com/bliki/CommandOrientedInterface.html) pattern, promoting separation of concerns, modularization, and improved code readability while reducing cognitive load. ## Terminology -Dew uses the following terminology: - -- **Action**: Operations that change the application state. We use the term "Action" to avoid confusion with similar terms in Go. It's equivalent to what is commonly known as a "Command" in [Command Query Separation (CQS)](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation) and [Command Query Responsibility Segregation (CQRS)](https://martinfowler.com/bliki/CQRS.html) patterns. -- **Query**: Operations that retrieve data. -- **Middleware**: Functions that execute logic (e.g., logging, authorization, transaction management) before and after command execution. -- **Bus**: Manages registration of handlers and routing of actions and queries to their respective handlers. +Dew uses four key concepts: -## What is the Command Oriented Interface Pattern? +1. **Action**: An operation that changes the application state. Similar to a "Command" in CQRS patterns. +2. **Query**: An operation that retrieves data without modifying the application state. +3. **Middleware**: A function that processes Actions and Queries before and/or after they are handled. Used for cross-cutting concerns like logging, authorization, or transactions. +4. **Bus**: The central component that manages Actions, Queries, and Middleware. It routes operations to their appropriate handlers. -It utilizes the [command-oriented interface](https://martinfowler.com/bliki/CommandOrientedInterface.html) pattern, which allows for separation of concerns, modularization, and better readability of the codebase, eliminating unnecessary cognitive load. +## Convention for Actions and Queries -You can find more about the pattern in the following articles: - -- [Command Oriented Interface by Martin Fowler](https://martinfowler.com/bliki/CommandOrientedInterface.html) -- [What is a command bus and why should you use it?](https://barryvanveen.nl/articles/49-what-is-a-command-bus-and-why-should-you-use-it) -- [Laravel Command Bus Pattern](https://laravel.com/docs/5.0/bus) - -## Motivation +Dew follows these conventions for `Action` and `Query` interfaces: -I've been working on multiple complex backend applications built in Go over the years, and looking for a way to make the code more readable, maintainable, and more fun to work with. I believe Command Bus architecture could be an answer to this problem. However, I couldn't find a library that fits my needs, so I decided to create Dew. +- **Action Interface**: Each action must implement a `Validate` method to ensure the action's data is valid before processing. +- **Query Interface**: Each query implements the `Query` interface, which is an empty interface. Queries don't require a `Validate` method as they don't modify application state. -Dew is designed to be lightweight with zero dependencies, making it easy to integrate into any Go project. - -## A Convention for Actions and Queries - -Dew relies on a convention for `Action` and `Query` interfaces: - -- **Action Interface**: Each action in Dew must implement a `Validate` method, as defined by the `Action` interface. This `Validate` method is responsible for checking that the action's data is correct before it is processed. -- **Query Interface**: Each query in any struct that implements the `Query` interface, which is an empty interface. Queries do not need a `Validate` method because they do not change the state of the application. - -Here's a simple example of how both interfaces are defined and used: +Example: ```go // MyAction represents an Action @@ -129,16 +82,20 @@ type MyQuery struct { AccountID string } -// MyQuery does not need a Validate method because it does not change state +// MyQuery doesn't need a Validate method as it doesn't change state ``` -Also, we use the function `dew.Dispatch` to send actions and `dew.Query` to send queries to the bus. The bus will then route the action or query to the appropriate handler based on the action or query type. The reason for using different functions for actions and queries is to make the code more readable and simpler to work with. You will see this when you start using Dew in your projects. +## Installation -## Usage +```bash +go get github.com/go-dew/dew +``` -### Setting Up the Bus +## Example -Create a bus and register handlers: +See [examples](examples) for more detailed examples. + +Basic usage: ```go package main @@ -149,128 +106,105 @@ import ( "github.com/go-dew/dew" ) -type MyHandler struct {} - -type MyAction struct { - Message string +// HelloAction is a simple action that greets the user. +type HelloAction struct { + Name string } -func (h *MyHandler) HandleMyAction(ctx context.Context, a *MyAction) error { - // handle command - fmt.Println("Handling action:", a) +// Validate checks if the name is valid. +func (c HelloAction) Validate(_ context.Context) error { + if c.Name == "" { + return fmt.Errorf("invalid name") + } return nil } func main() { + // Initialize the Command Bus. bus := dew.New() - // Register handlers - bus.Register(new(MyHandler)) -} - -``` - -### Dispatching Commands + // Register the handler for the HelloAction. + bus.Register(new(HelloHandler)) -Use the `Dispatch` function to send commands: + // Create a context with the bus. + ctx := dew.NewContext(context.Background(), bus) -```go -func main() { - ctx := context.Background() - bus := dew.New() - bus.Register(new(MyHandler)) - - a := &MyAction{Message: "Hello, Dew!"} - if err := dew.Dispatch(ctx, dew.NewAction(a)); err != nil { - fmt.Println("Error dispatching command:", err) + // Dispatch the action. + result, err := dew.Dispatch(ctx, &HelloAction{Name: "Dew"}) + if err != nil { + fmt.Println("Error:", err) + } else { + fmt.Printf("Result: %+v\n", result) } } -``` - -### Executing Queries - -`Query` handling example: - -```go - -type MyHandler struct {} - -type MyQuery struct { - Question string - Result string -} -func (h *MyHandler) HandleMyQuery(ctx context.Context, query *MyQuery) error { - // Return query result - query.Result = "Dew is a command bus library for Go." +type HelloHandler struct {} +func (h *HelloHandler) HandleHelloAction(ctx context.Context, cmd *HelloAction) error { + fmt.Printf("Hello, %s!\n", cmd.Name) // Output: Hello, Dew! return nil } +``` -func main() { - ctx := context.Background() - bus := dew.New() - bus.Register(new(MyHandler)) +## Usage - result, err := dew.Query(ctx, bus, &MyQuery{Question: "What is Dew?"}) - if err != nil { - fmt.Println("Error executing query:", err) - } else { - fmt.Println("Query result:", result.Result) - } -} +### Setting Up the Bus + +Create a bus and register handlers: + +```go +bus := dew.New() +bus.Register(new(MyHandler)) ``` -Dew provides `QueryAsync`, which allows for handling multiple queries concurrently. +### Dispatching Actions -`QueryAsync` usage example: +Use the `Dispatch` function to send actions: ```go -type AccountQuery struct { - AccountID string - Result float64 -} - -type WeatherQuery struct { - City string - Result string +ctx := dew.NewContext(context.Background(), bus) +result, err := dew.Dispatch(ctx, &MyAction{Message: "Hello, Dew!"}) +if err != nil { + fmt.Println("Error dispatching action:", err) +} else { + fmt.Printf("Action result: %+v\n", result) } +``` -type AccountHandler struct {} -type WeatherHandler struct {} +### Executing Queries -func (h *AccountHandler) HandleQuery(ctx context.Context, query *AccountQuery) error { - // Logic to retrieve account balance - query.Result = 10234.56 // Simulated balance - return nil -} +Use the `Query` function to execute queries: -func (h *WeatherHandler) HandleQuery(ctx context.Context, query *WeatherQuery) error { - // Logic to fetch weather forecast - query.Result = "Sunny with a chance of rain" // Simulated forecast - return nil +```go +ctx := dew.NewContext(context.Background(), bus) +result, err := dew.Query(ctx, &MyQuery{Question: "What is Dew?"}) +if err != nil { + fmt.Println("Error executing query:", err) +} else { + fmt.Printf("Query result: %+v\n", result) } +``` -func main() { - ctx := context.Background() - bus := dew.New() - bus.Register(new(AccountHandler)) - bus.Register(new(WeatherHandler)) +### Asynchronous Queries - accountQuery := &AccountQuery{AccountID: "12345"} - weatherQuery := &WeatherQuery{City: "New York"} +Use `QueryAsync` for handling multiple queries concurrently: - if err := dew.QueryAsync(ctx, dew.NewQuery(accountQuery), dew.NewQuery(weatherQuery)); err != nil { - fmt.Println("Error executing queries:", err) - } else { - fmt.Println("Account Balance for ID 12345:", accountQuery.Result) - fmt.Println("Weather in New York:", weatherQuery.Result) - } +```go +ctx := dew.NewContext(context.Background(), bus) +accountQuery := &AccountQuery{AccountID: "12345"} +weatherQuery := &WeatherQuery{City: "New York"} + +err := dew.QueryAsync(ctx, dew.NewQuery(accountQuery), dew.NewQuery(weatherQuery)) +if err != nil { + fmt.Println("Error executing queries:", err) +} else { + fmt.Println("Account Balance for ID 12345:", accountQuery.Result) + fmt.Println("Weather in New York:", weatherQuery.Result) } ``` ### Middleware -Middleware can be used to execute logic before and after command or query execution. Here is an example of a simple logging middleware: +Middleware can be used to execute logic before and after command or query execution: ```go func loggingMiddleware(next dew.Middleware) dew.Middleware { @@ -289,63 +223,9 @@ func main() { } ``` -Here is the interface for middleware: - -```go -// MiddlewareFunc is a type adapter to convert a function to a Middleware. -type MiddlewareFunc func(ctx Context) error - -// Handle calls the function h(ctx, command). -func (h MiddlewareFunc) Handle(ctx Context) error { - return h(ctx) -} - -// Middleware is an interface for handling middleware. -type Middleware interface { - // Handle executes the middleware. - Handle(ctx Context) error -} -``` - -### Grouping Handlers and Applying Middleware - -It's easy to group handlers and apply middleware to a group. You can also nest groups to apply middleware to a subset of handlers. It allows for a clean separation of concerns and reduces code duplication across handlers. - -Here is an example of grouping handlers and applying middleware: - -```go -func main() { - bus := dew.New() - bus.Group(func(bus dew.Bus) { - // Transaction middleware - bus.Use(dew.ACTION, middleware.Transaction) - // Logger middleware - bus.Use(dew.ALL, middleware.Logger) - // Register handlers - bus.Register(new(UserProfileHandler)) - - // Sub-grouping - bus.Group(func(g dew.Bus) { - // Tracing middleware - bus.Use(dew.ACTION, middleware.Tracing) - // Register sensitive handlers - bus.Register(new(SensitiveHandler)) - }) - - // Register more handlers - }) -} -``` - -### Notes about Middleware - -- Middleware for handlers can be applied per command or query, based on the `dew.ACTION`, `dew.QUERY` and `dew.ALL` constants. -- Middleware can be applied multiple times because they are executed per command or query. So make sure the middleware is idempotent when necessary. -- Middleware for `Dispatch` and `Query` functions can be configured using the `UseDispatch()` and `UseQuery()` methods on the bus. This middleware is executed once per `Dispatch` or `Query` call. +#### Transaction Middleware Example -## Middleware Examples: Handling Transactions in Dispatch - -Here is an example of a middleware that starts a transaction at the beginning of a command dispatch and rolls it back if any error occurs during the command's execution. +Here's an example of a middleware that manages database transactions: ```go package main @@ -411,9 +291,38 @@ func main() { } ``` -## Testing Example: Mocking Command Handlers +This middleware example demonstrates how to: + +1. Start a new database transaction before executing a command. +2. Attach the transaction to the context for use in handlers. +3. Commit the transaction if the command executes successfully. +4. Roll back the transaction if an error occurs during command execution. + +This pattern is particularly useful for ensuring data consistency across multiple database operations within a single command. + +### Grouping Handlers and Applying Middleware + +Group handlers and apply middleware to a subset of handlers: + +```go +func main() { + bus := dew.New() + bus.Group(func(bus dew.Bus) { + bus.Use(dew.ACTION, middleware.Transaction) + bus.Use(dew.ALL, middleware.Logger) + bus.Register(new(UserProfileHandler)) + + bus.Group(func(g dew.Bus) { + bus.Use(dew.ACTION, middleware.Tracing) + bus.Register(new(SensitiveHandler)) + }) + }) +} +``` + +## Testing -To mock command handlers for testing, you can create a new bus instance and register the mock handlers. +Testing with Dew is straightforward. You can create mock handlers and use them in your tests. Here's an example: ```go package example_test @@ -425,41 +334,86 @@ import ( "testing" ) -func TestExample(t *testing.T) { +func TestCreateUser(t *testing.T) { // Create a new bus instance - mux := dew.New() + bus := dew.New() - // Register your mock handlers - mux.Register(dew.HandlerFunc[CreateUserAction]( - func(ctx context.Context, command *CreateUserAction) error { - // mock logic - return nil - }, - )) + // Create a mock handler + mockHandler := &MockCreateUserHandler{ + t: t, + expectedName: "John Doe", + expectedEmail: "john@example.com", + } + + // Register the mock handler + bus.Register(mockHandler) + + // Create a context with the bus + ctx := dew.NewContext(context.Background(), bus) + + // Create the action + createUserAction := &action.CreateUserAction{ + Name: "John Doe", + Email: "john@example.com", + } + + // Dispatch the action + _, err := dew.Dispatch(ctx, createUserAction) + + // Check for errors + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Check if the mock handler was called + if !mockHandler.called { + t.Fatalf("Expected mock handler to be called") + } +} + +type MockCreateUserHandler struct { + t *testing.T + expectedName string + expectedEmail string + called bool +} - // test your code +func (m *MockCreateUserHandler) HandleCreateUserAction(ctx context.Context, action *action.CreateUserAction) error { + m.called = true + if action.Name != m.expectedName { + m.t.Errorf("Expected name %s, got %s", m.expectedName, action.Name) + } + if action.Email != m.expectedEmail { + m.t.Errorf("Expected email %s, got %s", m.expectedEmail, action.Email) + } + return nil } ``` +This example demonstrates how to: + +1. Create a mock bus and handler for testing. +2. Register the mock handler with the bus. +3. Create and dispatch an action. +4. Verify that the action was handled correctly by the mock handler. + +You can extend this pattern to test various scenarios, including error cases and different types of actions and queries. + ## Benchmarks Results as of May 23, 2024 with Go 1.22.2 on darwin/arm64 -```shell -BenchmarkMux/query-12 3012015 393.5 ns/op 168 B/op 7 allocs/op -BenchmarkMux/dispatch-12 2854291 419.1 ns/op 192 B/op 8 allocs/op -BenchmarkMux/query-with-middleware-12 2981778 407.8 ns/op 168 B/op 7 allocs/op -BenchmarkMux/dispatch-with-middleware-12 2699398 446.8 ns/op 192 B/op 8 allocs/op +``` +BenchmarkMux/query-12 3012015 393.5 ns/op 168 B/op 7 allocs/op +BenchmarkMux/dispatch-12 2854291 419.1 ns/op 192 B/op 8 allocs/op +BenchmarkMux/query-with-middleware-12 2981778 407.8 ns/op 168 B/op 7 allocs/op +BenchmarkMux/dispatch-with-middleware-12 2699398 446.8 ns/op 192 B/op 8 allocs/op ``` ## Contributing We welcome contributions to Dew! Please see the [contribution guide](CONTRIBUTING.md) for more information. -## Credits - -- The implementation of Trie data structure is inspired by [go-chi/chi](https://github.com/go-chi/chi). - ## License -Licensed under [MIT License](https://github.com/go-dew/dew/blob/main/LICENSE) +Licensed under [MIT License](https://github.com/go-dew/dew/blob/main/LICENSE) \ No newline at end of file diff --git a/bus.go b/bus.go index 45cb18a..d69fd62 100644 --- a/bus.go +++ b/bus.go @@ -25,9 +25,26 @@ type Bus interface { UseQuery(middlewares ...func(next Middleware) Middleware) } +type busKey struct{} + +// NewContext creates a new context with the given bus. +func NewContext(ctx context.Context, bus Bus) context.Context { + return context.WithValue(ctx, busKey{}, bus) +} + // FromContext returns the bus from the context. -func FromContext(ctx context.Context) Bus { - return ctx.Value(busCtxKey{}).(Bus) +func FromContext(ctx context.Context) (Bus, bool) { + bus, ok := ctx.Value(busKey{}).(Bus) + return bus, ok +} + +// MustFromContext returns the bus from the context, panicking if not found. +func MustFromContext(ctx context.Context) Bus { + bus, ok := FromContext(ctx) + if !ok { + panic("bus not found in context") + } + return bus } // Context represents the context for a command execution. @@ -49,5 +66,3 @@ type HandlerFunc[T any] func(ctx context.Context, command *T) error func (f HandlerFunc[T]) Handle(ctx context.Context, command *T) error { return f(ctx, command) } - -type busCtxKey struct{} diff --git a/bus_test.go b/bus_test.go new file mode 100644 index 0000000..5d451bc --- /dev/null +++ b/bus_test.go @@ -0,0 +1,42 @@ +package dew + +import ( + "context" + "testing" +) + +func TestMustFromContext(t *testing.T) { + t.Run("Panic if bus is not found in context", func(t *testing.T) { + defer func() { + // recover from panic + if r := recover(); r != nil { + // check if the panic message is the expected one + if r != "bus not found in context" { + t.Errorf("expected panic message: bus not found in context, got: %v", r) + } + } else { + t.Error("expected panic, got none") + } + }() + ctx := context.Background() + MustFromContext(ctx) + }) +} + +func TestNewContext(t *testing.T) { + t.Run("Return a new context with the given bus", func(t *testing.T) { + bus := New() + ctx := NewContext(context.Background(), bus) + if ctx == nil { + t.Error("expected context, got nil") + } + // check if the bus is in the context + b, ok := FromContext(ctx) + if !ok { + t.Error("expected bus in context, got none") + } + if b != bus { + t.Errorf("expected bus: %v, got: %v", bus, b) + } + }) +} diff --git a/command.go b/command.go index 2977e6d..f76bbfa 100644 --- a/command.go +++ b/command.go @@ -30,27 +30,26 @@ type CommandHandler[T Command] interface { Handle(ctx Context) error Command() Command Mux() *mux + Resolve(bus Bus) error } // NewAction creates an object that can be dispatched. // It panics if the handler is not found. -func NewAction[T Action](bus Bus, cmd *T) CommandHandler[T] { - h, mx := resolveHandler[T](bus) - return command[T]{ - mux: mx, - cmd: cmd, - handler: h, +func NewAction[T Action](cmd *T) CommandHandler[T] { + typ := typeFor[T]() + return &command[T]{ + cmd: cmd, + typ: typ, } } // NewQuery creates an object that can be dispatched. // It panics if the handler is not found. -func NewQuery[T QueryAction](bus Bus, cmd *T) CommandHandler[T] { - h, mx := resolveHandler[T](bus) - return command[T]{ - mux: mx, - cmd: cmd, - handler: h, +func NewQuery[T QueryAction](cmd *T) CommandHandler[T] { + typ := typeFor[T]() + return &command[T]{ + cmd: cmd, + typ: typ, } } @@ -59,20 +58,44 @@ type command[T Command] struct { mux *mux cmd *T handler HandlerFunc[T] + typ reflect.Type } -func (c command[T]) Handle(ctx Context) error { +func (c *command[T]) Handle(ctx Context) error { return c.handler(ctx.Context(), c.cmd) } -func (c command[T]) Command() Command { +func (c *command[T]) Command() Command { return c.cmd } -func (c command[T]) Mux() *mux { +func (c *command[T]) Mux() *mux { return c.mux } +func (c *command[T]) Resolve(bus Bus) error { + mx := bus.(*mux) + + h, mxx, ok := loadHandlerCache[T](c.typ, mx) + if ok { + c.handler = h + c.mux = mxx + return nil + } + + entry, ok := mx.entries.Load(c.typ) + if ok { + hh := entry.(*handler) + hhh := convertInterface[HandlerFunc[T]](hh.handler) + storeCache[T](mx.cache, c.typ, hh.mux, hhh) + c.handler = hhh + c.mux = hh.mux + return nil + } + + return fmt.Errorf("handler not found for %v", c.typ) +} + func convertInterface[T any](i any) T { var v T reflect.NewAt(reflect.TypeOf(v), unsafe.Pointer(&v)).Elem().Set(reflect.ValueOf(i)) @@ -99,27 +122,6 @@ func loadHandlerCache[T Command](typ reflect.Type, mx *mux) (HandlerFunc[T], *mu return nil, nil, false } -// resolveHandler returns the handler and mux for the given command. -func resolveHandler[T Command](bus Bus) (HandlerFunc[T], *mux) { - typ := typeFor[T]() - mx := bus.(*mux) - - h, mxx, ok := loadHandlerCache[T](typ, mx) - if ok { - return h, mxx - } - - entry, ok := mx.entries.Load(typ) - if ok { - hh := entry.(*handler) - hhh := convertInterface[HandlerFunc[T]](hh.handler) - storeCache[T](mx.cache, typ, hh.mux, hhh) - return hhh, hh.mux - } - - panic(fmt.Sprintf("handler not found for %s/%s", typ.PkgPath(), typ.String())) -} - // typeFor returns the reflect.Type for the given type. func typeFor[T any]() reflect.Type { var t T diff --git a/context.go b/context.go index 12bd3b9..86a1c48 100644 --- a/context.go +++ b/context.go @@ -2,6 +2,9 @@ package dew import "context" +var _ Context = (*BusContext)(nil) + +// BusContext represents the context for a command execution. type BusContext struct { ctx context.Context @@ -12,10 +15,6 @@ type BusContext struct { handler internalHandler } -func NewContext() *BusContext { - return &BusContext{} -} - type internalHandler interface { Handle(ctx Context) error Command() Command @@ -51,10 +50,7 @@ func (c *BusContext) Reset() { // Context returns the underlying context.Context. // If no context is set, it returns context.Background(). func (c *BusContext) Context() context.Context { - if c.ctx != nil { - return c.ctx - } - return context.Background() + return c.ctx } // WithValue returns a new Context with the given key-value pair added to the context. diff --git a/dew.go b/dew.go index e7438ab..6f6ec48 100644 --- a/dew.go +++ b/dew.go @@ -13,16 +13,32 @@ var ( ) // Dispatch executes the action. +func Dispatch[T Action](ctx context.Context, action *T) (*T, error) { + return action, DispatchMulti(ctx, NewAction(action)) +} + +// DispatchMulti executes all actions synchronously. // It assumes that all handlers have been registered to the same mux. -func Dispatch(ctx context.Context, actions ...CommandHandler[Action]) error { +func DispatchMulti(ctx context.Context, actions ...CommandHandler[Action]) error { if len(actions) == 0 { return nil } - mux := actions[0].Mux().root() + bus, ok := FromContext(ctx) + if !ok { + return errors.New("bus not found in context") + } + + for _, action := range actions { + if err := action.Resolve(bus); err != nil { + return err + } + } + + mux := bus.(*mux) rctx := mux.pool.Get().(*BusContext) rctx.Reset() - rctx.ctx = context.WithValue(ctx, busCtxKey{}, mux) + rctx.ctx = context.WithValue(ctx, busKey{}, mux) defer mux.pool.Put(rctx) @@ -40,13 +56,22 @@ func Dispatch(ctx context.Context, actions ...CommandHandler[Action]) error { } // Query executes the query and returns the result. -func Query[T QueryAction](ctx context.Context, bus Bus, query *T) (*T, error) { - queryObj := NewQuery(bus, query) - mux := queryObj.Mux().root() +func Query[T QueryAction](ctx context.Context, query *T) (*T, error) { + bus, ok := FromContext(ctx) + if !ok { + return nil, errors.New("bus not found in context") + } + + queryObj := NewQuery(query) + if err := queryObj.Resolve(bus); err != nil { + return nil, err + } + + mux := bus.(*mux) rctx := mux.pool.Get().(*BusContext) rctx.Reset() - rctx.ctx = context.WithValue(ctx, busCtxKey{}, mux) + rctx.ctx = context.WithValue(ctx, busKey{}, mux) defer mux.pool.Put(rctx) @@ -65,11 +90,22 @@ func QueryAsync(ctx context.Context, queries ...CommandHandler[Command]) error { if len(queries) == 0 { return nil } - mux := queries[0].Mux().root() + bus, ok := FromContext(ctx) + if !ok { + return errors.New("bus not found in context") + } + + for _, query := range queries { + if err := query.Resolve(bus); err != nil { + return err + } + } + + mux := bus.(*mux) rctx := mux.pool.Get().(*BusContext) // Get a context from the pool. rctx.Reset() - rctx.ctx = context.WithValue(ctx, busCtxKey{}, mux) + rctx.ctx = context.WithValue(ctx, busKey{}, mux) defer mux.pool.Put(rctx) // Ensure the context is put back into the pool. diff --git a/examples/authorization/auth.go b/examples/authorization/auth.go index 9d4371d..cdb2fdd 100644 --- a/examples/authorization/auth.go +++ b/examples/authorization/auth.go @@ -12,22 +12,22 @@ type CurrentUser struct { ID int } -func ctxWithCurrUser(ctx context.Context, u *CurrentUser) context.Context { +func authContext(ctx context.Context, u *CurrentUser) context.Context { return context.WithValue(ctx, userCtxKey{}, u) } -func currUserFromCtx(ctx context.Context) *CurrentUser { +func getCurrentUser(ctx context.Context) *CurrentUser { return ctx.Value(userCtxKey{}).(*CurrentUser) } -// isAuthorized checks if the current user is authorized. -func isAuthorized(ctx context.Context) bool { - return currUserFromCtx(ctx).ID == AdminID +// isAdmin checks if the current user is authorized. +func isAdmin(ctx context.Context) bool { + return getCurrentUser(ctx).ID == adminID } func AdminOnly(next dew.Middleware) dew.Middleware { return dew.MiddlewareFunc(func(ctx dew.Context) error { - if !isAuthorized(ctx.Context()) { + if !isAdmin(ctx.Context()) { // Return an unauthorized error. return ErrUnauthorized } diff --git a/examples/authorization/commands/action/org.go b/examples/authorization/commands/action/org.go index 88183b1..eefec17 100644 --- a/examples/authorization/commands/action/org.go +++ b/examples/authorization/commands/action/org.go @@ -20,5 +20,5 @@ func (c UpdateOrgAction) Validate(_ context.Context) error { } func (c UpdateOrgAction) Log() string { - return fmt.Sprintf("Updating organization with name: %s", c.Name) + return fmt.Sprintf("logging: %s", c.Name) } diff --git a/examples/authorization/handlers/org.go b/examples/authorization/handlers/org.go index ea853ab..d62c4b3 100644 --- a/examples/authorization/handlers/org.go +++ b/examples/authorization/handlers/org.go @@ -16,11 +16,10 @@ func NewOrgHandler() *OrgHandler { } func (h *OrgHandler) UpdateOrg(_ context.Context, command *action.UpdateOrgAction) error { - println("Updating organization name:", command.Name) return nil } func (h *OrgHandler) GetOrgDetails(_ context.Context, command *query.GetOrgDetailsQuery) error { - command.Result = "Get organization details" + command.Result = "Success" return nil } diff --git a/examples/authorization/main.go b/examples/authorization/main.go index 0fb0a38..0f99cae 100644 --- a/examples/authorization/main.go +++ b/examples/authorization/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "log" "github.com/go-dew/dew" "github.com/go-dew/dew/examples/authorization/commands/action" @@ -10,44 +11,80 @@ import ( "github.com/go-dew/dew/examples/authorization/handlers" ) +var ErrUnauthorized = fmt.Errorf("unauthorized") + var ( - // User IDs for the example. - AdminID = 1 - MemberID = 2 + adminID = 1 + memberID = 2 ) -var ErrUnauthorized = fmt.Errorf("unauthorized") - func main() { - // Initialize the Command Bus. - bus := dew.New() + if err := run(); err != nil { + log.Fatalf("Error: %v", err) + } +} + +func run() error { + bus := initializeBus() + + fmt.Println("--- Authorization Example ---") + + if err := runMemberScenario(bus); err != nil { + return fmt.Errorf("member scenario failed: %w", err) + } + + if err := runAdminScenario(bus); err != nil { + return fmt.Errorf("admin scenario failed: %w", err) + } + + fmt.Println("\n--- Authorization Example finished ---") + return nil +} - // Group the handlers and middleware for organization profile authorization. +func initializeBus() dew.Bus { + bus := dew.New() bus.Group(func(bus dew.Bus) { - // Set the authorization middleware bus.Use(dew.ACTION, AdminOnly) - - // Register logging middleware. bus.Use(dew.ALL, LogCommand) - - // Register the organization profile handler. bus.Register(handlers.NewOrgHandler()) }) + return bus +} + +func runMemberScenario(bus dew.Bus) error { + busContext := dew.NewContext(context.Background(), bus) + memberContext := authContext(busContext, &CurrentUser{ID: memberID}) + + fmt.Println("\n1. Execute a query to get the organization profile (should succeed for member).") + orgProfile, err := dew.Query(memberContext, &query.GetOrgDetailsQuery{}) + if err != nil { + return fmt.Errorf("unexpected error in GetOrgDetailsQuery: %w", err) + } + fmt.Printf("Organization Profile: %s\n", orgProfile.Result) + + fmt.Println("\n2. Dispatch an action to update the organization profile (should fail for member).") + _, err = dew.Dispatch(memberContext, &action.UpdateOrgAction{Name: "Foo"}) + if err == nil { + return fmt.Errorf("expected unauthorized error, got nil") + } + if err != ErrUnauthorized { + return fmt.Errorf("expected unauthorized error, got: %w", err) + } + fmt.Printf("Expected unauthorized error: %v\n", err) + + return nil +} + +func runAdminScenario(bus dew.Bus) error { + busContext := dew.NewContext(context.Background(), bus) + adminContext := authContext(busContext, &CurrentUser{ID: adminID}) + + fmt.Println("\n3. Dispatch an action to update the organization profile (should succeed for admin).") + err := dew.DispatchMulti(adminContext, dew.NewAction(&action.UpdateOrgAction{Name: "Foo"})) + if err != nil { + return fmt.Errorf("unexpected error in UpdateOrgAction: %w", err) + } + fmt.Println("\nOrganization profile updated successfully.") - // Dispatch an action to update the organization profile. Which should fail because the user is not authorized. - ctx := ctxWithCurrUser(context.Background(), &CurrentUser{ID: MemberID}) - err := dew.Dispatch(ctx, dew.NewAction(bus, &action.UpdateOrgAction{Name: "Dew"})) - println(fmt.Sprintf("Error: %v", err)) // Output: Error: unauthorized - - // Dispatch an action to update the organization profile. Which should succeed because the user is authorized. - ctx = ctxWithCurrUser(context.Background(), &CurrentUser{ID: AdminID}) - err = dew.Dispatch(ctx, dew.NewAction(bus, &action.UpdateOrgAction{Name: "Dew"})) - println(fmt.Sprintf("Error: %v", err)) // Output: Error: - - // Execute a query to get the organization profile. - ctx = ctxWithCurrUser(context.Background(), &CurrentUser{ID: MemberID}) - orgProfile, err := dew.Query(ctx, bus, &query.GetOrgDetailsQuery{}) - println( - fmt.Sprintf("Organization Profile: %s, Error: %v", orgProfile, err), - ) // Output: Organization Profile: , Error: + return nil } diff --git a/examples/hello-world/main.go b/examples/hello-world/main.go index a33e09a..2a82ea1 100644 --- a/examples/hello-world/main.go +++ b/examples/hello-world/main.go @@ -3,31 +3,60 @@ package main import ( "context" "fmt" + "log" + "os" "github.com/go-dew/dew" ) +// HelloAction represents the action of greeting someone. type HelloAction struct { Name string } +// Validate checks if the HelloAction is valid. func (c HelloAction) Validate(_ context.Context) error { if c.Name == "" { - return fmt.Errorf("invalid name") + return fmt.Errorf("name cannot be empty") } return nil } +// HelloHandler handles the HelloAction. +type HelloHandler struct{} + +// HandleHello is the handler function for HelloAction. +func (h *HelloHandler) HandleHello(ctx context.Context, cmd *HelloAction) error { + fmt.Printf("Hello, %s!\n", cmd.Name) + return nil +} + func main() { + if err := run(); err != nil { + log.Fatalf("Error: %v", err) + } +} + +func run() error { // Initialize the Command Bus. bus := dew.New() - // Register handler for HelloArgs. - bus.Register(dew.HandlerFunc[HelloAction](func(ctx context.Context, cmd *HelloAction) error { - println(fmt.Sprintf("Hello, %s!", cmd.Name)) // Output: Hello, Dew! - return nil - })) + // Register HelloHandler. + bus.Register(&HelloHandler{}) - // Dispatch HelloArgs. - _ = dew.Dispatch(context.Background(), dew.NewAction(bus, &HelloAction{Name: "Dew"})) + // Create a context with the bus. + ctx := dew.NewContext(context.Background(), bus) + + // Get the name from command-line arguments or use a default. + name := "Dew" + if len(os.Args) > 1 { + name = os.Args[1] + } + + // Create and dispatch HelloAction. + if _, err := dew.Dispatch(ctx, &HelloAction{Name: name}); err != nil { + return fmt.Errorf("failed to dispatch HelloAction: %w", err) + } + + return nil } diff --git a/mux.go b/mux.go index ebf43dc..07fcd0a 100644 --- a/mux.go +++ b/mux.go @@ -57,19 +57,12 @@ const ( func newMux() *mux { mux := &mux{entries: &sync.Map{}, pool: &sync.Pool{}} mux.pool.New = func() interface{} { - return NewContext() + return &BusContext{} } mux.cache = &syncMap{kv: make(map[reflect.Type]any)} return mux } -func (mx *mux) root() *mux { - if mx.parent == nil { - return mx - } - return mx.parent.root() -} - // Use appends the middlewares to the mux middleware chain. // The middleware chain will be executed in the order they were added. func (mx *mux) Use(op OpType, middlewares ...func(next Middleware) Middleware) { diff --git a/mux_test.go b/mux_test.go index 73bfa3b..da36db6 100644 --- a/mux_test.go +++ b/mux_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "sync/atomic" "testing" "time" @@ -15,57 +16,129 @@ func TestMux_BasicCommand(t *testing.T) { mux := dew.New() mux.Register(new(userHandler)) mux.Register(new(postHandler)) + ctx := dew.NewContext(context.Background(), mux) createUser := &createUser{Name: "john"} - testRunDispatch(t, dew.NewAction(mux, createUser)) + testRunDispatch(t, ctx, dew.NewAction(createUser)) if createUser.Result != "user created" { t.Fatalf("unexpected result: %s", createUser.Result) } createPost := &createPost{Title: "hello"} - testRunDispatch(t, dew.NewAction(mux, createPost)) + testRunDispatch(t, ctx, dew.NewAction(createPost)) if createPost.Result != "post created" { t.Fatalf("unexpected result: %s", createPost.Result) } } +func TestMux_DispatchError(t *testing.T) { + t.Run("BusNotFound", func(t *testing.T) { + ctx := context.Background() + err := dew.DispatchMulti(ctx, dew.NewAction(&createUser{Name: "john"})) + if err == nil { + t.Fatal("expected an error, but got nil") + } + if !strings.Contains(err.Error(), "bus not found") { + t.Fatalf("unexpected error: %v", err) + } + }) + t.Run("ResolveError", func(t *testing.T) { + mux := dew.New() + ctx := dew.NewContext(context.Background(), mux) + err := dew.DispatchMulti(ctx, dew.NewAction(&createUser{Name: "john"})) + if err == nil { + t.Fatal("expected an error, but got nil") + } + if !strings.Contains(err.Error(), "handler not found") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestMux_QueryError(t *testing.T) { + t.Run("BusNotFound", func(t *testing.T) { + _, err := dew.Query(context.Background(), &findUser{ID: 1}) + if err == nil { + t.Fatal("expected an error, but got nil") + } + if !strings.Contains(err.Error(), "bus not found") { + t.Fatalf("unexpected error: %v", err) + } + }) + t.Run("ResolveError", func(t *testing.T) { + mux := dew.New() + ctx := dew.NewContext(context.Background(), mux) + _, err := dew.Query(ctx, &findUser{ID: 1}) + if err == nil { + t.Fatal("expected an error, but got nil") + } + if !strings.Contains(err.Error(), "handler not found") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestMux_QueryAsyncError(t *testing.T) { + t.Run("BusNotFound", func(t *testing.T) { + err := dew.QueryAsync(context.Background(), dew.NewQuery(&findUser{ID: 1})) + if err == nil { + t.Fatal("expected an error, but got nil") + } + if !strings.Contains(err.Error(), "bus not found") { + t.Fatalf("unexpected error: %v", err) + } + }) + t.Run("ResolveError", func(t *testing.T) { + mux := dew.New() + ctx := dew.NewContext(context.Background(), mux) + err := dew.QueryAsync(ctx, dew.NewQuery(&findUser{ID: 1})) + if err == nil { + t.Fatal("expected an error, but got nil") + } + if !strings.Contains(err.Error(), "handler not found") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + func TestMux_ValueTypeHandler(t *testing.T) { var userHandler userHandler mux := dew.New() mux.Register(userHandler) + ctx := dew.NewContext(context.Background(), mux) createUser := &createUser{Name: "john"} - testRunDispatch(t, dew.NewAction(mux, createUser)) + testRunDispatch(t, ctx, dew.NewAction(createUser)) if createUser.Result != "user created" { t.Fatalf("unexpected result: %s", createUser.Result) } } func TestMux_HandlerNotFound(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Fatal("expected a panic") - } - }() - mux := dew.New() + ctx := dew.NewContext(context.Background(), mux) - _ = dew.NewAction(mux, &createUser{Name: "john"}) + action := dew.NewAction(&createUser{Name: "john"}) + err := dew.DispatchMulti(ctx, action) + if err == nil { + t.Fatal("expected an error, but got nil") + } } func TestMux_Query(t *testing.T) { mux := dew.New() mux.Register(new(userHandler)) + ctx := dew.NewContext(context.Background(), mux) // Test successful query - result := testRunQuery(t, mux, &findUser{ID: 1}) + result := testRunQuery(t, ctx, &findUser{ID: 1}) if result.Result != "john" { t.Fatalf("unexpected result: %s", result.Result) } // Test query error - _, err := dew.Query(context.Background(), mux, &findUser{ID: 2}) + _, err := dew.Query(ctx, &findUser{ID: 2}) if err == nil { t.Fatal("expected an error, but got nil") } @@ -104,20 +177,22 @@ func TestMux_QueryAsync(t *testing.T) { }, )) + ctx := dew.NewContext(context.Background(), mux) + commands := dew.Commands{ - dew.NewQuery(mux, &findUser{ID: 1}), - dew.NewQuery(mux, &findUser{ID: 2}), - dew.NewQuery(mux, &findUser{ID: 3}), - dew.NewQuery(mux, &findPost{ID: 1}), - dew.NewQuery(mux, &findPost{ID: 2}), - dew.NewQuery(mux, &findPost{ID: 3}), + dew.NewQuery(&findUser{ID: 1}), + dew.NewQuery(&findUser{ID: 2}), + dew.NewQuery(&findUser{ID: 3}), + dew.NewQuery(&findPost{ID: 1}), + dew.NewQuery(&findPost{ID: 2}), + dew.NewQuery(&findPost{ID: 3}), } // count time now := time.Now() // query - err := dew.QueryAsync(context.Background(), commands...) + err := dew.QueryAsync(ctx, commands...) if err != nil { t.Fatal(err) } @@ -172,13 +247,16 @@ func TestMux_QueryAsync_Error(t *testing.T) { return errPostNotFound }, )) + + ctx := dew.NewContext(context.Background(), mux) + commands := dew.Commands{ - dew.NewQuery(mux, &findUser{ID: 1}), - dew.NewQuery(mux, &findPost{ID: 1}), + dew.NewQuery(&findUser{ID: 1}), + dew.NewQuery(&findPost{ID: 1}), } // query - err := dew.QueryAsync(context.Background(), commands...) + err := dew.QueryAsync(ctx, commands...) if err == nil { t.Fatal("expected an error, but got nil") } @@ -205,11 +283,11 @@ func TestMux_Reentrant(t *testing.T) { mux.Register(dew.HandlerFunc[findUserPost]( func(ctx context.Context, query *findUserPost) error { - findUserQuery, err := dew.Query(ctx, dew.FromContext(ctx), &findUser{ID: query.ID}) + findUserQuery, err := dew.Query(ctx, &findUser{ID: query.ID}) if err != nil { return err } - postQuery, err := dew.Query(ctx, dew.FromContext(ctx), &findPost{ID: query.ID}) + postQuery, err := dew.Query(ctx, &findPost{ID: query.ID}) if err != nil { return err } @@ -219,7 +297,9 @@ func TestMux_Reentrant(t *testing.T) { }, )) - query, err := dew.Query(context.Background(), mux, &findUserPost{ID: 1}) + ctx := dew.NewContext(context.Background(), mux) + + query, err := dew.Query(ctx, &findUserPost{ID: 1}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -265,14 +345,16 @@ func TestMux_Middlewares(t *testing.T) { }, )) + ctx := dew.NewContext(context.Background(), mux) + command := &createUser{Name: "test"} - testRunDispatch(t, dew.NewAction(mux, command)) + testRunDispatch(t, ctx, dew.NewAction(command)) if command.Result != "[all][action]" { t.Fatalf("unexpected result: %s", command.Result) } query := &findUser{ID: 1} - result, err := dew.Query(context.Background(), mux, query) + result, err := dew.Query(ctx, query) if err != nil { t.Fatal(err) } @@ -282,7 +364,7 @@ func TestMux_Middlewares(t *testing.T) { // dispatch no action - if err := dew.Dispatch(context.Background()); err != nil { + if err := dew.DispatchMulti(ctx); err != nil { t.Fatalf("unexpected error: %v", err) } } @@ -310,13 +392,15 @@ func TestMux_DispatchMiddlewares(t *testing.T) { }, )) + ctx := dew.NewContext(context.Background(), mux) + createUsers := []*createUser{ {Name: "test"}, {Name: "john"}, } // query - findUser, err := dew.Query(context.Background(), mux, &findUser{ID: 1}) + findUser, err := dew.Query(ctx, &findUser{ID: 1}) if err != nil { t.Fatal(err) } @@ -330,9 +414,9 @@ func TestMux_DispatchMiddlewares(t *testing.T) { } // multiple commands - if err := dew.Dispatch(context.Background(), - dew.NewAction(mux, createUsers[0]), - dew.NewAction(mux, createUsers[1]), + if err := dew.DispatchMulti(ctx, + dew.NewAction(createUsers[0]), + dew.NewAction(createUsers[1]), ); err != nil { t.Fatal(err) } @@ -371,11 +455,11 @@ func TestMux_QueryMiddlewares(t *testing.T) { }, )) + ctx := dew.NewContext(context.Background(), mux) + // multiple commands createUser := &createUser{Name: "test"} - if err := dew.Dispatch(context.Background(), - dew.NewAction(mux, createUser), - ); err != nil { + if err := dew.DispatchMulti(ctx, dew.NewAction(createUser)); err != nil { t.Fatal(err) } @@ -389,7 +473,7 @@ func TestMux_QueryMiddlewares(t *testing.T) { } // query - findUser, err := dew.Query(context.Background(), mux, &findUser{ID: 1}) + findUser, err := dew.Query(ctx, &findUser{ID: 1}) if err != nil { t.Fatal(err) } @@ -450,20 +534,22 @@ func TestMux_Groups(t *testing.T) { }, )) + ctx := dew.NewContext(context.Background(), mux) + createUser := &createUser{Name: "john"} - testRunDispatch(t, dew.NewAction(mux, createUser)) + testRunDispatch(t, ctx, dew.NewAction(createUser)) if createUser.Result != "[global][user-action]john" { t.Fatalf("unexpected result: %s", createUser.Result) } createPost := &createPost{Title: "hello"} - testRunDispatch(t, dew.NewAction(mux, createPost)) + testRunDispatch(t, ctx, dew.NewAction(createPost)) if createPost.Result != "[global][post-action]hello" { t.Fatalf("unexpected result: %s", createPost.Result) } updateUser := &updateUser{} - testRunDispatch(t, dew.NewAction(mux, updateUser)) + testRunDispatch(t, ctx, dew.NewAction(updateUser)) if updateUser.Result != "[global]" { t.Fatalf("unexpected result: %s", updateUser.Result) } @@ -519,17 +605,19 @@ func TestMux_GroupsQuery(t *testing.T) { }, )) - findUser := testRunQuery(t, mux, &findUser{ID: 1}) + ctx := dew.NewContext(context.Background(), mux) + + findUser := testRunQuery(t, ctx, &findUser{ID: 1}) if findUser.Result != "[global][local1]john" { t.Fatalf("unexpected result: %s", findUser.Result) } - findPost := testRunQuery(t, mux, &findPost{ID: 1}) + findPost := testRunQuery(t, ctx, &findPost{ID: 1}) if findPost.Result != "[global][local2]post" { t.Fatalf("unexpected result: %s", findPost.Result) } - findTag := testRunQuery(t, mux, &findTagQuery{}) + findTag := testRunQuery(t, ctx, &findTagQuery{}) if findTag.Result != "[global]" { t.Fatalf("unexpected result: %s", findTag.Result) } @@ -540,8 +628,10 @@ func TestMux_ErrorHandling(t *testing.T) { mux := dew.New() mux.Register(new(userHandler)) + ctx := dew.NewContext(context.Background(), mux) + createUser := &createUser{Name: ""} - err := dew.Dispatch(context.Background(), dew.NewAction(mux, createUser)) + err := dew.DispatchMulti(ctx, dew.NewAction(createUser)) if err == nil { t.Fatal("expected an error, but got nil") } @@ -554,7 +644,9 @@ func TestMux_Validation(t *testing.T) { mux := dew.New() mux.Register(new(postHandler)) - err := dew.Dispatch(context.Background(), dew.NewAction(mux, &createPost{Title: ""})) + ctx := dew.NewContext(context.Background(), mux) + + err := dew.DispatchMulti(ctx, dew.NewAction(&createPost{Title: ""})) if err == nil { t.Fatal("expected a validation error, but got nil") } @@ -568,7 +660,7 @@ func TestMux_BusContext(t *testing.T) { mux.UseDispatch(func(next dew.Middleware) dew.Middleware { return dew.MiddlewareFunc(func(ctx dew.Context) error { - bus := dew.FromContext(ctx.Context()) + bus := dew.MustFromContext(ctx.Context()) if bus != mux { t.Fatal("expected bus not found") } @@ -594,7 +686,7 @@ func TestMux_BusContext(t *testing.T) { if ctx.Value("key") != "value" { t.Fatal("expected value not found") } - bus := dew.FromContext(ctx) + bus := dew.MustFromContext(ctx) if bus != mux { t.Fatal("expected bus not found") } @@ -602,12 +694,9 @@ func TestMux_BusContext(t *testing.T) { }, )) - testRunDispatch(t, dew.NewAction(mux, &createUser{Name: "john"})) + ctx := dew.NewContext(context.Background(), mux) - ctx := dew.NewContext() - if ctx.Context() == nil { - t.Fatal("context should not be nil") - } + testRunDispatch(t, ctx, dew.NewAction(&createUser{Name: "john"})) } func BenchmarkMux(b *testing.B) { @@ -615,24 +704,7 @@ func BenchmarkMux(b *testing.B) { mux1 := dew.New() mux1.Register(new(userHandler)) mux1.Register(new(postHandler)) - - mux2 := dew.New() - - mux2.Use(dew.ALL, func(next dew.Middleware) dew.Middleware { - return dew.MiddlewareFunc(func(ctx dew.Context) error { - return next.Handle(ctx) - }) - }) - - mux2.Group(func(mux dew.Bus) { - mux.Use(dew.ALL, func(next dew.Middleware) dew.Middleware { - return dew.MiddlewareFunc(func(ctx dew.Context) error { - return next.Handle(ctx) - }) - }) - mux.Register(new(userHandler)) - mux.Register(new(postHandler)) - }) + ctx1 := dew.NewContext(context.Background(), mux1) b.Run("query", func(b *testing.B) { @@ -640,7 +712,7 @@ func BenchmarkMux(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = dew.Query(context.Background(), mux1, &findUser{ID: 1}) + _, _ = dew.Query(ctx1, &findUser{ID: 1}) } }) @@ -650,17 +722,34 @@ func BenchmarkMux(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = dew.Dispatch(context.Background(), dew.NewAction(mux1, &createPost{Title: "john"})) + _ = dew.DispatchMulti(ctx1, dew.NewAction(&createPost{Title: "john"})) } }) + mux2 := dew.New() + mux2.Use(dew.ALL, func(next dew.Middleware) dew.Middleware { + return dew.MiddlewareFunc(func(ctx dew.Context) error { + return next.Handle(ctx) + }) + }) + mux2.Group(func(mux dew.Bus) { + mux.Use(dew.ALL, func(next dew.Middleware) dew.Middleware { + return dew.MiddlewareFunc(func(ctx dew.Context) error { + return next.Handle(ctx) + }) + }) + mux.Register(new(userHandler)) + mux.Register(new(postHandler)) + }) + ctx2 := dew.NewContext(context.Background(), mux2) + b.Run("query-with-middleware", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = dew.Query(context.Background(), mux2, &findUser{ID: 1}) + _, _ = dew.Query(ctx2, &findUser{ID: 1}) } }) @@ -670,23 +759,23 @@ func BenchmarkMux(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = dew.Dispatch(context.Background(), dew.NewAction(mux2, &createPost{Title: "john"})) + _ = dew.DispatchMulti(ctx2, dew.NewAction(&createPost{Title: "john"})) } }) } -func testRunQuery[T dew.QueryAction](t *testing.T, mux dew.Bus, query *T) *T { +func testRunQuery[T dew.QueryAction](t *testing.T, ctx context.Context, query *T) *T { t.Helper() - result, err := dew.Query[T](context.Background(), mux, query) + result, err := dew.Query[T](ctx, query) if err != nil { t.Fatal(err) } return result } -func testRunDispatch(t *testing.T, commands ...dew.CommandHandler[dew.Action]) { +func testRunDispatch(t *testing.T, ctx context.Context, commands ...dew.CommandHandler[dew.Action]) { t.Helper() - err := dew.Dispatch(context.Background(), commands...) + err := dew.DispatchMulti(ctx, commands...) if err != nil { t.Fatal(err) }