From c45e56579ce539c1b32276e6c2dfd37222dbac82 Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Tue, 30 Aug 2016 13:59:49 -0400 Subject: [PATCH 1/2] Punctuate and format documentation comments --- struct.go | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/struct.go b/struct.go index 19b0570..4e72fe3 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 { @@ -139,7 +140,7 @@ type DataCenterInfo struct { Metadata AmazonMetadataType `xml:"metadata" json:"metadata"` } -// 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"` From bb10d2c195fa6c4826ed3bb579291ffb6b468044 Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Tue, 30 Aug 2016 14:00:48 -0400 Subject: [PATCH 2/2] Accommodate non-Amazon data center info metadata The existing "Metadata" field in the DataCenterInfo struct conveys details specific to instances running within one of Amazon's data centers. For instances running in other types of data centers, introduce a sibling "AlternateMetadata" field, sending and populating it only when the DataCenterInfo's "Name" field is "Amazon". Retaining the original "Metadata" field maintains compatibility with existing callers, unless they had populated that field for data centers with names other than "Amazon". --- marshal.go | 161 +++++++++++++++++++++++++++++++++++++++++- struct.go | 13 +++- tests/marshal_test.go | 112 +++++++++++++++++++++++++++-- 3 files changed, 275 insertions(+), 11 deletions(-) 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 4e72fe3..07e08df 100644 --- a/struct.go +++ b/struct.go @@ -134,10 +134,17 @@ 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. 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) + }) + }) }) }) })