github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/params.go (about)

     1  package gophercloud
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/url"
     7  	"reflect"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  )
    12  
    13  /*
    14  BuildRequestBody builds a map[string]interface from the given `struct`, or
    15  collection of `structs`. If parent is not an empty string, the final
    16  map[string]interface returned will encapsulate the built one. Parent is
    17  required when passing a list of `structs`.
    18  For example:
    19  
    20  	disk := 1
    21  	createOpts := flavors.CreateOpts{
    22  	  ID:         "1",
    23  	  Name:       "m1.tiny",
    24  	  Disk:       &disk,
    25  	  RAM:        512,
    26  	  VCPUs:      1,
    27  	  RxTxFactor: 1.0,
    28  	}
    29  
    30  	body, err := gophercloud.BuildRequestBody(createOpts, "flavor")
    31  
    32  
    33  	opts := []rules.CreateOpts{
    34  		{
    35  			Direction:     "ingress",
    36  			PortRangeMin:  80,
    37  			EtherType:     rules.EtherType4,
    38  			PortRangeMax:  80,
    39  			Protocol:      "tcp",
    40  			SecGroupID:    "a7734e61-b545-452d-a3cd-0189cbd9747a",
    41  		},
    42  		{
    43  			Direction:    "ingress",
    44  			PortRangeMin: 443,
    45  			EtherType:    rules.EtherType4,
    46  			PortRangeMax: 443,
    47  			Protocol:     "tcp",
    48  			SecGroupID:   "a7734e61-b545-452d-a3cd-0189cbd9747a",
    49  		},
    50  	}
    51  
    52  	body, err := gophercloud.BuildRequestBody(opts, "security_group_rules")
    53  
    54  The above examples can be run as-is, however it is recommended to look at how
    55  BuildRequestBody is used within Gophercloud to more fully understand how it
    56  fits within the request process as a whole rather than use it directly as shown
    57  above.
    58  */
    59  func BuildRequestBody(opts any, parent string) (map[string]any, error) {
    60  	optsValue := reflect.ValueOf(opts)
    61  	if optsValue.Kind() == reflect.Ptr {
    62  		optsValue = optsValue.Elem()
    63  	}
    64  
    65  	optsType := reflect.TypeOf(opts)
    66  	if optsType.Kind() == reflect.Ptr {
    67  		optsType = optsType.Elem()
    68  	}
    69  
    70  	optsMap := make(map[string]any)
    71  	switch optsValue.Kind() {
    72  	case reflect.Struct:
    73  		//fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind())
    74  		for i := 0; i < optsValue.NumField(); i++ {
    75  			v := optsValue.Field(i)
    76  			f := optsType.Field(i)
    77  
    78  			if f.Name != strings.Title(f.Name) {
    79  				//fmt.Printf("Skipping field: %s...\n", f.Name)
    80  				continue
    81  			}
    82  
    83  			//fmt.Printf("Starting on field: %s...\n", f.Name)
    84  
    85  			zero := isZero(v)
    86  			//fmt.Printf("v is zero?: %v\n", zero)
    87  
    88  			// if the field has a required tag that's set to "true"
    89  			if requiredTag := f.Tag.Get("required"); requiredTag == "true" {
    90  				//fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero)
    91  				// if the field's value is zero, return a missing-argument error
    92  				if zero {
    93  					// if the field has a 'required' tag, it can't have a zero-value
    94  					err := ErrMissingInput{}
    95  					err.Argument = f.Name
    96  					return nil, err
    97  				}
    98  			}
    99  
   100  			if xorTag := f.Tag.Get("xor"); xorTag != "" {
   101  				//fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag)
   102  				xorField := optsValue.FieldByName(xorTag)
   103  				var xorFieldIsZero bool
   104  				if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) {
   105  					xorFieldIsZero = true
   106  				} else {
   107  					if xorField.Kind() == reflect.Ptr {
   108  						xorField = xorField.Elem()
   109  					}
   110  					xorFieldIsZero = isZero(xorField)
   111  				}
   112  				if !(zero != xorFieldIsZero) {
   113  					err := ErrMissingInput{}
   114  					err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag)
   115  					err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag)
   116  					return nil, err
   117  				}
   118  			}
   119  
   120  			if orTag := f.Tag.Get("or"); orTag != "" {
   121  				//fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag)
   122  				//fmt.Printf("field is zero?: %v\n", zero)
   123  				if zero {
   124  					orField := optsValue.FieldByName(orTag)
   125  					var orFieldIsZero bool
   126  					if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) {
   127  						orFieldIsZero = true
   128  					} else {
   129  						if orField.Kind() == reflect.Ptr {
   130  							orField = orField.Elem()
   131  						}
   132  						orFieldIsZero = isZero(orField)
   133  					}
   134  					if orFieldIsZero {
   135  						err := ErrMissingInput{}
   136  						err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag)
   137  						err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag)
   138  						return nil, err
   139  					}
   140  				}
   141  			}
   142  
   143  			jsonTag := f.Tag.Get("json")
   144  			if jsonTag == "-" {
   145  				continue
   146  			}
   147  
   148  			if v.Kind() == reflect.Slice || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Slice) {
   149  				sliceValue := v
   150  				if sliceValue.Kind() == reflect.Ptr {
   151  					sliceValue = sliceValue.Elem()
   152  				}
   153  
   154  				for i := 0; i < sliceValue.Len(); i++ {
   155  					element := sliceValue.Index(i)
   156  					if element.Kind() == reflect.Struct || (element.Kind() == reflect.Ptr && element.Elem().Kind() == reflect.Struct) {
   157  						_, err := BuildRequestBody(element.Interface(), "")
   158  						if err != nil {
   159  							return nil, err
   160  						}
   161  					}
   162  				}
   163  			}
   164  			if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) {
   165  				if zero {
   166  					//fmt.Printf("value before change: %+v\n", optsValue.Field(i))
   167  					if jsonTag != "" {
   168  						jsonTagPieces := strings.Split(jsonTag, ",")
   169  						if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" {
   170  							if v.CanSet() {
   171  								if !v.IsNil() {
   172  									if v.Kind() == reflect.Ptr {
   173  										v.Set(reflect.Zero(v.Type()))
   174  									}
   175  								}
   176  								//fmt.Printf("value after change: %+v\n", optsValue.Field(i))
   177  							}
   178  						}
   179  					}
   180  					continue
   181  				}
   182  
   183  				//fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name)
   184  				_, err := BuildRequestBody(v.Interface(), f.Name)
   185  				if err != nil {
   186  					return nil, err
   187  				}
   188  			}
   189  		}
   190  
   191  		//fmt.Printf("opts: %+v \n", opts)
   192  
   193  		b, err := json.Marshal(opts)
   194  		if err != nil {
   195  			return nil, err
   196  		}
   197  
   198  		//fmt.Printf("string(b): %s\n", string(b))
   199  
   200  		err = json.Unmarshal(b, &optsMap)
   201  		if err != nil {
   202  			return nil, err
   203  		}
   204  
   205  		//fmt.Printf("optsMap: %+v\n", optsMap)
   206  
   207  		if parent != "" {
   208  			optsMap = map[string]any{parent: optsMap}
   209  		}
   210  		//fmt.Printf("optsMap after parent added: %+v\n", optsMap)
   211  		return optsMap, nil
   212  	case reflect.Slice, reflect.Array:
   213  		optsMaps := make([]map[string]any, optsValue.Len())
   214  		for i := 0; i < optsValue.Len(); i++ {
   215  			b, err := BuildRequestBody(optsValue.Index(i).Interface(), "")
   216  			if err != nil {
   217  				return nil, err
   218  			}
   219  			optsMaps[i] = b
   220  		}
   221  		if parent == "" {
   222  			return nil, fmt.Errorf("Parent is required when passing an array or a slice.")
   223  		}
   224  		return map[string]any{parent: optsMaps}, nil
   225  	}
   226  	// Return an error if we can't work with the underlying type of 'opts'
   227  	return nil, fmt.Errorf("Options type is not a struct, a slice, or an array.")
   228  }
   229  
   230  // EnabledState is a convenience type, mostly used in Create and Update
   231  // operations. Because the zero value of a bool is FALSE, we need to use a
   232  // pointer instead to indicate zero-ness.
   233  type EnabledState *bool
   234  
   235  // Convenience vars for EnabledState values.
   236  var (
   237  	iTrue  = true
   238  	iFalse = false
   239  
   240  	Enabled  EnabledState = &iTrue
   241  	Disabled EnabledState = &iFalse
   242  )
   243  
   244  // IPVersion is a type for the possible IP address versions. Valid instances
   245  // are IPv4 and IPv6
   246  type IPVersion int
   247  
   248  const (
   249  	// IPv4 is used for IP version 4 addresses
   250  	IPv4 IPVersion = 4
   251  	// IPv6 is used for IP version 6 addresses
   252  	IPv6 IPVersion = 6
   253  )
   254  
   255  // IntToPointer is a function for converting integers into integer pointers.
   256  // This is useful when passing in options to operations.
   257  func IntToPointer(i int) *int {
   258  	return &i
   259  }
   260  
   261  /*
   262  MaybeString is an internal function to be used by request methods in individual
   263  resource packages.
   264  
   265  It takes a string that might be a zero value and returns either a pointer to its
   266  address or nil. This is useful for allowing users to conveniently omit values
   267  from an options struct by leaving them zeroed, but still pass nil to the JSON
   268  serializer so they'll be omitted from the request body.
   269  */
   270  func MaybeString(original string) *string {
   271  	if original != "" {
   272  		return &original
   273  	}
   274  	return nil
   275  }
   276  
   277  /*
   278  MaybeInt is an internal function to be used by request methods in individual
   279  resource packages.
   280  
   281  Like MaybeString, it accepts an int that may or may not be a zero value, and
   282  returns either a pointer to its address or nil. It's intended to hint that the
   283  JSON serializer should omit its field.
   284  */
   285  func MaybeInt(original int) *int {
   286  	if original != 0 {
   287  		return &original
   288  	}
   289  	return nil
   290  }
   291  
   292  /*
   293  func isUnderlyingStructZero(v reflect.Value) bool {
   294  	switch v.Kind() {
   295  	case reflect.Ptr:
   296  		return isUnderlyingStructZero(v.Elem())
   297  	default:
   298  		return isZero(v)
   299  	}
   300  }
   301  */
   302  
   303  var t time.Time
   304  
   305  func isZero(v reflect.Value) bool {
   306  	//fmt.Printf("\n\nchecking isZero for value: %+v\n", v)
   307  	switch v.Kind() {
   308  	case reflect.Ptr:
   309  		if v.IsNil() {
   310  			return true
   311  		}
   312  		return false
   313  	case reflect.Func, reflect.Map, reflect.Slice:
   314  		return v.IsNil()
   315  	case reflect.Array:
   316  		z := true
   317  		for i := 0; i < v.Len(); i++ {
   318  			z = z && isZero(v.Index(i))
   319  		}
   320  		return z
   321  	case reflect.Struct:
   322  		if v.Type() == reflect.TypeOf(t) {
   323  			return v.Interface().(time.Time).IsZero()
   324  		}
   325  		z := true
   326  		for i := 0; i < v.NumField(); i++ {
   327  			z = z && isZero(v.Field(i))
   328  		}
   329  		return z
   330  	}
   331  	// Compare other types directly:
   332  	z := reflect.Zero(v.Type())
   333  	//fmt.Printf("zero type for value: %+v\n\n\n", z)
   334  	return v.Interface() == z.Interface()
   335  }
   336  
   337  /*
   338  BuildQueryString is an internal function to be used by request methods in
   339  individual resource packages.
   340  
   341  It accepts a tagged structure and expands it into a URL struct. Field names are
   342  converted into query parameters based on a "q" tag. For example:
   343  
   344  	type struct Something {
   345  	   Bar string `q:"x_bar"`
   346  	   Baz int    `q:"lorem_ipsum"`
   347  	}
   348  
   349  	instance := Something{
   350  	   Bar: "AAA",
   351  	   Baz: "BBB",
   352  	}
   353  
   354  will be converted into "?x_bar=AAA&lorem_ipsum=BBB".
   355  
   356  The struct's fields may be strings, integers, slices, or boolean values. Fields
   357  left at their type's zero value will be omitted from the query.
   358  
   359  Slice are handled in one of two ways:
   360  
   361  	type struct Something {
   362  	   Bar []string `q:"bar"` // E.g. ?bar=1&bar=2
   363  	   Baz []int    `q:"baz" format="comma-separated"` // E.g. ?baz=1,2
   364  	}
   365  */
   366  func BuildQueryString(opts any) (*url.URL, error) {
   367  	optsValue := reflect.ValueOf(opts)
   368  	if optsValue.Kind() == reflect.Ptr {
   369  		optsValue = optsValue.Elem()
   370  	}
   371  
   372  	optsType := reflect.TypeOf(opts)
   373  	if optsType.Kind() == reflect.Ptr {
   374  		optsType = optsType.Elem()
   375  	}
   376  
   377  	params := url.Values{}
   378  
   379  	if optsValue.Kind() == reflect.Struct {
   380  		for i := 0; i < optsValue.NumField(); i++ {
   381  			v := optsValue.Field(i)
   382  			f := optsType.Field(i)
   383  			qTag := f.Tag.Get("q")
   384  
   385  			// if the field has a 'q' tag, it goes in the query string
   386  			if qTag != "" {
   387  				tags := strings.Split(qTag, ",")
   388  
   389  				// if the field is set, add it to the slice of query pieces
   390  				if !isZero(v) {
   391  				loop:
   392  					switch v.Kind() {
   393  					case reflect.Ptr:
   394  						v = v.Elem()
   395  						goto loop
   396  					case reflect.String:
   397  						params.Add(tags[0], v.String())
   398  					case reflect.Int:
   399  						params.Add(tags[0], strconv.FormatInt(v.Int(), 10))
   400  					case reflect.Bool:
   401  						params.Add(tags[0], strconv.FormatBool(v.Bool()))
   402  					case reflect.Slice:
   403  						var values []string
   404  						switch v.Type().Elem() {
   405  						case reflect.TypeOf(0):
   406  							for i := 0; i < v.Len(); i++ {
   407  								values = append(values, strconv.FormatInt(v.Index(i).Int(), 10))
   408  							}
   409  						default:
   410  							for i := 0; i < v.Len(); i++ {
   411  								values = append(values, v.Index(i).String())
   412  							}
   413  						}
   414  						if sliceFormat := f.Tag.Get("format"); sliceFormat == "comma-separated" {
   415  							params.Add(tags[0], strings.Join(values, ","))
   416  						} else {
   417  							params[tags[0]] = append(params[tags[0]], values...)
   418  						}
   419  					case reflect.Map:
   420  						if v.Type().Key().Kind() == reflect.String && v.Type().Elem().Kind() == reflect.String {
   421  							var s []string
   422  							for _, k := range v.MapKeys() {
   423  								value := v.MapIndex(k).String()
   424  								s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value))
   425  							}
   426  							params.Add(tags[0], fmt.Sprintf("{%s}", strings.Join(s, ", ")))
   427  						}
   428  					}
   429  				} else {
   430  					// if the field has a 'required' tag, it can't have a zero-value
   431  					if requiredTag := f.Tag.Get("required"); requiredTag == "true" {
   432  						return &url.URL{}, fmt.Errorf("Required query parameter [%s] not set.", f.Name)
   433  					}
   434  				}
   435  			}
   436  		}
   437  
   438  		return &url.URL{RawQuery: params.Encode()}, nil
   439  	}
   440  	// Return an error if the underlying type of 'opts' isn't a struct.
   441  	return nil, fmt.Errorf("Options type is not a struct.")
   442  }
   443  
   444  /*
   445  BuildHeaders is an internal function to be used by request methods in
   446  individual resource packages.
   447  
   448  It accepts an arbitrary tagged structure and produces a string map that's
   449  suitable for use as the HTTP headers of an outgoing request. Field names are
   450  mapped to header names based in "h" tags.
   451  
   452  	type struct Something {
   453  	  Bar string `h:"x_bar"`
   454  	  Baz int    `h:"lorem_ipsum"`
   455  	}
   456  
   457  	instance := Something{
   458  	  Bar: "AAA",
   459  	  Baz: "BBB",
   460  	}
   461  
   462  will be converted into:
   463  
   464  	map[string]string{
   465  	  "x_bar": "AAA",
   466  	  "lorem_ipsum": "BBB",
   467  	}
   468  
   469  Untagged fields and fields left at their zero values are skipped. Integers,
   470  booleans and string values are supported.
   471  */
   472  func BuildHeaders(opts any) (map[string]string, error) {
   473  	optsValue := reflect.ValueOf(opts)
   474  	if optsValue.Kind() == reflect.Ptr {
   475  		optsValue = optsValue.Elem()
   476  	}
   477  
   478  	optsType := reflect.TypeOf(opts)
   479  	if optsType.Kind() == reflect.Ptr {
   480  		optsType = optsType.Elem()
   481  	}
   482  
   483  	optsMap := make(map[string]string)
   484  	if optsValue.Kind() == reflect.Struct {
   485  		for i := 0; i < optsValue.NumField(); i++ {
   486  			v := optsValue.Field(i)
   487  			f := optsType.Field(i)
   488  			hTag := f.Tag.Get("h")
   489  
   490  			// if the field has a 'h' tag, it goes in the header
   491  			if hTag != "" {
   492  				tags := strings.Split(hTag, ",")
   493  
   494  				// if the field is set, add it to the slice of query pieces
   495  				if !isZero(v) {
   496  					if v.Kind() == reflect.Ptr {
   497  						v = v.Elem()
   498  					}
   499  					switch v.Kind() {
   500  					case reflect.String:
   501  						optsMap[tags[0]] = v.String()
   502  					case reflect.Int:
   503  						optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
   504  					case reflect.Int64:
   505  						optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
   506  					case reflect.Bool:
   507  						optsMap[tags[0]] = strconv.FormatBool(v.Bool())
   508  					}
   509  				} else {
   510  					// if the field has a 'required' tag, it can't have a zero-value
   511  					if requiredTag := f.Tag.Get("required"); requiredTag == "true" {
   512  						return optsMap, fmt.Errorf("Required header [%s] not set.", f.Name)
   513  					}
   514  				}
   515  			}
   516  
   517  		}
   518  		return optsMap, nil
   519  	}
   520  	// Return an error if the underlying type of 'opts' isn't a struct.
   521  	return optsMap, fmt.Errorf("Options type is not a struct.")
   522  }
   523  
   524  // IDSliceToQueryString takes a slice of elements and converts them into a query
   525  // string. For example, if name=foo and slice=[]int{20, 40, 60}, then the
   526  // result would be `?name=20&name=40&name=60'
   527  func IDSliceToQueryString(name string, ids []int) string {
   528  	str := ""
   529  	for k, v := range ids {
   530  		if k == 0 {
   531  			str += "?"
   532  		} else {
   533  			str += "&"
   534  		}
   535  		str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v))
   536  	}
   537  	return str
   538  }
   539  
   540  // IntWithinRange returns TRUE if an integer falls within a defined range, and
   541  // FALSE if not.
   542  func IntWithinRange(val, min, max int) bool {
   543  	return val > min && val < max
   544  }