Skip to content

Commit

Permalink
feat: zjson package (#58)
Browse files Browse the repository at this point in the history
* refactor: renamed json parsing error

* feat: parse json package

* fix: close closer if available

* docs: zjson docs

* docs: updated docs for json
  • Loading branch information
Oudwins authored Jan 2, 2025
1 parent 914b838 commit 2009561
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 56 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func main() {
}
```

#### **4. Its easy to use with http**
#### **4. Its easy to use with http & json**

The [zhttp package](https://zog.dev/packages/zhttp) has you covered for JSON, Forms and Query Params, just do:

Expand All @@ -103,6 +103,15 @@ import (
err := userSchema.Parse(zhttp.Request(r), &user)
```

If you are receiving json some other way you can use the [zjson package](https://zog.dev/packages/zjson)

```go
import (
zjson "github.com/Oudwins/zog/zjson"
)
err := userSchema.Parse(zjson.Decode(bytes.NewReader(jsonBytes)), &user)
```

#### **5. Or to validate your environment variables**

The [zenv package](https://zog.dev/packages/zenv) has you covered, just do:
Expand Down
11 changes: 10 additions & 1 deletion docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func main() {
}
```

#### **4. Its easy to use with http**
#### **4. Its easy to use with http & json**

The [zhttp package](https://zog.dev/packages/zhttp) has you covered for JSON, Forms and Query Params, just do:

Expand All @@ -63,6 +63,15 @@ import (
err := userSchema.Parse(zhttp.Request(r), &user)
```

If you are receiving json some other way you can use the [zjson package](https://zog.dev/packages/zjson)

```go
import (
zjson "github.com/Oudwins/zog/zjson"
)
err := userSchema.Parse(zjson.Decode(bytes.NewReader(jsonBytes)), &user)
```

#### **5. Or to validate your environment variables**

The [zenv package](https://zog.dev/packages/zenv) has you covered, just do:
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/packages/i18n.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 4
---

# i18n
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/packages/zconst.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 5
---

# zconst
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/packages/zenv.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 3
---

# zenv
Expand Down
35 changes: 35 additions & 0 deletions docs/docs/packages/zjson.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
sidebar_position: 2
---

# zjson

A very small package for using Zog schemas to parse json into structs. It exports a single function `Decode` which takes in an `io.Reader` or an `io.ReaderCloser` and returns the necessary structures for Zog to parse the json into a struct. This package is used by the `zhttp` package.

```go
import (
z "github.com/Oudwins/zog"
"github.com/Oudwins/zog/parsers/zjson"
"bytes"
)
var userSchema = z.Struct(z.Schema{
"name": z.String().Required(),
"age": z.Int().Required().GT(18),
})
type User struct {
Name string
Age int
}

func ParseJson(json []byte) {
var user User
errs := userSchema.Parse(zjson.Decode(bytes.NewReader(json)), &user)
if errs != nil {
// handle errors
}
user.Name // defined
user.Age // defined
}
```

> **WARNING** The `zjson` package does NOT currently support parsing into any data type that is NOT a struct.
3 changes: 2 additions & 1 deletion i18n/en/en.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ var Map zconst.LangMap = map[zconst.ZogType]map[zconst.ZogErrCode]string{
zconst.ErrCodeRequired: "is required",
zconst.ErrCodeNotNil: "must not be empty",
zconst.ErrCodeFallback: "struct is invalid",
// JSON
zconst.ErrCodeInvalidJSON: "invalid json body",
// ZHTTP ERRORS
zconst.ErrCodeZHTTPInvalidJSON: "invalid json body",
zconst.ErrCodeZHTTPInvalidForm: "invalid form data",
zconst.ErrCodeZHTTPInvalidQuery: "invalid query params",
},
Expand Down
3 changes: 2 additions & 1 deletion i18n/es/es.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ var Map zconst.LangMap = map[zconst.ZogType]map[zconst.ZogErrCode]string{
zconst.ErrCodeRequired: "Es obligatorio",
zconst.ErrCodeNotNil: "No debe estar vacio",
zconst.ErrCodeFallback: "Estructura no es válida",
// JSON
zconst.ErrCodeInvalidJSON: "JSON no válido",
// ZHTTP ERRORS
zconst.ErrCodeZHTTPInvalidJSON: "JSON no válido",
zconst.ErrCodeZHTTPInvalidForm: "Formulario no válido",
zconst.ErrCodeZHTTPInvalidQuery: "Parámetros de consulta no válidos",
},
Expand Down
52 changes: 52 additions & 0 deletions parsers/zjson/parseJson.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package zjson

import (
"encoding/json"
"errors"
"io"

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

// func Unmarshal(data []byte) p.DpFactory {
// return func() (p.DataProvider, p.ZogError) {
// var m map[string]any
// err := json.Unmarshal(data, &m)
// if err != nil {
// return nil, &p.ZogErr{C: zconst.ErrCodeInvalidJSON, Err: err}
// }
// if m == nil {
// return nil, &p.ZogErr{C: zconst.ErrCodeInvalidJSON, Err: errors.New("nill json body")}
// }
// return p.NewMapDataProvider(m), nil
// }
// }

// Decodes JSON data. Does not support json arrays or primitives
/*
- "null" -> nil -> Not accepted by zhttp -> errs["$root"]-> required error
- "{}" -> okay -> map[]{}
- "" -> parsing error -> errs["$root"]-> parsing error
- "1213" -> zhttp -> plain value
- struct schema -> hey this valid input
- "string is not an object"
*/
func Decode(r io.Reader) p.DpFactory {
return func() (p.DataProvider, p.ZogError) {
closer, ok := r.(io.Closer)
if ok {
defer closer.Close()
}
var m map[string]any
decod := json.NewDecoder(r)
err := decod.Decode(&m)
if err != nil {
return nil, &p.ZogErr{C: zconst.ErrCodeInvalidJSON, Err: err}
}
if m == nil {
return nil, &p.ZogErr{C: zconst.ErrCodeInvalidJSON, Err: errors.New("nill json body")}
}
return p.NewMapDataProvider(m), nil
}
}
3 changes: 2 additions & 1 deletion zconst/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ const (
ErrCodeTrue ZogErrCode = "true"
ErrCodeFalse ZogErrCode = "false"

// JSON
ErrCodeInvalidJSON ZogErrCode = "invalid_json" // invalid json body
// ZHTTP ERRORS
ErrCodeZHTTPInvalidJSON ZogErrCode = "invalid_json" // invalid json body
ErrCodeZHTTPInvalidForm ZogErrCode = "invalid_form" // invalid form data
ErrCodeZHTTPInvalidQuery ZogErrCode = "invalid_query" // invalid query params
)
68 changes: 25 additions & 43 deletions zhttp/zhttp.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package zhttp

import (
"encoding/json"
"errors"
"net/http"
"net/url"

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

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

var Config = struct {
Parsers struct {
Expand All @@ -24,17 +23,23 @@ var Config = struct {
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}
JSON: func(r *http.Request) p.DpFactory {
return zjson.Decode(r.Body)
},
Form: func(r *http.Request) p.DpFactory {
return func() (p.DataProvider, p.ZogError) {
err := r.ParseForm()
if err != nil {
return nil, &p.ZogErr{C: zconst.ErrCodeZHTTPInvalidForm, Err: err}
}
return form(r.Form), nil
}
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
Query: func(r *http.Request) p.DpFactory {
return func() (p.DataProvider, p.ZogError) {
// This handles generic GET request from browser. We treat it as url.Values
return form(r.URL.Query()), nil
}
},
},
}
Expand Down Expand Up @@ -68,39 +73,16 @@ func (u urlDataProvider) GetUnderlying() any {
// Parses JSON, Form & Query data from request based on Content-Type header
// Usage:
// schema.Parse(zhttp.Request(r), &dest)
// WARNING: FOR JSON PARSING DOES NOT SUPPORT JSON ARRAYS OR PRIMITIVES
func Request(r *http.Request) p.DpFactory {
return func() (p.DataProvider, p.ZogError) {
switch r.Header.Get("Content-Type") {
case "application/json":
return Config.Parsers.JSON(r)
case "application/x-www-form-urlencoded":
return Config.Parsers.Form(r)
default:
return Config.Parsers.Query(r)
}
}
}

// Parses JSON data from request body. Does not support json arrays or primitives
/*
- "null" -> nil -> Not accepted by zhttp -> errs["$root"]-> required error
- "{}" -> okay -> map[]{}
- "" -> parsing error -> errs["$root"]-> parsing error
- "1213" -> zhttp -> plain value
- struct schema -> hey this valid input
- "string is not an object"
*/
func parseJson(r *http.Request) (p.DataProvider, p.ZogError) {
var m map[string]any
decod := json.NewDecoder(r.Body)
err := decod.Decode(&m)
if err != nil {
return nil, &p.ZogErr{C: zconst.ErrCodeZHTTPInvalidJSON, Err: err}
}
if m == nil {
return nil, &p.ZogErr{C: zconst.ErrCodeZHTTPInvalidJSON, Err: errors.New("nill json body")}
switch r.Header.Get("Content-Type") {
case "application/json":
return Config.Parsers.JSON(r)
case "application/x-www-form-urlencoded":
return Config.Parsers.Form(r)
default:
return Config.Parsers.Query(r)
}
return p.NewMapDataProvider(m), nil
}

func form(data url.Values) p.DataProvider {
Expand Down
10 changes: 5 additions & 5 deletions zhttp/zhttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func TestParseJsonValid(t *testing.T) {
req, _ := http.NewRequest("POST", "/test", strings.NewReader(jsonData))
req.Header.Set("Content-Type", "application/json")

dp, err := Config.Parsers.JSON(req)
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"))
Expand All @@ -197,19 +197,19 @@ func TestParseJsonInvalid(t *testing.T) {
req, _ := http.NewRequest("POST", "/test", strings.NewReader(invalidJSON))
req.Header.Set("Content-Type", "application/json")

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

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

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

dp, err := Config.Parsers.JSON(req)
dp, err := Config.Parsers.JSON(req)()
assert.NotNil(t, err)
assert.Nil(t, dp)
}
Expand All @@ -219,7 +219,7 @@ func TestParseJsonWithEmptyObject(t *testing.T) {
req, _ := http.NewRequest("POST", "/test", strings.NewReader(jsonData))
req.Header.Set("Content-Type", "application/json")

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

0 comments on commit 2009561

Please sign in to comment.