diff --git a/application/article/getArticle/response.go b/application/article/getArticle/response.go index 7cd128e6..6d992da2 100644 --- a/application/article/getArticle/response.go +++ b/application/article/getArticle/response.go @@ -3,23 +3,22 @@ package getarticle import ( "time" + "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain/article" - "github.com/khanzadimahdi/testproject/domain/element" - "github.com/khanzadimahdi/testproject/domain/element/component" ) type Response struct { - UUID string `json:"uuid"` - Cover string `json:"cover"` - Video string `json:"video"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - Body string `json:"body"` - PublishedAt string `json:"published_at"` - Author authorResponse `json:"avatar"` - Tags []string `json:"tags"` - ViewCount uint `json:"view_count"` - Elements []elementResponse `json:"elements"` + UUID string `json:"uuid"` + Cover string `json:"cover"` + Video string `json:"video"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + Body string `json:"body"` + PublishedAt string `json:"published_at"` + Author authorResponse `json:"avatar"` + Tags []string `json:"tags"` + ViewCount uint `json:"view_count"` + Elements []element.Response `json:"elements"` } type authorResponse struct { @@ -27,47 +26,10 @@ type authorResponse struct { Avatar string `json:"avatar"` } -type articleResponse struct { - UUID string `json:"uuid"` - Cover string `json:"cover"` - Title string `json:"title"` - Author authorResponse `json:"author"` - PublishedAt string `json:"published_at"` - Excerpt string `json:"excerpt"` - Tags []string `json:"tags"` -} - -type elementResponse struct { - Type string `json:"type"` - Body any `json:"body"` -} - -type itemComponentResponse struct { - Type string `json:"type"` - Body any `json:"body"` -} - -type featuredComponentResponse struct { - Main itemComponentResponse `json:"main"` - Aside []itemComponentResponse `json:"aside"` -} - -type jumbotronComponentResponse struct { - itemComponentResponse -} - -func NewResponse(a article.Article, e []element.Element, elementsContent []article.Article) *Response { +func NewResponse(a article.Article, elementsResponse []element.Response) *Response { tags := make([]string, len(a.Tags)) copy(tags, a.Tags) - elements := make([]elementResponse, len(e)) - for i := range e { - elements[i] = elementResponse{ - Type: e[i].Type, - Body: toComponentResponse(e[i], elementsContent), - } - } - return &Response{ UUID: a.UUID, Cover: a.Cover, @@ -82,76 +44,6 @@ func NewResponse(a article.Article, e []element.Element, elementsContent []artic }, Tags: tags, ViewCount: a.ViewCount, - Elements: elements, - } -} - -func toComponentResponse(e element.Element, elementsContent []article.Article) any { - var c any - - if e.Type == "jumbotron" { - c = toJumbotronResponse(e.Body.(component.Jumbotron), elementsContent) + Elements: elementsResponse, } - - if e.Type == "featured" { - c = toFeaturedResponse(e.Body.(component.Featured), elementsContent) - } - - if e.Type == "item" { - c = toItemResponse(e.Body.(component.Item), elementsContent) - } - - return c -} - -func toJumbotronResponse(c component.Jumbotron, elementsContent []article.Article) jumbotronComponentResponse { - return jumbotronComponentResponse{ - itemComponentResponse: toItemResponse(c.Item, elementsContent), - } -} - -func toFeaturedResponse(c component.Featured, elementsContent []article.Article) featuredComponentResponse { - aside := make([]itemComponentResponse, len(c.Aside)) - - for i := range c.Aside { - aside[i] = toItemResponse(c.Aside[i], elementsContent) - } - - return featuredComponentResponse{ - Main: toItemResponse(c.Main, elementsContent), - Aside: aside, - } -} - -func toItemResponse(c component.Item, elementsContent []article.Article) itemComponentResponse { - var body any - for i := range elementsContent { - if elementsContent[i].UUID == c.UUID { - body = toArticleResponse([]article.Article{elementsContent[i]})[0] - break - } - } - - return itemComponentResponse{ - Type: c.Type, - Body: body, - } -} - -func toArticleResponse(a []article.Article) []articleResponse { - items := make([]articleResponse, len(a)) - - for i := range a { - items[i].UUID = a[i].UUID - items[i].Cover = a[i].Cover - items[i].Title = a[i].Title - items[i].Excerpt = a[i].Excerpt - items[i].Tags = a[i].Tags - items[i].PublishedAt = a[i].PublishedAt.Format(time.RFC3339) - - items[i].Author.Name = a[i].Author.Name - items[i].Author.Avatar = a[i].Author.Avatar - } - - return items } diff --git a/application/article/getArticle/usecase.go b/application/article/getArticle/usecase.go index a3826bdc..6fa0f183 100644 --- a/application/article/getArticle/usecase.go +++ b/application/article/getArticle/usecase.go @@ -3,23 +3,22 @@ package getarticle import ( "fmt" + "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain/article" - "github.com/khanzadimahdi/testproject/domain/element" - "github.com/khanzadimahdi/testproject/domain/element/component" ) type UseCase struct { articleRepository article.Repository - elementRepository element.Repository + elementRetriever *element.Retriever } func NewUseCase( articleRepository article.Repository, - elementRepository element.Repository, + elementRetriever *element.Retriever, ) *UseCase { return &UseCase{ articleRepository: articleRepository, - elementRepository: elementRepository, + elementRetriever: elementRetriever, } } @@ -29,35 +28,12 @@ func (uc *UseCase) Execute(UUID string) (*Response, error) { return nil, err } - elements, articles, err := uc.elements(a.UUID) + elementsResponse, err := uc.elementRetriever.RetrieveByVenues([]string{fmt.Sprintf("articles/%s", UUID)}) if err != nil { return nil, err } defer uc.articleRepository.IncreaseView(a.UUID, 1) - return NewResponse(a, elements, articles), nil -} - -func (uc *UseCase) elements(UUID string) ([]element.Element, []article.Article, error) { - elements, err := uc.elementRepository.GetByVenues([]string{fmt.Sprintf("articles/%s", UUID)}) - if err != nil { - return nil, nil, err - } - - items := make([]component.Item, 0, len(elements)) - for i := range elements { - items = append(items, elements[i].Body.Items()...) - } - - uuids := make([]string, len(items)) - for i := range items { - uuids[i] = items[i].UUID - } - articles, err := uc.articleRepository.GetByUUIDs(uuids) - if err != nil { - return nil, nil, err - } - - return elements, articles, nil + return NewResponse(a, elementsResponse), nil } diff --git a/application/article/getArticle/usecase_test.go b/application/article/getArticle/usecase_test.go index b742c39f..3fe2434d 100644 --- a/application/article/getArticle/usecase_test.go +++ b/application/article/getArticle/usecase_test.go @@ -6,9 +6,10 @@ import ( "github.com/stretchr/testify/assert" + "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" - "github.com/khanzadimahdi/testproject/domain/element" + domainElement "github.com/khanzadimahdi/testproject/domain/element" "github.com/khanzadimahdi/testproject/domain/element/component" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/elements" @@ -31,14 +32,14 @@ func TestUseCase_Execute(t *testing.T) { {UUID: "test-article-2"}, } i = []component.Item{ - {UUID: va[0].UUID}, - {UUID: va[1].UUID}, - {UUID: "not-exist-article-uuid"}, + {ContentUUID: va[0].UUID}, + {ContentUUID: va[1].UUID}, + {ContentUUID: "not-exist-article-uuid"}, } u = []string{ - i[0].UUID, - i[1].UUID, - i[2].UUID, + i[0].ContentUUID, + i[1].ContentUUID, + i[2].ContentUUID, } ) @@ -48,16 +49,18 @@ func TestUseCase_Execute(t *testing.T) { defer articlesRepository.AssertExpectations(t) mockComponent.On("Items").Once().Return(i) + mockComponent.On("Type").Return(component.ComponentTypeMock) defer mockComponent.AssertExpectations(t) - v := []element.Element{ + v := []domainElement.Element{ {Body: &mockComponent}, } elementsRepository.On("GetByVenues", venues).Once().Return(v, nil) defer elementsRepository.AssertExpectations(t) - response, err := NewUseCase(&articlesRepository, &elementsRepository).Execute("test-uuid") + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + response, err := NewUseCase(&articlesRepository, elementRetriever).Execute("test-uuid") assert.NotNil(t, response, "unexpected response") assert.NoError(t, err, "unexpected error") @@ -77,7 +80,8 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository.On("GetOnePublished", articleUUID).Once().Return(a, expectedErr) defer articlesRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + usecase := NewUseCase(&articlesRepository, elementRetriever) response, err := usecase.Execute("test-uuid") elementsRepository.AssertNotCalled(t, "GetByVenues") @@ -105,7 +109,8 @@ func TestUseCase_Execute(t *testing.T) { elementsRepository.On("GetByVenues", venues).Once().Return(nil, expectedErr) defer elementsRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + usecase := NewUseCase(&articlesRepository, elementRetriever) response, err := usecase.Execute("test-uuid") articlesRepository.AssertNotCalled(t, "GetByUUIDs") @@ -131,14 +136,14 @@ func TestUseCase_Execute(t *testing.T) { {UUID: "test-article-2"}, } i = []component.Item{ - {UUID: va[0].UUID}, - {UUID: va[1].UUID}, - {UUID: "not-exist-article-uuid"}, + {ContentUUID: va[0].UUID}, + {ContentUUID: va[1].UUID}, + {ContentUUID: "not-exist-article-uuid"}, } u = []string{ - i[0].UUID, - i[1].UUID, - i[2].UUID, + i[0].ContentUUID, + i[1].ContentUUID, + i[2].ContentUUID, } ) @@ -149,14 +154,15 @@ func TestUseCase_Execute(t *testing.T) { mockComponent.On("Items").Once().Return(i) defer mockComponent.AssertExpectations(t) - v := []element.Element{ + v := []domainElement.Element{ {Body: &mockComponent}, } elementsRepository.On("GetByVenues", venues).Once().Return(v, nil) defer elementsRepository.AssertExpectations(t) - response, err := NewUseCase(&articlesRepository, &elementsRepository).Execute("test-uuid") + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + response, err := NewUseCase(&articlesRepository, elementRetriever).Execute("test-uuid") articlesRepository.AssertNotCalled(t, "IncreaseView") @@ -181,14 +187,14 @@ func TestUseCase_Execute(t *testing.T) { {UUID: "test-article-2"}, } i = []component.Item{ - {UUID: va[0].UUID}, - {UUID: va[1].UUID}, - {UUID: "not-exist-article-uuid"}, + {ContentUUID: va[0].UUID}, + {ContentUUID: va[1].UUID}, + {ContentUUID: "not-exist-article-uuid"}, } u = []string{ - i[0].UUID, - i[1].UUID, - i[2].UUID, + i[0].ContentUUID, + i[1].ContentUUID, + i[2].ContentUUID, } ) @@ -198,16 +204,18 @@ func TestUseCase_Execute(t *testing.T) { defer articlesRepository.AssertExpectations(t) mockComponent.On("Items").Once().Return(i) + mockComponent.On("Type").Return(component.ComponentTypeMock) defer mockComponent.AssertExpectations(t) - v := []element.Element{ + v := []domainElement.Element{ {Body: &mockComponent}, } elementsRepository.On("GetByVenues", venues).Once().Return(v, nil) defer elementsRepository.AssertExpectations(t) - response, err := NewUseCase(&articlesRepository, &elementsRepository).Execute("test-uuid") + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + response, err := NewUseCase(&articlesRepository, elementRetriever).Execute("test-uuid") assert.NotNil(t, response, "unexpected response") assert.NoError(t, err, "unexpected error") diff --git a/application/auth/verify/request.go b/application/auth/verify/request.go index 1b9759a7..2bcdc101 100644 --- a/application/auth/verify/request.go +++ b/application/auth/verify/request.go @@ -2,8 +2,6 @@ package verify import "github.com/khanzadimahdi/testproject/domain" -type validationErrors map[string]string - type Request struct { Token string `json:"token"` Name string `json:"name"` diff --git a/application/dashboard/config/updateConfig/request.go b/application/dashboard/config/updateConfig/request.go index 4e0904a2..62f5b29b 100644 --- a/application/dashboard/config/updateConfig/request.go +++ b/application/dashboard/config/updateConfig/request.go @@ -2,8 +2,6 @@ package updateConfig import "github.com/khanzadimahdi/testproject/domain" -type validationErrors map[string]string - type Request struct { UserDefaultRoles []string `json:"user_default_roles"` } diff --git a/application/dashboard/element/createElement/request.go b/application/dashboard/element/createElement/request.go index 2d007bf5..a2610788 100644 --- a/application/dashboard/element/createElement/request.go +++ b/application/dashboard/element/createElement/request.go @@ -2,59 +2,211 @@ package createelement import ( "encoding/json" + "strconv" + "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/element" "github.com/khanzadimahdi/testproject/domain/element/component" ) -type validationErrors map[string]string - type Request struct { - Type string `json:"type"` - Body element.Component `json:"body"` - Venues []string `json:"venues"` + Body domain.Validatable `json:"-"` + Venues []string `json:"-"` } -func (e *Request) UnmarshalJSON(data []byte) error { - type Child Request +var _ domain.Validatable = &Request{} +var _ json.Unmarshaler = &Request{} + +func (r *Request) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if errs := r.Body.Validate(); len(errs) > 0 { + for errKey, errValue := range errs { + validationErrors["body."+errKey] = errValue + } + } + + return validationErrors +} + +type itemComponentRequest struct { + Type string `json:"type"` + ContentUUID string `json:"content_uuid"` + ContentType string `json:"content_type"` +} + +var _ domain.Validatable = &itemComponentRequest{} + +func (r *itemComponentRequest) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.Type) == 0 { + validationErrors["type"] = "required_field" + } + + if r.Type != component.ComponentTypeItem { + validationErrors["type"] = "invalid_value" + } + + if len(r.ContentUUID) == 0 { + validationErrors["content_uuid"] = "required_field" + } + + if len(r.ContentType) == 0 { + validationErrors["content_type"] = "required_field" + } + + return validationErrors +} + +type featuredComponentRequest struct { + Type string `json:"type"` + Main itemComponentRequest `json:"main"` + Aside []itemComponentRequest `json:"aside"` +} + +var _ domain.Validatable = &featuredComponentRequest{} + +func (r *featuredComponentRequest) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.Type) == 0 { + validationErrors["type"] = "required_field" + } + + if r.Type != component.ComponentTypeFeatured { + validationErrors["type"] = "invalid_value" + } + + if errs := r.Main.Validate(); len(errs) > 0 { + for errKey, errValue := range errs { + validationErrors["main."+errKey] = errValue + } + } + + for i, aside := range r.Aside { + if errs := aside.Validate(); len(errs) > 0 { + for errKey, errValue := range errs { + validationErrors["aside."+strconv.Itoa(i)+"."+errKey] = errValue + } + } + } + + return validationErrors +} + +type jumbotronComponentRequest struct { + Type string `json:"type"` + Item itemComponentRequest `json:"item"` +} + +var _ domain.Validatable = &jumbotronComponentRequest{} + +func (r *jumbotronComponentRequest) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.Type) == 0 { + validationErrors["type"] = "required_field" + } + + if r.Type != component.ComponentTypeJumbotron { + validationErrors["type"] = "invalid_value" + } + + if errs := r.Item.Validate(); len(errs) > 0 { + for errKey, errValue := range errs { + validationErrors["item."+errKey] = errValue + } + } + + return validationErrors +} +func (e *Request) UnmarshalJSON(data []byte) error { var tmp struct { - Child - Body json.RawMessage `json:"body"` + Venues []string `json:"venues"` + Component struct { + Type string `json:"type"` + } `json:"body"` } if err := json.Unmarshal(data, &tmp); err != nil { return err } - switch tmp.Type { - case "jumbotron": - j := component.Jumbotron{} - if err := json.Unmarshal(tmp.Body, &j); err != nil { + switch tmp.Component.Type { + case component.ComponentTypeItem: + var component struct { + Body itemComponentRequest `json:"body"` + } + + if err := json.Unmarshal(data, &component); err != nil { return err } - tmp.Child.Body = j - case "featured": - j := component.Featured{} - if err := json.Unmarshal(tmp.Body, &j); err != nil { + e.Body = &component.Body + case component.ComponentTypeJumbotron: + var component struct { + Body jumbotronComponentRequest `json:"body"` + } + + if err := json.Unmarshal(data, &component); err != nil { return err } - tmp.Child.Body = j - case "item": - j := component.Item{} - if err := json.Unmarshal(tmp.Body, &j); err != nil { + e.Body = &component.Body + case component.ComponentTypeFeatured: + var component struct { + Body featuredComponentRequest `json:"body"` + } + if err := json.Unmarshal(data, &component); err != nil { return err } - tmp.Child.Body = j + e.Body = &component.Body default: return element.ErrUnSupportedComponent } - *e = Request(tmp.Child) + e.Venues = tmp.Venues return nil } -func (r *Request) Validate() (bool, validationErrors) { - return true, nil +func (r *Request) ToElement() *element.Element { + e := &element.Element{ + Venues: r.Venues, + } + + switch r.Body.(type) { + case *itemComponentRequest: + e.Body = component.Item{ + ContentUUID: r.Body.(*itemComponentRequest).ContentUUID, + ContentType: r.Body.(*itemComponentRequest).ContentType, + } + case *jumbotronComponentRequest: + e.Body = component.Jumbotron{ + Item: component.Item{ + ContentUUID: r.Body.(*jumbotronComponentRequest).Item.ContentUUID, + ContentType: r.Body.(*jumbotronComponentRequest).Item.ContentType, + }, + } + case *featuredComponentRequest: + main := component.Item{ + ContentUUID: r.Body.(*featuredComponentRequest).Main.ContentUUID, + ContentType: r.Body.(*featuredComponentRequest).Main.ContentType, + } + + aside := make([]component.Item, len(r.Body.(*featuredComponentRequest).Aside)) + for i := range r.Body.(*featuredComponentRequest).Aside { + aside[i] = component.Item{ + ContentUUID: r.Body.(*featuredComponentRequest).Aside[i].ContentUUID, + ContentType: r.Body.(*featuredComponentRequest).Aside[i].ContentType, + } + } + + e.Body = component.Featured{ + Main: main, + Aside: aside, + } + } + + return e } diff --git a/application/dashboard/element/createElement/request_test.go b/application/dashboard/element/createElement/request_test.go index eff63e08..88162de1 100644 --- a/application/dashboard/element/createElement/request_test.go +++ b/application/dashboard/element/createElement/request_test.go @@ -8,21 +8,72 @@ import ( ) func TestRequest_Validate(t *testing.T) { - t.Run("always returns valid", func(t *testing.T) { + t.Run("valid request with item component", func(t *testing.T) { req := Request{ - Type: "jumbotron", - Body: component.Jumbotron{}, + Body: &itemComponentRequest{ + Type: component.ComponentTypeItem, + ContentUUID: "test-uuid", + ContentType: "article", + }, + Venues: []string{"venue1"}, + } + + errs := req.Validate() + + if len(errs) != 0 { + t.Errorf("Validate() returned %d errors, want 0: %v", len(errs), errs) + } + }) + + t.Run("valid request with jumbotron component", func(t *testing.T) { + req := Request{ + Body: &jumbotronComponentRequest{ + Type: component.ComponentTypeJumbotron, + Item: itemComponentRequest{ + Type: component.ComponentTypeItem, + ContentUUID: "test-uuid", + ContentType: "article", + }, + }, Venues: []string{"venue1"}, } - valid, errs := req.Validate() + errs := req.Validate() - if !valid { - t.Errorf("Validate() valid = false, want true") + if len(errs) != 0 { + t.Errorf("Validate() returned %d errors, want 0: %v", len(errs), errs) + } + }) + + t.Run("valid request with featured component", func(t *testing.T) { + req := Request{ + Body: &featuredComponentRequest{ + Type: component.ComponentTypeFeatured, + Main: itemComponentRequest{ + Type: component.ComponentTypeItem, + ContentUUID: "main-uuid", + ContentType: "article", + }, + Aside: []itemComponentRequest{ + { + Type: component.ComponentTypeItem, + ContentUUID: "aside-uuid-1", + ContentType: "article", + }, + { + Type: component.ComponentTypeItem, + ContentUUID: "aside-uuid-2", + ContentType: "article", + }, + }, + }, + Venues: []string{"venue1", "venue2"}, } + errs := req.Validate() + if len(errs) != 0 { - t.Errorf("Validate() returned %d errors, want 0", len(errs)) + t.Errorf("Validate() returned %d errors, want 0: %v", len(errs), errs) } }) } @@ -36,34 +87,40 @@ func TestRequest_UnmarshalJSON(t *testing.T) { }{ { name: "unmarshals jumbotron component", - json: `{"type":"jumbotron","body":{"title":"Test","subtitle":"Subtitle"},"venues":["venue1"]}`, + json: `{"body":{"type":"jumbotron","item":{"type":"item","content_uuid":"test-uuid","content_type":"article"}},"venues":["venue1"]}`, wantErr: false, check: func(r *Request) bool { - _, ok := r.Body.(component.Jumbotron) - return ok && r.Type == "jumbotron" + jumbotron, ok := r.Body.(*jumbotronComponentRequest) + return ok && jumbotron.Type == component.ComponentTypeJumbotron && len(r.Venues) == 1 }, }, { name: "unmarshals featured component", - json: `{"type":"featured","body":{},"venues":["venue1"]}`, + json: `{"body":{"type":"featured","main":{"type":"item","content_uuid":"main-uuid","content_type":"article"},"aside":[{"type":"item","content_uuid":"aside-uuid","content_type":"article"}]},"venues":["venue1"]}`, wantErr: false, check: func(r *Request) bool { - _, ok := r.Body.(component.Featured) - return ok && r.Type == "featured" + featured, ok := r.Body.(*featuredComponentRequest) + return ok && featured.Type == component.ComponentTypeFeatured && len(r.Venues) == 1 }, }, { name: "unmarshals item component", - json: `{"type":"item","body":{},"venues":["venue1"]}`, + json: `{"body":{"type":"item","content_uuid":"test-uuid","content_type":"article"},"venues":["venue1"]}`, wantErr: false, check: func(r *Request) bool { - _, ok := r.Body.(component.Item) - return ok && r.Type == "item" + item, ok := r.Body.(*itemComponentRequest) + return ok && item.Type == component.ComponentTypeItem && len(r.Venues) == 1 }, }, { name: "returns error for unsupported component type", - json: `{"type":"unsupported","body":{},"venues":["venue1"]}`, + json: `{"body":{"type":"unsupported"},"venues":["venue1"]}`, + wantErr: true, + check: func(r *Request) bool { return true }, + }, + { + name: "returns error for malformed json", + json: `{"body":}`, wantErr: true, check: func(r *Request) bool { return true }, }, diff --git a/application/dashboard/element/createElement/response.go b/application/dashboard/element/createElement/response.go index c2b0b8de..cbb23a3c 100644 --- a/application/dashboard/element/createElement/response.go +++ b/application/dashboard/element/createElement/response.go @@ -1,6 +1,8 @@ package createelement +import "github.com/khanzadimahdi/testproject/domain" + type Response struct { - ValidationErrors validationErrors `json:"errors,omitempty"` - UUID string `json:"uuid,omitempty"` + ValidationErrors domain.ValidationErrors `json:"errors,omitempty"` + UUID string `json:"uuid,omitempty"` } diff --git a/application/dashboard/element/createElement/usecase.go b/application/dashboard/element/createElement/usecase.go index 72e8f06e..79323edc 100644 --- a/application/dashboard/element/createElement/usecase.go +++ b/application/dashboard/element/createElement/usecase.go @@ -1,33 +1,30 @@ package createelement import ( + "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/element" ) type UseCase struct { elementRepository element.Repository + validator domain.Validator } -func NewUseCase(elementRepository element.Repository) *UseCase { +func NewUseCase(elementRepository element.Repository, validator domain.Validator) *UseCase { return &UseCase{ elementRepository: elementRepository, + validator: validator, } } func (uc *UseCase) Execute(request *Request) (*Response, error) { - if ok, validation := request.Validate(); !ok { + if validationErrors := uc.validator.Validate(request); len(validationErrors) > 0 { return &Response{ - ValidationErrors: validation, + ValidationErrors: validationErrors, }, nil } - elem := element.Element{ - Type: request.Type, - Body: request.Body, - Venues: request.Venues, - } - - uuid, err := uc.elementRepository.Save(&elem) + uuid, err := uc.elementRepository.Save(request.ToElement()) if err != nil { return nil, err } diff --git a/application/dashboard/element/getElement/response.go b/application/dashboard/element/getElement/response.go index abd6a25c..57f9835f 100644 --- a/application/dashboard/element/getElement/response.go +++ b/application/dashboard/element/getElement/response.go @@ -4,22 +4,73 @@ import ( "time" "github.com/khanzadimahdi/testproject/domain/element" + "github.com/khanzadimahdi/testproject/domain/element/component" ) type Response struct { UUID string `json:"uuid"` - Type string `json:"type"` Body any `json:"body"` Venues []string `json:"venues"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } +type itemComponentResponse struct { + Type string `json:"type"` + ContentUUID string `json:"content_uuid"` + ContentType string `json:"content_type"` +} + +type featuredComponentResponse struct { + Type string `json:"type"` + Main itemComponentResponse `json:"main"` + Aside []itemComponentResponse `json:"aside"` +} + +type jumbotronComponentResponse struct { + Type string `json:"type"` + Item itemComponentResponse `json:"item"` +} + +func toComponentResponse(c element.Component) any { + switch c.Type() { + case component.ComponentTypeItem: + return itemComponentResponse{ + Type: c.Type(), + ContentUUID: c.(component.Item).ContentUUID, + ContentType: c.(component.Item).ContentType, + } + case component.ComponentTypeFeatured: + featured := c.(component.Featured) + + aside := make([]itemComponentResponse, len(featured.Aside)) + for i := range featured.Aside { + aside[i] = itemComponentResponse{ + ContentUUID: featured.Aside[i].ContentUUID, + ContentType: featured.Aside[i].ContentType, + } + } + + return featuredComponentResponse{ + Type: c.Type(), + Main: toComponentResponse(featured.Main).(itemComponentResponse), + Aside: aside, + } + case component.ComponentTypeJumbotron: + jumbotron := c.(component.Jumbotron) + return jumbotronComponentResponse{ + Type: c.Type(), + Item: toComponentResponse(jumbotron.Item).(itemComponentResponse), + } + } + + return nil +} + func NewResponse(e element.Element) *Response { return &Response{ UUID: e.UUID, - Type: e.Type, - Body: e.Body, + Body: toComponentResponse(e.Body), Venues: e.Venues, CreatedAt: e.CreatedAt.Format(time.RFC3339), UpdatedAt: e.UpdatedAt.Format(time.RFC3339), diff --git a/application/dashboard/element/getElement/useCase_test.go b/application/dashboard/element/getElement/useCase_test.go index e8b5ce01..df660219 100644 --- a/application/dashboard/element/getElement/useCase_test.go +++ b/application/dashboard/element/getElement/useCase_test.go @@ -26,7 +26,6 @@ func TestUseCase_Execute(t *testing.T) { var ( a = element.Element{ UUID: "element-uuid-1", - Type: "item", Body: &mockComponent, Venues: []string{}, CreatedAt: time.Now(), @@ -34,6 +33,9 @@ func TestUseCase_Execute(t *testing.T) { } ) + mockComponent.On("Type").Return(component.ComponentTypeMock) + defer mockComponent.AssertExpectations(t) + elementRepository.On("GetOne", a.UUID).Return(a, nil) defer elementRepository.AssertExpectations(t) diff --git a/application/dashboard/element/getElements/response.go b/application/dashboard/element/getElements/response.go index 5f4316cf..d41dd55c 100644 --- a/application/dashboard/element/getElements/response.go +++ b/application/dashboard/element/getElements/response.go @@ -13,7 +13,7 @@ type Response struct { type elementResponse struct { UUID string `json:"uuid"` - Type string `json:"type"` + BodyType string `json:"body_type"` Venues []string `json:"venues"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` @@ -29,7 +29,7 @@ func NewResponse(a []element.Element, totalPages, currentPage uint) *Response { for i := range a { items[i].UUID = a[i].UUID - items[i].Type = a[i].Type + items[i].BodyType = a[i].Body.Type() items[i].Venues = a[i].Venues items[i].CreatedAt = a[i].CreatedAt.Format(time.RFC3339) items[i].UpdatedAt = a[i].UpdatedAt.Format(time.RFC3339) diff --git a/application/dashboard/element/updateElement/request.go b/application/dashboard/element/updateElement/request.go index 58894993..c8788405 100644 --- a/application/dashboard/element/updateElement/request.go +++ b/application/dashboard/element/updateElement/request.go @@ -2,61 +2,217 @@ package updateelement import ( "encoding/json" + "strconv" + "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/element" "github.com/khanzadimahdi/testproject/domain/element/component" ) -type validationErrors map[string]string - type Request struct { - UUID string `json:"uuid"` - Type string `json:"type"` - Body element.Component `json:"body"` - Venues []string `json:"venues"` + UUID string `json:"-"` + Body domain.Validatable `json:"-"` + Venues []string `json:"-"` } -func (e *Request) UnmarshalJSON(data []byte) error { - type Child Request +var _ domain.Validatable = &Request{} +var _ json.Unmarshaler = &Request{} + +func (r *Request) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.UUID) == 0 { + validationErrors["uuid"] = "required_field" + } + + if errs := r.Body.Validate(); len(errs) > 0 { + for errKey, errValue := range errs { + validationErrors["body."+errKey] = errValue + } + } + + return validationErrors +} + +type itemComponentRequest struct { + Type string `json:"type"` + ContentUUID string `json:"content_uuid"` + ContentType string `json:"content_type"` +} + +var _ domain.Validatable = &itemComponentRequest{} + +func (r *itemComponentRequest) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.Type) == 0 { + validationErrors["type"] = "required_field" + } + + if r.Type != component.ComponentTypeItem { + validationErrors["type"] = "invalid_value" + } + + if len(r.ContentUUID) == 0 { + validationErrors["content_uuid"] = "required_field" + } + + if len(r.ContentType) == 0 { + validationErrors["content_type"] = "required_field" + } + + return validationErrors +} + +type featuredComponentRequest struct { + Type string `json:"type"` + Main itemComponentRequest `json:"main"` + Aside []itemComponentRequest `json:"aside"` +} + +var _ domain.Validatable = &featuredComponentRequest{} + +func (r *featuredComponentRequest) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.Type) == 0 { + validationErrors["type"] = "required_field" + } + + if r.Type != component.ComponentTypeFeatured { + validationErrors["type"] = "invalid_value" + } + + if errs := r.Main.Validate(); len(errs) > 0 { + for errKey, errValue := range errs { + validationErrors["main."+errKey] = errValue + } + } + for i, aside := range r.Aside { + if errs := aside.Validate(); len(errs) > 0 { + for errKey, errValue := range errs { + validationErrors["aside."+strconv.Itoa(i)+"."+errKey] = errValue + } + } + } + + return validationErrors +} + +func (e *Request) UnmarshalJSON(data []byte) error { var tmp struct { - Child - Body json.RawMessage `json:"body"` + UUID string `json:"uuid"` + Venues []string `json:"venues"` + Component struct { + Type string `json:"type"` + } `json:"body"` } if err := json.Unmarshal(data, &tmp); err != nil { return err } - switch tmp.Type { - case "jumbotron": - j := component.Jumbotron{} - - if err := json.Unmarshal(tmp.Body, &j); err != nil { + switch tmp.Component.Type { + case component.ComponentTypeItem: + var component struct { + Body itemComponentRequest `json:"body"` + } + if err := json.Unmarshal(data, &component); err != nil { return err } - tmp.Child.Body = j - case "featured": - j := component.Featured{} - if err := json.Unmarshal(tmp.Body, &j); err != nil { + e.Body = &component.Body + case component.ComponentTypeJumbotron: + var component struct { + Body jumbotronComponentRequest `json:"body"` + } + if err := json.Unmarshal(data, &component); err != nil { return err } - tmp.Child.Body = j - case "item": - j := component.Item{} - if err := json.Unmarshal(tmp.Body, &j); err != nil { + e.Body = &component.Body + case component.ComponentTypeFeatured: + var component struct { + Body featuredComponentRequest `json:"body"` + } + if err := json.Unmarshal(data, &component); err != nil { return err } - tmp.Child.Body = j + e.Body = &component.Body default: return element.ErrUnSupportedComponent } - *e = Request(tmp.Child) + e.UUID = tmp.UUID + e.Venues = tmp.Venues return nil } -func (r *Request) Validate() (bool, validationErrors) { - return true, nil +type jumbotronComponentRequest struct { + Type string `json:"type"` + Item itemComponentRequest `json:"item"` +} + +var _ domain.Validatable = &jumbotronComponentRequest{} + +func (r *jumbotronComponentRequest) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.Type) == 0 { + validationErrors["type"] = "required_field" + } + + if r.Type != component.ComponentTypeJumbotron { + validationErrors["type"] = "invalid_value" + } + + if errs := r.Item.Validate(); len(errs) > 0 { + for errKey, errValue := range errs { + validationErrors["item."+errKey] = errValue + } + } + + return validationErrors +} + +func (r *Request) ToElement() *element.Element { + e := &element.Element{ + UUID: r.UUID, + Venues: r.Venues, + } + + switch r.Body.(type) { + case *itemComponentRequest: + e.Body = component.Item{ + ContentUUID: r.Body.(*itemComponentRequest).ContentUUID, + ContentType: r.Body.(*itemComponentRequest).ContentType, + } + case *jumbotronComponentRequest: + e.Body = component.Jumbotron{ + Item: component.Item{ + ContentUUID: r.Body.(*jumbotronComponentRequest).Item.ContentUUID, + ContentType: r.Body.(*jumbotronComponentRequest).Item.ContentType, + }, + } + case *featuredComponentRequest: + main := component.Item{ + ContentUUID: r.Body.(*featuredComponentRequest).Main.ContentUUID, + ContentType: r.Body.(*featuredComponentRequest).Main.ContentType, + } + + aside := make([]component.Item, len(r.Body.(*featuredComponentRequest).Aside)) + for i := range r.Body.(*featuredComponentRequest).Aside { + aside[i] = component.Item{ + ContentUUID: r.Body.(*featuredComponentRequest).Aside[i].ContentUUID, + ContentType: r.Body.(*featuredComponentRequest).Aside[i].ContentType, + } + } + + e.Body = component.Featured{ + Main: main, + Aside: aside, + } + } + + return e } diff --git a/application/dashboard/element/updateElement/request_test.go b/application/dashboard/element/updateElement/request_test.go index 8edaf1ee..cfa097fa 100644 --- a/application/dashboard/element/updateElement/request_test.go +++ b/application/dashboard/element/updateElement/request_test.go @@ -8,22 +8,75 @@ import ( ) func TestRequest_Validate(t *testing.T) { - t.Run("always returns valid", func(t *testing.T) { + t.Run("valid request with item component", func(t *testing.T) { req := Request{ - UUID: "element-uuid-123", - Type: "jumbotron", - Body: component.Jumbotron{}, + UUID: "element-uuid-123", + Body: &itemComponentRequest{ + Type: component.ComponentTypeItem, + ContentUUID: "test-uuid", + ContentType: "article", + }, Venues: []string{"venue1"}, } - valid, errs := req.Validate() + errs := req.Validate() + + if len(errs) != 0 { + t.Errorf("Validate() returned %d errors, want 0: %v", len(errs), errs) + } + }) - if !valid { - t.Errorf("Validate() valid = false, want true") + t.Run("valid request with jumbotron component", func(t *testing.T) { + req := Request{ + UUID: "element-uuid-123", + Body: &jumbotronComponentRequest{ + Type: component.ComponentTypeJumbotron, + Item: itemComponentRequest{ + Type: component.ComponentTypeItem, + ContentUUID: "content-uuid-123", + ContentType: "content-type-123", + }, + }, + Venues: []string{"venue1"}, } + errs := req.Validate() + if len(errs) != 0 { - t.Errorf("Validate() returned %d errors, want 0", len(errs)) + t.Errorf("Validate() returned %d errors, want 0: %v", len(errs), errs) + } + }) + + t.Run("valid request with featured component", func(t *testing.T) { + req := Request{ + UUID: "element-uuid-123", + Body: &featuredComponentRequest{ + Type: component.ComponentTypeFeatured, + Main: itemComponentRequest{ + Type: component.ComponentTypeItem, + ContentUUID: "main-uuid", + ContentType: "article", + }, + Aside: []itemComponentRequest{ + { + Type: component.ComponentTypeItem, + ContentUUID: "aside-uuid-1", + ContentType: "article", + }, + { + Type: component.ComponentTypeItem, + ContentUUID: "aside-uuid-2", + ContentType: "article", + }, + }, + }, + Venues: []string{"venue1", "venue2"}, + } + + errs := req.Validate() + + if len(errs) != 0 { + t.Errorf("Validate() returned %d errors, want 0: %v", len(errs), errs) } }) } @@ -36,35 +89,41 @@ func TestRequest_UnmarshalJSON(t *testing.T) { check func(*Request) bool }{ { - name: "unmarshals jumbotron component", - json: `{"uuid":"element-uuid-123","type":"jumbotron","body":{"title":"Test","subtitle":"Subtitle"},"venues":["venue1"]}`, + name: "unmarshals jumbotron component", + json: `{"uuid":"element-uuid-123","body":{"type":"jumbotron","item":{"type":"item","content_uuid":"test-uuid","content_type":"article"}},"venues":["venue1"]}`, wantErr: false, check: func(r *Request) bool { - _, ok := r.Body.(component.Jumbotron) - return ok && r.Type == "jumbotron" && r.UUID == "element-uuid-123" + jumbotron, ok := r.Body.(*jumbotronComponentRequest) + return ok && jumbotron.Type == component.ComponentTypeJumbotron && r.UUID == "element-uuid-123" && len(r.Venues) == 1 }, }, { - name: "unmarshals featured component", - json: `{"uuid":"element-uuid-123","type":"featured","body":{},"venues":["venue1"]}`, + name: "unmarshals featured component", + json: `{"uuid":"element-uuid-123","body":{"type":"featured","main":{"type":"item","content_uuid":"main-uuid","content_type":"article"},"aside":[{"type":"item","content_uuid":"aside-uuid","content_type":"article"}]},"venues":["venue1"]}`, wantErr: false, check: func(r *Request) bool { - _, ok := r.Body.(component.Featured) - return ok && r.Type == "featured" + featured, ok := r.Body.(*featuredComponentRequest) + return ok && featured.Type == component.ComponentTypeFeatured && r.UUID == "element-uuid-123" && len(r.Venues) == 1 }, }, { - name: "unmarshals item component", - json: `{"uuid":"element-uuid-123","type":"item","body":{},"venues":["venue1"]}`, + name: "unmarshals item component", + json: `{"uuid":"element-uuid-123","body":{"type":"item","content_uuid":"test-uuid","content_type":"article"},"venues":["venue1"]}`, wantErr: false, check: func(r *Request) bool { - _, ok := r.Body.(component.Item) - return ok && r.Type == "item" + item, ok := r.Body.(*itemComponentRequest) + return ok && item.Type == component.ComponentTypeItem && r.UUID == "element-uuid-123" && len(r.Venues) == 1 }, }, { name: "returns error for unsupported component type", - json: `{"uuid":"element-uuid-123","type":"unsupported","body":{},"venues":["venue1"]}`, + json: `{"uuid":"element-uuid-123","body":{"type":"unsupported"},"venues":["venue1"]}`, + wantErr: true, + check: func(r *Request) bool { return true }, + }, + { + name: "returns error for malformed json", + json: `{"body":}`, wantErr: true, check: func(r *Request) bool { return true }, }, @@ -86,4 +145,3 @@ func TestRequest_UnmarshalJSON(t *testing.T) { }) } } - diff --git a/application/dashboard/element/updateElement/response.go b/application/dashboard/element/updateElement/response.go index d8a56176..24ec80be 100644 --- a/application/dashboard/element/updateElement/response.go +++ b/application/dashboard/element/updateElement/response.go @@ -1,5 +1,7 @@ package updateelement +import "github.com/khanzadimahdi/testproject/domain" + type Response struct { - ValidationErrors validationErrors `json:"errors,omitempty"` + ValidationErrors domain.ValidationErrors `json:"errors,omitempty"` } diff --git a/application/dashboard/element/updateElement/usecase.go b/application/dashboard/element/updateElement/usecase.go index 3076df4b..b0c022e7 100644 --- a/application/dashboard/element/updateElement/usecase.go +++ b/application/dashboard/element/updateElement/usecase.go @@ -1,34 +1,30 @@ package updateelement import ( + "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/element" ) type UseCase struct { elementRepository element.Repository + validator domain.Validator } -func NewUseCase(elementRepository element.Repository) *UseCase { +func NewUseCase(elementRepository element.Repository, validator domain.Validator) *UseCase { return &UseCase{ elementRepository: elementRepository, + validator: validator, } } func (uc *UseCase) Execute(request *Request) (*Response, error) { - if ok, validation := request.Validate(); !ok { + if validationErrors := uc.validator.Validate(request); len(validationErrors) > 0 { return &Response{ - ValidationErrors: validation, + ValidationErrors: validationErrors, }, nil } - elem := element.Element{ - UUID: request.UUID, - Type: request.Type, - Body: request.Body, - Venues: request.Venues, - } - - if _, err := uc.elementRepository.Save(&elem); err != nil { + if _, err := uc.elementRepository.Save(request.ToElement()); err != nil { return nil, err } diff --git a/application/dashboard/profile/changepassword/request.go b/application/dashboard/profile/changepassword/request.go index f4e00124..8433804a 100644 --- a/application/dashboard/profile/changepassword/request.go +++ b/application/dashboard/profile/changepassword/request.go @@ -2,8 +2,6 @@ package changepassword import "github.com/khanzadimahdi/testproject/domain" -type validationErrors map[string]string - type Request struct { UserUUID string `json:"-"` CurrentPassword string `json:"current_password"` diff --git a/application/dashboard/profile/updateprofile/request.go b/application/dashboard/profile/updateprofile/request.go index 1dcbf39a..770bf953 100644 --- a/application/dashboard/profile/updateprofile/request.go +++ b/application/dashboard/profile/updateprofile/request.go @@ -1,6 +1,6 @@ package updateprofile -type validationErrors map[string]string +import "github.com/khanzadimahdi/testproject/domain" type Request struct { UserUUID string `json:"-"` @@ -10,24 +10,26 @@ type Request struct { Username string `json:"username"` } -func (r *Request) Validate() (bool, validationErrors) { - errors := make(validationErrors) +var _ domain.Validatable = &Request{} + +func (r *Request) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) if len(r.UserUUID) == 0 { - errors["uuid"] = "required_field" + validationErrors["uuid"] = "required_field" } if len(r.Name) == 0 { - errors["name"] = "required_field" + validationErrors["name"] = "required_field" } if len(r.Email) == 0 { - errors["email"] = "required_field" + validationErrors["email"] = "required_field" } if len(r.Username) == 0 { - errors["username"] = "required_field" + validationErrors["username"] = "required_field" } - return len(errors) == 0, errors + return validationErrors } diff --git a/application/dashboard/profile/updateprofile/request_test.go b/application/dashboard/profile/updateprofile/request_test.go index 2463c049..3c7c12c1 100644 --- a/application/dashboard/profile/updateprofile/request_test.go +++ b/application/dashboard/profile/updateprofile/request_test.go @@ -3,6 +3,7 @@ package updateprofile import ( "testing" + "github.com/khanzadimahdi/testproject/domain" "github.com/stretchr/testify/assert" ) @@ -11,7 +12,7 @@ func TestRequest_Validate(t *testing.T) { name string request Request want bool - wantErr validationErrors + wantErr domain.ValidationErrors }{ { name: "valid request", @@ -22,8 +23,7 @@ func TestRequest_Validate(t *testing.T) { Email: "user@example.com", Username: "johndoe", }, - want: true, - wantErr: validationErrors{}, + wantErr: domain.ValidationErrors{}, }, { name: "valid request with empty optional fields", @@ -33,8 +33,7 @@ func TestRequest_Validate(t *testing.T) { Email: "user@example.com", Username: "johndoe", }, - want: true, - wantErr: validationErrors{}, + wantErr: domain.ValidationErrors{}, }, { name: "invalid request with empty user uuid", @@ -44,8 +43,7 @@ func TestRequest_Validate(t *testing.T) { Email: "user@example.com", Username: "johndoe", }, - want: false, - wantErr: validationErrors{ + wantErr: domain.ValidationErrors{ "uuid": "required_field", }, }, @@ -57,8 +55,7 @@ func TestRequest_Validate(t *testing.T) { Email: "user@example.com", Username: "johndoe", }, - want: false, - wantErr: validationErrors{ + wantErr: domain.ValidationErrors{ "name": "required_field", }, }, @@ -70,8 +67,7 @@ func TestRequest_Validate(t *testing.T) { Email: "", Username: "johndoe", }, - want: false, - wantErr: validationErrors{ + wantErr: domain.ValidationErrors{ "email": "required_field", }, }, @@ -83,8 +79,7 @@ func TestRequest_Validate(t *testing.T) { Email: "user@example.com", Username: "", }, - want: false, - wantErr: validationErrors{ + wantErr: domain.ValidationErrors{ "username": "required_field", }, }, @@ -96,8 +91,7 @@ func TestRequest_Validate(t *testing.T) { Email: "", Username: "", }, - want: false, - wantErr: validationErrors{ + wantErr: domain.ValidationErrors{ "uuid": "required_field", "name": "required_field", "email": "required_field", @@ -108,10 +102,8 @@ func TestRequest_Validate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, gotErr := tt.request.Validate() - assert.Equal(t, tt.want, got) + gotErr := tt.request.Validate() assert.Equal(t, tt.wantErr, gotErr) }) } } - diff --git a/application/element/element.go b/application/element/element.go new file mode 100644 index 00000000..2943ea89 --- /dev/null +++ b/application/element/element.go @@ -0,0 +1,49 @@ +package element + +import ( + "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/element" + "github.com/khanzadimahdi/testproject/domain/element/component" +) + +// ElementRetriever is a service that retrieves elements by venues. +type Retriever struct { + articleRepository article.Repository + elementRepository element.Repository +} + +// NewRetriever creates a new Retriever. +func NewRetriever( + articleRepository article.Repository, + elementRepository element.Repository, +) *Retriever { + return &Retriever{ + articleRepository: articleRepository, + elementRepository: elementRepository, + } +} + +// RetrieveByVenues retrieves elements by venues. +func (r *Retriever) RetrieveByVenues(venues []string) ([]Response, error) { + elements, err := r.elementRepository.GetByVenues(venues) + if err != nil { + return nil, err + } + + items := make([]component.Item, 0, len(elements)) + for i := range elements { + items = append(items, elements[i].Body.Items()...) + } + + uuids := make([]string, len(items)) + for i := range items { + uuids[i] = items[i].ContentUUID + } + + articles, err := r.articleRepository.GetByUUIDs(uuids) + if err != nil { + return nil, err + } + + return NewResponse(elements, articles), nil +} diff --git a/application/element/response.go b/application/element/response.go new file mode 100644 index 00000000..24ad9190 --- /dev/null +++ b/application/element/response.go @@ -0,0 +1,123 @@ +package element + +import ( + "time" + + "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/element" + "github.com/khanzadimahdi/testproject/domain/element/component" +) + +type Response struct { + Type string `json:"type"` + Body any `json:"body"` +} + +type authorResponse struct { + Name string `json:"name"` + Avatar string `json:"avatar"` +} + +type articleResponse struct { + UUID string `json:"uuid"` + Cover string `json:"cover"` + Title string `json:"title"` + Author authorResponse `json:"author"` + PublishedAt string `json:"published_at"` + Excerpt string `json:"excerpt"` + Tags []string `json:"tags"` +} + +type itemComponentResponse struct { + Type string `json:"type"` + Body any `json:"body"` +} + +type featuredComponentResponse struct { + Main itemComponentResponse `json:"main"` + Aside []itemComponentResponse `json:"aside"` +} + +type jumbotronComponentResponse struct { + Item itemComponentResponse `json:"item"` +} + +func NewResponse(elements []element.Element, elementsContent []article.Article) []Response { + response := make([]Response, len(elements)) + for i := range elements { + response[i].Type = elements[i].Body.Type() + response[i].Body = toComponentResponse(elements[i].Body, elementsContent) + } + + return response +} + +func toComponentResponse(ec element.Component, elementsContent []article.Article) any { + var c any + + if ec.Type() == component.ComponentTypeJumbotron { + c = toJumbotronResponse(ec.(component.Jumbotron), elementsContent) + } + + if ec.Type() == component.ComponentTypeFeatured { + c = toFeaturedResponse(ec.(component.Featured), elementsContent) + } + + if ec.Type() == component.ComponentTypeItem { + c = toItemResponse(ec.(component.Item), elementsContent) + } + + return c +} + +func toJumbotronResponse(c component.Jumbotron, elementsContent []article.Article) jumbotronComponentResponse { + return jumbotronComponentResponse{ + Item: toItemResponse(c.Item, elementsContent), + } +} + +func toFeaturedResponse(c component.Featured, elementsContent []article.Article) featuredComponentResponse { + aside := make([]itemComponentResponse, len(c.Aside)) + + for i := range c.Aside { + aside[i] = toItemResponse(c.Aside[i], elementsContent) + } + + return featuredComponentResponse{ + Main: toItemResponse(c.Main, elementsContent), + Aside: aside, + } +} + +func toItemResponse(c component.Item, elementsContent []article.Article) itemComponentResponse { + var body any + for i := range elementsContent { + if elementsContent[i].UUID == c.ContentUUID { + body = toArticleResponse([]article.Article{elementsContent[i]})[0] + break + } + } + + return itemComponentResponse{ + Type: c.Type(), + Body: body, + } +} + +func toArticleResponse(a []article.Article) []articleResponse { + items := make([]articleResponse, len(a)) + + for i := range a { + items[i].UUID = a[i].UUID + items[i].Cover = a[i].Cover + items[i].Title = a[i].Title + items[i].Excerpt = a[i].Excerpt + items[i].Tags = a[i].Tags + items[i].PublishedAt = a[i].PublishedAt.Format(time.RFC3339) + + items[i].Author.Name = a[i].Author.Name + items[i].Author.Avatar = a[i].Author.Avatar + } + + return items +} diff --git a/application/home/response.go b/application/home/response.go index c03a5157..0e921067 100644 --- a/application/home/response.go +++ b/application/home/response.go @@ -3,15 +3,14 @@ package home import ( "time" + "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain/article" - "github.com/khanzadimahdi/testproject/domain/element" - "github.com/khanzadimahdi/testproject/domain/element/component" ) type Response struct { - All []articleResponse `json:"all"` - Popular []articleResponse `json:"popular"` - Elements []elementResponse `json:"elements"` + All []articleResponse `json:"all"` + Popular []articleResponse `json:"popular"` + Elements []element.Response `json:"elements"` } type articleResponse struct { @@ -29,95 +28,11 @@ type author struct { Avatar string `json:"avatar"` } -type elementResponse struct { - Type string `json:"type"` - Body any `json:"body"` -} - -type itemComponentResponse struct { - Type string `json:"type"` - Body any `json:"body"` -} - -type featuredComponentResponse struct { - Main itemComponentResponse `json:"main"` - Aside []itemComponentResponse `json:"aside"` -} - -type jumbotronComponentResponse struct { - itemComponentResponse -} - -func NewResponse(all, popular []article.Article, e []element.Element, elementsContent []article.Article) *Response { - elements := make([]elementResponse, len(e)) - for i := range e { - c := toComponentResponse(e[i], elementsContent) - if c == nil { - continue - } - - elements[i] = elementResponse{ - Type: e[i].Type, - Body: c, - } - } - +func NewResponse(all, popular []article.Article, elementsResponse []element.Response) *Response { return &Response{ All: toArticleResponse(all), Popular: toArticleResponse(popular), - Elements: elements, - } -} - -func toComponentResponse(e element.Element, elementsContent []article.Article) any { - var c any - - if e.Type == "jumbotron" { - c = toJumbotronResponse(e.Body.(component.Jumbotron), elementsContent) - } - - if e.Type == "featured" { - c = toFeaturedResponse(e.Body.(component.Featured), elementsContent) - } - - if e.Type == "item" { - c = toItemResponse(e.Body.(component.Item), elementsContent) - } - - return c -} - -func toJumbotronResponse(c component.Jumbotron, elementsContent []article.Article) jumbotronComponentResponse { - return jumbotronComponentResponse{ - itemComponentResponse: toItemResponse(c.Item, elementsContent), - } -} - -func toFeaturedResponse(c component.Featured, elementsContent []article.Article) featuredComponentResponse { - aside := make([]itemComponentResponse, len(c.Aside)) - - for i := range c.Aside { - aside[i] = toItemResponse(c.Aside[i], elementsContent) - } - - return featuredComponentResponse{ - Main: toItemResponse(c.Main, elementsContent), - Aside: aside, - } -} - -func toItemResponse(c component.Item, elementsContent []article.Article) itemComponentResponse { - var body any - for i := range elementsContent { - if elementsContent[i].UUID == c.UUID { - body = toArticleResponse([]article.Article{elementsContent[i]})[0] - break - } - } - - return itemComponentResponse{ - Type: c.Type, - Body: body, + Elements: elementsResponse, } } diff --git a/application/home/usecase.go b/application/home/usecase.go index e552b70d..062285e2 100644 --- a/application/home/usecase.go +++ b/application/home/usecase.go @@ -1,23 +1,22 @@ package home import ( + "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain/article" - "github.com/khanzadimahdi/testproject/domain/element" - "github.com/khanzadimahdi/testproject/domain/element/component" ) type UseCase struct { articleRepository article.Repository - elementRepository element.Repository + elementRetriever *element.Retriever } func NewUseCase( articleRepository article.Repository, - elementRepository element.Repository, + elementRetriever *element.Retriever, ) *UseCase { return &UseCase{ articleRepository: articleRepository, - elementRepository: elementRepository, + elementRetriever: elementRetriever, } } @@ -32,33 +31,10 @@ func (uc *UseCase) Execute() (*Response, error) { return nil, err } - elements, elementsArticles, err := uc.elements() + elementsResponse, err := uc.elementRetriever.RetrieveByVenues([]string{"home"}) if err != nil { return nil, err } - return NewResponse(all, popular, elements, elementsArticles), err -} - -func (uc *UseCase) elements() ([]element.Element, []article.Article, error) { - elements, err := uc.elementRepository.GetByVenues([]string{"home"}) - if err != nil { - return nil, nil, err - } - - items := make([]component.Item, 0, len(elements)) - for i := range elements { - items = append(items, elements[i].Body.Items()...) - } - - uuids := make([]string, len(items)) - for i := range items { - uuids[i] = items[i].UUID - } - articles, err := uc.articleRepository.GetByUUIDs(uuids) - if err != nil { - return nil, nil, err - } - - return elements, articles, nil + return NewResponse(all, popular, elementsResponse), nil } diff --git a/application/home/usecase_test.go b/application/home/usecase_test.go index 8c01e7b6..b72e8cbd 100644 --- a/application/home/usecase_test.go +++ b/application/home/usecase_test.go @@ -6,8 +6,9 @@ import ( "github.com/stretchr/testify/assert" + "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain/article" - "github.com/khanzadimahdi/testproject/domain/element" + domainElement "github.com/khanzadimahdi/testproject/domain/element" "github.com/khanzadimahdi/testproject/domain/element/component" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/elements" @@ -36,15 +37,15 @@ func TestUseCase_Execute(t *testing.T) { } i = []component.Item{ - {UUID: va[0].UUID}, - {UUID: va[1].UUID}, - {UUID: "not-exist-article-uuid"}, + {ContentUUID: va[0].UUID}, + {ContentUUID: va[1].UUID}, + {ContentUUID: "not-exist-article-uuid"}, } u = []string{ - i[0].UUID, - i[1].UUID, - i[2].UUID, + i[0].ContentUUID, + i[1].ContentUUID, + i[2].ContentUUID, } ) @@ -54,16 +55,18 @@ func TestUseCase_Execute(t *testing.T) { defer articlesRepository.AssertExpectations(t) mockComponent.On("Items").Once().Return(i) + mockComponent.On("Type").Return(component.ComponentTypeMock) defer mockComponent.AssertExpectations(t) - v := []element.Element{ + v := []domainElement.Element{ {Body: &mockComponent}, } elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(v, nil) defer elementsRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + usecase := NewUseCase(&articlesRepository, elementRetriever) response, err := usecase.Execute() assert.NotNil(t, response, "unexpected response") @@ -83,7 +86,8 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository.On("GetMostViewed", uint(4)).Once().Return(nil, expectedErr) defer articlesRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + usecase := NewUseCase(&articlesRepository, elementRetriever) response, err := usecase.Execute() articlesRepository.AssertNotCalled(t, "GetAllPublished") @@ -114,7 +118,8 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository.On("GetAllPublished", uint(0), uint(3)).Return(nil, expectedErr) defer articlesRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + usecase := NewUseCase(&articlesRepository, elementRetriever) response, err := usecase.Execute() elementsRepository.AssertNotCalled(t, "GetByVenues") @@ -147,7 +152,8 @@ func TestUseCase_Execute(t *testing.T) { elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(nil, expectedErr) defer elementsRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + usecase := NewUseCase(&articlesRepository, elementRetriever) response, err := usecase.Execute() articlesRepository.AssertNotCalled(t, "GetByUUIDs") @@ -176,15 +182,15 @@ func TestUseCase_Execute(t *testing.T) { } i = []component.Item{ - {UUID: va[0].UUID}, - {UUID: va[1].UUID}, - {UUID: "not-exist-article-uuid"}, + {ContentUUID: va[0].UUID}, + {ContentUUID: va[1].UUID}, + {ContentUUID: "not-exist-article-uuid"}, } u = []string{ - i[0].UUID, - i[1].UUID, - i[2].UUID, + i[0].ContentUUID, + i[1].ContentUUID, + i[2].ContentUUID, } expectedErr = errors.New("some error") @@ -198,14 +204,15 @@ func TestUseCase_Execute(t *testing.T) { mockComponent.On("Items").Once().Return(i) defer mockComponent.AssertExpectations(t) - v := []element.Element{ + v := []domainElement.Element{ {Body: &mockComponent}, } elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(v, nil) defer elementsRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + usecase := NewUseCase(&articlesRepository, elementRetriever) response, err := usecase.Execute() assert.Nil(t, response, "unexpected response") diff --git a/domain/element/component/featured.go b/domain/element/component/featured.go index 21ad6730..c942cfc0 100644 --- a/domain/element/component/featured.go +++ b/domain/element/component/featured.go @@ -1,5 +1,7 @@ package component +const ComponentTypeFeatured = "featured" + type Featured struct { Main Item Aside []Item @@ -12,3 +14,7 @@ func (c Featured) Items() []Item { return items } + +func (c Featured) Type() string { + return ComponentTypeFeatured +} diff --git a/domain/element/component/item.go b/domain/element/component/item.go index 277f1a77..e1eed0c9 100644 --- a/domain/element/component/item.go +++ b/domain/element/component/item.go @@ -1,10 +1,16 @@ package component +const ComponentTypeItem = "item" + type Item struct { - UUID string - Type string + ContentUUID string + ContentType string } func (c Item) Items() []Item { return []Item{c} } + +func (c Item) Type() string { + return ComponentTypeItem +} diff --git a/domain/element/component/jumbotron.go b/domain/element/component/jumbotron.go index 021f0a9c..98175286 100644 --- a/domain/element/component/jumbotron.go +++ b/domain/element/component/jumbotron.go @@ -1,9 +1,15 @@ package component +const ComponentTypeJumbotron = "jumbotron" + type Jumbotron struct { - Item + Item Item } func (c Jumbotron) Items() []Item { return []Item{c.Item} } + +func (c Jumbotron) Type() string { + return ComponentTypeJumbotron +} diff --git a/domain/element/component/mock.go b/domain/element/component/mock.go index bcc8a981..f8591833 100644 --- a/domain/element/component/mock.go +++ b/domain/element/component/mock.go @@ -4,6 +4,8 @@ import ( "github.com/stretchr/testify/mock" ) +const ComponentTypeMock = "mock" + type MockComponent struct { mock.Mock } @@ -17,3 +19,9 @@ func (c *MockComponent) Items() []Item { return nil } + +func (c *MockComponent) Type() string { + c.Mock.Called() + + return ComponentTypeMock +} diff --git a/domain/element/element.go b/domain/element/element.go index 569fd5f8..ca72b5a7 100644 --- a/domain/element/element.go +++ b/domain/element/element.go @@ -7,21 +7,25 @@ import ( "github.com/khanzadimahdi/testproject/domain/element/component" ) +// ErrUnSupportedComponent is an error that is returned when a component type is not supported. var ErrUnSupportedComponent error = errors.New("unsupported component type") +// Component is an interface that represents a component of an element. type Component interface { Items() []component.Item + Type() string } +// Element represents an element. type Element struct { UUID string - Type string Body Component Venues []string CreatedAt time.Time UpdatedAt time.Time } +// Repository represents a repository of elements. type Repository interface { GetAll(offset uint, limit uint) ([]Element, error) GetByVenues(Venues []string) ([]Element, error) diff --git a/infrastructure/ioc/providers/blog.go b/infrastructure/ioc/providers/blog.go index 512285c8..30b7b819 100644 --- a/infrastructure/ioc/providers/blog.go +++ b/infrastructure/ioc/providers/blog.go @@ -67,6 +67,7 @@ import ( getusers "github.com/khanzadimahdi/testproject/application/dashboard/user/getUsers" updateuser "github.com/khanzadimahdi/testproject/application/dashboard/user/updateUser" "github.com/khanzadimahdi/testproject/application/dashboard/user/userchangepassword" + "github.com/khanzadimahdi/testproject/application/element" getFile "github.com/khanzadimahdi/testproject/application/file/getFile" "github.com/khanzadimahdi/testproject/application/home" "github.com/khanzadimahdi/testproject/domain" @@ -233,9 +234,10 @@ func blog( configRepository := configrepository.NewRepository(database) authTokenGenerator := auth.NewTokenGenerator(jwt, rolesRepository) + elementRetriever := element.NewRetriever(articlesRepository, elementsRepository) // ---- public ---- - homeUseCase := home.NewUseCase(articlesRepository, elementsRepository) + homeUseCase := home.NewUseCase(articlesRepository, elementRetriever) loginUseCase := login.NewUseCase(userRepository, authTokenGenerator, hasher, translator, validator) refreshUseCase := refresh.NewUseCase(userRepository, jwt, authTokenGenerator, translator, validator) @@ -244,7 +246,7 @@ func blog( registerUseCase := register.NewUseCase(userRepository, asyncPublishSubscriber, translator, validator) verifyUseCase := verify.NewUseCase(userRepository, rolesRepository, configRepository, hasher, jwt, translator, validator) - getArticleUsecase := getArticle.NewUseCase(articlesRepository, elementsRepository) + getArticleUsecase := getArticle.NewUseCase(articlesRepository, elementRetriever) getArticlesUsecase := getArticles.NewUseCase(articlesRepository) getArticlesByHashtagUseCase := getArticlesByHashtag.NewUseCase(articlesRepository, validator) getFileUseCase := getFile.NewUseCase(filesRepository, fileStorage) @@ -302,11 +304,11 @@ func blog( dashboardGetUserFilesUseCase := dashboardGetUserFiles.NewUseCase(filesRepository) dashboardDeleteUserFileUseCase := dashboardDeleteUserFile.NewUseCase(filesRepository, fileStorage) - dashboardCreateElementUsecase := dashboardCreateElement.NewUseCase(elementsRepository) + dashboardCreateElementUsecase := dashboardCreateElement.NewUseCase(elementsRepository, validator) dashboardDeleteElementUsecase := dashboardDeleteElement.NewUseCase(elementsRepository) dashboardGetElementUsecase := dashboardGetElement.NewUseCase(elementsRepository) dashboardGetElementsUsecase := dashboardGetElements.NewUseCase(elementsRepository) - dashboardUpdateElementUsecase := dashboardUpdateElement.NewUseCase(elementsRepository) + dashboardUpdateElementUsecase := dashboardUpdateElement.NewUseCase(elementsRepository, validator) dashboardGetConfigUsecase := dashboardGetConfig.NewUseCase(configRepository) dashboardUpdateConfigUsecase := dashboardUpdateConfig.NewUseCase(configRepository, validator) diff --git a/infrastructure/repository/mongodb/elements/model.go b/infrastructure/repository/mongodb/elements/model.go index 37cd4ae7..a26d7547 100644 --- a/infrastructure/repository/mongodb/elements/model.go +++ b/infrastructure/repository/mongodb/elements/model.go @@ -10,51 +10,199 @@ import ( ) type ElementBson struct { - UUID string `bson:"_id,omitempty"` - Type string `bson:"type,omitempty"` - Body element.Component `bson:"body"` - Venues []string `bson:"venues"` - CreatedAt time.Time `bson:"created_at,omitempty"` - UpdatedAt time.Time `bson:"updated_at,omitempty"` + UUID string `bson:"_id,omitempty"` + Body any `bson:"body"` + Venues []string `bson:"venues"` + CreatedAt time.Time `bson:"created_at,omitempty"` + UpdatedAt time.Time `bson:"updated_at,omitempty"` } -func (e *ElementBson) UnmarshalBSON(data []byte) error { - type Child ElementBson +type ItemBson struct { + Type string `bson:"type"` + ContentUUID string `bson:"content_uuid"` + ContentType string `bson:"content_type"` +} + +type JumbotronBson struct { + Type string `bson:"type"` + Item ItemBson `bson:"item"` +} - var tmp struct { - Child `bson:",inline"` - Body bson.Raw `bson:"body"` +type FeaturedBson struct { + Type string `bson:"type"` + Main ItemBson `bson:"main"` + Aside []ItemBson `bson:"aside"` +} + +func (e *ElementBson) UnmarshalBSON(data []byte) error { + var temporary struct { + Element ElementBson `bson:",inline"` + Component struct { + Type string `bson:"type"` + } `bson:"component"` + Body bson.Raw `bson:"body"` } - if err := bson.Unmarshal(data, &tmp); err != nil { + if err := bson.Unmarshal(data, &temporary); err != nil { return err } - switch tmp.Type { - case "jumbotron": - j := component.Jumbotron{} - - if err := bson.Unmarshal(tmp.Body, &j); err != nil { + switch temporary.Component.Type { + case component.ComponentTypeItem: + var item component.Item + if err := bson.Unmarshal(temporary.Body, &item); err != nil { return err } - tmp.Child.Body = j - case "featured": - j := component.Featured{} - if err := bson.Unmarshal(tmp.Body, &j); err != nil { + temporary.Element.Body = item + case component.ComponentTypeJumbotron: + var jumbotron component.Jumbotron + if err := bson.Unmarshal(temporary.Body, &jumbotron); err != nil { return err } - tmp.Child.Body = j - case "item": - j := component.Item{} - if err := bson.Unmarshal(tmp.Body, &j); err != nil { + temporary.Element.Body = jumbotron.Item + case component.ComponentTypeFeatured: + var featured component.Featured + if err := bson.Unmarshal(temporary.Body, &featured); err != nil { return err } - tmp.Child.Body = j + temporary.Element.Body = featured.Items default: return element.ErrUnSupportedComponent } - *e = ElementBson(tmp.Child) + *e = temporary.Element return nil } + +// ToBson converts an element to a BSON object. +func elementToBson(e *element.Element) (ElementBson, error) { + body, err := componentToBson(e.Body) + if err != nil { + return ElementBson{}, err + } + + bson := ElementBson{ + UUID: e.UUID, + Body: body, + Venues: e.Venues, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } + + return bson, nil +} + +// toElement converts a BSON object to an element. +func bsonToElement(b *ElementBson) (element.Element, error) { + body, err := bsonToComponent(b.Body) + if err != nil { + return element.Element{}, err + } + + return element.Element{ + UUID: b.UUID, + Body: body, + Venues: b.Venues, + CreatedAt: b.CreatedAt, + UpdatedAt: b.UpdatedAt, + }, nil +} + +// componentToBson converts a component to a BSON object. +func componentToBson(c element.Component) (any, error) { + var bson any + + switch c.Type() { + case component.ComponentTypeItem: + item := c.(component.Item) + bson = ItemBson{ + Type: item.Type(), + ContentUUID: item.ContentUUID, + ContentType: item.ContentType, + } + case component.ComponentTypeJumbotron: + jumbotron := c.(component.Jumbotron) + + item, err := componentToBson(jumbotron.Item) + if err != nil { + return nil, err + } + + bson = JumbotronBson{ + Type: jumbotron.Type(), + Item: item.(ItemBson), + } + case component.ComponentTypeFeatured: + featured := c.(component.Featured) + + main, err := componentToBson(featured.Main) + if err != nil { + return nil, err + } + + asideItems := make([]ItemBson, len(featured.Items())) + for i := range featured.Items() { + item, err := componentToBson(featured.Items()[i]) + if err != nil { + return nil, err + } + asideItems[i] = item.(ItemBson) + } + + bson = FeaturedBson{ + Type: featured.Type(), + Main: main.(ItemBson), + Aside: asideItems, + } + default: + return nil, element.ErrUnSupportedComponent + } + + return bson, nil +} + +// bsonToComponent converts a BSON object to a component. +func bsonToComponent(b any) (element.Component, error) { + var c element.Component + + switch b.(type) { + case ItemBson: + c = component.Item{ + ContentUUID: b.(ItemBson).ContentUUID, + ContentType: b.(ItemBson).ContentType, + } + case JumbotronBson: + item, err := bsonToComponent(b.(JumbotronBson).Item) + if err != nil { + return nil, err + } + + c = component.Jumbotron{ + Item: item.(component.Item), + } + case FeaturedBson: + main, err := bsonToComponent(b.(FeaturedBson).Main) + if err != nil { + return nil, err + } + + asideItems := make([]component.Item, len(b.(FeaturedBson).Aside)) + for i := range b.(FeaturedBson).Aside { + asideItem, err := bsonToComponent(b.(FeaturedBson).Aside[i]) + if err != nil { + return nil, err + } + asideItems[i] = asideItem.(component.Item) + } + + c = component.Featured{ + Main: main.(component.Item), + Aside: asideItems, + } + default: + return nil, element.ErrUnSupportedComponent + } + + return c, nil +} diff --git a/infrastructure/repository/mongodb/elements/repository.go b/infrastructure/repository/mongodb/elements/repository.go index c12b2e97..1d585c75 100644 --- a/infrastructure/repository/mongodb/elements/repository.go +++ b/infrastructure/repository/mongodb/elements/repository.go @@ -60,14 +60,13 @@ func (r *ElementsRepository) GetAll(offset uint, limit uint) ([]element.Element, if err := cur.Decode(&a); err != nil { return nil, err } - items = append(items, element.Element{ - UUID: a.UUID, - Type: a.Type, - Body: a.Body, - Venues: a.Venues, - CreatedAt: a.CreatedAt, - UpdatedAt: a.UpdatedAt, - }) + + e, err := bsonToElement(&a) + if err != nil { + return nil, err + } + + items = append(items, e) } if err := cur.Err(); err != nil { @@ -95,14 +94,13 @@ func (r *ElementsRepository) GetByVenues(venues []string) ([]element.Element, er if err := cur.Decode(&a); err != nil { return nil, err } - items = append(items, element.Element{ - UUID: a.UUID, - Type: a.Type, - Body: a.Body, - Venues: a.Venues, - CreatedAt: a.CreatedAt, - UpdatedAt: a.UpdatedAt, - }) + + e, err := bsonToElement(&a) + if err != nil { + return nil, err + } + + items = append(items, e) } if err := cur.Err(); err != nil { @@ -124,14 +122,7 @@ func (r *ElementsRepository) GetOne(UUID string) (element.Element, error) { return element.Element{}, err } - return element.Element{ - UUID: a.UUID, - Type: a.Type, - Body: a.Body, - Venues: a.Venues, - CreatedAt: a.CreatedAt, - UpdatedAt: a.UpdatedAt, - }, nil + return bsonToElement(&a) } func (r *ElementsRepository) Count() (uint, error) { @@ -163,17 +154,14 @@ func (r *ElementsRepository) Save(a *element.Element) (string, error) { a.CreatedAt = now } - update := ElementBson{ - UUID: a.UUID, - Type: a.Type, - Body: a.Body, - Venues: a.Venues, - CreatedAt: a.CreatedAt, - UpdatedAt: now, + update, err := elementToBson(a) + if err != nil { + return "", err } + update.UpdatedAt = now upsert := true - _, err := r.collection.UpdateOne( + _, err = r.collection.UpdateOne( ctx, bson.D{{Key: "_id", Value: a.UUID}}, bson.M{"$set": update}, diff --git a/presentation/http/api/article/show_test.go b/presentation/http/api/article/show_test.go index 0667892b..b035a346 100644 --- a/presentation/http/api/article/show_test.go +++ b/presentation/http/api/article/show_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" getarticle "github.com/khanzadimahdi/testproject/application/article/getArticle" + "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" "github.com/khanzadimahdi/testproject/domain/author" @@ -54,7 +55,8 @@ func TestShowHandler(t *testing.T) { elementsRepository.On("GetByVenues", []string{fmt.Sprintf("articles/%s", a.UUID)}).Once().Return(nil, nil) defer elementsRepository.AssertExpectations(t) - handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &elementsRepository)) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, elementRetriever)) request := httptest.NewRequest(http.MethodGet, "/", nil) request.SetPathValue("uuid", a.UUID) @@ -86,7 +88,8 @@ func TestShowHandler(t *testing.T) { articlesRepository.On("GetOnePublished", a.UUID).Once().Return(article.Article{}, domain.ErrNotExists) defer articlesRepository.AssertExpectations(t) - handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &elementsRepository)) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, elementRetriever)) request := httptest.NewRequest(http.MethodGet, "/", nil) request.SetPathValue("uuid", a.UUID) @@ -118,7 +121,8 @@ func TestShowHandler(t *testing.T) { articlesRepository.On("GetOnePublished", a.UUID).Once().Return(article.Article{}, errors.New("an error has happened")) defer articlesRepository.AssertExpectations(t) - handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &elementsRepository)) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, elementRetriever)) request := httptest.NewRequest(http.MethodGet, "/", nil) request.SetPathValue("uuid", a.UUID) diff --git a/presentation/http/api/home/home_test.go b/presentation/http/api/home/home_test.go index 80e2f44e..f4e8398b 100644 --- a/presentation/http/api/home/home_test.go +++ b/presentation/http/api/home/home_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/application/home" "github.com/khanzadimahdi/testproject/domain/article" "github.com/khanzadimahdi/testproject/domain/author" @@ -82,7 +83,8 @@ func TestHomeHandler(t *testing.T) { elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(nil, nil) defer elementsRepository.AssertExpectations(t) - useCase := home.NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + useCase := home.NewUseCase(&articlesRepository, elementRetriever) handler := NewHomeHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -114,7 +116,8 @@ func TestHomeHandler(t *testing.T) { elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(nil, nil) defer elementsRepository.AssertExpectations(t) - useCase := home.NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + useCase := home.NewUseCase(&articlesRepository, elementRetriever) handler := NewHomeHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -139,7 +142,8 @@ func TestHomeHandler(t *testing.T) { articlesRepository.On("GetMostViewed", uint(4)).Once().Return(nil, errors.New("an error has happened")) defer articlesRepository.AssertExpectations(t) - useCase := home.NewUseCase(&articlesRepository, &elementsRepository) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository) + useCase := home.NewUseCase(&articlesRepository, elementRetriever) handler := NewHomeHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil)