diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go deleted file mode 100644 index 3f7087f..0000000 --- a/internal/reqdata/parameter.go +++ /dev/null @@ -1,95 +0,0 @@ -package reqdata - -import ( - "encoding/json" - "fmt" - "reflect" -) - -// Parameter represents an http request parameter -// that can be of type URL, GET, or FORM (multipart, json, urlencoded) -type Parameter struct { - // whether the value has been json-parsed - // for optimisation purpose, parameters are only parsed - // if they are required by the current service - Parsed bool - - // whether the value is a file - File bool - - // the actual parameter value - Value interface{} -} - -// Parse parameter (json-like) if not already done -func (i *Parameter) Parse() { - - /* ignore already parsed or nil*/ - if i.Parsed || i.Value == nil { - return - } - - /* parse value */ - i.Parsed = true - i.Value = parseParameter(i.Value) -} - -// parseParameter parses http URI/GET/POST data -// - []string : return array of json elements -// - string : return json if valid, else return raw string -func parseParameter(data interface{}) interface{} { - dtype := reflect.TypeOf(data) - dvalue := reflect.ValueOf(data) - - switch dtype.Kind() { - - /* (1) []string -> recursive */ - case reflect.Slice: - - // 1. ignore empty - if dvalue.Len() == 0 { - return data - } - - // 2. parse each element recursively - result := make([]interface{}, dvalue.Len()) - - for i, l := 0, dvalue.Len(); i < l; i++ { - element := dvalue.Index(i) - result[i] = parseParameter(element.Interface()) - } - return result - - /* (2) string -> parse */ - case reflect.String: - - // build json wrapper - wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String()) - - // try to parse as json - var result interface{} - err := json.Unmarshal([]byte(wrapper), &result) - - // return if success - if err != nil { - return dvalue.String() - } - - mapval, ok := result.(map[string]interface{}) - if !ok { - return dvalue.String() - } - - wrapped, ok := mapval["wrapped"] - if !ok { - return dvalue.String() - } - - return wrapped - - } - - /* (3) NIL if unknown type */ - return dvalue.Interface() - -} diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parse_parameter_test.go similarity index 70% rename from internal/reqdata/parameter_test.go rename to internal/reqdata/parse_parameter_test.go index fc7a94d..65e7c4b 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parse_parameter_test.go @@ -6,15 +6,9 @@ import ( ) func TestSimpleString(t *testing.T) { - p := Parameter{Value: "some-string"} - p.Parse() + p := parseParameter("some-string") - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - cast, canCast := p.Value.(string) + cast, canCast := p.(string) if !canCast { t.Errorf("expected parameter to be a string") t.FailNow() @@ -31,15 +25,9 @@ func TestSimpleFloat(t *testing.T) { for i, tcase := range tcases { t.Run("case "+string(i), func(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: tcase} - p.Parse() + p := parseParameter(tcase) - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - cast, canCast := p.Value.(float64) + cast, canCast := p.(float64) if !canCast { t.Errorf("expected parameter to be a float64") t.FailNow() @@ -58,16 +46,9 @@ func TestSimpleBool(t *testing.T) { for i, tcase := range tcases { t.Run("case "+string(i), func(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: tcase} + p := parseParameter(tcase) - p.Parse() - - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - cast, canCast := p.Value.(bool) + cast, canCast := p.(bool) if !canCast { t.Errorf("expected parameter to be a bool") t.FailNow() @@ -82,15 +63,9 @@ func TestSimpleBool(t *testing.T) { } func TestJsonStringSlice(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: `["str1", "str2"]`} - p.Parse() + p := parseParameter(`["str1", "str2"]`) - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - slice, canCast := p.Value.([]interface{}) + slice, canCast := p.([]interface{}) if !canCast { t.Errorf("expected parameter to be a []interface{}") t.FailNow() @@ -120,15 +95,9 @@ func TestJsonStringSlice(t *testing.T) { } func TestStringSlice(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: []string{"str1", "str2"}} - p.Parse() + p := parseParameter([]string{"str1", "str2"}) - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - slice, canCast := p.Value.([]interface{}) + slice, canCast := p.([]interface{}) if !canCast { t.Errorf("expected parameter to be a []interface{}") t.FailNow() @@ -168,15 +137,9 @@ func TestJsonPrimitiveBool(t *testing.T) { for i, tcase := range tcases { t.Run("case "+string(i), func(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: tcase.Raw} - p.Parse() + p := parseParameter(tcase.Raw) - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - cast, canCast := p.Value.(bool) + cast, canCast := p.(bool) if !canCast { t.Errorf("expected parameter to be a bool") t.FailNow() @@ -211,15 +174,9 @@ func TestJsonPrimitiveFloat(t *testing.T) { for i, tcase := range tcases { t.Run("case "+string(i), func(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: tcase.Raw} - p.Parse() + p := parseParameter(tcase.Raw) - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - cast, canCast := p.Value.(float64) + cast, canCast := p.(float64) if !canCast { t.Errorf("expected parameter to be a float64") t.FailNow() @@ -235,15 +192,9 @@ func TestJsonPrimitiveFloat(t *testing.T) { } func TestJsonBoolSlice(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: []string{"true", "false"}} - p.Parse() + p := parseParameter([]string{"true", "false"}) - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - slice, canCast := p.Value.([]interface{}) + slice, canCast := p.([]interface{}) if !canCast { t.Errorf("expected parameter to be a []interface{}") t.FailNow() @@ -273,15 +224,9 @@ func TestJsonBoolSlice(t *testing.T) { } func TestBoolSlice(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: []bool{true, false}} - p.Parse() + p := parseParameter([]bool{true, false}) - if !p.Parsed { - t.Errorf("expected parameter to be parsed") - t.FailNow() - } - - slice, canCast := p.Value.([]interface{}) + slice, canCast := p.([]interface{}) if !canCast { t.Errorf("expected parameter to be a []interface{}") t.FailNow() diff --git a/internal/reqdata/set.go b/internal/reqdata/set.go index 355a7a1..4baca97 100644 --- a/internal/reqdata/set.go +++ b/internal/reqdata/set.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "reflect" "git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/multipart" @@ -26,14 +27,14 @@ type Set struct { // - FORM: no prefix // - URL: '{uri_var}' // - GET: 'GET@' followed by the key in GET - Data map[string]*Parameter + Data map[string]interface{} } // New creates a new empty store. func New(service *config.Service) *Set { return &Set{ service: service, - Data: make(map[string]*Parameter), + Data: make(map[string]interface{}), } } @@ -53,17 +54,17 @@ func (i *Set) ExtractURI(req *http.Request) error { return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType) } + // parse parameter + parsed := parseParameter(value) + // check type - cast, valid := capture.Ref.Validator(value) + cast, valid := capture.Ref.Validator(parsed) if !valid { return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType) } // store cast value in 'Set' - i.Data[capture.Ref.Rename] = &Parameter{ - Value: cast, - } - + i.Data[capture.Ref.Rename] = cast } return nil @@ -86,16 +87,17 @@ func (i *Set) ExtractQuery(req *http.Request) error { continue } + // parse parameter + parsed := parseParameter(value) + // check type - cast, valid := param.Validator(value) + cast, valid := param.Validator(parsed) if !valid { return fmt.Errorf("%s: %w", name, ErrInvalidType) } - // store value - i.Data[param.Rename] = &Parameter{ - Value: cast, - } + // store cast value + i.Data[param.Rename] = cast } return nil @@ -167,10 +169,8 @@ func (i *Set) parseJSON(req *http.Request) error { return fmt.Errorf("%s: %w", name, ErrInvalidType) } - // store value - i.Data[param.Rename] = &Parameter{ - Value: cast, - } + // store cast value + i.Data[param.Rename] = cast } return nil @@ -197,16 +197,17 @@ func (i *Set) parseUrlencoded(req *http.Request) error { continue } + // parse parameter + parsed := parseParameter(value) + // check type - cast, valid := param.Validator(value) + cast, valid := param.Validator(parsed) if !valid { return fmt.Errorf("%s: %w", name, ErrInvalidType) } - // store value - i.Data[param.Rename] = &Parameter{ - Value: cast, - } + // store cast value + i.Data[param.Rename] = cast } return nil @@ -244,18 +245,79 @@ func (i *Set) parseMultipart(req *http.Request) error { continue } + // parse parameter + parsed := parseParameter(string(component.Data)) + // fail on invalid type - cast, valid := param.Validator(string(component.Data)) + cast, valid := param.Validator(parsed) if !valid { return fmt.Errorf("%s: %w", name, ErrInvalidType) } - // store value - i.Data[param.Rename] = &Parameter{ - Value: cast, - } + // store cast value + i.Data[param.Rename] = cast } return nil } + +// parseParameter parses http URI/GET/POST data +// - []string : return array of json elements +// - string : return json if valid, else return raw string +func parseParameter(data interface{}) interface{} { + dtype := reflect.TypeOf(data) + dvalue := reflect.ValueOf(data) + + switch dtype.Kind() { + + /* (1) []string -> recursive */ + case reflect.Slice: + + // 1. ignore empty + if dvalue.Len() == 0 { + return data + } + + // 2. parse each element recursively + result := make([]interface{}, dvalue.Len()) + + for i, l := 0, dvalue.Len(); i < l; i++ { + element := dvalue.Index(i) + result[i] = parseParameter(element.Interface()) + } + return result + + /* (2) string -> parse */ + case reflect.String: + + // build json wrapper + wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String()) + + // try to parse as json + var result interface{} + err := json.Unmarshal([]byte(wrapper), &result) + + // return if success + if err != nil { + return dvalue.String() + } + + mapval, ok := result.(map[string]interface{}) + if !ok { + return dvalue.String() + } + + wrapped, ok := mapval["wrapped"] + if !ok { + return dvalue.String() + } + + return wrapped + + } + + /* (3) NIL if unknown type */ + return dvalue.Interface() + +} diff --git a/internal/reqdata/set_test.go b/internal/reqdata/set_test.go index 8458afa..efcb459 100644 --- a/internal/reqdata/set_test.go +++ b/internal/reqdata/set_test.go @@ -280,7 +280,7 @@ func TestExtractQuery(t *testing.T) { t.FailNow() } - cast, canCast := param.Value.([]string) + cast, canCast := param.([]interface{}) if !canCast { t.Errorf("should return a []string (got '%v')", cast) t.FailNow() @@ -458,9 +458,9 @@ func TestExtractFormUrlEncoded(t *testing.T) { t.FailNow() } - cast, canCast := param.Value.([]string) + cast, canCast := param.([]interface{}) if !canCast { - t.Errorf("should return a []string (got '%v')", cast) + t.Errorf("should return a []interface{} (got '%v')", cast) t.FailNow() } @@ -606,8 +606,8 @@ func TestJsonParameters(t *testing.T) { valueType := reflect.TypeOf(value) - paramValue := param.Value - paramValueType := reflect.TypeOf(param.Value) + paramValue := param + paramValueType := reflect.TypeOf(param) if valueType != paramValueType { t.Errorf("should be of type %v (got '%v')", valueType, paramValueType) @@ -762,8 +762,8 @@ x valueType := reflect.TypeOf(value) - paramValue := param.Value - paramValueType := reflect.TypeOf(param.Value) + paramValue := param + paramValueType := reflect.TypeOf(param) if valueType != paramValueType { t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)