Current section
Files
Jump to
Current section
Files
lib/messagepack_rpc.ex
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Scott Thompson
defmodule MessagePackRPC do
@moduledoc """
This module provides functions to create and parse MessagePack-RPC messages.
According to its specification, MessagePack-RPC supports three message
types:
- Request: A method call that expects a response
- Notification: A method call that doesn't expect a response
- Response: The reply to a request. It will be successs, and carry a result,
or indicate an error.
Requests and Responses are paired using a message ID (an unsigned 16-bit
integer). Notifications are one-way messages and do not have a message ID.
## Messages in Elixir
In Elixir, messages are represented by tuples and this module has functions
to transform from the Elixir tuples to lists, the unpacked wire formats.
## Message Wire Formats
In transit, messages are represented by arrays that are then serialized
using MessagePack. The unpacked messages are represented here by lists:
- Request: [0, message_id, method, params]
- Response: [1, message_id, error, result]
- Notification: [2, method, params]
A response can have either a result or an error, but not both.
(or both the error and result can be `nil`)
"""
# message IDs are unsigned 16-bit integers
defguard is_valid_message_id(message_id)
when is_integer(message_id) and message_id >= 0 and message_id <= 65535
@type message_id :: 0..0xFFFF
@type method_name :: binary()
@type params :: list()
@type result :: any()
@type error :: any()
@typedoc "These are the tuples used to represent messages in Elixir"
@type rpc_message ::
{:request, {message_id(), method_name(), params()}}
| {:response, {:ok, message_id(), result()} | {:error, message_id(), error()}}
| {:notify, {method_name(), params()}}
@type unpacked_message :: list()
@spec rpc_request(message_id(), method_name(), params()) :: rpc_message()
def rpc_request(message_id, method_name, params \\ [])
when is_valid_message_id(message_id) and
is_binary(method_name) and is_list(params) do
{:request, {message_id, method_name, params}}
end
@spec rpc_notification(method_name(), params()) :: rpc_message()
def rpc_notification(method_name, params \\ [])
when is_binary(method_name) and is_list(params) do
{:notify, {method_name, params}}
end
@spec successful_response_message(message_id(), result()) :: rpc_message()
def successful_response_message(message_id, result)
when is_valid_message_id(message_id) do
{:response, {:ok, message_id, result}}
end
@spec error_response_message(message_id(), error()) :: rpc_message()
def error_response_message(message_id, error)
when is_valid_message_id(message_id) do
{:response, {:error, message_id, error}}
end
# Magic constants defined by MessagePack-RPC
@request 0
@response 1
@notify 2
@doc """
Converts an Elixir tuple into an unpacked message, the list representation of
an RPC message.
"""
@spec unpacked_message(rpc_message()) :: unpacked_message()
def unpacked_message({:request, {message_id, method_name, params}}) do
[@request, message_id, method_name, params]
end
def unpacked_message({:response, {:ok, message_id, result}}) do
[@response, message_id, nil, result]
end
def unpacked_message({:response, {:error, message_id, error}}) do
[@response, message_id, error, nil]
end
def unpacked_message({:notify, {method_name, params}}) do
[@notify, method_name, params]
end
def to_rpc_message([@request, message_id, method_name, params]) do
rpc_request(message_id, method_name, params)
end
def to_rpc_message([@response, message_id, nil, result]) do
successful_response_message(message_id, result)
end
def to_rpc_message([@response, message_id, error, nil]) do
error_response_message(message_id, error)
end
def to_rpc_message([@notify, method_name, params]) do
rpc_notification(method_name, params)
end
end