Skip to content

Commit 8045369

Browse files
story(issue-9): rest add multipart form data handling (#10)
* refactor(issue-9): rename NewEndpoint to NewProtoEndpoint to be more specific * refactor(issue-9): move towards a more composition based approach for lifting handlers * deps(issue-9): bump github.com/z5labs/bedrock to v0.12.0 * feat(issue-9): implement multipart form data handler helpers * feat(issue-9): add multipart form data endpoint examples * chore(docs): updated coverage badge. --------- Co-authored-by: GitHub Action <[email protected]>
1 parent 9d0b9ad commit 8045369

15 files changed

+525
-56
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Go Reference](https://pkg.go.dev/badge/github.com/z5labs/humus.svg)](https://pkg.go.dev/github.com/z5labs/humus)
44
[![Go Report Card](https://goreportcard.com/badge/github.com/z5labs/humus)](https://goreportcard.com/report/github.com/z5labs/humus)
5-
![Coverage](https://img.shields.io/badge/Coverage-71.0%25-brightgreen)
5+
![Coverage](https://img.shields.io/badge/Coverage-55.3%25-yellow)
66
[![build](https://github.com/z5labs/humus/actions/workflows/build.yaml/badge.svg)](https://github.com/z5labs/humus/actions/workflows/build.yaml)
77

88
**humus one stop shop framework for all Z5Labs projects in Go.**

example/internal/petstore/petstore.go

+37-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package petstore
77

88
import (
99
"context"
10+
"io"
1011
"sync"
1112

1213
"github.com/z5labs/humus/example/internal/petstorepb"
@@ -15,18 +16,20 @@ import (
1516
)
1617

1718
type InMemory struct {
18-
mu sync.Mutex
19-
pets map[int64]*petstorepb.Pet
19+
mu sync.Mutex
20+
pets map[int64]*petstorepb.Pet
21+
images map[int64][]byte
2022
}
2123

2224
func NewInMemory() *InMemory {
2325
return &InMemory{
24-
pets: make(map[int64]*petstorepb.Pet),
26+
pets: make(map[int64]*petstorepb.Pet),
27+
images: make(map[int64][]byte),
2528
}
2629
}
2730

2831
func (s *InMemory) Add(ctx context.Context, pet *petstorepb.Pet) {
29-
_, span := otel.Tracer("pet").Start(ctx, "Store.Add")
32+
_, span := otel.Tracer("petstore").Start(ctx, "InMemory.Add")
3033
defer span.End()
3134

3235
s.mu.Lock()
@@ -36,7 +39,7 @@ func (s *InMemory) Add(ctx context.Context, pet *petstorepb.Pet) {
3639
}
3740

3841
func (s *InMemory) Get(ctx context.Context, id int64) (*petstorepb.Pet, bool) {
39-
_, span := otel.Tracer("pet").Start(ctx, "Store.Get")
42+
_, span := otel.Tracer("petstore").Start(ctx, "InMemory.Get")
4043
defer span.End()
4144

4245
s.mu.Lock()
@@ -47,7 +50,7 @@ func (s *InMemory) Get(ctx context.Context, id int64) (*petstorepb.Pet, bool) {
4750
}
4851

4952
func (s *InMemory) Delete(ctx context.Context, id int64) {
50-
_, span := otel.Tracer("pet").Start(ctx, "Store.Delete")
53+
_, span := otel.Tracer("petstore").Start(ctx, "InMemory.Delete")
5154
defer span.End()
5255

5356
s.mu.Lock()
@@ -57,7 +60,7 @@ func (s *InMemory) Delete(ctx context.Context, id int64) {
5760
}
5861

5962
func (s *InMemory) Pets(ctx context.Context) []*petstorepb.Pet {
60-
_, span := otel.Tracer("pet").Start(ctx, "Store.Pets")
63+
_, span := otel.Tracer("petstore").Start(ctx, "InMemory.Pets")
6164
defer span.End()
6265

6366
s.mu.Lock()
@@ -69,3 +72,30 @@ func (s *InMemory) Pets(ctx context.Context) []*petstorepb.Pet {
6972
}
7073
return pets
7174
}
75+
76+
func (s *InMemory) IndexImage(ctx context.Context, pet *petstorepb.Pet, r io.Reader) error {
77+
_, span := otel.Tracer("petstore").Start(ctx, "InMemory.IndexImage")
78+
defer span.End()
79+
80+
b, err := io.ReadAll(r)
81+
if err != nil {
82+
return err
83+
}
84+
85+
s.mu.Lock()
86+
defer s.mu.Unlock()
87+
88+
s.images[pet.Id] = b
89+
return nil
90+
}
91+
92+
func (s *InMemory) GetImage(ctx context.Context, id int64) ([]byte, bool) {
93+
_, span := otel.Tracer("petstore").Start(ctx, "InMemory.GetImage")
94+
defer span.End()
95+
96+
s.mu.Lock()
97+
defer s.mu.Unlock()
98+
99+
img, exists := s.images[id]
100+
return img, exists
101+
}

example/rest-petstore/app/app.go

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ func Init(ctx context.Context, cfg Config) (humus.App, error) {
3030
rest.RegisterEndpoint(endpoint.DeletePet(store)),
3131
rest.RegisterEndpoint(endpoint.FindPetByID(store)),
3232
rest.RegisterEndpoint(endpoint.ListPets(store)),
33+
rest.RegisterEndpoint(endpoint.Upload(store, store)),
34+
rest.RegisterEndpoint(endpoint.Download(store, store)),
3335
)
3436

3537
return app, nil

example/rest-petstore/endpoint/add_pet.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ func AddPet(store AddStore) rest.Endpoint {
3232
return rest.NewEndpoint(
3333
http.MethodPost,
3434
"/pet",
35-
h,
35+
rest.ConsumesProto(
36+
rest.ProducesProto(h),
37+
),
3638
)
3739
}
3840

example/rest-petstore/endpoint/delete_pet.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ func DeletePet(store DeleteStore) rest.Endpoint {
3232
return rest.NewEndpoint(
3333
http.MethodDelete,
3434
"/pet/{id}",
35-
h,
35+
rest.ConsumesProto(
36+
rest.ProducesProto(h),
37+
),
3638
rest.PathParams(
3739
rest.PathParam{
3840
Name: "id",
+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) 2024 Z5Labs and Contributors
2+
//
3+
// This software is released under the MIT License.
4+
// https://opensource.org/licenses/MIT
5+
6+
package endpoint
7+
8+
import (
9+
"bytes"
10+
"context"
11+
"io"
12+
"net/http"
13+
"net/textproto"
14+
"strconv"
15+
"strings"
16+
17+
"github.com/z5labs/humus/example/internal/petstorepb"
18+
19+
"github.com/swaggest/openapi-go/openapi3"
20+
"github.com/z5labs/bedrock/pkg/ptr"
21+
"github.com/z5labs/humus/rest"
22+
"go.opentelemetry.io/otel"
23+
"google.golang.org/protobuf/proto"
24+
"google.golang.org/protobuf/types/known/emptypb"
25+
)
26+
27+
type GetImageIndex interface {
28+
GetImage(context.Context, int64) ([]byte, bool)
29+
}
30+
31+
type downloadHandler struct {
32+
store PetByIdStore
33+
images GetImageIndex
34+
}
35+
36+
func Download(store PetByIdStore, images GetImageIndex) rest.Endpoint {
37+
h := &downloadHandler{
38+
store: store,
39+
images: images,
40+
}
41+
42+
return rest.NewEndpoint(
43+
http.MethodGet,
44+
"/download/{id}",
45+
rest.ConsumesProto(
46+
rest.ProducesMultipartFormData(h),
47+
),
48+
rest.PathParams(
49+
rest.PathParam{
50+
Name: "id",
51+
Required: true,
52+
},
53+
),
54+
rest.Returns(http.StatusBadRequest),
55+
)
56+
}
57+
58+
type DownloadResponse struct {
59+
pet *petstorepb.Pet
60+
imageContent io.Reader
61+
}
62+
63+
func (DownloadResponse) OpenApiV3Schema() (*openapi3.Schema, error) {
64+
var req rest.ProtoRequest[petstorepb.Pet, *petstorepb.Pet]
65+
metadataSchema, err := req.OpenApiV3Schema()
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
var schema openapi3.Schema
71+
schema.WithType(openapi3.SchemaTypeObject)
72+
schema.WithProperties(map[string]openapi3.SchemaOrRef{
73+
"pet": {
74+
Schema: metadataSchema,
75+
},
76+
"image": {
77+
Schema: &openapi3.Schema{
78+
Type: ptr.Ref(openapi3.SchemaTypeString),
79+
Format: ptr.Ref("binary"),
80+
},
81+
},
82+
})
83+
return &schema, nil
84+
}
85+
86+
func (resp *DownloadResponse) WriteParts(mw rest.MultipartWriter) error {
87+
b, err := proto.Marshal(resp.pet)
88+
if err != nil {
89+
return err
90+
}
91+
92+
part, err := mw.CreatePart(textproto.MIMEHeader{})
93+
if err != nil {
94+
return err
95+
}
96+
97+
_, err = io.Copy(part, bytes.NewReader(b))
98+
if err != nil {
99+
return err
100+
}
101+
102+
part, err = mw.CreatePart(textproto.MIMEHeader{})
103+
if err != nil {
104+
return err
105+
}
106+
_, err = io.Copy(part, resp.imageContent)
107+
return err
108+
}
109+
110+
func (h *downloadHandler) Handle(ctx context.Context, req *emptypb.Empty) (*DownloadResponse, error) {
111+
spanCtx, span := otel.Tracer("endpoint").Start(ctx, "downloadHandler.Handle")
112+
defer span.End()
113+
114+
pathId := rest.PathValue(ctx, "id")
115+
pathId = strings.TrimSpace(pathId)
116+
if len(pathId) == 0 {
117+
return nil, rest.Error(http.StatusBadRequest, "missing pet id")
118+
}
119+
120+
id, err := strconv.ParseInt(pathId, 10, 64)
121+
if err != nil {
122+
span.RecordError(err)
123+
return nil, rest.Error(http.StatusBadRequest, "pet id must be an integer")
124+
}
125+
126+
pet, found := h.store.Get(spanCtx, id)
127+
if !found {
128+
return nil, rest.Error(http.StatusNotFound, "")
129+
}
130+
131+
resp := &DownloadResponse{
132+
pet: pet,
133+
imageContent: nil,
134+
}
135+
return resp, nil
136+
}

example/rest-petstore/endpoint/find_pet.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ func FindPetByID(store PetByIdStore) rest.Endpoint {
3434
return rest.NewEndpoint(
3535
http.MethodGet,
3636
"/pet/{id}",
37-
h,
37+
rest.ConsumesProto(
38+
rest.ProducesProto(h),
39+
),
3840
rest.PathParams(
3941
rest.PathParam{
4042
Name: "id",

example/rest-petstore/endpoint/list_pets.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ func ListPets(store ListStore) rest.Endpoint {
3232
return rest.NewEndpoint(
3333
http.MethodGet,
3434
"/pets",
35-
h,
35+
rest.ConsumesProto(
36+
rest.ProducesProto(h),
37+
),
3638
)
3739
}
3840

0 commit comments

Comments
 (0)