From 20095613cbe017772bb92e774cffcb4f8cda390b Mon Sep 17 00:00:00 2001 From: "Tristan M." <54208010+Oudwins@users.noreply.github.com> Date: Thu, 2 Jan 2025 14:43:18 +0100 Subject: [PATCH] feat: zjson package (#58) * refactor: renamed json parsing error * feat: parse json package * fix: close closer if available * docs: zjson docs * docs: updated docs for json --- README.md | 11 +++++- docs/docs/getting-started.md | 11 +++++- docs/docs/packages/i18n.md | 2 +- docs/docs/packages/zconst.md | 2 +- docs/docs/packages/zenv.md | 2 +- docs/docs/packages/zjson.md | 35 +++++++++++++++++++ i18n/en/en.go | 3 +- i18n/es/es.go | 3 +- parsers/zjson/parseJson.go | 52 +++++++++++++++++++++++++++ zconst/consts.go | 3 +- zhttp/zhttp.go | 68 +++++++++++++----------------------- zhttp/zhttp_test.go | 10 +++--- 12 files changed, 146 insertions(+), 56 deletions(-) create mode 100644 docs/docs/packages/zjson.md create mode 100644 parsers/zjson/parseJson.go diff --git a/README.md b/README.md index 6cabd31..4bcd472 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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: diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 44ffea0..f674a63 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -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: @@ -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: diff --git a/docs/docs/packages/i18n.md b/docs/docs/packages/i18n.md index 9578303..0f82177 100644 --- a/docs/docs/packages/i18n.md +++ b/docs/docs/packages/i18n.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 4 --- # i18n diff --git a/docs/docs/packages/zconst.md b/docs/docs/packages/zconst.md index 47d0126..c78fbe9 100644 --- a/docs/docs/packages/zconst.md +++ b/docs/docs/packages/zconst.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 --- # zconst diff --git a/docs/docs/packages/zenv.md b/docs/docs/packages/zenv.md index 29b3c2d..13908ff 100644 --- a/docs/docs/packages/zenv.md +++ b/docs/docs/packages/zenv.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 --- # zenv diff --git a/docs/docs/packages/zjson.md b/docs/docs/packages/zjson.md new file mode 100644 index 0000000..9714877 --- /dev/null +++ b/docs/docs/packages/zjson.md @@ -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. diff --git a/i18n/en/en.go b/i18n/en/en.go index 2a303f1..2707c06 100644 --- a/i18n/en/en.go +++ b/i18n/en/en.go @@ -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", }, diff --git a/i18n/es/es.go b/i18n/es/es.go index db28851..96b7a06 100644 --- a/i18n/es/es.go +++ b/i18n/es/es.go @@ -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", }, diff --git a/parsers/zjson/parseJson.go b/parsers/zjson/parseJson.go new file mode 100644 index 0000000..59e4f9b --- /dev/null +++ b/parsers/zjson/parseJson.go @@ -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 + } +} diff --git a/zconst/consts.go b/zconst/consts.go index b4fe0a1..10fcb6c 100644 --- a/zconst/consts.go +++ b/zconst/consts.go @@ -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 ) diff --git a/zhttp/zhttp.go b/zhttp/zhttp.go index 329232b..ede5c58 100644 --- a/zhttp/zhttp.go +++ b/zhttp/zhttp.go @@ -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 { @@ -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 + } }, }, } @@ -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 { diff --git a/zhttp/zhttp_test.go b/zhttp/zhttp_test.go index 21a33fd..6b1e33f 100644 --- a/zhttp/zhttp_test.go +++ b/zhttp/zhttp_test.go @@ -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")) @@ -197,11 +197,11 @@ 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) { @@ -209,7 +209,7 @@ func TestParseJsonWithNilValue(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.NotNil(t, err) assert.Nil(t, dp) } @@ -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()) }