Skip to content

Commit

Permalink
Merge pull request #1 from guptaaashutosh/update-with-json-util
Browse files Browse the repository at this point in the history
update jsonutil
  • Loading branch information
guptaaashutosh committed Jan 17, 2024
2 parents 73b5e2a + acfa208 commit 79979e0
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 35 deletions.
29 changes: 24 additions & 5 deletions data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/reflects"
"github.com/gookit/goutil/strutil"
"github.com/guptaaashutosh/go_validate/jsonutil"
)

const (
Expand All @@ -39,14 +40,21 @@ const (
// data (Un)marshal func
var (
Marshal MarshalFunc = json.Marshal
Unmarshal UnmarshalFunc = json.Unmarshal
//original
// Unmarshal UnmarshalFunc = json.Unmarshal
Unmarshal UnmarshalFunc = jsonutil.Unmarshal
)

type (
// MarshalFunc define
MarshalFunc func(v any) ([]byte, error)
//original
// UnmarshalFunc define
UnmarshalFunc func(data []byte, ptr any) error
// UnmarshalFunc func(data []byte, ptr any) error
// Modified
// Customization: use custom JSON Unmarshal to handle all unmarshalling errors.
// UnmarshalFunc define
UnmarshalFunc func(r *http.Request, data []byte, v interface{}) (int, error)
)

// DataFace data source interface definition
Expand Down Expand Up @@ -133,13 +141,24 @@ func (d *MapData) Validation(err ...error) *Validation {
return NewValidation(d)
}

//original
// BindJSON binds v to the JSON data in the request body.
// It calls json.Unmarshal and sets the value of v.
// func (d *MapData) BindJSON(ptr any) error {
// if len(d.bodyJSON) == 0 {
// return nil
// }
// return Unmarshal(d.bodyJSON, ptr)
// }
// Modified
// Customization: use custom JSON Unmarshal to handle all unmarshalling errors.
// BindJSON binds v to the JSON data in the request body.
// It calls json.Unmarshal and sets the value of v.
func (d *MapData) BindJSON(ptr any) error {
func (d *MapData) BindJSON(ptr interface{}) (int, error) {
if len(d.bodyJSON) == 0 {
return nil
return 0, nil
}
return Unmarshal(d.bodyJSON, ptr)
return Unmarshal(nil, d.bodyJSON, ptr)
}

/*************************************************************
Expand Down
8 changes: 6 additions & 2 deletions filtering.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func (r *FilterRule) Apply(v *Validation) (err error) {

// dont need check default value
if !v.CheckDefault {
v.safeData[field] = newVal // save validated value.
v.SaferData[field] = newVal // save validated value.
continue
}
}
Expand All @@ -185,7 +185,11 @@ func (r *FilterRule) Apply(v *Validation) (err error) {
if err != nil {
return err
}

// Customization: We need to overwrite original field value with filtered/converted value because filtered/converted value was not being loaded in struct while calling BindSafeData().
if v.SaferData[field] != "" {
// save filtered value.
v.SaferData[field] = newVal
}
// save filtered value.
v.filteredData[field] = newVal
}
Expand Down
103 changes: 103 additions & 0 deletions jsonutil/unmarshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Package jsonutil provides common utilities for properly handling JSON payloads in HTTP body.
package jsonutil

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"strings"
)

// Unmarshal provides a common implementation of JSON unmarshalling
// with well defined error handling.
// Unmarshal parses the JSON-encoded data and stores the result
// in the value pointed to by v. If v is nil or not a pointer,
// Unmarshal returns an InvalidUnmarshalError.
// It can unmarshal data available in request/data param.
func Unmarshal(r *http.Request, data []byte, v interface{}) (int, error) {
// ensure that some data is provided for unmarshalling
if r == nil && data == nil {
return http.StatusUnsupportedMediaType, fmt.Errorf("no data provided")
} else if r != nil && data != nil {
// if someone sends multiple data for unmarshalling then it gives below error
return http.StatusUnsupportedMediaType, fmt.Errorf("multiple data provided for unmarshalling not supported")
}

var d = &json.Decoder{}
// if "r" request is not empty then it will read data from request body to unmarshal that data into object provided in "v".
if r != nil && data == nil {
// read request body as []byte.
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("error reading request body")
}
// as we are only allowed to read request body once so we need to set request body after reading for further use of request data
// it will set request body which we have got in "bodyBytes" above.
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

// check if content type is valid or not.
if !HasContentType(r, "application/json") {
return http.StatusUnsupportedMediaType, fmt.Errorf("content-type is not application/json")
}

d = json.NewDecoder(bytes.NewReader(bodyBytes))
} else if data != nil && r == nil {
d = json.NewDecoder(bytes.NewReader(data))
}

// DisallowUnknownFields causes the Decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
// d.DisallowUnknownFields()

// handle errors returned while decoding data into object.
if err := d.Decode(&v); err != nil {
var syntaxErr *json.SyntaxError
var unmarshalError *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxErr):
return http.StatusBadRequest, fmt.Errorf("malformed json at position %v", syntaxErr.Offset)
case errors.Is(err, io.ErrUnexpectedEOF):
return http.StatusBadRequest, fmt.Errorf("malformed json")
case errors.As(err, &unmarshalError):
return http.StatusBadRequest, fmt.Errorf("invalid value %v at position %v", unmarshalError.Field, unmarshalError.Offset)
case strings.HasPrefix(err.Error(), "json: unknown field"):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return http.StatusBadRequest, fmt.Errorf("unknown field %s", fieldName)
case errors.Is(err, io.EOF):
return http.StatusBadRequest, fmt.Errorf("body must not be empty")
case err.Error() == "http: request body too large":
return http.StatusRequestEntityTooLarge, err
default:
return http.StatusInternalServerError, fmt.Errorf("failed to decode json %v", err)
}
}
if d.More() {
return http.StatusBadRequest, fmt.Errorf("body must contain only one JSON object")
}
return http.StatusOK, nil
}

// check for valid content type.
func HasContentType(r *http.Request, mimetype string) bool {
contentType := r.Header.Get("Content-type")
if contentType == "" {
return mimetype == "application/octet-stream"
}

for _, v := range strings.Split(contentType, ",") {
t, _, err := mime.ParseMediaType(v)
if err != nil {
break
}
if t == mimetype {
return true
}
}
return false
}
18 changes: 9 additions & 9 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,15 @@ func CalcLength(val any) int {
func valueCompare(srcVal, dstVal any, op string) (ok bool) {
srcVal = indirectValue(srcVal)

// string compare
if str1, ok := srcVal.(string); ok {
str2, err := strutil.ToString(dstVal)
if err != nil {
return false
}

return strutil.VersionCompare(str1, str2, op)
}
// Customization: unused process - String values cannot be compare like integers values
// if str1, ok := srcVal.(string); ok {
// str2, err := strutil.ToString(dstVal)
// if err != nil {
// return false
// }

// return strutil.VersionCompare(str1, str2, op)
// }

// as int or float to compare
return mathutil.Compare(srcVal, dstVal, op)
Expand Down
2 changes: 1 addition & 1 deletion validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func newEmpty() *Validation {
// trans: StdTranslator,
trans: NewTranslator(),
// validated data
safeData: make(map[string]any),
SaferData: make(map[string]any),
// validator names
validators: make(map[string]int8, 16),
// filtered data
Expand Down
8 changes: 4 additions & 4 deletions validating.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (v *Validation) Validate(scene ...string) bool {

v.hasValidated = true
if v.hasError { // clear safe data on error.
v.safeData = make(map[string]any)
v.SaferData = make(map[string]any)
}
return v.IsSuccess()
}
Expand Down Expand Up @@ -149,7 +149,7 @@ func (r *Rule) Apply(v *Validation) (stop bool) {
// dont need check default value
if !v.CheckDefault {
// save validated value.
v.safeData[field] = val
v.SaferData[field] = val
continue
}

Expand Down Expand Up @@ -185,7 +185,7 @@ func (r *Rule) Apply(v *Validation) (stop bool) {
// Todo: Update validation and filtering flow
if val != nil{
// Customization: We need to bind all data
v.safeData[field] = val
v.SaferData[field] = val
}
// empty value AND skip on empty.
if r.skipEmpty && isNotRequired && IsEmpty(val) {
Expand All @@ -195,7 +195,7 @@ func (r *Rule) Apply(v *Validation) (stop bool) {
// validate field value
if r.valueValidate(field, name, val, v) {
if val != nil {
v.safeData[field] = val // save validated value.
v.SaferData[field] = val // save validated value.
}
} else { // build and collect error message
v.AddError(field, r.validator, r.errorMessage(field, r.validator, v))
Expand Down
50 changes: 37 additions & 13 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ type Validation struct {
data DataFace
// all validated fields list
// fields []string

//orginal
// save filtered/validated safe data
safeData M
// safeData M
SaferData M // Customization: We need to update the typo of safeData to SaferData to access the variable outside the package for manual validation.
// filtered clean data
filteredData M
// save user custom set default values
Expand Down Expand Up @@ -116,7 +117,7 @@ func (v *Validation) ResetResult() {
v.hasFiltered = false
v.hasValidated = false
// result data
v.safeData = make(map[string]any)
v.SaferData = make(map[string]any)
v.filteredData = make(map[string]any)
}

Expand Down Expand Up @@ -418,7 +419,7 @@ func (v *Validation) tryGet(key string) (val any, exist, zero bool) {
}

// find from validated data. (such as has default value)
if val, ok := v.safeData[key]; ok {
if val, ok := v.SaferData[key]; ok {
return val, true, false
}

Expand Down Expand Up @@ -461,7 +462,7 @@ func (v *Validation) Safe(key string) (val any, ok bool) {
if v.data == nil { // check input data
return
}
val, ok = v.safeData[key]
val, ok = v.SaferData[key]
return
}

Expand All @@ -478,23 +479,46 @@ func (v *Validation) GetSafe(key string) any {
}

// BindStruct binding safe data to an struct.
func (v *Validation) BindStruct(ptr any) error {
// func (v *Validation) BindStruct(ptr any) error {
// return v.BindSafeData(ptr)
// }
// Modified
// Customization: use custom JSON Unmarshal to handle all unmarshalling errors.
// BindStruct binding safe data to an struct.
func (v *Validation) BindStruct(ptr interface{}) (int, error) {
return v.BindSafeData(ptr)
}

//original
// BindSafeData binding safe data to an struct.
func (v *Validation) BindSafeData(ptr any) error {
if len(v.safeData) == 0 { // no safe data.
return nil
// func (v *Validation) BindSafeData(ptr any) error {
// if len(v.safeData) == 0 { // no safe data.
// return nil
// }

// // to json bytes
// bts, err := Marshal(v.safeData)
// if err != nil {
// return err
// }

// return Unmarshal(bts, ptr)
// }
// Modified
// Customization: use custom JSON Unmarshal to handle all unmarshalling errors.
// BindSafeData binding safe data to an struct.
func (v *Validation) BindSafeData(ptr interface{}) (int, error) {
if len(v.SaferData) == 0 { // no safe data.
return 0, nil
}

// to json bytes
bts, err := Marshal(v.safeData)
bts, err := Marshal(v.SaferData)
if err != nil {
return err
return 0, err
}

return Unmarshal(bts, ptr)
return Unmarshal(nil, bts, ptr)
}

// Set value by key
Expand Down Expand Up @@ -566,7 +590,7 @@ func (v *Validation) IsFail() bool { return v.hasError }
func (v *Validation) IsSuccess() bool { return !v.hasError }

// SafeData get all validated safe data
func (v *Validation) SafeData() M { return v.safeData }
func (v *Validation) SafeData() M { return v.SaferData }

// FilteredData return filtered data.
func (v *Validation) FilteredData() M {
Expand Down
15 changes: 14 additions & 1 deletion validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -901,14 +901,27 @@ func IsCIDR(s string) bool {
return err == nil
}

//original
// IsJSON check if the string is valid JSON (note: uses json.Unmarshal).
// func IsJSON(s string) bool {
// if s == "" {
// return false
// }

// var js json.RawMessage
// return Unmarshal([]byte(s), &js) == nil
// }
// Modified
// Customization: use custom JSON Unmarshal to handle all unmarshalling errors.
// IsJSON check if the string is valid JSON (note: uses json.Unmarshal).
func IsJSON(s string) bool {
if s == "" {
return false
}

var js json.RawMessage
return Unmarshal([]byte(s), &js) == nil
_, err := Unmarshal(nil, []byte(s), &js)
return err == nil
}

// HasLowerCase check string has lower case
Expand Down

1 comment on commit 79979e0

@guptaaashutosh
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all right

Please sign in to comment.