diff --git a/config/handlertype.go b/config/handlertype.go index 9aebda6..5c000fd 100644 --- a/config/handlertype.go +++ b/config/handlertype.go @@ -1,5 +1,11 @@ package config +import ( + "iter" + + "github.com/dogmatiq/enginekit/internal/enum" +) + // HandlerType is an enumeration of the types of message handlers. type HandlerType int @@ -21,99 +27,37 @@ const ( ProjectionHandlerType ) -// HandlerTypes returns a list of all [HandlerType] values. -func HandlerTypes() []HandlerType { - return []HandlerType{ - AggregateHandlerType, - ProcessHandlerType, - IntegrationHandlerType, - ProjectionHandlerType, - } +// HandlerTypes returns a sequence that yields all valid [HandlerType] values. +func HandlerTypes() iter.Seq[HandlerType] { + return enum.Range(AggregateHandlerType, ProjectionHandlerType) } func (t HandlerType) String() string { - switch t { - case AggregateHandlerType: - return "aggregate" - case ProcessHandlerType: - return "process" - case IntegrationHandlerType: - return "integration" - case ProjectionHandlerType: - return "projection" - default: - panic("invalid handler type") - } -} - -// SwitchByHandlerTypeOf invokes one of the provided functions based on the -// [HandlerType] of h. -func SwitchByHandlerTypeOf( - h Handler, - aggregate func(*Aggregate), - process func(*Process), - integration func(*Integration), - projection func(*Projection), -) { - switch h := h.(type) { - case *Aggregate: - if aggregate == nil { - panic("no case function was provided for aggregate handlers") - } - aggregate(h) - case *Process: - if process == nil { - panic("no case function was provided for process handlers") - } - process(h) - case *Integration: - if integration == nil { - panic("no case function was provided for integration handlers") - } - integration(h) - case *Projection: - if projection == nil { - panic("no case function was provided for projection handlers") - } - projection(h) - default: - panic("invalid handler type") - } + return enum.String(t, "aggregate", "process", "integration", "projection") } // RouteCapabilities returns a value that describes the routing capabilities of // the handler type. func (t HandlerType) RouteCapabilities() RouteCapabilities { - switch t { - case AggregateHandlerType: - return RouteCapabilities{ + return RouteCapabilities{ + MapByHandlerType( + t, map[RouteType]RouteTypeCapability{ HandlesCommandRouteType: RouteTypeRequired, RecordsEventRouteType: RouteTypeRequired, }, - } - case IntegrationHandlerType: - return RouteCapabilities{ - map[RouteType]RouteTypeCapability{ - HandlesCommandRouteType: RouteTypeRequired, - RecordsEventRouteType: RouteTypeAllowed, - }, - } - case ProcessHandlerType: - return RouteCapabilities{ map[RouteType]RouteTypeCapability{ HandlesEventRouteType: RouteTypeRequired, ExecutesCommandRouteType: RouteTypeRequired, SchedulesTimeoutRouteType: RouteTypeAllowed, }, - } - case ProjectionHandlerType: - return RouteCapabilities{ + map[RouteType]RouteTypeCapability{ + HandlesCommandRouteType: RouteTypeRequired, + RecordsEventRouteType: RouteTypeAllowed, + }, map[RouteType]RouteTypeCapability{ HandlesEventRouteType: RouteTypeRequired, }, - } - default: - panic("invalid handler type") + ), } } diff --git a/config/handlertype_test.go b/config/handlertype_test.go deleted file mode 100644 index c243795..0000000 --- a/config/handlertype_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package config - -import "testing" - -func TestSwitchByHandlerTypeOf(t *testing.T) { - cases := []struct { - Handler Handler - Want string - }{ - { - &Aggregate{}, - "aggregate", - }, - { - &Process{}, - "process", - }, - { - &Integration{}, - "integration", - }, - { - &Projection{}, - "projection", - }, - } - - for _, c := range cases { - var got string - - SwitchByHandlerTypeOf( - c.Handler, - func(*Aggregate) { got = "aggregate" }, - func(*Process) { got = "process" }, - func(*Integration) { got = "integration" }, - func(*Projection) { got = "projection" }, - ) - - if got != c.Want { - t.Errorf("unexpected value: got %q, want %q", got, c.Want) - } - } -} diff --git a/config/handlertypeswitch.go b/config/handlertypeswitch.go new file mode 100644 index 0000000..7638256 --- /dev/null +++ b/config/handlertypeswitch.go @@ -0,0 +1,122 @@ +package config + +import ( + "github.com/dogmatiq/enginekit/internal/enum" +) + +// SwitchByHandlerType invokes one of the provided functions based on t. +// +// It provides a compile-time guarantee that all possible values are handled, +// even if new [HandlerType] values are added in the future. +// +// It panics if the function associated with t is nil, or if t is not a valid +// [HandlerType]. +func SwitchByHandlerType( + t HandlerType, + aggregate func(), + process func(), + integration func(), + projection func(), +) { + enum.Switch(t, aggregate, process, integration, projection) +} + +// MapByHandlerType maps t to a value of type T. +// +// It provides a compile-time guarantee that all possible values are handled, +// even if new [HandlerType] values are added in the future. +// +// It panics if t is not a valid [HandlerType]. +func MapByHandlerType[T any](t HandlerType, aggregate, process, integration, projection T) T { + return enum.Map(t, aggregate, process, integration, projection) +} + +// SwitchByHandlerTypeOf invokes one of the provided functions based on the +// [HandlerType] of h. +// +// It provides a compile-time guarantee that all types are handled, even if new +// [HandlerType] values are added in the future. +// +// It panics if the function associated with h's type is nil. +func SwitchByHandlerTypeOf( + h Handler, + aggregate func(*Aggregate), + process func(*Process), + integration func(*Integration), + projection func(*Projection), +) { + switch h := h.(type) { + case *Aggregate: + if aggregate == nil { + panic("no case function was provided for *config.Aggregate") + } + aggregate(h) + case *Process: + if process == nil { + panic("no case function was provided for *config.Process") + } + process(h) + case *Integration: + if integration == nil { + panic("no case function was provided for *config.Integration") + } + integration(h) + case *Projection: + if projection == nil { + panic("no case function was provided for *config.Projection") + } + projection(h) + default: + panic("invalid handler type") + } +} + +// MapByHandlerTypeOf invokes one of the provided functions based on the +// [HandlerType] of h, and returns the result. +// +// It provides a compile-time guarantee that all types are handled, even if new +// [HandlerType] values are added in the future. +// +// It panics if the function associated with h's type is nil. +func MapByHandlerTypeOf[T any]( + h Handler, + aggregate func(*Aggregate) T, + process func(*Process) T, + integration func(*Integration) T, + projection func(*Projection) T, +) (result T) { + SwitchByHandlerTypeOf( + h, + enum.AssignResult(aggregate, &result), + enum.AssignResult(process, &result), + enum.AssignResult(integration, &result), + enum.AssignResult(projection, &result), + ) + + return result +} + +// MapByHandlerTypeOfWithErr invokes one of the provided functions based on the +// [HandlerType] of h, and returns the result and error value. +// +// It provides a compile-time guarantee that all types are handled, even if new +// [HandlerType] values are added in the future. +// +// It panics if the function associated with h's type is nil. +func MapByHandlerTypeOfWithErr[T any]( + h Handler, + aggregate func(*Aggregate) (T, error), + process func(*Process) (T, error), + integration func(*Integration) (T, error), + projection func(*Projection) (T, error), +) (result T, err error) { + SwitchByHandlerTypeOf( + h, + enum.AssignResultErr(aggregate, &result, &err), + enum.AssignResultErr(process, &result, &err), + enum.AssignResultErr(integration, &result, &err), + enum.AssignResultErr(projection, &result, &err), + ) + + return result, err +} diff --git a/config/handlertypeswitch_test.go b/config/handlertypeswitch_test.go new file mode 100644 index 0000000..6bc9d22 --- /dev/null +++ b/config/handlertypeswitch_test.go @@ -0,0 +1,189 @@ +package config + +import ( + "errors" + "testing" + + "github.com/dogmatiq/enginekit/internal/test" +) + +func TestHandlerType(t *testing.T) { + test.Enum( + t, + test.EnumSpec[HandlerType]{ + Range: HandlerTypes, + Switch: SwitchByHandlerType, + MapToString: MapByHandlerType[string], + }, + ) +} + +func TestSwitchByHandlerTypeOf(t *testing.T) { + cases := []struct { + Handler Handler + Want string + }{ + {&Aggregate{}, "aggregate"}, + {&Process{}, "process"}, + {&Integration{}, "integration"}, + {&Projection{}, "projection"}, + } + + for _, c := range cases { + var got string + + SwitchByHandlerTypeOf( + c.Handler, + func(*Aggregate) { got = "aggregate" }, + func(*Process) { got = "process" }, + func(*Integration) { got = "integration" }, + func(*Projection) { got = "projection" }, + ) + + if got != c.Want { + t.Errorf("unexpected value: got %q, want %q", got, c.Want) + } + } + + t.Run("it panics when the associated function is nil", func(t *testing.T) { + cases := []struct { + Handler Handler + Want string + }{ + {&Aggregate{}, `no case function was provided for *config.Aggregate`}, + {&Process{}, `no case function was provided for *config.Process`}, + {&Integration{}, `no case function was provided for *config.Integration`}, + {&Projection{}, `no case function was provided for *config.Projection`}, + } + + for _, c := range cases { + test.ExpectPanic( + t, + c.Want, + func() { + SwitchByHandlerTypeOf(c.Handler, nil, nil, nil, nil) + }, + ) + } + }) + + t.Run("it panics when the handler type is invalid", func(t *testing.T) { + test.ExpectPanic( + t, + "invalid handler type", + func() { + SwitchByHandlerTypeOf( + nil, + func(*Aggregate) {}, + func(*Process) {}, + func(*Integration) {}, + func(*Projection) {}, + ) + }, + ) + }) +} + +func TestMapByHandlerTypeOf(t *testing.T) { + cases := []struct { + Handler Handler + Want string + }{ + {&Aggregate{}, "aggregate"}, + {&Process{}, "process"}, + {&Integration{}, "integration"}, + {&Projection{}, "projection"}, + } + + for _, c := range cases { + got := MapByHandlerTypeOf( + c.Handler, + func(*Aggregate) string { return "aggregate" }, + func(*Process) string { return "process" }, + func(*Integration) string { return "integration" }, + func(*Projection) string { return "projection" }, + ) + + if got != c.Want { + t.Errorf("unexpected value: got %q, want %q", got, c.Want) + } + } + + t.Run("it panics when the associated function is nil", func(t *testing.T) { + cases := []struct { + Handler Handler + Want string + }{ + {&Aggregate{}, `no case function was provided for *config.Aggregate`}, + {&Process{}, `no case function was provided for *config.Process`}, + {&Integration{}, `no case function was provided for *config.Integration`}, + {&Projection{}, `no case function was provided for *config.Projection`}, + } + + for _, c := range cases { + test.ExpectPanic( + t, + c.Want, + func() { + MapByHandlerTypeOf[int](c.Handler, nil, nil, nil, nil) + }, + ) + } + }) +} + +func TestMapByHandlerTypeOfWithErr(t *testing.T) { + cases := []struct { + Handler Handler + Want string + }{ + {&Aggregate{}, "aggregate"}, + {&Process{}, "process"}, + {&Integration{}, "integration"}, + {&Projection{}, "projection"}, + } + + for _, c := range cases { + got, gotErr := MapByHandlerTypeOfWithErr( + c.Handler, + func(*Aggregate) (string, error) { return "aggregate", errors.New("aggregate") }, + func(*Process) (string, error) { return "process", errors.New("process") }, + func(*Integration) (string, error) { return "integration", errors.New("integration") }, + func(*Projection) (string, error) { return "projection", errors.New("projection") }, + ) + + if got != c.Want { + t.Errorf("unexpected value: got %q, want %q", got, c.Want) + } + + if gotErr == nil { + t.Fatal("expected an error") + } + + if gotErr.Error() != c.Want { + t.Errorf("unexpected error: got %q, want %q", gotErr, c.Want) + } + } + + t.Run("it panics when the associated function is nil", func(t *testing.T) { + cases := []struct { + Handler Handler + Want string + }{ + {&Aggregate{}, `no case function was provided for *config.Aggregate`}, + {&Process{}, `no case function was provided for *config.Process`}, + {&Integration{}, `no case function was provided for *config.Integration`}, + {&Projection{}, `no case function was provided for *config.Projection`}, + } + + for _, c := range cases { + test.ExpectPanic( + t, + c.Want, + func() { + MapByHandlerTypeOfWithErr[int](c.Handler, nil, nil, nil, nil) + }, + ) + } + }) +}