Skip to content

Commit

Permalink
perf(go): refactor code for JSON body and query parameters handling (#64
Browse files Browse the repository at this point in the history
)
  • Loading branch information
haseeb-mhr authored Mar 14, 2024
1 parent e8e4c2f commit 1ab6558
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 129 deletions.
66 changes: 40 additions & 26 deletions https/callBuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -92,7 +93,7 @@ type defaultCallBuilder struct {
retryOption RequestRetryOption
retryConfig RetryConfiguration
clientError error
jsonData interface{}
jsonData any
formFields formParams
formParams formParams
queryParams formParams
Expand Down Expand Up @@ -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))
Expand All @@ -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))
}
}
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions https/callBuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
77 changes: 32 additions & 45 deletions https/formData.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
Expand All @@ -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
}
Expand All @@ -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))
}
}
}
Expand Down Expand Up @@ -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, &param)
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:
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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))
Expand Down
6 changes: 3 additions & 3 deletions https/formData_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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")
Expand Down
Loading

0 comments on commit 1ab6558

Please sign in to comment.