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 }