Skip to content

Commit

Permalink
feat: add request decode func (#1)
Browse files Browse the repository at this point in the history
Add the go request decode function that supports json unmarshalling, string query params and string headers
  • Loading branch information
Jesse0Michael authored Oct 22, 2021
1 parent d58c0a5 commit 8244e8d
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage.out
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
test:
go test -cover ./...
golangci-lint run ./...

test-coverage:
go test -coverpkg ./... -coverprofile coverage.out ./... && go tool cover -html=coverage.out
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#GO Request

Go Request is a package built to decode `*http.Request` data into a custom struct. Using struct tags we are about to pull HTTP request data from the request's:
- Body
- Header
- Query
- Path

Example using the following request:
```bash
curl --location --request POST 'www.example.com/user/adam?game=go' \
--header 'X-DELAY: 60' \
--header 'Content-Type: application/json' \
--data-raw '{
"state": "idle"
}'
```


```go
import ("github.com/jesse0michael/go-request")

type MyRequest struct {
Name string `path:"name"`
Game string `query:"game"`
State string `json:"state"`
Delay int64 `header:"X-DELAY"`
}


func(w http.ResponseWriter, r *http.Request) {
var req MyRequest

err := request.Decode(r, &req)
if err != nil {
w.WriteHeader(400)
}

fmt.Println(req)
}
```

```
```
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/jesse0michael/go-request

go 1.17

require github.com/gorilla/mux v1.8.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
84 changes: 84 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package request

import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
)

func Decode(r *http.Request, data interface{}) error {
err := decodeBody(r, data)
if err != nil {
return err
}

err = decodeRequest(r, data)
if err != nil {
return err
}

return nil
}

func decodeRequest(r *http.Request, data interface{}) error {
typ := reflect.TypeOf(data)
if typ == nil {
return fmt.Errorf("invalid decode type: nil")
}
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
if typ.Kind() != reflect.Struct {
return fmt.Errorf("invalid decode type: %v", typ.Kind())
}

val := reflect.ValueOf(data).Elem()
for i := 0; i < typ.NumField(); i++ {
err := decodeField(r, typ.Field(i), val.Field(i))
if err != nil {
return err
}
}
return nil
}

func decodeField(r *http.Request, typ reflect.StructField, val reflect.Value) error {
query := r.URL.Query()
queryTag := typ.Tag.Get("query")
if queryTag != "" {
if query.Has(queryTag) {
v := query.Get(queryTag)
val.Set(reflect.ValueOf(v))
}
}

headerTag := typ.Tag.Get("header")
if headerTag != "" {
if r.Header.Get(headerTag) != "" {
v := r.Header.Get(headerTag)
val.Set(reflect.ValueOf(v))
}
}

return nil
}

func decodeBody(r *http.Request, data interface{}) error {
b, err := io.ReadAll(r.Body)
if err != nil {
return err
}
defer r.Body.Close()

switch r.Header.Get("Content-Type") {
case "application/json":
err := json.Unmarshal(b, &data)
if err != nil {
return err
}
}

return nil
}
69 changes: 69 additions & 0 deletions request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package request

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"

"github.com/gorilla/mux"
)

type MyRequest struct {
Name string `path:"name"`
Game string `query:"game"`
State string `json:"state"`
Delay string `header:"X-DELAY"`
}

func ExampleDecode() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req MyRequest
err := Decode(r, &req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
}

fmt.Printf("%+v\n", req)
}))
defer ts.Close()

body := `{"state":"idle"}`
req, _ := http.NewRequest(http.MethodPost, ts.URL+"?game=go", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-DELAY", "60")
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("request failed")
}
if resp.StatusCode == http.StatusBadRequest {
fmt.Println("decode failed")
}
// Output:
// {Name: Game:go State:idle Delay:60}
}

func ExampleDecode_mux() {
r := mux.NewRouter()
r.Handle("/test/{name}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req MyRequest
err := Decode(r, &req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
}

fmt.Printf("%+v\n", req)
}))

body := `{"state":"idle"}`
req, _ := http.NewRequest(http.MethodPost, "http://www.example.com/test/user?game=go", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-DELAY", "60")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code == http.StatusBadRequest {
fmt.Println("decode failed")
}
// Output:
// {Name: Game:go State:idle Delay:60}
}

0 comments on commit 8244e8d

Please sign in to comment.