From 5ea6ee2f5beb2bacf0d74ce6da17a1f9a391a101 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Wed, 8 Jun 2022 22:19:42 +0700 Subject: lib/http: add function to unmarshal url.Values using tag `form:` UnmarshalForm read struct fields tagged with `form:` from out as key and set its using the value from url.Values based on that key. If the field does not have `form:` tag but it is exported, then it will use the field name, in case insensitive. Only the following types are supported: bool, int/intX, uint/uintX, floatX, string, []byte, or type that implement BinaryUnmarshaler (UnmarshalBinary), json.Unmarshaler (UnmarshalJSON), or TextUnmarshaler (UnmarshalText). A bool type can be set to true using the following string value: "true", "yes", or "1". If the input contains multiple values but the field type is not slice, the field will be set using the first value. It will return an error if the out variable is not set-able (the type is not a pointer to a struct). It will not return an error if one of the input value is not match with field type. --- lib/http/form.go | 157 +++++++++++++++++++++++++++++++++ lib/http/form_example_test.go | 198 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 lib/http/form.go create mode 100644 lib/http/form_example_test.go diff --git a/lib/http/form.go b/lib/http/form.go new file mode 100644 index 00000000..b691c319 --- /dev/null +++ b/lib/http/form.go @@ -0,0 +1,157 @@ +// Copyright 2022, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package http + +import ( + "fmt" + "net/url" + "reflect" + "strings" + + libreflect "github.com/shuLhan/share/lib/reflect" +) + +const ( + structTagKey = "form" +) + +// UnmarshalForm read struct fields tagged with `form:` from out as key and +// set its using the value from url.Values based on that key. +// If the field does not have `form:` tag but it is exported, then it will use +// the field name, in case insensitive. +// +// Only the following types are supported: bool, int/intX, uint/uintX, +// floatX, string, []byte, or type that implement BinaryUnmarshaler +// (UnmarshalBinary), json.Unmarshaler (UnmarshalJSON), or TextUnmarshaler +// (UnmarshalText). +// +// A bool type can be set to true using the following string value: "true", +// "yes", or "1". +// +// If the input contains multiple values but the field type is not slice, +// the field will be set using the first value. +// +// It will return an error if the out variable is not set-able (the type is +// not a pointer to a struct). +// It will not return an error if one of the input value is not match with +// field type. +func UnmarshalForm(in url.Values, out interface{}) (err error) { + var ( + logp = "UnmarshalForm" + vout reflect.Value = reflect.ValueOf(out) + rtype reflect.Type = vout.Type() + rkind reflect.Kind = rtype.Kind() + + tstruct reflect.Type + field reflect.StructField + fval reflect.Value + fptr reflect.Value + key, val string + vals []string + x int + hasTag bool + ) + + if rkind != reflect.Ptr { + return fmt.Errorf("%s: expecting *T got %T", logp, out) + } + + if vout.IsNil() { + return fmt.Errorf("%s: %T is not initialized", logp, out) + } + + vout = vout.Elem() + rtype = rtype.Elem() + rkind = rtype.Kind() + if rkind == reflect.Ptr { + rtype = rtype.Elem() + rkind = rtype.Kind() + if rkind != reflect.Struct { + return fmt.Errorf("%s: expecting *T or **T got %T", logp, out) + } + + if vout.IsNil() { + vout.Set(reflect.New(rtype)) // vout = new(T) + vout = vout.Elem() + } else { + vout = vout.Elem() + } + } else { + if rkind != reflect.Struct { + return fmt.Errorf("%s: expecting *T or **T got %T", logp, out) + } + } + + tstruct = rtype + + for ; x < vout.NumField(); x++ { + field = tstruct.Field(x) + + key, _, hasTag = libreflect.Tag(field, structTagKey) + if len(key) == 0 && !hasTag { + // Field is unexported. + continue + } + + vals = in[key] + if len(vals) == 0 { + if hasTag { + // Tag is defined and not empty, but no value + // in input. + continue + } + + // Tag is not defined, search lower case field name. + key = strings.ToLower(key) + vals = in[key] + if len(vals) == 0 { + continue + } + } + + // Now that we have the value, store it into field by its + // type. + fval = vout.Field(x) + rtype = fval.Type() + rkind = fval.Kind() + + if rkind == reflect.Ptr { + // F *T + rtype = rtype.Elem() // T <= *T + rkind = rtype.Kind() + fptr = reflect.New(rtype) // f = new(T) + fval.Set(fptr) // F = f + } else { + // F T + fptr = fval.Addr() // f = &F + } + + if len(vals) > 1 { + if rkind == reflect.Slice { + for _, val = range vals { + err = libreflect.Set(fptr, val) + if err != nil { + continue + } + } + } else { + // Form contains multiple values, use + // only the first value to set the field. + val = vals[0] + err = libreflect.Set(fptr, val) + if err != nil { + continue + } + } + } else { + val = vals[0] + err = libreflect.Set(fptr, val) + if err != nil { + continue + } + } + } + return nil +} diff --git a/lib/http/form_example_test.go b/lib/http/form_example_test.go new file mode 100644 index 00000000..3482be72 --- /dev/null +++ b/lib/http/form_example_test.go @@ -0,0 +1,198 @@ +package http + +import ( + "fmt" + "math/big" + "net/url" +) + +func ExampleUnmarshalForm() { + type T struct { + Rat *big.Rat `form:"big.Rat"` + String string `form:"string"` + Bytes []byte `form:"bytes"` + Int int `form:""` // With empty tag. + F64 float64 `form:"f64"` + F32 float32 `form:"f32"` + NotSet int16 `form:"notset"` + Uint8 uint8 `form:" uint8 "` + Bool bool // Without tag. + } + var ( + in = url.Values{} + + out T + ptrOut *T + err error + ) + + in.Set("big.Rat", "1.2345") + in.Set("string", "a_string") + in.Set("bytes", "bytes") + in.Set("int", "1") + in.Set("f64", "6.4") + in.Set("f32", "3.2") + in.Set("uint8", "2") + in.Set("bool", "true") + + err = UnmarshalForm(in, &out) + if err != nil { + fmt.Println(err) + } else { + fmt.Printf("%+v\n", out) + } + + // Set the struct without initialized. + err = UnmarshalForm(in, &ptrOut) + if err != nil { + fmt.Println(err) + } else { + fmt.Printf("%+v\n", ptrOut) + } + + //Output: + //{Rat:2469/2000 String:a_string Bytes:[98 121 116 101 115] Int:1 F64:6.4 F32:3.2 NotSet:0 Uint8:2 Bool:true} + //&{Rat:2469/2000 String:a_string Bytes:[98 121 116 101 115] Int:1 F64:6.4 F32:3.2 NotSet:0 Uint8:2 Bool:true} +} + +func ExampleUnmarshalForm_error() { + type T struct { + Int int + } + + var ( + in = url.Values{} + + out T + ptrOut *T + err error + ) + + // Passing out as unsetable by function. + err = UnmarshalForm(in, out) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(out) + } + + // Passing out as un-initialized pointer. + err = UnmarshalForm(in, ptrOut) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(out) + } + + // Set the field with invalid type. + in.Set("int", "a") + err = UnmarshalForm(in, &out) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(out) + } + + //Output: + //UnmarshalForm: expecting *T got http.T + //UnmarshalForm: *http.T is not initialized + //{0} + +} + +func ExampleUnmarshalForm_slice() { + type SliceT struct { + NotSlice string `form:"multi_value"` + SliceString []string `form:"slice_string"` + SliceInt []int `form:"slice_int"` + } + + var ( + in = url.Values{} + + sliceOut SliceT + ptrSliceOut *SliceT + err error + ) + + in.Add("multi_value", "first") + in.Add("multi_value", "second") + in.Add("slice_string", "multi") + in.Add("slice_string", "value") + in.Add("slice_int", "123") + in.Add("slice_int", "456") + + err = UnmarshalForm(in, &sliceOut) + if err != nil { + fmt.Println(err) + } else { + fmt.Printf("%+v\n", sliceOut) + } + + err = UnmarshalForm(in, &ptrSliceOut) + if err != nil { + fmt.Println(err) + } else { + fmt.Printf("%+v\n", ptrSliceOut) + } + + //Output: + //{NotSlice:first SliceString:[multi value] SliceInt:[123 456]} + //&{NotSlice:first SliceString:[multi value] SliceInt:[123 456]} +} + +func ExampleUnmarshalForm_zero() { + type T struct { + Rat *big.Rat `form:"big.Rat"` + String string `form:"string"` + Bytes []byte `form:"bytes"` + Int int `form:""` // With empty tag. + F64 float64 `form:"f64"` + F32 float32 `form:"f32"` + NotSet int16 `form:"notset"` + Uint8 uint8 `form:" uint8 "` + Bool bool // Without tag. + } + var ( + in = url.Values{} + + out T + err error + ) + + in.Set("big.Rat", "1.2345") + in.Set("string", "a_string") + in.Set("bytes", "bytes") + in.Set("int", "1") + in.Set("f64", "6.4") + in.Set("f32", "3.2") + in.Set("uint8", "2") + in.Set("bool", "true") + + err = UnmarshalForm(in, &out) + if err != nil { + fmt.Println(err) + } else { + fmt.Printf("%+v\n", out) + } + + in.Set("bool", "") + in.Set("int", "") + in.Set("uint8", "") + in.Set("f32", "") + in.Set("f64", "") + in.Set("string", "") + in.Set("bytes", "") + in.Set("big.Rat", "") + + err = UnmarshalForm(in, &out) + if err != nil { + fmt.Println(err) + } else { + fmt.Printf("%+v\n", out) + } + + //Output: + //{Rat:2469/2000 String:a_string Bytes:[98 121 116 101 115] Int:1 F64:6.4 F32:3.2 NotSet:0 Uint8:2 Bool:true} + //{Rat:0/1 String: Bytes:[] Int:0 F64:0 F32:0 NotSet:0 Uint8:0 Bool:false} +} -- cgit v1.3