bigquery: produce better error messages for InferSchema

Fixes #1335.

Change-Id: I415dcd0749eb07fb754e5812aaf12d8a86576906
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/38630
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jean de Klerk <deklerk@google.com>
diff --git a/bigquery/params.go b/bigquery/params.go
index 868f2bb..5957022 100644
--- a/bigquery/params.go
+++ b/bigquery/params.go
@@ -44,7 +44,7 @@
 		return "", false, nil, err
 	}
 	if name != "" && !validFieldName.MatchString(name) {
-		return "", false, nil, errInvalidFieldName
+		return "", false, nil, invalidFieldNameError(name)
 	}
 	for _, opt := range opts {
 		if opt != nullableTagOption {
@@ -56,6 +56,12 @@
 	return name, keep, opts, nil
 }
 
+type invalidFieldNameError string
+
+func (e invalidFieldNameError) Error() string {
+	return fmt.Sprintf("bigquery: invalid name %q of field in struct", string(e))
+}
+
 var fieldCache = fields.NewCache(bqTagParser, nil, nil)
 
 var (
diff --git a/bigquery/schema.go b/bigquery/schema.go
index 3936aa4..a170c7b 100644
--- a/bigquery/schema.go
+++ b/bigquery/schema.go
@@ -137,12 +137,8 @@
 )
 
 var (
-	errNoStruct             = errors.New("bigquery: can only infer schema from struct or pointer to struct")
-	errUnsupportedFieldType = errors.New("bigquery: unsupported type of field in struct")
-	errInvalidFieldName     = errors.New("bigquery: invalid name of field in struct")
-	errBadNullable          = errors.New(`bigquery: use "nullable" only for []byte and struct pointers; for all other types, use a NullXXX type`)
-	errEmptyJSONSchema      = errors.New("bigquery: empty JSON schema")
-	fieldTypes              = map[FieldType]bool{
+	errEmptyJSONSchema = errors.New("bigquery: empty JSON schema")
+	fieldTypes         = map[FieldType]bool{
 		StringFieldType:    true,
 		BytesFieldType:     true,
 		IntegerFieldType:   true,
@@ -265,7 +261,7 @@
 	switch t.Kind() {
 	case reflect.Ptr:
 		if t.Elem().Kind() != reflect.Struct {
-			return nil, errNoStruct
+			return nil, noStructError{t}
 		}
 		t = t.Elem()
 		fallthrough
@@ -273,15 +269,15 @@
 	case reflect.Struct:
 		return inferFields(t)
 	default:
-		return nil, errNoStruct
+		return nil, noStructError{t}
 	}
 }
 
 // inferFieldSchema infers the FieldSchema for a Go type
-func inferFieldSchema(rt reflect.Type, nullable bool) (*FieldSchema, error) {
+func inferFieldSchema(fieldName string, rt reflect.Type, nullable bool) (*FieldSchema, error) {
 	// Only []byte and struct pointers can be tagged nullable.
 	if nullable && !(rt == typeOfByteSlice || rt.Kind() == reflect.Ptr && rt.Elem().Kind() == reflect.Struct) {
-		return nil, errBadNullable
+		return nil, badNullableError{fieldName, rt}
 	}
 	switch rt {
 	case typeOfByteSlice:
@@ -308,13 +304,13 @@
 		et := rt.Elem()
 		if et != typeOfByteSlice && (et.Kind() == reflect.Slice || et.Kind() == reflect.Array) {
 			// Multi dimensional slices/arrays are not supported by BigQuery
-			return nil, errUnsupportedFieldType
+			return nil, unsupportedFieldTypeError{fieldName, rt}
 		}
 		if nullableFieldType(et) != "" {
 			// Repeated nullable types are not supported by BigQuery.
-			return nil, errUnsupportedFieldType
+			return nil, unsupportedFieldTypeError{fieldName, rt}
 		}
-		f, err := inferFieldSchema(et, false)
+		f, err := inferFieldSchema(fieldName, et, false)
 		if err != nil {
 			return nil, err
 		}
@@ -323,7 +319,7 @@
 		return f, nil
 	case reflect.Ptr:
 		if rt.Elem().Kind() != reflect.Struct {
-			return nil, errUnsupportedFieldType
+			return nil, unsupportedFieldTypeError{fieldName, rt}
 		}
 		fallthrough
 	case reflect.Struct:
@@ -339,7 +335,7 @@
 	case reflect.Float32, reflect.Float64:
 		return &FieldSchema{Required: !nullable, Type: FloatFieldType}, nil
 	default:
-		return nil, errUnsupportedFieldType
+		return nil, unsupportedFieldTypeError{fieldName, rt}
 	}
 }
 
@@ -358,7 +354,7 @@
 				break
 			}
 		}
-		f, err := inferFieldSchema(field.Type, nullable)
+		f, err := inferFieldSchema(field.Name, field.Type, nullable)
 		if err != nil {
 			return nil, err
 		}
@@ -494,3 +490,29 @@
 
 	return convertSchemaFromJSON(bigQuerySchema)
 }
+
+type noStructError struct {
+	typ reflect.Type
+}
+
+func (e noStructError) Error() string {
+	return fmt.Sprintf("bigquery: can only infer schema from struct or pointer to struct, not %s", e.typ)
+}
+
+type badNullableError struct {
+	name string
+	typ  reflect.Type
+}
+
+func (e badNullableError) Error() string {
+	return fmt.Sprintf(`bigquery: field %q of type %s: use "nullable" only for []byte and struct pointers; for all other types, use a NullXXX type`, e.name, e.typ)
+}
+
+type unsupportedFieldTypeError struct {
+	name string
+	typ  reflect.Type
+}
+
+func (e unsupportedFieldTypeError) Error() string {
+	return fmt.Sprintf("bigquery: field %q: type %s is not supported", e.name, e.typ)
+}
diff --git a/bigquery/schema_test.go b/bigquery/schema_test.go
index 9eb0a39..ce4a750 100644
--- a/bigquery/schema_test.go
+++ b/bigquery/schema_test.go
@@ -714,52 +714,31 @@
 }
 
 func TestTagInferenceErrors(t *testing.T) {
-	testCases := []struct {
-		in  interface{}
-		err error
-	}{
-		{
-			in: struct {
-				LongTag int `bigquery:"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"`
-			}{},
-			err: errInvalidFieldName,
-		},
-		{
-			in: struct {
-				UnsupporedStartChar int `bigquery:"øab"`
-			}{},
-			err: errInvalidFieldName,
-		},
-		{
-			in: struct {
-				UnsupportedEndChar int `bigquery:"abø"`
-			}{},
-			err: errInvalidFieldName,
-		},
-		{
-			in: struct {
-				UnsupportedMiddleChar int `bigquery:"aøb"`
-			}{},
-			err: errInvalidFieldName,
-		},
-		{
-			in: struct {
-				StartInt int `bigquery:"1abc"`
-			}{},
-			err: errInvalidFieldName,
-		},
-		{
-			in: struct {
-				Hyphens int `bigquery:"a-b"`
-			}{},
-			err: errInvalidFieldName,
-		},
+	testCases := []interface{}{
+		struct {
+			LongTag int `bigquery:"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"`
+		}{},
+		struct {
+			UnsupporedStartChar int `bigquery:"øab"`
+		}{},
+		struct {
+			UnsupportedEndChar int `bigquery:"abø"`
+		}{},
+		struct {
+			UnsupportedMiddleChar int `bigquery:"aøb"`
+		}{},
+		struct {
+			StartInt int `bigquery:"1abc"`
+		}{},
+		struct {
+			Hyphens int `bigquery:"a-b"`
+		}{},
 	}
 	for i, tc := range testCases {
-		want := tc.err
-		_, got := InferSchema(tc.in)
-		if got != want {
-			t.Errorf("%d: inferring TableSchema: got:\n%#v\nwant:\n%#v", i, got, want)
+
+		_, got := InferSchema(tc)
+		if _, ok := got.(invalidFieldNameError); !ok {
+			t.Errorf("%d: inferring TableSchema: got:\n%#v\nwant invalidFieldNameError", i, got)
 		}
 	}
 
@@ -773,115 +752,114 @@
 
 func TestSchemaErrors(t *testing.T) {
 	testCases := []struct {
-		in  interface{}
-		err error
+		in   interface{}
+		want interface{}
 	}{
 		{
-			in:  []byte{},
-			err: errNoStruct,
+			in:   []byte{},
+			want: noStructError{},
 		},
 		{
-			in:  new(int),
-			err: errNoStruct,
+			in:   new(int),
+			want: noStructError{},
 		},
 		{
-			in:  struct{ Uint uint }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ Uint uint }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ Uint64 uint64 }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ Uint64 uint64 }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ Uintptr uintptr }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ Uintptr uintptr }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ Complex complex64 }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ Complex complex64 }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ Map map[string]int }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ Map map[string]int }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ Chan chan bool }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ Chan chan bool }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ Ptr *int }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ Ptr *int }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ Interface interface{} }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ Interface interface{} }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ MultiDimensional [][]int }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ MultiDimensional [][]int }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ MultiDimensional [][][]byte }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ MultiDimensional [][][]byte }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ SliceOfPointer []*int }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ SliceOfPointer []*int }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ SliceOfNull []NullInt64 }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ SliceOfNull []NullInt64 }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ ChanSlice []chan bool }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ ChanSlice []chan bool }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ NestedChan struct{ Chan []chan bool } }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ NestedChan struct{ Chan []chan bool } }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
 			in: struct {
 				X int `bigquery:",nullable"`
 			}{},
-			err: errBadNullable,
+			want: badNullableError{},
 		},
 		{
 			in: struct {
 				X bool `bigquery:",nullable"`
 			}{},
-			err: errBadNullable,
+			want: badNullableError{},
 		},
 		{
 			in: struct {
 				X struct{ N int } `bigquery:",nullable"`
 			}{},
-			err: errBadNullable,
+			want: badNullableError{},
 		},
 		{
 			in: struct {
 				X []int `bigquery:",nullable"`
 			}{},
-			err: errBadNullable,
+			want: badNullableError{},
 		},
 		{
-			in:  struct{ X *[]byte }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ X *[]byte }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ X *[]int }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ X *[]int }{},
+			want: unsupportedFieldTypeError{},
 		},
 		{
-			in:  struct{ X *int }{},
-			err: errUnsupportedFieldType,
+			in:   struct{ X *int }{},
+			want: unsupportedFieldTypeError{},
 		},
 	}
 	for _, tc := range testCases {
-		want := tc.err
 		_, got := InferSchema(tc.in)
-		if got != want {
-			t.Errorf("%#v: got:\n%#v\nwant:\n%#v", tc.in, got, want)
+		if reflect.TypeOf(got) != reflect.TypeOf(tc.want) {
+			t.Errorf("%#v: got:\n%#v\nwant type %T", tc.in, got, tc.want)
 		}
 	}
 }