Zack Williams | e940c7a | 2019-08-21 14:25:39 -0700 | [diff] [blame^] | 1 | /* |
| 2 | Copyright 2014 The Kubernetes Authors. |
| 3 | |
| 4 | Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | you may not use this file except in compliance with the License. |
| 6 | You may obtain a copy of the License at |
| 7 | |
| 8 | http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | |
| 10 | Unless required by applicable law or agreed to in writing, software |
| 11 | distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | See the License for the specific language governing permissions and |
| 14 | limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package queryparams |
| 18 | |
| 19 | import ( |
| 20 | "fmt" |
| 21 | "net/url" |
| 22 | "reflect" |
| 23 | "strings" |
| 24 | ) |
| 25 | |
| 26 | // Marshaler converts an object to a query parameter string representation |
| 27 | type Marshaler interface { |
| 28 | MarshalQueryParameter() (string, error) |
| 29 | } |
| 30 | |
| 31 | // Unmarshaler converts a string representation to an object |
| 32 | type Unmarshaler interface { |
| 33 | UnmarshalQueryParameter(string) error |
| 34 | } |
| 35 | |
| 36 | func jsonTag(field reflect.StructField) (string, bool) { |
| 37 | structTag := field.Tag.Get("json") |
| 38 | if len(structTag) == 0 { |
| 39 | return "", false |
| 40 | } |
| 41 | parts := strings.Split(structTag, ",") |
| 42 | tag := parts[0] |
| 43 | if tag == "-" { |
| 44 | tag = "" |
| 45 | } |
| 46 | omitempty := false |
| 47 | parts = parts[1:] |
| 48 | for _, part := range parts { |
| 49 | if part == "omitempty" { |
| 50 | omitempty = true |
| 51 | break |
| 52 | } |
| 53 | } |
| 54 | return tag, omitempty |
| 55 | } |
| 56 | |
| 57 | func formatValue(value interface{}) string { |
| 58 | return fmt.Sprintf("%v", value) |
| 59 | } |
| 60 | |
| 61 | func isPointerKind(kind reflect.Kind) bool { |
| 62 | return kind == reflect.Ptr |
| 63 | } |
| 64 | |
| 65 | func isStructKind(kind reflect.Kind) bool { |
| 66 | return kind == reflect.Struct |
| 67 | } |
| 68 | |
| 69 | func isValueKind(kind reflect.Kind) bool { |
| 70 | switch kind { |
| 71 | case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, |
| 72 | reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, |
| 73 | reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, |
| 74 | reflect.Float64, reflect.Complex64, reflect.Complex128: |
| 75 | return true |
| 76 | default: |
| 77 | return false |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func zeroValue(value reflect.Value) bool { |
| 82 | return reflect.DeepEqual(reflect.Zero(value.Type()).Interface(), value.Interface()) |
| 83 | } |
| 84 | |
| 85 | func customMarshalValue(value reflect.Value) (reflect.Value, bool) { |
| 86 | // Return unless we implement a custom query marshaler |
| 87 | if !value.CanInterface() { |
| 88 | return reflect.Value{}, false |
| 89 | } |
| 90 | |
| 91 | marshaler, ok := value.Interface().(Marshaler) |
| 92 | if !ok { |
| 93 | if !isPointerKind(value.Kind()) && value.CanAddr() { |
| 94 | marshaler, ok = value.Addr().Interface().(Marshaler) |
| 95 | if !ok { |
| 96 | return reflect.Value{}, false |
| 97 | } |
| 98 | } else { |
| 99 | return reflect.Value{}, false |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | // Don't invoke functions on nil pointers |
| 104 | // If the type implements MarshalQueryParameter, AND the tag is not omitempty, AND the value is a nil pointer, "" seems like a reasonable response |
| 105 | if isPointerKind(value.Kind()) && zeroValue(value) { |
| 106 | return reflect.ValueOf(""), true |
| 107 | } |
| 108 | |
| 109 | // Get the custom marshalled value |
| 110 | v, err := marshaler.MarshalQueryParameter() |
| 111 | if err != nil { |
| 112 | return reflect.Value{}, false |
| 113 | } |
| 114 | return reflect.ValueOf(v), true |
| 115 | } |
| 116 | |
| 117 | func addParam(values url.Values, tag string, omitempty bool, value reflect.Value) { |
| 118 | if omitempty && zeroValue(value) { |
| 119 | return |
| 120 | } |
| 121 | val := "" |
| 122 | iValue := fmt.Sprintf("%v", value.Interface()) |
| 123 | |
| 124 | if iValue != "<nil>" { |
| 125 | val = iValue |
| 126 | } |
| 127 | values.Add(tag, val) |
| 128 | } |
| 129 | |
| 130 | func addListOfParams(values url.Values, tag string, omitempty bool, list reflect.Value) { |
| 131 | for i := 0; i < list.Len(); i++ { |
| 132 | addParam(values, tag, omitempty, list.Index(i)) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | // Convert takes an object and converts it to a url.Values object using JSON tags as |
| 137 | // parameter names. Only top-level simple values, arrays, and slices are serialized. |
| 138 | // Embedded structs, maps, etc. will not be serialized. |
| 139 | func Convert(obj interface{}) (url.Values, error) { |
| 140 | result := url.Values{} |
| 141 | if obj == nil { |
| 142 | return result, nil |
| 143 | } |
| 144 | var sv reflect.Value |
| 145 | switch reflect.TypeOf(obj).Kind() { |
| 146 | case reflect.Ptr, reflect.Interface: |
| 147 | sv = reflect.ValueOf(obj).Elem() |
| 148 | default: |
| 149 | return nil, fmt.Errorf("expecting a pointer or interface") |
| 150 | } |
| 151 | st := sv.Type() |
| 152 | if !isStructKind(st.Kind()) { |
| 153 | return nil, fmt.Errorf("expecting a pointer to a struct") |
| 154 | } |
| 155 | |
| 156 | // Check all object fields |
| 157 | convertStruct(result, st, sv) |
| 158 | |
| 159 | return result, nil |
| 160 | } |
| 161 | |
| 162 | func convertStruct(result url.Values, st reflect.Type, sv reflect.Value) { |
| 163 | for i := 0; i < st.NumField(); i++ { |
| 164 | field := sv.Field(i) |
| 165 | tag, omitempty := jsonTag(st.Field(i)) |
| 166 | if len(tag) == 0 { |
| 167 | continue |
| 168 | } |
| 169 | ft := field.Type() |
| 170 | |
| 171 | kind := ft.Kind() |
| 172 | if isPointerKind(kind) { |
| 173 | ft = ft.Elem() |
| 174 | kind = ft.Kind() |
| 175 | if !field.IsNil() { |
| 176 | field = reflect.Indirect(field) |
| 177 | // If the field is non-nil, it should be added to params |
| 178 | // and the omitempty should be overwite to false |
| 179 | omitempty = false |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | switch { |
| 184 | case isValueKind(kind): |
| 185 | addParam(result, tag, omitempty, field) |
| 186 | case kind == reflect.Array || kind == reflect.Slice: |
| 187 | if isValueKind(ft.Elem().Kind()) { |
| 188 | addListOfParams(result, tag, omitempty, field) |
| 189 | } |
| 190 | case isStructKind(kind) && !(zeroValue(field) && omitempty): |
| 191 | if marshalValue, ok := customMarshalValue(field); ok { |
| 192 | addParam(result, tag, omitempty, marshalValue) |
| 193 | } else { |
| 194 | convertStruct(result, ft, field) |
| 195 | } |
| 196 | } |
| 197 | } |
| 198 | } |