From a9d9bb9d47ced055db5ad3da1accd67e221288f3 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Fri, 20 Jun 2014 15:12:12 -0700 Subject: [PATCH] Add intelligent type marshalling/unmarshalling --- pkg/api/helper.go | 111 +++++++++++++++++++++++++++++++++++++++++ pkg/api/helper_test.go | 74 +++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 pkg/api/helper.go create mode 100644 pkg/api/helper_test.go diff --git a/pkg/api/helper.go b/pkg/api/helper.go new file mode 100644 index 00000000000..3aeddb0b0ff --- /dev/null +++ b/pkg/api/helper.go @@ -0,0 +1,111 @@ +/* +Copyright 2014 Google Inc. 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 api + +import ( + "encoding/json" + "fmt" + "reflect" +) + +var knownTypes = map[string]reflect.Type{} + +func init() { + types := []interface{}{ + PodList{}, Pod{}, ReplicationControllerList{}, + ReplicationController{}, ServiceList{}, Service{}, + } + for _, obj := range types { + t := reflect.TypeOf(obj) + knownTypes[t.Name()] = t + } +} + +// Returns the name of the type (sans pointer), and its kind field. Takes pointer-to-struct.. +func nameAndJSONBase(obj interface{}) (string, reflect.Value, error) { + v := reflect.ValueOf(obj) + if v.Kind() != reflect.Ptr { + return "", reflect.Value{}, fmt.Errorf("expected pointer, but got %v", v.Type().Name()) + } + v = v.Elem() + name := v.Type().Name() + if v.Kind() != reflect.Struct { + return "", reflect.Value{}, fmt.Errorf("expected struct, but got %v", name) + } + jsonBase := v.FieldByName("JSONBase") + if !jsonBase.IsValid() { + return "", reflect.Value{}, fmt.Errorf("struct %v lacks embedded JSON type", name) + } + return name, jsonBase, nil +} + +// Encode turns the given api object into an appropriate JSON string. +// Will return an error if the object doesn't have an embedded JSONBase. +// Obj must be a pointer to a struct. Note, this sets the object's Kind +// field. +func Encode(obj interface{}) (data []byte, err error) { + name, jsonBase, err := nameAndJSONBase(obj) + if err != nil { + return nil, err + } + if _, contains := knownTypes[name]; !contains { + return nil, fmt.Errorf("struct %v can't be unmarshalled because it's not in knownTypes", name) + } + jsonBase.FieldByName("Kind").Set(reflect.ValueOf(name)) + return json.Marshal(obj) +} + +// Decode converts a JSON string back into a pointer to an api object. Deduces the type +// based upon the Kind field (set by encode). +func Decode(data []byte) (interface{}, error) { + findKind := struct { + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + }{} + err := json.Unmarshal(data, &findKind) + if err != nil { + return nil, fmt.Errorf("Couldn't get kind: %#v", err) + } + objType, found := knownTypes[findKind.Kind] + if !found { + return nil, fmt.Errorf("%v is not a known type", findKind.Kind) + } + obj := reflect.New(objType).Interface() + err = json.Unmarshal(data, obj) + if err != nil { + return nil, err + } + return obj, nil +} + +// DecodeInto parses a JSON string and stores it in obj. Returns an error +// if data.Kind is set and doesn't match the type of obj. Obj should be a +// pointer to an api type. +func DecodeInto(data []byte, obj interface{}) error { + err := json.Unmarshal(data, obj) + if err != nil { + return err + } + name, jsonBase, err := nameAndJSONBase(obj) + if err != nil { + return err + } + foundName := jsonBase.FieldByName("Kind").Interface().(string) + if foundName != "" && foundName != name { + return fmt.Errorf("data had kind %v, but passed object was of type %v", foundName, name) + } + return nil +} diff --git a/pkg/api/helper_test.go b/pkg/api/helper_test.go new file mode 100644 index 00000000000..93ed887c6cc --- /dev/null +++ b/pkg/api/helper_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2014 Google Inc. 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 api + +import ( + "reflect" + "testing" +) + +func runTest(t *testing.T, source interface{}) { + name := reflect.TypeOf(source).Name() + data, err := Encode(source) + if err != nil { + t.Errorf("%v: %v", name, err) + return + } + obj2, err := Decode(data) + if err != nil { + t.Errorf("%v: %v", name, err) + return + } + if !reflect.DeepEqual(source, obj2) { + t.Errorf("%v: wanted %#v, got %#v", name, source, obj2) + return + } + obj3 := reflect.New(reflect.TypeOf(source).Elem()).Interface() + err = DecodeInto(data, obj3) + if err != nil { + t.Errorf("%v: %v", name, err) + return + } + if !reflect.DeepEqual(source, obj3) { + t.Errorf("%v: wanted %#v, got %#v", name, source, obj3) + return + } +} + +func TestTypes(t *testing.T) { + // TODO: auto-fill all fields. + table := []interface{}{ + &Pod{ + JSONBase: JSONBase{ + ID: "mylittlepod", + }, + Labels: map[string]string{ + "name": "pinky", + }, + }, + &Service{}, + &ServiceList{}, + &ReplicationControllerList{}, + &ReplicationController{}, + &PodList{}, + } + for _, item := range table { + runTest(t, item) + } +} + +// TODO: test rejection of bad JSON.