github.com/micro/go-micro/v2@v2.9.1/util/qson/qson.go (about) 1 // Package qson implmenets decoding of URL query params 2 // into JSON and Go values (using JSON struct tags). 3 // 4 // See https://golang.org/pkg/encoding/json/ for more 5 // details on JSON struct tags. 6 package qson 7 8 import ( 9 "encoding/json" 10 "errors" 11 "net/url" 12 "regexp" 13 "strings" 14 ) 15 16 var ( 17 // ErrInvalidParam is returned when invalid data is provided to the ToJSON or Unmarshal function. 18 // Specifically, this will be returned when there is no equals sign present in the URL query parameter. 19 ErrInvalidParam error = errors.New("qson: invalid url query param provided") 20 21 bracketSplitter *regexp.Regexp 22 ) 23 24 func init() { 25 bracketSplitter = regexp.MustCompile("\\[|\\]") 26 } 27 28 // Unmarshal will take a dest along with URL 29 // query params and attempt to first turn the query params 30 // into JSON and then unmarshal those into the dest variable 31 // 32 // BUG(joncalhoun): If a URL query param value is something 33 // like 123 but is expected to be parsed into a string this 34 // will currently result in an error because the JSON 35 // transformation will assume this is intended to be an int. 36 // This should only affect the Unmarshal function and 37 // could likely be fixed, but someone will need to submit a 38 // PR if they want that fixed. 39 func Unmarshal(dst interface{}, query string) error { 40 b, err := ToJSON(query) 41 if err != nil { 42 return err 43 } 44 return json.Unmarshal(b, dst) 45 } 46 47 // ToJSON will turn a query string like: 48 // cat=1&bar%5Bone%5D%5Btwo%5D=2&bar[one][red]=112 49 // Into a JSON object with all the data merged as nicely as 50 // possible. Eg the example above would output: 51 // {"bar":{"one":{"two":2,"red":112}}} 52 func ToJSON(query string) ([]byte, error) { 53 var ( 54 builder interface{} = make(map[string]interface{}) 55 ) 56 params := strings.Split(query, "&") 57 for _, part := range params { 58 tempMap, err := queryToMap(part) 59 if err != nil { 60 return nil, err 61 } 62 builder = merge(builder, tempMap) 63 } 64 return json.Marshal(builder) 65 } 66 67 // queryToMap turns something like a[b][c]=4 into 68 // map[string]interface{}{ 69 // "a": map[string]interface{}{ 70 // "b": map[string]interface{}{ 71 // "c": 4, 72 // }, 73 // }, 74 // } 75 func queryToMap(param string) (map[string]interface{}, error) { 76 rawKey, rawValue, err := splitKeyAndValue(param) 77 if err != nil { 78 return nil, err 79 } 80 rawValue, err = url.QueryUnescape(rawValue) 81 if err != nil { 82 return nil, err 83 } 84 rawKey, err = url.QueryUnescape(rawKey) 85 if err != nil { 86 return nil, err 87 } 88 89 pieces := bracketSplitter.Split(rawKey, -1) 90 key := pieces[0] 91 92 // If len==1 then rawKey has no [] chars and we can just 93 // decode this as key=value into {key: value} 94 if len(pieces) == 1 { 95 var value interface{} 96 // First we try parsing it as an int, bool, null, etc 97 err = json.Unmarshal([]byte(rawValue), &value) 98 if err != nil { 99 // If we got an error we try wrapping the value in 100 // quotes and processing it as a string 101 err = json.Unmarshal([]byte("\""+rawValue+"\""), &value) 102 if err != nil { 103 // If we can't decode as a string we return the err 104 return nil, err 105 } 106 } 107 return map[string]interface{}{ 108 key: value, 109 }, nil 110 } 111 112 // If len > 1 then we have something like a[b][c]=2 113 // so we need to turn this into {"a": {"b": {"c": 2}}} 114 // To do this we break our key into two pieces: 115 // a and b[c] 116 // and then we set {"a": queryToMap("b[c]", value)} 117 ret := make(map[string]interface{}, 0) 118 ret[key], err = queryToMap(buildNewKey(rawKey) + "=" + rawValue) 119 if err != nil { 120 return nil, err 121 } 122 123 // When URL params have a set of empty brackets (eg a[]=1) 124 // it is assumed to be an array. This will get us the 125 // correct value for the array item and return it as an 126 // []interface{} so that it can be merged properly. 127 if pieces[1] == "" { 128 temp := ret[key].(map[string]interface{}) 129 ret[key] = []interface{}{temp[""]} 130 } 131 return ret, nil 132 } 133 134 // buildNewKey will take something like: 135 // origKey = "bar[one][two]" 136 // pieces = [bar one two ] 137 // and return "one[two]" 138 func buildNewKey(origKey string) string { 139 pieces := bracketSplitter.Split(origKey, -1) 140 ret := origKey[len(pieces[0])+1:] 141 ret = ret[:len(pieces[1])] + ret[len(pieces[1])+1:] 142 return ret 143 } 144 145 // splitKeyAndValue splits a URL param at the last equal 146 // sign and returns the two strings. If no equal sign is 147 // found, the ErrInvalidParam error is returned. 148 func splitKeyAndValue(param string) (string, string, error) { 149 li := strings.LastIndex(param, "=") 150 if li == -1 { 151 return "", "", ErrInvalidParam 152 } 153 return param[:li], param[li+1:], nil 154 }