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  }