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  }