Merge pull request #529 from lavalamp/recursiveApiObj

Add APIObject for generic inclusion of API objects
pull/6/head
Clayton Coleman 2014-07-20 14:25:35 -04:00
commit 28b7b53c72
7 changed files with 189 additions and 35 deletions

90
pkg/api/apiobj.go Normal file
View File

@ -0,0 +1,90 @@
/*
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 (
"gopkg.in/v1/yaml"
)
// Encode()/Decode() are the canonical way of converting an API object to/from
// wire format. This file provides utility functions which permit doing so
// recursively, such that API objects of types known only at run time can be
// embedded within other API types.
// UnmarshalJSON implements the json.Unmarshaler interface.
func (a *APIObject) UnmarshalJSON(b []byte) error {
// Handle JSON's "null": Decode() doesn't expect it.
if len(b) == 4 && string(b) == "null" {
a.Object = nil
return nil
}
obj, err := Decode(b)
if err != nil {
return err
}
a.Object = obj
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (a APIObject) MarshalJSON() ([]byte, error) {
if a.Object == nil {
// Encode unset/nil objects as JSON's "null".
return []byte("null"), nil
}
return Encode(a.Object)
}
// SetYAML implements the yaml.Setter interface.
func (a *APIObject) SetYAML(tag string, value interface{}) bool {
if value == nil {
a.Object = nil
return true
}
// Why does the yaml package send value as a map[interface{}]interface{}?
// It's especially frustrating because encoding/json does the right thing
// by giving a []byte. So here we do the embarrasing thing of re-encode and
// de-encode the right way.
// TODO: Write a version of Decode that uses reflect to turn this value
// into an API object.
b, err := yaml.Marshal(value)
if err != nil {
panic("yaml can't reverse it's own object")
}
obj, err := Decode(b)
if err != nil {
return false
}
a.Object = obj
return true
}
// GetYAML implements the yaml.Getter interface.
func (a APIObject) GetYAML() (tag string, value interface{}) {
if a.Object == nil {
value = "null"
return
}
// Encode returns JSON, which is conveniently a subset of YAML.
v, err := Encode(a.Object)
if err != nil {
panic("impossible to encode API object!")
}
return tag, v
}

72
pkg/api/apiobj_test.go Normal file
View File

@ -0,0 +1,72 @@
/*
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"
"reflect"
"testing"
)
func TestAPIObject(t *testing.T) {
type EmbeddedTest struct {
JSONBase `yaml:",inline" json:",inline"`
Object APIObject `yaml:"object,omitempty" json:"object,omitempty"`
EmptyObject APIObject `yaml:"emptyObject,omitempty" json:"emptyObject,omitempty"`
}
AddKnownTypes(EmbeddedTest{})
outer := &EmbeddedTest{
JSONBase: JSONBase{ID: "outer"},
Object: APIObject{
&EmbeddedTest{
JSONBase: JSONBase{ID: "inner"},
},
},
}
wire, err := Encode(outer)
if err != nil {
t.Fatalf("Unexpected encode error '%v'", err)
}
t.Logf("Wire format is:\n%v\n", string(wire))
decoded, err := Decode(wire)
if err != nil {
t.Fatalf("Unexpected decode error %v", err)
}
if e, a := outer, decoded; !reflect.DeepEqual(e, a) {
t.Errorf("Expected: %#v but got %#v", e, a)
}
// test JSON decoding, too, since api.Decode uses yaml unmarshalling.
var decodedViaJSON EmbeddedTest
err = json.Unmarshal(wire, &decodedViaJSON)
if err != nil {
t.Fatalf("Unexpected decode error %v", err)
}
// Things that Decode would have done for us:
decodedViaJSON.Kind = ""
if e, a := outer, &decodedViaJSON; !reflect.DeepEqual(e, a) {
t.Errorf("Expected: %#v but got %#v", e, a)
}
}

View File

@ -66,7 +66,7 @@ func FindJSONBaseRO(obj interface{}) (JSONBase, error) {
v = v.Elem() v = v.Elem()
} }
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
return JSONBase{}, fmt.Errorf("expected struct, but got %v", v.Type().Name()) return JSONBase{}, fmt.Errorf("expected struct, but got %v (%#v)", v.Type().Name(), v.Interface())
} }
jsonBase := v.FieldByName("JSONBase") jsonBase := v.FieldByName("JSONBase")
if !jsonBase.IsValid() { if !jsonBase.IsValid() {
@ -125,7 +125,7 @@ func nameAndJSONBase(obj interface{}) (string, *JSONBase, error) {
v = v.Elem() v = v.Elem()
name := v.Type().Name() name := v.Type().Name()
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
return "", nil, fmt.Errorf("expected struct, but got %v", name) return "", nil, fmt.Errorf("expected struct, but got %v: %v (%#v)", v.Kind(), v.Type().Name(), v.Interface())
} }
jsonBase := v.FieldByName("JSONBase") jsonBase := v.FieldByName("JSONBase")
if !jsonBase.IsValid() { if !jsonBase.IsValid() {

View File

@ -66,7 +66,7 @@ type Volume struct {
// Source represents the location and type of a volume to mount. // Source represents the location and type of a volume to mount.
// This is optional for now. If not specified, the Volume is implied to be an EmptyDir. // This is optional for now. If not specified, the Volume is implied to be an EmptyDir.
// This implied behavior is deprecated and will be removed in a future version. // This implied behavior is deprecated and will be removed in a future version.
Source *VolumeSource `yaml:"source" json:"source"` Source *VolumeSource `yaml:"source" json:"source"`
} }
type VolumeSource struct { type VolumeSource struct {
@ -86,7 +86,7 @@ type HostDirectory struct {
Path string `yaml:"path" json:"path"` Path string `yaml:"path" json:"path"`
} }
type EmptyDirectory struct {} type EmptyDirectory struct{}
// Port represents a network port in a single container // Port represents a network port in a single container
type Port struct { type Port struct {
@ -345,6 +345,16 @@ type WatchEvent struct {
// The type of the watch event; added, modified, or deleted. // The type of the watch event; added, modified, or deleted.
Type watch.EventType Type watch.EventType
// An object which can be decoded via api.Decode // For added or modified objects, this is the new object; for deleted objects,
EmbeddedObject []byte // it's the state of the object immediately prior to its deletion.
Object APIObject
}
// APIObject has appropriate encoder and decoder functions, such that on the wire, it's
// stored as a []byte, but in memory, the contained object is accessable as an interface{}
// via the Get() function. Only objects having a JSONBase may be stored via APIObject.
// The purpose of this is to allow an API object of type known only at runtime to be
// embedded within other API objects.
type APIObject struct {
Object interface{}
} }

View File

@ -208,7 +208,7 @@ func TestValidateManifest(t *testing.T) {
Version: "v1beta1", Version: "v1beta1",
ID: "abc", ID: "abc",
Volumes: []Volume{{Name: "vol1", Source: &VolumeSource{HostDirectory: &HostDirectory{"/mnt/vol1"}}}, Volumes: []Volume{{Name: "vol1", Source: &VolumeSource{HostDirectory: &HostDirectory{"/mnt/vol1"}}},
{Name: "vol2", Source: &VolumeSource{HostDirectory: &HostDirectory{"/mnt/vol2"}}}}, {Name: "vol2", Source: &VolumeSource{HostDirectory: &HostDirectory{"/mnt/vol2"}}}},
Containers: []Container{ Containers: []Container{
{ {
Name: "abc", Name: "abc",

View File

@ -503,14 +503,9 @@ func (w *WatchServer) HandleWS(ws *websocket.Conn) {
// End of results. // End of results.
return return
} }
wireFormat, err := api.Encode(event.Object) err := websocket.JSON.Send(ws, &api.WatchEvent{
if err != nil { Type: event.Type,
glog.Errorf("error encoding %#v: %v", event.Object, err) Object: api.APIObject{event.Object},
return
}
err = websocket.JSON.Send(ws, &api.WatchEvent{
Type: event.Type,
EmbeddedObject: wireFormat,
}) })
if err != nil { if err != nil {
// Client disconnect. // Client disconnect.
@ -555,14 +550,9 @@ func (self *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// End of results. // End of results.
return return
} }
wireFormat, err := api.Encode(event.Object) err := encoder.Encode(&api.WatchEvent{
if err != nil { Type: event.Type,
glog.Errorf("error encoding %#v: %v", event.Object, err) Object: api.APIObject{event.Object},
return
}
err = encoder.Encode(&api.WatchEvent{
Type: event.Type,
EmbeddedObject: wireFormat,
}) })
if err != nil { if err != nil {
// Client disconnect. // Client disconnect.

View File

@ -595,12 +595,8 @@ func TestWatchWebsocket(t *testing.T) {
if got.Type != action { if got.Type != action {
t.Errorf("Unexpected type: %v", got.Type) t.Errorf("Unexpected type: %v", got.Type)
} }
apiObj, err := api.Decode(got.EmbeddedObject) if e, a := object, got.Object.Object; !reflect.DeepEqual(e, a) {
if err != nil { t.Errorf("Expected %v, got %v", e, a)
t.Fatalf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(object, apiObj) {
t.Errorf("Expected %v, got %v", object, apiObj)
} }
} }
@ -654,12 +650,8 @@ func TestWatchHTTP(t *testing.T) {
if got.Type != action { if got.Type != action {
t.Errorf("Unexpected type: %v", got.Type) t.Errorf("Unexpected type: %v", got.Type)
} }
apiObj, err := api.Decode(got.EmbeddedObject) if e, a := object, got.Object.Object; !reflect.DeepEqual(e, a) {
if err != nil { t.Errorf("Expected %v, got %v", e, a)
t.Fatalf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(object, apiObj) {
t.Errorf("Expected %v, got %v", object, apiObj)
} }
} }