Repository: cloudevents/sdk-go
Pull request #230 (2019-10-01 19:42:51)

Single point of parsing for Time
All time parsing calls types.ParseTime()

Return errors from time parsing

Added types.ConvertError error type for all conversion errors.

Improved error messages and tightened up some error handling.

n3wscott: LGTM!!

Looks like the commit has to be signed?
alanconway: Dammit, still haven't figured out how to make emacs do that for me. Should
be good now.

On Tue, Oct 1, 2019 at 6:48 PM Scott Nichols <notifications@github.com>
wrote:

> LGTM!!
>
> Looks like the commit has to be signed?
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/cloudevents/sdk-go/pull/230?email_source=notifications&email_token=AB3LUXU5ALJUHDEYYTNZPALQMPHSPA5CNFSM4I4N7JTKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEAC74UY#issuecomment-537263699>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AB3LUXRAHOJ6CLYOJVVHVTDQMPHSPANCNFSM4I4N7JTA>
> .
>


Patch:
diff --git a/pkg/cloudevents/eventcontext_v03.go b/pkg/cloudevents/eventcontext_v03.go
index 1ca8c37..2e714d1 100644
--- a/pkg/cloudevents/eventcontext_v03.go
+++ b/pkg/cloudevents/eventcontext_v03.go
@@ -83,9 +83,9 @@ func (ec *EventContextV03) SetExtension(name string, value interface{}) error {
 	if value == nil {
 		delete(ec.Extensions, name)
 	} else {
-		v, err := types.ValueOf(value)
+		v, err := types.Validate(value)
 		if err == nil {
-			ec.Extensions[name] = v.Interface()
+			ec.Extensions[name] = v
 		}
 		return err
 	}
diff --git a/pkg/cloudevents/transport/amqp/codec.go b/pkg/cloudevents/transport/amqp/codec.go
index e41d066..80949bf 100644
--- a/pkg/cloudevents/transport/amqp/codec.go
+++ b/pkg/cloudevents/transport/amqp/codec.go
@@ -197,10 +197,10 @@ func (c Codec) fromHeaders(h map[string]interface{}, event *cloudevents.Event) e
 	delete(h, prefix+"source")
 
 	if t, ok := h[prefix+"time"].(string); ok {
-		if timestamp := types.ParseTimestamp(t); timestamp != nil {
-			if err := ec.SetTime(timestamp.Time); err != nil {
-				return err
-			}
+		if timestamp, err := types.ParseTimestamp(t); err != nil {
+			return err
+		} else if err := ec.SetTime(timestamp.Time); err != nil {
+			return err
 		}
 	}
 	delete(h, prefix+"time")
diff --git a/pkg/cloudevents/transport/http/codec_v01.go b/pkg/cloudevents/transport/http/codec_v01.go
index cf680db..c3f4fa1 100644
--- a/pkg/cloudevents/transport/http/codec_v01.go
+++ b/pkg/cloudevents/transport/http/codec_v01.go
@@ -182,7 +182,11 @@ func (v CodecV01) fromHeaders(h http.Header) (cloudevents.EventContextV01, error
 	if source != nil {
 		ec.Source = *source
 	}
-	ec.EventTime = types.ParseTimestamp(h.Get("CE-EventTime"))
+	var err error
+	ec.EventTime, err = types.ParseTimestamp(h.Get("CE-EventTime"))
+	if err != nil {
+		return ec, err
+	}
 	h.Del("CE-EventTime")
 	etv := h.Get("CE-EventTypeVersion")
 	h.Del("CE-EventTypeVersion")
diff --git a/pkg/cloudevents/transport/http/codec_v02.go b/pkg/cloudevents/transport/http/codec_v02.go
index 8e7283b..70ed2fa 100644
--- a/pkg/cloudevents/transport/http/codec_v02.go
+++ b/pkg/cloudevents/transport/http/codec_v02.go
@@ -191,7 +191,11 @@ func (v CodecV02) fromHeaders(h http.Header) (cloudevents.EventContextV02, error
 	}
 	h.Del("ce-source")
 
-	ec.Time = types.ParseTimestamp(h.Get("ce-time"))
+	var err error
+	ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
+	if err != nil {
+		return ec, err
+	}
 	h.Del("ce-time")
 
 	ec.SchemaURL = types.ParseURLRef(h.Get("ce-schemaurl"))
diff --git a/pkg/cloudevents/transport/http/codec_v03.go b/pkg/cloudevents/transport/http/codec_v03.go
index f481205..4687c04 100644
--- a/pkg/cloudevents/transport/http/codec_v03.go
+++ b/pkg/cloudevents/transport/http/codec_v03.go
@@ -222,7 +222,11 @@ func (v CodecV03) fromHeaders(h http.Header) (cloudevents.EventContextV03, error
 	}
 	h.Del("ce-subject")
 
-	ec.Time = types.ParseTimestamp(h.Get("ce-time"))
+	var err error
+	ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
+	if err != nil {
+		return ec, err
+	}
 	h.Del("ce-time")
 
 	ec.SchemaURL = types.ParseURLRef(h.Get("ce-schemaurl"))
diff --git a/pkg/cloudevents/transport/http/codec_v1.go b/pkg/cloudevents/transport/http/codec_v1.go
index 8e52040..ed0e736 100644
--- a/pkg/cloudevents/transport/http/codec_v1.go
+++ b/pkg/cloudevents/transport/http/codec_v1.go
@@ -194,7 +194,11 @@ func (v CodecV1) fromHeaders(h http.Header) (cloudevents.EventContextV1, error)
 	}
 	h.Del("ce-subject")
 
-	ec.Time = types.ParseTimestamp(h.Get("ce-time"))
+	var err error
+	ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
+	if err != nil {
+		return ec, err
+	}
 	h.Del("ce-time")
 
 	ec.DataSchema = types.ParseURI(h.Get("ce-dataschema"))
diff --git a/pkg/cloudevents/transport/pubsub/codec.go b/pkg/cloudevents/transport/pubsub/codec.go
index 71e2526..ab9249c 100644
--- a/pkg/cloudevents/transport/pubsub/codec.go
+++ b/pkg/cloudevents/transport/pubsub/codec.go
@@ -187,10 +187,10 @@ func (c Codec) fromAttributes(a map[string]string, event *cloudevents.Event) err
 	delete(a, prefix+"source")
 
 	if t := a[prefix+"time"]; t != "" {
-		if timestamp := types.ParseTimestamp(t); timestamp != nil {
-			if err := ec.SetTime(timestamp.Time); err != nil {
-				return err
-			}
+		if timestamp, err := types.ParseTimestamp(t); err != nil {
+			return err
+		} else if err := ec.SetTime(timestamp.Time); err != nil {
+			return err
 		}
 	}
 	delete(a, prefix+"time")
diff --git a/pkg/cloudevents/types/doc.go b/pkg/cloudevents/types/doc.go
index f9ea711..0e13426 100644
--- a/pkg/cloudevents/types/doc.go
+++ b/pkg/cloudevents/types/doc.go
@@ -1,12 +1,14 @@
 /*
 Package types implements the CloudEvents type system.
 
-CloudEvents defines a small set of abstract types for all event context
-attribute values, including custom extension attributes. Each CloudEvents type
-has a corresponding native Go type and a canonical string encoding.
+CloudEvents defines a set of abstract types for event context attributes. Each
+type has a corresponding native Go type and a canonical string encoding.  The
+native Go types used to represent the CloudEvents types are:
+
+    bool, int32, string, []byte, *url.URL, time.Time
 
  +----------------+----------------+-----------------------------------+
- |CloudEvents Type|Native Type     |Convertible From Types             |
+ |CloudEvents Type|Native Type     |Convertible From                   |
  +================+================+===================================+
  |Bool            |bool            |bool                               |
  +----------------+----------------+-----------------------------------+
@@ -25,16 +27,16 @@ has a corresponding native Go type and a canonical string encoding.
  |Timestamp       |time.Time       |time.Time, types.Timestamp         |
  +----------------+----------------+-----------------------------------+
 
-The native types used to represent CloudEvents types are:
-
-    bool, int32, string, []byte, *url.URL, time.Time
+Extension attributes may be stored as a native type or a canonical string.  The
+To<Type> functions will convert to the desired <Type> from any convertible type
+or from the canonical string form.
 
-This package provides Parse... and Format... functions to convert to/from
-canonical strings. Use standard url.URL.String() and url.Parse() for URL
-values, the canonical string for a string is the string itself.
+The Parse<Type> and Format<Type> functions convert native types to/from
+canonical strings.
 
-It provides To... functions to convert any extension attribute value, in native
-or string form, to the expected type.
+Note are no Parse or Format functions for URL or string. For URL use the
+standard url.Parse() and url.URL.String(). The canonical string format of a
+string is the string itself.
 
 */
 package types
diff --git a/pkg/cloudevents/types/timestamp.go b/pkg/cloudevents/types/timestamp.go
index 6534aac..3ae1c7d 100644
--- a/pkg/cloudevents/types/timestamp.go
+++ b/pkg/cloudevents/types/timestamp.go
@@ -16,15 +16,12 @@ type Timestamp struct {
 }
 
 // ParseTimestamp attempts to parse the given time assuming RFC3339 layout
-func ParseTimestamp(t string) *Timestamp {
-	if t == "" {
-		return nil
+func ParseTimestamp(s string) (*Timestamp, error) {
+	if s == "" {
+		return nil, nil
 	}
-	timestamp, err := time.Parse(time.RFC3339Nano, t)
-	if err != nil {
-		return nil
-	}
-	return &Timestamp{Time: timestamp}
+	tt, err := ParseTime(s)
+	return &Timestamp{Time: tt}, err
 }
 
 // MarshalJSON implements a custom json marshal method used when this type is
@@ -33,8 +30,7 @@ func (t *Timestamp) MarshalJSON() ([]byte, error) {
 	if t == nil || t.IsZero() {
 		return []byte(`""`), nil
 	}
-	rfc3339 := fmt.Sprintf("%q", t.UTC().Format(time.RFC3339Nano))
-	return []byte(rfc3339), nil
+	return []byte(fmt.Sprintf("%q", t)), nil
 }
 
 // UnmarshalJSON implements the json unmarshal method used when this type is
@@ -44,10 +40,9 @@ func (t *Timestamp) UnmarshalJSON(b []byte) error {
 	if err := json.Unmarshal(b, &timestamp); err != nil {
 		return err
 	}
-	if pt := ParseTimestamp(timestamp); pt != nil {
-		*t = *pt
-	}
-	return nil
+	var err error
+	t.Time, err = ParseTime(timestamp)
+	return err
 }
 
 // MarshalXML implements a custom xml marshal method used when this type is
@@ -56,8 +51,7 @@ func (t *Timestamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
 	if t == nil || t.IsZero() {
 		return e.EncodeElement(nil, start)
 	}
-	v := t.UTC().Format(time.RFC3339Nano)
-	return e.EncodeElement(v, start)
+	return e.EncodeElement(t.String(), start)
 }
 
 // UnmarshalXML implements the xml unmarshal method used when this type is
@@ -67,17 +61,10 @@ func (t *Timestamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
 	if err := d.DecodeElement(&timestamp, &start); err != nil {
 		return err
 	}
-	if pt := ParseTimestamp(timestamp); pt != nil {
-		*t = *pt
-	}
-	return nil
+	var err error
+	t.Time, err = ParseTime(timestamp)
+	return err
 }
 
-// String outputs the time using layout RFC3339.
-func (t *Timestamp) String() string {
-	if t == nil {
-		return time.Time{}.UTC().Format(time.RFC3339Nano)
-	}
-
-	return t.UTC().Format(time.RFC3339Nano)
-}
+// String outputs the time using RFC3339 format.
+func (t Timestamp) String() string { return FormatTime(t.Time) }
diff --git a/pkg/cloudevents/types/value.go b/pkg/cloudevents/types/value.go
index fa6e336..803be2f 100644
--- a/pkg/cloudevents/types/value.go
+++ b/pkg/cloudevents/types/value.go
@@ -42,15 +42,24 @@ func ParseInteger(v string) (int32, error) {
 func ParseBinary(v string) ([]byte, error) { return base64.StdEncoding.DecodeString(v) }
 
 // ParseTime parse canonical string format: RFC3339 with nanoseconds
-func ParseTime(v string) (time.Time, error) { return time.Parse(time.RFC3339Nano, v) }
+func ParseTime(v string) (time.Time, error) {
+	t, err := time.Parse(time.RFC3339Nano, v)
+	if err != nil {
+		err := convertErr(time.Time{}, v)
+		err.extra = ": not in RFC3339 format"
+		return time.Time{}, err
+	}
+	return t, nil
+}
 
 // Format returns the canonical string format of v, where v can be
 // any type that is convertible to a CloudEvents type.
 func Format(v interface{}) (string, error) {
 	v, err := Validate(v)
-	switch v := v.(type) {
-	case nil:
+	if err != nil {
 		return "", err
+	}
+	switch v := v.(type) {
 	case bool:
 		return FormatBool(v), nil
 	case int32:
@@ -125,26 +134,32 @@ func Validate(v interface{}) (interface{}, error) {
 // ToBool accepts a bool value or canonical "true"/"false" string.
 func ToBool(v interface{}) (bool, error) {
 	v, err := Validate(v)
+	if err != nil {
+		return false, err
+	}
 	switch v := v.(type) {
 	case bool:
 		return v, nil
 	case string:
 		return ParseBool(v)
 	default:
-		return false, convertErr("Bool", v, err)
+		return false, convertErr(true, v)
 	}
 }
 
 // ToInteger accepts any numeric value in int32 range, or canonical string.
 func ToInteger(v interface{}) (int32, error) {
 	v, err := Validate(v)
+	if err != nil {
+		return 0, err
+	}
 	switch v := v.(type) {
 	case int32:
 		return v, nil
 	case string:
 		return ParseInteger(v)
 	default:
-		return 0, convertErr("Integer", v, err)
+		return 0, convertErr(int32(0), v)
 	}
 }
 
@@ -154,30 +169,39 @@ func ToInteger(v interface{}) (int32, error) {
 // Format functions for that.
 func ToString(v interface{}) (string, error) {
 	v, err := Validate(v)
+	if err != nil {
+		return "", err
+	}
 	switch v := v.(type) {
 	case string:
 		return v, nil
 	default:
-		return "", convertErr("String", v, err)
+		return "", convertErr("", v)
 	}
 }
 
 // ToBinary returns a []byte value, decoding from base64 string if necessary.
 func ToBinary(v interface{}) ([]byte, error) {
 	v, err := Validate(v)
+	if err != nil {
+		return nil, err
+	}
 	switch v := v.(type) {
 	case []byte:
 		return v, nil
 	case string:
 		return base64.StdEncoding.DecodeString(v)
 	default:
-		return nil, convertErr("Binary", v, err)
+		return nil, convertErr([]byte(nil), v)
 	}
 }
 
 // ToURL returns a *url.URL value, parsing from string if necessary.
 func ToURL(v interface{}) (*url.URL, error) {
 	v, err := Validate(v)
+	if err != nil {
+		return nil, err
+	}
 	switch v := v.(type) {
 	case *url.URL:
 		return v, nil
@@ -188,13 +212,16 @@ func ToURL(v interface{}) (*url.URL, error) {
 		}
 		return u, nil
 	default:
-		return nil, convertErr("URL", v, err)
+		return nil, convertErr((*url.URL)(nil), v)
 	}
 }
 
 // ToTime returns a time.Time value, parsing from RFC3339 string if necessary.
 func ToTime(v interface{}) (time.Time, error) {
 	v, err := Validate(v)
+	if err != nil {
+		return time.Time{}, err
+	}
 	switch v := v.(type) {
 	case time.Time:
 		return v, nil
@@ -205,17 +232,29 @@ func ToTime(v interface{}) (time.Time, error) {
 		}
 		return ts, nil
 	default:
-		return time.Time{}, convertErr("Time", v, err)
+		return time.Time{}, convertErr(time.Time{}, v)
 	}
 }
 
-func convertErr(name string, v interface{}, err error) error {
-	if err != nil {
-		return err
-	}
-	return fmt.Errorf("cannot convert %T to %s", v, name)
+type ConvertErr struct {
+	// Value being converted
+	Value interface{}
+	// Type of attempted conversion
+	Type reflect.Type
+
+	extra string
+}
+
+func (e *ConvertErr) Error() string {
+	return fmt.Sprintf("cannot convert %#v to %s%s", e.Value, e.Type, e.extra)
+}
+
+func convertErr(target, v interface{}) *ConvertErr {
+	return &ConvertErr{Value: v, Type: reflect.TypeOf(target)}
 }
 
 func rangeErr(v interface{}) error {
-	return fmt.Errorf("%v is out of range for Integer", v)
+	e := convertErr(int32(0), v)
+	e.extra = ": out of range"
+	return e
 }


Test patch:
diff --git a/pkg/cloudevents/event_test.go b/pkg/cloudevents/event_test.go
index 6cd4198..0151ea0 100644
--- a/pkg/cloudevents/event_test.go
+++ b/pkg/cloudevents/event_test.go
@@ -782,7 +782,7 @@ func TestExtensions(t *testing.T) {
 			},
 			extension:    "anothertest",
 			wantError:    true,
-			wantErrorMsg: "cannot convert int32 to String",
+			wantErrorMsg: "cannot convert 1 to string",
 		},
 		"full v02, test extension": {
 			event: ce.Event{
@@ -797,7 +797,7 @@ func TestExtensions(t *testing.T) {
 			},
 			extension:    "anothertest",
 			wantError:    true,
-			wantErrorMsg: "cannot convert int32 to String",
+			wantErrorMsg: "cannot convert 1 to string",
 		},
 		"full v03, test extension": {
 			event: ce.Event{
@@ -812,7 +812,7 @@ func TestExtensions(t *testing.T) {
 			},
 			extension:    "anothertest",
 			wantError:    true,
-			wantErrorMsg: "cannot convert int32 to String",
+			wantErrorMsg: "cannot convert 1 to string",
 		},
 		"full v1, test extension": {
 			event: ce.Event{
@@ -827,7 +827,7 @@ func TestExtensions(t *testing.T) {
 			},
 			extension:    "anothertest",
 			wantError:    true,
-			wantErrorMsg: "cannot convert int32 to String",
+			wantErrorMsg: "cannot convert 1 to string",
 		},
 	}
 	for n, tc := range testCases {
diff --git a/pkg/cloudevents/types/timestamp_test.go b/pkg/cloudevents/types/timestamp_test.go
index 669eeb5..8c5ae4e 100644
--- a/pkg/cloudevents/types/timestamp_test.go
+++ b/pkg/cloudevents/types/timestamp_test.go
@@ -1,294 +1,82 @@
 package types_test
 
 import (
+	"encoding/json"
 	"encoding/xml"
-	"strings"
+	"fmt"
 	"testing"
 	"time"
 
 	"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
-	"github.com/google/go-cmp/cmp"
+	"github.com/stretchr/testify/assert"
 )
 
-func TestParseTimestamp(t *testing.T) {
-	testCases := map[string]struct {
-		t    string
-		want *types.Timestamp
-	}{
-		"empty": {
-			want: nil,
-		},
-		"empty string": {
-			t:    "",
-			want: nil,
-		},
-		"invalid format": {
-			t:    "2019-02-28",
-			want: nil,
-		},
-		"RFC3339 format": {
-			t: "1984-02-28T15:04:05Z",
-			want: func() *types.Timestamp {
-				t, _ := time.Parse(time.RFC3339, "1984-02-28T15:04:05Z")
-				return &types.Timestamp{Time: t}
-			}(),
-		},
-		"RFC3339Nano format": {
-			t: "1984-02-28T15:04:05.999999999Z",
-			want: func() *types.Timestamp {
-				t, _ := time.Parse(time.RFC3339Nano, "1984-02-28T15:04:05.999999999Z")
-				return &types.Timestamp{Time: t}
-			}(),
-		},
+func TestTimestampParseString(t *testing.T) {
+	ok := func(s string, want time.Time) {
+		t.Helper()
+		got, err := types.ParseTimestamp(s)
+		assert.NoError(t, err)
+		assert.Equal(t, want, got.Time)
+		assert.Equal(t, s, got.String())
 	}
-	for n, tc := range testCases {
-		tc := tc // Don't use range variable in func literal.
-		t.Run(n, func(t *testing.T) {
+	ok("1984-02-28T15:04:05Z", time.Date(1984, 02, 28, 15, 04, 05, 0, time.UTC))
+	ok("1984-02-28T15:04:05.999999999Z", time.Date(1984, 02, 28, 15, 04, 05, 999999999, time.UTC))
 
-			got := types.ParseTimestamp(tc.t)
-
-			if diff := cmp.Diff(tc.want, got); diff != "" {
-				t.Errorf("unexpected object (-want, +got) = %v", diff)
-			}
-		})
-	}
-}
-
-func TestJsonMarshalTimestamp(t *testing.T) {
-	testCases := map[string]struct {
-		t    string
-		want []byte
-	}{
-		"empty": {
-			want: []byte(`""`),
-		},
-		"empty string": {
-			t:    "",
-			want: []byte(`""`),
-		},
-		"invalid format": {
-			t:    "2019-02-28",
-			want: []byte(`""`),
-		},
-		"RFC3339 format": {
-			t:    "1984-02-28T15:04:05Z",
-			want: []byte(`"1984-02-28T15:04:05Z"`),
-		},
-		"RFC3339Nano format": {
-			t:    "1984-02-28T15:04:05.999999999Z",
-			want: []byte(`"1984-02-28T15:04:05.999999999Z"`),
-		},
-	}
-	for n, tc := range testCases {
-		tc := tc // Don't use range variable in func literal.
-		t.Run(n, func(t *testing.T) {
-
-			tt := types.ParseTimestamp(tc.t)
-			got, _ := tt.MarshalJSON()
-
-			if diff := cmp.Diff(tc.want, got); diff != "" {
-				t.Logf("got: %s", string(got))
-				t.Errorf("unexpected object (-want, +got) = %v", diff)
-			}
-		})
-	}
-}
-
-func TestXMLMarshalTimestamp(t *testing.T) {
-	testCases := map[string]struct {
-		t       string
-		want    []byte
-		wantErr string
-	}{
-		"empty": {
-			want: []byte(nil),
-		},
-		"empty string": {
-			t:    "",
-			want: []byte(nil),
-		},
-		"invalid format": {
-			t:    "2019-02-28",
-			want: []byte(nil),
-		},
-		"RFC3339 format": {
-			t:    "1984-02-28T15:04:05Z",
-			want: []byte(`<Timestamp>1984-02-28T15:04:05Z</Timestamp>`),
-		},
-		"RFC3339Nano format": {
-			t:    "1984-02-28T15:04:05.999999999Z",
-			want: []byte(`<Timestamp>1984-02-28T15:04:05.999999999Z</Timestamp>`),
-		},
-	}
-	for n, tc := range testCases {
-		tc := tc // Don't use range variable in func literal.
-		t.Run(n, func(t *testing.T) {
-
-			var got []byte
-			var err error
-			tt := types.ParseTimestamp(tc.t)
-
-			got, err = xml.Marshal(tt)
-
-			if tc.wantErr != "" {
-				if err != nil {
-					gotErr := err.Error()
-					if !strings.Contains(gotErr, tc.wantErr) {
-						t.Errorf("unexpected error, expected to contain %q, got: %q", tc.wantErr, gotErr)
-					}
-				} else {
-					t.Errorf("expected error to contain %q, got: nil", tc.wantErr)
-				}
-				return
-			}
-
-			if diff := cmp.Diff(tc.want, got); diff != "" {
-				t.Logf("got: %s", string(got))
-				t.Errorf("unexpected object (-want, +got) = %v", diff)
-			}
-		})
-	}
-}
-
-func TestJsonUnmarshalTimestamp(t *testing.T) {
-	testCases := map[string]struct {
-		b       []byte
-		want    *types.Timestamp
-		wantErr string
-	}{
-		"empty": {
-			wantErr: "unexpected end of JSON input",
-		},
-		"invalid format": {
-			b:       []byte("2019-02-28"),
-			wantErr: "invalid character '-' after top-level value",
-		},
-		"RFC3339 format": {
-			b: []byte(`"1984-02-28T15:04:05Z"`),
-			want: func() *types.Timestamp {
-				t, _ := time.Parse(time.RFC3339, "1984-02-28T15:04:05Z")
-				return &types.Timestamp{Time: t}
-			}(),
-		},
-		"RFC3339Nano format": {
-			b: []byte(`"1984-02-28T15:04:05.999999999Z"`),
-			want: func() *types.Timestamp {
-				t, _ := time.Parse(time.RFC3339Nano, "1984-02-28T15:04:05.999999999Z")
-				return &types.Timestamp{Time: t}
-			}(),
-		},
-	}
-	for n, tc := range testCases {
-		tc := tc // Don't use range variable in func literal.
-		t.Run(n, func(t *testing.T) {
-
-			got := &types.Timestamp{}
-			err := got.UnmarshalJSON(tc.b)
-
-			if tc.wantErr != "" || err != nil {
-				var gotErr string
-				if err != nil {
-					gotErr = err.Error()
-				}
-				if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" {
-					t.Errorf("unexpected error (-want, +got) = %v", diff)
-				}
-				return
-			}
-
-			if diff := cmp.Diff(tc.want, got); diff != "" {
-				t.Errorf("unexpected object (-want, +got) = %v", diff)
-			}
-		})
+	bad := func(s, wanterr string) {
+		t.Helper()
+		_, err := types.ParseTime(s)
+		assert.EqualError(t, err, wanterr)
 	}
+	bad("", "cannot convert \"\" to time.Time: not in RFC3339 format")
+	bad("2019-02-28", "cannot convert \"2019-02-28\" to time.Time: not in RFC3339 format")
 }
 
-func TestXMLUnmarshalTimestamp(t *testing.T) {
-	testCases := map[string]struct {
-		b       []byte
-		want    *types.Timestamp
-		wantErr string
-	}{
-		"empty": {
-			wantErr: "EOF",
-		},
-		"invalid format": {
-			b:    []byte("<Timestamp>2019-02-28</Timestamp>"),
-			want: &types.Timestamp{},
-		},
-		"nil": {
-			b:    []byte("<Timestamp></Timestamp>"),
-			want: &types.Timestamp{},
-		},
-		"RFC3339 format": {
-			b: []byte(`<Timestamp>1984-02-28T15:04:05Z</Timestamp>`),
-			want: func() *types.Timestamp {
-				t, _ := time.Parse(time.RFC3339, "1984-02-28T15:04:05Z")
-				return &types.Timestamp{Time: t}
-			}(),
-		},
-		"RFC3339Nano format": {
-			b: []byte(`<Timestamp>1984-02-28T15:04:05.999999999Z</Timestamp>`),
-			want: func() *types.Timestamp {
-				t, _ := time.Parse(time.RFC3339Nano, "1984-02-28T15:04:05.999999999Z")
-				return &types.Timestamp{Time: t}
-			}(),
-		},
+func TestJsonMarshalUnmarshalTimestamp(t *testing.T) {
+	ok := func(ts string) {
+		t.Helper()
+		tt, err := types.ParseTime(ts)
+		assert.NoError(t, err)
+		got, err := json.Marshal(types.Timestamp{Time: tt})
+		assert.NoError(t, err)
+		assert.Equal(t, fmt.Sprintf(`"%s"`, ts), string(got))
 	}
-	for n, tc := range testCases {
-		tc := tc // Don't use range variable in func literal.
-		t.Run(n, func(t *testing.T) {
-
-			got := &types.Timestamp{}
-
-			err := xml.Unmarshal(tc.b, got)
-
-			if tc.wantErr != "" || err != nil {
-				var gotErr string
-				if err != nil {
-					gotErr = err.Error()
-				}
-				if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" {
-					t.Errorf("unexpected error (-want, +got) = %v", diff)
-				}
-				return
-			}
-
-			if diff := cmp.Diff(tc.want, got); diff != "" {
-				t.Errorf("unexpected object (-want, +got) = %v", diff)
-			}
-		})
+	ok("1984-02-28T15:04:05Z")
+	ok("1984-02-28T15:04:05.999999999Z")
+
+	bad := func(s, wanterr string) {
+		t.Helper()
+		var ts types.Timestamp
+		err := json.Unmarshal([]byte(s), &ts)
+		assert.EqualError(t, err, wanterr)
 	}
+	bad("", "unexpected end of JSON input")
+	bad("2019-02-28", "invalid character '-' after top-level value")
 }
 
-func TestTimestampString(t *testing.T) {
-	testCases := map[string]struct {
-		t    string
-		want string
-	}{
-		"empty": {
-			want: "0001-01-01T00:00:00Z",
-		},
-		"RFC3339 format": {
-			t:    "1984-02-28T15:04:05Z",
-			want: `1984-02-28T15:04:05Z`,
-		},
-		"RFC3339Nano format": {
-			t:    "1984-02-28T15:04:05.999999999Z",
-			want: `1984-02-28T15:04:05.999999999Z`,
-		},
+func TestXMLMarshalUnmarshalTimestamp(t *testing.T) {
+	ok := func(tstr string) {
+		t.Helper()
+		tt, err := types.ParseTime(tstr)
+		assert.NoError(t, err)
+		got, err := xml.Marshal(types.Timestamp{Time: tt})
+		assert.NoError(t, err)
+		assert.Equal(t, fmt.Sprintf("<Timestamp>%s</Timestamp>", tstr), string(got))
+		var ts types.Timestamp
+		err = xml.Unmarshal(got, &ts)
+		assert.NoError(t, err)
+		assert.Equal(t, tt, ts.Time)
 	}
-	for n, tc := range testCases {
-		tc := tc // Don't use range variable in func literal.
-		t.Run(n, func(t *testing.T) {
-
-			tt := types.ParseTimestamp(tc.t)
-			got := tt.String()
-
-			if diff := cmp.Diff(tc.want, got); diff != "" {
-				t.Logf("got: %s", got)
-				t.Errorf("unexpected string (-want, +got) = %v", diff)
-			}
-		})
+	ok("1984-02-28T15:04:05Z")
+	ok("1984-02-28T15:04:05.999999999Z")
+
+	bad := func(s, wanterr string) {
+		t.Helper()
+		var ts types.Timestamp
+		err := xml.Unmarshal([]byte(s), &ts)
+		assert.EqualError(t, err, wanterr)
 	}
+	bad("", "EOF")
+	bad("2019-02-28", "EOF")
+	bad("<Timestamp>2019-02-28</Timestamp>", "cannot convert \"2019-02-28\" to time.Time: not in RFC3339 format")
+	bad("<Timestamp></Timestamp>", "cannot convert \"\" to time.Time: not in RFC3339 format")
 }
diff --git a/pkg/cloudevents/types/value_test.go b/pkg/cloudevents/types/value_test.go
index 668911e..0ef7289 100644
--- a/pkg/cloudevents/types/value_test.go
+++ b/pkg/cloudevents/types/value_test.go
@@ -41,7 +41,7 @@ func Example() {
 	// 123 <nil>
 	// 456 <nil>
 	// 99999 <nil>
-	// 0 2147483648 is out of range for Integer
+	// 0 cannot convert 2147483648 to int32: out of range
 	// 0 strconv.ParseFloat: parsing "not an int": invalid syntax
 }
 
@@ -106,7 +106,7 @@ func TestBool(t *testing.T) {
 	x.ok(false, false, "false")
 
 	x.err("notabool", "strconv.ParseBool: parsing \"notabool\": invalid syntax")
-	x.err(0, "cannot convert int32 to Bool")
+	x.err(0, "cannot convert 0 to bool")
 	x.err(nil, "invalid CloudEvents value: <nil>")
 }
 
@@ -141,18 +141,18 @@ func TestInteger(t *testing.T) {
 	x.str(".9", int32(0))
 	x.str("-.9", int32(0))
 
-	x.err(math.MaxInt32+1, "2147483648 is out of range for Integer")
-	x.err(uint32(math.MaxInt32+1), "2147483648 is out of range for Integer")
-	x.err(int64(math.MaxInt32+1), "2147483648 is out of range for Integer")
-	x.err(int64(math.MinInt32-1), "-2147483649 is out of range for Integer")
-	x.err(float64(math.MinInt32-1), "-2.147483649e+09 is out of range for Integer")
-	x.err(float64(math.MaxInt32+1), "2.147483648e+09 is out of range for Integer")
+	x.err(math.MaxInt32+1, "cannot convert 2147483648 to int32: out of range")
+	x.err(uint32(math.MaxInt32+1), "cannot convert 0x80000000 to int32: out of range")
+	x.err(int64(math.MaxInt32+1), "cannot convert 2147483648 to int32: out of range")
+	x.err(int64(math.MinInt32-1), "cannot convert -2147483649 to int32: out of range")
+	x.err(float64(math.MinInt32-1), "cannot convert -2.147483649e+09 to int32: out of range")
+	x.err(float64(math.MaxInt32+1), "cannot convert 2.147483648e+09 to int32: out of range")
 	// Float32 doesn't keep all the bits of an int32 so we need to exaggerate fof range error.
-	x.err(float64(2*math.MinInt32), "-4.294967296e+09 is out of range for Integer")
-	x.err(float64(-2*math.MaxInt32), "-4.294967294e+09 is out of range for Integer")
+	x.err(float64(2*math.MinInt32), "cannot convert -4.294967296e+09 to int32: out of range")
+	x.err(float64(-2*math.MaxInt32), "cannot convert -4.294967294e+09 to int32: out of range")
 
 	x.err("X", "strconv.ParseFloat: parsing \"X\": invalid syntax")
-	x.err(true, "cannot convert bool to Integer")
+	x.err(true, "cannot convert true to int32")
 	x.err(nil, "invalid CloudEvents value: <nil>")
 }
 
@@ -206,7 +206,7 @@ func TestTime(t *testing.T) {
 	x.str(timeStr, someTime)
 
 	x.err(nil, "invalid CloudEvents value: <nil>")
-	x.err(5, "cannot convert int32 to Time")
+	x.err(5, "cannot convert 5 to time.Time")
 	x.err((*time.Time)(nil), "invalid CloudEvents value: (*time.Time)(nil)")
 	x.err((*types.Timestamp)(nil), "invalid CloudEvents value: (*types.Timestamp)(nil)")
 	x.err("not a time", "parsing time \"not a time\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"not a time\" as \"2006\"")