diff --git a/marshal.go b/marshal.go index 46ed617..5f72971 100644 --- a/marshal.go +++ b/marshal.go @@ -5,6 +5,7 @@ package fargo import ( "encoding/json" "encoding/xml" + "io" "strconv" ) @@ -122,17 +123,22 @@ func (i *InstanceMetadata) MarshalJSON() ([]byte, error) { return i.Raw, nil } +// startLocalName creates a start-tag of an XML element with the given local name and no namespace name. +func startLocalName(local string) xml.StartElement { + return xml.StartElement{Name: xml.Name{Space: "", Local: local}} +} + // MarshalXML is a custom XML marshaler for InstanceMetadata. func (i InstanceMetadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error { tokens := []xml.Token{start} if i.parsed != nil { for key, value := range i.parsed { - t := xml.StartElement{Name: xml.Name{"", key}} - tokens = append(tokens, t, xml.CharData(value.(string)), xml.EndElement{t.Name}) + t := startLocalName(key) + tokens = append(tokens, t, xml.CharData(value.(string)), xml.EndElement{Name: t.Name}) } } - tokens = append(tokens, xml.EndElement{start.Name}) + tokens = append(tokens, xml.EndElement{Name: start.Name}) for _, t := range tokens { err := e.EncodeToken(t) @@ -144,3 +150,152 @@ func (i InstanceMetadata) MarshalXML(e *xml.Encoder, start xml.StartElement) err // flush to ensure tokens are written return e.Flush() } + +type metadataMap map[string]string + +// MarshalXML is a custom XML marshaler for metadataMap, mapping each metadata name/value pair to a +// correspondingly named XML element with the pair's value as character data content. +func (m metadataMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if err := e.EncodeToken(start); err != nil { + return err + } + + for k, v := range m { + if err := e.EncodeElement(v, startLocalName(k)); err != nil { + return err + } + } + + return e.EncodeToken(start.End()) +} + +// UnmarshalXML is a custom XML unmarshaler for metadataMap, mapping each XML element's name and +// character data content to a corresponding metadata name/value pair. +func (m metadataMap) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var v string + for { + t, err := d.Token() + if err != nil { + if err == io.EOF { + break + } + return err + } + if k, ok := t.(xml.StartElement); ok { + if err := d.DecodeElement(&v, &k); err != nil { + return err + } + m[k.Name.Local] = v + } + } + return nil +} + +func metadataValue(i DataCenterInfo) interface{} { + if i.Name == Amazon { + return i.Metadata + } + return metadataMap(i.AlternateMetadata) +} + +var ( + startName = startLocalName("name") + startMetadata = startLocalName("metadata") +) + +// MarshalXML is a custom XML marshaler for DataCenterInfo, writing either Metadata or AlternateMetadata +// depending on the type of data center indicated by the Name. +func (i DataCenterInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if err := e.EncodeToken(start); err != nil { + return err + } + + if err := e.EncodeElement(i.Name, startName); err != nil { + return err + } + if err := e.EncodeElement(metadataValue(i), startMetadata); err != nil { + return err + } + + return e.EncodeToken(start.End()) +} + +type preliminaryDataCenterInfo struct { + Name string `xml:"name" json:"name"` + Metadata metadataMap `xml:"metadata" json:"metadata"` +} + +func bindValue(dst *string, src map[string]string, k string) bool { + if v, ok := src[k]; ok { + *dst = v + return true + } + return false +} + +func populateAmazonMetadata(dst *AmazonMetadataType, src map[string]string) { + bindValue(&dst.AmiLaunchIndex, src, "ami-launch-index") + bindValue(&dst.LocalHostname, src, "local-hostname") + bindValue(&dst.AvailabilityZone, src, "availability-zone") + bindValue(&dst.InstanceID, src, "instance-id") + bindValue(&dst.PublicIpv4, src, "public-ipv4") + bindValue(&dst.PublicHostname, src, "public-hostname") + bindValue(&dst.AmiManifestPath, src, "ami-manifest-path") + bindValue(&dst.LocalIpv4, src, "local-ipv4") + bindValue(&dst.HostName, src, "hostname") + bindValue(&dst.AmiID, src, "ami-id") + bindValue(&dst.InstanceType, src, "instance-type") +} + +func adaptDataCenterInfo(dst *DataCenterInfo, src preliminaryDataCenterInfo) { + dst.Name = src.Name + if src.Name == Amazon { + populateAmazonMetadata(&dst.Metadata, src.Metadata) + } else { + dst.AlternateMetadata = src.Metadata + } +} + +// UnmarshalXML is a custom XML unmarshaler for DataCenterInfo, populating either Metadata or AlternateMetadata +// depending on the type of data center indicated by the Name. +func (i *DataCenterInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + p := preliminaryDataCenterInfo{ + Metadata: make(map[string]string, 11), + } + if err := d.DecodeElement(&p, &start); err != nil { + return err + } + adaptDataCenterInfo(i, p) + return nil +} + +// MarshalJSON is a custom JSON marshaler for DataCenterInfo, writing either Metadata or AlternateMetadata +// depending on the type of data center indicated by the Name. +func (i DataCenterInfo) MarshalJSON() ([]byte, error) { + type named struct { + Name string `json:"name"` + } + if i.Name == Amazon { + return json.Marshal(struct { + named + Metadata AmazonMetadataType `json:"metadata"` + }{named{i.Name}, i.Metadata}) + } + return json.Marshal(struct { + named + Metadata map[string]string `json:"metadata"` + }{named{i.Name}, i.AlternateMetadata}) +} + +// UnmarshalJSON is a custom JSON unmarshaler for DataCenterInfo, populating either Metadata or AlternateMetadata +// depending on the type of data center indicated by the Name. +func (i *DataCenterInfo) UnmarshalJSON(b []byte) error { + p := preliminaryDataCenterInfo{ + Metadata: make(map[string]string, 11), + } + if err := json.Unmarshal(b, &p); err != nil { + return err + } + adaptDataCenterInfo(i, p) + return nil +} diff --git a/struct.go b/struct.go index 19b0570..07e08df 100644 --- a/struct.go +++ b/struct.go @@ -6,13 +6,13 @@ import ( "time" ) -// EurekaUrlSlugs is a map of resource names -> eureka URLs +// EurekaUrlSlugs is a map of resource names->Eureka URLs. var EurekaURLSlugs = map[string]string{ "Apps": "apps", "Instances": "instances", } -// EurekaConnection is the settings required to make eureka requests +// EurekaConnection is the settings required to make Eureka requests. type EurekaConnection struct { ServiceUrls []string ServicePort int @@ -26,30 +26,30 @@ type EurekaConnection struct { UseJson bool } -// GetAppsResponseJson lets us deserialize the eureka/v2/apps response JSON--a wrapped GetAppsResponse +// GetAppsResponseJson lets us deserialize the eureka/v2/apps response JSON—a wrapped GetAppsResponse. type GetAppsResponseJson struct { Response *GetAppsResponse `json:"applications"` } -// GetAppsResponse lets us deserialize the eureka/v2/apps response XML +// GetAppsResponse lets us deserialize the eureka/v2/apps response XML. type GetAppsResponse struct { Applications []*Application `xml:"application" json:"application"` AppsHashcode string `xml:"apps__hashcode" json:"apps__hashcode"` VersionsDelta int `xml:"versions__delta" json:"versions__delta"` } -// Application deserializeable from Eureka JSON +// Application deserializeable from Eureka JSON. type GetAppResponseJson struct { Application Application `json:"application"` } -// Application deserializeable from Eureka XML +// Application deserializeable from Eureka XML. type Application struct { Name string `xml:"name" json:"name"` Instances []*Instance `xml:"instance" json:"instance"` } -// StatusType is an enum of the different statuses allowed by Eureka +// StatusType is an enum of the different statuses allowed by Eureka. type StatusType string // Supported statuses @@ -67,12 +67,12 @@ const ( MyOwn = "MyOwn" ) -// RegisterInstanceJson lets us serialize the eureka/v2/apps/ request JSON--a wrapped Instance +// RegisterInstanceJson lets us serialize the eureka/v2/apps/ request JSON—a wrapped Instance. type RegisterInstanceJson struct { Instance *Instance `json:"instance"` } -// Instance [de]serializeable [to|from] Eureka XML +// Instance [de]serializeable [to|from] Eureka XML. type Instance struct { XMLName struct{} `xml:"instance" json:"-"` HostName string `xml:"hostName" json:"hostName"` @@ -102,21 +102,22 @@ type Instance struct { UniqueID func(i Instance) string `xml:"-" json:"-"` } -// Port struct used for JSON [un]marshaling only -// looks like: "port":{"@enabled":"true", "$":"7101"}, +// Port struct used for JSON [un]marshaling only. +// An example: +// "port":{"@enabled":"true", "$":"7101"} type Port struct { Number string `json:"$"` Enabled string `json:"@enabled"` } -// InstanceMetadata represents the eureka metadata, which is arbitrary XML. See -// metadata.go for more info. +// InstanceMetadata represents the eureka metadata, which is arbitrary XML. +// See metadata.go for more info. type InstanceMetadata struct { Raw []byte `xml:",innerxml" json:"-"` parsed map[string]interface{} } -// AmazonMetadataType is information about AZ's, AMI's, and the AWS instance +// AmazonMetadataType is information about AZ's, AMI's, and the AWS instance. // // from http://docs.amazonwebservices.com/AWSEC2/latest/DeveloperGuide/index.html?AESDG-chapter-instancedata.html type AmazonMetadataType struct { @@ -133,13 +134,20 @@ type AmazonMetadataType struct { InstanceType string `xml:"instance-type" json:"instance-type"` } -// DataCenterInfo is only really useful when running in AWS. +// DataCenterInfo indicates which type of data center hosts this instance +// and conveys details about the instance's environment. type DataCenterInfo struct { - Name string `xml:"name" json:"name"` - Metadata AmazonMetadataType `xml:"metadata" json:"metadata"` + // Name indicates which type of data center hosts this instance. + Name string + // Metadata provides details specific to an Amazon data center, + // populated and honored when the Name field's value is "Amazon". + Metadata AmazonMetadataType + // AlternateMetadata provides details specific to a data center other than Amazon, + // populated and honored when the Name field's value is not "Amazon". + AlternateMetadata map[string]string } -// LeaseInfo tells us about the renewal from Eureka, including how old it is +// LeaseInfo tells us about the renewal from Eureka, including how old it is. type LeaseInfo struct { RenewalIntervalInSecs int32 `xml:"renewalIntervalInSecs" json:"renewalIntervalInSecs"` DurationInSecs int32 `xml:"durationInSecs" json:"durationInSecs"` diff --git a/tests/marshal_test.go b/tests/marshal_test.go index 9e60d95..7b4ec3c 100644 --- a/tests/marshal_test.go +++ b/tests/marshal_test.go @@ -4,11 +4,13 @@ package fargo_test import ( "encoding/json" + "encoding/xml" "fmt" - "github.com/hudl/fargo" - . "github.com/smartystreets/goconvey/convey" "io/ioutil" "testing" + + "github.com/hudl/fargo" + . "github.com/smartystreets/goconvey/convey" ) func TestJsonMarshal(t *testing.T) { @@ -44,18 +46,118 @@ func TestJsonMarshal(t *testing.T) { } func TestMetadataMarshal(t *testing.T) { - Convey("Given an InstanceMetadata", t, func() { + Convey("Given an Instance with metadata", t, func() { ins := &fargo.Instance{} ins.SetMetadataString("key1", "value1") ins.SetMetadataString("key2", "value2") - Convey("When the metadata are marshalled", func() { + Convey("When the metadata are marshalled as JSON", func() { b, err := json.Marshal(&ins.Metadata) - fmt.Printf("(debug info b = %s)", b) Convey("The marshalled JSON should have these values", func() { + So(err, ShouldBeNil) So(string(b), ShouldEqual, `{"key1":"value1","key2":"value2"}`) + }) + }) + + Convey("When the metadata are marshalled as XML", func() { + b, err := xml.Marshal(&ins.Metadata) + + Convey("The marshalled XML should have this value", func() { So(err, ShouldBeNil) + So(string(b), ShouldBeIn, + "value1value2", + "value2value1") + }) + }) + }) +} + +func TestDataCenterInfoMarshal(t *testing.T) { + Convey("Given an Instance situated in a data center", t, func() { + ins := &fargo.Instance{} + + Convey("When the data center name is \"Amazon\"", func() { + ins.DataCenterInfo.Name = fargo.Amazon + ins.DataCenterInfo.Metadata.InstanceID = "123" + ins.DataCenterInfo.Metadata.HostName = "expected.local" + + Convey("When the data center info is marshalled as JSON", func() { + b, err := json.Marshal(&ins.DataCenterInfo) + + Convey("The marshalled JSON should have these values", func() { + So(err, ShouldBeNil) + So(string(b), ShouldEqual, `{"name":"Amazon","metadata":{"ami-launch-index":"","local-hostname":"","availability-zone":"","instance-id":"123","public-ipv4":"","public-hostname":"","ami-manifest-path":"","local-ipv4":"","hostname":"expected.local","ami-id":"","instance-type":""}}`) + + Convey("The value unmarshalled from JSON should have the same values as the original", func() { + d := fargo.DataCenterInfo{} + err := json.Unmarshal(b, &d) + + So(err, ShouldBeNil) + So(d, ShouldResemble, ins.DataCenterInfo) + }) + }) + }) + + Convey("When the data center info is marshalled as XML", func() { + b, err := xml.Marshal(&ins.DataCenterInfo) + + Convey("The marshalled XML should have this value", func() { + So(err, ShouldBeNil) + So(string(b), ShouldEqual, "Amazon123expected.local") + + Convey("The value unmarshalled from XML should have the same values as the original", func() { + d := fargo.DataCenterInfo{} + err := xml.Unmarshal(b, &d) + + So(err, ShouldBeNil) + So(d, ShouldResemble, ins.DataCenterInfo) + }) + }) + }) + }) + + Convey("When the data center name is not \"Amazon\"", func() { + ins.DataCenterInfo.Name = fargo.MyOwn + ins.DataCenterInfo.AlternateMetadata = map[string]string{ + "instanceId": "123", + "hostName": "expected.local", + } + + Convey("When the data center info is marshalled as JSON", func() { + b, err := json.Marshal(&ins.DataCenterInfo) + + Convey("The marshalled JSON should have these values", func() { + So(err, ShouldBeNil) + So(string(b), ShouldEqual, `{"name":"MyOwn","metadata":{"hostName":"expected.local","instanceId":"123"}}`) + + Convey("The value unmarshalled from JSON should have the same values as the original", func() { + d := fargo.DataCenterInfo{} + err := json.Unmarshal(b, &d) + + So(err, ShouldBeNil) + So(d, ShouldResemble, ins.DataCenterInfo) + }) + }) + }) + + Convey("When the data center info is marshalled as XML", func() { + b, err := xml.Marshal(&ins.DataCenterInfo) + + Convey("The marshalled XML should have this value", func() { + So(err, ShouldBeNil) + So(string(b), ShouldBeIn, + "MyOwnexpected.local123", + "MyOwn123expected.local") + + Convey("The value unmarshalled from XML should have the same values as the original", func() { + d := fargo.DataCenterInfo{} + err := xml.Unmarshal(b, &d) + + So(err, ShouldBeNil) + So(d, ShouldResemble, ins.DataCenterInfo) + }) + }) }) }) })