Write an openapi spec for the worker API and use `deepmap/oapi-codegen`
to generate scaffolding for the server-side using the `labstack/echo`
server.
Incidentally, echo by default returns the errors in the same format that
worker API always has:
{ "message": "..." }
The API itself is unchanged to make this change easier to understand. It
will be changed to better suit our needs in future commits.
337 lines
8.2 KiB
Go
337 lines
8.2 KiB
Go
// Copyright 2019 DeepMap, 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 runtime
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Given an input value, such as a primitive type, array or object, turn it
|
|
// into a parameter based on style/explode definition.
|
|
func StyleParam(style string, explode bool, paramName string, value interface{}) (string, error) {
|
|
t := reflect.TypeOf(value)
|
|
v := reflect.ValueOf(value)
|
|
|
|
// Things may be passed in by pointer, we need to dereference, so return
|
|
// error on nil.
|
|
if t.Kind() == reflect.Ptr {
|
|
if v.IsNil() {
|
|
return "", fmt.Errorf("value is a nil pointer")
|
|
}
|
|
v = reflect.Indirect(v)
|
|
t = v.Type()
|
|
}
|
|
|
|
switch t.Kind() {
|
|
case reflect.Slice:
|
|
n := v.Len()
|
|
sliceVal := make([]interface{}, n)
|
|
for i := 0; i < n; i++ {
|
|
sliceVal[i] = v.Index(i).Interface()
|
|
}
|
|
return styleSlice(style, explode, paramName, sliceVal)
|
|
case reflect.Struct:
|
|
return styleStruct(style, explode, paramName, value)
|
|
case reflect.Map:
|
|
return styleMap(style, explode, paramName, value)
|
|
default:
|
|
return stylePrimitive(style, explode, paramName, value)
|
|
}
|
|
}
|
|
|
|
func styleSlice(style string, explode bool, paramName string, values []interface{}) (string, error) {
|
|
if style == "deepObject" {
|
|
if !explode {
|
|
return "", errors.New("deepObjects must be exploded")
|
|
}
|
|
return MarshalDeepObject(values, paramName)
|
|
}
|
|
|
|
var prefix string
|
|
var separator string
|
|
|
|
switch style {
|
|
case "simple":
|
|
separator = ","
|
|
case "label":
|
|
prefix = "."
|
|
if explode {
|
|
separator = "."
|
|
} else {
|
|
separator = ","
|
|
}
|
|
case "matrix":
|
|
prefix = fmt.Sprintf(";%s=", paramName)
|
|
if explode {
|
|
separator = prefix
|
|
} else {
|
|
separator = ","
|
|
}
|
|
case "form":
|
|
prefix = fmt.Sprintf("%s=", paramName)
|
|
if explode {
|
|
separator = "&" + prefix
|
|
} else {
|
|
separator = ","
|
|
}
|
|
case "spaceDelimited":
|
|
prefix = fmt.Sprintf("%s=", paramName)
|
|
if explode {
|
|
separator = "&" + prefix
|
|
} else {
|
|
separator = " "
|
|
}
|
|
case "pipeDelimited":
|
|
prefix = fmt.Sprintf("%s=", paramName)
|
|
if explode {
|
|
separator = "&" + prefix
|
|
} else {
|
|
separator = "|"
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("unsupported style '%s'", style)
|
|
}
|
|
|
|
// We're going to assume here that the array is one of simple types.
|
|
var err error
|
|
parts := make([]string, len(values))
|
|
for i, v := range values {
|
|
parts[i], err = primitiveToString(v)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error formatting '%s': %s", paramName, err)
|
|
}
|
|
}
|
|
return prefix + strings.Join(parts, separator), nil
|
|
}
|
|
|
|
func sortedKeys(strMap map[string]string) []string {
|
|
keys := make([]string, len(strMap))
|
|
i := 0
|
|
for k := range strMap {
|
|
keys[i] = k
|
|
i++
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
|
|
// This is a special case. The struct may be a time, in which case, marshal
|
|
// it in RFC3339 format.
|
|
func marshalTimeValue(value interface{}) (string, bool) {
|
|
if timeVal, ok := value.(*time.Time); ok {
|
|
return timeVal.Format(time.RFC3339Nano), true
|
|
}
|
|
|
|
if timeVal, ok := value.(time.Time); ok {
|
|
return timeVal.Format(time.RFC3339Nano), true
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func styleStruct(style string, explode bool, paramName string, value interface{}) (string, error) {
|
|
if timeVal, ok := marshalTimeValue(value); ok {
|
|
return stylePrimitive(style, explode, paramName, timeVal)
|
|
}
|
|
|
|
if style == "deepObject" {
|
|
if !explode {
|
|
return "", errors.New("deepObjects must be exploded")
|
|
}
|
|
return MarshalDeepObject(value, paramName)
|
|
}
|
|
|
|
// Otherwise, we need to build a dictionary of the struct's fields. Each
|
|
// field may only be a primitive value.
|
|
v := reflect.ValueOf(value)
|
|
t := reflect.TypeOf(value)
|
|
fieldDict := make(map[string]string)
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
fieldT := t.Field(i)
|
|
// Find the json annotation on the field, and use the json specified
|
|
// name if available, otherwise, just the field name.
|
|
tag := fieldT.Tag.Get("json")
|
|
fieldName := fieldT.Name
|
|
if tag != "" {
|
|
tagParts := strings.Split(tag, ",")
|
|
name := tagParts[0]
|
|
if name != "" {
|
|
fieldName = name
|
|
}
|
|
}
|
|
f := v.Field(i)
|
|
|
|
// Unset optional fields will be nil pointers, skip over those.
|
|
if f.Type().Kind() == reflect.Ptr && f.IsNil() {
|
|
continue
|
|
}
|
|
str, err := primitiveToString(f.Interface())
|
|
if err != nil {
|
|
return "", fmt.Errorf("error formatting '%s': %s", paramName, err)
|
|
}
|
|
fieldDict[fieldName] = str
|
|
}
|
|
|
|
return processFieldDict(style, explode, paramName, fieldDict)
|
|
}
|
|
|
|
func styleMap(style string, explode bool, paramName string, value interface{}) (string, error) {
|
|
if style == "deepObject" {
|
|
if !explode {
|
|
return "", errors.New("deepObjects must be exploded")
|
|
}
|
|
return MarshalDeepObject(value, paramName)
|
|
}
|
|
|
|
dict, ok := value.(map[string]interface{})
|
|
if !ok {
|
|
return "", errors.New("map not of type map[string]interface{}")
|
|
}
|
|
|
|
fieldDict := make(map[string]string)
|
|
for fieldName, value := range dict {
|
|
str, err := primitiveToString(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error formatting '%s': %s", paramName, err)
|
|
}
|
|
fieldDict[fieldName] = str
|
|
}
|
|
|
|
return processFieldDict(style, explode, paramName, fieldDict)
|
|
}
|
|
|
|
func processFieldDict(style string, explode bool, paramName string, fieldDict map[string]string) (string, error) {
|
|
var parts []string
|
|
|
|
// This works for everything except deepObject. We'll handle that one
|
|
// separately.
|
|
if style != "deepObject" {
|
|
if explode {
|
|
for _, k := range sortedKeys(fieldDict) {
|
|
v := fieldDict[k]
|
|
parts = append(parts, k+"="+v)
|
|
}
|
|
} else {
|
|
for _, k := range sortedKeys(fieldDict) {
|
|
v := fieldDict[k]
|
|
parts = append(parts, k)
|
|
parts = append(parts, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
var prefix string
|
|
var separator string
|
|
|
|
switch style {
|
|
case "simple":
|
|
separator = ","
|
|
case "label":
|
|
prefix = "."
|
|
if explode {
|
|
separator = prefix
|
|
} else {
|
|
separator = ","
|
|
}
|
|
case "matrix":
|
|
if explode {
|
|
separator = ";"
|
|
prefix = ";"
|
|
} else {
|
|
separator = ","
|
|
prefix = fmt.Sprintf(";%s=", paramName)
|
|
}
|
|
case "form":
|
|
if explode {
|
|
separator = "&"
|
|
} else {
|
|
prefix = fmt.Sprintf("%s=", paramName)
|
|
separator = ","
|
|
}
|
|
case "deepObject":
|
|
{
|
|
if !explode {
|
|
return "", fmt.Errorf("deepObject parameters must be exploded")
|
|
}
|
|
for _, k := range sortedKeys(fieldDict) {
|
|
v := fieldDict[k]
|
|
part := fmt.Sprintf("%s[%s]=%s", paramName, k, v)
|
|
parts = append(parts, part)
|
|
}
|
|
separator = "&"
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("unsupported style '%s'", style)
|
|
}
|
|
|
|
return prefix + strings.Join(parts, separator), nil
|
|
}
|
|
|
|
func stylePrimitive(style string, explode bool, paramName string, value interface{}) (string, error) {
|
|
strVal, err := primitiveToString(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var prefix string
|
|
switch style {
|
|
case "simple":
|
|
case "label":
|
|
prefix = "."
|
|
case "matrix":
|
|
prefix = fmt.Sprintf(";%s=", paramName)
|
|
case "form":
|
|
prefix = fmt.Sprintf("%s=", paramName)
|
|
default:
|
|
return "", fmt.Errorf("unsupported style '%s'", style)
|
|
}
|
|
return prefix + strVal, nil
|
|
}
|
|
|
|
// Converts a primitive value to a string. We need to do this based on the
|
|
// Kind of an interface, not the Type to work with aliased types.
|
|
func primitiveToString(value interface{}) (string, error) {
|
|
var output string
|
|
|
|
// Values may come in by pointer for optionals, so make sure to dereferene.
|
|
v := reflect.Indirect(reflect.ValueOf(value))
|
|
t := v.Type()
|
|
kind := t.Kind()
|
|
|
|
switch kind {
|
|
case reflect.Int8, reflect.Int32, reflect.Int64, reflect.Int:
|
|
output = strconv.FormatInt(v.Int(), 10)
|
|
case reflect.Float32, reflect.Float64:
|
|
output = strconv.FormatFloat(v.Float(), 'f', -1, 64)
|
|
case reflect.Bool:
|
|
if v.Bool() {
|
|
output = "true"
|
|
} else {
|
|
output = "false"
|
|
}
|
|
case reflect.String:
|
|
output = v.String()
|
|
default:
|
|
return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String())
|
|
}
|
|
return output, nil
|
|
}
|