github.com/opentelekomcloud/gophertelekomcloud@v0.9.3/internal/extract/json.go (about)

     1  package extract
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"reflect"
    10  )
    11  
    12  func intoPtr(body io.Reader, to interface{}, label string) error {
    13  	if label == "" {
    14  		return Into(body, &to)
    15  	}
    16  
    17  	var m map[string]interface{}
    18  	err := Into(body, &m)
    19  	if err != nil {
    20  		return err
    21  	}
    22  
    23  	b, err := JsonMarshal(m[label])
    24  	if err != nil {
    25  		return err
    26  	}
    27  
    28  	toValue := reflect.ValueOf(to)
    29  	if toValue.Kind() == reflect.Ptr {
    30  		toValue = toValue.Elem()
    31  	}
    32  
    33  	switch toValue.Kind() {
    34  	case reflect.Slice:
    35  		typeOfV := toValue.Type().Elem()
    36  		if typeOfV.Kind() == reflect.Struct {
    37  			if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous {
    38  				newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0)
    39  
    40  				for _, v := range m[label].([]interface{}) {
    41  					// For each iteration of the slice, we create a new struct.
    42  					// This is to work around a bug where elements of a slice
    43  					// are reused and not overwritten when the same copy of the
    44  					// struct is used:
    45  					//
    46  					// https://github.com/golang/go/issues/21092
    47  					// https://github.com/golang/go/issues/24155
    48  					// https://play.golang.org/p/NHo3ywlPZli
    49  					newType := reflect.New(typeOfV).Elem()
    50  
    51  					b, err := JsonMarshal(v)
    52  					if err != nil {
    53  						return err
    54  					}
    55  
    56  					// This is needed for structs with an UnmarshalJSON method.
    57  					// Technically this is just unmarshalling the response into
    58  					// a struct that is never used, but it's good enough to
    59  					// trigger the UnmarshalJSON method.
    60  					for i := 0; i < newType.NumField(); i++ {
    61  						s := newType.Field(i).Addr().Interface()
    62  
    63  						// Unmarshal is used rather than NewDecoder to also work
    64  						// around the above-mentioned bug.
    65  						err = json.Unmarshal(b, s)
    66  						if err != nil {
    67  							continue
    68  						}
    69  					}
    70  
    71  					newSlice = reflect.Append(newSlice, newType)
    72  				}
    73  
    74  				// "to" should now be properly modeled to receive the
    75  				// JSON response body and unmarshal into all the correct
    76  				// fields of the struct or composed extension struct
    77  				// at the end of this method.
    78  				toValue.Set(newSlice)
    79  			}
    80  		}
    81  	case reflect.Struct:
    82  		typeOfV := toValue.Type()
    83  		if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous {
    84  			for i := 0; i < toValue.NumField(); i++ {
    85  				toField := toValue.Field(i)
    86  				if toField.Kind() == reflect.Struct {
    87  					s := toField.Addr().Interface()
    88  					err = json.NewDecoder(bytes.NewReader(b)).Decode(s)
    89  					if err != nil {
    90  						return err
    91  					}
    92  				}
    93  			}
    94  		}
    95  	}
    96  
    97  	err = json.Unmarshal(b, &to)
    98  	return err
    99  }
   100  
   101  // JsonMarshal marshals input to bytes via buffer with disabled HTML escaping.
   102  func JsonMarshal(t interface{}) ([]byte, error) {
   103  	buffer := &bytes.Buffer{}
   104  	enc := json.NewEncoder(buffer)
   105  	enc.SetEscapeHTML(false)
   106  	err := enc.Encode(t)
   107  	return buffer.Bytes(), err
   108  }
   109  
   110  // Into parses input as JSON and convert to a structure.
   111  func Into(body io.Reader, to interface{}) error {
   112  	if closer, ok := body.(io.ReadCloser); ok {
   113  		defer closer.Close()
   114  	}
   115  
   116  	byteBody, err := io.ReadAll(body)
   117  	if err != nil {
   118  		return fmt.Errorf("error reading from stream: %w", err)
   119  	}
   120  
   121  	if len(byteBody) == 0 {
   122  		return nil // empty body - nothing to extract
   123  	}
   124  
   125  	err = json.Unmarshal(byteBody, to)
   126  	if err != nil && !errors.Is(err, io.EOF) {
   127  		return fmt.Errorf("error extracting %s into %T: %w", byteBody, to, err)
   128  	}
   129  
   130  	return nil
   131  }
   132  
   133  func typeCheck(to interface{}, kind reflect.Kind) error {
   134  	t := reflect.TypeOf(to)
   135  	if k := t.Kind(); k != reflect.Ptr {
   136  		return fmt.Errorf("expected pointer, got %v", k)
   137  	}
   138  
   139  	if kind != t.Elem().Kind() {
   140  		return fmt.Errorf("expected pointer to %v, got: %v", kind.String(), t)
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  // IntoStructPtr will unmarshal the given body into the provided Struct.
   147  func IntoStructPtr(body io.Reader, to interface{}, label string) error {
   148  	err := typeCheck(to, reflect.Struct)
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	return intoPtr(body, to, label)
   154  }
   155  
   156  // IntoSlicePtr will unmarshal the provided body into the provided Slice.
   157  func IntoSlicePtr(body io.Reader, to interface{}, label string) error {
   158  	err := typeCheck(to, reflect.Slice)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	return intoPtr(body, to, label)
   164  }