diff --git a/CHANGELOG.md b/CHANGELOG.md index 294986f..6b0b739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/marshaler/marshaler.go b/marshaler/marshaler.go index d4c6624..5a4697c 100644 --- a/marshaler/marshaler.go +++ b/marshaler/marshaler.go @@ -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) @@ -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 @@ -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 @@ -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) @@ -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, diff --git a/marshaler/marshaler_test.go b/marshaler/marshaler_test.go index d93621f..10acb23 100644 --- a/marshaler/marshaler_test.go +++ b/marshaler/marshaler_test.go @@ -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{""}) diff --git a/marshaler/mime.go b/marshaler/mime.go index 5195b49..7f2ad2e 100644 --- a/marshaler/mime.go +++ b/marshaler/mime.go @@ -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 } diff --git a/marshaler/packet.go b/marshaler/packet.go index 2709f7b..bee70ac 100644 --- a/marshaler/packet.go +++ b/marshaler/packet.go @@ -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 -} diff --git a/protobuf/envelopepb/transcoder.go b/protobuf/envelopepb/transcoder.go index 7def7bd..3592685 100644 --- a/protobuf/envelopepb/transcoder.go +++ b/protobuf/envelopepb/transcoder.go @@ -2,6 +2,7 @@ package envelopepb import ( "mime" + reflect "reflect" "strings" "github.com/dogmatiq/enginekit/marshaler" @@ -10,9 +11,9 @@ 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 @@ -20,13 +21,12 @@ type Transcoder struct { // 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 @@ -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 } diff --git a/protobuf/envelopepb/transcoder_test.go b/protobuf/envelopepb/transcoder_test.go index 8bff83a..987a3c9 100644 --- a/protobuf/envelopepb/transcoder_test.go +++ b/protobuf/envelopepb/transcoder_test.go @@ -4,22 +4,26 @@ 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 { @@ -27,8 +31,8 @@ func TestTranscoder(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+json; type=different`, `application/vnd.google.protobuf+json; type=different; extra=true`, `application/vnd.google.protobuf+json; no-type=true`, @@ -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 { @@ -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, @@ -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) + } + }) }