From 619332d58ed6f18f5dade3fdea341ee7bdc5d281 Mon Sep 17 00:00:00 2001 From: Cesar Wong Date: Wed, 6 May 2015 10:27:01 -0400 Subject: [PATCH] Utility to convert versioned runtime objects to a set query parameters JSON struct tags are used as parameter names, fields that do not have the omitempty marker are always included. --- pkg/conversion/queryparams/convert.go | 118 +++++++++++++++++ pkg/conversion/queryparams/convert_test.go | 143 +++++++++++++++++++++ pkg/conversion/queryparams/doc.go | 19 +++ 3 files changed, 280 insertions(+) create mode 100644 pkg/conversion/queryparams/convert.go create mode 100644 pkg/conversion/queryparams/convert_test.go create mode 100644 pkg/conversion/queryparams/doc.go diff --git a/pkg/conversion/queryparams/convert.go b/pkg/conversion/queryparams/convert.go new file mode 100644 index 00000000000..943eeab4a4b --- /dev/null +++ b/pkg/conversion/queryparams/convert.go @@ -0,0 +1,118 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 queryparams + +import ( + "fmt" + "net/url" + "reflect" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func jsonTag(field reflect.StructField) (string, bool) { + structTag := field.Tag.Get("json") + if len(structTag) == 0 { + return "", false + } + parts := strings.Split(structTag, ",") + tag := parts[0] + if tag == "-" { + tag = "" + } + omitempty := false + parts = parts[1:] + for _, part := range parts { + if part == "omitempty" { + omitempty = true + break + } + } + return tag, omitempty +} + +func formatValue(value interface{}) string { + return fmt.Sprintf("%v", value) +} + +func isValueKind(kind reflect.Kind) bool { + switch kind { + case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, + reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, + reflect.Float64, reflect.Complex64, reflect.Complex128: + return true + default: + return false + } +} + +func zeroValue(value reflect.Value) bool { + return reflect.DeepEqual(reflect.Zero(value.Type()).Interface(), value.Interface()) +} + +func addParam(values url.Values, tag string, omitempty bool, value reflect.Value) { + if omitempty && zeroValue(value) { + return + } + values.Add(tag, fmt.Sprintf("%v", value.Interface())) +} + +func addListOfParams(values url.Values, tag string, omitempty bool, list reflect.Value) { + for i := 0; i < list.Len(); i++ { + addParam(values, tag, omitempty, list.Index(i)) + } +} + +// Convert takes a versioned runtime.Object and serializes it to a url.Values object +// using JSON tags as parameter names. Only top-level simple values, arrays, and slices +// are serialized. Embedded structs, maps, etc. will not be serialized. +func Convert(obj runtime.Object) (url.Values, error) { + result := url.Values{} + if obj == nil { + return result, nil + } + var sv reflect.Value + switch reflect.TypeOf(obj).Kind() { + case reflect.Ptr, reflect.Interface: + sv = reflect.ValueOf(obj).Elem() + default: + return nil, fmt.Errorf("Expecting a pointer or interface") + } + st := sv.Type() + if st.Kind() != reflect.Struct { + return nil, fmt.Errorf("Expecting a pointer to a struct") + } + for i := 0; i < st.NumField(); i++ { + field := sv.Field(i) + tag, omitempty := jsonTag(st.Field(i)) + if len(tag) == 0 { + continue + } + ft := field.Type() + switch { + case isValueKind(ft.Kind()): + addParam(result, tag, omitempty, field) + case ft.Kind() == reflect.Array || ft.Kind() == reflect.Slice: + if isValueKind(ft.Elem().Kind()) { + addListOfParams(result, tag, omitempty, field) + } + } + } + return result, nil +} diff --git a/pkg/conversion/queryparams/convert_test.go b/pkg/conversion/queryparams/convert_test.go new file mode 100644 index 00000000000..2a124482c0b --- /dev/null +++ b/pkg/conversion/queryparams/convert_test.go @@ -0,0 +1,143 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 queryparams + +import ( + "net/url" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +type namedString string +type namedBool bool + +type bar struct { + Float1 float32 `json:"float1"` + Float2 float64 `json:"float2"` + Int1 int64 `json:"int1,omitempty"` + Int2 int32 `json:"int2,omitempty"` + Int3 int16 `json:"int3,omitempty"` + Str1 string `json:"str1,omitempty"` + Ignored int + Ignored2 string +} + +func (*bar) IsAnAPIObject() {} + +type foo struct { + Str string `json:"str"` + Integer int `json:"integer,omitempty"` + Slice []string `json:"slice,omitempty"` + Boolean bool `json:"boolean,omitempty"` + NamedStr namedString `json:"namedStr,omitempty"` + NamedBool namedBool `json:"namedBool,omitempty"` + Foobar bar `json:"foobar,omitempty"` + Testmap map[string]string `json:"testmap,omitempty"` +} + +func (*foo) IsAnAPIObject() {} + +func validateResult(t *testing.T, input interface{}, actual, expected url.Values) { + local := url.Values{} + for k, v := range expected { + local[k] = v + } + for k, v := range actual { + if ev, ok := local[k]; !ok || !reflect.DeepEqual(ev, v) { + if !ok { + t.Errorf("%#v: actual value key %s not found in expected map", input, k) + } else { + t.Errorf("%#v: values don't match: actual: %#v, expected: %#v", input, v, ev) + } + break + } + delete(local, k) + } + if len(local) > 0 { + t.Errorf("%#v: expected map has keys that were not found in actual map: %#v", input, local) + } +} + +func TestConvert(t *testing.T) { + tests := []struct { + input runtime.Object + expected url.Values + }{ + { + input: &foo{ + Str: "hello", + }, + expected: url.Values{"str": {"hello"}}, + }, + { + input: &foo{ + Str: "test string", + Slice: []string{"one", "two", "three"}, + Integer: 234, + Boolean: true, + }, + expected: url.Values{"str": {"test string"}, "slice": {"one", "two", "three"}, "integer": {"234"}, "boolean": {"true"}}, + }, + { + input: &foo{ + Str: "named types", + NamedStr: "value1", + NamedBool: true, + }, + expected: url.Values{"str": {"named types"}, "namedStr": {"value1"}, "namedBool": {"true"}}, + }, + { + input: &foo{ + Str: "ignore embedded struct", + Foobar: bar{ + Float1: 5.0, + }, + }, + expected: url.Values{"str": {"ignore embedded struct"}}, + }, + { + // Ignore untagged fields + input: &bar{ + Float1: 23.5, + Float2: 100.7, + Int1: 1, + Int2: 2, + Int3: 3, + Ignored: 1, + Ignored2: "ignored", + }, + expected: url.Values{"float1": {"23.5"}, "float2": {"100.7"}, "int1": {"1"}, "int2": {"2"}, "int3": {"3"}}, + }, + { + // include fields that are not tagged omitempty + input: &foo{ + NamedStr: "named str", + }, + expected: url.Values{"str": {""}, "namedStr": {"named str"}}, + }, + } + + for _, test := range tests { + result, err := Convert(test.input) + if err != nil { + t.Errorf("Unexpected error while converting %#v: %v", test.input, err) + } + validateResult(t, test.input, result, test.expected) + } +} diff --git a/pkg/conversion/queryparams/doc.go b/pkg/conversion/queryparams/doc.go new file mode 100644 index 00000000000..0e9127a1899 --- /dev/null +++ b/pkg/conversion/queryparams/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 queryparams provides conversion from versioned +// runtime objects to URL query values +package queryparams