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  }