Skip to content

Commit

Permalink
Support content types following structured syntax suffixes
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
dan-j authored and duglin committed Jan 30, 2024
1 parent e17833e commit bc1a952
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 14 deletions.
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

0 comments on commit bc1a952

Please sign in to comment.