Skip to content

Commit

Permalink
feat: zhttp package now supports providing your own custom parsers (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
Oudwins authored Nov 8, 2024
1 parent 4c5b4bd commit e8a111f
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 37 deletions.
16 changes: 16 additions & 0 deletions docs/docs/packages/zhttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,19 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {
```

> **WARNING** The `zhttp` package does NOT currently support parsing into any data type that is NOT a struct.
## Complex Forms

If you need to parse complex forms or query params such as those parsed by packages like [qs](https://www.npmjs.com/package/qs), for example:

```js
assert.deepEqual(qs.parse("foo[bar]=baz"), {
foo: {
bar: "baz",
},
});
```

zhttp does not currently support this types of forms (see [issue #8](https://github.com/Oudwins/zog/issues/8)). However I suggest you try using the [form go package](https://github.com/go-playground/form) which supports this type of parsing. You can integrate the library with zhttp by overriding the `zhttp.Config.Parsers.Form` function.

> **WARNING**: This is depends on `DataProviders` which are not yet documented and may change in the future. I encourage you to avoid doing this unless you really need to.
4 changes: 2 additions & 2 deletions internals/DataProviders.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/Oudwins/zog/zconst"
)

type DpFactory = func() (DataProvider, *ZogErr)
type DpFactory = func() (DataProvider, ZogError)

// This is used for parsing structs & maps
type DataProvider interface {
Expand Down Expand Up @@ -62,7 +62,7 @@ func (e *EmptyDataProvider) GetUnderlying() any {
return e.Underlying
}

func TryNewAnyDataProvider(val any) (DataProvider, *ZogErr) {
func TryNewAnyDataProvider(val any) (DataProvider, ZogError) {
dp, ok := val.(DataProvider)
if ok {
return dp, nil
Expand Down
14 changes: 11 additions & 3 deletions internals/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ type ZogError interface {
// returns the data value that caused the error.
// if using Schema.Parse(data, dest) then this will be the value of data.
Value() any
// Sets the data value that caused the error.
// if using Schema.Parse(data, dest) then this will be the value of data.
SValue(any) ZogError
// Returns destination type. i.e The zconst.ZogType of the value that was validated.
// if Using Schema.Parse(data, dest) then this will be the type of dest.
Dtype() string
// Sets destination type. i.e The zconst.ZogType of the value that was validated.
// if Using Schema.Parse(data, dest) then this will be the type of dest.
SDType(zconst.ZogType) ZogError
// returns the params map for the error. Taken from the Test that caused the error. This may be nil if Test has no params.
Params() map[string]any
// Sets the params map for the error. Taken from the Test that caused the error. This may be nil if Test has no params.
SParams(map[string]any) ZogError
// returns the human readable, user-friendly message for the error. This is safe to expose to the user.
Message() string
// sets the human readable, user-friendly message for the error. This is safe to expose to the user.
Expand Down Expand Up @@ -52,7 +60,7 @@ func (e *ZogErr) Code() zconst.ZogErrCode {
func (e *ZogErr) Value() any {
return e.Value
}
func (e *ZogErr) SValue(v any) *ZogErr {
func (e *ZogErr) SValue(v any) ZogError {
e.Val = v
return e
}
Expand All @@ -61,7 +69,7 @@ func (e *ZogErr) SValue(v any) *ZogErr {
func (e *ZogErr) Dtype() string {
return e.Typ
}
func (e *ZogErr) SDType(t string) *ZogErr {
func (e *ZogErr) SDType(t zconst.ZogType) ZogError {
e.Typ = t
return e
}
Expand All @@ -70,7 +78,7 @@ func (e *ZogErr) Params() map[string]any {
return e.ParamsM
}

func (e *ZogErr) SParams(p map[string]any) *ZogErr {
func (e *ZogErr) SParams(p map[string]any) ZogError {
e.ParamsM = p
return e
}
Expand Down
5 changes: 3 additions & 2 deletions struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,13 @@ func (v *structProcessor) process(data any, dest any, path p.PathBuilder, ctx Pa

// 2.5 check for required & errors
if err != nil {
code := err.Code()
// This means its optional and we got an error coercing the value to a DataProvider, so we can ignore it
if v.required == nil && err.C == zconst.ErrCodeCoerce {
if v.required == nil && code == zconst.ErrCodeCoerce {
return
}
// This means that its required but we got an error coercing the value or a factory errored with required
if v.required != nil && (err.C == zconst.ErrCodeCoerce || err.C == zconst.ErrCodeRequired) {
if v.required != nil && (code == zconst.ErrCodeCoerce || code == zconst.ErrCodeRequired) {
ctx.NewError(path, Errors.FromTest(data, destType, v.required, ctx))
return
}
Expand Down
48 changes: 35 additions & 13 deletions zhttp/zhttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,42 @@ package zhttp
import (
"encoding/json"
"errors"
"io"
"net/http"
"net/url"

p "github.com/Oudwins/zog/internals"
"github.com/Oudwins/zog/zconst"
)

type ParserFunc = func(r *http.Request) (p.DataProvider, p.ZogError)

var Config = struct {
Parsers struct {
JSON ParserFunc
Form ParserFunc
Query ParserFunc
}
}{
Parsers: struct {
JSON ParserFunc
Form ParserFunc
Query ParserFunc
}{
JSON: parseJson,
Form: func(r *http.Request) (p.DataProvider, p.ZogError) {
err := r.ParseForm()
if err != nil {
return nil, &p.ZogErr{C: zconst.ErrCodeZHTTPInvalidForm, Err: err}
}
return form(r.Form), nil
},
Query: func(r *http.Request) (p.DataProvider, p.ZogError) {
// This handles generic GET request from browser. We treat it as url.Values
return form(r.URL.Query()), nil
},
},
}

type urlDataProvider struct {
Data url.Values
}
Expand Down Expand Up @@ -41,20 +69,14 @@ func (u urlDataProvider) GetUnderlying() any {
// Usage:
// schema.Parse(zhttp.Request(r), &dest)
func Request(r *http.Request) p.DpFactory {
return func() (p.DataProvider, *p.ZogErr) {
return func() (p.DataProvider, p.ZogError) {
switch r.Header.Get("Content-Type") {
case "application/json":
return parseJson(r.Body)
return Config.Parsers.JSON(r)
case "application/x-www-form-urlencoded":
err := r.ParseForm()
if err != nil {
return nil, &p.ZogErr{C: zconst.ErrCodeZHTTPInvalidForm, Err: err}
}
return form(r.Form), nil
return Config.Parsers.Form(r)
default:
// This handles generic GET request from browser. We treat it as url.Values
params := r.URL.Query()
return form(params), nil
return Config.Parsers.Query(r)
}
}
}
Expand All @@ -68,9 +90,9 @@ func Request(r *http.Request) p.DpFactory {
- struct schema -> hey this valid input
- "string is not an object"
*/
func parseJson(data io.ReadCloser) (p.DataProvider, *p.ZogErr) {
func parseJson(r *http.Request) (p.DataProvider, p.ZogError) {
var m map[string]any
decod := json.NewDecoder(data)
decod := json.NewDecoder(r.Body)
err := decod.Decode(&m)
if err != nil {
return nil, &p.ZogErr{C: zconst.ErrCodeZHTTPInvalidJSON, Err: err}
Expand Down
33 changes: 16 additions & 17 deletions zhttp/zhttp_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package zhttp

import (
"io"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -184,47 +183,47 @@ func TestRequestContentTypeDefault(t *testing.T) {

func TestParseJsonValid(t *testing.T) {
jsonData := `{"name":"John","age":30}`
reader := io.NopCloser(strings.NewReader(jsonData))
req, _ := http.NewRequest("POST", "/test", strings.NewReader(jsonData))
req.Header.Set("Content-Type", "application/json")

dp, err := parseJson(reader)
dp, err := Config.Parsers.JSON(req)
assert.Nil(t, err)
assert.Equal(t, "John", dp.Get("name"))
assert.Equal(t, float64(30), dp.Get("age"))
}

func TestParseJsonInvalid(t *testing.T) {
invalidJSON := `{"name":"John","age":30`
reader := io.NopCloser(strings.NewReader(invalidJSON))
req, _ := http.NewRequest("POST", "/test", strings.NewReader(invalidJSON))
req.Header.Set("Content-Type", "application/json")

dp, err := parseJson(reader)
dp, err := Config.Parsers.JSON(req)

assert.Error(t, err)
assert.Nil(t, dp)
assert.Equal(t, zconst.ErrCodeZHTTPInvalidJSON, err.C)
assert.Equal(t, zconst.ErrCodeZHTTPInvalidJSON, err.Code())
}

func TestParseJsonWithNilValue(t *testing.T) {
jsonData := `null`
reader := io.NopCloser(strings.NewReader(jsonData))
_, err := parseJson(reader)
req, _ := http.NewRequest("POST", "/test", strings.NewReader(jsonData))
req.Header.Set("Content-Type", "application/json")

dp, err := Config.Parsers.JSON(req)
assert.NotNil(t, err)
assert.Nil(t, dp)
}

func TestParseJsonWithEmptyObject(t *testing.T) {
jsonData := `{}`
reader := io.NopCloser(strings.NewReader(jsonData))
dp, err := parseJson(reader)
req, _ := http.NewRequest("POST", "/test", strings.NewReader(jsonData))
req.Header.Set("Content-Type", "application/json")

dp, err := Config.Parsers.JSON(req)
assert.Nil(t, err)
assert.Equal(t, map[string]any{}, dp.GetUnderlying())
}

func TestParseJsonWithPlainValue(t *testing.T) {
jsonData := `"string"`
reader := io.NopCloser(strings.NewReader(jsonData))
_, err := parseJson(reader)
assert.NotNil(t, err)
}

func TestForm(t *testing.T) {
data := url.Values{
"name": []string{"John"},
Expand Down

0 comments on commit e8a111f

Please sign in to comment.