Documentation
¶
Overview ¶
Package content provides a content-type codec layer for mailbox messages.
Mailbox stores message bodies as plain text strings. This package provides a convention for encoding structured or binary content into text-safe bodies and decoding them back, using message headers to carry content type and schema information.
The content package does NOT modify any mailbox interfaces. It operates entirely through the existing public API (SetBody, SetHeader, GetBody, GetHeaders, and — for the legacy convention — SetMetadata/GetMetadata). Mailbox remains text-first; this package is an opt-in layer on top.
Header Convention ¶
Structured messages carry content type and schema as first-class headers:
- store.HeaderContentType: MIME type (e.g., "application/json", "application/protobuf")
- store.HeaderSchema: optional schema identifier (e.g., "sensor.reading/v1")
Messages without a Content-Type header are plain text. No codec is needed to read or write them.
For backward compatibility, the older metadata convention (the content_type and schema metadata keys set by Encode) is still honored: Decode, ContentType, and Schema read the headers first and fall back to metadata. New code should prefer EncodeWithHeaders; Encode is deprecated.
Codec Interface ¶
A Codec converts between raw bytes and a text-safe string:
- Text-safe formats (JSON, XML) pass through unchanged.
- Binary formats (protobuf, msgpack) are base64-encoded for storage.
The application handles serialization (struct to bytes) separately. The codec handles only the text-encoding concern.
Usage ¶
Sending a structured message:
data, _ := json.Marshal(sensorReading)
body, headers, _ := content.EncodeWithHeaders(content.JSON, data, content.WithSchema("sensor.reading/v1"))
draft, _ := mb.Compose()
draft.SetSubject("Reading").SetRecipients("consumer-svc").SetBody(body)
for k, v := range headers {
draft.SetHeader(k, v)
}
draft.Send(ctx)
Reading a structured message:
msg, _ := mb.Get(ctx, id) raw, _ := content.Decode(msg, registry) var reading SensorReading json.Unmarshal(raw, &reading)
Plugin Integration ¶
Mailbox [SendHook] plugins can inspect the header convention to validate, route, or forward structured messages to external systems. The content package provides ContentType and Schema helpers for reading the content type and schema without hardcoding key names.
Example (ServiceToService) ¶
This example demonstrates two services communicating through mailbox using structured JSON messages.
The order-service encodes an OrderPlaced struct into the message body with content-type and schema metadata. The fulfillment-service receives the message, inspects the metadata to determine the format, and decodes the body back to the original struct.
The same pattern works with binary formats: swap content.JSON for content.Protobuf or content.MsgPack. Binary codecs base64-encode the body automatically. The receiving side uses the registry to pick the right decoder based on the content_type metadata.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/content"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
// -- shared types (both services import these) --
type OrderPlaced struct {
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
Total int `json:"total_cents"`
}
const orderSchema = "order.placed/v1"
// -- infrastructure setup --
svc, err := mailbox.New(mailbox.Config{}, mailbox.WithStore(memory.New()))
if err != nil {
log.Fatal(err)
}
if err := svc.Connect(ctx); err != nil {
log.Fatal(err)
}
defer svc.Close(ctx)
// -- order-service: sends a structured message --
order := OrderPlaced{
OrderID: "ord-123",
UserID: "user-456",
Total: 4999,
}
payload, _ := json.Marshal(order)
// Encode returns body + metadata. Swap content.JSON for
// content.Protobuf or content.MsgPack for binary formats.
body, meta, err := content.Encode(content.JSON, payload, content.WithSchema(orderSchema))
if err != nil {
log.Fatal(err)
}
sender := svc.Client("order-service")
draft, _ := sender.Compose()
draft.SetSubject("New Order").SetRecipients("fulfillment-service").SetBody(body)
for k, v := range meta {
draft.SetMetadata(k, v)
}
if _, err := draft.Send(ctx); err != nil {
log.Fatal(err)
}
// -- fulfillment-service: receives and decodes the message --
registry := content.DefaultRegistry()
receiver := svc.Client("fulfillment-service")
inbox, _ := receiver.Folder(ctx, store.FolderInbox, mailbox.ListOptions{})
for _, msg := range inbox.All() {
ct := content.ContentType(msg)
schema := content.Schema(msg)
// Route by content type and schema
if ct == "application/json" && schema == orderSchema {
raw, err := content.Decode(msg, registry)
if err != nil {
log.Fatal(err)
}
var received OrderPlaced
if err := json.Unmarshal(raw, &received); err != nil {
log.Fatal(err)
}
fmt.Println("order_id:", received.OrderID)
fmt.Println("user_id:", received.UserID)
fmt.Println("total_cents:", received.Total)
fmt.Println("content_type:", ct)
fmt.Println("schema:", schema)
}
}
}
Output: order_id: ord-123 user_id: user-456 total_cents: 4999 content_type: application/json schema: order.placed/v1
Index ¶
- Constants
- Variables
- func ContentType(msg store.MessageReader) string
- func Decode(msg store.MessageReader, registry *Registry) ([]byte, error)
- func EncodeWithHeaders(codec Codec, data []byte, opts ...EncodeOption) (string, map[string]string, error)
- func Schema(msg store.MessageReader) string
- type Codec
- type EncodeOption
- type Metadata
- type Registry
Examples ¶
Constants ¶
const ( // MetaContentType is the metadata key for the MIME content type. // Example values: "application/json", "application/protobuf", "text/plain". MetaContentType = "content_type" // MetaSchema is the optional metadata key for a schema identifier. // The format is application-defined. Examples: "sensor.reading/v1", "order.placed/v2". MetaSchema = "schema" )
Metadata keys used by the content codec convention. These are set on message metadata by Encode and read by Decode. Plugins and external systems should use these constants instead of hardcoding string values.
Variables ¶
var ( // ErrUnsupportedContentType is returned when no codec is registered for a content type. ErrUnsupportedContentType = errors.New("content: unsupported content type") // ErrEncoding is returned when a codec fails to encode data. ErrEncoding = errors.New("content: encoding failed") // ErrDecoding is returned when a codec fails to decode a body. ErrDecoding = errors.New("content: decoding failed") )
Sentinel errors.
Functions ¶
func ContentType ¶
func ContentType(msg store.MessageReader) string
ContentType returns the content type from message headers or metadata. Checks the Content-Type header first, then falls back to the metadata convention for backward compatibility.
func Decode ¶
func Decode(msg store.MessageReader, registry *Registry) ([]byte, error)
Decode reads the content_type from message metadata, looks up the corresponding codec in the registry, and decodes the body to raw bytes.
If no content_type metadata is set, the body is returned as raw bytes (plain text fallback).
func EncodeWithHeaders ¶ added in v0.5.0
func EncodeWithHeaders(codec Codec, data []byte, opts ...EncodeOption) (string, map[string]string, error)
EncodeWithHeaders encodes data using the codec and returns the text-safe body string along with headers (Content-Type, and optionally Schema) that should be set on the draft or send request via SetHeader.
Unlike Encode, which uses metadata keys, EncodeWithHeaders uses first-class message headers (store.HeaderContentType, store.HeaderSchema).
Options:
- WithSchema sets the Schema header.
Example ¶
ExampleEncodeWithHeaders demonstrates encoding a JSON struct into body + headers.
package main
import (
"encoding/json"
"fmt"
"github.com/rbaliyan/mailbox/content"
"github.com/rbaliyan/mailbox/store"
)
func main() {
type SensorReading struct {
Temperature int `json:"temperature"`
Unit string `json:"unit"`
}
reading := SensorReading{Temperature: 72, Unit: "F"}
data, _ := json.Marshal(reading)
body, headers, _ := content.EncodeWithHeaders(content.JSON, data, content.WithSchema("sensor.reading/v1"))
fmt.Println("body:", body)
fmt.Println("Content-Type:", headers[store.HeaderContentType])
fmt.Println("Schema:", headers[store.HeaderSchema])
}
Output: body: {"temperature":72,"unit":"F"} Content-Type: application/json Schema: sensor.reading/v1
func Schema ¶
func Schema(msg store.MessageReader) string
Schema returns the schema identifier from message headers or metadata. Checks the Schema header first, then falls back to the metadata convention for backward compatibility.
Types ¶
type Codec ¶
type Codec interface {
// ContentType returns the MIME type this codec handles.
ContentType() string
// Encode converts raw bytes to a text-safe string for storage in the
// message body.
Encode(data []byte) (string, error)
// Decode converts a text body back to the original raw bytes.
Decode(body string) ([]byte, error)
}
Codec converts between raw bytes and a text-safe string representation.
Implementations handle a specific content type. Text-safe formats (JSON, XML) typically pass through unchanged. Binary formats (protobuf, msgpack) use base64 encoding.
The application is responsible for serializing structs to bytes before encoding and deserializing bytes after decoding. The codec handles only the text-safety concern.
var ( // JSON is a pass-through codec for application/json content. // JSON is already text-safe UTF-8, so no encoding is needed. JSON Codec = textCodec{/* contains filtered or unexported fields */} // XML is a pass-through codec for application/xml content. XML Codec = textCodec{/* contains filtered or unexported fields */} // Plain is a pass-through codec for text/plain content. // This is useful when you want to explicitly mark a message as plain text // rather than leaving the content_type unset. Plain Codec = textCodec{/* contains filtered or unexported fields */} // Protobuf is a base64 codec for application/protobuf content. // Binary protobuf bytes are base64-encoded for text-safe storage. Protobuf Codec = binaryCodec{/* contains filtered or unexported fields */} // MsgPack is a base64 codec for application/msgpack content. // Binary msgpack bytes are base64-encoded for text-safe storage. MsgPack Codec = binaryCodec{/* contains filtered or unexported fields */} // OctetStream is a base64 codec for application/octet-stream content. // Arbitrary binary data is base64-encoded for text-safe storage. OctetStream Codec = binaryCodec{/* contains filtered or unexported fields */} )
Built-in codecs for common content types.
Text-safe codecs (JSON, XML, Plain) are zero-cost pass-throughs since these formats are already valid UTF-8 text. Binary codecs (Protobuf, MsgPack) use standard base64 encoding.
type EncodeOption ¶
type EncodeOption func(*encodeOptions)
EncodeOption configures Encode behavior.
func WithSchema ¶
func WithSchema(schema string) EncodeOption
WithSchema sets the schema metadata key on the message.
type Metadata ¶
Metadata is a set of key-value pairs to apply to a draft or send request.
func Encode
deprecated
Encode encodes data using the codec and returns the text-safe body string along with metadata entries (content_type, and optionally schema) that should be set on the draft or send request.
The returned Metadata is intentionally separate from the body so that Encode works with any draft type (mailbox.DraftComposer, store.DraftMessage, or mailbox.SendRequest) without depending on their differing method signatures.
Deprecated: Use EncodeWithHeaders for new code. EncodeWithHeaders uses first-class message headers instead of metadata keys for content type and schema.
Options:
- WithSchema sets the schema metadata key.
Example ¶
ExampleEncode demonstrates encoding a JSON struct into body + metadata.
package main
import (
"encoding/json"
"fmt"
"github.com/rbaliyan/mailbox/content"
)
func main() {
type SensorReading struct {
Temperature int `json:"temperature"`
Unit string `json:"unit"`
}
reading := SensorReading{Temperature: 72, Unit: "F"}
data, _ := json.Marshal(reading)
body, meta, _ := content.Encode(content.JSON, data, content.WithSchema("sensor.reading/v1"))
fmt.Println("body:", body)
fmt.Println("content_type:", meta[content.MetaContentType])
fmt.Println("schema:", meta[content.MetaSchema])
}
Output: body: {"temperature":72,"unit":"F"} content_type: application/json schema: sensor.reading/v1
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry maps content types to codecs. It is safe for concurrent use.
func DefaultRegistry ¶
func DefaultRegistry() *Registry
DefaultRegistry returns a registry pre-loaded with all built-in codecs.
func NewRegistry ¶
NewRegistry creates a registry pre-loaded with the given codecs.