diff --git a/marshal.go b/marshal.go index a6f9eac..8bd3938 100644 --- a/marshal.go +++ b/marshal.go @@ -5,42 +5,66 @@ package fargo import ( "encoding/json" "encoding/xml" + "fmt" "io" "strconv" ) -// Temporary structs used for GetAppsResponse unmarshalling -type getAppsResponseArray GetAppsResponse -type getAppsResponseSingle struct { - Application *Application `json:"application"` - AppsHashcode string `json:"apps__hashcode"` - VersionsDelta int `json:"versions__delta"` +func intFromJSONNumberOrString(jv interface{}, description string) (int, error) { + switch v := jv.(type) { + case float64: + return int(v), nil + case string: + n, err := strconv.Atoi(v) + if err != nil { + return 0, err + } + return n, nil + default: + return 0, fmt.Errorf("unexpected %s: %[2]v (type %[2]T)", description, jv) + } } // UnmarshalJSON is a custom JSON unmarshaler for GetAppsResponse to deal with // sometimes non-wrapped Application arrays when there is only a single Application item. func (r *GetAppsResponse) UnmarshalJSON(b []byte) error { marshalLog.Debugf("GetAppsResponse.UnmarshalJSON b:%s\n", string(b)) - var err error + resolveDelta := func(d interface{}) (int, error) { + return intFromJSONNumberOrString(d, "versions delta") + } // Normal array case - var ra getAppsResponseArray - if err = json.Unmarshal(b, &ra); err == nil { - marshalLog.Debug("GetAppsResponse.UnmarshalJSON ra:%+v\n", ra) - *r = GetAppsResponse(ra) - return nil + type getAppsResponse GetAppsResponse + auxArray := struct { + *getAppsResponse + VersionsDelta interface{} `json:"versions__delta"` + }{ + getAppsResponse: (*getAppsResponse)(r), } + var err error + if err = json.Unmarshal(b, &auxArray); err == nil { + marshalLog.Debugf("GetAppsResponse.UnmarshalJSON array:%+v\n", auxArray) + r.VersionsDelta, err = resolveDelta(auxArray.VersionsDelta) + return err + } + // Bogus non-wrapped case - var rs getAppsResponseSingle - if err = json.Unmarshal(b, &rs); err == nil { - marshalLog.Debug("GetAppsResponse.UnmarshalJSON rs:%+v\n", rs) - r.Applications = make([]*Application, 1, 1) - r.Applications[0] = rs.Application - r.AppsHashcode = rs.AppsHashcode - r.VersionsDelta = rs.VersionsDelta - return nil + auxSingle := struct { + Application *Application `json:"application"` + AppsHashcode string `json:"apps__hashcode"` + VersionsDelta interface{} `json:"versions__delta"` + }{} + if err := json.Unmarshal(b, &auxSingle); err != nil { + return err } - return err + marshalLog.Debugf("GetAppsResponse.UnmarshalJSON single:%+v\n", auxSingle) + if r.VersionsDelta, err = resolveDelta(auxSingle.VersionsDelta); err != nil { + return err + } + r.Applications = make([]*Application, 1, 1) + r.Applications[0] = auxSingle.Application + r.AppsHashcode = auxSingle.AppsHashcode + return nil } // Temporary structs used for Application unmarshalling @@ -59,7 +83,7 @@ func (a *Application) UnmarshalJSON(b []byte) error { // Normal array case var aa applicationArray if err = json.Unmarshal(b, &aa); err == nil { - marshalLog.Debug("Application.UnmarshalJSON aa:%+v\n", aa) + marshalLog.Debugf("Application.UnmarshalJSON aa:%+v\n", aa) *a = Application(aa) return nil } @@ -67,7 +91,7 @@ func (a *Application) UnmarshalJSON(b []byte) error { // Bogus non-wrapped case var as applicationSingle if err = json.Unmarshal(b, &as); err == nil { - marshalLog.Debug("Application.UnmarshalJSON as:%+v\n", as) + marshalLog.Debugf("Application.UnmarshalJSON as:%+v\n", as) a.Name = as.Name a.Instances = make([]*Instance, 1, 1) a.Instances[0] = as.Instance @@ -76,42 +100,6 @@ func (a *Application) UnmarshalJSON(b []byte) error { return err } -// jsonFormatPort describes an instance's network port, including whether its registrant considers -// the port to be enabled or disabled. -// -// Example JSON encoding: -// -// "port":{"@enabled":"true", "$":"7101"} -// -// Note that later versions of Eureka write the port number as a JSON number rather than as a -// decimal-formatted string. -type jsonFormatPort struct { - Number string `json:"$"` - Enabled string `json:"@enabled"` -} - -func boolAsString(b bool) string { - if b { - return "true" - } - return "false" -} - -func makeJSONFormatPort(port int, enabled bool) jsonFormatPort { - return jsonFormatPort{ - strconv.Itoa(port), - boolAsString(enabled), - } -} - -func parsePort(s string) int { - n, err := strconv.Atoi(s) - if err != nil { - log.Warningf("Invalid port error: %s", err.Error()) - } - return n -} - func stringAsBool(s string) bool { return s == "true" } @@ -119,36 +107,78 @@ func stringAsBool(s string) bool { // UnmarshalJSON is a custom JSON unmarshaler for Instance, transcribing the two composite port // specifications up to top-level fields. func (i *Instance) UnmarshalJSON(b []byte) error { + // Preclude recursive calls to MarshalJSON. type instance Instance + // inboundJSONFormatPort describes an instance's network port, including whether its registrant + // considers the port to be enabled or disabled. + // + // Example JSON encoding: + // + // Eureka versions 1.2.1 and prior: + // "port":{"@enabled":"true", "$":"7101"} + // + // Eureka version 1.2.2 and later: + // "port":{"@enabled":"true", "$":7101} + // + // Note that later versions of Eureka write the port number as a JSON number rather than as a + // decimal-formatted string. We accept it as either an integer or a string. Strangely, the + // "@enabled" field remains a string. + type inboundJSONFormatPort struct { + Number interface{} `json:"$"` + Enabled bool `json:"@enabled,string"` + } aux := struct { *instance - Port jsonFormatPort `json:"port"` - SecurePort jsonFormatPort `json:"securePort"` + Port inboundJSONFormatPort `json:"port"` + SecurePort inboundJSONFormatPort `json:"securePort"` }{ instance: (*instance)(i), } if err := json.Unmarshal(b, &aux); err != nil { return err } - i.Port = parsePort(aux.Port.Number) - i.PortEnabled = stringAsBool(aux.Port.Enabled) - i.SecurePort = parsePort(aux.SecurePort.Number) - i.SecurePortEnabled = stringAsBool(aux.SecurePort.Enabled) + resolvePort := func(port interface{}) (int, error) { + return intFromJSONNumberOrString(port, "port number") + } + var err error + if i.Port, err = resolvePort(aux.Port.Number); err != nil { + return err + } + i.PortEnabled = aux.Port.Enabled + if i.SecurePort, err = resolvePort(aux.SecurePort.Number); err != nil { + return err + } + i.SecurePortEnabled = aux.SecurePort.Enabled return nil } // MarshalJSON is a custom JSON marshaler for Instance, adapting the top-level raw port values to // the composite port specifications. func (i *Instance) MarshalJSON() ([]byte, error) { + // Preclude recursive calls to MarshalJSON. type instance Instance + // outboundJSONFormatPort describes an instance's network port, including whether its registrant + // considers the port to be enabled or disabled. + // + // Example JSON encoding: + // + // "port":{"@enabled":"true", "$":"7101"} + // + // Note that later versions of Eureka write the port number as a JSON number rather than as a + // decimal-formatted string. We emit the port number as a string, not knowing the Eureka + // server's version. Strangely, the "@enabled" field remains a string. + type outboundJSONFormatPort struct { + Number int `json:"$,string"` + Enabled bool `json:"@enabled,string"` + } aux := struct { *instance - Port jsonFormatPort `json:"port"` - SecurePort jsonFormatPort `json:"securePort"` + Port outboundJSONFormatPort `json:"port"` + SecurePort outboundJSONFormatPort `json:"securePort"` }{ (*instance)(i), - makeJSONFormatPort(i.Port, i.PortEnabled), - makeJSONFormatPort(i.SecurePort, i.SecurePortEnabled), + outboundJSONFormatPort{i.Port, i.PortEnabled}, + outboundJSONFormatPort{i.SecurePort, i.SecurePortEnabled}, } return json.Marshal(&aux) } diff --git a/tests/marshal_sample/apps-sample-1-1-post-v122.json b/tests/marshal_sample/apps-sample-1-1-post-v122.json new file mode 100644 index 0000000..d0bafe6 --- /dev/null +++ b/tests/marshal_sample/apps-sample-1-1-post-v122.json @@ -0,0 +1,49 @@ +{ + "applications":{ + "versions__delta":"1", + "apps__hashcode":"UP_24_", + "application":[ + { + "name":"TESTENG.METRICSD", + "instance":{ + "hostName":"192.168.40.94:7101", + "app":"TESTENG.METRICSD", + "ipAddr":"192.168.40.94", + "status":"UP", + "overriddenstatus":"UP", + "port":{ + "@enabled":"true", + "$":7101 + }, + "securePort":{ + "@enabled":"true", + "$":7101 + }, + "countryId":1, + "dataCenterInfo":{ + "@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name":"MyOwn" + }, + "leaseInfo":{ + "renewalIntervalInSecs":30, + "durationInSecs":90, + "registrationTimestamp":1402450388650, + "lastRenewalTimestamp":1402552694257, + "evictionTimestamp":0, + "serviceUpTimestamp":1402450388774 + }, + "metadata":{ + "service-query-uri":"https://192.168.40.94:7101", + "base-uri":"https://192.168.40.94:7101", + "swagger-api-docs":"https://192.168.40.94:7101/api-docs" + }, + "vipAddress":"[]", + "isCoordinatingDiscoveryServer":false, + "lastUpdatedTimestamp":1402450388774, + "lastDirtyTimestamp":1402450388774, + "actionType":"MODIFIED" + } + } + ] + } +} diff --git a/tests/marshal_test.go b/tests/marshal_test.go index 1619459..85d6368 100644 --- a/tests/marshal_test.go +++ b/tests/marshal_test.go @@ -3,6 +3,7 @@ package fargo_test // MIT Licensed (see README.md) - Copyright (c) 2013 Hudl <@Hudl> import ( + "bytes" "encoding/json" "encoding/xml" "fmt" @@ -58,7 +59,7 @@ func portsEqual(actual, expected *fargo.Instance) { } func jsonEncodedInstanceHasPortsEqualTo(b []byte, expected *fargo.Instance) { - Convey("And reading them back should yield the equivalent value", func() { + Convey("Reading them back should yield the equivalent value", func() { var decoded fargo.Instance err := json.Unmarshal(b, &decoded) So(err, ShouldBeNil) @@ -92,6 +93,10 @@ func TestPortsMarshal(t *testing.T) { So(s, ShouldContainSubstring, `,"securePort":{"$":"0","@enabled":"false"}`) jsonEncodedInstanceHasPortsEqualTo(b, &ins) + + Convey("When the Eureka server is version 1.22 or later", func() { + jsonEncodedInstanceHasPortsEqualTo(bytes.Replace(b, []byte(`"80"`), []byte("80"), -1), &ins) + }) }) }) @@ -124,6 +129,10 @@ func TestPortsMarshal(t *testing.T) { So(s, ShouldContainSubstring, `,"securePort":{"$":"443","@enabled":"true"}`) jsonEncodedInstanceHasPortsEqualTo(b, &ins) + + Convey("When the Eureka server is version 1.22 or later", func() { + jsonEncodedInstanceHasPortsEqualTo(bytes.Replace(b, []byte(`"443"`), []byte("443"), -1), &ins) + }) }) }) @@ -158,6 +167,12 @@ func TestPortsMarshal(t *testing.T) { So(s, ShouldContainSubstring, `,"securePort":{"$":"443","@enabled":"true"}`) jsonEncodedInstanceHasPortsEqualTo(b, &ins) + + Convey("When the Eureka server is version 1.22 or later", func() { + b = bytes.Replace(b, []byte(`"80"`), []byte("80"), -1) + b = bytes.Replace(b, []byte(`"443"`), []byte("443"), -1) + jsonEncodedInstanceHasPortsEqualTo(b, &ins) + }) }) }) diff --git a/tests/net_test.go b/tests/net_test.go index 8eaf15c..bb24c56 100644 --- a/tests/net_test.go +++ b/tests/net_test.go @@ -218,6 +218,7 @@ func TestRegistration(t *testing.T) { i := fargo.Instance{ HostName: "i-123456", Port: 9090, + PortEnabled: true, App: "TESTAPP", IPAddr: "127.0.0.10", VipAddress: "127.0.0.10", @@ -231,6 +232,7 @@ func TestRegistration(t *testing.T) { j := fargo.Instance{ HostName: "i-6543", Port: 9090, + PortEnabled: true, App: "TESTAPP", IPAddr: "127.0.0.10", VipAddress: "127.0.0.10", @@ -264,6 +266,7 @@ func TestReregistration(t *testing.T) { i := fargo.Instance{ HostName: "i-123456", Port: 9090, + PortEnabled: true, App: "TESTAPP", IPAddr: "127.0.0.10", VipAddress: "127.0.0.10", @@ -305,6 +308,7 @@ func DontTestDeregistration(t *testing.T) { i := fargo.Instance{ HostName: "i-123456", Port: 9090, + PortEnabled: true, App: "TESTAPP", IPAddr: "127.0.0.10", VipAddress: "127.0.0.10",