# 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