ChannelClient.Format behaviour (channel_client v0.1.1)

Copy Markdown

Behaviour for pluggable wire formats.

A format owns both the structure of a frame and its encoding. The socket resolves a single format module at startup and routes every outbound/inbound frame through it, so formats are fully responsible for the bytes on the wire.

Built-in formats

  • ChannelClient.Formats.JSON - Phoenix Channels JSON encoding, protocol versions 1.0.0 (JSON objects) and 2.0.0 (JSON arrays), using any JSON library. This is the default.
  • ChannelClient.Formats.ETF - Erlang External Term Format carrying the protocol v2 array shape (term_to_binary/binary_to_term). Zero dependencies; ideal for Elixir/Erlang-to-Elixir services. Decoding uses :safe mode, so hostile peers cannot inject atoms.
  • ChannelClient.Formats.TOON - compact text format (Token-Oriented Object Notation) delegating to a codec library such as toon_ex (default module Toon, not a hard dependency).
  • ChannelClient.Formats.BTOON - binary variant of TOON (default codec module BToon, not a hard dependency).

Selecting a format

# Default: JSON v2 with Jason
ChannelClient.Socket.start_link(url: "ws://...")

# ETF
ChannelClient.Socket.start_link(url: "ws://...", format: :etf)

# TOON / BTOON (requires a TOON codec library, e.g. toon_ex)
ChannelClient.Socket.start_link(url: "ws://...", format: :toon)
ChannelClient.Socket.start_link(url: "ws://...", format: :btoon)

# JSON v1 with Poison
ChannelClient.Socket.start_link(
  url: "ws://...",
  format: {ChannelClient.Formats.JSON, version: "1.0.0", json_library: Poison}
)

# Custom module
ChannelClient.Socket.start_link(url: "ws://...", format: MyApp.MsgPack)

The legacy options still work and remain the default path: vsn: "1.0.0" selects the JSON protocol version and json_library: (alias serializer:) swaps the JSON codec.

Writing a custom format

defmodule MyApp.MsgPack do
  @behaviour ChannelClient.Format

  @impl true
  def encode!(%ChannelClient.Message{} = msg, _opts) do
    Msgpax.pack!([msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload])
  end

  @impl true
  def decode!(payload, _opts) when is_binary(payload) do
    [join_ref, ref, topic, event, payload | _] = Msgpax.unpack!(payload)
    %ChannelClient.Message{join_ref: join_ref, ref: ref, topic: topic, event: event, payload: payload}
  end
end

encode!/2 may raise on unencodable payloads — the socket converts that into {:error, reason} for sync callers and logs+drops for async pushes. decode!/2 may raise on malformed frames — those are logged and dropped instead of crashing the socket. Prefer returning tagged tuples from your own code only if you want custom error terms.

Summary

Callbacks

Parses raw transport payload back into a %Message{}. May raise on malformed input.

Serializes a message into wire bytes/iodata. May raise on invalid payloads.

Validates and normalizes format options. Invoked once at socket startup; return the keyword list to be handed back to every callback. Optional — defaults to returning opts unchanged.

Functions

Safe decode wrapper used by the socket: never raises.

Safe encode wrapper used by the socket: never raises.

Resolves a :format option value into {module, opts}.

Types

opts()

@type opts() :: keyword()

result()

@type result() :: {:ok, term()} | {:error, term()}

Callbacks

decode!(term, opts)

@callback decode!(term(), opts()) :: ChannelClient.Message.t() | no_return()

Parses raw transport payload back into a %Message{}. May raise on malformed input.

encode!(t, opts)

@callback encode!(ChannelClient.Message.t(), opts()) :: iodata() | no_return()

Serializes a message into wire bytes/iodata. May raise on invalid payloads.

init(opts)

(optional)
@callback init(opts()) :: opts()

Validates and normalizes format options. Invoked once at socket startup; return the keyword list to be handed back to every callback. Optional — defaults to returning opts unchanged.

Functions

decode(format, payload, opts)

@spec decode(module(), term(), opts()) ::
  {:ok, ChannelClient.Message.t()} | {:error, term()}

Safe decode wrapper used by the socket: never raises.

encode(format, message, opts)

@spec encode(module(), ChannelClient.Message.t(), opts()) ::
  {:ok, iodata()} | {:error, term()}

Safe encode wrapper used by the socket: never raises.

resolve(format, legacy_opts)

@spec resolve(
  term(),
  keyword()
) :: {module(), keyword()}

Resolves a :format option value into {module, opts}.

Accepts nil (falls back to legacy vsn/json_library options), built-in shorthands (:json, :etf), bare modules, or {module, opts} tuples. Raises ArgumentError on unusable values so misconfiguration fails at startup.