From f4128b02c2df5f368f439ddb2873c8ffc541f0e3 Mon Sep 17 00:00:00 2001 From: Alex Shtin Date: Tue, 28 Jan 2025 09:23:36 -0800 Subject: [PATCH] Basic CHASM registry implementation --- chasm/component.go | 2 + chasm/component_mock.go | 89 ++++++++++++ chasm/library.go | 50 +++++++ chasm/library_mock.go | 117 ++++++++++++++++ chasm/registrable_component.go | 86 ++++++++++++ chasm/registrable_task.go | 66 +++++++++ chasm/registry.go | 174 +++++++++++++++++------ chasm/registry_test.go | 248 +++++++++++++++++++++++++++++++++ chasm/task.go | 20 +-- chasm/task_mock.go | 92 ++++++++++++ 10 files changed, 890 insertions(+), 54 deletions(-) create mode 100644 chasm/component_mock.go create mode 100644 chasm/library.go create mode 100644 chasm/library_mock.go create mode 100644 chasm/registrable_component.go create mode 100644 chasm/registrable_task.go create mode 100644 chasm/registry_test.go create mode 100644 chasm/task_mock.go diff --git a/chasm/component.go b/chasm/component.go index 8cd85865337..43f47107503 100644 --- a/chasm/component.go +++ b/chasm/component.go @@ -22,6 +22,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +//go:generate mockgen -copyright_file ../LICENSE -package $GOPACKAGE -source $GOFILE -destination component_mock.go + package chasm import "context" diff --git a/chasm/component_mock.go b/chasm/component_mock.go new file mode 100644 index 00000000000..7ccea11a0d9 --- /dev/null +++ b/chasm/component_mock.go @@ -0,0 +1,89 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Code generated by MockGen. DO NOT EDIT. +// Source: component.go +// +// Generated by this command: +// +// mockgen -copyright_file ../LICENSE -package chasm -source component.go -destination component_mock.go +// + +// Package chasm is a generated GoMock package. +package chasm + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockComponent is a mock of Component interface. +type MockComponent struct { + ctrl *gomock.Controller + recorder *MockComponentMockRecorder +} + +// MockComponentMockRecorder is the mock recorder for MockComponent. +type MockComponentMockRecorder struct { + mock *MockComponent +} + +// NewMockComponent creates a new mock instance. +func NewMockComponent(ctrl *gomock.Controller) *MockComponent { + mock := &MockComponent{ctrl: ctrl} + mock.recorder = &MockComponentMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockComponent) EXPECT() *MockComponentMockRecorder { + return m.recorder +} + +// LifecycleState mocks base method. +func (m *MockComponent) LifecycleState() LifecycleState { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LifecycleState") + ret0, _ := ret[0].(LifecycleState) + return ret0 +} + +// LifecycleState indicates an expected call of LifecycleState. +func (mr *MockComponentMockRecorder) LifecycleState() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LifecycleState", reflect.TypeOf((*MockComponent)(nil).LifecycleState)) +} + +// mustEmbedUnimplementedComponent mocks base method. +func (m *MockComponent) mustEmbedUnimplementedComponent() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedComponent") +} + +// mustEmbedUnimplementedComponent indicates an expected call of mustEmbedUnimplementedComponent. +func (mr *MockComponentMockRecorder) mustEmbedUnimplementedComponent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedComponent", reflect.TypeOf((*MockComponent)(nil).mustEmbedUnimplementedComponent)) +} diff --git a/chasm/library.go b/chasm/library.go new file mode 100644 index 00000000000..31a14896daf --- /dev/null +++ b/chasm/library.go @@ -0,0 +1,50 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +//go:generate mockgen -copyright_file ../LICENSE -package $GOPACKAGE -source $GOFILE -destination library_mock.go + +package chasm + +type ( + Library interface { + Name() string + Components() []RegistrableComponent + Tasks() []RegistrableTask + // Service() + + mustEmbedUnimplementedLibrary() + } + + UnimplementedLibrary struct{} +) + +func (UnimplementedLibrary) Components() []RegistrableComponent { + return nil +} + +func (UnimplementedLibrary) Tasks() []RegistrableTask { + return nil +} + +func (UnimplementedLibrary) mustEmbedUnimplementedLibrary() {} diff --git a/chasm/library_mock.go b/chasm/library_mock.go new file mode 100644 index 00000000000..7af8e63088b --- /dev/null +++ b/chasm/library_mock.go @@ -0,0 +1,117 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Code generated by MockGen. DO NOT EDIT. +// Source: library.go +// +// Generated by this command: +// +// mockgen -copyright_file ../LICENSE -package chasm -source library.go -destination library_mock.go +// + +// Package chasm is a generated GoMock package. +package chasm + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockLibrary is a mock of Library interface. +type MockLibrary struct { + ctrl *gomock.Controller + recorder *MockLibraryMockRecorder +} + +// MockLibraryMockRecorder is the mock recorder for MockLibrary. +type MockLibraryMockRecorder struct { + mock *MockLibrary +} + +// NewMockLibrary creates a new mock instance. +func NewMockLibrary(ctrl *gomock.Controller) *MockLibrary { + mock := &MockLibrary{ctrl: ctrl} + mock.recorder = &MockLibraryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLibrary) EXPECT() *MockLibraryMockRecorder { + return m.recorder +} + +// Components mocks base method. +func (m *MockLibrary) Components() []RegistrableComponent { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Components") + ret0, _ := ret[0].([]RegistrableComponent) + return ret0 +} + +// Components indicates an expected call of Components. +func (mr *MockLibraryMockRecorder) Components() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Components", reflect.TypeOf((*MockLibrary)(nil).Components)) +} + +// Name mocks base method. +func (m *MockLibrary) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockLibraryMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockLibrary)(nil).Name)) +} + +// Tasks mocks base method. +func (m *MockLibrary) Tasks() []RegistrableTask { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Tasks") + ret0, _ := ret[0].([]RegistrableTask) + return ret0 +} + +// Tasks indicates an expected call of Tasks. +func (mr *MockLibraryMockRecorder) Tasks() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tasks", reflect.TypeOf((*MockLibrary)(nil).Tasks)) +} + +// mustEmbedUnimplementedLibrary mocks base method. +func (m *MockLibrary) mustEmbedUnimplementedLibrary() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedLibrary") +} + +// mustEmbedUnimplementedLibrary indicates an expected call of mustEmbedUnimplementedLibrary. +func (mr *MockLibraryMockRecorder) mustEmbedUnimplementedLibrary() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedLibrary", reflect.TypeOf((*MockLibrary)(nil).mustEmbedUnimplementedLibrary)) +} diff --git a/chasm/registrable_component.go b/chasm/registrable_component.go new file mode 100644 index 00000000000..4be99a351dd --- /dev/null +++ b/chasm/registrable_component.go @@ -0,0 +1,86 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package chasm + +import ( + "reflect" +) + +var ( + EmptyRegistrableComponent = RegistrableComponent{} +) + +type ( + RegistrableComponent struct { + name string + goType reflect.Type + + ephemeral bool + singleCluster bool + shardingFn func(EntityKey) string + } + + RegistrableComponentOption func(*RegistrableComponent) +) + +func NewRegistrableComponent[C Component]( + name string, + opts ...RegistrableComponentOption, +) RegistrableComponent { + rc := RegistrableComponent{ + name: name, + goType: reflect.TypeFor[C](), + shardingFn: func(_ EntityKey) string { return "" }, + } + for _, opt := range opts { + opt(&rc) + } + return rc +} + +func WithEphemeral() RegistrableComponentOption { + return func(rc *RegistrableComponent) { + rc.ephemeral = true + } +} + +// Is there any use case where we don't want to replicate certain instances of a archetype? +func WithSingleCluster() RegistrableComponentOption { + return func(rc *RegistrableComponent) { + rc.singleCluster = true + } +} + +func WithShardingFn( + shardingFn func(EntityKey) string, +) RegistrableComponentOption { + return func(rc *RegistrableComponent) { + rc.shardingFn = shardingFn + } +} + +func (rc RegistrableComponent) Name() string { + return rc.name +} diff --git a/chasm/registrable_task.go b/chasm/registrable_task.go new file mode 100644 index 00000000000..f5e8269d7d0 --- /dev/null +++ b/chasm/registrable_task.go @@ -0,0 +1,66 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package chasm + +import ( + "reflect" +) + +var ( + EmptyRegistrableTask = RegistrableTask{} +) + +type ( + RegistrableTask struct { + name string + goType reflect.Type + componentGoType reflect.Type // It is not clear how this one is used. + handler any + } + + RegistrableTaskOption func(*RegistrableTask) +) + +// NOTE: C is not Component but any. +func NewRegistrableTask[C any, T any]( + name string, + handler TaskHandler[C, T], + opts ...RegistrableTaskOption, +) RegistrableTask { + rt := RegistrableTask{ + name: name, + goType: reflect.TypeFor[T](), + componentGoType: reflect.TypeFor[C](), + handler: handler, + } + for _, opt := range opts { + opt(&rt) + } + return rt +} + +func (rt RegistrableTask) Name() string { + return rt.name +} diff --git a/chasm/registry.go b/chasm/registry.go index 7c8bc57b189..5d3d687f0cc 100644 --- a/chasm/registry.go +++ b/chasm/registry.go @@ -24,67 +24,149 @@ package chasm -type Registry struct{} - -func (r *Registry) RegisterLibrary(lib Library) { - panic("not implemented") -} - -type Library interface { - Name() string - Components() []RegistrableComponent - Tasks() []RegistrableTask - // Service() - - mustEmbedUnimplementedLibrary() +import ( + "errors" + "fmt" + "reflect" + "regexp" +) + +var ( + // This is golang type identifier regex. + nameValidator = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) +) + +type ( + Registry struct { + components map[string]RegistrableComponent // fully qualified name -> component + componentNames map[reflect.Type]string // component type -> fully qualified name + + tasks map[string]RegistrableTask // fully qualified name -> task + taskNames map[reflect.Type]string // task type -> fully qualified name + } +) + +func NewRegistry() *Registry { + return &Registry{ + components: make(map[string]RegistrableComponent), + componentNames: make(map[reflect.Type]string), + tasks: make(map[string]RegistrableTask), + taskNames: make(map[reflect.Type]string), + } } -type UnimplementedLibrary struct{} - -func (UnimplementedLibrary) Components() []RegistrableComponent { +func (r *Registry) Register(lib Library) error { + if err := r.validateName(lib.Name()); err != nil { + return err + } + for _, c := range lib.Components() { + if err := r.registerComponent(lib.Name(), c); err != nil { + return err + } + } + for _, t := range lib.Tasks() { + if err := r.registerTask(lib.Name(), t); err != nil { + return err + } + } return nil } -func (UnimplementedLibrary) Tasks() []RegistrableTask { - return nil +func (r *Registry) Component(fqn string) (RegistrableComponent, bool) { + rc, ok := r.components[fqn] + if !ok { + return EmptyRegistrableComponent, false + } + return rc, ok } -func (UnimplementedLibrary) mustEmbedUnimplementedLibrary() {} - -type RegistrableComponent struct { +func (r *Registry) Task(fqn string) (RegistrableTask, bool) { + rt, ok := r.tasks[fqn] + if !ok { + return EmptyRegistrableTask, false + } + return rt, ok } -func NewRegistrableComponent[C Component]( - name string, - opts ...RegistrableComponentOption, -) RegistrableComponent { - panic("not implemented") +func (r *Registry) ComponentFor(componentInstance any) (RegistrableComponent, bool) { + fqn, ok := r.componentNames[reflect.TypeOf(componentInstance)] + if !ok { + return EmptyRegistrableComponent, false + } + return r.Component(fqn) } -type RegistrableComponentOption func(*RegistrableComponent) - -func EntityEphemeral() RegistrableComponentOption { - panic("not implemented") +func (r *Registry) TaskFor(taskInstance any) (RegistrableTask, bool) { + fqn, ok := r.taskNames[reflect.TypeOf(taskInstance)] + if !ok { + return EmptyRegistrableTask, false + } + return r.Task(fqn) } -// Is there any use case where we don't want to replicate -// certain instances of a archetype? -func EntitySingleCluster() RegistrableComponentOption { - panic("not implemented") +func (r *Registry) fqn(libName, name string) string { + return libName + "." + name } -func EntityShardingFn( - func(EntityKey) string, -) RegistrableComponentOption { - panic("not implemented") +func (r *Registry) registerComponent( + libName string, + c RegistrableComponent, +) error { + if err := r.validateName(c.name); err != nil { + return err + } + fqn := r.fqn(libName, c.name) + if _, ok := r.components[fqn]; ok { + return fmt.Errorf("component %s is already registered", fqn) + } + // TODO: this step is redundant. c.goType implements Component interface, therefore it must be a struct. + if !(c.goType.Kind() == reflect.Struct || + (c.goType.Kind() == reflect.Ptr && c.goType.Elem().Kind() == reflect.Struct)) { + return fmt.Errorf("component type %s must be struct or pointer to struct", c.goType.String()) + } + if _, ok := r.componentNames[c.goType]; ok { + return fmt.Errorf("component type %s is already registered", c.goType.String()) + } + r.components[fqn] = c + r.componentNames[c.goType] = fqn + return nil +} +func (r *Registry) registerTask( + libName string, + t RegistrableTask, +) error { + if err := r.validateName(t.name); err != nil { + return err + } + fqn := r.fqn(libName, t.name) + if _, ok := r.tasks[fqn]; ok { + return fmt.Errorf("task %s is already registered", fqn) + } + if !(t.goType.Kind() == reflect.Struct || + (t.goType.Kind() == reflect.Ptr && t.goType.Elem().Kind() == reflect.Struct)) { + return fmt.Errorf("task type %s must be struct or pointer to struct", t.goType.String()) + } + if _, ok := r.taskNames[t.goType]; ok { + return fmt.Errorf("task type %s is already registered", t.goType.String()) + } + if !(t.componentGoType.Kind() == reflect.Interface || + (t.componentGoType.Kind() == reflect.Struct || + (t.componentGoType.Kind() == reflect.Ptr && t.componentGoType.Elem().Kind() == reflect.Struct)) && + t.componentGoType.AssignableTo(reflect.TypeOf((*Component)(nil)).Elem())) { + return fmt.Errorf("component type %s must be and interface or struct that implements Component interface", t.componentGoType.String()) + } + + r.tasks[fqn] = t + r.taskNames[t.goType] = fqn + return nil } -type RegistrableTask struct{} - -func NewRegistrableTask[C any, T any]( - name string, - handler TaskHandler[C, T], - // opts ...RegistrableTaskOptions, no options right now -) RegistrableTask { - panic("not implemented") +func (r *Registry) validateName(n string) error { + if n == "" { + return errors.New("name must not be empty") + } + if !nameValidator.MatchString(n) { + return fmt.Errorf("name %s is invalid. name must follow golang identifier rules: %s", n, nameValidator.String()) + } + return nil } diff --git a/chasm/registry_test.go b/chasm/registry_test.go new file mode 100644 index 00000000000..f45b6c3ee35 --- /dev/null +++ b/chasm/registry_test.go @@ -0,0 +1,248 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package chasm_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.temporal.io/server/chasm" + "go.uber.org/mock/gomock" +) + +type ( + testTask1 struct{} + testTask2 struct{} + testTaskComponentInterface interface { + DoSomething() + } +) + +func TestRegistry_RegisterComponents_Success(t *testing.T) { + r := chasm.NewRegistry() + ctrl := gomock.NewController(t) + lib := chasm.NewMockLibrary(ctrl) + lib.EXPECT().Name().Return("TestLibrary").AnyTimes() + lib.EXPECT().Components().Return([]chasm.RegistrableComponent{ + chasm.NewRegistrableComponent[*chasm.MockComponent]("Component1"), + }) + + lib.EXPECT().Tasks().Return(nil) + + err := r.Register(lib) + require.NoError(t, err) + + rc1, ok := r.Component("TestLibrary.Component1") + require.True(t, ok) + require.Equal(t, "Component1", rc1.Name()) + + missingRC, ok := r.Component("TestLibrary.Component2") + require.False(t, ok) + require.Equal(t, chasm.EmptyRegistrableComponent, missingRC) + + cInstance1 := chasm.NewMockComponent(ctrl) + rc2, ok := r.ComponentFor(cInstance1) + require.True(t, ok) + require.Equal(t, "Component1", rc2.Name()) + + cInstance2 := "invalid component instance" + rc3, ok := r.ComponentFor(cInstance2) + require.False(t, ok) + require.Equal(t, chasm.EmptyRegistrableComponent, rc3) +} + +func TestRegistry_RegisterTasks_Success(t *testing.T) { + r := chasm.NewRegistry() + ctrl := gomock.NewController(t) + lib := chasm.NewMockLibrary(ctrl) + lib.EXPECT().Name().Return("TestLibrary").AnyTimes() + lib.EXPECT().Components().Return(nil) + + lib.EXPECT().Tasks().Return([]chasm.RegistrableTask{ + chasm.NewRegistrableTask[*chasm.MockComponent, testTask1]("Task1", chasm.NewMockTaskHandler[*chasm.MockComponent, testTask1](ctrl)), + chasm.NewRegistrableTask[testTaskComponentInterface, testTask2]("Task2", chasm.NewMockTaskHandler[testTaskComponentInterface, testTask2](ctrl)), + }) + + err := r.Register(lib) + require.NoError(t, err) + + rt1, ok := r.Task("TestLibrary.Task1") + require.True(t, ok) + require.Equal(t, "Task1", rt1.Name()) + + missingRT, ok := r.Task("TestLibrary.TaskMissing") + require.False(t, ok) + require.Equal(t, chasm.EmptyRegistrableTask, missingRT) + + tInstance1 := testTask2{} + rt2, ok := r.TaskFor(tInstance1) + require.True(t, ok) + require.Equal(t, "Task2", rt2.Name()) + + tInstance2 := "invalid task instance" + rt3, ok := r.TaskFor(tInstance2) + require.False(t, ok) + require.Equal(t, chasm.EmptyRegistrableTask, rt3) +} + +func TestRegistry_Register_LibraryError(t *testing.T) { + ctrl := gomock.NewController(t) + lib := chasm.NewMockLibrary(ctrl) + + t.Run("library name must not be empty", func(t *testing.T) { + lib.EXPECT().Name().Return("") + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "name must not be empty") + }) + + t.Run("library name must follow rules", func(t *testing.T) { + lib.EXPECT().Name().Return("bad.lib.name") + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "name must follow golang identifier rules") + }) +} + +func TestRegistry_RegisterComponents_Error(t *testing.T) { + ctrl := gomock.NewController(t) + lib := chasm.NewMockLibrary(ctrl) + lib.EXPECT().Name().Return("TestLibrary").AnyTimes() + + t.Run("component name must not be empty", func(t *testing.T) { + lib.EXPECT().Components().Return([]chasm.RegistrableComponent{ + chasm.NewRegistrableComponent[*chasm.MockComponent](""), + }) + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "name must not be empty") + }) + + t.Run("component name must follow rules", func(t *testing.T) { + lib.EXPECT().Components().Return([]chasm.RegistrableComponent{ + chasm.NewRegistrableComponent[*chasm.MockComponent]("bad.component.name"), + }) + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "name must follow golang identifier rules") + }) + + t.Run("component is already registered by name", func(t *testing.T) { + lib.EXPECT().Components().Return([]chasm.RegistrableComponent{ + chasm.NewRegistrableComponent[*chasm.MockComponent]("Component1"), + chasm.NewRegistrableComponent[*chasm.MockComponent]("Component1"), + }) + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "is already registered") + }) + + t.Run("component is already registered by type", func(t *testing.T) { + lib.EXPECT().Components().Return([]chasm.RegistrableComponent{ + chasm.NewRegistrableComponent[*chasm.MockComponent]("Component1"), + chasm.NewRegistrableComponent[*chasm.MockComponent]("Component2"), + }) + r := chasm.NewRegistry() + + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "is already registered") + }) +} + +func TestRegistry_RegisterTasks_Error(t *testing.T) { + ctrl := gomock.NewController(t) + lib := chasm.NewMockLibrary(ctrl) + lib.EXPECT().Name().Return("TestLibrary").AnyTimes() + lib.EXPECT().Components().Return(nil).AnyTimes() + + t.Run("task name must not be empty", func(t *testing.T) { + r := chasm.NewRegistry() + lib.EXPECT().Tasks().Return([]chasm.RegistrableTask{ + chasm.NewRegistrableTask[*chasm.MockComponent, testTask1]("", chasm.NewMockTaskHandler[*chasm.MockComponent, testTask1](ctrl)), + }) + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "name must not be empty") + }) + + t.Run("task name must follow rules", func(t *testing.T) { + lib.EXPECT().Tasks().Return([]chasm.RegistrableTask{ + chasm.NewRegistrableTask[*chasm.MockComponent, testTask1]("bad.task.name", chasm.NewMockTaskHandler[*chasm.MockComponent, testTask1](ctrl)), + }) + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "name must follow golang identifier rules") + }) + + t.Run("task is already registered by name", func(t *testing.T) { + lib.EXPECT().Tasks().Return([]chasm.RegistrableTask{ + chasm.NewRegistrableTask[*chasm.MockComponent, testTask1]("Task1", chasm.NewMockTaskHandler[*chasm.MockComponent, testTask1](ctrl)), + chasm.NewRegistrableTask[*chasm.MockComponent, testTask1]("Task1", chasm.NewMockTaskHandler[*chasm.MockComponent, testTask1](ctrl)), + }) + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "is already registered") + }) + + t.Run("task is already registered by type", func(t *testing.T) { + lib.EXPECT().Tasks().Return([]chasm.RegistrableTask{ + chasm.NewRegistrableTask[*chasm.MockComponent, testTask1]("Task1", chasm.NewMockTaskHandler[*chasm.MockComponent, testTask1](ctrl)), + chasm.NewRegistrableTask[*chasm.MockComponent, testTask1]("Task2", chasm.NewMockTaskHandler[*chasm.MockComponent, testTask1](ctrl)), + }) + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "is already registered") + }) + + t.Run("task component struct must implement Component", func(t *testing.T) { + lib.EXPECT().Tasks().Return([]chasm.RegistrableTask{ + // MockComponent has only pointer receivers and therefore does not implement Component interface. + chasm.NewRegistrableTask[chasm.MockComponent, testTask1]("Task1", chasm.NewMockTaskHandler[chasm.MockComponent, testTask1](ctrl)), + }) + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "struct that implements Component interface") + }) + + t.Run("task must be struct", func(t *testing.T) { + lib.EXPECT().Tasks().Return([]chasm.RegistrableTask{ + chasm.NewRegistrableTask[*chasm.MockComponent, string]("Task1", chasm.NewMockTaskHandler[*chasm.MockComponent, string](ctrl)), + }) + r := chasm.NewRegistry() + err := r.Register(lib) + require.Error(t, err) + require.Contains(t, err.Error(), "must be struct or pointer to struct") + }) +} diff --git a/chasm/task.go b/chasm/task.go index c5a9263d882..6d780715ab2 100644 --- a/chasm/task.go +++ b/chasm/task.go @@ -22,6 +22,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +//go:generate mockgen -copyright_file ../LICENSE -package $GOPACKAGE -source $GOFILE -destination task_mock.go + package chasm import ( @@ -29,12 +31,14 @@ import ( "time" ) -type TaskAttributes struct { - ScheduledTime time.Time - Destination string -} +type ( + TaskAttributes struct { + ScheduledTime time.Time + Destination string + } -type TaskHandler[C any, T any] interface { - Validate(Context, C, T) error - Execute(context.Context, ComponentRef, T) error -} + TaskHandler[C any, T any] interface { + Validate(Context, C, T) error + Execute(context.Context, ComponentRef, T) error + } +) diff --git a/chasm/task_mock.go b/chasm/task_mock.go new file mode 100644 index 00000000000..de462e9371f --- /dev/null +++ b/chasm/task_mock.go @@ -0,0 +1,92 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Code generated by MockGen. DO NOT EDIT. +// Source: task.go +// +// Generated by this command: +// +// mockgen -copyright_file ../LICENSE -package chasm -source task.go -destination task_mock.go +// + +// Package chasm is a generated GoMock package. +package chasm + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockTaskHandler is a mock of TaskHandler interface. +type MockTaskHandler[C any, T any] struct { + ctrl *gomock.Controller + recorder *MockTaskHandlerMockRecorder[C, T] +} + +// MockTaskHandlerMockRecorder is the mock recorder for MockTaskHandler. +type MockTaskHandlerMockRecorder[C any, T any] struct { + mock *MockTaskHandler[C, T] +} + +// NewMockTaskHandler creates a new mock instance. +func NewMockTaskHandler[C any, T any](ctrl *gomock.Controller) *MockTaskHandler[C, T] { + mock := &MockTaskHandler[C, T]{ctrl: ctrl} + mock.recorder = &MockTaskHandlerMockRecorder[C, T]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTaskHandler[C, T]) EXPECT() *MockTaskHandlerMockRecorder[C, T] { + return m.recorder +} + +// Execute mocks base method. +func (m *MockTaskHandler[C, T]) Execute(arg0 context.Context, arg1 ComponentRef, arg2 T) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Execute", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Execute indicates an expected call of Execute. +func (mr *MockTaskHandlerMockRecorder[C, T]) Execute(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockTaskHandler[C, T])(nil).Execute), arg0, arg1, arg2) +} + +// Validate mocks base method. +func (m *MockTaskHandler[C, T]) Validate(arg0 Context, arg1 C, arg2 T) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Validate indicates an expected call of Validate. +func (mr *MockTaskHandlerMockRecorder[C, T]) Validate(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockTaskHandler[C, T])(nil).Validate), arg0, arg1, arg2) +}