go/protocol/encoding.go (127 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package protocol
import (
"encoding/json"
stderr "errors"
"fmt"
"github.com/Azure/iot-operations-sdks/go/protocol/errors"
"github.com/Azure/iot-operations-sdks/go/protocol/internal/constants"
)
type (
// Encoding is a translation between a concrete Go type T and encoded data.
// All methods *must* be thread-safe.
Encoding[T any] interface {
Serialize(T) (*Data, error)
Deserialize(*Data) (T, error)
}
// Data represents encoded values along with their transmitted content type.
Data struct {
Payload []byte
ContentType string
PayloadFormat byte
}
// JSON is a simple implementation of a JSON encoding.
JSON[T any] struct{}
// Empty represents an encoding that contains no value.
Empty struct{}
// Raw represents a raw byte stream.
Raw struct{}
// Custom represents data that is externally serialized into a byte stream via custom code.
Custom struct{}
)
// ErrUnsupportedContentType should be returned if the content type is not
// supported by this encoding.
var ErrUnsupportedContentType = stderr.New("unsupported content type")
// Utility to serialize with a protocol error.
func serialize[T any](encoding Encoding[T], value T) (data *Data, err error) {
defer func() {
if ePanic := recover(); ePanic != nil {
err = payloadError("cannot serialize payload", ePanic)
}
}()
data, err = encoding.Serialize(value)
if err != nil {
return nil, payloadError("cannot serialize payload", err)
}
return data, nil
}
// Utility to deserialize with a protocol error.
func deserialize[T any](encoding Encoding[T], data *Data) (value T, err error) {
defer func() {
if ePanic := recover(); ePanic != nil {
err = payloadError("cannot deserialize payload", err)
}
}()
value, err = encoding.Deserialize(data)
if err != nil {
if stderr.Is(err, ErrUnsupportedContentType) {
return value, &errors.Client{
Message: "content type mismatch",
Kind: errors.HeaderInvalid{
HeaderName: constants.ContentType,
HeaderValue: data.ContentType,
},
}
}
return value, payloadError("cannot deserialize payload", err)
}
return value, nil
}
func payloadError(msg string, err any) error {
switch e := err.(type) {
case *errors.Client:
return e
case error:
return &errors.Client{
Message: msg,
Kind: errors.PayloadInvalid{},
Nested: e,
}
default:
return &errors.Client{
Message: msg,
Kind: errors.PayloadInvalid{},
Nested: stderr.New(fmt.Sprint(e)),
}
}
}
// Serialize translates the Go type T into JSON bytes.
func (JSON[T]) Serialize(t T) (*Data, error) {
bytes, err := json.Marshal(t)
if err != nil {
return nil, err
}
return &Data{bytes, "application/json", 1}, nil
}
// Deserialize translates JSON bytes into the Go type T.
func (JSON[T]) Deserialize(data *Data) (T, error) {
var t T
switch data.ContentType {
case "", "application/json":
err := json.Unmarshal(data.Payload, &t)
return t, err
default:
return t, ErrUnsupportedContentType
}
}
// Serialize validates that the payload is empty.
func (Empty) Serialize(t any) (*Data, error) {
if t != nil {
return nil, &errors.Client{
Message: "unexpected payload for empty type",
Kind: errors.PayloadInvalid{},
}
}
return &Data{}, nil
}
// Deserialize validates that the payload is empty.
func (Empty) Deserialize(data *Data) (any, error) {
if len(data.Payload) != 0 {
return nil, &errors.Client{
Message: "unexpected payload for empty type",
Kind: errors.PayloadInvalid{},
}
}
return nil, nil
}
// Serialize returns the bytes unchanged.
func (Raw) Serialize(t []byte) (*Data, error) {
return &Data{t, "application/octet-stream", 0}, nil
}
// Deserialize returns the bytes unchanged.
func (Raw) Deserialize(data *Data) ([]byte, error) {
switch data.ContentType {
case "", "application/octet-stream":
return data.Payload, nil
default:
return nil, ErrUnsupportedContentType
}
}
// Serialize returns the data unchanged.
func (Custom) Serialize(t Data) (*Data, error) {
return &t, nil
}
// Deserialize returns the data unchanged.
func (Custom) Deserialize(data *Data) (Data, error) {
return *data, nil
}