forked from microsoft/azure-devops-go-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding fix for GitHub issue 59 (microsoft#59)
- Loading branch information
Nick Iodice
committed
May 16, 2020
1 parent
2e6b76e
commit d70a5f2
Showing
4 changed files
with
261 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package azuredevops | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"strings" | ||
"time" | ||
) | ||
|
||
type Time struct { | ||
Time time.Time | ||
} | ||
|
||
func tryUnmarshalJSON(b []byte) (time.Time, error) { | ||
t := time.Time{} | ||
err := json.Unmarshal(b, &t) | ||
|
||
// ignore errors for 0001-01-01T00:00:00 dates. The Azure DevOps service | ||
// returns default dates in a format that is invalid for a time.Time. The | ||
// correct value would have a 'z' at the end to represent utc. We are going | ||
// to ignore this error, and set the value to the default time.Time value. | ||
// https://github.com/microsoft/azure-devops-go-api/issues/17 | ||
if err != nil { | ||
if parseError, ok := err.(*time.ParseError); ok && parseError.Value == "\"0001-01-01T00:00:00\"" { | ||
err = nil | ||
} | ||
} | ||
|
||
return t, err | ||
} | ||
|
||
func makeTimeParseFunction(layout string) func(b []byte) (time.Time, error) { | ||
return func(b []byte) (time.Time, error) { | ||
s := strings.Trim(string(b), "\"") | ||
if s == "null" { | ||
return time.Time{}, nil | ||
} | ||
return time.Parse(layout, s) | ||
} | ||
} | ||
|
||
// UnmarshalJSON attempts a variety of time marshalling strategies. The first one that works | ||
// will be used. If none succeede, then an error is returned. | ||
func (t *Time) UnmarshalJSON(b []byte) error { | ||
|
||
// The dates returned by the Azure DevOps services do not conform to any predictable | ||
// format or standard. The strategy list here tries to account for as many permutations | ||
// as possible by applying the following strategies: | ||
// | ||
// (1) Default Go time deserialization | ||
// (2) Attempt each of the time formats defined as constants in the time library. | ||
// These are documented here: https://golang.org/pkg/time/#pkg-constants | ||
// (3) Special case times found to be returned by the Azure DevOps services | ||
// | ||
// The first "working" strategy is used. | ||
strategies := []func([]byte) (time.Time, error){ | ||
tryUnmarshalJSON, | ||
makeTimeParseFunction(time.ANSIC), | ||
makeTimeParseFunction(time.UnixDate), | ||
makeTimeParseFunction(time.RubyDate), | ||
makeTimeParseFunction(time.RFC822), | ||
makeTimeParseFunction(time.RFC822Z), | ||
makeTimeParseFunction(time.RFC850), | ||
makeTimeParseFunction(time.RFC1123), | ||
makeTimeParseFunction(time.RFC1123Z), | ||
makeTimeParseFunction(time.RFC3339), | ||
makeTimeParseFunction(time.RFC3339Nano), | ||
makeTimeParseFunction(time.Stamp), | ||
makeTimeParseFunction(time.StampMilli), | ||
makeTimeParseFunction(time.StampMicro), | ||
makeTimeParseFunction(time.StampNano), | ||
makeTimeParseFunction("2006-01-02T15:04:05.9999999"), // https://github.com/microsoft/azure-devops-go-api/issues/59 | ||
} | ||
|
||
for _, strategy := range strategies { | ||
parsedTime, err := strategy(b) | ||
if err == nil { | ||
t.Time = parsedTime | ||
return nil | ||
} | ||
} | ||
|
||
return fmt.Errorf("Unable to deserialize time (%s). %d strategies were attempted", string(b), len(strategies)) | ||
} | ||
|
||
func (t *Time) MarshalJSON() ([]byte, error) { | ||
return json.Marshal(t.Time) | ||
} | ||
|
||
func (t Time) String() string { | ||
return t.Time.String() | ||
} | ||
|
||
func (t Time) Equal(u Time) bool { | ||
return t.Time.Equal(u.Time) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package azuredevops | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
"github.com/google/uuid" | ||
) | ||
|
||
type TimeTestStruct struct { | ||
T Time | ||
} | ||
|
||
func getJSONBytes(timeStr string) []byte { | ||
return []byte(fmt.Sprintf("{ \"T\": \"%s\" }", timeStr)) | ||
} | ||
|
||
func TestTime_MarshallingRoundTripForSupportedCases(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
timeStr string | ||
}{ | ||
{ | ||
"ANSIC", | ||
"Mon Jan 2 15:04:05 2006", | ||
}, { | ||
"UnixDate", | ||
"Mon Jan 2 15:04:05 MST 2006", | ||
}, { | ||
"RubyDate", | ||
"Mon Jan 02 15:04:05 -0700 2006", | ||
}, { | ||
"RFC822", | ||
"02 Jan 06 15:04 MST", | ||
}, { | ||
"RFC822Z", | ||
"02 Jan 06 15:04 -0700", | ||
}, { | ||
"RFC850", | ||
"Monday, 02-Jan-06 15:04:05 MST", | ||
}, { | ||
"RFC1123", | ||
"Mon, 02 Jan 2006 15:04:05 MST", | ||
}, { | ||
"RFC1123Z", | ||
"Mon, 02 Jan 2006 15:04:05 -0700", | ||
}, { | ||
"RFC3339_1", | ||
"2006-01-02T15:04:05Z", | ||
}, { | ||
"RFC3339_2", | ||
"2006-01-02T15:04:05+07:00", | ||
}, { | ||
"RFC3339Nano_1", | ||
"2006-01-02T15:04:05.999999999Z", | ||
}, { | ||
"RFC3339Nano_2", | ||
"2006-01-02T15:04:05.999999999+07:00", | ||
}, { | ||
"Stamp", | ||
"Jan 2 15:04:05", | ||
}, { | ||
"StampMilli", | ||
"Jan 2 15:04:05.000", | ||
}, { | ||
"StampMicro", | ||
"Jan 2 15:04:05.000000", | ||
}, { | ||
"StampNano", | ||
"Jan 2 15:04:05.000000000", | ||
}, { | ||
"GitHubIssue59", | ||
"2006-01-02T15:04:05.9999999", | ||
}, | ||
} | ||
|
||
for _, testCase := range testCases { | ||
expectedTime, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05+00:00") | ||
|
||
t.Run(testCase.name, func(t *testing.T) { | ||
s := TimeTestStruct{} | ||
err := json.Unmarshal(getJSONBytes(testCase.timeStr), &s) | ||
if err != nil { | ||
t.Errorf("Unexpectedly could not unmarshal %s into a valid time struct: %+v", testCase.timeStr, err) | ||
} | ||
|
||
// Not all timestamps have the same year and timezone information. So we can pick a subset of properties to test | ||
if expectedTime.Day() != s.T.Time.Day() { | ||
t.Errorf("Expected day %+v but got %+v", expectedTime.Day(), s.T.Time.Day()) | ||
} | ||
|
||
if expectedTime.Hour() != s.T.Time.Hour() { | ||
t.Errorf("Expected hour %+v but got %+v", expectedTime.Hour(), s.T.Time.Hour()) | ||
} | ||
|
||
if expectedTime.Minute() != s.T.Time.Minute() { | ||
t.Errorf("Expected minute %+v but got %+v", expectedTime.Minute(), s.T.Time.Minute()) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestModels_Unmarshal_Time(t *testing.T) { | ||
text := []byte("{\"id\":\"d221ad31-3a7b-52c0-b71d-b255b1ff63ba\",\"time1\":\"0001-01-01T00:00:00\",\"time2\":\"2019-09-01T00:07:26Z\",\"int\":10,\"string\":\"test string\"}") | ||
testModel := TestModel{} | ||
|
||
testModel.Time1 = &Time{} | ||
testModel.Time1.Time = time.Now() // this ensures we test the value is set back to default when issue #17 is hit. | ||
|
||
err := json.Unmarshal(text, &testModel) | ||
if err != nil { | ||
t.Errorf("Error occurred during deserialization: %v", err) | ||
} | ||
if (testModel.Time1.Time != time.Time{}) { | ||
t.Errorf("Expecting deserialized time to equal default time. Actual time: %v", testModel.Time1) | ||
} | ||
|
||
parsedTime, err := time.Parse(time.RFC3339, "2019-09-01T00:07:26Z") | ||
if err != nil { | ||
t.Errorf(err.Error()) | ||
} | ||
if testModel.Time2.Time != parsedTime { | ||
t.Errorf("Expected time: %v Actual time: %v", parsedTime, testModel.Time1.Time) | ||
} | ||
} | ||
|
||
func TestModels_Marshal_Unmarshal_Time(t *testing.T) { | ||
testModel1 := TestModel{} | ||
testModel1.Time1 = &Time{} | ||
testModel1.Time1.Time = time.Now() | ||
b, err := json.Marshal(testModel1) | ||
if err != nil { | ||
t.Errorf(err.Error()) | ||
} | ||
|
||
testModel2 := TestModel{} | ||
err = json.Unmarshal(b, &testModel2) | ||
if err != nil { | ||
t.Errorf(err.Error()) | ||
} | ||
|
||
if testModel1.Time1 != testModel1.Time1 { | ||
t.Errorf("Expected time: %v Actual time: %v", testModel1.Time1, testModel1.Time2) | ||
} | ||
|
||
if testModel1.Time1.Time != testModel1.Time1.Time { | ||
t.Errorf("Expected time: %v Actual time: %v", testModel1.Time1.Time, testModel1.Time2.Time) | ||
} | ||
} | ||
|
||
type TestModel struct { | ||
Id *uuid.UUID `json:"id,omitempty"` | ||
Time1 *Time `json:"time1,omitempty"` | ||
Time2 *Time `json:"time2,omitempty"` | ||
Int *uint64 `json:"int,omitempty"` | ||
String *string `json:"string,omitempty"` | ||
} |