api/common/request.go (115 lines of code) (raw):
// Copyright (c) 2017-2018 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"encoding/json"
"io/ioutil"
"net/http"
"reflect"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/uber/aresdb/utils"
)
// WithRequest defines function callback with parsed request
type WithRequest func(request interface{})
// ReadRequest reads request.
// obj passed into this method has to be a pointer to a struct of request object
// Each request object will have path params tagged as `path:""` if needed
// and post body tagged as `body:""` if needed
// path tag must have parameter name, which will be used to read path param
// body tag field has to be a struct.
// eg.
// type AddEnumCaseRequest struct {
// TableName string `path:"table"`
// ColumnName string `path:"column"`
// Body struct {
// EnumCase string `json:"enumCase"`
// } `body:""`
// }
func ReadRequest(r *http.Request, obj interface{}, withRequests ...WithRequest) error {
vValue := reflect.ValueOf(obj)
vType := reflect.TypeOf(obj)
if vType.Kind() != reflect.Ptr || vType.Elem().Kind() != reflect.Struct {
return utils.APIError{
Code: http.StatusInternalServerError,
Message: "Expecting request object to be a pointer to struct",
}
}
var formParsed bool
for i := 0; i < vType.Elem().NumField(); i++ {
var isPathParam, isQueryParam, isHeaderParam, optional bool
var paramName, paramValue string
field := vType.Elem().Field(i)
valueField := vValue.Elem().Field(i)
// If it's anonymous field, we apply ReadRequest to this struct directly.
if field.Type.Kind() == reflect.Struct && field.Anonymous {
if err := ReadRequest(r, valueField.Addr().Interface(), withRequests...); err != nil {
return err
}
}
if paramName, isHeaderParam = field.Tag.Lookup("header"); isHeaderParam {
tagValues := strings.Split(paramName, ",")
paramName = tagValues[0]
if len(tagValues) == 2 && tagValues[1] == "optional" {
optional = true
}
paramValue = r.Header.Get(paramName)
} else if paramName, isPathParam = field.Tag.Lookup("path"); isPathParam {
vars := mux.Vars(r)
if vars == nil {
return ErrMissingParameter
}
paramValue = vars[paramName]
} else if paramName, isQueryParam = field.Tag.Lookup("query"); isQueryParam {
tagValues := strings.Split(paramName, ",")
paramName = tagValues[0]
if len(tagValues) == 2 && tagValues[1] == "optional" {
optional = true
}
if !formParsed {
if err := r.ParseForm(); err != nil && !optional {
return ErrMissingParameter
}
formParsed = true
}
paramValue = r.Form.Get(paramName)
}
if isPathParam || isQueryParam || isHeaderParam {
if paramValue == "" {
if optional {
continue
}
return ErrMissingParameter
}
// Only string and int is supported in request path fields.
switch field.Type.Kind() {
case reflect.String:
valueField.SetString(paramValue)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
intVal, err := strconv.ParseInt(paramValue, 10, 64)
if err != nil {
return ErrMissingParameter
}
valueField.SetInt(intVal)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
uintVal, err := strconv.ParseUint(paramValue, 10, 64)
if err != nil {
return ErrMissingParameter
}
valueField.SetUint(uintVal)
default:
return ErrMissingParameter
}
} else if _, isPostBody := field.Tag.Lookup("body"); isPostBody {
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
return utils.APIError{
Code: http.StatusBadRequest,
Message: ErrMsgFailedToReadRequestBody,
Cause: err,
}
}
switch valueField.Addr().Interface().(type) {
case *[]byte:
valueField.SetBytes(requestBody)
case *json.RawMessage:
valueField.SetBytes(requestBody)
default:
err = json.Unmarshal(requestBody, valueField.Addr().Interface())
if err != nil {
return utils.APIError{
Code: http.StatusBadRequest,
Message: ErrMsgFailedToUnmarshalRequest,
Cause: err,
}
}
}
}
}
for _, withRequest := range withRequests {
withRequest(vValue.Elem().Interface())
}
return nil
}