From c68ed0e77640d1afdd04ba8c58714791fab1255f Mon Sep 17 00:00:00 2001
From: dan-j <5727701+dan-j@users.noreply.github.com>
Date: Fri, 19 Jan 2024 23:23:12 +0000
Subject: [PATCH] Support content types following structured syntax suffixes
This enables the encoding/decoding of custom content types which
use the `+json` or `+xml` suffix to indicate that their encoding
is JSON or XML respectively. Users can also add their own suffixes
to the registry in a similar fashion to how `datacodec.AddDecoder()`
and `dataodec.AddEncoder()` work, but via the `AddStructuredSuffixDecoder`
and `AddStructuredSuffixEncoder` functions.
Signed-off-by: dan-j <5727701+dan-j@users.noreply.github.com>
---
v2/event/datacodec/codec.go | 59 +++++++++++++++
v2/event/datacodec/codec_test.go | 123 +++++++++++++++++++++++++++----
2 files changed, 168 insertions(+), 14 deletions(-)
diff --git a/v2/event/datacodec/codec.go b/v2/event/datacodec/codec.go
index 3e077740b..6f5d1f4c5 100644
--- a/v2/event/datacodec/codec.go
+++ b/v2/event/datacodec/codec.go
@@ -8,6 +8,7 @@ package datacodec
import (
"context"
"fmt"
+ "strings"
"github.com/cloudevents/sdk-go/v2/event/datacodec/json"
"github.com/cloudevents/sdk-go/v2/event/datacodec/text"
@@ -26,9 +27,20 @@ type Encoder func(ctx context.Context, in interface{}) ([]byte, error)
var decoder map[string]Decoder
var encoder map[string]Encoder
+// ssDecoder is a map of content-type structured suffixes as defined in
+// [Structured Syntax Suffixes](https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml),
+// which may be used to match content types such as application/vnd.custom-app+json
+var ssDecoder map[string]Decoder
+
+// ssEncoder is a map of content-type structured suffixes similar to ssDecoder.
+var ssEncoder map[string]Encoder
+
func init() {
decoder = make(map[string]Decoder, 10)
+ ssDecoder = make(map[string]Decoder, 10)
+
encoder = make(map[string]Encoder, 10)
+ ssEncoder = make(map[string]Encoder, 10)
AddDecoder("", json.Decode)
AddDecoder("application/json", json.Decode)
@@ -37,12 +49,18 @@ func init() {
AddDecoder("text/xml", xml.Decode)
AddDecoder("text/plain", text.Decode)
+ AddStructuredSuffixDecoder("json", json.Decode)
+ AddStructuredSuffixDecoder("xml", xml.Decode)
+
AddEncoder("", json.Encode)
AddEncoder("application/json", json.Encode)
AddEncoder("text/json", json.Encode)
AddEncoder("application/xml", xml.Encode)
AddEncoder("text/xml", xml.Encode)
AddEncoder("text/plain", text.Encode)
+
+ AddStructuredSuffixEncoder("json", json.Encode)
+ AddStructuredSuffixEncoder("xml", xml.Encode)
}
// AddDecoder registers a decoder for a given content type. The codecs will use
@@ -51,12 +69,34 @@ func AddDecoder(contentType string, fn Decoder) {
decoder[contentType] = fn
}
+// AddStructuredSuffixDecoder registers a decoder for content-types which match the given structured
+// syntax suffix as defined by
+// [Structured Syntax Suffixes](https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml).
+// This allows users to register custom decoders for non-standard content types which follow the
+// structured syntax suffix standard (e.g. application/vnd.custom-app+json).
+//
+// Suffix should not include the "+" character, and "json" and "xml" are registered by default.
+func AddStructuredSuffixDecoder(suffix string, fn Decoder) {
+ ssDecoder[suffix] = fn
+}
+
// AddEncoder registers an encoder for a given content type. The codecs will
// use these to encode the data payload for a cloudevent.Event object.
func AddEncoder(contentType string, fn Encoder) {
encoder[contentType] = fn
}
+// AddStructuredSuffixEncoder registers an encoder for content-types which match the given
+// structured syntax suffix as defined by
+// [Structured Syntax Suffixes](https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml).
+// This allows users to register custom encoders for non-standard content types which follow the
+// structured syntax suffix standard (e.g. application/vnd.custom-app+json).
+//
+// Suffix should not include the "+" character, and "json" and "xml" are registered by default.
+func AddStructuredSuffixEncoder(suffix string, fn Encoder) {
+ ssEncoder[suffix] = fn
+}
+
// Decode looks up and invokes the decoder registered for the given content
// type. An error is returned if no decoder is registered for the given
// content type.
@@ -64,6 +104,11 @@ func Decode(ctx context.Context, contentType string, in []byte, out interface{})
if fn, ok := decoder[contentType]; ok {
return fn(ctx, in, out)
}
+
+ if fn, ok := ssDecoder[structuredSuffix(contentType)]; ok {
+ return fn(ctx, in, out)
+ }
+
return fmt.Errorf("[decode] unsupported content type: %q", contentType)
}
@@ -74,5 +119,19 @@ func Encode(ctx context.Context, contentType string, in interface{}) ([]byte, er
if fn, ok := encoder[contentType]; ok {
return fn(ctx, in)
}
+
+ if fn, ok := ssEncoder[structuredSuffix(contentType)]; ok {
+ return fn(ctx, in)
+ }
+
return nil, fmt.Errorf("[encode] unsupported content type: %q", contentType)
}
+
+func structuredSuffix(contentType string) string {
+ parts := strings.Split(contentType, "+")
+ if len(parts) >= 2 {
+ return parts[len(parts)-1]
+ }
+
+ return ""
+}
diff --git a/v2/event/datacodec/codec_test.go b/v2/event/datacodec/codec_test.go
index 0fd96ef5d..bc6ec3558 100644
--- a/v2/event/datacodec/codec_test.go
+++ b/v2/event/datacodec/codec_test.go
@@ -11,9 +11,10 @@ import (
"strings"
"testing"
+ "github.com/google/go-cmp/cmp"
+
"github.com/cloudevents/sdk-go/v2/event/datacodec"
"github.com/cloudevents/sdk-go/v2/types"
- "github.com/google/go-cmp/cmp"
)
func strptr(s string) *string { return &s }
@@ -25,11 +26,12 @@ type Example struct {
func TestCodecDecode(t *testing.T) {
testCases := map[string]struct {
- contentType string
- decoder datacodec.Decoder
- in []byte
- want interface{}
- wantErr string
+ contentType string
+ decoder datacodec.Decoder
+ structuredSuffix string
+ in []byte
+ want interface{}
+ wantErr string
}{
"empty": {},
"invalid content type": {
@@ -50,12 +52,24 @@ func TestCodecDecode(t *testing.T) {
"b": "banana",
},
},
+ "application/vnd.custom-type+json": {
+ contentType: "application/vnd.custom-type+json",
+ in: []byte(`{"a":"apple","b":"banana"}`),
+ want: &map[string]string{
+ "a": "apple",
+ "b": "banana",
+ },
+ },
"application/xml": {
contentType: "application/xml",
in: []byte(`7Hello, Structured Encoding v1.0!`),
want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"},
},
-
+ "application/vnd.custom-type+xml": {
+ contentType: "application/vnd.custom-type+xml",
+ in: []byte(`7Hello, Structured Encoding v1.0!`),
+ want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"},
+ },
"custom content type": {
contentType: "unit/testing",
in: []byte("Hello, Testing"),
@@ -82,12 +96,44 @@ func TestCodecDecode(t *testing.T) {
},
wantErr: "expecting unit test error",
},
+ "custom structured suffix": {
+ contentType: "unit/testing+custom",
+ structuredSuffix: "custom",
+ in: []byte("Hello, Testing"),
+ decoder: func(ctx context.Context, in []byte, out interface{}) error {
+ if s, k := out.(*map[string]string); k {
+ if (*s) == nil {
+ (*s) = make(map[string]string)
+ }
+ (*s)["upper"] = strings.ToUpper(string(in))
+ (*s)["lower"] = strings.ToLower(string(in))
+ }
+ return nil
+ },
+ want: &map[string]string{
+ "upper": "HELLO, TESTING",
+ "lower": "hello, testing",
+ },
+ },
+ "custom structured suffix error": {
+ contentType: "unit/testing+custom",
+ structuredSuffix: "custom",
+ in: []byte("Hello, Testing"),
+ decoder: func(ctx context.Context, in []byte, out interface{}) error {
+ return fmt.Errorf("expecting unit test error")
+ },
+ wantErr: "expecting unit test error",
+ },
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
if tc.decoder != nil {
- datacodec.AddDecoder(tc.contentType, tc.decoder)
+ if tc.structuredSuffix == "" {
+ datacodec.AddDecoder(tc.contentType, tc.decoder)
+ } else {
+ datacodec.AddStructuredSuffixDecoder(tc.structuredSuffix, tc.decoder)
+ }
}
got, _ := types.Allocate(tc.want)
@@ -111,11 +157,12 @@ func TestCodecDecode(t *testing.T) {
func TestCodecEncode(t *testing.T) {
testCases := map[string]struct {
- contentType string
- encoder datacodec.Encoder
- in interface{}
- want []byte
- wantErr string
+ contentType string
+ structuredSuffix string
+ encoder datacodec.Encoder
+ in interface{}
+ want []byte
+ wantErr string
}{
"empty": {},
"invalid content type": {
@@ -138,11 +185,24 @@ func TestCodecEncode(t *testing.T) {
},
want: []byte(`{"a":"apple","b":"banana"}`),
},
+ "application/vnd.custom-type+json": {
+ contentType: "application/vnd.custom-type+json",
+ in: map[string]string{
+ "a": "apple",
+ "b": "banana",
+ },
+ want: []byte(`{"a":"apple","b":"banana"}`),
+ },
"application/xml": {
contentType: "application/xml",
in: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"},
want: []byte(`7Hello, Structured Encoding v1.0!`),
},
+ "application/vnd.custom-type+xml": {
+ contentType: "application/vnd.custom-type+xml",
+ in: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"},
+ want: []byte(`7Hello, Structured Encoding v1.0!`),
+ },
"custom content type": {
contentType: "unit/testing",
@@ -173,12 +233,47 @@ func TestCodecEncode(t *testing.T) {
},
wantErr: "expecting unit test error",
},
+ "custom structured suffix": {
+ contentType: "unit/testing+custom",
+ structuredSuffix: "custom",
+ in: []string{
+ "Hello,",
+ "Testing",
+ },
+ encoder: func(ctx context.Context, in interface{}) ([]byte, error) {
+ if s, ok := in.([]string); ok {
+ sb := strings.Builder{}
+ for _, v := range s {
+ if sb.Len() > 0 {
+ sb.WriteString(" ")
+ }
+ sb.WriteString(v)
+ }
+ return []byte(sb.String()), nil
+ }
+ return nil, fmt.Errorf("don't get here")
+ },
+ want: []byte("Hello, Testing"),
+ },
+ "custom structured suffix error": {
+ contentType: "unit/testing+custom",
+ structuredSuffix: "custom",
+ in: []byte("Hello, Testing"),
+ encoder: func(ctx context.Context, in interface{}) ([]byte, error) {
+ return nil, fmt.Errorf("expecting unit test error")
+ },
+ wantErr: "expecting unit test error",
+ },
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
if tc.encoder != nil {
- datacodec.AddEncoder(tc.contentType, tc.encoder)
+ if tc.structuredSuffix == "" {
+ datacodec.AddEncoder(tc.contentType, tc.encoder)
+ } else {
+ datacodec.AddStructuredSuffixEncoder(tc.structuredSuffix, tc.encoder)
+ }
}
got, err := datacodec.Encode(context.TODO(), tc.contentType, tc.in)