mirror of
https://github.com/logos-storage/nim-json-rpc.git
synced 2026-01-03 22:23:09 +00:00
* Improve resilience against null fields * Fix client processMessage when handling error * Improve both client and server resilience against fields and elements with null value
322 lines
8.4 KiB
Nim
322 lines
8.4 KiB
Nim
# json-rpc
|
|
# Copyright (c) 2023 Status Research & Development GmbH
|
|
# Licensed under either of
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
# at your option.
|
|
# This file may not be copied, modified, or distributed except according to
|
|
# those terms.
|
|
|
|
import
|
|
std/hashes,
|
|
results,
|
|
json_serialization,
|
|
json_serialization/stew/results as jser_results
|
|
|
|
export
|
|
results,
|
|
json_serialization
|
|
|
|
# This module implements JSON-RPC 2.0 Specification
|
|
# https://www.jsonrpc.org/specification
|
|
|
|
type
|
|
# Special object of Json-RPC 2.0
|
|
JsonRPC2* = object
|
|
|
|
RequestParamKind* = enum
|
|
rpPositional
|
|
rpNamed
|
|
|
|
ParamDescRx* = object
|
|
kind* : JsonValueKind
|
|
param*: JsonString
|
|
|
|
ParamDescNamed* = object
|
|
name*: string
|
|
value*: JsonString
|
|
|
|
# Request params received by server
|
|
RequestParamsRx* = object
|
|
case kind*: RequestParamKind
|
|
of rpPositional:
|
|
positional*: seq[ParamDescRx]
|
|
of rpNamed:
|
|
named*: seq[ParamDescNamed]
|
|
|
|
# Request params sent by client
|
|
RequestParamsTx* = object
|
|
case kind*: RequestParamKind
|
|
of rpPositional:
|
|
positional*: seq[JsonString]
|
|
of rpNamed:
|
|
named*: seq[ParamDescNamed]
|
|
|
|
RequestIdKind* = enum
|
|
riNull
|
|
riNumber
|
|
riString
|
|
|
|
RequestId* = object
|
|
case kind*: RequestIdKind
|
|
of riNumber:
|
|
num*: int
|
|
of riString:
|
|
str*: string
|
|
of riNull:
|
|
discard
|
|
|
|
# Request received by server
|
|
RequestRx* = object
|
|
jsonrpc* : results.Opt[JsonRPC2]
|
|
id* : RequestId
|
|
`method`*: results.Opt[string]
|
|
params* : RequestParamsRx
|
|
|
|
# Request sent by client
|
|
RequestTx* = object
|
|
jsonrpc* : JsonRPC2
|
|
id* : results.Opt[RequestId]
|
|
`method`*: string
|
|
params* : RequestParamsTx
|
|
|
|
ResponseError* = object
|
|
code* : int
|
|
message*: string
|
|
data* : results.Opt[JsonString]
|
|
|
|
ResponseKind* = enum
|
|
rkResult
|
|
rkError
|
|
|
|
# Response sent by server
|
|
ResponseTx* = object
|
|
jsonrpc* : JsonRPC2
|
|
id* : RequestId
|
|
case kind*: ResponseKind
|
|
of rkResult:
|
|
result* : JsonString
|
|
of rkError:
|
|
error* : ResponseError
|
|
|
|
# Response received by client
|
|
ResponseRx* = object
|
|
jsonrpc*: results.Opt[JsonRPC2]
|
|
id* : results.Opt[RequestId]
|
|
result* : JsonString
|
|
error* : results.Opt[ResponseError]
|
|
|
|
ReBatchKind* = enum
|
|
rbkSingle
|
|
rbkMany
|
|
|
|
RequestBatchRx* = object
|
|
case kind*: ReBatchKind
|
|
of rbkMany:
|
|
many* : seq[RequestRx]
|
|
of rbkSingle:
|
|
single*: RequestRx
|
|
|
|
RequestBatchTx* = object
|
|
case kind*: ReBatchKind
|
|
of rbkMany:
|
|
many* : seq[RequestTx]
|
|
of rbkSingle:
|
|
single*: RequestTx
|
|
|
|
ResponseBatchRx* = object
|
|
case kind*: ReBatchKind
|
|
of rbkMany:
|
|
many* : seq[ResponseRx]
|
|
of rbkSingle:
|
|
single*: ResponseRx
|
|
|
|
ResponseBatchTx* = object
|
|
case kind*: ReBatchKind
|
|
of rbkMany:
|
|
many* : seq[ResponseTx]
|
|
of rbkSingle:
|
|
single*: ResponseTx
|
|
|
|
# don't mix the json-rpc system encoding with the
|
|
# actual response/params encoding
|
|
createJsonFlavor JrpcSys,
|
|
automaticObjectSerialization = false,
|
|
requireAllFields = false,
|
|
omitOptionalFields = true, # Skip optional fields==none in Writer
|
|
allowUnknownFields = true,
|
|
skipNullFields = true # Skip optional fields==null in Reader
|
|
|
|
ResponseError.useDefaultSerializationIn JrpcSys
|
|
RequestTx.useDefaultWriterIn JrpcSys
|
|
RequestRx.useDefaultReaderIn JrpcSys
|
|
|
|
const
|
|
JsonRPC2Literal = JsonString("\"2.0\"")
|
|
|
|
{.push gcsafe, raises: [].}
|
|
|
|
func hash*(x: RequestId): hashes.Hash =
|
|
var h = 0.Hash
|
|
case x.kind:
|
|
of riNumber: h = h !& hash(x.num)
|
|
of riString: h = h !& hash(x.str)
|
|
of riNull: h = h !& hash("null")
|
|
result = !$(h)
|
|
|
|
func `$`*(x: RequestId): string =
|
|
case x.kind:
|
|
of riNumber: $x.num
|
|
of riString: x.str
|
|
of riNull: "null"
|
|
|
|
func `==`*(a, b: RequestId): bool =
|
|
if a.kind != b.kind:
|
|
return false
|
|
case a.kind
|
|
of riNumber: a.num == b.num
|
|
of riString: a.str == b.str
|
|
of riNull: true
|
|
|
|
func meth*(rx: RequestRx): Opt[string] =
|
|
rx.`method`
|
|
|
|
proc readValue*(r: var JsonReader[JrpcSys], val: var JsonRPC2)
|
|
{.gcsafe, raises: [IOError, JsonReaderError].} =
|
|
let version = r.parseAsString()
|
|
if version != JsonRPC2Literal:
|
|
r.raiseUnexpectedValue("Invalid JSON-RPC version, want=" &
|
|
JsonRPC2Literal.string & " got=" & version.string)
|
|
|
|
proc writeValue*(w: var JsonWriter[JrpcSys], val: JsonRPC2)
|
|
{.gcsafe, raises: [IOError].} =
|
|
w.writeValue JsonRPC2Literal
|
|
|
|
proc readValue*(r: var JsonReader[JrpcSys], val: var RequestId)
|
|
{.gcsafe, raises: [IOError, JsonReaderError].} =
|
|
let tok = r.tokKind
|
|
case tok
|
|
of JsonValueKind.Number:
|
|
val = RequestId(kind: riNumber, num: r.parseInt(int))
|
|
of JsonValueKind.String:
|
|
val = RequestId(kind: riString, str: r.parseString())
|
|
of JsonValueKind.Null:
|
|
val = RequestId(kind: riNull)
|
|
r.parseNull()
|
|
else:
|
|
r.raiseUnexpectedValue("Invalid RequestId, must be Number, String, or Null, got=" & $tok)
|
|
|
|
proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestId)
|
|
{.gcsafe, raises: [IOError].} =
|
|
case val.kind
|
|
of riNumber: w.writeValue val.num
|
|
of riString: w.writeValue val.str
|
|
of riNull: w.writeValue JsonString("null")
|
|
|
|
proc readValue*(r: var JsonReader[JrpcSys], val: var RequestParamsRx)
|
|
{.gcsafe, raises: [IOError, SerializationError].} =
|
|
let tok = r.tokKind
|
|
case tok
|
|
of JsonValueKind.Array:
|
|
val = RequestParamsRx(kind: rpPositional)
|
|
r.parseArray:
|
|
val.positional.add ParamDescRx(
|
|
kind: r.tokKind(),
|
|
param: r.parseAsString(),
|
|
)
|
|
of JsonValueKind.Object:
|
|
val = RequestParamsRx(kind: rpNamed)
|
|
for key in r.readObjectFields():
|
|
val.named.add ParamDescNamed(
|
|
name: key,
|
|
value: r.parseAsString(),
|
|
)
|
|
else:
|
|
r.raiseUnexpectedValue("RequestParam must be either array or object, got=" & $tok)
|
|
|
|
proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestParamsTx)
|
|
{.gcsafe, raises: [IOError].} =
|
|
case val.kind
|
|
of rpPositional:
|
|
w.writeArray val.positional
|
|
of rpNamed:
|
|
w.beginRecord RequestParamsTx
|
|
for x in val.named:
|
|
w.writeField(x.name, x.value)
|
|
w.endRecord()
|
|
|
|
proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseTx)
|
|
{.gcsafe, raises: [IOError].} =
|
|
w.beginRecord ResponseTx
|
|
w.writeField("jsonrpc", val.jsonrpc)
|
|
w.writeField("id", val.id)
|
|
if val.kind == rkResult:
|
|
w.writeField("result", val.result)
|
|
else:
|
|
w.writeField("error", val.error)
|
|
w.endRecord()
|
|
|
|
proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseRx)
|
|
{.gcsafe, raises: [IOError, SerializationError].} =
|
|
# We need to overload ResponseRx reader because
|
|
# we don't want to skip null fields
|
|
r.parseObjectWithoutSkip(key):
|
|
case key
|
|
of "jsonrpc": r.readValue(val.jsonrpc)
|
|
of "id" : r.readValue(val.id)
|
|
of "result" : val.result = r.parseAsString()
|
|
of "error" : r.readValue(val.error)
|
|
else: discard
|
|
|
|
proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestBatchTx)
|
|
{.gcsafe, raises: [IOError].} =
|
|
if val.kind == rbkMany:
|
|
w.writeArray(val.many)
|
|
else:
|
|
w.writeValue(val.single)
|
|
|
|
proc readValue*(r: var JsonReader[JrpcSys], val: var RequestBatchRx)
|
|
{.gcsafe, raises: [IOError, SerializationError].} =
|
|
let tok = r.tokKind
|
|
case tok
|
|
of JsonValueKind.Array:
|
|
val = RequestBatchRx(kind: rbkMany)
|
|
r.readValue(val.many)
|
|
of JsonValueKind.Object:
|
|
val = RequestBatchRx(kind: rbkSingle)
|
|
r.readValue(val.single)
|
|
else:
|
|
r.raiseUnexpectedValue("RequestBatch must be either array or object, got=" & $tok)
|
|
|
|
proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseBatchTx)
|
|
{.gcsafe, raises: [IOError].} =
|
|
if val.kind == rbkMany:
|
|
w.writeArray(val.many)
|
|
else:
|
|
w.writeValue(val.single)
|
|
|
|
proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseBatchRx)
|
|
{.gcsafe, raises: [IOError, SerializationError].} =
|
|
let tok = r.tokKind
|
|
case tok
|
|
of JsonValueKind.Array:
|
|
val = ResponseBatchRx(kind: rbkMany)
|
|
r.readValue(val.many)
|
|
of JsonValueKind.Object:
|
|
val = ResponseBatchRx(kind: rbkSingle)
|
|
r.readValue(val.single)
|
|
else:
|
|
r.raiseUnexpectedValue("ResponseBatch must be either array or object, got=" & $tok)
|
|
|
|
proc toTx*(params: RequestParamsRx): RequestParamsTx =
|
|
case params.kind:
|
|
of rpPositional:
|
|
result = RequestParamsTx(kind: rpPositional)
|
|
for x in params.positional:
|
|
result.positional.add x.param
|
|
of rpNamed:
|
|
result = RequestParamsTx(kind: rpNamed)
|
|
result.named = params.named
|
|
|
|
{.pop.}
|