mirror of
https://github.com/luxfi/safe-ios.git
synced 2026-10-30 15:19:20 +00:00
467 lines
15 KiB
Swift
467 lines
15 KiB
Swift
//
|
|
// JsonRpc2.swift
|
|
// JsonRpc2
|
|
//
|
|
// Created by Dmitry Bespalov on 15.12.21.
|
|
//
|
|
|
|
import Foundation
|
|
import Json
|
|
|
|
public enum JsonRpc2 {
|
|
public struct Request {
|
|
public var jsonrpc: String
|
|
public var method: String
|
|
public var params: Params?
|
|
public var id: Id?
|
|
|
|
public init(jsonrpc: String, method: String, params: JsonRpc2.Params?, id: JsonRpc2.Id?) {
|
|
self.jsonrpc = jsonrpc
|
|
self.method = method
|
|
self.params = params
|
|
self.id = id
|
|
}
|
|
}
|
|
|
|
public enum Params {
|
|
case array(Json.Array)
|
|
case object(Json.Object)
|
|
}
|
|
|
|
public enum Id: Hashable {
|
|
case string(String)
|
|
case int(Int)
|
|
case uint(UInt)
|
|
case double(Double)
|
|
case null
|
|
}
|
|
|
|
public struct Response {
|
|
public var jsonrpc: String
|
|
public var result: Json.Element?
|
|
public var error: Error?
|
|
public var id: Id
|
|
|
|
public init(jsonrpc: String, result: Json.Element?, error: JsonRpc2.Error?, id: JsonRpc2.Id) {
|
|
self.jsonrpc = jsonrpc
|
|
self.result = result
|
|
self.error = error
|
|
self.id = id
|
|
}
|
|
}
|
|
|
|
public struct Error: Swift.Error {
|
|
public var code: Int
|
|
public var message: String
|
|
public var data: Json.Element?
|
|
|
|
public init(code: Int, message: String, data: Json.Element?) {
|
|
self.code = code
|
|
self.message = message
|
|
self.data = data
|
|
}
|
|
}
|
|
|
|
public struct BatchRequest {
|
|
public var requests: [Request?]
|
|
|
|
public init(requests: [Request?]) throws {
|
|
// we'd like to throw error if we're empty or all are nils
|
|
// otherwise the 'nil' are as a placeholder for some invalid request.
|
|
let validatedRequests = requests.compactMap { $0 }
|
|
if validatedRequests.isEmpty {
|
|
throw Error(code: -32600, message: "Invalid Request", data: nil)
|
|
}
|
|
self.requests = requests
|
|
}
|
|
}
|
|
|
|
public enum BatchResponse {
|
|
case response(Response)
|
|
case array([Response])
|
|
}
|
|
}
|
|
|
|
// support for typed params | response results | error data:
|
|
// since all of them are codable, one can encode them and
|
|
// then decode to a typed param in order to not deal with other stuff.
|
|
|
|
// request encoding/decoding: compiler-generated
|
|
extension JsonRpc2.Request: Codable {}
|
|
|
|
extension JsonRpc2.Params: Encodable {
|
|
public func encode(to encoder: Encoder) throws {
|
|
// encode each associated value
|
|
var container = encoder.singleValueContainer()
|
|
switch self {
|
|
case .array(let value):
|
|
try container.encode(value)
|
|
case .object(let value):
|
|
try container.encode(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2.Params: Decodable {
|
|
public init(from decoder: Decoder) throws {
|
|
// try array
|
|
let container = try decoder.singleValueContainer()
|
|
|
|
if let value = try? container.decode(Json.Array.self) {
|
|
self = .array(value)
|
|
}
|
|
// try object
|
|
else if let value = try? container.decode(Json.Object.self) {
|
|
self = .object(value)
|
|
}
|
|
// fail with error
|
|
else {
|
|
let error = DecodingError.typeMismatch(
|
|
Self.self,
|
|
DecodingError.Context(codingPath: decoder.codingPath,
|
|
debugDescription: "Unexpected params type",
|
|
underlyingError: nil)
|
|
)
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2.Id: Encodable {
|
|
public func encode(to encoder: Encoder) throws {
|
|
// encode each associated value or null
|
|
var container = encoder.singleValueContainer()
|
|
switch self {
|
|
case .string(let value):
|
|
try container.encode(value)
|
|
case .int(let value):
|
|
try container.encode(value)
|
|
case .uint(let value):
|
|
try container.encode(value)
|
|
case .double(let value):
|
|
try container.encode(value)
|
|
case .null:
|
|
try container.encodeNil()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2.Id: Decodable {
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
|
|
// try int
|
|
if let value = try? container.decode(Int.self) {
|
|
self = .int(value)
|
|
}
|
|
// try uint
|
|
else if let value = try? container.decode(UInt.self) {
|
|
self = .uint(value)
|
|
}
|
|
// try double
|
|
else if let value = try? container.decode(Double.self) {
|
|
self = .double(value)
|
|
}
|
|
// try string
|
|
else if let value = try? container.decode(String.self) {
|
|
self = .string(value)
|
|
}
|
|
// try null
|
|
else if container.decodeNil() {
|
|
self = .null
|
|
}
|
|
// fail with error
|
|
else {
|
|
let error = DecodingError.typeMismatch(
|
|
Self.self,
|
|
DecodingError.Context(codingPath: decoder.codingPath,
|
|
debugDescription: "Unexpected id type",
|
|
underlyingError: nil)
|
|
)
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// id: comparable
|
|
// only works if both are numbers int uint will be cast to Int(clamping:) and ints will be cast to Double
|
|
// strings are compared only to strings
|
|
// nulls and other combinations return false
|
|
// otherwise they're not comparable (returns false).
|
|
extension JsonRpc2.Id: Comparable {
|
|
public static func < (lhs: JsonRpc2.Id, rhs: JsonRpc2.Id) -> Bool {
|
|
switch (lhs, rhs) {
|
|
|
|
case let (.int(a), .int(b)):
|
|
return a < b
|
|
case let (.int(a), .uint(b)):
|
|
return a < b
|
|
case let (.int(a), .double(b)):
|
|
return Double(a) < b
|
|
|
|
|
|
case let (.uint(a), .uint(b)):
|
|
return a < b
|
|
case let (.uint(a), .int(b)):
|
|
return Int(clamping: a) < b
|
|
case let (.uint(a), .double(b)):
|
|
return Double(a) < b
|
|
|
|
case let (.string(a), .string(b)):
|
|
return a < b
|
|
|
|
case let (.double(a), .double(b)):
|
|
return a < b
|
|
case let (.double(a), .int(b)):
|
|
return a < Double(b)
|
|
case let (.double(a), .uint(b)):
|
|
return a < Double(b)
|
|
|
|
case (.null, .null):
|
|
return false
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// response encoding/decoding: compiler-generated
|
|
extension JsonRpc2.Response: Codable {
|
|
enum Key: String, CodingKey {
|
|
case jsonrpc
|
|
case result
|
|
case error
|
|
case id
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: Key.self)
|
|
jsonrpc = try container.decode(String.self, forKey: .jsonrpc)
|
|
if container.contains(.result) {
|
|
result = try container.decode(Json.Element.self, forKey: .result)
|
|
error = nil
|
|
} else {
|
|
result = nil
|
|
error = try container.decodeIfPresent(JsonRpc2.Error.self, forKey: .error)
|
|
}
|
|
id = try container.decode(JsonRpc2.Id.self, forKey: .id)
|
|
}
|
|
}
|
|
|
|
// error encoding/decoding: compiler-generated
|
|
extension JsonRpc2.Error: Codable {}
|
|
|
|
extension JsonRpc2.BatchRequest: Encodable {
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
try container.encode(requests)
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2.BatchRequest: Decodable {
|
|
public init(from decoder: Decoder) throws {
|
|
var container = try decoder.unkeyedContainer()
|
|
var requests = [JsonRpc2.Request?]()
|
|
// try decoding each request and skip the invalid requests
|
|
if let count = container.count {
|
|
for _ in (0..<count) {
|
|
let request: JsonRpc2.Request?
|
|
do {
|
|
request = try container.decode(JsonRpc2.Request.self)
|
|
} catch {
|
|
// error decoding request, it is invalid. We put a placeholder - nil
|
|
request = nil
|
|
// we must continue, so we should decode a generic json
|
|
_ = try container.decode(Json.Element.self)
|
|
}
|
|
requests.append(request)
|
|
}
|
|
} else {
|
|
requests = try container.decode([JsonRpc2.Request].self)
|
|
}
|
|
try self.init(requests: requests)
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2.BatchResponse: Encodable {
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
switch self {
|
|
case .response(let value):
|
|
try container.encode(value)
|
|
case .array(let value):
|
|
try container.encode(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2.BatchResponse: Decodable {
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
// try array response
|
|
if let value = try? container.decode([JsonRpc2.Response].self) {
|
|
self = .array(value)
|
|
}
|
|
// try single response
|
|
else if let value = try? container.decode(JsonRpc2.Response.self) {
|
|
self = .response(value)
|
|
}
|
|
// fail with error
|
|
else {
|
|
let error = DecodingError.typeMismatch(
|
|
Self.self,
|
|
DecodingError.Context(codingPath: decoder.codingPath,
|
|
debugDescription: "Unexpected batch response",
|
|
underlyingError: nil)
|
|
)
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// default implementation
|
|
extension JsonRpc2.Params: EncodableConvertible, DecodableConvertible {}
|
|
|
|
|
|
// utility to create requests and responses for the same rpc call
|
|
@available(*, deprecated: 13, message: "Use JsonRpc2MethodCall or JsonRpc2MethodWithCompletion")
|
|
public protocol JsonRpc2Method {
|
|
static var name: String { get }
|
|
associatedtype Return
|
|
}
|
|
|
|
extension JsonRpc2Method {
|
|
public static var name: String { String(describing: self) }
|
|
}
|
|
|
|
extension JsonRpc2Method where Self: Encodable {
|
|
public func request(id: JsonRpc2.Id? = nil) throws -> JsonRpc2.Request {
|
|
try JsonRpc2.Request(
|
|
jsonrpc: "2.0",
|
|
method: Self.name,
|
|
params: JsonRpc2.Params(value: self),
|
|
id: id)
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2Method where Return: Decodable {
|
|
public func result(from element: Json.Element) throws -> Return {
|
|
try element.convert(to: Return.self)
|
|
}
|
|
}
|
|
|
|
public protocol JsonRpc2MethodCall {
|
|
var methodName: String { get }
|
|
func request(id: JsonRpc2.Id?) throws -> JsonRpc2.Request
|
|
func handle(response: JsonRpc2.Response)
|
|
func handle(error: Error)
|
|
}
|
|
|
|
public protocol JsonRpc2MethodWithCompletion: JsonRpc2MethodCall {
|
|
var completion: (Result<ReturnType, Error>) -> Void { get }
|
|
associatedtype ReturnType
|
|
func convert(json: Json.Element) throws -> ReturnType
|
|
}
|
|
|
|
extension JsonRpc2MethodCall {
|
|
public var methodName: String {
|
|
String(describing: type(of: self))
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2MethodCall where Self: Encodable {
|
|
public func request(id: JsonRpc2.Id?) throws -> JsonRpc2.Request {
|
|
try JsonRpc2.Request(
|
|
jsonrpc: "2.0",
|
|
method: methodName,
|
|
params: JsonRpc2.Params(value: self),
|
|
id: id)
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2MethodWithCompletion where ReturnType: Decodable {
|
|
public func handle(response: JsonRpc2.Response) {
|
|
if let error = response.error {
|
|
handle(error: error)
|
|
return
|
|
}
|
|
guard let json = response.result else {
|
|
return
|
|
}
|
|
do {
|
|
let result = try convert(json: json)
|
|
completion(.success(result))
|
|
} catch {
|
|
handle(error: error)
|
|
}
|
|
}
|
|
|
|
public func convert(json: Json.Element) throws -> ReturnType {
|
|
try json.convert(to: ReturnType.self)
|
|
}
|
|
|
|
public func handle(error: Error) {
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2.Error {
|
|
// JSON RPC Errors
|
|
// | code | message | meaning |
|
|
// | ---------------- | ---------------- | -------------------------------------------------- |
|
|
// | -32700 | Parse error | Invalid JSON was received by the server. |
|
|
// | -32600 | Invalid Request | The JSON sent is not a valid Request object. |
|
|
// | -32601 | Method not found | The method does not exist or 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. |
|
|
|
|
// Invalid JSON was received by the server.
|
|
public static let parseError = JsonRpc2.Error(code: -32700, message: "Parse error", data: nil)
|
|
|
|
// The JSON sent is not a valid Request object.
|
|
public static let invalidRequest = JsonRpc2.Error(code: -32600, message: "Invalid Request", data: nil)
|
|
|
|
// The method does not exist or is not available.
|
|
public static let methodNotFound = JsonRpc2.Error(code: -32601, message: "Method not found", data: nil)
|
|
|
|
// Invalid method parameter(s).
|
|
public static let invalidParams = JsonRpc2.Error(code: -32602, message: "Invalid params", data: nil)
|
|
|
|
// Internal JSON-RPC error.
|
|
public static let internalError = JsonRpc2.Error(code: -32603, message: "Internal error", data: nil)
|
|
|
|
// Reserved for implementation-defined server-errors. Start of the range.
|
|
public static let serverError00 = JsonRpc2.Error(code: -32000, message: "Server error", data: nil)
|
|
|
|
// Reserved for implementation-defined server-errors. End of the range.
|
|
public static let serverError99 = JsonRpc2.Error(code: -32099, message: "Server error", data: nil)
|
|
}
|
|
|
|
extension JsonRpc2.Error {
|
|
public func with(error: Swift.Error) -> JsonRpc2.Error {
|
|
with(data: Json.NSError(error as NSError).toJson())
|
|
}
|
|
|
|
public func with(data: Json.Element) -> JsonRpc2.Error {
|
|
var newValue = self
|
|
newValue.data = data
|
|
return newValue
|
|
}
|
|
}
|
|
|
|
extension JsonRpc2.Error: LocalizedError {
|
|
public var errorDescription: String? {
|
|
var result = "Error \(code): \(message)"
|
|
if let json = data {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
|
|
if let data = try? encoder.encode(json), let string = String(data: data, encoding: .utf8) {
|
|
result += "\nData: \(string)"
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|