github.com/opentelekomcloud/gophertelekomcloud@v0.9.3/internal/build/query_string.go (about) 1 package build 2 3 import ( 4 "fmt" 5 "net/url" 6 "reflect" 7 "strconv" 8 "strings" 9 10 "github.com/opentelekomcloud/gophertelekomcloud/internal/multierr" 11 ) 12 13 /* 14 QueryString is an internal function to be used by request methods in 15 individual resource packages. 16 17 It accepts a tagged structure and expands it into a URL struct. Field names are 18 converted into query parameters based on a "q" tag. For example: 19 20 type QueryStruct struct { 21 Bar string `q:"x_bar"` 22 Baz int `q:"lorem_ipsum"` 23 } 24 25 instance := QueryStruct{ 26 Bar: "AAA", 27 Baz: "BBB", 28 } 29 30 will be converted into "?x_bar=AAA&lorem_ipsum=BBB". 31 32 The struct's fields may be strings, integers, or boolean values. Fields left at 33 their type's zero value will be omitted from the query. 34 */ 35 func QueryString(opts interface{}) (*url.URL, error) { 36 if opts == nil { 37 return nil, fmt.Errorf("error building query string: %w", ErrNilOpts) 38 } 39 40 optsValue := reflect.ValueOf(opts) 41 if optsValue.Kind() == reflect.Ptr { 42 optsValue = optsValue.Elem() 43 } 44 45 optsType := reflect.TypeOf(opts) 46 if optsType.Kind() == reflect.Ptr { 47 optsType = optsType.Elem() 48 } 49 50 params := url.Values{} 51 52 if optsValue.Kind() != reflect.Struct { 53 // Return an error if the underlying type of 'opts' isn't a struct. 54 return nil, fmt.Errorf("error building query string: options type is not a struct") 55 } 56 57 mErr := multierr.MultiError{} 58 59 for i := 0; i < optsValue.NumField(); i++ { 60 v := optsValue.Field(i) 61 field := optsType.Field(i) 62 63 // Otherwise, the field is not set. 64 // We duplicate the check from ValidateTags to avoid double reflect package usage 65 // TODO: investigate performance difference when using ValidateTags 66 if v.IsZero() { 67 if structFieldRequired(field) { 68 // And the field is required. Return an error. 69 mErr = append(mErr, fmt.Errorf("required query parameter [%s] not set", field.Name)) 70 } 71 continue // skip empty fields 72 } 73 74 if v.Kind() == reflect.Ptr { 75 v = v.Elem() 76 } 77 78 // if the field is set, add it to the slice of query pieces 79 qTag := field.Tag.Get("q") 80 81 // if the field has a 'q' tag, it goes in the query string 82 if qTag == "" { 83 continue 84 } 85 86 tags := strings.Split(qTag, ",") 87 88 switch v.Kind() { 89 case reflect.String: 90 params.Add(tags[0], v.String()) 91 case reflect.Int, reflect.Int32, reflect.Int64: 92 params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) 93 case reflect.Bool: 94 params.Add(tags[0], strconv.FormatBool(v.Bool())) 95 case reflect.Slice: 96 switch v.Type().Elem() { 97 case reflect.TypeOf(0): 98 for i := 0; i < v.Len(); i++ { 99 params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10)) 100 } 101 default: 102 for i := 0; i < v.Len(); i++ { 103 params.Add(tags[0], v.Index(i).String()) 104 } 105 } 106 case reflect.Map: 107 keyKind := v.Type().Key().Kind() 108 valueKind := v.Type().Elem().Kind() 109 if keyKind == reflect.String && valueKind == reflect.String { 110 var s []string 111 for _, k := range v.MapKeys() { 112 value := v.MapIndex(k).String() 113 s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value)) 114 } 115 params.Add(tags[0], fmt.Sprintf("{%s}", strings.Join(s, ", "))) 116 } else { 117 mErr = append(mErr, fmt.Errorf("expected map[string]string, got map[%s]%s", keyKind, valueKind)) 118 } 119 } 120 } 121 122 if err := mErr.ErrorOrNil(); err != nil { 123 return nil, fmt.Errorf("error building query string: %w", err) 124 } 125 126 return &url.URL{RawQuery: params.Encode()}, nil 127 }