Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support content types following structured syntax suffixes #1007

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions v2/event/datacodec/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -51,19 +69,46 @@ 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.
func Decode(ctx context.Context, contentType string, in []byte, out interface{}) error {
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)
}

Expand All @@ -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 ""
}
123 changes: 109 additions & 14 deletions v2/event/datacodec/codec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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": {
Expand All @@ -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(`<Example><Sequence>7</Sequence><Message>Hello, Structured Encoding v1.0!</Message></Example>`),
want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"},
},

"application/vnd.custom-type+xml": {
contentType: "application/vnd.custom-type+xml",
in: []byte(`<Example><Sequence>7</Sequence><Message>Hello, Structured Encoding v1.0!</Message></Example>`),
want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"},
},
"custom content type": {
contentType: "unit/testing",
in: []byte("Hello, Testing"),
Expand All @@ -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)
Expand All @@ -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": {
Expand All @@ -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(`<Example><Sequence>7</Sequence><Message>Hello, Structured Encoding v1.0!</Message></Example>`),
},
"application/vnd.custom-type+xml": {
contentType: "application/vnd.custom-type+xml",
in: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"},
want: []byte(`<Example><Sequence>7</Sequence><Message>Hello, Structured Encoding v1.0!</Message></Example>`),
},

"custom content type": {
contentType: "unit/testing",
Expand Down Expand Up @@ -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)
Expand Down