From c61640ab5c1184f316cc3d739daed045855fb0ca Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Sun, 31 Mar 2024 15:10:53 -0400 Subject: [PATCH] feature: #279 Added generic route for writing resources * Updated write controller to NOT use commands * Added Initialize method to resource repository to instantiate resource from projection or create a new one * Made it so that the ResourceRepository stores the reference to projections * Added GORM projections that serves as the default projection that stores resources as well as the event store which stores the events. * Added Routes for resources that are linked to the DefaultWriteController --- v2/go.mod | 1 + v2/go.sum | 5 + v2/rest/api.go | 4 +- v2/rest/command.go | 3 +- v2/rest/config.go | 11 +-- v2/rest/controllers.go | 70 +++++++++----- v2/rest/controllers_test.go | 28 ++++-- v2/rest/event.go | 21 +++- v2/rest/event_test.go | 20 ++-- v2/rest/gorm.go | 160 ++++++++++++++++++++---------- v2/rest/initializers.go | 22 ++++- v2/rest/interfaces.go | 13 +-- v2/rest/receivers.go | 2 +- v2/rest/repository.go | 65 +++++++++++-- v2/rest/repository_test.go | 118 ++++++++++++++++++++-- v2/rest/resource.go | 83 +++++++++++----- v2/rest/resource_test.go | 4 +- v2/rest/rest_mocks_test.go | 188 ++++++++++++++++++++++++++---------- 18 files changed, 608 insertions(+), 210 deletions(-) diff --git a/v2/go.mod b/v2/go.mod index baf9d24c..18c59c48 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -61,4 +61,5 @@ require ( golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/datatypes v1.2.0 // indirect ) diff --git a/v2/go.sum b/v2/go.sum index f2a5de7b..ccee7075 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -59,6 +59,7 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= @@ -96,6 +97,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= @@ -218,6 +220,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -226,6 +229,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= +gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= diff --git a/v2/rest/api.go b/v2/rest/api.go index ce0dc157..5d416bd2 100644 --- a/v2/rest/api.go +++ b/v2/rest/api.go @@ -30,9 +30,9 @@ var API = fx.Module("rest", NewEcho, NewZap, NewGORM, - NewGORMResourceRepository, NewCommandDispatcher, - NewGORMEventStore, + NewResourceRepository, + NewGORMProjection, ), fx.Invoke(RouteInitializer, registerHooks), ) diff --git a/v2/rest/command.go b/v2/rest/command.go index af77d144..796e188d 100644 --- a/v2/rest/command.go +++ b/v2/rest/command.go @@ -17,7 +17,6 @@ type CommandDispatcherParams struct { fx.In CommandConfigs []CommandConfig `group:"commandHandlers"` Logger Log - Repository Repository } type CommandDispatcherResult struct { @@ -44,7 +43,7 @@ type DefaultCommandDispatcher struct { dispatch sync.Mutex } -func (e *DefaultCommandDispatcher) Dispatch(ctx context.Context, command *Command, repository Repository, logger Log) (interface{}, error) { +func (e *DefaultCommandDispatcher) Dispatch(ctx context.Context, command *Command, repository *ResourceRepository, logger Log) (interface{}, error) { var wg sync.WaitGroup var err error var result interface{} diff --git a/v2/rest/config.go b/v2/rest/config.go index 83d3344a..22a254bd 100644 --- a/v2/rest/config.go +++ b/v2/rest/config.go @@ -41,16 +41,13 @@ type WeOSConfigResult struct { func WeOSConfig(p WeOSConfigParams) (WeOSConfigResult, error) { if p.Config != nil { var config *APIConfig - if _, ok := p.Config.Extensions[WeOSConfigExtension]; ok { - data, err := p.Config.Extensions[WeOSConfigExtension].(json.RawMessage).MarshalJSON() + if data, ok := p.Config.Extensions[WeOSConfigExtension]; ok { + dataBytes, err := json.Marshal(data) if err != nil { + p.Logger.Errorf("error encountered marshalling config '%s'", err) return WeOSConfigResult{}, err } - err = json.Unmarshal(data, &config) - if err != nil { - return WeOSConfigResult{}, err - } - + err = json.Unmarshal(dataBytes, &config) return WeOSConfigResult{ Config: config, }, nil diff --git a/v2/rest/controllers.go b/v2/rest/controllers.go index 9e31da5b..86f2121b 100644 --- a/v2/rest/controllers.go +++ b/v2/rest/controllers.go @@ -11,7 +11,46 @@ import ( ) // DefaultWriteController handles the write operations (create, update, delete) -func DefaultWriteController(logger Log, commandDispatcher CommandDispatcher, resourceRepository Repository, api *openapi3.T, pathMap map[string]*openapi3.PathItem, operation map[string]*openapi3.Operation) echo.HandlerFunc { +func DefaultWriteController(logger Log, commandDispatcher CommandDispatcher, resourceRepository *ResourceRepository, api *openapi3.T, pathMap map[string]*openapi3.PathItem, operation map[string]*openapi3.Operation) echo.HandlerFunc { + + var err error + + return func(ctxt echo.Context) error { + var sequenceNo string + var seq int + + //getting etag from context + etag := ctxt.Request().Header.Get("If-Match") + if etag != "" { + _, sequenceNo = SplitEtag(etag) + seq, err = strconv.Atoi(sequenceNo) + if err != nil { + return NewControllerError("unexpected error updating content type. invalid sequence number", err, http.StatusBadRequest) + } + } + + body, err := io.ReadAll(ctxt.Request().Body) + if err != nil { + ctxt.Logger().Debugf("unexpected error reading request body: %s", err) + return NewControllerError("unexpected error reading request body", err, http.StatusBadRequest) + } + resource, err := resourceRepository.Initialize(ctxt.Request().Context(), logger, body) + //if the sequence number is not one more than the current sequence number then return an error + if seq != 0 && resource.GetSequenceNo() != seq+1 { + return NewControllerError("unexpected error updating content type. invalid sequence number", err, http.StatusPreconditionFailed) + } + //set etag in response header + ctxt.Response().Header().Set("ETag", fmt.Sprintf("%s.%d", resource.GetID(), resource.GetSequenceNo())) + if resource.GetSequenceNo() == 1 { + return ctxt.JSON(http.StatusCreated, resource) + } else { + return ctxt.JSON(http.StatusOK, resource) + } + } +} + +// DefaultExecuteController handles the write operations that have a command associated with them +func DefaultExecuteController(logger Log, commandDispatcher CommandDispatcher, resourceRepository *ResourceRepository, api *openapi3.T, pathMap map[string]*openapi3.PathItem, operation map[string]*openapi3.Operation) echo.HandlerFunc { var commandName string var err error var schema *openapi3.Schema @@ -52,7 +91,6 @@ func DefaultWriteController(logger Log, commandDispatcher CommandDispatcher, res return func(ctxt echo.Context) error { var sequenceNo string var seq int - var commandResponse interface{} //getting etag from context etag := ctxt.Request().Header.Get("If-Match") @@ -64,13 +102,12 @@ func DefaultWriteController(logger Log, commandDispatcher CommandDispatcher, res } } - var resource *BasicResource body, err := io.ReadAll(ctxt.Request().Body) if err != nil { ctxt.Logger().Debugf("unexpected error reading request body: %s", err) return NewControllerError("unexpected error reading request body", err, http.StatusBadRequest) } - resource, err = new(BasicResource).FromSchema(api, body) + resource, err := resourceRepository.Initialize(ctxt.Request().Context(), logger, body) //not sure this is correct payload, err := json.Marshal(&ResourceCreateParams{ Resource: resource, @@ -89,28 +126,17 @@ func DefaultWriteController(logger Log, commandDispatcher CommandDispatcher, res AccountID: GetAccount(ctxt.Request().Context()), }, } - commandResponse, err = commandDispatcher.Dispatch(ctxt.Request().Context(), command, resourceRepository, ctxt.Logger()) + _, err = commandDispatcher.Dispatch(ctxt.Request().Context(), command, resourceRepository, ctxt.Logger()) //an error handler `HTTPErrorHandler` can be defined on the echo instance to handle error responses if err != nil { } - - //TODO the type of command and/or the api configuration should determine the status code - switch commandName { - case "create": - //set etag in response header - if entity, ok := commandResponse.(*BasicResource); ok { - ctxt.Response().Header().Set("ETag", fmt.Sprintf("%s.%d", entity.GetID(), entity.GetSequenceNo())) - return ctxt.JSON(http.StatusCreated, entity) - } - return ctxt.JSON(http.StatusCreated, commandResponse) - default: - //check to see if the response is a map or string - if stringResponse, ok := commandResponse.(string); ok { - return ctxt.String(http.StatusOK, stringResponse) - } else { - return ctxt.JSON(http.StatusOK, commandResponse) - } + //set etag in response header + ctxt.Response().Header().Set("ETag", fmt.Sprintf("%s.%d", resource.GetID(), resource.GetSequenceNo())) + if resource.GetSequenceNo() == 1 { + return ctxt.JSON(http.StatusCreated, resource) + } else { + return ctxt.JSON(http.StatusOK, resource) } } } diff --git a/v2/rest/controllers_test.go b/v2/rest/controllers_test.go index 24a73cec..0e873b9e 100644 --- a/v2/rest/controllers_test.go +++ b/v2/rest/controllers_test.go @@ -30,14 +30,29 @@ func TestDefaultWriteController(t *testing.T) { }, } - t.Run("create a simple resource", func(t *testing.T) { - repository := &RepositoryMock{ - PersistFunc: func(ctxt context.Context, logger rest.Log, resources []rest.Resource) []error { + t.Run("create a simple resource for the first time", func(t *testing.T) { + defaultProjection := &ProjectionMock{ + GetByURIFunc: func(ctxt context.Context, logger rest.Log, uri string) (rest.Resource, error) { + return nil, nil + }, + } + eventStore := &EventStoreMock{ + AddSubscriberFunc: func(config rest.EventHandlerConfig) error { return nil }, } + params := rest.ResourceRepositoryParams{ + EventStore: eventStore, + DefaultProjection: defaultProjection, + Config: schema, + } + result, err := rest.NewResourceRepository(params) + if err != nil { + t.Fatalf("expected no error, got %s", err) + } + repository := result.Repository commandDispatcher := &CommandDispatcherMock{ - DispatchFunc: func(ctx context.Context, command *rest.Command, repository rest.Repository, logger rest.Log) (interface{}, error) { + DispatchFunc: func(ctx context.Context, command *rest.Command, repository *rest.ResourceRepository, logger rest.Log) (interface{}, error) { return nil, nil }, } @@ -55,10 +70,5 @@ func TestDefaultWriteController(t *testing.T) { if resp.Code != http.StatusCreated { t.Errorf("expected status code %d, got %d", http.StatusCreated, resp.Code) } - - if len(commandDispatcher.DispatchCalls()) != 1 { - t.Errorf("expected dispatch to be called once, got %d", len(commandDispatcher.DispatchCalls())) - } - }) } diff --git a/v2/rest/event.go b/v2/rest/event.go index ac112e3a..ff991b6c 100644 --- a/v2/rest/event.go +++ b/v2/rest/event.go @@ -2,13 +2,15 @@ package rest import ( "encoding/json" + "github.com/getkin/kin-openapi/openapi3" + "gorm.io/gorm" ) type Event struct { - ID string `json:"id"` + gorm.Model Type string `json:"type"` Payload json.RawMessage `json:"payload"` - Meta EventMeta `json:"meta"` + Meta EventMeta `json:"meta" gorm:"embedded"` Version int `json:"version"` errors []error } @@ -53,3 +55,18 @@ func (e *Event) GetID() string { //TODO implement me panic("implement me") } + +func (e *Event) FromBytes(schema *openapi3.T, data []byte) (Resource, error) { + //TODO implement me + panic("implement me") +} + +func (e *Event) IsValid() bool { + //TODO implement me + panic("implement me") +} + +func (e *Event) GetErrors() []error { + //TODO implement me + panic("implement me") +} diff --git a/v2/rest/event_test.go b/v2/rest/event_test.go index 2b318c58..1227490e 100644 --- a/v2/rest/event_test.go +++ b/v2/rest/event_test.go @@ -8,10 +8,10 @@ import ( func TestDefaultEventDisptacher_AddSubscriber(t *testing.T) { t.Run("add subscriber for event type only", func(t *testing.T) { - eventDispatcher := new(rest.GORMEventStore) + eventDispatcher := new(rest.GORMProjection) err := eventDispatcher.AddSubscriber(rest.EventHandlerConfig{ Type: "create", - Handler: func(ctx context.Context, logger rest.Log, event rest.Event) error { + Handler: func(ctx context.Context, logger rest.Log, event *rest.Event) error { return nil }, }) @@ -31,11 +31,11 @@ func TestDefaultEventDisptacher_AddSubscriber(t *testing.T) { } }) t.Run("add subscriber for resource type and event", func(t *testing.T) { - eventDispatcher := new(rest.GORMEventStore) + eventDispatcher := new(rest.GORMProjection) err := eventDispatcher.AddSubscriber(rest.EventHandlerConfig{ ResourceType: "Article", Type: "create", - Handler: func(ctx context.Context, logger rest.Log, event rest.Event) error { + Handler: func(ctx context.Context, logger rest.Log, event *rest.Event) error { return nil }, }) @@ -55,7 +55,7 @@ func TestDefaultEventDisptacher_AddSubscriber(t *testing.T) { } }) t.Run("adding subscriber without handler should throw error", func(t *testing.T) { - eventDispatcher := new(rest.GORMEventStore) + eventDispatcher := new(rest.GORMProjection) err := eventDispatcher.AddSubscriber(rest.EventHandlerConfig{ Type: "create", }) @@ -83,10 +83,10 @@ func TestResourceRepository_Dispatch(t *testing.T) { t.Run("should trigger resource specific handler and generic event type handler", func(t *testing.T) { createHandlerHit := false articleCreateHandlerHit := false - eventDispatcher := new(rest.GORMEventStore) + eventDispatcher := new(rest.GORMProjection) err := eventDispatcher.AddSubscriber(rest.EventHandlerConfig{ Type: "create", - Handler: func(ctx context.Context, logger rest.Log, event rest.Event) error { + Handler: func(ctx context.Context, logger rest.Log, event *rest.Event) error { createHandlerHit = true return nil }, @@ -97,7 +97,7 @@ func TestResourceRepository_Dispatch(t *testing.T) { err = eventDispatcher.AddSubscriber(rest.EventHandlerConfig{ Type: "create", ResourceType: "Article", - Handler: func(ctx context.Context, logger rest.Log, event rest.Event) error { + Handler: func(ctx context.Context, logger rest.Log, event *rest.Event) error { articleCreateHandlerHit = true return nil }, @@ -105,12 +105,12 @@ func TestResourceRepository_Dispatch(t *testing.T) { if err != nil { t.Errorf("expected no error, got %s", err) } - errors := eventDispatcher.Dispatch(context.Background(), rest.Event{ + errors := eventDispatcher.Dispatch(context.Background(), logger, &rest.Event{ Type: "create", Meta: rest.EventMeta{ ResourceType: "Article", }, - }, logger) + }) if len(errors) != 0 { t.Errorf("expected no errors, got %d", len(errors)) } diff --git a/v2/rest/gorm.go b/v2/rest/gorm.go index ff668afd..02a803a7 100644 --- a/v2/rest/gorm.go +++ b/v2/rest/gorm.go @@ -2,6 +2,7 @@ package rest import ( "database/sql" + "encoding/json" "errors" "fmt" awsconfig "github.com/aws/aws-sdk-go-v2/config" @@ -171,63 +172,38 @@ func NewGORM(p GORMParams) (GORMResult, error) { }, err } -type GORMResourceRepositoryParams struct { - fx.In - GormDB *gorm.DB -} - -type GORMResourceRepositoryResult struct { - fx.Out - Repository *GORMResourceRepository -} - -func NewGORMResourceRepository(p GORMResourceRepositoryParams) GORMResourceRepositoryResult { - return GORMResourceRepositoryResult{ - Repository: &GORMResourceRepository{ - db: p.GormDB, - }, - } -} - -type GORMResourceRepository struct { - db *gorm.DB -} - -func (w GORMResourceRepository) GetByURI(ctxt context.Context, logger Log, uri string) (resource *BasicResource, err error) { - w.db.First(&resource, "id = ?", uri) - err = w.db.Error - return resource, err -} - -func (w GORMResourceRepository) Save(ctxt context.Context, logger Log, resource *BasicResource) error { - result := w.db.Save(resource) - return result.Error -} - -func (w GORMResourceRepository) Delete(ctxt context.Context, logger Log, resource *BasicResource) error { - result := w.db.Delete(resource) - return result.Error -} - -type GORMEventStoreParams struct { +type GORMProjectionParams struct { fx.In GORMDB *gorm.DB EventConfigs []EventHandlerConfig `group:"eventHandlers"` } -type GORMEventStoreResult struct { +type GORMProjectionResult struct { fx.Out - Dispatcher EventDispatcher + Dispatcher EventStore + DefaultProjection Projection `name:"defaultProjection"` } -func NewGORMEventStore(p GORMEventStoreParams) GORMEventStoreResult { - dispatcher := &GORMEventStore{ +func NewGORMProjection(p GORMProjectionParams) (result GORMProjectionResult, err error) { + dispatcher := &GORMProjection{ handlers: make(map[string]map[string][]EventHandler), + gormDB: p.GORMDB, + } + err = p.GORMDB.AutoMigrate(&Event{}, &BasicResource{}) + if err != nil { + return result, err } for _, config := range p.EventConfigs { - dispatcher.AddSubscriber(config) + err = dispatcher.AddSubscriber(config) + if err != nil { + return result, err + } } - return GORMEventStoreResult{} + result = GORMProjectionResult{ + Dispatcher: dispatcher, + DefaultProjection: dispatcher, + } + return result, nil } type EventHandlerConfig struct { @@ -236,12 +212,15 @@ type EventHandlerConfig struct { Handler EventHandler } -type GORMEventStore struct { +// GORMProjection is a projection that uses GORM to persist events +type GORMProjection struct { handlers map[string]map[string][]EventHandler handlerPanicked bool + gormDB *gorm.DB } -func (e *GORMEventStore) Dispatch(ctx context.Context, event Event, logger Log) []error { +// Dispatch dispatches the event to the handlers +func (e *GORMProjection) Dispatch(ctx context.Context, logger Log, event *Event) []error { //mutex helps keep state between routines var errors []error var wg sync.WaitGroup @@ -282,7 +261,8 @@ func (e *GORMEventStore) Dispatch(ctx context.Context, event Event, logger Log) return errors } -func (e *GORMEventStore) AddSubscriber(handler EventHandlerConfig) error { +// AddSubscriber adds a subscriber to the event dispatcher +func (e *GORMProjection) AddSubscriber(handler EventHandlerConfig) error { if handler.Handler == nil { return fmt.Errorf("event handler cannot be nil") } @@ -299,29 +279,103 @@ func (e *GORMEventStore) AddSubscriber(handler EventHandlerConfig) error { return nil } -func (e *GORMEventStore) GetSubscribers(resourceType string) map[string][]EventHandler { +func (e *GORMProjection) GetSubscribers(resourceType string) map[string][]EventHandler { if handlers, ok := e.handlers[resourceType]; ok { return handlers } return nil } -func (e *GORMEventStore) GetByURI(ctxt context.Context, logger Log, uri string) (Resource, error) { +func (e *GORMProjection) GetByURI(ctxt context.Context, logger Log, uri string) (Resource, error) { //TODO implement me panic("implement me") } -func (e *GORMEventStore) GetByKey(ctxt context.Context, identifiers map[string]interface{}) (Resource, error) { +func (e *GORMProjection) GetByKey(ctxt context.Context, identifiers map[string]interface{}) (Resource, error) { //TODO implement me panic("implement me") } -func (e *GORMEventStore) GetList(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]Resource, int64, error) { +func (e *GORMProjection) GetList(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]Resource, int64, error) { //TODO implement me panic("implement me") } -func (e *GORMEventStore) GetByProperties(ctxt context.Context, identifiers map[string]interface{}) ([]Entity, error) { +func (e *GORMProjection) GetByProperties(ctxt context.Context, identifiers map[string]interface{}) ([]Entity, error) { //TODO implement me panic("implement me") } + +// Persist persists the events to the database +func (e *GORMProjection) Persist(ctxt context.Context, logger Log, resources []Resource) (errs []error) { + //TODO not sure this casting is needed + var events []*Event + for _, resource := range resources { + if event, ok := resource.(*Event); ok { + events = append(events, event) + } else { + errs = append(errs, errors.New("resource is not an event")) + } + } + result := e.gormDB.Save(resources) + if result.Error != nil { + errs = append(errs, result.Error) + } + for _, event := range events { + e.Dispatch(ctxt, logger, event) + } + return errs +} + +func (e *GORMProjection) Remove(ctxt context.Context, logger Log, resources []Resource) []error { + //TODO implement me + panic("implement me") +} + +func (e *GORMProjection) GetEventHandlers() []EventHandlerConfig { + return []EventHandlerConfig{ + { + ResourceType: "", + Type: "create", + Handler: e.ResourceUpdateHandler, + }, + { + ResourceType: "", + Type: "update", + Handler: e.ResourceUpdateHandler, + }, + { + ResourceType: "", + Type: "delete", + Handler: e.ResourceDeleteHandler, + }, + } +} + +// ResourceUpdateHandler handles Create Update operations +func (e *GORMProjection) ResourceUpdateHandler(ctx context.Context, logger Log, event *Event) (err error) { + basicResource := new(BasicResource) + err = json.Unmarshal(event.Payload, &basicResource) + if err != nil { + return err + } + result := e.gormDB.Save(basicResource) + if result.Error != nil { + return result.Error + } + return err +} + +// ResourceDeleteHandler handles Delete operations +func (e *GORMProjection) ResourceDeleteHandler(ctx context.Context, logger Log, event *Event) (err error) { + basicResource := new(BasicResource) + err = json.Unmarshal(event.Payload, &basicResource) + if err != nil { + return err + } + result := e.gormDB.Delete(basicResource) + if result.Error != nil { + return result.Error + } + return err +} diff --git a/v2/rest/initializers.go b/v2/rest/initializers.go index da897da3..548daf7c 100644 --- a/v2/rest/initializers.go +++ b/v2/rest/initializers.go @@ -3,16 +3,30 @@ package rest import ( "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" + "go.uber.org/fx" + "net/http" ) type RouteParams struct { - Config *openapi3.T - Echo *echo.Echo + fx.In + Config *openapi3.T + Echo *echo.Echo + Logger Log + CommandDispatcher CommandDispatcher + ResourceRepository *ResourceRepository } -func RouteInitializer(techo *echo.Echo, config *openapi3.T) { +func RouteInitializer(p RouteParams) { //TODO read all the routes and configurations to determine which controller and - techo.Add("GET", "/health", func(c echo.Context) error { + p.Echo.Add(http.MethodGet, "/health", func(c echo.Context) error { return c.String(200, "OK") }) + p.Echo.Add(http.MethodPost, "/*", DefaultWriteController(p.Logger, p.CommandDispatcher, &ResourceRepository{}, p.Config, nil, nil)) + p.Echo.Add(http.MethodPut, "/*", DefaultWriteController(p.Logger, p.CommandDispatcher, &ResourceRepository{}, p.Config, nil, nil)) + p.Echo.Add(http.MethodPatch, "/*", DefaultWriteController(p.Logger, p.CommandDispatcher, &ResourceRepository{}, p.Config, nil, nil)) + + //output registered endpoints for debugging purposes + for _, route := range p.Echo.Routes() { + p.Logger.Debugf("Registered routes '%s' '%s'", route.Method, route.Path) + } } diff --git a/v2/rest/interfaces.go b/v2/rest/interfaces.go index dfc2130f..e11e5156 100644 --- a/v2/rest/interfaces.go +++ b/v2/rest/interfaces.go @@ -11,13 +11,13 @@ type ( //Middleware that is bound to an OpenAPI operation Middleware func(commandDispatcher CommandDispatcher, repository Repository, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc //Controller is the handler for a specific operation - Controller func(commandDispatcher CommandDispatcher, repository Repository, path map[string]*openapi3.PathItem, operation map[string]*openapi3.Operation) echo.HandlerFunc + Controller func(commandDispatcher CommandDispatcher, repository *ResourceRepository, path map[string]*openapi3.PathItem, operation map[string]*openapi3.Operation) echo.HandlerFunc //OperationInitializer initialzers that are run when processing OpenAPI operations GlobalInitializer func(context.Context, *openapi3.T) (context.Context, error) OperationInitializer func(context.Context, string, string, *openapi3.T, *openapi3.PathItem, *openapi3.Operation) (context.Context, error) PathInitializer func(context.Context, string, *openapi3.T, *openapi3.PathItem) (context.Context, error) - CommandHandler func(ctx context.Context, command *Command, repository Repository, logger Log) (interface{}, error) - EventHandler func(ctx context.Context, logger Log, event Event) error + CommandHandler func(ctx context.Context, command *Command, repository *ResourceRepository, logger Log) (interface{}, error) + EventHandler func(ctx context.Context, logger Log, event *Event) error ) type Entity interface { @@ -38,15 +38,15 @@ type Repository interface { } type CommandDispatcher interface { - Dispatch(ctx context.Context, command *Command, repository Repository, logger Log) (interface{}, error) + Dispatch(ctx context.Context, command *Command, repository *ResourceRepository, logger Log) (interface{}, error) AddSubscriber(command CommandConfig) map[string][]CommandHandler GetSubscribers() map[string][]CommandHandler } type EventDispatcher interface { AddSubscriber(handler EventHandlerConfig) error - GetSubscribers() []EventHandler - Dispatch(ctx context.Context, event Event, logger Log) []error + GetSubscribers(resourceType string) map[string][]EventHandler + Dispatch(ctx context.Context, logger Log, event *Event) []error } type EventStore interface { @@ -62,4 +62,5 @@ type Projection interface { // GetList returns a paginated result of content entities GetList(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]Resource, int64, error) GetByProperties(ctxt context.Context, identifiers map[string]interface{}) ([]Entity, error) + GetEventHandlers() []EventHandlerConfig } diff --git a/v2/rest/receivers.go b/v2/rest/receivers.go index 8dc956fd..bb44a375 100644 --- a/v2/rest/receivers.go +++ b/v2/rest/receivers.go @@ -7,7 +7,7 @@ import ( ) type ResourceCreateParams struct { - Resource *BasicResource + Resource Resource Schema *openapi3.Schema } diff --git a/v2/rest/repository.go b/v2/rest/repository.go index 44bcd66f..a408f1e4 100644 --- a/v2/rest/repository.go +++ b/v2/rest/repository.go @@ -1,30 +1,48 @@ package rest import ( + "github.com/getkin/kin-openapi/openapi3" "go.uber.org/fx" "golang.org/x/net/context" ) type ResourceRepositoryParams struct { fx.In - EventStore EventStore - Handlers []EventHandlerConfig `group:"projections"` + EventStore EventStore + Projections []Projection `group:"projections"` + DefaultProjection Projection `name:"defaultProjection" optional:"true"` + Config *openapi3.T } type ResourceRepositoryResult struct { fx.Out - Repository ResourceRepository + Repository *ResourceRepository } // NewResourceRepository creates a new resource repository and registers all the event handlers func NewResourceRepository(p ResourceRepositoryParams) (ResourceRepositoryResult, error) { - repo := ResourceRepository{ - eventStore: p.EventStore, + repo := &ResourceRepository{ + eventStore: p.EventStore, + defaultProjection: p.DefaultProjection, + projections: p.Projections, + config: p.Config, } - for _, handler := range p.Handlers { - err := p.EventStore.AddSubscriber(handler) - if err != nil { - return ResourceRepositoryResult{}, err + + if p.DefaultProjection != nil { + for _, handler := range p.DefaultProjection.GetEventHandlers() { + err := p.EventStore.AddSubscriber(handler) + if err != nil { + return ResourceRepositoryResult{}, err + } + } + } + + for _, projection := range p.Projections { + for _, handler := range projection.GetEventHandlers() { + err := p.EventStore.AddSubscriber(handler) + if err != nil { + return ResourceRepositoryResult{}, err + } } } return ResourceRepositoryResult{ @@ -33,7 +51,26 @@ func NewResourceRepository(p ResourceRepositoryParams) (ResourceRepositoryResult } type ResourceRepository struct { - eventStore EventStore + eventStore EventStore + defaultProjection Projection + projections []Projection + config *openapi3.T +} + +func (r *ResourceRepository) Initialize(ctxt context.Context, logger Log, payload []byte) (resource Resource, err error) { + //try to get the resource from the default projection and if it doesn't exist create it + resource, err = r.defaultProjection.GetByURI(ctxt, logger, "") + if err != nil { + logger.Debugf("error encountered getting resource from default projection '%s'", err) + return nil, err + } + if resource == nil { + resource = new(BasicResource) + } + + resource, err = resource.FromBytes(r.config, payload) + + return resource, err } func (r *ResourceRepository) Persist(ctxt context.Context, logger Log, resources []Resource) []error { @@ -48,3 +85,11 @@ func (r *ResourceRepository) Remove(ctxt context.Context, logger Log, resources //TODO implement me panic("implement me") } + +func (r *ResourceRepository) GetProjections() []Projection { + return r.projections +} + +func (r *ResourceRepository) GetDefaultProjection() Projection { + return r.defaultProjection +} diff --git a/v2/rest/repository_test.go b/v2/rest/repository_test.go index d3c635ab..b2b3dc0d 100644 --- a/v2/rest/repository_test.go +++ b/v2/rest/repository_test.go @@ -7,6 +7,116 @@ import ( "testing" ) +func TestResourceRepository_Initialize(t *testing.T) { + logger := &LogMock{ + DebugfFunc: func(format string, args ...interface{}) { + + }, + DebugFunc: func(args ...interface{}) { + + }, + ErrorfFunc: func(format string, args ...interface{}) { + + }, + ErrorFunc: func(args ...interface{}) { + + }, + } + schema, err := openapi3.NewLoader().LoadFromFile("fixtures/blog.yaml") + if err != nil { + t.Fatalf("error encountered loading schema '%s'", err) + } + t.Run("create new resource if one doesn't already exist", func(t *testing.T) { + defaultProjection := &ProjectionMock{ + GetByURIFunc: func(ctxt context.Context, logger rest.Log, uri string) (rest.Resource, error) { + return nil, nil + }, + } + eventStore := &EventStoreMock{ + AddSubscriberFunc: func(config rest.EventHandlerConfig) error { + return nil + }, + } + params := rest.ResourceRepositoryParams{ + EventStore: eventStore, + DefaultProjection: defaultProjection, + Config: schema, + } + result, err := rest.NewResourceRepository(params) + if err != nil { + t.Fatalf("expected no error, got %s", err) + } + resourceRepository := result.Repository + resource, err := resourceRepository.Initialize(context.Background(), logger, []byte(`{ + "@id": "/blogs/test", + "@type": "http://schema.org/Blog", + "title": "test" + }`)) + if err != nil { + t.Fatalf("expected no error, got %s", err) + } + if resource == nil { + t.Fatalf("expected resource to be created") + } + if resource.GetID() != "/blogs/test" { + t.Errorf("expected resource id to be '/blogs/test', got %s", resource.GetID()) + } + }) + t.Run("use the resource projection if it's available", func(t *testing.T) { + defaultProjection := &ProjectionMock{ + GetByURIFunc: func(ctxt context.Context, logger rest.Log, uri string) (rest.Resource, error) { + resource := &rest.BasicResource{ + Metadata: rest.ResourceMetadata{ + ID: "/blogs/test", + SequenceNo: 2, + Type: "http://schema.org/Blog", + Version: 2, + UserID: "", + AccountID: "", + }, + } + return resource, nil + }, + } + eventStore := &EventStoreMock{ + AddSubscriberFunc: func(config rest.EventHandlerConfig) error { + return nil + }, + } + params := rest.ResourceRepositoryParams{ + EventStore: eventStore, + DefaultProjection: defaultProjection, + Config: schema, + } + result, err := rest.NewResourceRepository(params) + if err != nil { + t.Fatalf("expected no error, got %s", err) + } + resourceRepository := result.Repository + resource, err := resourceRepository.Initialize(context.Background(), logger, []byte(`{ + "@id": "/blogs/test", + "@type": "http://schema.org/Blog", + "title": "New Title" + }`)) + if err != nil { + t.Fatalf("expected no error, got %s", err) + } + if resource == nil { + t.Fatalf("expected resource to be created") + } + if resource.GetID() != "/blogs/test" { + t.Errorf("expected resource id to be '/blogs/test', got %s", resource.GetID()) + } + + if resource.GetSequenceNo() != 3 { + t.Errorf("expected sequence number to be 3, got %d", resource.GetSequenceNo()) + } + if basicResource, ok := resource.(*rest.BasicResource); !ok || basicResource.GetString("title") != "New Title" { + t.Errorf("expected title to be 'New Title', got %s", basicResource.GetString("title")) + } + }) +} + func TestResourceRepository_Persist(t *testing.T) { logger := &LogMock{ @@ -30,7 +140,7 @@ func TestResourceRepository_Persist(t *testing.T) { t.Run("should trigger Blog create event", func(t *testing.T) { createBlogHandlerHit := false - resource, err := new(rest.BasicResource).FromSchema(schema, []byte(`{ + resource, err := new(rest.BasicResource).FromBytes(schema, []byte(`{ "@id": "/blogs/test", "@type": "http://schema.org/Blog", "title": "test" @@ -43,12 +153,8 @@ func TestResourceRepository_Persist(t *testing.T) { AddSubscriberFunc: func(config rest.EventHandlerConfig) error { return nil }, - DispatchFunc: func(ctx context.Context, event rest.Event, logger rest.Log) []error { - //TODO check that the event is the correct one - createBlogHandlerHit = true - return nil - }, PersistFunc: func(ctxt context.Context, logger rest.Log, resources []rest.Resource) []error { + createBlogHandlerHit = true return nil }, } diff --git a/v2/rest/resource.go b/v2/rest/resource.go index a19d1774..cfaf097a 100644 --- a/v2/rest/resource.go +++ b/v2/rest/resource.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "github.com/getkin/kin-openapi/openapi3" - "github.com/segmentio/ksuid" + "gorm.io/datatypes" "time" ) @@ -19,16 +19,19 @@ type Resource interface { GetType() string GetSequenceNo() int GetID() string + FromBytes(schema *openapi3.T, data []byte) (Resource, error) + IsValid() bool + GetErrors() []error } type BasicResource struct { - body map[string]interface{} - Metadata ResourceMetadata - newEvents []Resource + Body datatypes.JSONMap `gorm:"default:'[]'; null'"` + Metadata ResourceMetadata `gorm:"embedded"` + newEvents []Resource `gorm:"-"` } type ResourceMetadata struct { - ID string + ID string `gorm:"primaryKey"` SequenceNo int Type string Version int64 @@ -38,30 +41,41 @@ type ResourceMetadata struct { // MarshalJSON customizes the JSON encoding of BasicResource func (r *BasicResource) MarshalJSON() ([]byte, error) { - return json.Marshal(r.body) + return json.Marshal(r.Body) } // UnmarshalJSON customizes the JSON decoding of BasicResource -func (r *BasicResource) UnmarshalJSON(data []byte) error { +func (r *BasicResource) UnmarshalJSON(data []byte) (err error) { // You might want to initialize your map if it's nil - if r.body == nil { - r.body = make(map[string]interface{}) - r.body["@context"] = map[string]interface{}{} - r.body["@type"] = "" + if r.Body == nil { + r.Body = make(map[string]interface{}) + r.Body["@context"] = map[string]interface{}{} } // Unmarshal data into the map - return json.Unmarshal(data, &r.body) + err = json.Unmarshal(data, &r.Body) + if ttype, ok := r.Body["@type"].(string); ok { + r.Metadata.Type = ttype + } + + if id, ok := r.Body["@id"].(string); ok { + r.Metadata.ID = id + } + return err } -// FromSchema creates a new BasicResource from a schema and data -func (r *BasicResource) FromSchema(schema *openapi3.T, data []byte) (*BasicResource, error) { +// FromBytes creates a new BasicResource from a schema and data +func (r *BasicResource) FromBytes(schema *openapi3.T, data []byte) (Resource, error) { err := r.UnmarshalJSON(data) //TODO use the schema to validate the data //TODO fill in any missing blanks if r.GetType() == "" { return nil, fmt.Errorf("missing type") } - r.NewChange(NewResourceEvent("create", r, r.body)) + eventType := "create" + if r.GetSequenceNo() > 0 { + eventType = "update" + } + r.NewChange(NewResourceEvent(eventType, r, r.Body)) return r, err } @@ -81,17 +95,11 @@ func (r *BasicResource) GetErrors() []error { } func (r *BasicResource) GetID() string { - if id, ok := r.body["@id"].(string); ok { - return id - } - return "" + return r.Metadata.ID } func (r *BasicResource) GetType() string { - if ttype, ok := r.body["@type"].(string); ok { - return ttype - } - return "" + return r.Metadata.Type } func (r *BasicResource) GetSequenceNo() int { @@ -114,12 +122,39 @@ func (r *BasicResource) Persist() { r.newEvents = nil } +func (r *BasicResource) GetString(propertyName string) string { + if value, ok := r.Body[propertyName].(string); ok { + return value + } + return "" +} + +func (r *BasicResource) GetBool(propertyName string) bool { + if value, ok := r.Body[propertyName].(bool); ok { + return value + } + return false +} + +func (r *BasicResource) GetInt(propertyName string) int { + if value, ok := r.Body[propertyName].(int); ok { + return value + } + return 0 +} + +func (r *BasicResource) GetFloat(propertyName string) float64 { + if value, ok := r.Body[propertyName].(float64); ok { + return value + } + return 0.0 +} + func NewResourceEvent(eventType string, resource Resource, tpayload map[string]interface{}) *Event { var payload json.RawMessage payload, _ = json.Marshal(tpayload) return &Event{ - ID: ksuid.New().String(), Type: eventType, Payload: payload, Version: 2, diff --git a/v2/rest/resource_test.go b/v2/rest/resource_test.go index 3422ba81..8c994f42 100644 --- a/v2/rest/resource_test.go +++ b/v2/rest/resource_test.go @@ -12,7 +12,7 @@ func TestBasicResource_FromSchema(t *testing.T) { t.Fatalf("error encountered loading schema '%s'", err) } t.Run("create a simple resource", func(t *testing.T) { - resource, err := new(rest.BasicResource).FromSchema(schema, []byte(`{ + resource, err := new(rest.BasicResource).FromBytes(schema, []byte(`{ "@id": "http://example.com/resource/1", "@type": "http://schema.org/Blog", "title": "test" @@ -44,7 +44,7 @@ func TestBasicResource_FromSchema(t *testing.T) { } }) t.Run("resource type not specified should return an error", func(t *testing.T) { - _, err := new(rest.BasicResource).FromSchema(schema, []byte(`{ + _, err := new(rest.BasicResource).FromBytes(schema, []byte(`{ "@id": "http://example.com/resource/1", "title": "test" }`)) diff --git a/v2/rest/rest_mocks_test.go b/v2/rest/rest_mocks_test.go index 4d306036..a119b17c 100644 --- a/v2/rest/rest_mocks_test.go +++ b/v2/rest/rest_mocks_test.go @@ -842,6 +842,9 @@ var _ rest.Projection = &ProjectionMock{} // GetByURIFunc: func(ctxt context.Context, logger rest.Log, uri string) (rest.Resource, error) { // panic("mock out the GetByURI method") // }, +// GetEventHandlersFunc: func() []rest.EventHandlerConfig { +// panic("mock out the GetEventHandlers method") +// }, // GetListFunc: func(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]rest.Resource, int64, error) { // panic("mock out the GetList method") // }, @@ -861,6 +864,9 @@ type ProjectionMock struct { // GetByURIFunc mocks the GetByURI method. GetByURIFunc func(ctxt context.Context, logger rest.Log, uri string) (rest.Resource, error) + // GetEventHandlersFunc mocks the GetEventHandlers method. + GetEventHandlersFunc func() []rest.EventHandlerConfig + // GetListFunc mocks the GetList method. GetListFunc func(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]rest.Resource, int64, error) @@ -889,6 +895,9 @@ type ProjectionMock struct { // URI is the uri argument value. URI string } + // GetEventHandlers holds details about calls to the GetEventHandlers method. + GetEventHandlers []struct { + } // GetList holds details about calls to the GetList method. GetList []struct { // Ctx is the ctx argument value. @@ -905,10 +914,11 @@ type ProjectionMock struct { FilterOptions map[string]interface{} } } - lockGetByKey sync.RWMutex - lockGetByProperties sync.RWMutex - lockGetByURI sync.RWMutex - lockGetList sync.RWMutex + lockGetByKey sync.RWMutex + lockGetByProperties sync.RWMutex + lockGetByURI sync.RWMutex + lockGetEventHandlers sync.RWMutex + lockGetList sync.RWMutex } // GetByKey calls GetByKeyFunc. @@ -1023,6 +1033,33 @@ func (mock *ProjectionMock) GetByURICalls() []struct { return calls } +// GetEventHandlers calls GetEventHandlersFunc. +func (mock *ProjectionMock) GetEventHandlers() []rest.EventHandlerConfig { + if mock.GetEventHandlersFunc == nil { + panic("ProjectionMock.GetEventHandlersFunc: method is nil but Projection.GetEventHandlers was just called") + } + callInfo := struct { + }{} + mock.lockGetEventHandlers.Lock() + mock.calls.GetEventHandlers = append(mock.calls.GetEventHandlers, callInfo) + mock.lockGetEventHandlers.Unlock() + return mock.GetEventHandlersFunc() +} + +// GetEventHandlersCalls gets all the calls that were made to GetEventHandlers. +// Check the length with: +// +// len(mockedProjection.GetEventHandlersCalls()) +func (mock *ProjectionMock) GetEventHandlersCalls() []struct { +} { + var calls []struct { + } + mock.lockGetEventHandlers.RLock() + calls = mock.calls.GetEventHandlers + mock.lockGetEventHandlers.RUnlock() + return calls +} + // GetList calls GetListFunc. func (mock *ProjectionMock) GetList(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]rest.Resource, int64, error) { if mock.GetListFunc == nil { @@ -1088,7 +1125,7 @@ var _ rest.CommandDispatcher = &CommandDispatcherMock{} // AddSubscriberFunc: func(command rest.CommandConfig) map[string][]rest.CommandHandler { // panic("mock out the AddSubscriber method") // }, -// DispatchFunc: func(ctx context.Context, command *rest.Command, repository rest.Repository, logger rest.Log) (interface{}, error) { +// DispatchFunc: func(ctx context.Context, command *rest.Command, repository *rest.ResourceRepository, logger rest.Log) (interface{}, error) { // panic("mock out the Dispatch method") // }, // GetSubscribersFunc: func() map[string][]rest.CommandHandler { @@ -1105,7 +1142,7 @@ type CommandDispatcherMock struct { AddSubscriberFunc func(command rest.CommandConfig) map[string][]rest.CommandHandler // DispatchFunc mocks the Dispatch method. - DispatchFunc func(ctx context.Context, command *rest.Command, repository rest.Repository, logger rest.Log) (interface{}, error) + DispatchFunc func(ctx context.Context, command *rest.Command, repository *rest.ResourceRepository, logger rest.Log) (interface{}, error) // GetSubscribersFunc mocks the GetSubscribers method. GetSubscribersFunc func() map[string][]rest.CommandHandler @@ -1124,7 +1161,7 @@ type CommandDispatcherMock struct { // Command is the command argument value. Command *rest.Command // Repository is the repository argument value. - Repository rest.Repository + Repository *rest.ResourceRepository // Logger is the logger argument value. Logger rest.Log } @@ -1170,14 +1207,14 @@ func (mock *CommandDispatcherMock) AddSubscriberCalls() []struct { } // Dispatch calls DispatchFunc. -func (mock *CommandDispatcherMock) Dispatch(ctx context.Context, command *rest.Command, repository rest.Repository, logger rest.Log) (interface{}, error) { +func (mock *CommandDispatcherMock) Dispatch(ctx context.Context, command *rest.Command, repository *rest.ResourceRepository, logger rest.Log) (interface{}, error) { if mock.DispatchFunc == nil { panic("CommandDispatcherMock.DispatchFunc: method is nil but CommandDispatcher.Dispatch was just called") } callInfo := struct { Ctx context.Context Command *rest.Command - Repository rest.Repository + Repository *rest.ResourceRepository Logger rest.Log }{ Ctx: ctx, @@ -1198,13 +1235,13 @@ func (mock *CommandDispatcherMock) Dispatch(ctx context.Context, command *rest.C func (mock *CommandDispatcherMock) DispatchCalls() []struct { Ctx context.Context Command *rest.Command - Repository rest.Repository + Repository *rest.ResourceRepository Logger rest.Log } { var calls []struct { Ctx context.Context Command *rest.Command - Repository rest.Repository + Repository *rest.ResourceRepository Logger rest.Log } mock.lockDispatch.RLock() @@ -1253,10 +1290,10 @@ var _ rest.EventDispatcher = &EventDispatcherMock{} // AddSubscriberFunc: func(handler rest.EventHandlerConfig) error { // panic("mock out the AddSubscriber method") // }, -// DispatchFunc: func(ctx context.Context, event rest.Event, logger rest.Log) []error { +// DispatchFunc: func(ctx context.Context, logger rest.Log, event *rest.Event) []error { // panic("mock out the Dispatch method") // }, -// GetSubscribersFunc: func() []rest.EventHandler { +// GetSubscribersFunc: func(resourceType string) map[string][]rest.EventHandler { // panic("mock out the GetSubscribers method") // }, // } @@ -1270,10 +1307,10 @@ type EventDispatcherMock struct { AddSubscriberFunc func(handler rest.EventHandlerConfig) error // DispatchFunc mocks the Dispatch method. - DispatchFunc func(ctx context.Context, event rest.Event, logger rest.Log) []error + DispatchFunc func(ctx context.Context, logger rest.Log, event *rest.Event) []error // GetSubscribersFunc mocks the GetSubscribers method. - GetSubscribersFunc func() []rest.EventHandler + GetSubscribersFunc func(resourceType string) map[string][]rest.EventHandler // calls tracks calls to the methods. calls struct { @@ -1286,13 +1323,15 @@ type EventDispatcherMock struct { Dispatch []struct { // Ctx is the ctx argument value. Ctx context.Context - // Event is the event argument value. - Event rest.Event // Logger is the logger argument value. Logger rest.Log + // Event is the event argument value. + Event *rest.Event } // GetSubscribers holds details about calls to the GetSubscribers method. GetSubscribers []struct { + // ResourceType is the resourceType argument value. + ResourceType string } } lockAddSubscriber sync.RWMutex @@ -1333,23 +1372,23 @@ func (mock *EventDispatcherMock) AddSubscriberCalls() []struct { } // Dispatch calls DispatchFunc. -func (mock *EventDispatcherMock) Dispatch(ctx context.Context, event rest.Event, logger rest.Log) []error { +func (mock *EventDispatcherMock) Dispatch(ctx context.Context, logger rest.Log, event *rest.Event) []error { if mock.DispatchFunc == nil { panic("EventDispatcherMock.DispatchFunc: method is nil but EventDispatcher.Dispatch was just called") } callInfo := struct { Ctx context.Context - Event rest.Event Logger rest.Log + Event *rest.Event }{ Ctx: ctx, - Event: event, Logger: logger, + Event: event, } mock.lockDispatch.Lock() mock.calls.Dispatch = append(mock.calls.Dispatch, callInfo) mock.lockDispatch.Unlock() - return mock.DispatchFunc(ctx, event, logger) + return mock.DispatchFunc(ctx, logger, event) } // DispatchCalls gets all the calls that were made to Dispatch. @@ -1358,13 +1397,13 @@ func (mock *EventDispatcherMock) Dispatch(ctx context.Context, event rest.Event, // len(mockedEventDispatcher.DispatchCalls()) func (mock *EventDispatcherMock) DispatchCalls() []struct { Ctx context.Context - Event rest.Event Logger rest.Log + Event *rest.Event } { var calls []struct { Ctx context.Context - Event rest.Event Logger rest.Log + Event *rest.Event } mock.lockDispatch.RLock() calls = mock.calls.Dispatch @@ -1373,16 +1412,19 @@ func (mock *EventDispatcherMock) DispatchCalls() []struct { } // GetSubscribers calls GetSubscribersFunc. -func (mock *EventDispatcherMock) GetSubscribers() []rest.EventHandler { +func (mock *EventDispatcherMock) GetSubscribers(resourceType string) map[string][]rest.EventHandler { if mock.GetSubscribersFunc == nil { panic("EventDispatcherMock.GetSubscribersFunc: method is nil but EventDispatcher.GetSubscribers was just called") } callInfo := struct { - }{} + ResourceType string + }{ + ResourceType: resourceType, + } mock.lockGetSubscribers.Lock() mock.calls.GetSubscribers = append(mock.calls.GetSubscribers, callInfo) mock.lockGetSubscribers.Unlock() - return mock.GetSubscribersFunc() + return mock.GetSubscribersFunc(resourceType) } // GetSubscribersCalls gets all the calls that were made to GetSubscribers. @@ -1390,8 +1432,10 @@ func (mock *EventDispatcherMock) GetSubscribers() []rest.EventHandler { // // len(mockedEventDispatcher.GetSubscribersCalls()) func (mock *EventDispatcherMock) GetSubscribersCalls() []struct { + ResourceType string } { var calls []struct { + ResourceType string } mock.lockGetSubscribers.RLock() calls = mock.calls.GetSubscribers @@ -1412,7 +1456,7 @@ var _ rest.EventStore = &EventStoreMock{} // AddSubscriberFunc: func(handler rest.EventHandlerConfig) error { // panic("mock out the AddSubscriber method") // }, -// DispatchFunc: func(ctx context.Context, event rest.Event, logger rest.Log) []error { +// DispatchFunc: func(ctx context.Context, logger rest.Log, event *rest.Event) []error { // panic("mock out the Dispatch method") // }, // GetByKeyFunc: func(ctxt context.Context, identifiers map[string]interface{}) (rest.Resource, error) { @@ -1424,10 +1468,13 @@ var _ rest.EventStore = &EventStoreMock{} // GetByURIFunc: func(ctxt context.Context, logger rest.Log, uri string) (rest.Resource, error) { // panic("mock out the GetByURI method") // }, +// GetEventHandlersFunc: func() []rest.EventHandlerConfig { +// panic("mock out the GetEventHandlers method") +// }, // GetListFunc: func(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]rest.Resource, int64, error) { // panic("mock out the GetList method") // }, -// GetSubscribersFunc: func() []rest.EventHandler { +// GetSubscribersFunc: func(resourceType string) map[string][]rest.EventHandler { // panic("mock out the GetSubscribers method") // }, // PersistFunc: func(ctxt context.Context, logger rest.Log, resources []rest.Resource) []error { @@ -1447,7 +1494,7 @@ type EventStoreMock struct { AddSubscriberFunc func(handler rest.EventHandlerConfig) error // DispatchFunc mocks the Dispatch method. - DispatchFunc func(ctx context.Context, event rest.Event, logger rest.Log) []error + DispatchFunc func(ctx context.Context, logger rest.Log, event *rest.Event) []error // GetByKeyFunc mocks the GetByKey method. GetByKeyFunc func(ctxt context.Context, identifiers map[string]interface{}) (rest.Resource, error) @@ -1458,11 +1505,14 @@ type EventStoreMock struct { // GetByURIFunc mocks the GetByURI method. GetByURIFunc func(ctxt context.Context, logger rest.Log, uri string) (rest.Resource, error) + // GetEventHandlersFunc mocks the GetEventHandlers method. + GetEventHandlersFunc func() []rest.EventHandlerConfig + // GetListFunc mocks the GetList method. GetListFunc func(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]rest.Resource, int64, error) // GetSubscribersFunc mocks the GetSubscribers method. - GetSubscribersFunc func() []rest.EventHandler + GetSubscribersFunc func(resourceType string) map[string][]rest.EventHandler // PersistFunc mocks the Persist method. PersistFunc func(ctxt context.Context, logger rest.Log, resources []rest.Resource) []error @@ -1481,10 +1531,10 @@ type EventStoreMock struct { Dispatch []struct { // Ctx is the ctx argument value. Ctx context.Context - // Event is the event argument value. - Event rest.Event // Logger is the logger argument value. Logger rest.Log + // Event is the event argument value. + Event *rest.Event } // GetByKey holds details about calls to the GetByKey method. GetByKey []struct { @@ -1509,6 +1559,9 @@ type EventStoreMock struct { // URI is the uri argument value. URI string } + // GetEventHandlers holds details about calls to the GetEventHandlers method. + GetEventHandlers []struct { + } // GetList holds details about calls to the GetList method. GetList []struct { // Ctx is the ctx argument value. @@ -1526,6 +1579,8 @@ type EventStoreMock struct { } // GetSubscribers holds details about calls to the GetSubscribers method. GetSubscribers []struct { + // ResourceType is the resourceType argument value. + ResourceType string } // Persist holds details about calls to the Persist method. Persist []struct { @@ -1546,15 +1601,16 @@ type EventStoreMock struct { Resources []rest.Resource } } - lockAddSubscriber sync.RWMutex - lockDispatch sync.RWMutex - lockGetByKey sync.RWMutex - lockGetByProperties sync.RWMutex - lockGetByURI sync.RWMutex - lockGetList sync.RWMutex - lockGetSubscribers sync.RWMutex - lockPersist sync.RWMutex - lockRemove sync.RWMutex + lockAddSubscriber sync.RWMutex + lockDispatch sync.RWMutex + lockGetByKey sync.RWMutex + lockGetByProperties sync.RWMutex + lockGetByURI sync.RWMutex + lockGetEventHandlers sync.RWMutex + lockGetList sync.RWMutex + lockGetSubscribers sync.RWMutex + lockPersist sync.RWMutex + lockRemove sync.RWMutex } // AddSubscriber calls AddSubscriberFunc. @@ -1590,23 +1646,23 @@ func (mock *EventStoreMock) AddSubscriberCalls() []struct { } // Dispatch calls DispatchFunc. -func (mock *EventStoreMock) Dispatch(ctx context.Context, event rest.Event, logger rest.Log) []error { +func (mock *EventStoreMock) Dispatch(ctx context.Context, logger rest.Log, event *rest.Event) []error { if mock.DispatchFunc == nil { panic("EventStoreMock.DispatchFunc: method is nil but EventStore.Dispatch was just called") } callInfo := struct { Ctx context.Context - Event rest.Event Logger rest.Log + Event *rest.Event }{ Ctx: ctx, - Event: event, Logger: logger, + Event: event, } mock.lockDispatch.Lock() mock.calls.Dispatch = append(mock.calls.Dispatch, callInfo) mock.lockDispatch.Unlock() - return mock.DispatchFunc(ctx, event, logger) + return mock.DispatchFunc(ctx, logger, event) } // DispatchCalls gets all the calls that were made to Dispatch. @@ -1615,13 +1671,13 @@ func (mock *EventStoreMock) Dispatch(ctx context.Context, event rest.Event, logg // len(mockedEventStore.DispatchCalls()) func (mock *EventStoreMock) DispatchCalls() []struct { Ctx context.Context - Event rest.Event Logger rest.Log + Event *rest.Event } { var calls []struct { Ctx context.Context - Event rest.Event Logger rest.Log + Event *rest.Event } mock.lockDispatch.RLock() calls = mock.calls.Dispatch @@ -1741,6 +1797,33 @@ func (mock *EventStoreMock) GetByURICalls() []struct { return calls } +// GetEventHandlers calls GetEventHandlersFunc. +func (mock *EventStoreMock) GetEventHandlers() []rest.EventHandlerConfig { + if mock.GetEventHandlersFunc == nil { + panic("EventStoreMock.GetEventHandlersFunc: method is nil but EventStore.GetEventHandlers was just called") + } + callInfo := struct { + }{} + mock.lockGetEventHandlers.Lock() + mock.calls.GetEventHandlers = append(mock.calls.GetEventHandlers, callInfo) + mock.lockGetEventHandlers.Unlock() + return mock.GetEventHandlersFunc() +} + +// GetEventHandlersCalls gets all the calls that were made to GetEventHandlers. +// Check the length with: +// +// len(mockedEventStore.GetEventHandlersCalls()) +func (mock *EventStoreMock) GetEventHandlersCalls() []struct { +} { + var calls []struct { + } + mock.lockGetEventHandlers.RLock() + calls = mock.calls.GetEventHandlers + mock.lockGetEventHandlers.RUnlock() + return calls +} + // GetList calls GetListFunc. func (mock *EventStoreMock) GetList(ctx context.Context, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]rest.Resource, int64, error) { if mock.GetListFunc == nil { @@ -1794,16 +1877,19 @@ func (mock *EventStoreMock) GetListCalls() []struct { } // GetSubscribers calls GetSubscribersFunc. -func (mock *EventStoreMock) GetSubscribers() []rest.EventHandler { +func (mock *EventStoreMock) GetSubscribers(resourceType string) map[string][]rest.EventHandler { if mock.GetSubscribersFunc == nil { panic("EventStoreMock.GetSubscribersFunc: method is nil but EventStore.GetSubscribers was just called") } callInfo := struct { - }{} + ResourceType string + }{ + ResourceType: resourceType, + } mock.lockGetSubscribers.Lock() mock.calls.GetSubscribers = append(mock.calls.GetSubscribers, callInfo) mock.lockGetSubscribers.Unlock() - return mock.GetSubscribersFunc() + return mock.GetSubscribersFunc(resourceType) } // GetSubscribersCalls gets all the calls that were made to GetSubscribers. @@ -1811,8 +1897,10 @@ func (mock *EventStoreMock) GetSubscribers() []rest.EventHandler { // // len(mockedEventStore.GetSubscribersCalls()) func (mock *EventStoreMock) GetSubscribersCalls() []struct { + ResourceType string } { var calls []struct { + ResourceType string } mock.lockGetSubscribers.RLock() calls = mock.calls.GetSubscribers