// Package qson implmenets decoding of URL query params // into JSON and Go values (using JSON struct tags). // // See https://golang.org/pkg/encoding/json/ for more // details on JSON struct tags. package qson import ( "encoding/json" "errors" "net/url" "regexp" "strings" ) var ( // ErrInvalidParam is returned when invalid data is provided to the ToJSON or Unmarshal function. // Specifically, this will be returned when there is no equals sign present in the URL query parameter. ErrInvalidParam error = errors.New("qson: invalid url query param provided") bracketSplitter *regexp.Regexp ) func init() { bracketSplitter = regexp.MustCompile("\\[|\\]") } // Unmarshal will take a dest along with URL // query params and attempt to first turn the query params // into JSON and then unmarshal those into the dest variable // // BUG(joncalhoun): If a URL query param value is something // like 123 but is expected to be parsed into a string this // will currently result in an error because the JSON // transformation will assume this is intended to be an int. // This should only affect the Unmarshal function and // could likely be fixed, but someone will need to submit a // PR if they want that fixed. func Unmarshal(dst interface{}, query string) error { b, err := ToJSON(query) if err != nil { return err } return json.Unmarshal(b, dst) } // ToJSON will turn a query string like: // // cat=1&bar%5Bone%5D%5Btwo%5D=2&bar[one][red]=112 // // Into a JSON object with all the data merged as nicely as // possible. Eg the example above would output: // // {"bar":{"one":{"two":2,"red":112}}} func ToJSON(query string) ([]byte, error) { var ( builder interface{} = make(map[string]interface{}) ) params := strings.Split(query, "&") for _, part := range params { tempMap, err := queryToMap(part) if err != nil { return nil, err } builder = merge(builder, tempMap) } return json.Marshal(builder) } // queryToMap turns something like a[b][c]=4 into // // map[string]interface{}{ // "a": map[string]interface{}{ // "b": map[string]interface{}{ // "c": 4, // }, // }, // } func queryToMap(param string) (map[string]interface{}, error) { rawKey, rawValue, err := splitKeyAndValue(param) if err != nil { return nil, err } rawValue, err = url.QueryUnescape(rawValue) if err != nil { return nil, err } rawKey, err = url.QueryUnescape(rawKey) if err != nil { return nil, err } pieces := bracketSplitter.Split(rawKey, -1) key := pieces[0] // If len==1 then rawKey has no [] chars and we can just // decode this as key=value into {key: value} if len(pieces) == 1 { var value interface{} // First we try parsing it as an int, bool, null, etc err = json.Unmarshal([]byte(rawValue), &value) if err != nil { // If we got an error we try wrapping the value in // quotes and processing it as a string err = json.Unmarshal([]byte("\""+rawValue+"\""), &value) if err != nil { // If we can't decode as a string we return the err return nil, err } } return map[string]interface{}{ key: value, }, nil } // If len > 1 then we have something like a[b][c]=2 // so we need to turn this into {"a": {"b": {"c": 2}}} // To do this we break our key into two pieces: // a and b[c] // and then we set {"a": queryToMap("b[c]", value)} ret := make(map[string]interface{}, 0) ret[key], err = queryToMap(buildNewKey(rawKey) + "=" + rawValue) if err != nil { return nil, err } // When URL params have a set of empty brackets (eg a[]=1) // it is assumed to be an array. This will get us the // correct value for the array item and return it as an // []interface{} so that it can be merged properly. if pieces[1] == "" { temp := ret[key].(map[string]interface{}) ret[key] = []interface{}{temp[""]} } return ret, nil } // buildNewKey will take something like: // origKey = "bar[one][two]" // pieces = [bar one two ] // and return "one[two]" func buildNewKey(origKey string) string { pieces := bracketSplitter.Split(origKey, -1) ret := origKey[len(pieces[0])+1:] ret = ret[:len(pieces[1])] + ret[len(pieces[1])+1:] return ret } // splitKeyAndValue splits a URL param at the last equal // sign and returns the two strings. If no equal sign is // found, the ErrInvalidParam error is returned. func splitKeyAndValue(param string) (string, string, error) { li := strings.LastIndex(param, "=") if li == -1 { return "", "", ErrInvalidParam } return param[:li], param[li+1:], nil }