Skip to content

Commit d043495

Browse files
author
Arkadius Jonczek
committed
feat: first working version
1 parent 5f95f9d commit d043495

17 files changed

+825
-2
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@
1919

2020
# Go workspace file
2121
go.work
22+
23+
# IntelliJ
24+
.idea

README.md

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,33 @@
1-
# go-rest-protobuf-provalidate-demo
2-
Go + REST API + Protobuf + Protovalidate (DEMO)
1+
# go-rest-api-protobuf-protovalidate-demo
2+
3+
This demo is about to demonstrate how to use protobuf and protovalidate with REST API written in go.
4+
5+
## Requirements
6+
7+
To generate the go files for the protobuf definitions you need `buf`.
8+
9+
## Usage
10+
11+
### Protobuf
12+
13+
To generate the go files for the protobuf definitions run:
14+
15+
```shell
16+
buf generate
17+
```
18+
19+
### Server
20+
21+
Start http server with:
22+
23+
```shell
24+
go run cmd/server/main.go
25+
```
26+
27+
### Client
28+
29+
Then request the server with:
30+
31+
```shell
32+
curl localhost:8080/customer
33+
```

buf.gen.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
version: v1
2+
managed:
3+
enabled: true
4+
plugins:
5+
- plugin: go
6+
out: pkg/proto
7+
opt:
8+
- paths=source_relative
9+
strategy: directory

buf.work.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
version: v1
3+
directories:
4+
- proto

cmd/server/main.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import (
4+
"github.com/arkadiusjonczek/go-rest-api-protobuf-protovalidate-demo.git/internal/pkg/app"
5+
v1 "github.com/arkadiusjonczek/go-rest-api-protobuf-protovalidate-demo.git/pkg/proto/demo/v1"
6+
"github.com/bufbuild/protovalidate-go"
7+
"log"
8+
"net/http"
9+
)
10+
11+
const (
12+
Addr = ":8080"
13+
)
14+
15+
func main() {
16+
log.Println("Application started")
17+
18+
protovalidate, err := protovalidate.New()
19+
if err != nil {
20+
log.Fatal(err)
21+
}
22+
23+
decoder := app.NewCustomerDecoder(protovalidate)
24+
store := app.NewInMemoryStore[v1.Customer]()
25+
handler := app.NewGenericCrudHandler[v1.Customer](decoder, store)
26+
router := app.NewCrudRouter[v1.Customer](handler)
27+
28+
if err := http.ListenAndServe(Addr, router); err != nil {
29+
log.Fatal(err)
30+
}
31+
32+
log.Println("Application stopped")
33+
}

go.mod

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module github.com/arkadiusjonczek/go-rest-api-protobuf-protovalidate-demo.git
2+
3+
go 1.21.5
4+
5+
require (
6+
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1
7+
github.com/bufbuild/protovalidate-go v0.6.0
8+
github.com/gorilla/mux v1.8.1
9+
google.golang.org/protobuf v1.33.0
10+
)
11+
12+
require (
13+
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
14+
github.com/google/cel-go v0.20.0 // indirect
15+
github.com/stoewer/go-strcase v1.3.0 // indirect
16+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
17+
golang.org/x/text v0.14.0 // indirect
18+
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb // indirect
19+
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect
20+
)

go.sum

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1 h1:0nWhrRcnkgw1kwJ7xibIO8bqfOA7pBzBjGCDBxIHch8=
2+
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1/go.mod h1:Tgn5bgL220vkFOI0KPStlcClPeOJzAv4uT+V8JXGUnw=
3+
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
4+
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
5+
github.com/bufbuild/protovalidate-go v0.6.0 h1:Jgs1kFuZ2LHvvdj8SpCLA1W/+pXS8QSM3F/E2l3InPY=
6+
github.com/bufbuild/protovalidate-go v0.6.0/go.mod h1:1LamgoYHZ2NdIQH0XGczGTc6Z8YrTHjcJVmiBaar4t4=
7+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10+
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
11+
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
12+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
13+
github.com/google/cel-go v0.20.0 h1:h4n6DOCppEMpWERzllyNkntl7JrDyxoE543KWS6BLpc=
14+
github.com/google/cel-go v0.20.0/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
15+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
16+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
17+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
18+
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
19+
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
20+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
21+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
22+
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
23+
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
24+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
25+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
26+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
27+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
28+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
29+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
30+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
31+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
32+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
33+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
34+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
35+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
36+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
37+
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI=
38+
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
39+
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb h1:Isk1sSH7bovx8Rti2wZK0UZF6oraBDK74uoyLEEVFN0=
40+
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
41+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
42+
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
43+
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
44+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
45+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
46+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
47+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/pkg/app/decoder.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package app
2+
3+
import (
4+
"github.com/bufbuild/protovalidate-go"
5+
"google.golang.org/protobuf/encoding/protojson"
6+
7+
v1 "github.com/arkadiusjonczek/go-rest-api-protobuf-protovalidate-demo.git/pkg/proto/demo/v1"
8+
)
9+
10+
type Decoder[E Entity] interface {
11+
Decode(body []byte) (*E, error)
12+
}
13+
14+
var _ Decoder[v1.Customer] = (*CustomerDecoder)(nil)
15+
16+
type CustomerDecoder struct {
17+
unmarshalOptions *protojson.UnmarshalOptions
18+
protovalidate *protovalidate.Validator
19+
}
20+
21+
func NewCustomerDecoder(protovalidate *protovalidate.Validator) *CustomerDecoder {
22+
return &CustomerDecoder{
23+
unmarshalOptions: &protojson.UnmarshalOptions{
24+
AllowPartial: true,
25+
DiscardUnknown: true,
26+
},
27+
protovalidate: protovalidate,
28+
}
29+
}
30+
31+
func (d *CustomerDecoder) Decode(body []byte) (*v1.Customer, error) {
32+
entry := &v1.Customer{}
33+
34+
err := d.unmarshalOptions.Unmarshal(body, entry)
35+
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
err = d.protovalidate.Validate(entry)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
return entry, nil
46+
}

internal/pkg/app/handler.go

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package app
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
"reflect"
10+
"strconv"
11+
12+
"github.com/gorilla/mux"
13+
14+
v1 "github.com/arkadiusjonczek/go-rest-api-protobuf-protovalidate-demo.git/pkg/proto/demo/v1"
15+
)
16+
17+
type CrudHandler interface {
18+
Create(w http.ResponseWriter, r *http.Request)
19+
Read(w http.ResponseWriter, r *http.Request)
20+
ReadAll(w http.ResponseWriter, r *http.Request)
21+
Update(w http.ResponseWriter, r *http.Request)
22+
Delete(w http.ResponseWriter, r *http.Request)
23+
}
24+
25+
type Entity interface {
26+
v1.Customer | v1.Customer2
27+
}
28+
29+
var _ CrudHandler = (*GenericCrudHandler[v1.Customer])(nil)
30+
31+
type GenericCrudHandler[E Entity] struct {
32+
decoder Decoder[E]
33+
store Store[E]
34+
}
35+
36+
func NewGenericCrudHandler[E Entity](decoder Decoder[E], store Store[E]) *GenericCrudHandler[E] {
37+
return &GenericCrudHandler[E]{
38+
decoder: decoder,
39+
store: store,
40+
}
41+
}
42+
43+
func (h *GenericCrudHandler[E]) Create(w http.ResponseWriter, r *http.Request) {
44+
log.Printf("Got request: %v", r)
45+
46+
body, _ := io.ReadAll(r.Body)
47+
48+
log.Printf("Got body: %s", body)
49+
50+
// Unmarshal
51+
52+
entry, err := h.decoder.Decode(body)
53+
if err != nil {
54+
w.WriteHeader(http.StatusBadRequest)
55+
fmt.Fprintf(w, "Error: %s", err)
56+
return
57+
}
58+
59+
log.Printf("Got entry of type %s: %#v", reflect.TypeOf(entry), entry)
60+
61+
// Add to store
62+
63+
id, err := h.store.Add(entry)
64+
if err != nil {
65+
w.WriteHeader(http.StatusBadRequest)
66+
fmt.Fprintf(w, "Error: %s", err)
67+
return
68+
}
69+
70+
w.WriteHeader(http.StatusOK)
71+
fmt.Fprintf(w, "Created entry with id: %d", id)
72+
}
73+
74+
func (h *GenericCrudHandler[E]) Read(w http.ResponseWriter, r *http.Request) {
75+
log.Printf("Got request: %v", r)
76+
77+
vars := mux.Vars(r)
78+
idString := vars["id"]
79+
80+
id, _ := strconv.Atoi(idString)
81+
82+
// Get from store
83+
84+
entry, err := h.store.Get(id)
85+
if err != nil {
86+
w.WriteHeader(http.StatusBadRequest)
87+
fmt.Fprintf(w, "Error: %s", err)
88+
return
89+
}
90+
91+
w.WriteHeader(http.StatusOK)
92+
json.NewEncoder(w).Encode(entry)
93+
}
94+
95+
func (h *GenericCrudHandler[E]) ReadAll(w http.ResponseWriter, r *http.Request) {
96+
log.Printf("Got request: %v", r)
97+
98+
// Get from store
99+
100+
entries, err := h.store.GetAll()
101+
if err != nil {
102+
w.WriteHeader(http.StatusBadRequest)
103+
fmt.Fprintf(w, "Error: %s", err)
104+
return
105+
}
106+
107+
w.WriteHeader(http.StatusOK)
108+
json.NewEncoder(w).Encode(entries)
109+
}
110+
111+
func (h *GenericCrudHandler[E]) Update(w http.ResponseWriter, r *http.Request) {
112+
log.Printf("Got request: %v", r)
113+
114+
vars := mux.Vars(r)
115+
idString := vars["id"]
116+
117+
id, _ := strconv.Atoi(idString)
118+
119+
body, _ := io.ReadAll(r.Body)
120+
121+
log.Printf("Got body: %s", body)
122+
123+
// Unmarshal
124+
125+
entry, err := h.decoder.Decode(body)
126+
if err != nil {
127+
w.WriteHeader(http.StatusBadRequest)
128+
fmt.Fprintf(w, "Error: %s", err)
129+
return
130+
}
131+
132+
log.Printf("Got entry of type %s: %#v", reflect.TypeOf(entry), entry)
133+
134+
// Update in store
135+
136+
err = h.store.Update(id, entry)
137+
if err != nil {
138+
w.WriteHeader(http.StatusBadRequest)
139+
fmt.Fprintf(w, "Error: %s", err)
140+
return
141+
}
142+
143+
w.WriteHeader(http.StatusOK)
144+
fmt.Fprintf(w, "Updated entry with id: %d", id)
145+
}
146+
func (h *GenericCrudHandler[E]) Delete(w http.ResponseWriter, r *http.Request) {
147+
log.Printf("Got request: %v", r)
148+
149+
vars := mux.Vars(r)
150+
idString := vars["id"]
151+
152+
id, _ := strconv.Atoi(idString)
153+
154+
// Delete in store
155+
156+
err := h.store.Delete(id)
157+
if err != nil {
158+
w.WriteHeader(http.StatusBadRequest)
159+
fmt.Fprintf(w, "Error: %s", err)
160+
return
161+
}
162+
163+
w.WriteHeader(http.StatusOK)
164+
fmt.Fprintf(w, "Deleted entry with id: %d", id)
165+
}

0 commit comments

Comments
 (0)