From acfa2087cd392b4a04b88dc304918c257118cd05 Mon Sep 17 00:00:00 2001 From: Aashutosh Date: Wed, 17 Jan 2024 13:00:37 +0530 Subject: [PATCH] update jsonutil --- data_source.go | 29 ++++++++++-- filtering.go | 8 +++- jsonutil/unmarshal.go | 103 ++++++++++++++++++++++++++++++++++++++++++ util.go | 18 ++++---- validate.go | 2 +- validating.go | 8 ++-- validation.go | 50 ++++++++++++++------ validators.go | 15 +++++- 8 files changed, 198 insertions(+), 35 deletions(-) create mode 100644 jsonutil/unmarshal.go diff --git a/data_source.go b/data_source.go index 32a41ae..8dc52ff 100644 --- a/data_source.go +++ b/data_source.go @@ -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 ( @@ -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 @@ -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) } /************************************************************* diff --git a/filtering.go b/filtering.go index 00d0600..0939bb5 100644 --- a/filtering.go +++ b/filtering.go @@ -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 } } @@ -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 } diff --git a/jsonutil/unmarshal.go b/jsonutil/unmarshal.go new file mode 100644 index 0000000..d3391c7 --- /dev/null +++ b/jsonutil/unmarshal.go @@ -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 +} \ No newline at end of file diff --git a/util.go b/util.go index 64f6e69..789eb21 100644 --- a/util.go +++ b/util.go @@ -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) diff --git a/validate.go b/validate.go index 9636ead..3729ae1 100644 --- a/validate.go +++ b/validate.go @@ -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 diff --git a/validating.go b/validating.go index 80212dd..af8c922 100644 --- a/validating.go +++ b/validating.go @@ -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() } @@ -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 } @@ -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) { @@ -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)) diff --git a/validation.go b/validation.go index f60ed6f..077da6c 100644 --- a/validation.go +++ b/validation.go @@ -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 @@ -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) } @@ -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 } @@ -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 } @@ -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 @@ -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 { diff --git a/validators.go b/validators.go index e667d3b..5b04bc1 100644 --- a/validators.go +++ b/validators.go @@ -901,6 +901,18 @@ 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 == "" { @@ -908,7 +920,8 @@ func IsJSON(s string) bool { } 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