Skip to content

Commit

Permalink
[Refactor] Introduce extract functions (#391)
Browse files Browse the repository at this point in the history
[Refactor] Introduce extract functions

What this PR does / why we need it
Using extract functions allows to avoid using Result
structure and its derivatives
Which issue this PR fixes

Part of #389
Special notes for your reviewer
This is what I was talking about in comments of #389 and in #381 and #384 discussions
Also, if unit tests are required, I will provide them later today

Reviewed-by: Artem Lifshits <None>
Reviewed-by: Aloento <None>
Reviewed-by: Anton Sidelnikov <None>
  • Loading branch information
outcatcher committed Aug 10, 2022
1 parent 5617dc1 commit e92c6cf
Show file tree
Hide file tree
Showing 6 changed files with 464 additions and 145 deletions.
13 changes: 7 additions & 6 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
issues:
exclude-rules:
# Exclude some staticcheck messages
- linters:
- staticcheck
text: "SA1008:"
linters-settings:
staticcheck:
checks:
- all
# Exclude some staticcheck messages
- '-SA1008' # "content-length" is not canonical, avoid OBS headers warnings
- '-SA1019' # deprecations, used to avoid Extract... deprecation warnings
3 changes: 3 additions & 0 deletions internal/extract/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package extract contains functions for extracting JSON results into given structure or slice pointers.
// Those are wrappers over `json.Marshall` and `json.Unmarshall` functions with additional validation built it
package extract
153 changes: 153 additions & 0 deletions internal/extract/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package extract

import (
"bytes"
"encoding/json"
"fmt"
"io"
"reflect"
)

func intoPtr(body, to interface{}, label string) error {
if label == "" {
return Into(body, &to)
}

var m map[string]interface{}
err := Into(body, &m)
if err != nil {
return err
}

b, err := JsonMarshal(m[label])
if err != nil {
return err
}

toValue := reflect.ValueOf(to)
if toValue.Kind() == reflect.Ptr {
toValue = toValue.Elem()
}

switch toValue.Kind() {
case reflect.Slice:
typeOfV := toValue.Type().Elem()
if typeOfV.Kind() == reflect.Struct {
if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous {
newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0)

for _, v := range m[label].([]interface{}) {
// For each iteration of the slice, we create a new struct.
// This is to work around a bug where elements of a slice
// are reused and not overwritten when the same copy of the
// struct is used:
//
// https://github.com/golang/go/issues/21092
// https://github.com/golang/go/issues/24155
// https://play.golang.org/p/NHo3ywlPZli
newType := reflect.New(typeOfV).Elem()

b, err := JsonMarshal(v)
if err != nil {
return err
}

// This is needed for structs with an UnmarshalJSON method.
// Technically this is just unmarshalling the response into
// a struct that is never used, but it's good enough to
// trigger the UnmarshalJSON method.
for i := 0; i < newType.NumField(); i++ {
s := newType.Field(i).Addr().Interface()

// Unmarshal is used rather than NewDecoder to also work
// around the above-mentioned bug.
err = json.Unmarshal(b, s)
if err != nil {
continue
}
}

newSlice = reflect.Append(newSlice, newType)
}

// "to" should now be properly modeled to receive the
// JSON response body and unmarshal into all the correct
// fields of the struct or composed extension struct
// at the end of this method.
toValue.Set(newSlice)
}
}
case reflect.Struct:
typeOfV := toValue.Type()
if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous {
for i := 0; i < toValue.NumField(); i++ {
toField := toValue.Field(i)
if toField.Kind() == reflect.Struct {
s := toField.Addr().Interface()
err = json.NewDecoder(bytes.NewReader(b)).Decode(s)
if err != nil {
return err
}
}
}
}
}

err = json.Unmarshal(b, &to)
return err
}

func JsonMarshal(t interface{}) ([]byte, error) {
buffer := &bytes.Buffer{}
enc := json.NewEncoder(buffer)
enc.SetEscapeHTML(false)
err := enc.Encode(t)
return buffer.Bytes(), err
}

func Into(body interface{}, to interface{}) error {
if reader, ok := body.(io.Reader); ok {
if readCloser, ok := reader.(io.Closer); ok {
defer readCloser.Close()
}
return json.NewDecoder(reader).Decode(to)
}

b, err := JsonMarshal(body)
if err != nil {
return err
}
err = json.Unmarshal(b, to)

return err
}

// IntoStructPtr will unmarshal the given body into the provided
// interface{} (to).
func IntoStructPtr(body, to interface{}, label string) error {
t := reflect.TypeOf(to)
if k := t.Kind(); k != reflect.Ptr {
return fmt.Errorf("expected pointer, got %v", k)
}
switch t.Elem().Kind() {
case reflect.Struct:
return intoPtr(body, to, label)
default:
return fmt.Errorf("expected pointer to struct, got: %v", t)
}
}

// IntoSlicePtr will unmarshal the provided body into the provided
// interface{} (to).
func IntoSlicePtr(body, to interface{}, label string) error {
t := reflect.TypeOf(to)
if k := t.Kind(); k != reflect.Ptr {
return fmt.Errorf("expected pointer, got %v", k)
}
switch t.Elem().Kind() {
case reflect.Slice:
return intoPtr(body, to, label)
default:
return fmt.Errorf("expected pointer to slice, got: %v", t)
}
}
Loading

0 comments on commit e92c6cf

Please sign in to comment.