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 }