14287: Propagate "distinct" param.
[arvados.git] / lib / controller / router / request.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package router
6
7 import (
8         "encoding/json"
9         "fmt"
10         "io"
11         "mime"
12         "net/http"
13         "strconv"
14         "strings"
15
16         "github.com/julienschmidt/httprouter"
17 )
18
19 // Parse req as an Arvados V1 API request and return the request
20 // parameters.
21 //
22 // If the request has a parameter whose name is attrsKey (e.g.,
23 // "collection"), it is renamed to "attrs".
24 func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[string]interface{}, error) {
25         err := req.ParseForm()
26         if err != nil {
27                 return nil, httpError(http.StatusBadRequest, err)
28         }
29         params := map[string]interface{}{}
30         for k, values := range req.Form {
31                 for _, v := range values {
32                         switch {
33                         case boolParams[k]:
34                                 params[k] = stringToBool(v)
35                         case v == "null" || v == "":
36                                 params[k] = nil
37                         case strings.HasPrefix(v, "["):
38                                 var j []interface{}
39                                 err := json.Unmarshal([]byte(v), &j)
40                                 if err != nil {
41                                         return nil, err
42                                 }
43                                 params[k] = j
44                         case strings.HasPrefix(v, "{"):
45                                 var j map[string]interface{}
46                                 err := json.Unmarshal([]byte(v), &j)
47                                 if err != nil {
48                                         return nil, err
49                                 }
50                                 params[k] = j
51                         case strings.HasPrefix(v, "\""):
52                                 var j string
53                                 err := json.Unmarshal([]byte(v), &j)
54                                 if err != nil {
55                                         return nil, err
56                                 }
57                                 params[k] = j
58                         case k == "limit" || k == "offset":
59                                 params[k], err = strconv.ParseInt(v, 10, 64)
60                                 if err != nil {
61                                         return nil, err
62                                 }
63                         default:
64                                 params[k] = v
65                         }
66                         // TODO: Need to accept "?foo[]=bar&foo[]=baz"
67                         // as foo=["bar","baz"]?
68                 }
69         }
70
71         // Decode body as JSON if Content-Type request header is
72         // missing or application/json.
73         mt := req.Header.Get("Content-Type")
74         if ct, _, err := mime.ParseMediaType(mt); err != nil && mt != "" {
75                 return nil, fmt.Errorf("error parsing media type %q: %s", mt, err)
76         } else if (ct == "application/json" || mt == "") && req.ContentLength != 0 {
77                 jsonParams := map[string]interface{}{}
78                 err := json.NewDecoder(req.Body).Decode(&jsonParams)
79                 if err != nil {
80                         return nil, httpError(http.StatusBadRequest, err)
81                 }
82                 for k, v := range jsonParams {
83                         params[k] = v
84                 }
85                 if attrsKey != "" && params[attrsKey] == nil {
86                         // Copy top-level parameters from JSON request
87                         // body into params[attrsKey]. Some SDKs rely
88                         // on this Rails API feature; see
89                         // https://api.rubyonrails.org/v5.2.1/classes/ActionController/ParamsWrapper.html
90                         params[attrsKey] = jsonParams
91                 }
92         }
93
94         routeParams, _ := req.Context().Value(httprouter.ParamsKey).(httprouter.Params)
95         for _, p := range routeParams {
96                 params[p.Key] = p.Value
97         }
98
99         if v, ok := params[attrsKey]; ok && attrsKey != "" {
100                 params["attrs"] = v
101                 delete(params, attrsKey)
102         }
103
104         if order, ok := params["order"].(string); ok {
105                 // We must accept strings ("foo, bar desc") and arrays
106                 // (["foo", "bar desc"]) because RailsAPI does.
107                 // Convert to an array here before trying to unmarshal
108                 // into options structs.
109                 if order == "" {
110                         delete(params, "order")
111                 } else {
112                         params["order"] = strings.Split(order, ",")
113                 }
114         }
115
116         return params, nil
117 }
118
119 // Copy src to dst, using json as an intermediate format in order to
120 // invoke src's json-marshaling and dst's json-unmarshaling behaviors.
121 func (rtr *router) transcode(src interface{}, dst interface{}) error {
122         var errw error
123         pr, pw := io.Pipe()
124         go func() {
125                 defer pw.Close()
126                 errw = json.NewEncoder(pw).Encode(src)
127         }()
128         defer pr.Close()
129         err := json.NewDecoder(pr).Decode(dst)
130         if errw != nil {
131                 return errw
132         }
133         return err
134 }
135
136 var boolParams = map[string]bool{
137         "distinct":             true,
138         "ensure_unique_name":   true,
139         "include_trash":        true,
140         "include_old_versions": true,
141 }
142
143 func stringToBool(s string) bool {
144         switch s {
145         case "", "false", "0":
146                 return false
147         default:
148                 return true
149         }
150 }