diff --git a/v2/soap/types/types.go b/v2/soap/types/types.go index 853dbc8..974cadb 100644 --- a/v2/soap/types/types.go +++ b/v2/soap/types/types.go @@ -1,14 +1,7 @@ // Package types defines types that encode values in SOAP requests and responses. -// -// Based on https://openconnectivity.org/upnp-specs/UPnP-arch-DeviceArchitecture-v2.0-20200417.pdf -// pages 58-60. -// -// Date/time formats are based on http://www.w3.org/TR/1998/NOTE-datetime-19980827 package types import ( - "bytes" - "encoding" "encoding/base64" "encoding/hex" "errors" @@ -16,13 +9,21 @@ import ( "net/url" "regexp" "strconv" + "strings" "time" "unicode/utf8" ) +var ( + // localLoc acts like time.Local for this package, but is faked out by the + // unit tests to ensure that things stay constant (especially when running + // this test in a place where local time is UTC which might mask bugs). + localLoc = time.Local +) + type SOAPValue interface { - encoding.TextMarshaler - encoding.TextUnmarshaler + Marshal() (string, error) + Unmarshal(s string) error } type UI1 uint8 @@ -38,12 +39,12 @@ func (v *UI1) String() string { return strconv.FormatUint(uint64(*v), 10) } -func (v *UI1) MarshalText() ([]byte, error) { - return strconv.AppendUint(nil, uint64(*v), 10), nil +func (v *UI1) Marshal() (string, error) { + return v.String(), nil } -func (v *UI1) UnmarshalText(b []byte) error { - v2, err := strconv.ParseUint(string(b), 10, 8) +func (v *UI1) Unmarshal(s string) error { + v2, err := strconv.ParseUint(s, 10, 8) *v = UI1(v2) return err } @@ -61,12 +62,12 @@ func (v *UI2) String() string { return strconv.FormatUint(uint64(*v), 10) } -func (v *UI2) MarshalText() ([]byte, error) { - return strconv.AppendUint(nil, uint64(*v), 10), nil +func (v *UI2) Marshal() (string, error) { + return v.String(), nil } -func (v *UI2) UnmarshalText(b []byte) error { - v2, err := strconv.ParseUint(string(b), 10, 16) +func (v *UI2) Unmarshal(s string) error { + v2, err := strconv.ParseUint(s, 10, 16) *v = UI2(v2) return err } @@ -84,12 +85,12 @@ func (v *UI4) String() string { return strconv.FormatUint(uint64(*v), 10) } -func (v *UI4) MarshalText() ([]byte, error) { - return strconv.AppendUint(nil, uint64(*v), 10), nil +func (v *UI4) Marshal() (string, error) { + return v.String(), nil } -func (v *UI4) UnmarshalText(b []byte) error { - v2, err := strconv.ParseUint(string(b), 10, 32) +func (v *UI4) Unmarshal(s string) error { + v2, err := strconv.ParseUint(s, 10, 32) *v = UI4(v2) return err } @@ -107,12 +108,12 @@ func (v *UI8) String() string { return strconv.FormatUint(uint64(*v), 10) } -func (v *UI8) MarshalText() ([]byte, error) { - return strconv.AppendUint(nil, uint64(*v), 10), nil +func (v *UI8) Marshal() (string, error) { + return v.String(), nil } -func (v *UI8) UnmarshalText(b []byte) error { - v2, err := strconv.ParseUint(string(b), 10, 64) +func (v *UI8) Unmarshal(s string) error { + v2, err := strconv.ParseUint(s, 10, 64) *v = UI8(v2) return err } @@ -130,12 +131,12 @@ func (v *I1) String() string { return strconv.FormatInt(int64(*v), 10) } -func (v *I1) MarshalText() ([]byte, error) { - return strconv.AppendInt(nil, int64(*v), 10), nil +func (v *I1) Marshal() (string, error) { + return v.String(), nil } -func (v *I1) UnmarshalText(b []byte) error { - v2, err := strconv.ParseInt(string(b), 10, 8) +func (v *I1) Unmarshal(s string) error { + v2, err := strconv.ParseInt(s, 10, 8) *v = I1(v2) return err } @@ -153,12 +154,12 @@ func (v *I2) String() string { return strconv.FormatInt(int64(*v), 10) } -func (v *I2) MarshalText() ([]byte, error) { - return strconv.AppendInt(nil, int64(*v), 10), nil +func (v *I2) Marshal() (string, error) { + return v.String(), nil } -func (v *I2) UnmarshalText(b []byte) error { - v2, err := strconv.ParseInt(string(b), 10, 16) +func (v *I2) Unmarshal(s string) error { + v2, err := strconv.ParseInt(s, 10, 16) *v = I2(v2) return err } @@ -176,12 +177,12 @@ func (v *I4) String() string { return strconv.FormatInt(int64(*v), 10) } -func (v *I4) MarshalText() ([]byte, error) { - return strconv.AppendInt(nil, int64(*v), 10), nil +func (v *I4) Marshal() (string, error) { + return v.String(), nil } -func (v *I4) UnmarshalText(b []byte) error { - v2, err := strconv.ParseInt(string(b), 10, 32) +func (v *I4) Unmarshal(s string) error { + v2, err := strconv.ParseInt(s, 10, 32) *v = I4(v2) return err } @@ -199,12 +200,12 @@ func (v *I8) String() string { return strconv.FormatInt(int64(*v), 10) } -func (v *I8) MarshalText() ([]byte, error) { - return strconv.AppendInt(nil, int64(*v), 10), nil +func (v *I8) Marshal() (string, error) { + return v.String(), nil } -func (v *I8) UnmarshalText(b []byte) error { - v2, err := strconv.ParseInt(string(b), 10, 64) +func (v *I8) Unmarshal(s string) error { + v2, err := strconv.ParseInt(s, 10, 64) *v = I8(v2) return err } @@ -219,19 +220,15 @@ func NewR4(v float32) *R4 { } func (v *R4) String() string { - return string(v.marshalText(nil)) + return strconv.FormatFloat(float64(*v), 'G', -1, 32) } -func (v *R4) marshalText(b []byte) []byte { - return strconv.AppendFloat(b, float64(*v), 'g', -1, 32) +func (v *R4) Marshal() (string, error) { + return v.String(), nil } -func (v *R4) MarshalText() ([]byte, error) { - return v.marshalText(nil), nil -} - -func (v *R4) UnmarshalText(b []byte) error { - v2, err := strconv.ParseFloat(string(b), 32) +func (v *R4) Unmarshal(s string) error { + v2, err := strconv.ParseFloat(s, 32) *v = R4(v2) return err } @@ -246,182 +243,49 @@ func NewR8(v float64) *R8 { } func (v *R8) String() string { - return string(v.marshalText(nil)) + return strconv.FormatFloat(float64(*v), 'G', -1, 64) } -func (v *R8) marshalText(b []byte) []byte { - return strconv.AppendFloat(nil, float64(*v), 'g', -1, 64) +func (v *R8) Marshal() (string, error) { + return v.String(), nil } -func (v *R8) MarshalText() ([]byte, error) { - return v.marshalText(nil), nil -} - -func (v *R8) UnmarshalText(b []byte) error { - v2, err := strconv.ParseFloat(string(b), 64) +func (v *R8) Unmarshal(s string) error { + v2, err := strconv.ParseFloat(s, 64) *v = R8(v2) return err } -const Fixed14_4Denominator = 1e4 -const Fixed14_4MaxInteger = 1e14 - 1 -const Fixed14_4MaxFractional = 1e18 - 1 -const Fixed14_4MaxFrac = Fixed14_4Denominator - 1 +// FloatFixed14_4 maps a float64 to the SOAP "fixed.14.4" type. +type FloatFixed14_4 float64 -// Fixed14_4 represents a fixed point number with up to 14 decimal digits -// before the decimal point (integer part), and up to 4 decimal digits -// after the decimal point (fractional part). -// -// Corresponds to the SOAP "fixed.14.4" type. -// -// This is a struct to avoid accidentally using the value directly as an -// integer. -type Fixed14_4 struct { - // Fractional divided by 1e4 is the fixed point value. Take care setting - // this directly, it should only contain values in the range (-1e18, 1e18). - Fractional int64 +var _ SOAPValue = new(FloatFixed14_4) + +func NewFloatFixed14_4(v float64) *FloatFixed14_4 { + v2 := FloatFixed14_4(v) + return &v2 } -var _ SOAPValue = &Fixed14_4{} - -// Fixed14_4FromParts creates a Fixed14_4 from components. -// Bounds: -// * Both intPart and fracPart must have the same sign. -// * -1e14 < intPart < 1e14 -// * -1e4 < fracPart < 1e4 -func Fixed14_4FromParts(intPart int64, fracPart int16) (Fixed14_4, error) { - var v Fixed14_4 - err := v.SetParts(intPart, fracPart) - return v, err +func (v *FloatFixed14_4) String() string { + return strconv.FormatFloat(float64(*v), 'f', 4, 64) } -// SetFromParts sets the value based on the integer component and the fractional component. -// Bounds: -// * Both intPart and fracPart must have the same sign. -// * -1e14 < intPart < 1e14 -// * -1e4 < fracPart < 1e4 -func (v *Fixed14_4) SetParts(intPart int64, fracPart int16) error { - if (intPart < 0) != (fracPart < 0) { - return fmt.Errorf("want intPart and fracPart with same sign, got %d and %d", - intPart, fracPart) +func (v *FloatFixed14_4) Marshal() (string, error) { + if *v >= 1e14 || *v <= -1e14 { + return "", fmt.Errorf("soap fixed14.4: value %v out of bounds", v) } - if intPart < -Fixed14_4MaxInteger || intPart > Fixed14_4MaxInteger { - return fmt.Errorf("want intPart in range (-1e14,1e14), got %d", intPart) - } - if fracPart < -Fixed14_4MaxFrac || fracPart > Fixed14_4MaxFrac { - return fmt.Errorf("want fracPart in range (-1e4,1e4), got %d", fracPart) - } - v.Fractional = intPart*Fixed14_4Denominator + int64(fracPart) - return nil + return v.String(), nil } -// Returns the integer part and fractional part of the fixed point number. -func (v Fixed14_4) Parts() (int64, int16) { - return v.Fractional / Fixed14_4Denominator, int16(v.Fractional % Fixed14_4Denominator) -} - -// Fixed14_4FromFractional creates a Fixed14_4 from an integer, where the -// parameter divided by 1e4 is the fixed point value. -func Fixed14_4FromFractional(fracValue int64) (Fixed14_4, error) { - var v Fixed14_4 - err := v.SetFractional(fracValue) - return v, err -} - -// SetFromFractional sets the value of the fixed point number, where fracValue -// divided by 1e4 is the fixed point value. Unlike setting v.Fractional -// directly, this checks the value. -func (v *Fixed14_4) SetFractional(fracValue int64) error { - if fracValue < -Fixed14_4MaxFractional || fracValue > Fixed14_4MaxFractional { - return fmt.Errorf("want intPart in range (-1e18,1e18), got %d", fracValue) - } - v.Fractional = fracValue - return nil -} - -// Fixed14_4FromFloat creates a Fixed14_4 from a float64. Returns error if the -// float is outside the range. -func Fixed14_4FromFloat(f float64) (Fixed14_4, error) { - i := int64(f * Fixed14_4Denominator) - return Fixed14_4FromFractional(i) -} - -// SetFloat64 sets the value of the fixed point number from a float64. Returns -// error if the float is outside the range. -func (v *Fixed14_4) SetFloat64(f float64) error { - i := int64(f * Fixed14_4Denominator) - return v.SetFractional(i) -} - -func (v Fixed14_4) Float64() float64 { - return float64(v.Fractional) / Fixed14_4Denominator -} - -func (v *Fixed14_4) String() string { - return string(v.marshalText(nil)) -} - -func (v *Fixed14_4) marshalText(b []byte) []byte { - intPart, fracPart := v.Parts() - if fracPart < 0 { - fracPart = -fracPart - } - b = strconv.AppendInt(b, intPart, 10) - b = append(b, '.') - b = appendInt(b, int64(fracPart), 4) - return b -} - -func (v *Fixed14_4) MarshalText() ([]byte, error) { - return v.marshalText(nil), nil -} - -var decimalByte = []byte{'.'} - -func (v *Fixed14_4) UnmarshalText(b []byte) error { - parts := bytes.SplitN(b, decimalByte, 2) - intPart, err := strconv.ParseInt(string(parts[0]), 10, 64) +func (v *FloatFixed14_4) Unmarshal(s string) error { + v2, err := strconv.ParseFloat(s, 64) if err != nil { return err } - - var fracPart int64 - if len(parts) >= 2 && len(parts[1]) > 0 { - fracStr := parts[1] - - for _, r := range fracStr { - if r < '0' || r > '9' { - return fmt.Errorf("found non-digit in fractional component of %q", string(b)) - } - } - - // Take only the 4 most significant digits of the fractional component. - fracStr = fracStr[:min(len(fracStr), 4)] - - fracPart, err = strconv.ParseInt(string(fracStr), 10, 16) - if err != nil { - return err - } - if fracPart < 0 { - // This shouldn't happen by virtue of earlier digit-only check. - return fmt.Errorf("got negative fractional component in %q", string(b)) - } - - switch len(fracStr) { - case 1: - fracPart *= 1000 - case 2: - fracPart *= 100 - case 3: - fracPart *= 10 - case 4: - fracPart *= 1 - } - if intPart < 0 { - fracPart = -fracPart - } + if v2 >= 1e14 || v2 <= -1e14 { + return fmt.Errorf("soap fixed14.4: value %q out of bounds", s) } - v.SetParts(intPart, int16(fracPart)) + *v = FloatFixed14_4(v2) return nil } @@ -436,28 +300,25 @@ func NewChar(v rune) *Char { } func (v *Char) String() string { - return string(rune(*v)) + return string(*v) } -func (v *Char) MarshalText() ([]byte, error) { +func (v *Char) Marshal() (string, error) { if *v == 0 { - return nil, errors.New("soap char: rune 0 is not allowed") + return "", errors.New("soap char: rune 0 is not allowed") } - result := make([]byte, utf8.RuneLen(rune(*v))) - n := utf8.EncodeRune(result, rune(*v)) - result = result[0:n] - return result, nil + return v.String(), nil } -func (v *Char) UnmarshalText(b []byte) error { - if len(b) == 0 { +func (v *Char) Unmarshal(s string) error { + if len(s) == 0 { return errors.New("soap char: got empty string") } - r, n := utf8.DecodeRune(b) - if n != len(b) { - return fmt.Errorf("soap char: value %q is not a single rune", string(b)) + v2, n := utf8.DecodeRune([]byte(s)) + if n != len(s) { + return fmt.Errorf("soap char: value %q is not a single rune", s) } - *v = Char(r) + *v = Char(v2) return nil } @@ -470,49 +331,23 @@ func NewString(v string) *String { return &v2 } -func (v *String) MarshalText() ([]byte, error) { - return []byte(*v), nil +func (v *String) Marshal() (string, error) { + return string(*v), nil } -func (v *String) UnmarshalText(b []byte) error { - *v = String(b) +func (v *String) Unmarshal(s string) error { + *v = String(s) return nil } -func parseInt(b []byte, err *error) int { - v, parseErr := strconv.ParseInt(string(b), 10, 64) +func parseInt(s string, err *error) int { + v, parseErr := strconv.ParseInt(s, 10, 64) if parseErr != nil { *err = parseErr } return int(v) } -var zeroDigits []byte = []byte("000") - -// appendInt appends `n` in decimal to `b`, with up to 3 digits of -// zero-padding to fill to a minimum of 4 digits. -func appendInt(b []byte, n int64, padding int) []byte { - if n > -1000 && n < 1000 { - if n < 0 { - n = -n - b = append(b, '-') - } - var digits int - if n < 10 { - digits = 1 - } else if n < 100 { - digits = 2 - } else if n < 1000 { - digits = 3 - } - numZeros := padding - digits - if numZeros > 0 { - b = append(b, zeroDigits[:numZeros]...) - } - } - return strconv.AppendInt(b, n, 10) -} - var dateRegexps = []*regexp.Regexp{ // yyyy[-mm[-dd]] regexp.MustCompile(`^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$`), @@ -520,23 +355,117 @@ var dateRegexps = []*regexp.Regexp{ regexp.MustCompile(`^(\d{4})(?:(\d{2})(?:(\d{2}))?)?$`), } -type prefixRemainder struct { - prefix []byte - remainder []byte +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 } -// prefixUntilAny returns a prefix of the leading bytes prior to any -// characters in `chars`, and the remainder. If no character from `chars` is -// present in `b`, then returns `b` as `prefix`, and empty `remainder`. -// -// prefixUntilAny([]byte("123/abc"), "/") => {[]byte("123"), []byte("/abc")} -// prefixUntilAny([]byte("123"), "/") => {[]byte("123"), []byte("")} -func prefixUntilAny(b []byte, chars string) prefixRemainder { - i := bytes.IndexAny(b, chars) - if i == -1 { - return prefixRemainder{prefix: b, remainder: nil} +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) (TimeOfDay, error) { + var parts []string + for _, re := range timeRegexps { + parts = re.FindStringSubmatch(s) + if parts != nil { + break + } } - return prefixRemainder{prefix: b[:i], remainder: b[i:]} + if parts == nil { + return TimeOfDay{}, fmt.Errorf("soap time: value %q is not in ISO8601 time format", s) + } + + var err error + var hour, minute, second int8 + hour = int8(parseInt(parts[1], &err)) + if len(parts[2]) != 0 { + minute = int8(parseInt(parts[2], &err)) + if len(parts[3]) != 0 { + second = int8(parseInt(parts[3], &err)) + } + } + + if err != nil { + return TimeOfDay{}, fmt.Errorf("soap time: %q: %v", s, err) + } + + return TimeOfDay{hour, minute, second}, nil +} + +// (+|-)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 } // TimeOfDay is used in cases where SOAP "time" or "time.tz" is used. @@ -549,476 +478,279 @@ type TimeOfDay struct { var _ SOAPValue = &TimeOfDay{} -func TimeOfDayFromTime(t time.Time) TimeOfDay { - h, m, s := t.Clock() - return TimeOfDay{int8(h), int8(m), int8(s)} -} - -func (tod *TimeOfDay) SetFromTime(t time.Time) { - h, m, s := t.Clock() - tod.Hour = int8(h) - tod.Minute = int8(m) - tod.Second = int8(s) -} - // Sets components based on duration since midnight. -func (tod *TimeOfDay) SetFromDuration(d time.Duration) error { +func (v *TimeOfDay) SetFromDuration(d time.Duration) error { if d < 0 || d > 24*time.Hour { return fmt.Errorf("out of range of SOAP time type: %v", d) } - tod.Hour = int8(d / time.Hour) + v.Hour = int8(d / time.Hour) d = d % time.Hour - tod.Minute = int8(d / time.Minute) + v.Minute = int8(d / time.Minute) d = d % time.Minute - tod.Second = int8(d / time.Second) + v.Second = int8(d / time.Second) return nil } // Returns duration since midnight. -func (tod TimeOfDay) ToDuration() time.Duration { - return time.Duration(tod.Hour)*time.Hour + - time.Duration(tod.Minute)*time.Minute + - time.Duration(tod.Second)*time.Second +func (v *TimeOfDay) ToDuration() time.Duration { + return time.Duration(v.Hour)*time.Hour + + time.Duration(v.Minute)*time.Minute + + time.Duration(v.Second)*time.Second } -func (tod *TimeOfDay) String() string { - return string(tod.marshalText(nil)) +func (v *TimeOfDay) String() string { + return fmt.Sprintf("%02d:%02d:%02d", v.Hour, v.Minute, v.Second) +} + +func (v *TimeOfDay) Equal(o *TimeOfDay) bool { + return v.Hour == o.Hour && v.Minute == o.Minute && v.Second == o.Second } // IsValid returns true iff v is positive and <= 24 hours. // It allows equal to 24 hours as a special case as 24:00:00 is an allowed // value by the SOAP type. -func (tod TimeOfDay) CheckValid() error { - if (tod.Hour < 0 || tod.Minute < 0 || tod.Second < 0) || - (tod.Hour == 24 && (tod.Minute > 0 || tod.Second > 0)) || - tod.Hour > 24 || tod.Minute >= 60 || tod.Second >= 60 { - return fmt.Errorf("soap time: value %v has components(s) out of range", tod) +func (v *TimeOfDay) CheckValid() error { + if (v.Hour < 0 || v.Minute < 0 || v.Second < 0) || + (v.Hour == 24 && (v.Minute > 0 || v.Second > 0)) || + v.Hour > 24 || v.Minute >= 60 || v.Second >= 60 { + return fmt.Errorf("soap time: value %v has components(s) out of range", v) } return nil } -// clear removes data from v, setting to default values. -func (tod *TimeOfDay) clear() { - tod.Hour = 0 - tod.Minute = 0 - tod.Second = 0 -} - -func (tod *TimeOfDay) marshalText(b []byte) []byte { - b = appendInt(b, int64(tod.Hour), 2) - b = append(b, ':') - b = appendInt(b, int64(tod.Minute), 2) - b = append(b, ':') - b = appendInt(b, int64(tod.Second), 2) - return b -} - -func (tod *TimeOfDay) MarshalText() ([]byte, error) { - if err := tod.CheckValid(); err != nil { - return nil, err +func (v *TimeOfDay) Marshal() (string, error) { + if err := v.CheckValid(); err != nil { + return "", err } - return tod.marshalText(nil), nil + return v.String(), nil } -var timeRegexps = []*regexp.Regexp{ - // hh:mm:ss - regexp.MustCompile(`^(\d{2})(\d{2})(\d{2})$`), - // hhmmss - regexp.MustCompile(`^(\d{2}):(\d{2}):(\d{2})$`), -} - -func (tod *TimeOfDay) UnmarshalText(b []byte) error { - tod.clear() - var parts [][]byte - for _, re := range timeRegexps { - parts = re.FindSubmatch(b) - if parts != nil { - break - } - } - if parts == nil { - return fmt.Errorf("value %q is not in ISO8601 time format", string(b)) - } - +func (v *TimeOfDay) Unmarshal(s string) error { var err error - tod.Hour = int8(parseInt(parts[1], &err)) - tod.Minute = int8(parseInt(parts[2], &err)) - tod.Second = int8(parseInt(parts[3], &err)) + *v, err = parseTimeParts(s) if err != nil { - return fmt.Errorf("value %q is not in ISO8601 time format: %v", string(b), err) + return err } - return tod.CheckValid() + return v.CheckValid() } -// TimeOfDayTZ maps to the SOAP "time.tz" type. +// TimeOfDayTZ is used in cases where SOAP "time.tz" is used. type TimeOfDayTZ struct { // Components of the time of day. TimeOfDay TimeOfDay - // Timezone designator. - TZ TZD + // Set to true if Offset is specified. If false, then the timezone is + // unspecified (and by ISO8601 - implies some "local" time). + HasOffset bool + + // 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 int } var _ SOAPValue = &TimeOfDayTZ{} -func (todz *TimeOfDayTZ) String() string { - return string(todz.TZ.marshalText(nil)) +func (v *TimeOfDayTZ) String() string { + return fmt.Sprintf("%v %t %+03d:%02d:%02d", v.TimeOfDay, v.HasOffset, v.Offset/3600, (v.Offset%3600)/60, v.Offset%60) } -// clear removes data from v, setting to default values. -func (todz *TimeOfDayTZ) clear() { - todz.TimeOfDay.clear() - todz.TZ.clear() +func (v *TimeOfDayTZ) Equal(o *TimeOfDayTZ) bool { + return v.TimeOfDay.Equal(&o.TimeOfDay) && + v.HasOffset == o.HasOffset && v.Offset == o.Offset } -func (todz *TimeOfDayTZ) MarshalText() ([]byte, error) { - b := todz.TimeOfDay.marshalText(nil) - b = todz.TZ.marshalText(b) - return b, nil -} - -func (todz *TimeOfDayTZ) UnmarshalText(b []byte) error { - todz.clear() - parts := prefixUntilAny(b, "Z+-") - if err := todz.TimeOfDay.UnmarshalText(parts.prefix); err != nil { - return err - } - return todz.TZ.unmarshalText(parts.remainder) -} - -// Date maps to the SOAP "date" type. Marshaling and Unmarshalling does *not* -// check if components are in range. -type Date struct { - Year int - Month time.Month - Day int -} - -var _ SOAPValue = &Date{} - -func DateFromTime(t time.Time) Date { - var d Date - d.SetFromTime(t) - return d -} - -func (d *Date) SetFromTime(t time.Time) { - d.Year = t.Year() - d.Month = t.Month() - d.Day = t.Day() -} - -// ToTime returns a time.Time from the date components, at midnight, and using -// the given location. -func (d Date) ToTime(loc *time.Location) time.Time { - return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc) -} - -func (d *Date) String() string { - return string(d.marshalText(nil)) -} - -// CheckValid returns an error if the date components are out of range. -func (d Date) CheckValid() error { - y, m, day := d.ToTime(time.UTC).Date() - if y != d.Year || m != d.Month || day != d.Day { - return fmt.Errorf("SOAP date component(s) out of range in %v", d) - } - return nil -} - -// clear removes data from d, setting to default (invalid/zero) values. -func (d *Date) clear() { - d.Year = 0 - d.Month = 0 - d.Day = 0 -} - -func (d *Date) marshalText(b []byte) []byte { - b = appendInt(b, int64(d.Year), 4) - b = append(b, '-') - b = appendInt(b, int64(d.Month), 2) - b = append(b, '-') - b = appendInt(b, int64(d.Day), 2) - return b -} - -func (d *Date) MarshalText() ([]byte, error) { - return d.marshalText(nil), nil -} - -func (d *Date) UnmarshalText(b []byte) error { - d.clear() - var parts [][]byte - for _, re := range dateRegexps { - parts = re.FindSubmatch(b) - if parts != nil { - break - } - } - if parts == nil { - return fmt.Errorf("error parsing date: value %q is not in a recognized ISO8601 date format", string(b)) - } - - var err error - d.Year = parseInt(parts[1], &err) - d.Month = time.January - d.Day = 1 - if len(parts[2]) != 0 { - d.Month = time.Month(parseInt(parts[2], &err)) - if len(parts[3]) != 0 { - d.Day = parseInt(parts[3], &err) - } - } - +func (v *TimeOfDayTZ) Marshal() (string, error) { + tod, err := v.TimeOfDay.Marshal() if err != nil { - return fmt.Errorf("error parsing date %q: %v", string(b), err) + return "", err + } + + tz := "" + if v.HasOffset { + if v.Offset == 0 { + tz = "Z" + } else { + offsetMins := v.Offset / 60 + sign := '+' + if offsetMins < 1 { + offsetMins = -offsetMins + sign = '-' + } + tz = fmt.Sprintf("%c%02d:%02d", sign, offsetMins/60, offsetMins%60) + } + } + + return tod + tz, nil +} + +func (v *TimeOfDayTZ) Unmarshal(s string) error { + zoneIndex := strings.IndexAny(s, "Z+-") + var timePart string + if zoneIndex == -1 { + v.HasOffset = false + v.Offset = 0 + timePart = s + } else { + v.HasOffset = true + timePart = s[:zoneIndex] + var err error + v.Offset, err = parseTimezone(s[zoneIndex:]) + if err != nil { + return err + } + } + + if err := v.TimeOfDay.Unmarshal(timePart); err != nil { + return err } return nil } -// DateTime maps to SOAP "dateTime" type. -type DateTime struct { - Date Date - TimeOfDay TimeOfDay +// DateLocal maps time.Time to the SOAP "date" type. Dates map to midnight in +// the local time zone. The time of day components are ignored when +// marshalling. +type DateLocal time.Time + +var _ SOAPValue = &DateLocal{} + +func NewDateLocal(v time.Time) *DateLocal { + v2 := DateLocal(v) + return &v2 } -var _ SOAPValue = &DateTime{} - -func DateTimeFromTime(v time.Time) DateTime { - dt := DateTime{} - dt.Date.SetFromTime(v) - dt.TimeOfDay.SetFromTime(v) - return dt +func (v DateLocal) String() string { + return v.ToTime().String() } -func (dt *DateTime) String() string { - return string(dt.marshalText(nil)) +func (v DateLocal) ToTime() time.Time { + return time.Time(v) } -func (dt DateTime) ToTime(loc *time.Location) time.Time { - return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, - int(dt.TimeOfDay.Hour), int(dt.TimeOfDay.Minute), int(dt.TimeOfDay.Second), 0, - loc) +func (v *DateLocal) Marshal() (string, error) { + return time.Time(*v).In(localLoc).Format("2006-01-02"), nil } -// clear removes data from dt, setting to default values. -func (dt *DateTime) clear() { - dt.Date.clear() - dt.TimeOfDay.clear() -} - -func (dt *DateTime) marshalText(b []byte) []byte { - b = dt.Date.marshalText(b) - b = append(b, 'T') - b = dt.TimeOfDay.marshalText(b) - return b -} - -func (dt *DateTime) MarshalText() ([]byte, error) { - return dt.marshalText(nil), nil -} - -func (dt *DateTime) UnmarshalText(b []byte) error { - dt.clear() - parts := prefixUntilAny(b, "T") - if err := dt.Date.UnmarshalText(parts.prefix); err != nil { - return err - } - - if len(parts.remainder) == 0 { - return nil - } - - if parts.remainder[0] != 'T' { - return fmt.Errorf("missing 'T' time separator in dateTime %q", string(b)) - } - - return dt.TimeOfDay.UnmarshalText(parts.remainder[1:]) -} - -// DateTime maps to SOAP type "dateTime.tz". -type DateTimeTZ struct { - Date Date - TimeOfDay TimeOfDay - TZ TZD -} - -var _ SOAPValue = &DateTimeTZ{} - -func DateTimeTZFromTime(t time.Time) DateTimeTZ { - return DateTimeTZ{ - Date: DateFromTime(t), - TimeOfDay: TimeOfDayFromTime(t), - TZ: TZDFromTime(t), - } -} - -func (dtz *DateTimeTZ) String() string { - return string(dtz.marshalText(nil)) -} - -// Time converts `dtz` to time.Time, using defaultLoc as the default location if -// `dtz` contains no offset information. -func (dtz DateTimeTZ) Time(defaultLoc *time.Location) time.Time { - return time.Date( - dtz.Date.Year, - dtz.Date.Month, - dtz.Date.Day, - int(dtz.TimeOfDay.Hour), - int(dtz.TimeOfDay.Minute), - int(dtz.TimeOfDay.Second), - 0, - dtz.TZ.Location(defaultLoc), - ) -} - -// clear removes data from dtz, setting to default values. -func (dtz *DateTimeTZ) clear() { - dtz.Date.clear() - dtz.TimeOfDay.clear() - dtz.TZ.clear() -} - -func (dtz *DateTimeTZ) marshalText(b []byte) []byte { - b = dtz.Date.marshalText(b) - b = append(b, 'T') - b = dtz.TimeOfDay.marshalText(b) - b = dtz.TZ.marshalText(b) - return b -} - -func (dtz *DateTimeTZ) MarshalText() ([]byte, error) { - return dtz.marshalText(nil), nil -} - -func (dtz *DateTimeTZ) UnmarshalText(b []byte) error { - dtz.clear() - dateParts := prefixUntilAny(b, "T") - if err := dtz.Date.UnmarshalText(dateParts.prefix); err != nil { - return err - } - - if len(dateParts.remainder) == 0 { - return nil - } - - // Trim the leading "T" between date and time. - remainder := dateParts.remainder[1:] - timeParts := prefixUntilAny(remainder, "Z+-") - if err := dtz.TimeOfDay.UnmarshalText(timeParts.prefix); err != nil { - return err - } - - if len(timeParts.remainder) == 0 { - return nil - } - - return dtz.TZ.unmarshalText(timeParts.remainder) -} - -// TZD is a timezone designator. Not a full SOAP time in itself, but used as -// part of timezone-aware date/time types that are. -type TZD struct { - // Offset is the timezone offset in seconds. Note that the SOAP encoding - // only encodes precisions up to minutes, this is in seconds for - // interoperability with time.Time. - Offset int - // HasTZ specifies if a timezone offset is specified or not. - HasTZ bool -} - -func TZDFromTime(t time.Time) TZD { - _, offset := t.Zone() - return TZD{ - Offset: offset, - HasTZ: true, - } -} - -func TZDOffset(secondsOffset int) TZD { - return TZD{ - Offset: secondsOffset, - HasTZ: true, - } -} - -// Location returns an appropriate *time.Location (time.UTC or time.FixedZone), -// or defaultLoc if `v` contains no offset information. -func (tzd TZD) Location(defaultLoc *time.Location) *time.Location { - if !tzd.HasTZ { - return defaultLoc - } - if tzd.Offset == 0 { - return time.UTC - } - return time.FixedZone(tzd.String(), tzd.Offset) -} - -func (tzd *TZD) String() string { - return string(tzd.marshalText(nil)) -} - -// clear removes offset information from `v`. -func (tzd *TZD) clear() { - tzd.Offset = 0 - tzd.HasTZ = false -} - -func (tzd *TZD) marshalText(b []byte) []byte { - if !tzd.HasTZ { - return b - } - - if tzd.Offset == 0 { - b = append(b, 'Z') - return b - } - - offsetMins := tzd.Offset / 60 - var sign byte = '+' - if offsetMins < 1 { - offsetMins = -offsetMins - sign = '-' - } - h, m := offsetMins/60, offsetMins%60 - b = append(b, sign) - b = appendInt(b, int64(h), 2) - b = append(b, ':') - b = appendInt(b, int64(m), 2) - return b -} - -// (+|-)(hh):(mm) -var timezoneRegexp = regexp.MustCompile(`^([+-])(\d{2}):(\d{2})$`) - -func (tzd *TZD) unmarshalText(b []byte) error { - tzd.clear() - if len(b) == 0 { - return nil - } - tzd.HasTZ = true - if len(b) == 1 && b[0] == 'Z' { - return nil - } - parts := timezoneRegexp.FindSubmatch(b) - if parts == nil { - return fmt.Errorf("value %q is not in ISO8601 timezone format", string(b)) - } - - var err error - tzd.Offset = parseInt(parts[2], &err) * 3600 - tzd.Offset += parseInt(parts[3], &err) * 60 - if len(parts[1]) == 1 && parts[1][0] == '-' { - tzd.Offset = -tzd.Offset - } - +func (v *DateLocal) Unmarshal(s string) error { + year, month, day, err := parseDateParts(s) if err != nil { - err = fmt.Errorf("value %q is not in ISO8601 timezone format: %v", string(b), err) + return err + } + *v = DateLocal(time.Date(year, time.Month(month), day, 0, 0, 0, 0, localLoc)) + return nil +} + +// MarshalDateTime maps time.Time to SOAP "dateTime" type, with the local timezone. +type DateTimeLocal time.Time + +var _ SOAPValue = &DateTimeLocal{} + +func NewDateTimeLocal(v time.Time) *DateTimeLocal { + v2 := DateTimeLocal(v) + return &v2 +} + +func (v DateTimeLocal) String() string { + return v.ToTime().String() +} + +func (v DateTimeLocal) ToTime() time.Time { + return time.Time(v) +} + +func (v *DateTimeLocal) Marshal() (string, error) { + return v.ToTime().In(localLoc).Format("2006-01-02T15:04:05"), nil +} + +func (v *DateTimeLocal) Unmarshal(s string) error { + dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s) + if err != nil { + return err } + if len(zoneStr) != 0 { + return fmt.Errorf("soap datetime: unexpected timezone in %q", s) + } + + year, month, day, err := parseDateParts(dateStr) + if err != nil { + return err + } + + var tod TimeOfDay + if len(timeStr) != 0 { + tod, err = parseTimeParts(timeStr) + if err != nil { + return err + } + } + + *v = DateTimeLocal(time.Date(year, time.Month(month), day, + int(tod.Hour), int(tod.Minute), int(tod.Second), 0, + localLoc)) + return nil +} + +// DateTimeLocal maps time.Time to SOAP "dateTime.tz" type, using the local +// timezone when one is unspecified. +type DateTimeTZLocal time.Time + +var _ SOAPValue = &DateTimeTZLocal{} + +func NewDateTimeTZLocal(v time.Time) *DateTimeTZLocal { + v2 := DateTimeTZLocal(v) + return &v2 +} + +func (v DateTimeTZLocal) String() string { + return v.ToTime().String() +} + +func (v DateTimeTZLocal) ToTime() time.Time { + return time.Time(v) +} + +func (v *DateTimeTZLocal) Marshal() (string, error) { + return time.Time(*v).Format("2006-01-02T15:04:05-07:00"), nil +} + +func (v *DateTimeTZLocal) Unmarshal(s string) error { + dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s) + if err != nil { + return err + } + + year, month, day, err := parseDateParts(dateStr) + if err != nil { + return err + } + + var tod TimeOfDay + var location *time.Location = localLoc + if len(timeStr) != 0 { + tod, err = parseTimeParts(timeStr) + if err != nil { + return err + } + if len(zoneStr) != 0 { + var offset int + offset, err = parseTimezone(zoneStr) + if err != nil { + return err + } + if offset == 0 { + location = time.UTC + } else { + location = time.FixedZone("", offset) + } + } + } + + *v = DateTimeTZLocal(time.Date(year, time.Month(month), day, + int(tod.Hour), int(tod.Minute), int(tod.Second), 0, + location)) return nil } @@ -1038,21 +770,21 @@ func (v *Boolean) String() string { return "false" } -func (v *Boolean) MarshalText() ([]byte, error) { +func (v *Boolean) Marshal() (string, error) { if *v { - return []byte{'1'}, nil + return "1", nil } - return []byte{'0'}, nil + return "0", nil } -func (v *Boolean) UnmarshalText(b []byte) error { - switch string(b) { +func (v *Boolean) Unmarshal(s string) error { + switch s { case "0", "false", "no": *v = false case "1", "true", "yes": *v = true default: - return fmt.Errorf("soap boolean: %q is not a valid boolean value", string(b)) + return fmt.Errorf("soap boolean: %q is not a valid boolean value", s) } return nil } @@ -1071,16 +803,13 @@ func (v *BinBase64) String() string { return base64.StdEncoding.EncodeToString(*v) } -func (v *BinBase64) MarshalText() ([]byte, error) { - result := make([]byte, base64.StdEncoding.EncodedLen(len(*v))) - base64.StdEncoding.Encode(result, []byte(*v)) - return result, nil +func (v *BinBase64) Marshal() (string, error) { + return v.String(), nil } -func (v *BinBase64) UnmarshalText(b []byte) error { - *v = make(BinBase64, base64.StdEncoding.DecodedLen(len(b))) - n, err := base64.StdEncoding.Decode([]byte(*v), b) - *v = (*v)[:n] +func (v *BinBase64) Unmarshal(s string) error { + v2, err := base64.StdEncoding.DecodeString(s) + *v = v2 return err } @@ -1098,16 +827,13 @@ func (v *BinHex) String() string { return hex.EncodeToString(*v) } -func (v *BinHex) MarshalText() ([]byte, error) { - result := make([]byte, hex.EncodedLen(len(*v))) - hex.Encode(result, []byte(*v)) - return result, nil +func (v *BinHex) Marshal() (string, error) { + return v.String(), nil } -func (v *BinHex) UnmarshalText(b []byte) error { - *v = make(BinHex, hex.DecodedLen(len(b))) - n, err := hex.Decode(*v, b) - *v = (*v)[:n] +func (v *BinHex) Unmarshal(s string) error { + v2, err := hex.DecodeString(s) + *v = v2 return err } @@ -1124,22 +850,15 @@ func (v *URI) ToURL() *url.URL { return (*url.URL)(v) } -func (v *URI) MarshalText() ([]byte, error) { - return []byte((*url.URL)(v).String()), nil +func (v *URI) Marshal() (string, error) { + return (*url.URL)(v).String(), nil } -func (v *URI) UnmarshalText(b []byte) error { - v2, err := url.Parse(string(b)) +func (v *URI) Unmarshal(s string) error { + v2, err := url.Parse(s) if err != nil { return err } *v = URI(*v2) return nil } - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/v2/soap/types/types_test.go b/v2/soap/types/types_test.go index c3007e0..7d6e979 100644 --- a/v2/soap/types/types_test.go +++ b/v2/soap/types/types_test.go @@ -3,21 +3,10 @@ package types import ( "bytes" "fmt" - "math" "testing" "time" ) -var dummyLoc = time.FixedZone("DummyTZ", 6*3600) - -func newFixed14_4Parts(intPart int64, fracPart int16) *Fixed14_4 { - v, err := Fixed14_4FromParts(intPart, fracPart) - if err != nil { - panic(err) - } - return &v -} - type isEqual func(got, want SOAPValue) bool type typeTestCase struct { @@ -40,6 +29,12 @@ type unmarshalCase struct { } func Test(t *testing.T) { + // Fake out the local time for the implementation. + localLoc = time.FixedZone("Fake/Local", 6*3600) + defer func() { + localLoc = time.Local + }() + badNumbers := []string{"", " ", "abc"} typeTestCases := []typeTestCase{ @@ -140,37 +135,20 @@ func Test(t *testing.T) { }, { - makeValue: func() SOAPValue { return &Fixed14_4{} }, - isEqual: func(got, want SOAPValue) bool { - return got.(*Fixed14_4).Fractional == want.(*Fixed14_4).Fractional - }, + makeValue: func() SOAPValue { return new(FloatFixed14_4) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*FloatFixed14_4) == *want.(*FloatFixed14_4) }, marshalTests: []marshalCase{ - {newFixed14_4Parts(0, 0), "0.0000"}, - {newFixed14_4Parts(1, 2), "1.0002"}, - {newFixed14_4Parts(1, 20), "1.0020"}, - {newFixed14_4Parts(1, 200), "1.0200"}, - {newFixed14_4Parts(1, 2000), "1.2000"}, - {newFixed14_4Parts(-1, -2), "-1.0002"}, - {newFixed14_4Parts(1234, 5678), "1234.5678"}, - {newFixed14_4Parts(-1234, -5678), "-1234.5678"}, - {newFixed14_4Parts(9999_99999_99999, 9999), "99999999999999.9999"}, - {newFixed14_4Parts(-9999_99999_99999, -9999), "-99999999999999.9999"}, + {NewFloatFixed14_4(0), "0.0000"}, + {NewFloatFixed14_4(1), "1.0000"}, + {NewFloatFixed14_4(1.2346), "1.2346"}, + {NewFloatFixed14_4(-1), "-1.0000"}, + {NewFloatFixed14_4(-1.2346), "-1.2346"}, + {NewFloatFixed14_4(1e13), "10000000000000.0000"}, + {NewFloatFixed14_4(-1e13), "-10000000000000.0000"}, }, - unmarshalErrs: append([]string{ - "", ".", "0.00000000abc", "0.-5", - }, badNumbers...), - unmarshalTests: []unmarshalCase{ - {"010", newFixed14_4Parts(10, 0)}, - {"0", newFixed14_4Parts(0, 0)}, - {"0.", newFixed14_4Parts(0, 0)}, - {"0.000005", newFixed14_4Parts(0, 0)}, - {"1.2", newFixed14_4Parts(1, 2000)}, - {"1.20", newFixed14_4Parts(1, 2000)}, - {"1.200", newFixed14_4Parts(1, 2000)}, - {"1.02", newFixed14_4Parts(1, 200)}, - {"1.020", newFixed14_4Parts(1, 200)}, - {"1.002", newFixed14_4Parts(1, 20)}, - {"1.00200005", newFixed14_4Parts(1, 20)}, + marshalErrs: []SOAPValue{ + NewFloatFixed14_4(1e14), + NewFloatFixed14_4(-1e14), }, }, @@ -186,10 +164,24 @@ func Test(t *testing.T) { unmarshalErrs: []string{"aa", ""}, }, + { + makeValue: func() SOAPValue { return new(DateLocal) }, + isEqual: func(got, want SOAPValue) bool { + return got.(*DateLocal).ToTime().Equal(want.(*DateLocal).ToTime()) + }, + marshalTests: []marshalCase{ + {NewDateLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)), "2013-10-08"}, + }, + unmarshalTests: []unmarshalCase{ + {"20131008", NewDateLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc))}, + }, + unmarshalErrs: []string{"", "-1"}, + }, + { makeValue: func() SOAPValue { return new(TimeOfDay) }, isEqual: func(got, want SOAPValue) bool { - return got.(*TimeOfDay).equal(*want.(*TimeOfDay)) + return got.(*TimeOfDay).Equal(want.(*TimeOfDay)) }, marshalTests: []marshalCase{ {&TimeOfDay{}, "00:00:00"}, @@ -212,29 +204,36 @@ func Test(t *testing.T) { "00:00:60", // Unexpected timezone component: "01:02:03Z", + "01:02:03+01", "01:02:03+01:23", - "01:02:03+01:23", - "01:02:03-01:23", + "01:02:03+0123", + "01:02:03-01", "01:02:03-01:23", + "01:02:03-0123", }, }, { makeValue: func() SOAPValue { return new(TimeOfDayTZ) }, isEqual: func(got, want SOAPValue) bool { - return got.(*TimeOfDayTZ).equal(*want.(*TimeOfDayTZ)) + return got.(*TimeOfDayTZ).Equal(want.(*TimeOfDayTZ)) }, marshalTests: []marshalCase{ {&TimeOfDayTZ{}, "00:00:00"}, // ISO8601 special case - {&TimeOfDayTZ{TimeOfDay{24, 0, 0}, TZD{}}, "24:00:00"}, - {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(0)}, "01:02:03Z"}, - {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(3600 + 23*60)}, "01:02:03+01:23"}, - {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(-(3600 + 23*60))}, "01:02:03-01:23"}, + {&TimeOfDayTZ{TimeOfDay{24, 0, 0}, false, 0}, "24:00:00"}, + {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 0}, "01:02:03Z"}, + {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 3600 + 23*60}, "01:02:03+01:23"}, + {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, -(3600 + 23*60)}, "01:02:03-01:23"}, }, unmarshalTests: []unmarshalCase{ - {"010203+01:23", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(3600 + 23*60)}}, - {"010203-01:23", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(-(3600 + 23*60))}}, + {"000000", &TimeOfDayTZ{}}, + {"01Z", &TimeOfDayTZ{TimeOfDay{1, 0, 0}, true, 0}}, + {"01+01", &TimeOfDayTZ{TimeOfDay{1, 0, 0}, true, 3600}}, + {"01:02:03+01", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 3600}}, + {"01:02:03+0123", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 3600 + 23*60}}, + {"01:02:03-01", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, -3600}}, + {"01:02:03-0123", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, -(3600 + 23*60)}}, }, unmarshalErrs: []string{ // Misformatted values: @@ -249,54 +248,63 @@ func Test(t *testing.T) { }, { - makeValue: func() SOAPValue { return new(Date) }, + makeValue: func() SOAPValue { return new(DateLocal) }, isEqual: func(got, want SOAPValue) bool { - a, b := got.(*Date), want.(*Date) - return a.Year == b.Year && a.Month == b.Month && a.Day == b.Day + return got.(*DateLocal).ToTime().Equal(want.(*DateLocal).ToTime()) }, marshalTests: []marshalCase{ - {&Date{2013, 10, 8}, "2013-10-08"}, + {NewDateLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)), "2013-10-08"}, }, unmarshalTests: []unmarshalCase{ - {"20131008", &Date{2013, 10, 8}}, + {"20131008", NewDateLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc))}, + }, + unmarshalErrs: []string{ + // Unexpected time component. + "2013-10-08T10:30:50", + // Unexpected timezone component. + "2013-10-08+01", }, - unmarshalErrs: []string{"", "-1"}, }, { - makeValue: func() SOAPValue { return new(DateTime) }, + makeValue: func() SOAPValue { return new(DateTimeLocal) }, isEqual: func(got, want SOAPValue) bool { - return got.(*DateTime).equal(*want.(*DateTime)) + return got.(*DateTimeLocal).ToTime().Equal(want.(*DateTimeLocal).ToTime()) }, marshalTests: []marshalCase{ - {DateTimeFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, dummyLoc)).ptr(), "2013-10-08T00:00:00"}, - {DateTimeFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, dummyLoc)).ptr(), "2013-10-08T10:30:50"}, + {NewDateTimeLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)), "2013-10-08T00:00:00"}, + {NewDateTimeLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)), "2013-10-08T10:30:50"}, }, unmarshalTests: []unmarshalCase{ - {"20131008", DateTimeFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, dummyLoc)).ptr()}, + {"20131008", NewDateTimeLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc))}, }, unmarshalErrs: []string{ // Unexpected timezone component. - "2013-10-08T10:30:50+01:00", + "2013-10-08T10:30:50+01", }, }, { - makeValue: func() SOAPValue { return new(DateTimeTZ) }, + makeValue: func() SOAPValue { return new(DateTimeTZLocal) }, isEqual: func(got, want SOAPValue) bool { - return got.(*DateTimeTZ).equal(*want.(*DateTimeTZ)) + return got.(*DateTimeTZLocal).ToTime().Equal(want.(*DateTimeTZLocal).ToTime()) }, marshalTests: []marshalCase{ - {DateTimeTZFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, dummyLoc)).ptr(), "2013-10-08T00:00:00+06:00"}, - {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, dummyLoc)).ptr(), "2013-10-08T10:30:50+06:00"}, - {DateTimeTZFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, time.UTC)).ptr(), "2013-10-08T00:00:00Z"}, - {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)).ptr(), "2013-10-08T10:30:50Z"}, - {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))).ptr(), "2013-10-08T10:30:50+01:23"}, - {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))).ptr(), "2013-10-08T10:30:50-01:23"}, + {NewDateTimeTZLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)), "2013-10-08T00:00:00+06:00"}, + {NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)), "2013-10-08T10:30:50+06:00"}, + {NewDateTimeTZLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, time.UTC)), "2013-10-08T00:00:00+00:00"}, + {NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)), "2013-10-08T10:30:50+00:00"}, + {NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))), "2013-10-08T10:30:50+01:23"}, + {NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))), "2013-10-08T10:30:50-01:23"}, }, unmarshalTests: []unmarshalCase{ - {"2013-10-08T10:30:50", &DateTimeTZ{Date{2013, 10, 8}, TimeOfDay{10, 30, 50}, TZD{}}}, - {"2013-10-08T10:30:50+00:00", DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)).ptr()}, + {"20131008", NewDateTimeTZLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc))}, + {"2013-10-08T10:30:50", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc))}, + {"2013-10-08T10:30:50Z", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC))}, + {"2013-10-08T10:30:50+01", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:00", 3600)))}, + {"2013-10-08T10:30:50+0123", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60)))}, + {"2013-10-08T10:30:50-01", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:00", -3600)))}, + {"2013-10-08T10:30:50-0123", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60))))}, }, }, @@ -372,11 +380,10 @@ func Test(t *testing.T) { for i, mt := range tt.marshalTests { mt := mt t.Run(fmt.Sprintf("marshalTest#%d_%v", i, mt.input), func(t *testing.T) { - gotBytes, err := mt.input.MarshalText() + got, err := mt.input.Marshal() if err != nil { t.Errorf("got unexpected error: %v", err) } - got := string(gotBytes) if got != mt.want { t.Errorf("got %q, want: %q", got, mt.want) } @@ -385,7 +392,7 @@ func Test(t *testing.T) { for i, input := range tt.marshalErrs { input := input t.Run(fmt.Sprintf("marshalErr#%d_%v", i, input), func(t *testing.T) { - got, err := input.MarshalText() + got, err := input.Marshal() if err == nil { t.Errorf("got %q, want error", got) } @@ -395,8 +402,8 @@ func Test(t *testing.T) { ut := ut t.Run(fmt.Sprintf("unmarshalTest#%d_%q", i, ut.input), func(t *testing.T) { got := tt.makeValue() - if err := got.UnmarshalText([]byte(ut.input)); err != nil { - t.Errorf("got unexpected error: %v", err) + if err := got.Unmarshal(ut.input); err != nil { + t.Errorf("got error, want success") } if !tt.isEqual(got, ut.want) { t.Errorf("got %v, want %v", got, ut.want) @@ -407,7 +414,7 @@ func Test(t *testing.T) { input := input t.Run(fmt.Sprintf("unmarshalErrs#%d_%q", i, input), func(t *testing.T) { got := tt.makeValue() - if err := got.UnmarshalText([]byte(input)); err == nil { + if err := got.Unmarshal(input); err == nil { t.Errorf("got %v, want error", got) } }) @@ -415,123 +422,3 @@ func Test(t *testing.T) { }) } } - -func TestFixed14_4(t *testing.T) { - t.Run("Parts", func(t *testing.T) { - tests := []struct { - intPart int64 - fracPart int16 - fractional int64 - }{ - {0, 0, 0}, - {1, 2, 1_0002}, - {-1, -2, -1_0002}, - {1234, 5678, 1234_5678}, - {-1234, -5678, -1234_5678}, - {9999_99999_99999, 9999, 9999_99999_99999_9999}, - {-9999_99999_99999, -9999, -9999_99999_99999_9999}, - } - for _, test := range tests { - test := test - t.Run(fmt.Sprintf("FromParts(%d,%d)", test.intPart, test.fracPart), func(t *testing.T) { - got, err := Fixed14_4FromParts(test.intPart, test.fracPart) - if err != nil { - t.Errorf("got error %v, want success", err) - } - if got.Fractional != test.fractional { - t.Errorf("got %d, want %d", got.Fractional, test.fractional) - } - }) - t.Run(fmt.Sprintf("%d.Parts()", test.fractional), func(t *testing.T) { - v, err := Fixed14_4FromFractional(test.fractional) - if err != nil { - t.Errorf("got error %v, want success", err) - } - gotIntPart, gotFracPart := v.Parts() - if gotIntPart != test.intPart { - t.Errorf("got %d, want %d", gotIntPart, test.intPart) - } - if gotFracPart != test.fracPart { - t.Errorf("got %d, want %d", gotFracPart, test.fracPart) - } - }) - } - }) - t.Run("Float", func(t *testing.T) { - tests := []struct { - flt float64 - fix *Fixed14_4 - }{ - {0, newFixed14_4Parts(0, 0)}, - {1234.5678, newFixed14_4Parts(1234, 5678)}, - {-1234.5678, newFixed14_4Parts(-1234, -5678)}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("To/FromFloat(%v)", test.fix), func(t *testing.T) { - gotFix, err := Fixed14_4FromFloat(test.flt) - if err != nil { - t.Errorf("got error %v, want success", err) - } - if gotFix.Fractional != test.fix.Fractional { - t.Errorf("got %v, want %v", gotFix, test.fix) - } - - gotFlt := test.fix.Float64() - if math.Abs(gotFlt-test.flt) > 1e-6 { - t.Errorf("got %f, want %f", gotFlt, test.flt) - } - }) - } - - errTests := []float64{ - 1e50, - -1e50, - 1e14, - -1e14, - math.NaN(), - math.Inf(1), - math.Inf(-1), - } - for _, test := range errTests { - t.Run(fmt.Sprintf("ErrorFromFloat(%f)", test), func(t *testing.T) { - got, err := Fixed14_4FromFloat(test) - if err == nil { - t.Errorf("got success and %v, want error", got) - } - }) - } - }) -} - -// methods only used in testing: - -func (v TimeOfDay) equal(o TimeOfDay) bool { - return v.Hour == o.Hour && v.Minute == o.Minute && v.Second == o.Second -} - -func (v TimeOfDayTZ) equal(o TimeOfDayTZ) bool { - return v.TimeOfDay.equal(o.TimeOfDay) && v.TZ.equal(o.TZ) -} - -func (d Date) equal(o Date) bool { - return d.Year == o.Year && d.Month == o.Month && d.Day == o.Day -} - -func (dtz DateTime) ptr() *DateTime { return &dtz } - -func (dt DateTime) equal(o DateTime) bool { - return dt.Date.equal(o.Date) && dt.TimeOfDay.equal(o.TimeOfDay) -} - -func (dtz DateTimeTZ) ptr() *DateTimeTZ { return &dtz } - -func (dtz DateTimeTZ) equal(o DateTimeTZ) bool { - return dtz.Date.equal(o.Date) && - dtz.TimeOfDay.equal(o.TimeOfDay) && - dtz.TZ.equal(o.TZ) -} - -func (tzd TZD) equal(o TZD) bool { - return tzd.Offset == o.Offset && tzd.HasTZ == o.HasTZ -}