From 1ab65587fa1284d0a4fc373b44ecd73696ee4beb Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <109739087+haseeb-mhr@users.noreply.github.com> Date: Thu, 14 Mar 2024 10:30:04 +0500 Subject: [PATCH] perf(go): refactor code for JSON body and query parameters handling (#64) --- https/callBuilder.go | 66 ++++++++++++++++++------------- https/callBuilder_test.go | 4 +- https/formData.go | 77 +++++++++++++++---------------------- https/formData_test.go | 6 +-- testHelper/bodyMatchers.go | 14 +++---- utilities/apiHelper.go | 48 ++++------------------- utilities/apiHelper_test.go | 12 +++--- 7 files changed, 98 insertions(+), 129 deletions(-) diff --git a/https/callBuilder.go b/https/callBuilder.go index f426d72..31505da 100644 --- a/https/callBuilder.go +++ b/https/callBuilder.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/url" + "reflect" "strconv" "strings" "time" @@ -37,14 +38,14 @@ type baseUrlProvider func(server string) string type CallBuilder interface { AppendPath(path string) AppendTemplateParam(param string) - AppendTemplateParams(params interface{}) + AppendTemplateParams(params any) AppendErrors(errors map[string]ErrorBuilder[error]) BaseUrl(arg string) Method(httpMethodName string) validateMethod() error Accept(acceptHeaderValue string) ContentType(contentTypeHeaderValue string) - Header(name string, value interface{}) + Header(name string, value any) CombineHeaders(headersToMerge map[string]string) QueryParam(name string, value any) QueryParamWithArraySerializationOption(name string, value any, option ArraySerializationOption) @@ -57,7 +58,7 @@ type CallBuilder interface { validateFormData() error Text(body string) FileStream(file FileWrapper) - Json(data interface{}) + Json(data any) validateJson() error intercept(interceptor HttpInterceptor) InterceptRequest(interceptor func(httpRequest *http.Request) *http.Request) @@ -92,7 +93,7 @@ type defaultCallBuilder struct { retryOption RequestRetryOption retryConfig RetryConfiguration clientError error - jsonData interface{} + jsonData any formFields formParams formParams formParams queryParams formParams @@ -169,7 +170,7 @@ func (cb *defaultCallBuilder) AppendPath(path string) { // AppendTemplateParam appends the provided parameter to the existing path in the CallBuilder as a URL template parameter. func (cb *defaultCallBuilder) AppendTemplateParam(param string) { - if strings.Contains(cb.path, "%s") { + if strings.Contains(cb.path, "%v") { cb.path = fmt.Sprintf(cb.path, "/"+url.QueryEscape(param)) } else { cb.AppendPath(url.QueryEscape(param)) @@ -178,15 +179,19 @@ func (cb *defaultCallBuilder) AppendTemplateParam(param string) { // AppendTemplateParams appends the provided parameters to the existing path in the CallBuilder as URL template parameters. // It accepts a slice of strings or a slice of integers as the params argument. -func (cb *defaultCallBuilder) AppendTemplateParams(params interface{}) { - switch x := params.(type) { - case []string: - for _, param := range x { - cb.AppendTemplateParam(param) - } - case []int: - for _, param := range x { - cb.AppendTemplateParam(strconv.Itoa(int(param))) +func (cb *defaultCallBuilder) AppendTemplateParams(params any) { + reflectValue := reflect.ValueOf(params) + if reflectValue.Type().Kind() == reflect.Slice { + for i := 0; i < reflectValue.Len(); i++ { + innerParam := reflectValue.Index(i).Interface() + switch x := innerParam.(type) { + case string: + cb.AppendTemplateParam(x) + case int: + cb.AppendTemplateParam(strconv.Itoa(int(x))) + default: + cb.AppendTemplateParam(fmt.Sprintf("%v", x)) + } } } } @@ -250,7 +255,7 @@ func (cb *defaultCallBuilder) ContentType(contentTypeHeaderValue string) { // It takes the name of the header and the value of the header as arguments. func (cb *defaultCallBuilder) Header( name string, - value interface{}, + value any, ) { if cb.headers == nil { cb.headers = make(map[string]string) @@ -297,8 +302,8 @@ func (cb *defaultCallBuilder) validateQueryParams() error { } // QueryParams sets multiple query parameters for the API call. -// It takes a map of string keys and interface{} values representing the query parameters. -func (cb *defaultCallBuilder) QueryParams(parameters map[string]interface{}) { +// It takes a map of string keys and any values representing the query parameters. +func (cb *defaultCallBuilder) QueryParams(parameters map[string]any) { cb.query = utilities.PrepareQueryParams(cb.query, parameters) } @@ -377,7 +382,7 @@ func (cb *defaultCallBuilder) FileStream(file FileWrapper) { } // Json sets the request body for the API call as JSON. -func (cb *defaultCallBuilder) Json(data interface{}) { +func (cb *defaultCallBuilder) Json(data any) { cb.jsonData = data } @@ -387,22 +392,31 @@ func (cb *defaultCallBuilder) Json(data interface{}) { // If there is an error during marshaling, it returns an internalError. func (cb *defaultCallBuilder) validateJson() error { if cb.jsonData != nil { - bytes, err := json.Marshal(cb.jsonData) + dataBytes, err := json.Marshal(cb.jsonData) if err != nil { return internalError{Body: fmt.Sprintf("Unable to marshal the given data: %v", err.Error()), FileInfo: "CallBuilder.go/validateJson"} } - cb.body = string(bytes) - contentType := JSON_CONTENT_TYPE - var testMap map[string]any - errTest := json.Unmarshal(bytes, &testMap) - if errTest != nil { - contentType = TEXT_CONTENT_TYPE + if !cb.isOAFJson(dataBytes) { + cb.body = string(dataBytes) + cb.setContentTypeIfNotSet(JSON_CONTENT_TYPE) } - cb.setContentTypeIfNotSet(contentType) } return nil } +func (cb *defaultCallBuilder) isOAFJson(dataBytes []byte) bool { + switch reflect.TypeOf(cb.jsonData).Kind() { + case reflect.Struct, reflect.Ptr: + var testObj map[string]any + structErr := json.Unmarshal(dataBytes, &testObj) + if structErr != nil { + cb.Text(fmt.Sprintf("%v", cb.jsonData)) + return true + } + } + return false +} + // setContentTypeIfNotSet sets the "Content-Type" header if it is not already set in the CallBuilder. // It takes the contentType as an argument and sets it as the value for the "Content-Type" header. // If the headers map is nil, it initializes it before setting the header. diff --git a/https/callBuilder_test.go b/https/callBuilder_test.go index eb8e489..08dc33b 100644 --- a/https/callBuilder_test.go +++ b/https/callBuilder_test.go @@ -90,7 +90,7 @@ func TestAppendPathEmptyPath(t *testing.T) { } func TestAppendTemplateParamsStrings(t *testing.T) { - request := GetCallBuilder(ctx, "GET", "/template/%s", nil) + request := GetCallBuilder(ctx, "GET", "/template/%v", nil) request.AppendTemplateParams([]string{"abc", "def"}) _, response, err := request.CallAsJson() if err != nil { @@ -105,7 +105,7 @@ func TestAppendTemplateParamsStrings(t *testing.T) { } func TestAppendTemplateParamsIntegers(t *testing.T) { - request := GetCallBuilder(ctx, "GET", "/template/%s", nil) + request := GetCallBuilder(ctx, "GET", "/template/%v", nil) request.AppendTemplateParams([]int{1, 2, 3, 4, 5}) _, response, err := request.CallAsJson() if err != nil { diff --git a/https/formData.go b/https/formData.go index 1d1d2ae..583c5ab 100644 --- a/https/formData.go +++ b/https/formData.go @@ -21,10 +21,10 @@ type FormParam struct { } type formParam struct { - Key string - Value any - Headers http.Header - ArraySerializationOption ArraySerializationOption + key string + value any + headers http.Header + arraySerializationOption ArraySerializationOption } type formParams []formParam @@ -41,7 +41,7 @@ func (fp *FormParams) Add(formParam FormParam) { // Add appends a FormParam to the FormParams collection. func (fp *formParams) add(formParam formParam) { - if formParam.Value != nil { + if formParam.value != nil { *fp = append(*fp, formParam) } } @@ -53,7 +53,7 @@ func (fp *formParams) prepareFormFields(form url.Values) error { form = url.Values{} } for _, param := range *fp { - paramsMap, err := toMap(param.Key, param.Value, param.ArraySerializationOption) + paramsMap, err := toMap(param.key, param.value, param.arraySerializationOption) if err != nil { return err } @@ -72,22 +72,22 @@ func (fp *formParams) prepareMultipartFields() (bytes.Buffer, string, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) for _, field := range *fp { - switch fieldValue := field.Value.(type) { + switch fieldValue := field.value.(type) { case FileWrapper: mediaParam := map[string]string{ - "name": field.Key, + "name": field.key, "filename": fieldValue.FileName, } - formParamWriter(writer, field.Headers, mediaParam, fieldValue.File) + formParamWriter(writer, field.headers, mediaParam, fieldValue.File) default: - paramsMap, err := toMap(field.Key, field.Value, field.ArraySerializationOption) + paramsMap, err := toMap(field.key, field.value, field.arraySerializationOption) if err != nil { return *body, writer.FormDataContentType(), err } for key, values := range paramsMap { mediaParam := map[string]string{"name": key} for _, value := range values { - formParamWriter(writer, field.Headers, mediaParam, []byte(value)) + formParamWriter(writer, field.headers, mediaParam, []byte(value)) } } } @@ -119,25 +119,16 @@ func formParamWriter( return nil } -func toMap(keyPrefix string, paramObj any, option ArraySerializationOption) (map[string][]string, error) { - if paramObj == nil { +func toMap(keyPrefix string, param any, option ArraySerializationOption) (map[string][]string, error) { + if param == nil { return map[string][]string{}, nil } - var param any - marshalBytes, err := json.Marshal(toStructPtr(paramObj)) - if err == nil && reflect.TypeOf(paramObj).Kind() != reflect.Map { - err = json.Unmarshal(marshalBytes, ¶m) - if err != nil { - return map[string][]string{}, nil - } - } else { - param = paramObj - } - switch reflect.TypeOf(param).Kind() { - case reflect.Struct, reflect.Ptr: + case reflect.Ptr: return processStructAndPtr(keyPrefix, param, option) + case reflect.Struct: + return processStructAndPtr(keyPrefix, toStructPtr(param), option) case reflect.Map: return processMap(keyPrefix, param, option) case reflect.Slice: @@ -148,11 +139,10 @@ func toMap(keyPrefix string, paramObj any, option ArraySerializationOption) (map } func processStructAndPtr(keyPrefix string, param any, option ArraySerializationOption) (map[string][]string, error) { - innerMap, err := structToMap(param) - if err != nil { - return nil, err - } - return toMap(keyPrefix, innerMap, option) + innerData, err := structToAny(param) + if err != nil { return nil, err } + + return toMap(keyPrefix, innerData, option) } func processMap(keyPrefix string, param any, option ArraySerializationOption) (map[string][]string, error) { @@ -175,7 +165,7 @@ func processSlice(keyPrefix string, param any, option ArraySerializationOption) result := make(map[string][]string) for i := 0; i < reflectValue.Len(); i++ { innerStruct := reflectValue.Index(i).Interface() - var indexStr interface{} + var indexStr any switch innerStruct.(type) { case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128, string: indexStr = nil @@ -194,35 +184,32 @@ func processSlice(keyPrefix string, param any, option ArraySerializationOption) func processDefault(keyPrefix string, param any) (map[string][]string, error) { var defaultValue string - switch in := param.(type) { - case string: - defaultValue = in + switch reflect.TypeOf(param).Kind() { + case reflect.String: + defaultValue = fmt.Sprintf("%v", param) default: - dataBytes, err := json.Marshal(in) + dataBytes, err := json.Marshal(param) if err == nil { defaultValue = string(dataBytes) } else { - defaultValue = fmt.Sprintf("%v", in) + defaultValue = fmt.Sprintf("%v", param) } } return map[string][]string{keyPrefix: {defaultValue}}, nil } -// structToMap converts a given data structure to a map. -func structToMap(data any) (map[string]any, error) { - if reflect.TypeOf(data).Kind() != reflect.Ptr { - data = toStructPtr(data) - } +// structToAny converts a given data structure into an any type. +func structToAny(data any) (any, error) { dataBytes, err := json.Marshal(data) if err != nil { return nil, err } - mapData := make(map[string]interface{}) - err = json.Unmarshal(dataBytes, &mapData) - return mapData, err + var innerData any + err = json.Unmarshal(dataBytes, &innerData) + return innerData, err } -// Return a pointer to the supplied struct via interface{} +// Return a pointer to the supplied struct via any func toStructPtr(obj any) any { // Create a new instance of the underlying type vp := reflect.New(reflect.TypeOf(obj)) diff --git a/https/formData_test.go b/https/formData_test.go index a64b4c6..5d6a532 100644 --- a/https/formData_test.go +++ b/https/formData_test.go @@ -19,9 +19,9 @@ func GetStruct() Person { } func TestStructToMap(t *testing.T) { - result, _ := structToMap(GetStruct()) + result, _ := structToAny(GetStruct()) - expected := map[string]interface{}{ + expected := map[string]any{ "Name": "Bisma", "Employed": true, } @@ -32,7 +32,7 @@ func TestStructToMap(t *testing.T) { } func TestStructToMapMarshallingError(t *testing.T) { - result, err := structToMap(math.Inf(1)) + result, err := structToAny(math.Inf(1)) if err == nil && result != nil { t.Error("Failed:\nExpected error in marshalling infinity number") diff --git a/testHelper/bodyMatchers.go b/testHelper/bodyMatchers.go index 1fd617c..66ac2db 100644 --- a/testHelper/bodyMatchers.go +++ b/testHelper/bodyMatchers.go @@ -15,7 +15,7 @@ import ( // NativeBodyMatcher compares the JSON response body with the expected JSON body. func NativeBodyMatcher(test *testing.T, expectedBody string, responseObject any) { responseBytes, _ := json.Marshal(responseObject) - var expected, response interface{} + var expected, response any expectedError := json.Unmarshal([]byte(expectedBody), &expected) responseError := json.Unmarshal(responseBytes, &response) @@ -32,7 +32,7 @@ func NativeBodyMatcher(test *testing.T, expectedBody string, responseObject any) // The responseObject and expectedBody should have the same keys. func KeysBodyMatcher(test *testing.T, expectedBody string, responseObject any, checkArrayCount, checkArrayOrder bool) { responseBytes, _ := json.Marshal(responseObject) - var response, expected map[string]interface{} + var response, expected map[string]any responseErr := json.Unmarshal(responseBytes, &response) expectedErr := json.Unmarshal([]byte(expectedBody), &expected) @@ -49,7 +49,7 @@ func KeysBodyMatcher(test *testing.T, expectedBody string, responseObject any, c // The responseObject and expectedBody should have the same keys and their corresponding values should be equal. func KeysAndValuesBodyMatcher[T any](test *testing.T, expectedBody string, responseObject T, checkArrayCount, checkArrayOrder bool) { responseBytes, _ := json.Marshal(&responseObject) - var response, expected map[string]interface{} + var response, expected map[string]any responseErr := json.Unmarshal(responseBytes, &response) expectedErr := json.Unmarshal([]byte(expectedBody), &expected) @@ -64,7 +64,7 @@ func KeysAndValuesBodyMatcher[T any](test *testing.T, expectedBody string, respo // matchKeysAndValues is a helper function used by KeysBodyMatcher and KeysAndValuesBodyMatcher // to compare the JSON keys and values. -func matchKeysAndValues(response, expected map[string]interface{}, checkArrayCount, checkArrayOrder, checkValues bool) bool { +func matchKeysAndValues(response, expected map[string]any, checkArrayCount, checkArrayOrder, checkValues bool) bool { if checkArrayCount && len(expected) != len(response) { return false } @@ -74,8 +74,8 @@ func matchKeysAndValues(response, expected map[string]interface{}, checkArrayCou if reflect.ValueOf(value).Kind() != reflect.Map { return false } - responseSubMap := responseValue.(map[string]interface{}) - expectedSubMap := value.(map[string]interface{}) + responseSubMap := responseValue.(map[string]any) + expectedSubMap := value.(map[string]any) if !matchKeysAndValues(responseSubMap, expectedSubMap, checkArrayCount, checkArrayOrder, checkValues) { return false } @@ -113,6 +113,6 @@ func IsSameInputBytes(test *testing.T, expectedBytes []byte, receivedBytes []byt } // SliceToCommaSeparatedString converts a slice to a comma-separated string representation. -func SliceToCommaSeparatedString(slice interface{}) string { +func SliceToCommaSeparatedString(slice any) string { return strings.Join(strings.Split(fmt.Sprint(slice), " "), ",") } diff --git a/utilities/apiHelper.go b/utilities/apiHelper.go index 5a8f358..b90e4e0 100644 --- a/utilities/apiHelper.go +++ b/utilities/apiHelper.go @@ -29,7 +29,7 @@ func DecodeResults[T any](decoder *json.Decoder) (T, error) { } // PrepareQueryParams adds key-value pairs from the data map to the existing URL query parameters. -func PrepareQueryParams(queryParams url.Values, data map[string]interface{}) url.Values { +func PrepareQueryParams(queryParams url.Values, data map[string]any) url.Values { if queryParams == nil { queryParams = url.Values{} } @@ -42,58 +42,26 @@ func PrepareQueryParams(queryParams url.Values, data map[string]interface{}) url // JsonDecoderToString decodes a JSON value from the provided json.Decoder into a string. func JsonDecoderToString(dec *json.Decoder) (string, error) { - var str string - for { - if err := dec.Decode(&str); err == io.EOF { - break - } else if err != nil { - return "", err - } - } - return str, nil + return DecodeResults[string](dec) } // JsonDecoderToStringSlice decodes a JSON array from the provided json.Decoder into a string slice. func JsonDecoderToStringSlice(dec *json.Decoder) ([]string, error) { - var arr []string - for { - if err := dec.Decode(&arr); err == io.EOF { - break - } else if err != nil { - return nil, err - } - } - return arr, nil + return DecodeResults[[]string](dec) } // JsonDecoderToIntSlice decodes a JSON array from the provided json.Decoder into an int slice. func JsonDecoderToIntSlice(dec *json.Decoder) ([]int, error) { - var arr []int - for { - if err := dec.Decode(&arr); err == io.EOF { - break - } else if err != nil { - return nil, err - } - } - return arr, nil + return DecodeResults[[]int](dec) } // JsonDecoderToBooleanSlice decodes a JSON array from the provided json.Decoder into a bool slice. func JsonDecoderToBooleanSlice(dec *json.Decoder) ([]bool, error) { - var arr []bool - for { - if err := dec.Decode(&arr); err == io.EOF { - break - } else if err != nil { - return nil, err - } - } - return arr, nil + return DecodeResults[[]bool](dec) } // ToTimeSlice converts a slice of strings or int64 values to a slice of time.Time values using the specified format. -func ToTimeSlice(slice interface{}, format string) ([]time.Time, error) { +func ToTimeSlice(slice any, format string) ([]time.Time, error) { result := make([]time.Time, 0) if slice == nil { return []time.Time{}, nil @@ -136,7 +104,7 @@ func TimeToStringSlice(slice []time.Time, format string) []string { } // ToTimeMap converts a map with string or int64 values to a map with time.Time values using the specified format. -func ToTimeMap(dict interface{}, format string) (map[string]time.Time, error) { +func ToTimeMap(dict any, format string) (map[string]time.Time, error) { result := make(map[string]time.Time) if dict == nil { return map[string]time.Time{}, nil @@ -160,7 +128,7 @@ func ToTimeMap(dict interface{}, format string) (map[string]time.Time, error) { } // ToNullableTimeMap converts a map with nullable string or int64 values to a map with nullable time.Time values using the specified format. -func ToNullableTimeMap(dict interface{}, format string) (map[string]*time.Time, error) { +func ToNullableTimeMap(dict any, format string) (map[string]*time.Time, error) { result := make(map[string]*time.Time) if dict == nil { return map[string]*time.Time{}, nil diff --git a/utilities/apiHelper_test.go b/utilities/apiHelper_test.go index 49c7d71..88a6599 100644 --- a/utilities/apiHelper_test.go +++ b/utilities/apiHelper_test.go @@ -559,7 +559,7 @@ func TestPrepareQueryParamsDuplicateData(t *testing.T) { "key": []string{"value"}, "key1": []string{"1"}, } - data := map[string]interface{}{ + data := map[string]any{ "key": "value", "key1": 1, } @@ -590,7 +590,7 @@ func TestPrepareQueryParamsNilData(t *testing.T) { } func TestPrepareQueryParamsNilQueryParams(t *testing.T) { - data := map[string]interface{}{ + data := map[string]any{ "key": "value", "key1": 1, } @@ -606,7 +606,7 @@ func TestPrepareQueryParamsNilQueryParams(t *testing.T) { func TestPrepareQueryParamsEmptyQueryParams(t *testing.T) { queryParams := url.Values{} - data := map[string]interface{}{ + data := map[string]any{ "key": "value", "key1": 1, } @@ -625,7 +625,7 @@ func TestPrepareQueryParamsAppendQueryParams(t *testing.T) { "key": []string{"value"}, "key1": []string{"1"}, } - data := map[string]interface{}{ + data := map[string]any{ "key": "value1", "key1": 2, } @@ -644,7 +644,7 @@ func TestPrepareQueryParamsAppendEmptyData(t *testing.T) { "key": []string{"value"}, "key1": []string{"1"}, } - data := map[string]interface{}{} + data := map[string]any{} result := PrepareQueryParams(queryParams, data) expected := url.Values{ "key": []string{"value"}, @@ -766,7 +766,7 @@ func GetTimeSlice(format string) []time.Time { return []time.Time{time1, time2} } -func GetJsonDecoded(arr interface{}) *json.Decoder { +func GetJsonDecoded(arr any) *json.Decoder { buffer := &bytes.Buffer{} json.NewEncoder(buffer).Encode(arr) byteSlice := buffer.Bytes()