github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/results.go (about) 1 package gophercloud 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "reflect" 10 "strconv" 11 "time" 12 ) 13 14 /* 15 Result is an internal type to be used by individual resource packages, but its 16 methods will be available on a wide variety of user-facing embedding types. 17 18 It acts as a base struct that other Result types, returned from request 19 functions, can embed for convenience. All Results capture basic information 20 from the HTTP transaction that was performed, including the response body, 21 HTTP headers, and any errors that happened. 22 23 Generally, each Result type will have an Extract method that can be used to 24 further interpret the result's payload in a specific context. Extensions or 25 providers can then provide additional extraction functions to pull out 26 provider- or extension-specific information as well. 27 */ 28 type Result struct { 29 // Body is the payload of the HTTP response from the server. In most cases, 30 // this will be the deserialized JSON structure. 31 Body any 32 33 // StatusCode is the HTTP status code of the original response. Will be 34 // one of the OkCodes defined on the gophercloud.RequestOpts that was 35 // used in the request. 36 StatusCode int 37 38 // Header contains the HTTP header structure from the original response. 39 Header http.Header 40 41 // Err is an error that occurred during the operation. It's deferred until 42 // extraction to make it easier to chain the Extract call. 43 Err error 44 } 45 46 // ExtractInto allows users to provide an object into which `Extract` will extract 47 // the `Result.Body`. This would be useful for OpenStack providers that have 48 // different fields in the response object than OpenStack proper. 49 func (r Result) ExtractInto(to any) error { 50 if r.Err != nil { 51 return r.Err 52 } 53 54 if reader, ok := r.Body.(io.Reader); ok { 55 if readCloser, ok := reader.(io.Closer); ok { 56 defer readCloser.Close() 57 } 58 return json.NewDecoder(reader).Decode(to) 59 } 60 61 b, err := json.Marshal(r.Body) 62 if err != nil { 63 return err 64 } 65 err = json.Unmarshal(b, to) 66 67 return err 68 } 69 70 func (r Result) extractIntoPtr(to any, label string) error { 71 if label == "" { 72 return r.ExtractInto(&to) 73 } 74 75 var m map[string]any 76 err := r.ExtractInto(&m) 77 if err != nil { 78 return err 79 } 80 81 b, err := json.Marshal(m[label]) 82 if err != nil { 83 return err 84 } 85 86 toValue := reflect.ValueOf(to) 87 if toValue.Kind() == reflect.Ptr { 88 toValue = toValue.Elem() 89 } 90 91 switch toValue.Kind() { 92 case reflect.Slice: 93 typeOfV := toValue.Type().Elem() 94 if typeOfV.Kind() == reflect.Struct { 95 if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { 96 newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0) 97 98 if mSlice, ok := m[label].([]any); ok { 99 for _, v := range mSlice { 100 // For each iteration of the slice, we create a new struct. 101 // This is to work around a bug where elements of a slice 102 // are reused and not overwritten when the same copy of the 103 // struct is used: 104 // 105 // https://github.com/golang/go/issues/21092 106 // https://github.com/golang/go/issues/24155 107 // https://play.golang.org/p/NHo3ywlPZli 108 newType := reflect.New(typeOfV).Elem() 109 110 b, err := json.Marshal(v) 111 if err != nil { 112 return err 113 } 114 115 // This is needed for structs with an UnmarshalJSON method. 116 // Technically this is just unmarshalling the response into 117 // a struct that is never used, but it's good enough to 118 // trigger the UnmarshalJSON method. 119 for i := 0; i < newType.NumField(); i++ { 120 s := newType.Field(i).Addr().Interface() 121 122 // Unmarshal is used rather than NewDecoder to also work 123 // around the above-mentioned bug. 124 err = json.Unmarshal(b, s) 125 if err != nil { 126 return err 127 } 128 } 129 130 newSlice = reflect.Append(newSlice, newType) 131 } 132 } 133 134 // "to" should now be properly modeled to receive the 135 // JSON response body and unmarshal into all the correct 136 // fields of the struct or composed extension struct 137 // at the end of this method. 138 toValue.Set(newSlice) 139 140 // jtopjian: This was put into place to resolve the issue 141 // described at 142 // https://github.com/gophercloud/gophercloud/issues/1963 143 // 144 // This probably isn't the best fix, but it appears to 145 // be resolving the issue, so I'm going to implement it 146 // for now. 147 // 148 // For future readers, this entire case statement could 149 // use a review. 150 return nil 151 } 152 } 153 case reflect.Struct: 154 typeOfV := toValue.Type() 155 if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { 156 for i := 0; i < toValue.NumField(); i++ { 157 toField := toValue.Field(i) 158 if toField.Kind() == reflect.Struct { 159 s := toField.Addr().Interface() 160 err = json.NewDecoder(bytes.NewReader(b)).Decode(s) 161 if err != nil { 162 return err 163 } 164 } 165 } 166 } 167 } 168 169 err = json.Unmarshal(b, &to) 170 return err 171 } 172 173 // ExtractIntoStructPtr will unmarshal the Result (r) into the provided 174 // any (to). 175 // 176 // NOTE: For internal use only 177 // 178 // `to` must be a pointer to an underlying struct type 179 // 180 // If provided, `label` will be filtered out of the response 181 // body prior to `r` being unmarshalled into `to`. 182 func (r Result) ExtractIntoStructPtr(to any, label string) error { 183 if r.Err != nil { 184 return r.Err 185 } 186 187 if to == nil { 188 return fmt.Errorf("Expected pointer, got %T", to) 189 } 190 191 t := reflect.TypeOf(to) 192 if k := t.Kind(); k != reflect.Ptr { 193 return fmt.Errorf("Expected pointer, got %v", k) 194 } 195 196 if reflect.ValueOf(to).IsNil() { 197 return fmt.Errorf("Expected pointer, got %T", to) 198 } 199 200 switch t.Elem().Kind() { 201 case reflect.Struct: 202 return r.extractIntoPtr(to, label) 203 default: 204 return fmt.Errorf("Expected pointer to struct, got: %v", t) 205 } 206 } 207 208 // ExtractIntoSlicePtr will unmarshal the Result (r) into the provided 209 // any (to). 210 // 211 // NOTE: For internal use only 212 // 213 // `to` must be a pointer to an underlying slice type 214 // 215 // If provided, `label` will be filtered out of the response 216 // body prior to `r` being unmarshalled into `to`. 217 func (r Result) ExtractIntoSlicePtr(to any, label string) error { 218 if r.Err != nil { 219 return r.Err 220 } 221 222 if to == nil { 223 return fmt.Errorf("Expected pointer, got %T", to) 224 } 225 226 t := reflect.TypeOf(to) 227 if k := t.Kind(); k != reflect.Ptr { 228 return fmt.Errorf("Expected pointer, got %v", k) 229 } 230 231 if reflect.ValueOf(to).IsNil() { 232 return fmt.Errorf("Expected pointer, got %T", to) 233 } 234 235 switch t.Elem().Kind() { 236 case reflect.Slice: 237 return r.extractIntoPtr(to, label) 238 default: 239 return fmt.Errorf("Expected pointer to slice, got: %v", t) 240 } 241 } 242 243 // PrettyPrintJSON creates a string containing the full response body as 244 // pretty-printed JSON. It's useful for capturing test fixtures and for 245 // debugging extraction bugs. If you include its output in an issue related to 246 // a buggy extraction function, we will all love you forever. 247 func (r Result) PrettyPrintJSON() string { 248 pretty, err := json.MarshalIndent(r.Body, "", " ") 249 if err != nil { 250 panic(err.Error()) 251 } 252 return string(pretty) 253 } 254 255 // ErrResult is an internal type to be used by individual resource packages, but 256 // its methods will be available on a wide variety of user-facing embedding 257 // types. 258 // 259 // It represents results that only contain a potential error and 260 // nothing else. Usually, if the operation executed successfully, the Err field 261 // will be nil; otherwise it will be stocked with a relevant error. Use the 262 // ExtractErr method 263 // to cleanly pull it out. 264 type ErrResult struct { 265 Result 266 } 267 268 // ExtractErr is a function that extracts error information, or nil, from a result. 269 func (r ErrResult) ExtractErr() error { 270 return r.Err 271 } 272 273 /* 274 HeaderResult is an internal type to be used by individual resource packages, but 275 its methods will be available on a wide variety of user-facing embedding types. 276 277 It represents a result that only contains an error (possibly nil) and an 278 http.Header. This is used, for example, by the objectstorage packages in 279 openstack, because most of the operations don't return response bodies, but do 280 have relevant information in headers. 281 */ 282 type HeaderResult struct { 283 Result 284 } 285 286 // ExtractInto allows users to provide an object into which `Extract` will 287 // extract the http.Header headers of the result. 288 func (r HeaderResult) ExtractInto(to any) error { 289 if r.Err != nil { 290 return r.Err 291 } 292 293 tmpHeaderMap := map[string]string{} 294 for k, v := range r.Header { 295 if len(v) > 0 { 296 tmpHeaderMap[k] = v[0] 297 } 298 } 299 300 b, err := json.Marshal(tmpHeaderMap) 301 if err != nil { 302 return err 303 } 304 err = json.Unmarshal(b, to) 305 306 return err 307 } 308 309 // RFC3339Milli describes a common time format used by some API responses. 310 const RFC3339Milli = "2006-01-02T15:04:05.999999Z" 311 312 type JSONRFC3339Milli time.Time 313 314 func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error { 315 b := bytes.NewBuffer(data) 316 dec := json.NewDecoder(b) 317 var s string 318 if err := dec.Decode(&s); err != nil { 319 return err 320 } 321 t, err := time.Parse(RFC3339Milli, s) 322 if err != nil { 323 return err 324 } 325 *jt = JSONRFC3339Milli(t) 326 return nil 327 } 328 329 const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999" 330 331 type JSONRFC3339MilliNoZ time.Time 332 333 func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error { 334 var s string 335 if err := json.Unmarshal(data, &s); err != nil { 336 return err 337 } 338 if s == "" { 339 return nil 340 } 341 t, err := time.Parse(RFC3339MilliNoZ, s) 342 if err != nil { 343 return err 344 } 345 *jt = JSONRFC3339MilliNoZ(t) 346 return nil 347 } 348 349 type JSONRFC1123 time.Time 350 351 func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error { 352 var s string 353 if err := json.Unmarshal(data, &s); err != nil { 354 return err 355 } 356 if s == "" { 357 return nil 358 } 359 t, err := time.Parse(time.RFC1123, s) 360 if err != nil { 361 return err 362 } 363 *jt = JSONRFC1123(t) 364 return nil 365 } 366 367 type JSONUnix time.Time 368 369 func (jt *JSONUnix) UnmarshalJSON(data []byte) error { 370 var s string 371 if err := json.Unmarshal(data, &s); err != nil { 372 return err 373 } 374 if s == "" { 375 return nil 376 } 377 unix, err := strconv.ParseInt(s, 10, 64) 378 if err != nil { 379 return err 380 } 381 t = time.Unix(unix, 0) 382 *jt = JSONUnix(t) 383 return nil 384 } 385 386 // RFC3339NoZ is the time format used in Heat (Orchestration). 387 const RFC3339NoZ = "2006-01-02T15:04:05" 388 389 type JSONRFC3339NoZ time.Time 390 391 func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error { 392 var s string 393 if err := json.Unmarshal(data, &s); err != nil { 394 return err 395 } 396 if s == "" { 397 return nil 398 } 399 t, err := time.Parse(RFC3339NoZ, s) 400 if err != nil { 401 return err 402 } 403 *jt = JSONRFC3339NoZ(t) 404 return nil 405 } 406 407 // RFC3339ZNoT is the time format used in Zun (Containers Service). 408 const RFC3339ZNoT = "2006-01-02 15:04:05-07:00" 409 410 type JSONRFC3339ZNoT time.Time 411 412 func (jt *JSONRFC3339ZNoT) UnmarshalJSON(data []byte) error { 413 var s string 414 if err := json.Unmarshal(data, &s); err != nil { 415 return err 416 } 417 if s == "" { 418 return nil 419 } 420 t, err := time.Parse(RFC3339ZNoT, s) 421 if err != nil { 422 return err 423 } 424 *jt = JSONRFC3339ZNoT(t) 425 return nil 426 } 427 428 // RFC3339ZNoTNoZ is another time format used in Zun (Containers Service). 429 const RFC3339ZNoTNoZ = "2006-01-02 15:04:05" 430 431 type JSONRFC3339ZNoTNoZ time.Time 432 433 func (jt *JSONRFC3339ZNoTNoZ) UnmarshalJSON(data []byte) error { 434 var s string 435 if err := json.Unmarshal(data, &s); err != nil { 436 return err 437 } 438 if s == "" { 439 return nil 440 } 441 t, err := time.Parse(RFC3339ZNoTNoZ, s) 442 if err != nil { 443 return err 444 } 445 *jt = JSONRFC3339ZNoTNoZ(t) 446 return nil 447 } 448 449 /* 450 Link is an internal type to be used in packages of collection resources that are 451 paginated in a certain way. 452 453 It's a response substructure common to many paginated collection results that is 454 used to point to related pages. Usually, the one we care about is the one with 455 Rel field set to "next". 456 */ 457 type Link struct { 458 Href string `json:"href"` 459 Rel string `json:"rel"` 460 } 461 462 /* 463 ExtractNextURL is an internal function useful for packages of collection 464 resources that are paginated in a certain way. 465 466 It attempts to extract the "next" URL from slice of Link structs, or 467 "" if no such URL is present. 468 */ 469 func ExtractNextURL(links []Link) (string, error) { 470 var url string 471 472 for _, l := range links { 473 if l.Rel == "next" { 474 url = l.Href 475 } 476 } 477 478 if url == "" { 479 return "", nil 480 } 481 482 return url, nil 483 }