diff --git a/v2/soap/types/types.go b/v2/soap/types/types.go new file mode 100644 index 0000000..974cadb --- /dev/null +++ b/v2/soap/types/types.go @@ -0,0 +1,864 @@ +// Package types defines types that encode values in SOAP requests and responses. +package types + +import ( + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "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 { + Marshal() (string, error) + Unmarshal(s string) error +} + +type UI1 uint8 + +var _ SOAPValue = new(UI1) + +func NewUI1(v uint8) *UI1 { + v2 := UI1(v) + return &v2 +} + +func (v *UI1) String() string { + return strconv.FormatUint(uint64(*v), 10) +} + +func (v *UI1) Marshal() (string, error) { + return v.String(), nil +} + +func (v *UI1) Unmarshal(s string) error { + v2, err := strconv.ParseUint(s, 10, 8) + *v = UI1(v2) + return err +} + +type UI2 uint16 + +var _ SOAPValue = new(UI2) + +func NewUI2(v uint16) *UI2 { + v2 := UI2(v) + return &v2 +} + +func (v *UI2) String() string { + return strconv.FormatUint(uint64(*v), 10) +} + +func (v *UI2) Marshal() (string, error) { + return v.String(), nil +} + +func (v *UI2) Unmarshal(s string) error { + v2, err := strconv.ParseUint(s, 10, 16) + *v = UI2(v2) + return err +} + +type UI4 uint32 + +var _ SOAPValue = new(UI4) + +func NewUI4(v uint32) *UI4 { + v2 := UI4(v) + return &v2 +} + +func (v *UI4) String() string { + return strconv.FormatUint(uint64(*v), 10) +} + +func (v *UI4) Marshal() (string, error) { + return v.String(), nil +} + +func (v *UI4) Unmarshal(s string) error { + v2, err := strconv.ParseUint(s, 10, 32) + *v = UI4(v2) + return err +} + +type UI8 uint64 + +var _ SOAPValue = new(UI8) + +func NewUI8(v uint64) *UI8 { + v2 := UI8(v) + return &v2 +} + +func (v *UI8) String() string { + return strconv.FormatUint(uint64(*v), 10) +} + +func (v *UI8) Marshal() (string, error) { + return v.String(), nil +} + +func (v *UI8) Unmarshal(s string) error { + v2, err := strconv.ParseUint(s, 10, 64) + *v = UI8(v2) + return err +} + +type I1 int8 + +var _ SOAPValue = new(I1) + +func NewI1(v int8) *I1 { + v2 := I1(v) + return &v2 +} + +func (v *I1) String() string { + return strconv.FormatInt(int64(*v), 10) +} + +func (v *I1) Marshal() (string, error) { + return v.String(), nil +} + +func (v *I1) Unmarshal(s string) error { + v2, err := strconv.ParseInt(s, 10, 8) + *v = I1(v2) + return err +} + +type I2 int16 + +var _ SOAPValue = new(I2) + +func NewI2(v int16) *I2 { + v2 := I2(v) + return &v2 +} + +func (v *I2) String() string { + return strconv.FormatInt(int64(*v), 10) +} + +func (v *I2) Marshal() (string, error) { + return v.String(), nil +} + +func (v *I2) Unmarshal(s string) error { + v2, err := strconv.ParseInt(s, 10, 16) + *v = I2(v2) + return err +} + +type I4 int32 + +var _ SOAPValue = new(I4) + +func NewI4(v int32) *I4 { + v2 := I4(v) + return &v2 +} + +func (v *I4) String() string { + return strconv.FormatInt(int64(*v), 10) +} + +func (v *I4) Marshal() (string, error) { + return v.String(), nil +} + +func (v *I4) Unmarshal(s string) error { + v2, err := strconv.ParseInt(s, 10, 32) + *v = I4(v2) + return err +} + +type I8 int64 + +var _ SOAPValue = new(I8) + +func NewI8(v int64) *I8 { + v2 := I8(v) + return &v2 +} + +func (v *I8) String() string { + return strconv.FormatInt(int64(*v), 10) +} + +func (v *I8) Marshal() (string, error) { + return v.String(), nil +} + +func (v *I8) Unmarshal(s string) error { + v2, err := strconv.ParseInt(s, 10, 64) + *v = I8(v2) + return err +} + +type R4 float32 + +var _ SOAPValue = new(R4) + +func NewR4(v float32) *R4 { + v2 := R4(v) + return &v2 +} + +func (v *R4) String() string { + return strconv.FormatFloat(float64(*v), 'G', -1, 32) +} + +func (v *R4) Marshal() (string, error) { + return v.String(), nil +} + +func (v *R4) Unmarshal(s string) error { + v2, err := strconv.ParseFloat(s, 32) + *v = R4(v2) + return err +} + +type R8 float64 + +var _ SOAPValue = new(R8) + +func NewR8(v float64) *R8 { + v2 := R8(v) + return &v2 +} + +func (v *R8) String() string { + return strconv.FormatFloat(float64(*v), 'G', -1, 64) +} + +func (v *R8) Marshal() (string, error) { + return v.String(), nil +} + +func (v *R8) Unmarshal(s string) error { + v2, err := strconv.ParseFloat(s, 64) + *v = R8(v2) + return err +} + +// FloatFixed14_4 maps a float64 to the SOAP "fixed.14.4" type. +type FloatFixed14_4 float64 + +var _ SOAPValue = new(FloatFixed14_4) + +func NewFloatFixed14_4(v float64) *FloatFixed14_4 { + v2 := FloatFixed14_4(v) + return &v2 +} + +func (v *FloatFixed14_4) String() string { + return strconv.FormatFloat(float64(*v), 'f', 4, 64) +} + +func (v *FloatFixed14_4) Marshal() (string, error) { + if *v >= 1e14 || *v <= -1e14 { + return "", fmt.Errorf("soap fixed14.4: value %v out of bounds", v) + } + return v.String(), nil +} + +func (v *FloatFixed14_4) Unmarshal(s string) error { + v2, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + if v2 >= 1e14 || v2 <= -1e14 { + return fmt.Errorf("soap fixed14.4: value %q out of bounds", s) + } + *v = FloatFixed14_4(v2) + return nil +} + +// Char maps rune to SOAP "char" type. +type Char rune + +var _ SOAPValue = new(Char) + +func NewChar(v rune) *Char { + v2 := Char(v) + return &v2 +} + +func (v *Char) String() string { + return string(*v) +} + +func (v *Char) Marshal() (string, error) { + if *v == 0 { + return "", errors.New("soap char: rune 0 is not allowed") + } + return v.String(), nil +} + +func (v *Char) Unmarshal(s string) error { + if len(s) == 0 { + return errors.New("soap char: got empty string") + } + 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(v2) + return nil +} + +type String string + +var _ SOAPValue = new(String) + +func NewString(v string) *String { + v2 := String(v) + return &v2 +} + +func (v *String) Marshal() (string, error) { + return string(*v), nil +} + +func (v *String) Unmarshal(s string) error { + *v = String(s) + return 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) (TimeOfDay, error) { + var parts []string + for _, re := range timeRegexps { + parts = re.FindStringSubmatch(s) + if parts != nil { + break + } + } + 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. +// It contains non-timezone aware components. +type TimeOfDay struct { + Hour int8 + Minute int8 + Second int8 +} + +var _ SOAPValue = &TimeOfDay{} + +// Sets components based on duration since midnight. +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) + } + v.Hour = int8(d / time.Hour) + d = d % time.Hour + v.Minute = int8(d / time.Minute) + d = d % time.Minute + v.Second = int8(d / time.Second) + return nil +} + +// Returns duration since midnight. +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 (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 (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 +} + +func (v *TimeOfDay) Marshal() (string, error) { + if err := v.CheckValid(); err != nil { + return "", err + } + return v.String(), nil +} + +func (v *TimeOfDay) Unmarshal(s string) error { + var err error + *v, err = parseTimeParts(s) + if err != nil { + return err + } + + return v.CheckValid() +} + +// TimeOfDayTZ is used in cases where SOAP "time.tz" is used. +type TimeOfDayTZ struct { + // Components of the time of day. + TimeOfDay TimeOfDay + + // 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 (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) +} + +func (v *TimeOfDayTZ) Equal(o *TimeOfDayTZ) bool { + return v.TimeOfDay.Equal(&o.TimeOfDay) && + v.HasOffset == o.HasOffset && v.Offset == o.Offset +} + +func (v *TimeOfDayTZ) Marshal() (string, error) { + tod, err := v.TimeOfDay.Marshal() + if err != nil { + 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 +} + +// 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 +} + +func (v DateLocal) String() string { + return v.ToTime().String() +} + +func (v DateLocal) ToTime() time.Time { + return time.Time(v) +} + +func (v *DateLocal) Marshal() (string, error) { + return time.Time(*v).In(localLoc).Format("2006-01-02"), nil +} + +func (v *DateLocal) Unmarshal(s string) error { + year, month, day, err := parseDateParts(s) + if err != nil { + 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 +} + +type Boolean bool + +var _ SOAPValue = new(Boolean) + +func NewBoolean(v bool) *Boolean { + v2 := Boolean(v) + return &v2 +} + +func (v *Boolean) String() string { + if *v { + return "true" + } + return "false" +} + +func (v *Boolean) Marshal() (string, error) { + if *v { + return "1", nil + } + return "0", nil +} + +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", s) + } + return nil +} + +// BinBase64 maps []byte to SOAP "bin.base64" type. +type BinBase64 []byte + +var _ SOAPValue = new(BinBase64) + +func NewBinBase64(v []byte) *BinBase64 { + v2 := BinBase64(v) + return &v2 +} + +func (v *BinBase64) String() string { + return base64.StdEncoding.EncodeToString(*v) +} + +func (v *BinBase64) Marshal() (string, error) { + return v.String(), nil +} + +func (v *BinBase64) Unmarshal(s string) error { + v2, err := base64.StdEncoding.DecodeString(s) + *v = v2 + return err +} + +// BinHex maps []byte to SOAP "bin.hex" type. +type BinHex []byte + +var _ SOAPValue = new(BinHex) + +func NewBinHex(v []byte) *BinHex { + v2 := BinHex(v) + return &v2 +} + +func (v *BinHex) String() string { + return hex.EncodeToString(*v) +} + +func (v *BinHex) Marshal() (string, error) { + return v.String(), nil +} + +func (v *BinHex) Unmarshal(s string) error { + v2, err := hex.DecodeString(s) + *v = v2 + return err +} + +// URI maps *url.URL to SOAP "uri" type. +type URI url.URL + +var _ SOAPValue = new(URI) + +func (v *URI) String() string { + return v.ToURL().String() +} + +func (v *URI) ToURL() *url.URL { + return (*url.URL)(v) +} + +func (v *URI) Marshal() (string, error) { + return (*url.URL)(v).String(), nil +} + +func (v *URI) Unmarshal(s string) error { + v2, err := url.Parse(s) + if err != nil { + return err + } + *v = URI(*v2) + return nil +} diff --git a/v2/soap/types/types_test.go b/v2/soap/types/types_test.go new file mode 100644 index 0000000..7d6e979 --- /dev/null +++ b/v2/soap/types/types_test.go @@ -0,0 +1,424 @@ +package types + +import ( + "bytes" + "fmt" + "testing" + "time" +) + +type isEqual func(got, want SOAPValue) bool + +type typeTestCase struct { + makeValue func() SOAPValue + isEqual isEqual + marshalTests []marshalCase + marshalErrs []SOAPValue + unmarshalTests []unmarshalCase + unmarshalErrs []string +} + +type marshalCase struct { + input SOAPValue + want string +} + +type unmarshalCase struct { + input string + want SOAPValue +} + +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{ + { + makeValue: func() SOAPValue { return new(UI1) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*UI1) == *want.(*UI1) }, + marshalTests: []marshalCase{ + {NewUI1(0), "0"}, + {NewUI1(1), "1"}, + {NewUI1(255), "255"}, + }, + unmarshalErrs: append([]string{"-1", "256"}, badNumbers...), + }, + + { + makeValue: func() SOAPValue { return new(UI2) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*UI2) == *want.(*UI2) }, + marshalTests: []marshalCase{ + {NewUI2(0), "0"}, + {NewUI2(1), "1"}, + {NewUI2(65535), "65535"}, + }, + unmarshalErrs: append([]string{"-1", "65536"}, badNumbers...), + }, + + { + makeValue: func() SOAPValue { return new(UI4) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*UI4) == *want.(*UI4) }, + marshalTests: []marshalCase{ + {NewUI4(0), "0"}, + {NewUI4(1), "1"}, + {NewUI4(4294967295), "4294967295"}, + }, + unmarshalErrs: append([]string{"-1", "4294967296"}, badNumbers...), + }, + + { + makeValue: func() SOAPValue { return new(UI8) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*UI8) == *want.(*UI8) }, + marshalTests: []marshalCase{ + {NewUI8(0), "0"}, + {NewUI8(1), "1"}, + {NewUI8(18446744073709551615), "18446744073709551615"}, + }, + unmarshalErrs: append([]string{"-1", "18446744073709551616"}, badNumbers...), + }, + + { + makeValue: func() SOAPValue { return new(I1) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*I1) == *want.(*I1) }, + marshalTests: []marshalCase{ + {NewI1(0), "0"}, + {NewI1(1), "1"}, + {NewI1(-1), "-1"}, + {NewI1(127), "127"}, + {NewI1(-128), "-128"}, + }, + unmarshalErrs: append([]string{"-129", "128"}, badNumbers...), + }, + + { + makeValue: func() SOAPValue { return new(I2) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*I2) == *want.(*I2) }, + marshalTests: []marshalCase{ + {NewI2(0), "0"}, + {NewI2(1), "1"}, + {NewI2(-1), "-1"}, + {NewI2(32767), "32767"}, + {NewI2(-32768), "-32768"}, + }, + unmarshalErrs: append([]string{"-32769", "32768"}, badNumbers...), + }, + + { + makeValue: func() SOAPValue { return new(I4) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*I4) == *want.(*I4) }, + marshalTests: []marshalCase{ + {NewI4(0), "0"}, + {NewI4(1), "1"}, + {NewI4(-1), "-1"}, + {NewI4(2147483647), "2147483647"}, + {NewI4(-2147483648), "-2147483648"}, + }, + unmarshalErrs: append([]string{"-2147483649", "2147483648"}, badNumbers...), + }, + + { + makeValue: func() SOAPValue { return new(I8) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*I8) == *want.(*I8) }, + marshalTests: []marshalCase{ + {NewI8(0), "0"}, + {NewI8(1), "1"}, + {NewI8(-1), "-1"}, + {NewI8(9223372036854775807), "9223372036854775807"}, + {NewI8(-9223372036854775808), "-9223372036854775808"}, + }, + unmarshalErrs: append([]string{"-9223372036854775809", "9223372036854775808"}, badNumbers...), + }, + + { + makeValue: func() SOAPValue { return new(FloatFixed14_4) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*FloatFixed14_4) == *want.(*FloatFixed14_4) }, + marshalTests: []marshalCase{ + {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"}, + }, + marshalErrs: []SOAPValue{ + NewFloatFixed14_4(1e14), + NewFloatFixed14_4(-1e14), + }, + }, + + { + makeValue: func() SOAPValue { return new(Char) }, + isEqual: func(got, want SOAPValue) bool { return *got.(*Char) == *want.(*Char) }, + marshalTests: []marshalCase{ + {NewChar('a'), "a"}, + {NewChar('z'), "z"}, + {NewChar('\u1234'), "\u1234"}, + }, + marshalErrs: []SOAPValue{NewChar(0)}, + 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)) + }, + marshalTests: []marshalCase{ + {&TimeOfDay{}, "00:00:00"}, + // ISO8601 special case + {&TimeOfDay{Hour: 24}, "24:00:00"}, + }, + unmarshalTests: []unmarshalCase{ + {"000000", &TimeOfDay{}}, + }, + unmarshalErrs: []string{ + // Misformatted values: + "foo 01:02:03", "foo\n01:02:03", "01:02:03 foo", "01:02:03\nfoo", "01:02:03Z", + "01:02:03+01", "01:02:03+01:23", "01:02:03+0123", "01:02:03-01", "01:02:03-01:23", + "01:02:03-0123", + // Values out of range: + "24:01:00", + "24:00:01", + "25:00:00", + "00:60:00", + "00:00:60", + // Unexpected timezone component: + "01:02:03Z", + "01:02:03+01", + "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)) + }, + marshalTests: []marshalCase{ + {&TimeOfDayTZ{}, "00:00:00"}, + // ISO8601 special case + {&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{ + {"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: + "foo 01:02:03", "foo\n01:02:03", "01:02:03 foo", "01:02:03\nfoo", + // Values out of range: + "24:01:00", + "24:00:01", + "25:00:00", + "00:60:00", + "00:00:60", + }, + }, + + { + 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{ + // Unexpected time component. + "2013-10-08T10:30:50", + // Unexpected timezone component. + "2013-10-08+01", + }, + }, + + { + makeValue: func() SOAPValue { return new(DateTimeLocal) }, + isEqual: func(got, want SOAPValue) bool { + return got.(*DateTimeLocal).ToTime().Equal(want.(*DateTimeLocal).ToTime()) + }, + marshalTests: []marshalCase{ + {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", NewDateTimeLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc))}, + }, + unmarshalErrs: []string{ + // Unexpected timezone component. + "2013-10-08T10:30:50+01", + }, + }, + + { + makeValue: func() SOAPValue { return new(DateTimeTZLocal) }, + isEqual: func(got, want SOAPValue) bool { + return got.(*DateTimeTZLocal).ToTime().Equal(want.(*DateTimeTZLocal).ToTime()) + }, + marshalTests: []marshalCase{ + {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{ + {"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))))}, + }, + }, + + { + makeValue: func() SOAPValue { return new(Boolean) }, + isEqual: func(got, want SOAPValue) bool { + return *got.(*Boolean) == *want.(*Boolean) + }, + marshalTests: []marshalCase{ + {NewBoolean(true), "1"}, + {NewBoolean(false), "0"}, + }, + unmarshalTests: []unmarshalCase{ + {"true", NewBoolean(true)}, + {"false", NewBoolean(false)}, + {"yes", NewBoolean(true)}, + {"no", NewBoolean(false)}, + }, + unmarshalErrs: []string{"", "2", "-1"}, + }, + + { + makeValue: func() SOAPValue { return new(BinBase64) }, + isEqual: func(got, want SOAPValue) bool { + return bytes.Equal(*got.(*BinBase64), *want.(*BinBase64)) + }, + marshalTests: []marshalCase{ + {&BinBase64{}, ""}, + {NewBinBase64([]byte("a")), "YQ=="}, + {NewBinBase64([]byte("Longer String.")), "TG9uZ2VyIFN0cmluZy4="}, + {NewBinBase64([]byte("Longer Aligned.")), "TG9uZ2VyIEFsaWduZWQu"}, + }, + }, + + { + makeValue: func() SOAPValue { return new(BinHex) }, + isEqual: func(got, want SOAPValue) bool { + return bytes.Equal(*got.(*BinHex), *want.(*BinHex)) + }, + marshalTests: []marshalCase{ + {&BinHex{}, ""}, + {NewBinHex([]byte("a")), "61"}, + {NewBinHex([]byte("Longer String.")), "4c6f6e67657220537472696e672e"}, + }, + unmarshalTests: []unmarshalCase{ + {"4C6F6E67657220537472696E672E", NewBinHex([]byte("Longer String."))}, + }, + }, + + { + makeValue: func() SOAPValue { return new(URI) }, + isEqual: func(got, want SOAPValue) bool { + return got.(*URI).ToURL().String() == want.(*URI).ToURL().String() + }, + marshalTests: []marshalCase{ + {&URI{Scheme: "http", Host: "example.com", Path: "/path"}, "http://example.com/path"}, + }, + }, + } + + for _, tt := range typeTestCases { + tt := tt + + // Convert marshalTests into additional round trip equivalent unmarshalTests + for _, mt := range tt.marshalTests { + tt.unmarshalTests = append(tt.unmarshalTests, unmarshalCase{ + input: mt.want, + want: mt.input, + }) + } + + t.Run(fmt.Sprintf("%T", tt.makeValue()), func(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) { + got, err := mt.input.Marshal() + if err != nil { + t.Errorf("got unexpected error: %v", err) + } + if got != mt.want { + t.Errorf("got %q, want: %q", got, mt.want) + } + }) + } + for i, input := range tt.marshalErrs { + input := input + t.Run(fmt.Sprintf("marshalErr#%d_%v", i, input), func(t *testing.T) { + got, err := input.Marshal() + if err == nil { + t.Errorf("got %q, want error", got) + } + }) + } + for i, ut := range tt.unmarshalTests { + ut := ut + t.Run(fmt.Sprintf("unmarshalTest#%d_%q", i, ut.input), func(t *testing.T) { + got := tt.makeValue() + 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) + } + }) + } + for i, input := range tt.unmarshalErrs { + input := input + t.Run(fmt.Sprintf("unmarshalErrs#%d_%q", i, input), func(t *testing.T) { + got := tt.makeValue() + if err := got.Unmarshal(input); err == nil { + t.Errorf("got %v, want error", got) + } + }) + } + }) + } +}