functions/metadata: handle string resources

Some legacy events arrive with the resource field of the metadata as a
string value in the JSON, rather than the struct. Handle these appropriately
in a custom JSON unmarshaller.

Change-Id: I5634ee6be8ee80d73a93cd8d79b3a3adbe588716
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/48050
Reviewed-by: Tyler Bui-Palsulich <tbp@google.com>
diff --git a/functions/metadata/metadata.go b/functions/metadata/metadata.go
index a5dfd32..1a08f39 100644
--- a/functions/metadata/metadata.go
+++ b/functions/metadata/metadata.go
@@ -43,6 +43,38 @@
 	Name string `json:"name"`
 	// Type is the type of event.
 	Type string `json:"type"`
+	// Path is the path to the resource type (deprecated).
+	// This is the case for some deprecated GCS
+	// notifications, which populate the resource field as a string containing the topic
+	// rather than as the expected dictionary.
+	// See the Attributes section of https://cloud.google.com/storage/docs/pubsub-notifications
+	// for more details.
+	RawPath string `json:"-"`
+}
+
+// UnmarshalJSON specializes the Resource unmarshalling to handle the case where the
+// value is a string instead of a map. See the comment above on RawPath for why this
+// needs to be handled.
+func (r *Resource) UnmarshalJSON(data []byte) error {
+	// Try to unmarshal the resource into a string.
+	var path string
+	if err := json.Unmarshal(data, &path); err == nil {
+		r.RawPath = path
+		return nil
+	}
+
+	// Otherwise, accept whatever the result of the normal unmarshal would be.
+	// Need to define a new type, otherwise it infinitely recurses and panics.
+	type resource Resource
+	var res resource
+	if err := json.Unmarshal(data, &res); err != nil {
+		return err
+	}
+
+	r.Service = res.Service
+	r.Name = res.Name
+	r.Type = res.Type
+	return nil
 }
 
 type contextKey string
diff --git a/functions/metadata/metadata_test.go b/functions/metadata/metadata_test.go
index 04622ca..f6c77f4 100644
--- a/functions/metadata/metadata_test.go
+++ b/functions/metadata/metadata_test.go
@@ -16,8 +16,12 @@
 
 import (
 	"context"
+	"encoding/json"
 	"reflect"
 	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
 )
 
 func TestMetadata(t *testing.T) {
@@ -43,3 +47,76 @@
 		t.Errorf("FromContext got no error, wanted an error")
 	}
 }
+
+func TestUnmarshalJSON(t *testing.T) {
+	ts, err := time.Parse("2006-01-02T15:04:05Z07:00", "2019-11-04T23:01:10.112Z")
+	if err != nil {
+		t.Fatalf("Error parsing time: %v.", err)
+	}
+	var tests = []struct {
+		name string
+		data []byte
+		want Metadata
+	}{
+		{
+			name: "MetadataWithResource",
+			data: []byte(`{
+				"eventId": "1234567",
+				"timestamp": "2019-11-04T23:01:10.112Z",
+				"eventType": "google.pubsub.topic.publish",
+				"resource": {
+						"service": "pubsub.googleapis.com",
+						"name": "mytopic",
+						"type": "type.googleapis.com/google.pubsub.v1.PubsubMessage"
+				},
+				"data": {
+						"@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage",
+						"attributes": null,
+						"data": "test data"
+						}
+				}`),
+			want: Metadata{
+				EventID:   "1234567",
+				Timestamp: ts,
+				EventType: "google.pubsub.topic.publish",
+				Resource: &Resource{
+					Service: "pubsub.googleapis.com",
+					Name:    "mytopic",
+					Type:    "type.googleapis.com/google.pubsub.v1.PubsubMessage",
+				},
+			},
+		},
+		{
+			name: "MetadataWithString",
+			data: []byte(`{
+				"eventId": "1234567",
+				"timestamp": "2019-11-04T23:01:10.112Z",
+				"eventType": "google.pubsub.topic.publish",
+				"resource": "projects/myproject/mytopic",
+				"data": {
+						"@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage",
+						"attributes": null,
+						"data": "test data"
+						}
+				}`),
+			want: Metadata{
+				EventID:   "1234567",
+				Timestamp: ts,
+				EventType: "google.pubsub.topic.publish",
+				Resource: &Resource{
+					RawPath: "projects/myproject/mytopic",
+				},
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		var m Metadata
+		if err := json.Unmarshal(tc.data, &m); err != nil {
+			t.Errorf("UnmarshalJSON(%s) error: %v", tc.name, err)
+		}
+		if !cmp.Equal(m, tc.want) {
+			t.Errorf("UnmarshalJSON(%s) error: got %v, want %v", tc.name, m, tc.want)
+		}
+	}
+}