From ecf830777a2001009314dc3a82250d85c4734c4a Mon Sep 17 00:00:00 2001 From: John Beisley Date: Sun, 27 Oct 2013 19:08:31 +0000 Subject: [PATCH] Add marshalling for dateTime.tz type. --- soap/types.go | 265 ++++++++++++++++++++++++++++++++++++--------- soap/types_test.go | 57 ++++++++-- 2 files changed, 259 insertions(+), 63 deletions(-) diff --git a/soap/types.go b/soap/types.go index ad28422..dbc6650 100644 --- a/soap/types.go +++ b/soap/types.go @@ -57,6 +57,133 @@ func UnmarshalChar(s string) (rune, error) { return r, nil } +func parseInt(s string, err *error) int { + v, parseErr := strconv.ParseInt(s, 10, 64) + if parseErr != nil { + *err = parseErr + } + return int(v) +} + +var dateRegexps = []*regexp.Regexp{ + // yyyy[-mm[-dd]] + regexp.MustCompile(`^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$`), + // yyyy[mm[dd]] + regexp.MustCompile(`^(\d{4})(?:(\d{2})(?:(\d{2}))?)?$`), +} + +func parseDateParts(s string) (year, month, day int, err error) { + var parts []string + for _, re := range dateRegexps { + parts = re.FindStringSubmatch(s) + if parts != nil { + break + } + } + if parts == nil { + err = fmt.Errorf("soap date: value %q is not in a recognized ISO8601 date format", s) + return + } + + year = parseInt(parts[1], &err) + month = 1 + day = 1 + if len(parts[2]) != 0 { + month = parseInt(parts[2], &err) + if len(parts[3]) != 0 { + day = parseInt(parts[3], &err) + } + } + + if err != nil { + err = fmt.Errorf("soap date: %q: %v", s, err) + } + + return +} + +var timeRegexps = []*regexp.Regexp{ + // hh[:mm[:ss]] + regexp.MustCompile(`^(\d{2})(?::(\d{2})(?::(\d{2}))?)?$`), + // hh[mm[ss]] + regexp.MustCompile(`^(\d{2})(?:(\d{2})(?:(\d{2}))?)?$`), +} + +func parseTimeParts(s string) (hour, minute, second int, err error) { + var parts []string + for _, re := range timeRegexps { + parts = re.FindStringSubmatch(s) + if parts != nil { + break + } + } + if parts == nil { + err = fmt.Errorf("soap time: value %q is not in ISO8601 time format", s) + return + } + + hour = parseInt(parts[1], &err) + if len(parts[2]) != 0 { + minute = parseInt(parts[2], &err) + if len(parts[3]) != 0 { + second = parseInt(parts[3], &err) + } + } + + if err != nil { + err = fmt.Errorf("soap time: %q: %v", s, err) + } + + return +} + +// (+|-)hh[[:]mm] +var timezoneRegexp = regexp.MustCompile(`^([+-])(\d{2})(?::?(\d{2}))?$`) + +func parseTimezone(s string) (offset int, err error) { + if s == "Z" { + return 0, nil + } + parts := timezoneRegexp.FindStringSubmatch(s) + if parts == nil { + err = fmt.Errorf("soap timezone: value %q is not in ISO8601 timezone format", s) + return + } + + offset = parseInt(parts[2], &err) * 3600 + if len(parts[3]) != 0 { + offset += parseInt(parts[3], &err) * 60 + } + if parts[1] == "-" { + offset = -offset + } + + if err != nil { + err = fmt.Errorf("soap timezone: %q: %v", s, err) + } + + return +} + +var completeDateTimeZoneRegexp = regexp.MustCompile(`^([^T]+)(?:T([^-+Z]+)(.+)?)?$`) + +// splitCompleteDateTimeZone splits date, time and timezone apart from an +// ISO8601 string. It does not ensure that the contents of each part are +// correct, it merely splits on certain delimiters. +// e.g "2010-09-08T12:15:10+0700" => "2010-09-08", "12:15:10", "+0700". +// Timezone can only be present if time is also present. +func splitCompleteDateTimeZone(s string) (dateStr, timeStr, zoneStr string, err error) { + parts := completeDateTimeZoneRegexp.FindStringSubmatch(s) + if parts == nil { + err = fmt.Errorf("soap date/time/zone: value %q is not in ISO8601 datetime format", s) + return + } + dateStr = parts[1] + timeStr = parts[2] + zoneStr = parts[3] + return +} + // MarshalDate marshals time.Time to SOAP "date" type. Note that this converts // to local time, and discards the time-of-day components. func MarshalDate(v time.Time) (string, error) { @@ -68,12 +195,11 @@ var dateFmts = []string{"2006-01-02", "20060102"} // UnmarshalDate unmarshals time.Time from SOAP "date" type. This outputs the // date as midnight in the local time zone. func UnmarshalDate(s string) (time.Time, error) { - for _, f := range dateFmts { - if t, err := time.ParseInLocation(f, s, localLoc); err == nil { - return t, nil - } + year, month, day, err := parseDateParts(s) + if err != nil { + return time.Time{}, err } - return time.Time{}, fmt.Errorf("soap date: value %q is not in a recognized date format", s) + return time.Date(year, time.Month(month), day, 0, 0, 0, 0, localLoc), nil } // TimeOfDay is used in cases where SOAP "time" or "time.tz" is used. @@ -88,7 +214,7 @@ type TimeOfDay struct { // Offset is non-zero only if time.tz is used. It is otherwise ignored. If // non-zero, then it is regarded as a UTC offset in seconds. Note that the // sub-minutes is ignored by the marshal function. - Offset int16 + Offset int } // MarshalTimeOfDay marshals TimeOfDay to the "time" type. @@ -139,24 +265,27 @@ func MarshalTimeOfDayTz(v TimeOfDay) (string, error) { return fmt.Sprintf("%02d:%02d:%02d%s", hour, minute, second, tz), nil } -var timeRegexp = regexp.MustCompile( - `^(\d\d)(?::?(\d\d)(?::?(\d\d))?)?` + // hh[:mm[:ss]] - `(?:(Z)|([+-])(\d\d)(?::?(\d\d))?)?$`) // Z | ±hh[:mm] - // UnmarshalTimeOfDayTz unmarshals TimeOfDay from the "time.tz" type. -func UnmarshalTimeOfDayTz(s string) (TimeOfDay, error) { - parts := timeRegexp.FindStringSubmatch(s) - if parts == nil { - return TimeOfDay{}, fmt.Errorf("soap time.tz: value %q is not in ISO8601 time format", s) +func UnmarshalTimeOfDayTz(s string) (tod TimeOfDay, err error) { + zoneIndex := strings.IndexAny(s, "Z+-") + var timePart string + var hasOffset bool + var offset int + if zoneIndex == -1 { + hasOffset = false + timePart = s + } else { + hasOffset = true + timePart = s[:zoneIndex] + if offset, err = parseTimezone(s[zoneIndex:]); err != nil { + return + } } - // HH:MM:SS parsing. - parts = parts[1:] - var iParts [3]int64 - for i, pStr := range parts[:3] { - iParts[i], _ = strconv.ParseInt(pStr, 10, 64) + hour, minute, second, err := parseTimeParts(timePart) + if err != nil { + return } - hour, minute, second := iParts[0], iParts[1], iParts[2] fromMidnight := time.Duration(hour*3600+minute*60+second) * time.Second @@ -166,51 +295,85 @@ func UnmarshalTimeOfDayTz(s string) (TimeOfDay, error) { return TimeOfDay{}, fmt.Errorf("soap time.tz: value %q has value(s) out of range", s) } - // Timezone offset parsing. - hasOffset := false - var offset int64 - if parts[3] == "Z" { - hasOffset = true - offset = 0 - } else if parts[4] != "" { - hasOffset = true - hours, _ := strconv.ParseInt(parts[5], 10, 64) - var mins int64 - if parts[6] != "" { - mins, _ = strconv.ParseInt(parts[6], 10, 64) - } - offset = hours*3600 + mins*60 - if parts[4] == "-" { - offset = -offset - } - } return TimeOfDay{ FromMidnight: time.Duration(hour*3600+minute*60+second) * time.Second, HasOffset: hasOffset, - Offset: int16(offset), + Offset: offset, }, nil } -// MarshalDatetime marshals time.Time to SOAP "date" type. Note that this +// MarshalDateTime marshals time.Time to SOAP "dateTime" type. Note that this // converts to local time. -func MarshalDatetime(v time.Time) (string, error) { +func MarshalDateTime(v time.Time) (string, error) { return v.In(localLoc).Format("2006-01-02T15:04:05"), nil } -// UnmarshalDatetime unmarshals time.Time from the SOAP "dateTime" type. This +// UnmarshalDateTime unmarshals time.Time from the SOAP "dateTime" type. This // returns a value in the local timezone. -func UnmarshalDatetime(s string) (time.Time, error) { - parts := strings.SplitN(s, "T", 2) - datePart, err := UnmarshalDate(parts[0]) +func UnmarshalDateTime(s string) (result time.Time, err error) { + dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s) if err != nil { - return time.Time{}, err + return } - if len(parts) == 2 { - timePart, err := UnmarshalTimeOfDay(parts[1]) + + if len(zoneStr) != 0 { + err = fmt.Errorf("soap datetime: unexpected timezone in %q", s) + return + } + + year, month, day, err := parseDateParts(dateStr) + if err != nil { + return + } + + var hour, minute, second int + if len(timeStr) != 0 { + hour, minute, second, err = parseTimeParts(timeStr) if err != nil { - return time.Time{}, err + return } - datePart = datePart.Add(timePart.FromMidnight) } - return datePart, nil + + result = time.Date(year, time.Month(month), day, hour, minute, second, 0, localLoc) + return +} + +// MarshalDateTimeTz marshals time.Time to SOAP "dateTime.tz" type. +func MarshalDateTimeTz(v time.Time) (string, error) { + return v.Format("2006-01-02T15:04:05-07:00"), nil +} + +// UnmarshalDateTimeTz unmarshals time.Time from the SOAP "dateTime.tz" type. +// This returns a value in the local timezone when the timezone is unspecified. +func UnmarshalDateTimeTz(s string) (result time.Time, err error) { + dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s) + if err != nil { + return + } + + year, month, day, err := parseDateParts(dateStr) + if err != nil { + return + } + + var hour, minute, second int + var location *time.Location = localLoc + if len(timeStr) != 0 { + hour, minute, second, err = parseTimeParts(timeStr) + if err != nil { + return + } + if len(zoneStr) != 0 { + var offset int + offset, err = parseTimezone(zoneStr) + if offset == 0 { + location = time.UTC + } else { + location = time.FixedZone("", offset) + } + } + } + + result = time.Date(year, time.Month(month), day, hour, minute, second, 0, location) + return } diff --git a/soap/types_test.go b/soap/types_test.go index 50c927d..54ff08a 100644 --- a/soap/types_test.go +++ b/soap/types_test.go @@ -65,7 +65,7 @@ func (v DateTest) Equal(result interface{}) bool { } func (v DateTest) Dupe(tag string) convTest { if tag != "no:dateTime" { - return DatetimeTest{v.Time} + return DateTimeTest{v.Time} } return nil } @@ -104,15 +104,33 @@ func (v TimeOfDayTzTest) Equal(result interface{}) bool { return v.TimeOfDay == result.(TimeOfDay) } -type DatetimeTest struct{ time.Time } +type DateTimeTest struct{ time.Time } -func (v DatetimeTest) Marshal() (string, error) { - return MarshalDatetime(time.Time(v.Time)) +func (v DateTimeTest) Marshal() (string, error) { + return MarshalDateTime(time.Time(v.Time)) } -func (v DatetimeTest) Unmarshal(s string) (interface{}, error) { - return UnmarshalDatetime(s) +func (v DateTimeTest) Unmarshal(s string) (interface{}, error) { + return UnmarshalDateTime(s) } -func (v DatetimeTest) Equal(result interface{}) bool { +func (v DateTimeTest) Equal(result interface{}) bool { + return v.Time.Equal(result.(time.Time)) +} +func (v DateTimeTest) Dupe(tag string) convTest { + if tag != "no:dateTime.tz" { + return DateTimeTzTest{v.Time} + } + return nil +} + +type DateTimeTzTest struct{ time.Time } + +func (v DateTimeTzTest) Marshal() (string, error) { + return MarshalDateTimeTz(time.Time(v.Time)) +} +func (v DateTimeTzTest) Unmarshal(s string) (interface{}, error) { + return UnmarshalDateTimeTz(s) +} +func (v DateTimeTzTest) Equal(result interface{}) bool { return v.Time.Equal(result.(time.Time)) } @@ -195,11 +213,26 @@ func Test(t *testing.T) { {str: "01:02:03-01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}}, {str: "01:02:03-0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}, noMarshal: true}, - // datetime - {str: "2013-10-08T00:00:00", value: DatetimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}}, - {str: "20131008", value: DatetimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true}, - {str: "2013-10-08T10:30:50", value: DatetimeTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}}, - {str: "2013-10-08T10:30:50T", value: DatetimeTest{}, wantUnmarshalErr: true, noMarshal: true}, + // dateTime + {str: "2013-10-08T00:00:00", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, tag: "no:dateTime.tz"}, + {str: "20131008", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true}, + {str: "2013-10-08T10:30:50", value: DateTimeTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, tag: "no:dateTime.tz"}, + {str: "2013-10-08T10:30:50T", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true}, + {str: "2013-10-08T10:30:50+01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"}, + {str: "2013-10-08T10:30:50+01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"}, + {str: "2013-10-08T10:30:50+0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"}, + {str: "2013-10-08T10:30:50-01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"}, + {str: "2013-10-08T10:30:50-01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"}, + {str: "2013-10-08T10:30:50-0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"}, + + // dateTime.tz + {str: "2013-10-08T10:30:50", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, noMarshal: true}, + {str: "2013-10-08T10:30:50+01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:00", 3600))}, noMarshal: true}, + {str: "2013-10-08T10:30:50+01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}}, + {str: "2013-10-08T10:30:50+0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}, noMarshal: true}, + {str: "2013-10-08T10:30:50-01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:00", -3600))}, noMarshal: true}, + {str: "2013-10-08T10:30:50-01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}}, + {str: "2013-10-08T10:30:50-0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}, noMarshal: true}, } // Generate extra test cases from convTests that implement duper.