Skip to content

Commit

Permalink
feat(go): models api
Browse files Browse the repository at this point in the history
  • Loading branch information
rot1024 committed Nov 24, 2023
1 parent 1fff20b commit 1ab17e1
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 172 deletions.
75 changes: 75 additions & 0 deletions go/cms.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ var ErrNotFound = errors.New("not found")
type Interface interface {
GetModel(ctx context.Context, modelID string) (*Model, error)
GetModelByKey(ctx context.Context, proejctID, modelID string) (*Model, error)
GetModelsPartially(ctx context.Context, projectIDOrAlias string, page, perPage int) (*Models, error)
GetModelsInParallel(ctx context.Context, modelID string, limit int) (*Models, error)
GetModels(ctx context.Context, projectIDOrAlias string) (*Models, error)
GetItem(ctx context.Context, itemID string, asset bool) (*Item, error)
GetItemsPartially(ctx context.Context, modelID string, page, perPage int, asset bool) (*Items, error)
GetItems(ctx context.Context, modelID string, asset bool) (*Items, error)
Expand Down Expand Up @@ -109,6 +112,78 @@ func (c *CMS) GetModelByKey(ctx context.Context, projectKey, modelKey string) (*
return model, nil
}

func (c *CMS) GetModelsPartially(ctx context.Context, projectIDOrAlias string, page, perPage int) (*Models, error) {
b, err := c.send(ctx, http.MethodGet, []string{"api", "projects", projectIDOrAlias, "models"}, "", paginationQuery(page, perPage))
if err != nil {
return nil, fmt.Errorf("failed to get models: %w", err)
}
defer func() { _ = b.Close() }()

models := &Models{}
if err := json.NewDecoder(b).Decode(models); err != nil {
return nil, fmt.Errorf("failed to parse models: %w", err)
}

return models, nil
}

func (c *CMS) GetModels(ctx context.Context, projectIDOrAlias string) (models *Models, _ error) {
const perPage = 100
for p := 1; ; p++ {
i, err := c.GetModelsPartially(ctx, projectIDOrAlias, p, perPage)
if err != nil {
return nil, err
}

if i == nil || i.PerPage <= 0 {
return nil, fmt.Errorf("invalid response: %#v", i)
}

if models == nil {
models = i
} else {
models.Models = append(models.Models, i.Models...)
}

allPageCount := i.TotalCount / i.PerPage
if i.Page >= allPageCount {
break
}
}

return models, nil
}

func (c *CMS) GetModelsInParallel(ctx context.Context, modelID string, limit int) (*Models, error) {
const perPage = 100
if limit <= 0 {
limit = 5
}

res, err := parallel(limit, func(p int) (*Models, int, error) {
r, err := c.GetModelsPartially(ctx, modelID, p+1, perPage)
if err != nil || r == nil {
if r == nil || r.PerPage == 0 {
err = fmt.Errorf("invalid response: %#v", r)
}
return nil, 0, err
}
return r, int(math.Ceil(float64(r.TotalCount) / float64(perPage))), nil
})
if err != nil {
return nil, err
}

res2 := res[0]
res2.Models = lo.FlatMap(res, func(i *Models, _ int) []Model {
if i == nil {
return nil
}
return i.Models
})
return res2, nil
}

func (c *CMS) GetItem(ctx context.Context, itemID string, asset bool) (*Item, error) {
b, err := c.send(ctx, http.MethodGet, []string{"api", "items", itemID}, "", c.assetParam(asset))
if err != nil {
Expand Down
209 changes: 209 additions & 0 deletions go/cms_mock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package cms

import (
"io"
"net/http"
"strconv"
"strings"
"testing"
"time"

"github.com/jarcoal/httpmock"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)

func mockCMS(t *testing.T, host, token string) func(string) int {
t.Helper()

checkHeader := func(next func(req *http.Request) (any, error)) func(req *http.Request) (*http.Response, error) {
return func(req *http.Request) (*http.Response, error) {
if t := parseToken(req); t != token {
return httpmock.NewJsonResponse(http.StatusUnauthorized, "unauthorized")
}

if req.Method != "GET" {
if c := req.Header.Get("Content-Type"); c != "application/json" && !strings.HasPrefix(c, "multipart/form-data") {
return httpmock.NewJsonResponse(http.StatusUnsupportedMediaType, "unsupported media type")
}
}

res, err := next(req)
if err != nil {
return nil, err
}
return httpmock.NewJsonResponse(http.StatusOK, res)
}
}

httpmock.RegisterResponder("GET", host+"/api/items/a", checkHeader(func(r *http.Request) (any, error) {
return map[string]any{
"id": "a",
"fields": []map[string]string{{"id": "f", "type": "text", "value": "t"}},
}, nil
}))

modelResponder := checkHeader(func(r *http.Request) (any, error) {
return map[string]any{
"id": "idid",
"key": "mmm",
"lastModified": time.Date(2023, time.April, 1, 1, 0, 0, 0, time.UTC),
}, nil
})

modelsResponder := checkHeader(func(r *http.Request) (any, error) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
perPage, _ := strconv.Atoi(r.URL.Query().Get("perPage"))
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 50
}
max := page * perPage
if max > len(testModels) {
max = len(testModels)
}

return map[string]any{
"models": testModels[(page-1)*perPage : max],
"page": page,
"perPage": perPage,
"totalCount": len(testModels),
}, nil
})

itemsResponder := checkHeader(func(r *http.Request) (any, error) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
perPage, _ := strconv.Atoi(r.URL.Query().Get("perPage"))
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 50
}
max := page * perPage
if max > len(testItems) {
max = len(testItems)
}

return map[string]any{
"items": testItems[(page-1)*perPage : max],
"page": page,
"perPage": perPage,
"totalCount": len(testItems),
}, nil
})

emptyItemsResponder := checkHeader(func(r *http.Request) (any, error) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
perPage, _ := strconv.Atoi(r.URL.Query().Get("perPage"))
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 50
}

return map[string]any{
"items": nil,
"page": page,
"perPage": perPage,
"totalCount": 0,
}, nil
})

httpmock.RegisterResponder("GET", host+"/api/projects/ppp/models/mmm", modelResponder)
httpmock.RegisterResponder("GET", host+"/api/models/mmm", modelResponder)
httpmock.RegisterResponder("GET", host+"/api/projects/ppp/models", modelsResponder)
httpmock.RegisterResponder("GET", host+"/api/projects/ppp/models/mmm/items", itemsResponder)
httpmock.RegisterResponder("GET", host+"/api/projects/ppp/models/empty/items", emptyItemsResponder)
httpmock.RegisterResponder("GET", host+"/api/models/mmm/items", itemsResponder)
httpmock.RegisterResponder("GET", host+"/api/models/empty/items", emptyItemsResponder)

httpmock.RegisterResponder("POST", host+"/api/projects/ppp/models/mmm/items", checkHeader(func(r *http.Request) (any, error) {
return map[string]any{
"id": "a",
"fields": []map[string]string{{"id": "f", "type": "text", "value": "t"}},
}, nil
}))

httpmock.RegisterResponder("PATCH", host+"/api/items/a", checkHeader(func(r *http.Request) (any, error) {
return map[string]any{
"id": "a",
"fields": []map[string]string{{"id": "f", "type": "text", "value": "t"}},
}, nil
}))

httpmock.RegisterResponder("POST", host+"/api/models/a/items", checkHeader(func(r *http.Request) (any, error) {
return map[string]any{
"id": "a",
"fields": []map[string]string{{"id": "f", "type": "text", "value": "t"}},
}, nil
}))

httpmock.RegisterResponder("DELETE", host+"/api/items/a", checkHeader(func(r *http.Request) (any, error) {
return nil, nil
}))

httpmock.RegisterResponder("POST", host+"/api/projects/ppp/assets", checkHeader(func(r *http.Request) (any, error) {
if c := r.Header.Get("Content-Type"); strings.HasPrefix(c, "multipart/form-data") {
f, fh, err := r.FormFile("file")
if err != nil {
return nil, err
}
defer func() {
_ = f.Close()
}()
d, _ := io.ReadAll(f)
assert.Equal(t, "datadata", string(d))
assert.Equal(t, "file.txt", fh.Filename)
}

return map[string]any{
"id": "idid",
}, nil
}))

httpmock.RegisterResponder("POST", host+"/api/items/itit/comments", checkHeader(func(r *http.Request) (any, error) {
return map[string]any{}, nil
}))

httpmock.RegisterResponder("POST", host+"/api/assets/c/comments", checkHeader(func(r *http.Request) (any, error) {
return map[string]any{}, nil
}))

httpmock.RegisterResponder("GET", host+"/api/assets/a", checkHeader(func(r *http.Request) (any, error) {
return map[string]any{
"id": "a",
"url": "url",
}, nil
}))

return func(p string) int {
b, a, _ := strings.Cut(p, " ")
return httpmock.GetCallCountInfo()[b+" "+host+a]
}
}

func parseToken(r *http.Request) string {
aut := r.Header.Get("Authorization")
_, token, found := strings.Cut(aut, "Bearer ")
if !found {
return ""
}
return token
}

var testItems = lo.Map(lo.Range(500), func(i, _ int) Item {
return Item{
ID: strconv.Itoa(i),
Fields: []*Field{{ID: "f", Type: "text", Value: "t"}},
}
})

var testModels = lo.Map(lo.Range(500), func(i, _ int) Model {
return Model{
ID: strconv.Itoa(i),
}
})
Loading

0 comments on commit 1ab17e1

Please sign in to comment.