From 349ddb84c2ef59d1c36dc73b9f5cd805d83d4c40 Mon Sep 17 00:00:00 2001 From: aphilbert Date: Sat, 29 Jan 2022 12:16:04 -0400 Subject: [PATCH] feature: WEOS-1135 Improved how we use schemas to create the response * Switched to getting the events in the controller by using the event repository * Added method to ContentEntity that returns the entity as an interface map whose keys are what was set in the OpenAPI spec * Added constructor for ContentEnty to create with schema and events * Added method to Content entity to convert the name of property in dynamic struct back that what was set in the OpenAPI spec --- controllers/rest/controller_standard.go | 12 +- controllers/rest/controller_standard_test.go | 17 ++- controllers/rest/utils.go | 2 +- model/content_entity.go | 46 +++++-- model/content_entity_test.go | 128 +++++++++++++++++++ 5 files changed, 185 insertions(+), 20 deletions(-) diff --git a/controllers/rest/controller_standard.go b/controllers/rest/controller_standard.go index 1cf73a3f..ce8c2bce 100644 --- a/controllers/rest/controller_standard.go +++ b/controllers/rest/controller_standard.go @@ -413,18 +413,16 @@ func (c *StandardControllers) View(app model.Service, spec *openapi3.Swagger, pa if entityID != "" { //get the entity using the sequence no. if seqInt != 0 { - r, er := model.GetContentBySequenceNumber(app.EventRepository(), entityID, int64(seqInt)) + //get the events up to the sequence + events, err := app.EventRepository().GetByAggregateAndSequenceRange(entityID, 0, int64(seqInt)) + //create content entity + r, er := new(model.ContentEntity).FromSchemaWithEvents(ctxt.Request().Context(), contentTypeSchema.Value, events) err = er if r.SequenceNo == 0 { return NewControllerError("No entity found", err, http.StatusNotFound) } if r != nil && r.ID != "" { //get the map from the entity - if r.Property != nil { - result = r.Property.(map[string]interface{}) - } else { - result = make(map[string]interface{}) - } - + result = r.ToMap() } result["weos_id"] = r.ID result["sequence_no"] = r.SequenceNo diff --git a/controllers/rest/controller_standard_test.go b/controllers/rest/controller_standard_test.go index 9802a9ff..b5d942f6 100644 --- a/controllers/rest/controller_standard_test.go +++ b/controllers/rest/controller_standard_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "io" "io/ioutil" "mime/multipart" "net/http" @@ -838,8 +839,16 @@ func TestStandardControllers_View(t *testing.T) { e.GET("/blogs/:"+paramName, controller, mw) e.ServeHTTP(resp, req) - response := resp.Result() - defer response.Body.Close() + response, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("invalid response '%s'", err) + } + defer resp.Body.Reset() + + //check that properties of the scehma are in the response even if it was not set in the event + if !strings.Contains(string(response), "title") { + t.Errorf("expected the response to have '%s' based on the schema, got '%s'", "title", string(response)) + } //confirm the entity is retrieved to get entity id if len(projection.GetByKeyCalls()) != 1 { @@ -850,8 +859,8 @@ func TestStandardControllers_View(t *testing.T) { t.Errorf("expected the event repository to be called %d time, called %d times", 1, len(eventRepository.GetByAggregateAndSequenceRangeCalls())) } - if response.StatusCode != 200 { - t.Errorf("expected response code to be %d, got %d", 200, response.StatusCode) + if resp.Code != 200 { + t.Errorf("expected response code to be %d, got %d", 200, resp.Code) } }) t.Run("view with invalid sequence no", func(t *testing.T) { diff --git a/controllers/rest/utils.go b/controllers/rest/utils.go index c6ab4414..70cac9e5 100644 --- a/controllers/rest/utils.go +++ b/controllers/rest/utils.go @@ -207,7 +207,7 @@ func GetContentBySequenceNumber(eventRepository model.EventRepository, id string if err != nil { return nil, err } - err = entity.ApplyChanges(events) + err = entity.ApplyEvents(events) return entity, err } diff --git a/model/content_entity.go b/model/content_entity.go index 901abfbe..6fbd0be9 100644 --- a/model/content_entity.go +++ b/model/content_entity.go @@ -16,6 +16,7 @@ type ContentEntity struct { AggregateRoot Schema *openapi3.Schema Property interface{} + reader ds.Reader } //IsValid checks if the property is valid using the IsNull function @@ -128,6 +129,7 @@ func (w *ContentEntity) FromSchema(ctx context.Context, ref *openapi3.Schema) (* } } w.Property = instance.Build().New() + w.reader = ds.NewReader(w.Property) return w, nil } @@ -150,7 +152,7 @@ func (w *ContentEntity) FromSchemaWithValues(ctx context.Context, schema *openap } event := NewEntityEvent("create", w, w.ID, payload) w.NewChange(event) - return w, w.ApplyChanges([]*Event{event}) + return w, w.ApplyEvents([]*Event{event}) } func (w *ContentEntity) Update(ctx context.Context, existingPayload json.RawMessage, updatedPayload json.RawMessage) (*ContentEntity, error) { @@ -171,7 +173,7 @@ func (w *ContentEntity) Update(ctx context.Context, existingPayload json.RawMess event := NewEntityEvent("update", w, w.ID, updatedPayload) w.NewChange(event) - return w, w.ApplyChanges([]*Event{event}) + return w, w.ApplyEvents([]*Event{event}) } //GetString returns the string property value stored of a given the property name @@ -270,18 +272,21 @@ func (w *ContentEntity) GetTime(name string) time.Time { return *reader.GetField(name).PointerTime() } -func GetContentBySequenceNumber(eventRepository EventRepository, id string, sequence_no int64) (*ContentEntity, error) { - entity := &ContentEntity{} - events, err := eventRepository.GetByAggregateAndSequenceRange(id, 0, sequence_no) +//FromSchemaWithEvents create content entity using schema and events +func (w *ContentEntity) FromSchemaWithEvents(ctx context.Context, ref *openapi3.Schema, changes []*Event) (*ContentEntity, error) { + entity, err := w.FromSchema(ctx, ref) + if err != nil { + return nil, err + } + err = entity.ApplyEvents(changes) if err != nil { return nil, err } - err = entity.ApplyChanges(events) return entity, err } -//ApplyChanges apply the new changes from payload to the entity -func (w *ContentEntity) ApplyChanges(changes []*Event) error { +//ApplyEvents apply the new changes from payload to the entity +func (w *ContentEntity) ApplyEvents(changes []*Event) error { for _, change := range changes { w.SequenceNo = change.Meta.SequenceNo w.ID = change.Meta.EntityID @@ -311,3 +316,28 @@ func (w *ContentEntity) ApplyChanges(changes []*Event) error { } return nil } + +//ToMap return entity has a map +func (w *ContentEntity) ToMap() map[string]interface{} { + result := make(map[string]interface{}) + //get all fields and return the map + fields := w.reader.GetAllFields() + for _, field := range fields { + //check if the lowercase version of the field is the same as the schema and use the scehma version instead + result[w.GetOriginalFieldName(field.Name())] = field.Interface() + } + return result +} + +//GetOriginalFieldName the original name of the field as defined in the schema (the field is Title cased when converted to struct) +func (w *ContentEntity) GetOriginalFieldName(structName string) string { + if w.Schema != nil { + for key, _ := range w.Schema.Properties { + if strings.ToLower(key) == strings.ToLower(structName) { + return key + } + } + } + + return "" +} diff --git a/model/content_entity_test.go b/model/content_entity_test.go index 7e9fd04d..f9b1e854 100644 --- a/model/content_entity_test.go +++ b/model/content_entity_test.go @@ -158,3 +158,131 @@ func TestContentEntity_Update(t *testing.T) { t.Errorf("expected the updated description to be '%s', got '%s'", "Updated Description", existingEntity.GetString("Description")) } } + +func TestContentEntity_FromSchemaWithEvents(t *testing.T) { + //load open api spec + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog.yaml") + if err != nil { + t.Fatalf("unexpected error occured '%s'", err) + } + var contentType string + var contentTypeSchema *openapi3.SchemaRef + contentType = "Blog" + contentTypeSchema = swagger.Components.Schemas[contentType] + ctx := context.Background() + ctx = context.WithValue(ctx, weosContext.CONTENT_TYPE, &weosContext.ContentType{ + Name: contentType, + Schema: contentTypeSchema.Value, + }) + ctx = context.WithValue(ctx, weosContext.USER_ID, "123") + + mockEvent1 := &model.Event{ + ID: "1234sd", + Type: "create", + Payload: nil, + Meta: model.EventMeta{ + EntityID: "1234sd", + EntityType: "Blog", + SequenceNo: 1, + User: "", + Module: "", + RootID: "", + Group: "", + Created: "", + }, + Version: 0, + } + mockEvent2 := &model.Event{ + ID: "1234sd", + Type: "update", + Payload: nil, + Meta: model.EventMeta{ + EntityID: "1234sd", + EntityType: "Blog", + SequenceNo: 2, + User: "", + Module: "", + RootID: "", + Group: "", + Created: "", + }, + Version: 0, + } + events := []*model.Event{mockEvent1, mockEvent2} + + entity, err := new(model.ContentEntity).FromSchemaWithEvents(ctx, swagger.Components.Schemas["Blog"].Value, events) + if err != nil { + t.Fatalf("unexpected error instantiating content entity '%s'", err) + } + + if entity.Property == nil { + t.Fatal("expected item to be returned") + } + + if entity.GetString("Title") != "" { + t.Errorf("expected there to be a field '%s' with value '%s' got '%s'", "Title", " ", entity.GetString("Title")) + } +} + +func TestContentEntity_ToMap(t *testing.T) { + //load open api spec + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog.yaml") + if err != nil { + t.Fatalf("unexpected error occured '%s'", err) + } + var contentType string + var contentTypeSchema *openapi3.SchemaRef + contentType = "Blog" + contentTypeSchema = swagger.Components.Schemas[contentType] + ctx := context.Background() + ctx = context.WithValue(ctx, weosContext.CONTENT_TYPE, &weosContext.ContentType{ + Name: contentType, + Schema: contentTypeSchema.Value, + }) + ctx = context.WithValue(ctx, weosContext.USER_ID, "123") + + entity, err := new(model.ContentEntity).FromSchema(ctx, swagger.Components.Schemas["Blog"].Value) + if err != nil { + t.Fatalf("unexpected error instantiating content entity '%s'", err) + } + + if entity.Property == nil { + t.Fatal("expected item to be returned") + } + + result := entity.ToMap() + if err != nil { + t.Fatalf("unexpected error getting map '%s'", err) + } + + if _, ok := result["title"]; !ok { + t.Errorf("expected '%s' to be in map", "title") + } +} + +func TestContentEntity_GetOriginalFieldName(t *testing.T) { + //load open api spec + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog.yaml") + if err != nil { + t.Fatalf("unexpected error occured '%s'", err) + } + var contentType string + var contentTypeSchema *openapi3.SchemaRef + contentType = "Blog" + contentTypeSchema = swagger.Components.Schemas[contentType] + ctx := context.Background() + ctx = context.WithValue(ctx, weosContext.CONTENT_TYPE, &weosContext.ContentType{ + Name: contentType, + Schema: contentTypeSchema.Value, + }) + ctx = context.WithValue(ctx, weosContext.USER_ID, "123") + + entity, err := new(model.ContentEntity).FromSchema(ctx, swagger.Components.Schemas["Blog"].Value) + if err != nil { + t.Fatalf("unexpected error instantiating content entity '%s'", err) + } + originalName := entity.GetOriginalFieldName("Title") + if originalName != "title" { + t.Errorf("expected the original field name for '%s' to be '%s', got '%s'", "Title", "title", originalName) + } +}