Skip to content

Commit

Permalink
Add Marshaler.UnmarshalTypeFromMediaType().
Browse files Browse the repository at this point in the history
  • Loading branch information
jmalloc committed Sep 29, 2024
1 parent deb6c8b commit f337a02
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 40 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ The format is based on [Keep a Changelog], and this project adheres to
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html

## [0.13.0] - 2024-09-30

### Added

- Added `Marshaler.UnmarshalTypeFromMediaType()`.

### Removed

- Removed `Envelope.PortableName`. The `MediaType` field is now guaranteed to
include the portable name as a parameter.
- Removed `Packet.PortableName()`.

## [0.12.2] - 2024-09-30

### Fixed
Expand Down
21 changes: 13 additions & 8 deletions marshaler/marshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ type Marshaler interface {
// UnmarshalType unmarshals a type from its portable string representation.
UnmarshalType(n string) (reflect.Type, error)

// MarshalTypeFromMediaType returns the type that is represented by the
// given media-type.
UnmarshalTypeFromMediaType(mediaType string) (reflect.Type, error)

// Marshal returns a binary representation of v.
Marshal(v any) (Packet, error)

Expand Down Expand Up @@ -144,7 +148,6 @@ func New(
return m, nil
}

// MarshalType marshals a type to its portable representation.
func (m *marshaler) MarshalType(rt reflect.Type) (string, error) {
if bt, ok := m.types[rt]; ok {
return bt.defaultPortableName, nil
Expand All @@ -156,7 +159,6 @@ func (m *marshaler) MarshalType(rt reflect.Type) (string, error) {
)
}

// UnmarshalType unmarshals a type from its portable representation.
func (m *marshaler) UnmarshalType(n string) (reflect.Type, error) {
if rt, ok := m.typeByPortableName[n]; ok {
return rt, nil
Expand All @@ -168,7 +170,15 @@ func (m *marshaler) UnmarshalType(n string) (reflect.Type, error) {
)
}

// Marshal returns a binary representation of v.
func (m *marshaler) UnmarshalTypeFromMediaType(mediaType string) (reflect.Type, error) {
_, n, err := parseMediaType(mediaType)
if err != nil {
return nil, err
}

return m.UnmarshalType(n)
}

func (m *marshaler) Marshal(v any) (Packet, error) {
rt := reflect.TypeOf(v)

Expand All @@ -193,11 +203,6 @@ func (m *marshaler) Marshal(v any) (Packet, error) {
)
}

// MarshalAs returns a binary representation of v encoded using a format
// associated with one of the supplied media-types.
//
// mediaTypes is a list of acceptible media-types, in order of preference.
// If none of the media-types are supported, ok is false.
func (m *marshaler) MarshalAs(
v any,
mediaTypes []string,
Expand Down
14 changes: 14 additions & 0 deletions marshaler/marshaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,20 @@ func TestMarshaler(t *testing.T) {
})
})

t.Run("func UnmarshalTypeFromMediaType()", func(t *testing.T) {
t.Run("it returns the reflection type", func(t *testing.T) {
got, err := marshaler.UnmarshalTypeFromMediaType("application/vnd.google.protobuf; type=dogmatiq.enginekit.marshaler.stubs1.ProtoMessage")
if err != nil {
t.Fatal(err)
}

want := reflect.TypeFor[*stubs1.ProtoMessage]()
if got != want {
t.Fatalf("unexpected type: got %v, want %v", got, want)
}
})
})

t.Run("func Marshal()", func(t *testing.T) {
t.Run("it marshals using the first suitable codec", func(t *testing.T) {
got, err := marshaler.Marshal(Value{"<value>"})
Expand Down
4 changes: 2 additions & 2 deletions marshaler/mime.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ func formatMediaType(base string, portableName string) string {

// parseMediaType returns the media-type and the portable type name encoded in
// the packet's MIME media-type.
func parseMediaType(mediatype string) (string, string, error) {
mt, params, err := mime.ParseMediaType(mediatype)
func parseMediaType(mediaType string) (string, string, error) {
mt, params, err := mime.ParseMediaType(mediaType)
if err != nil {
return "", "", err
}
Expand Down
11 changes: 0 additions & 11 deletions marshaler/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,3 @@ type Packet struct {
// Data is the marshaled binary data.
Data []byte
}

// PortableName returns the portable name of the type represented by the data.
//
// It panics if the media-type does not have a value "type" parameter.
func (p Packet) PortableName() string {
_, n, err := parseMediaType(p.MediaType)
if err != nil {
panic(err)
}
return n
}
23 changes: 14 additions & 9 deletions protobuf/envelopepb/transcoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package envelopepb

import (
"mime"
reflect "reflect"
"strings"

"github.com/dogmatiq/enginekit/marshaler"
Expand All @@ -10,23 +11,22 @@ import (

// Transcoder re-encodes messages to different media-types on the fly.
type Transcoder struct {
// MediaTypes is a map of the message's "portable name" to a list of
// supported media-types, in order of preference.
MediaTypes map[string][]string
// MediaTypes is a map of the message's type to a list of supported
// media-types, in order of preference.
MediaTypes map[reflect.Type][]string

// Marshaler is the marshaler to use to unmarshal and marshal messages.
Marshaler marshaler.Marshaler
}

// Transcode re-encodes the message in env to one of the supported media-types.
func (t *Transcoder) Transcode(env *Envelope) (*Envelope, bool, error) {
packet := marshaler.Packet{
MediaType: env.MediaType,
Data: env.Data,
rt, err := t.Marshaler.UnmarshalTypeFromMediaType(env.MediaType)
if err != nil {
return nil, false, err
}

name := packet.PortableName()
supported := t.MediaTypes[name]
supported := t.MediaTypes[rt]

if len(supported) == 0 {
return nil, false, nil
Expand All @@ -41,7 +41,12 @@ func (t *Transcoder) Transcode(env *Envelope) (*Envelope, bool, error) {
}
}

m, err := t.Marshaler.Unmarshal(packet)
m, err := t.Marshaler.Unmarshal(
marshaler.Packet{
MediaType: env.MediaType,
Data: env.Data,
},
)
if err != nil {
return nil, false, err
}
Expand Down
60 changes: 50 additions & 10 deletions protobuf/envelopepb/transcoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,35 @@ import (
reflect "reflect"
"testing"

. "github.com/dogmatiq/enginekit/enginetest/stubs"
"github.com/dogmatiq/enginekit/internal/test"
"github.com/dogmatiq/enginekit/marshaler"
"github.com/dogmatiq/enginekit/marshaler/codecs/json"
"github.com/dogmatiq/enginekit/marshaler/codecs/protobuf"
. "github.com/dogmatiq/enginekit/protobuf/envelopepb"
"github.com/dogmatiq/enginekit/protobuf/envelopepb/internal/stubs"
. "github.com/dogmatiq/enginekit/protobuf/envelopepb/internal/stubs"
"google.golang.org/protobuf/proto"
)

func TestTranscoder(t *testing.T) {
m, err := marshaler.New(
[]reflect.Type{
reflect.TypeFor[*stubs.ProtoMessage](),
reflect.TypeFor[*ProtoMessage](),
reflect.TypeOf(CommandA1),
},
[]marshaler.Codec{
protobuf.DefaultJSONCodec,
protobuf.DefaultTextCodec,
json.DefaultCodec,
},
)
if err != nil {
t.Fatal(err)
}

transcoder := &Transcoder{
MediaTypes: map[string][]string{
`dogmatiq.enginekit.protobuf.envelopepb.stubs.ProtoMessage`: {
MediaTypes: map[reflect.Type][]string{
reflect.TypeFor[*ProtoMessage](): {
`application/vnd.google.protobuf+json; type=different`,
`application/vnd.google.protobuf+json; type=different; extra=true`,
`application/vnd.google.protobuf+json; no-type=true`,
Expand Down Expand Up @@ -96,10 +100,10 @@ func TestTranscoder(t *testing.T) {
)
})

t.Run("it returns an error if the recipient does not support any encodings", func(t *testing.T) {
t.Run("it returns false if the recipient does not support any encodings", func(t *testing.T) {
_, ok, err := transcoder.Transcode(
&Envelope{
MediaType: `text/plain; type=unrecognized`,
MediaType: `text/plain; type="CommandStub[TypeA]"`,
},
)
if err != nil {
Expand All @@ -111,13 +115,13 @@ func TestTranscoder(t *testing.T) {
}
})

t.Run("it returns an error if the marshaler does not support any of the encodings supported by the recipient", func(t *testing.T) {
t.Run("it returns false if the marshaler does not support any of the encodings supported by the recipient", func(t *testing.T) {
transcoder := &Transcoder{
MediaTypes: map[string][]string{
`dogmatiq.enginekit.protobuf.envelopepb.stubs.ProtoMessage`: {
MediaTypes: map[reflect.Type][]string{
reflect.TypeFor[*ProtoMessage](): {
`application/vnd.google.protobuf; type=different`,
`application/vnd.google.protobuf; type=different; extra=true`,
`application/vnd.google.protobuf`,
`application/vnd.google.protobuf; no-type=true`,
},
},
Marshaler: m,
Expand All @@ -137,4 +141,40 @@ func TestTranscoder(t *testing.T) {
t.Error("expected ok to be false")
}
})

t.Run("it returns an error if the marshaler does not support the original encoding", func(t *testing.T) {
_, _, err := transcoder.Transcode(
&Envelope{
MediaType: `application/unsupported; type=irrelevant`,
},
)
if err == nil {
t.Fatal("expected an error")
}

got := err.Error()
want := `the portable type name 'irrelevant' is not recognized`

if got != want {
t.Errorf("unexpected error: got %q, want %q", got, want)
}
})

t.Run("it returns an error if the original encoding does not have a type", func(t *testing.T) {
_, _, err := transcoder.Transcode(
&Envelope{
MediaType: `application/unsupported`,
},
)
if err == nil {
t.Fatal("expected an error")
}

got := err.Error()
want := `the media-type does not specify a 'type' parameter`

if got != want {
t.Errorf("unexpected error: got %q, want %q", got, want)
}
})
}

0 comments on commit f337a02

Please sign in to comment.