Packages

Encoders and decoders for JSON-RPC 2.0

Current section

Files

Jump to
jsonrpcx src jsonrpcx.gleam
Raw

src/jsonrpcx.gleam

//// JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol.
//// Primarily this specification defines several data structures and the rules
//// around their processing. It is transport agnostic in that the concepts can be
//// used within the same process, over sockets, over http, or in many various
//// message passing environments. It uses JSON (RFC 4627) as data format.
////
//// The error codes from and including -32768 to -32000 are reserved for
//// pre-defined errors. Any code within this range, but not defined explicitly below
//// is reserved for future use.
////
//// | code | message | meaning |
//// | --- | --- | --- |
//// | -32700 | Parse error | Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. |
//// | -32600 | Invalid Request | The JSON sent is not a valid Request object. |
//// | -32601 | Method not found | The method does not exist / is not available. |
//// | -32602 | Invalid params | Invalid method parameter(s). |
//// | -32603 | Internal error | Internal JSON-RPC error. |
//// | -32000 to -32099 | Server error | Reserved for implementation-defined server-errors. |
////
//// The remainder of the space is available for application defined errors.
import gleam/dynamic/decode.{type Decoder, type Dynamic}
import gleam/function
import gleam/json.{type Json}
import gleam/list
import gleam/option.{type Option, None, Some}
/// A union of the JSON-RPC message types.
pub type Message {
RequestMessage(Request(Dynamic))
NotificationMessage(Notification(Dynamic))
ResponseMessage(Response(Dynamic))
ErrorResponseMessage(ErrorResponse(Dynamic))
BatchRequestMessage(BatchRequest(Dynamic))
BatchResponseMessage(BatchResponse(Dynamic))
}
pub fn message_decoder() -> Decoder(Message) {
let request = request_decoder(decode.dynamic) |> decode.map(RequestMessage)
let notification =
notification_decoder(decode.dynamic) |> decode.map(NotificationMessage)
let response = response_decoder(decode.dynamic) |> decode.map(ResponseMessage)
let error_response =
error_response_decoder(decode.dynamic) |> decode.map(ErrorResponseMessage)
let batch_request = batch_request_decoder() |> decode.map(BatchRequestMessage)
let batch_response =
batch_response_decoder() |> decode.map(BatchResponseMessage)
decode.one_of(request, [
notification,
response,
error_response,
batch_request,
batch_response,
])
}
/// A request that is made up of a list of JSON-RPC requests and/or notifications.
/// Items in a batch request MUST be able to be processed in any other. As such,
/// order is not preserved.
pub opaque type BatchRequest(a) {
BatchRequest(List(BatchRequestItem(a)))
}
/// retrieve the list of BatchRequestItems
pub fn batch_request_items(
batch_request: BatchRequest(a),
) -> List(BatchRequestItem(a)) {
let BatchRequest(batch) = batch_request
batch
}
/// Create a new, empty batch request
pub fn batch_request() -> BatchRequest(Json) {
BatchRequest([])
}
pub fn batch_request_to_json(batch_request: BatchRequest(Json)) -> Json {
let BatchRequest(batch) = batch_request
json.array(batch, batch_request_item_to_json)
}
pub fn batch_request_decoder() -> Decoder(BatchRequest(Dynamic)) {
decode.list(batch_request_item_decoder()) |> decode.map(BatchRequest)
}
/// Add a request to a batch request. Batch requests have no guarantees about
/// order. Therefore order is not preserved when your batch request is sent.
pub fn add_request(
batch_request: BatchRequest(Json),
request: Request(a),
params_to_json: fn(a) -> Json,
) -> BatchRequest(Json) {
let BatchRequest(batch) = batch_request
let request =
Request(..request, params: option.map(request.params, params_to_json))
|> BatchRequestItemRequest
BatchRequest([request, ..batch])
}
/// Add a notification to a batch request. Batch requests have no guarantees about
/// order. Therefore order is not preserved when your batch request is sent.
pub fn add_notification(
batch_request: BatchRequest(Json),
notification: Notification(a),
params_to_json: fn(a) -> Json,
) -> BatchRequest(Json) {
let BatchRequest(batch) = batch_request
let notification =
Notification(
..notification,
params: option.map(notification.params, params_to_json),
)
|> BatchRequestItemNotification
BatchRequest([notification, ..batch])
}
/// A union of the types allowed in a single batch request.
pub type BatchRequestItem(a) {
BatchRequestItemRequest(Request(a))
BatchRequestItemNotification(Notification(a))
}
pub fn batch_request_item_decoder() -> Decoder(BatchRequestItem(Dynamic)) {
let request =
request_decoder(decode.dynamic) |> decode.map(BatchRequestItemRequest)
let notification =
notification_decoder(decode.dynamic)
|> decode.map(BatchRequestItemNotification)
decode.one_of(request, [notification])
}
pub fn batch_request_item_to_json(item: BatchRequestItem(Json)) {
case item {
BatchRequestItemRequest(msg) -> request_to_json(msg, function.identity)
BatchRequestItemNotification(msg) ->
notification_to_json(msg, function.identity)
}
}
/// A response that is made up of a list of JSON-RPC responses and/or error responses.
/// A batch response should contain a response or error response for each
/// request in the request batch (except for notifications).
/// Items in a batch response are not ordered. The client is responsible to map
/// the response IDs to their own request IDs.
pub opaque type BatchResponse(a) {
BatchResponse(List(BatchResponseItem(a)))
}
/// retrieve the list of BatchResponseItems
pub fn batch_response_items(batch_response: BatchResponse(a)) {
let BatchResponse(batch) = batch_response
batch
}
/// Create a new, empty batch response
pub fn batch_response() -> BatchResponse(Json) {
BatchResponse([])
}
pub fn batch_response_decoder() -> Decoder(BatchResponse(Dynamic)) {
decode.list(batch_response_item_decoder()) |> decode.map(BatchResponse)
}
pub fn batch_response_to_json(batch_response: BatchResponse(Json)) {
let BatchResponse(batch) = batch_response
json.array(batch, batch_response_item_to_json)
}
/// Add a response to a batch response. Batch responses have no guarantees about
/// order. Therefore order is not preserved when your batch response is sent.
pub fn add_response(
batch_response: BatchResponse(Json),
response: Response(a),
result_to_json: fn(a) -> Json,
) -> BatchResponse(Json) {
let BatchResponse(batch) = batch_response
let response =
Response(..response, result: result_to_json(response.result))
|> BatchResponseItemResponse
BatchResponse([response, ..batch])
}
/// Add an error response to a batch response. Batch responses have no guarantees about
/// order. Therefore order is not preserved when your batch response is sent.
pub fn add_error_response(
batch_response: BatchResponse(Json),
error_response: ErrorResponse(a),
data_to_json: fn(a) -> Json,
) -> BatchResponse(Json) {
let BatchResponse(batch) = batch_response
let error =
ErrorBody(
..error_response.error,
data: option.map(error_response.error.data, data_to_json),
)
let error_response =
ErrorResponse(..error_response, error:)
|> BatchResponseItemErrorResponse
BatchResponse([error_response, ..batch])
}
/// A union of the types allowed in a single batch response.
pub type BatchResponseItem(a) {
BatchResponseItemResponse(Response(a))
BatchResponseItemErrorResponse(ErrorResponse(a))
}
pub fn batch_response_item_to_json(item: BatchResponseItem(Json)) {
case item {
BatchResponseItemErrorResponse(msg) ->
error_response_to_json(msg, function.identity)
BatchResponseItemResponse(msg) -> response_to_json(msg, function.identity)
}
}
pub fn batch_response_item_decoder() -> Decoder(BatchResponseItem(Dynamic)) {
let response =
response_decoder(decode.dynamic) |> decode.map(BatchResponseItemResponse)
let error_response =
error_response_decoder(decode.dynamic)
|> decode.map(BatchResponseItemErrorResponse)
decode.one_of(response, [error_response])
}
/// Specifies the version of the JSON-RPC protocol. Only 2.0 is supported.
pub type Version {
V2
}
pub fn version_to_json(_version: Version) -> Json {
json.string("2.0")
}
pub fn version_decoder() -> Decoder(Version) {
use v <- decode.then(decode.string)
case v {
"2.0" -> decode.success(V2)
_ -> decode.failure(V2, "unsupported JSON-RPC version: " <> v)
}
}
/// An identifier established by the Client that MUST contain a String, Number,
/// or NULL value. The value SHOULD normally not be Null.
pub type Id {
StringId(String)
IntId(Int)
NullId
}
/// Creates an Int ID
pub fn id(id: Int) -> Id {
IntId(id)
}
pub fn id_to_json(id: Id) -> Json {
case id {
IntId(id) -> json.int(id)
NullId -> json.null()
StringId(id) -> json.string(id)
}
}
pub fn id_decoder() -> Decoder(Id) {
let string_decoder = decode.string |> decode.map(StringId)
let others =
decode.optional(decode.int |> decode.map(IntId))
|> decode.map(option.unwrap(_, NullId))
decode.one_of(string_decoder, [others])
}
/// An RPC call to a server
pub type Request(params) {
Request(
/// Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0".
jsonrpc: Version,
/// A String containing the name of the method to be invoked. Method names
/// that begin with the word rpc followed by a period character (U+002E or
/// ASCII 46) are reserved for rpc-internal methods and extensions and MUST
/// NOT be used for anything else.
method: String,
/// An identifier established by the Client that MUST contain a String, Number,
/// or NULL value. The value SHOULD normally not be Null.
id: Id,
/// A Structured value that holds the parameter values to be used during the
/// invocation of the method. This member MAY be omitted.
params: Option(params),
)
}
/// Creates a new request with empty params
pub fn request(method method: String, id id: Id) -> Request(params) {
Request(jsonrpc: V2, method:, id:, params: None)
}
/// Sets the params of this request
pub fn request_params(request: Request(a), params: params) -> Request(params) {
Request(..request, params: Some(params))
}
pub fn request_to_json(
request: Request(params),
encode_params: fn(params) -> Json,
) -> Json {
let Request(jsonrpc:, method:, id:, params:) = request
let params = case params {
Some(params) -> [#("params", encode_params(params))]
None -> []
}
json.object([
#("jsonrpc", version_to_json(jsonrpc)),
#("method", json.string(method)),
#("id", id_to_json(id)),
..params
])
}
pub fn request_decoder(
params_decoder: Decoder(params),
) -> Decoder(Request(params)) {
use jsonrpc <- decode.field("jsonrpc", version_decoder())
use method <- decode.field("method", decode.string)
use id <- decode.field("id", id_decoder())
use params <- decode.optional_field(
"params",
None,
decode.optional(params_decoder),
)
decode.success(Request(jsonrpc:, method:, id:, params:))
}
/// A type that can help with type inference for RPC objects that omit optional
/// fields.
pub opaque type Nothing {
Nothing
}
/// Encode json for the Nothing type, which is impossible to create. This
/// is intended for fields you KNOW are omitted, and therefore it will never be
/// ran. It will always return `null` if ran.
pub fn nothing_to_json(_nothing: Nothing) -> Json {
json.null()
}
/// A decoder for the Nothing type, which is impossible to create. This decoder
/// is intended for fields you KNOW are omitted, and therefore it will never be
/// ran. It will always fail if ran
pub fn nothing_decoder() -> Decoder(Nothing) {
decode.failure(Nothing, "Attempted to decode a Nothing type.")
}
/// A notification signifies the Client's lack of interest in the corresponding
/// Response object, and as such no Response object needs to be returned to the
/// client. The Server MUST NOT reply to a Notification, including those that
/// are within a batch request.
///
/// Notifications are not confirmable by definition, since they do not have a
/// Response object to be returned. As such, the Client would not be aware of
/// any errors (like e.g. "Invalid params","Internal error").
pub type Notification(params) {
Notification(
/// Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0".
jsonrpc: Version,
/// A String containing the name of the method to be invoked. Method names
/// that begin with the word rpc followed by a period character (U+002E or
/// ASCII 46) are reserved for rpc-internal methods and extensions and MUST
/// NOT be used for anything else.
method: String,
/// A Structured value that holds the parameter values to be used during the
/// invocation of the method. This member MAY be omitted.
params: Option(params),
)
}
/// Creates a new notification with empty params
pub fn notification(method: String) -> Notification(params) {
Notification(jsonrpc: V2, method:, params: None)
}
/// Sets the params for this notification
pub fn notification_params(
notification: Notification(a),
params: params,
) -> Notification(params) {
Notification(..notification, params: Some(params))
}
pub fn notification_to_json(
notification: Notification(params),
encode_params: fn(params) -> Json,
) -> Json {
let Notification(jsonrpc:, method:, params:) = notification
let params = case params {
Some(params) -> [#("params", encode_params(params))]
None -> []
}
json.object([
#("jsonrpc", version_to_json(jsonrpc)),
#("method", json.string(method)),
..params
])
}
pub fn notification_decoder(
params_decoder: Decoder(params),
) -> Decoder(Notification(params)) {
use jsonrpc <- decode.field("jsonrpc", version_decoder())
use method <- decode.field("method", decode.string)
use params <- decode.optional_field(
"params",
None,
decode.optional(params_decoder),
)
decode.success(Notification(jsonrpc:, method:, params:))
}
/// When an RPC call is made, the Server MUST reply with a Response, except for
/// in the case of Notifications.
pub type Response(result) {
Response(
/// Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0".
jsonrpc: Version,
/// It MUST be the same as the value of the id member in the Request Object.
id: Id,
/// The value of this member is determined by the method invoked on the
/// Server.
result: result,
)
}
/// Creates a new response
pub fn response(result result: result, id id: Id) -> Response(result) {
Response(jsonrpc: V2, id:, result:)
}
pub fn response_to_json(
response: Response(result),
encode_result: fn(result) -> Json,
) -> Json {
let Response(jsonrpc:, id:, result:) = response
json.object([
#("jsonrpc", version_to_json(jsonrpc)),
#("id", id_to_json(id)),
#("result", encode_result(result)),
])
}
pub fn response_decoder(
result_decoder: Decoder(result),
) -> Decoder(Response(result)) {
use jsonrpc <- decode.field("jsonrpc", version_decoder())
use id <- decode.field("id", id_decoder())
use result <- decode.field("result", result_decoder)
decode.success(Response(jsonrpc:, id:, result:))
}
/// When an RPC call encounters an error, the server MUST send an error
/// response, except in the case of Notifications.
pub type ErrorResponse(data) {
ErrorResponse(
/// Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0".
jsonrpc: Version,
/// It MUST be the same as the value of the id member in the Request Object.
/// If there was an error in detecting the id in the Request object (e.g.
/// Parse error/Invalid Request), it MUST be Null.
id: Id,
/// When a rpc call encounters an error, the Response Object MUST contain
/// the error member with a value that is a Object with the following
/// members:
error: ErrorBody(data),
)
}
/// Creates a new error response with empty data. Error code and message are
/// populated with the values from `error`.
pub fn error_response(
error error: JsonRpcError,
id id: Id,
) -> ErrorResponse(data) {
ErrorResponse(
jsonrpc: V2,
id:,
error: ErrorBody(code: error.code, message: error.message, data: None),
)
}
/// Sets the data of this error response
pub fn error_response_data(
error_response: ErrorResponse(a),
data: data,
) -> ErrorResponse(data) {
let error = ErrorBody(..error_response.error, data: Some(data))
ErrorResponse(..error_response, error:)
}
pub fn error_response_to_json(
error_response: ErrorResponse(data),
encode_data: fn(data) -> Json,
) -> Json {
let ErrorResponse(jsonrpc:, id:, error:) = error_response
json.object([
#("jsonrpc", version_to_json(jsonrpc)),
#("id", id_to_json(id)),
#("error", error_to_json(error, encode_data)),
])
}
pub fn error_response_decoder(
data_decoder: Decoder(data),
) -> Decoder(ErrorResponse(data)) {
use jsonrpc <- decode.field("jsonrpc", version_decoder())
use id <- decode.field("id", id_decoder())
use error <- decode.field("error", error_decoder(data_decoder))
decode.success(ErrorResponse(jsonrpc:, id:, error:))
}
/// When an RPC call encounters an error, the Response Object MUST contain the
/// error member
pub type ErrorBody(data) {
ErrorBody(
/// A Number that indicates the error type that occurred.
code: Int,
/// A String providing a short description of the error.
/// The message SHOULD be limited to a concise single sentence.
message: String,
/// A Primitive or Structured value that contains additional information
/// about the error.
/// This may be omitted.
/// The value of this member is defined by the Server (e.g. detailed error
/// information, nested errors etc.).
data: Option(data),
)
}
pub fn error_to_json(
error: ErrorBody(data),
encode_data: fn(data) -> Json,
) -> Json {
let ErrorBody(code:, message:, data:) = error
let data = case data {
Some(data) -> [#("data", encode_data(data))]
None -> []
}
json.object([
#("code", json.int(code)),
#("message", json.string(message)),
..data
])
}
pub fn error_decoder(data_decoder: Decoder(data)) -> Decoder(ErrorBody(data)) {
use code <- decode.field("code", decode.int)
use message <- decode.field("message", decode.string)
use data <- decode.optional_field("data", None, decode.optional(data_decoder))
decode.success(ErrorBody(code:, message:, data:))
}
pub fn error_response_from(
json_error: json.DecodeError,
id: Id,
) -> ErrorResponse(Nothing) {
case json_error {
json.UnableToDecode(errors) -> from_decode_errors(errors, id)
_ -> error_response(parse_error, id)
}
}
fn from_decode_errors(errors: List(decode.DecodeError), id: Id) {
let params_error =
list.all(errors, fn(error) { list.first(error.path) == Ok("params") })
case params_error {
True -> error_response(invalid_params, id)
False -> error_response(invalid_request, id)
}
}
// ERRORS ----------------------------------------------------------------------
/// Invalid JSON was received by the server. An error occurred on the server
/// while parsing the JSON text.
pub const parse_error = JsonRpcError(-32_700, "Parse error")
/// The JSON sent is not a valid Request object.
pub const invalid_request = JsonRpcError(-32_600, "Invalid Request")
/// The method does not exist / is not available.
pub const method_not_found = JsonRpcError(-32_601, "Method not found")
/// Invalid method parameter(s).
pub const invalid_params = JsonRpcError(-32_602, "Invalid params")
/// Internal JSON-RPC error.
pub const internal_error = JsonRpcError(-32_603, "Internal error")
/// Represents the error code and associated message. Error types provided by the JSON-RPC spec are already defined in the module.
pub opaque type JsonRpcError {
JsonRpcError(code: Int, message: String)
}
/// Retrieve this error's code
pub fn error_code(error: JsonRpcError) {
error.code
}
/// Retrieve this error's message
pub fn error_message(error: JsonRpcError) {
error.message
}
/// An error defined for your specific application.
/// The error code MUST not be within the range -32768 to -32000, otherwise
/// `Error(Nil)` will be returned.
/// The message SHOULD be limited to a concise single sentence.
pub fn application_error(
code: Int,
message: String,
) -> Result(JsonRpcError, Nil) {
case code >= -32_768 && code <= -32_000 {
True -> Error(Nil)
False -> Ok(JsonRpcError(code, message))
}
}
/// An error reserved for implementation-defined server-errors.
/// The error code MUST be within the range -32099 to -32000, otherwise
/// `Error(Nil)` will be returned.
pub fn server_error(code: Int) -> Result(JsonRpcError, Nil) {
case code >= -32_099 && code <= -32_000 {
True -> Ok(JsonRpcError(code, "Server error"))
False -> Error(Nil)
}
}
/// Get the appropriate `JsonRpcError` based on a request's `json.DecodeError`.
pub fn json_error(error: json.DecodeError) -> JsonRpcError {
case error {
json.UnableToDecode(errors) -> decode_errors(errors)
_ -> parse_error
}
}
/// Get the appropriate `JsonRpcError` based on a request's `decode.DecodeError`s.
pub fn decode_errors(errors: List(decode.DecodeError)) -> JsonRpcError {
case list.all(errors, param_error) {
True -> invalid_params
False -> invalid_request
}
}
fn param_error(error: decode.DecodeError) {
list.first(error.path) == Ok("params")
}