├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .spi.yml ├── .gitignore ├── .editorconfig ├── Sources └── JSONRPC │ ├── Documentation.docc │ └── JSONRPC.md │ ├── DataChannel.swift │ ├── JSONRPCError.swift │ ├── DataChannel+Stdio.swift │ ├── DataChannel+WebSocket.swift │ ├── JSONId.swift │ ├── DataChannel+PredefinedMessages.swift │ ├── AsyncStreamPolyfill.swift │ ├── DataChannel+Actor.swift │ ├── JSONValue.swift │ ├── JSONRPCSession.swift │ ├── Protocol.swift │ └── JSONValueDecoder.swift ├── Package.swift ├── Tests └── JSONRPCTests │ ├── JSONIdTests.swift │ ├── DataChannelTests.swift │ ├── XCTest+Fulfillment.swift │ ├── JSONValueTests.swift │ ├── ProtocolTests.swift │ ├── JSONRPCSessionTests.swift │ └── JSONValueDecoderTests.swift ├── LICENSE ├── README.md └── CODE_OF_CONDUCT.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [mattmassicotte] 2 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [JSONRPC] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | /Carthage 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /Sources/JSONRPC/Documentation.docc/JSONRPC.md: -------------------------------------------------------------------------------- 1 | # ``JSONRPC`` 2 | 3 | A simple Swift library for JSON-RPC 4 | 5 | ## Overview 6 | 7 | JSONRPC features strong type-safety and makes no assumptions about the underlying transport stream. 8 | -------------------------------------------------------------------------------- /Sources/JSONRPC/DataChannel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Provides reading and writing facilities. 4 | public struct DataChannel: Sendable { 5 | public typealias WriteHandler = @Sendable (Data) async throws -> Void 6 | public typealias DataSequence = AsyncStream 7 | 8 | public let writeHandler: WriteHandler 9 | public let dataSequence: DataSequence 10 | 11 | public init(writeHandler: @escaping WriteHandler, dataSequence: DataSequence) { 12 | self.writeHandler = writeHandler 13 | self.dataSequence = dataSequence 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/JSONRPC/JSONRPCError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONRPCError.swift 3 | // JSONRPC 4 | // 5 | // Created by Matthew Massicotte on 2021-07-09. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias JSONRPCError = Int 11 | 12 | public struct JSONRPCErrors { 13 | public static let parse: JSONRPCError = -32700 14 | public static let invalidRequest: JSONRPCError = 32600 15 | public static let methodNotFound: JSONRPCError = 32601 16 | public static let invalidParams: JSONRPCError = 32602 17 | public static let internalError: JSONRPCError = 32603 18 | } 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let settings: [SwiftSetting] = [ 6 | .enableExperimentalFeature("StrictConcurrency") 7 | ] 8 | 9 | let package = Package( 10 | name: "JSONRPC", 11 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], 12 | products: [ 13 | .library(name: "JSONRPC", targets: ["JSONRPC"]), 14 | ], 15 | dependencies: [ 16 | ], 17 | targets: [ 18 | .target(name: "JSONRPC", dependencies: [], swiftSettings: settings), 19 | .testTarget(name: "JSONRPCTests", dependencies: ["JSONRPC"], swiftSettings: settings), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /Tests/JSONRPCTests/JSONIdTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import JSONRPC 3 | 4 | final class JSONIdTests: XCTestCase { 5 | func testNumericEncode() throws { 6 | let obj = JSONId(1) 7 | 8 | let data = try JSONEncoder().encode(obj) 9 | 10 | XCTAssertEqual(data, "1".data(using: .utf8)!) 11 | } 12 | 13 | func testIntegerLiteral() throws { 14 | let obj: JSONId = 1 15 | 16 | XCTAssertEqual(obj, JSONId.numericId(1)) 17 | } 18 | 19 | func testStringEncode() throws { 20 | let obj = JSONId("1") 21 | 22 | let data = try JSONEncoder().encode(obj) 23 | 24 | XCTAssertEqual(data, "\"1\"".data(using: .utf8)!) 25 | } 26 | 27 | func testStringLiteral() throws { 28 | let obj: JSONId = "1" 29 | 30 | XCTAssertEqual(obj, JSONId.stringId("1")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/JSONRPC/DataChannel+Stdio.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(Linux) 3 | import Glibc 4 | #else 5 | import Darwin.C 6 | #endif 7 | 8 | extension FileHandle { 9 | public var dataStream: AsyncStream { 10 | let (stream, continuation) = AsyncStream.makeStream() 11 | 12 | readabilityHandler = { handle in 13 | let data = handle.availableData 14 | 15 | if data.isEmpty { 16 | handle.readabilityHandler = nil 17 | continuation.finish() 18 | return 19 | } 20 | 21 | continuation.yield(data) 22 | } 23 | 24 | return stream 25 | } 26 | } 27 | 28 | extension DataChannel { 29 | @available(*, deprecated, renamed: "stdio", message: "Use stdio instead") 30 | public static func stdioPipe() -> DataChannel { 31 | stdio() 32 | } 33 | 34 | public static func stdio(flushWrites: Bool = true) -> DataChannel { 35 | 36 | let writeHandler: DataChannel.WriteHandler = { data in 37 | FileHandle.standardOutput.write(data) 38 | if flushWrites { 39 | fflush(stdout) 40 | } 41 | } 42 | 43 | return DataChannel(writeHandler: writeHandler, dataSequence: FileHandle.standardInput.dataStream) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.md' 9 | - 'CODE_OF_CONDUCT.md' 10 | - '.editorconfig' 11 | - '.spi.yml' 12 | pull_request: 13 | branches: 14 | - main 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | test: 22 | name: Test 23 | runs-on: macOS-15 24 | timeout-minutes: 30 25 | env: 26 | DEVELOPER_DIR: /Applications/Xcode_16.3.app 27 | strategy: 28 | matrix: 29 | destination: 30 | - "platform=macOS" 31 | - "platform=macOS,variant=Mac Catalyst" 32 | - "platform=iOS Simulator,name=iPhone 16" 33 | - "platform=tvOS Simulator,name=Apple TV" 34 | - "platform=watchOS Simulator,name=Apple Watch Series 10 (42mm)" 35 | - "platform=visionOS Simulator,name=Apple Vision Pro" 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Test platform ${{ matrix.destination }} 39 | run: set -o pipefail && xcodebuild -scheme JSONRPC -destination "${{ matrix.destination }}" test | xcbeautify 40 | 41 | linux_test: 42 | name: Test Linux 43 | runs-on: ubuntu-latest 44 | timeout-minutes: 30 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | - name: Swiftly 49 | uses: vapor/swiftly-action@v0.2.0 50 | with: 51 | toolchain: 6.1.0 52 | - name: Test 53 | run: swift test 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Chime 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Sources/JSONRPC/DataChannel+WebSocket.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if !os(Linux) 4 | extension DataChannel { 5 | /// A channel that facilitates communication over WebSockets. 6 | public static func webSocket( 7 | url: URL, 8 | terminationHandler: @escaping @Sendable () -> Void 9 | ) throws -> DataChannel { 10 | let socketSession: URLSession = .init(configuration: .default) 11 | let socket: URLSessionWebSocketTask = socketSession.webSocketTask(with: url) 12 | 13 | socket.resume() 14 | 15 | let (stream, continuation) = DataSequence.makeStream() 16 | 17 | Task { 18 | while socket.state == .running { 19 | do { 20 | let message = try await socket.receive() 21 | switch message { 22 | case .data(let data): 23 | continuation.yield(data) 24 | case .string(let string): 25 | continuation.yield(Data(string.utf8)) 26 | @unknown default: 27 | fatalError("Unhandled message type") 28 | } 29 | } catch { 30 | if socket.state == .canceling { 31 | terminationHandler() 32 | } 33 | continuation.finish() 34 | throw error 35 | } 36 | } 37 | } 38 | 39 | let writeHandler: DataChannel.WriteHandler = { 40 | try await socket.send(.data($0)) 41 | } 42 | 43 | return DataChannel(writeHandler: writeHandler, dataSequence: stream) 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Tests/JSONRPCTests/DataChannelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import JSONRPC 3 | 4 | final class DataChannelTests: XCTestCase { 5 | func testEmptyChannelBlocking() async throws { 6 | let channel = DataActor(queue: Array()) 7 | 8 | let receiveTask = Task { 9 | let receivedData = await channel.recv() 10 | return String(data: receivedData, encoding: .utf8)! 11 | } 12 | 13 | // try await Task.sleep(for: Duration.seconds(0.05)) 14 | while await channel.numBlocked == 0 { 15 | continue 16 | } 17 | 18 | let msg = "hello" 19 | await channel.send(msg.data(using: .utf8)!) 20 | let receivedMsg = await receiveTask.result 21 | #if compiler(>=6.0) 22 | XCTAssertEqual(msg, receivedMsg.get()) 23 | #else 24 | // try is needed without the typed throws 25 | XCTAssertEqual(msg, try receivedMsg.get()) 26 | #endif 27 | 28 | await channel.send(msg.data(using: .utf8)!) 29 | 30 | let numSent = await channel.numSent 31 | let numReceived = await channel.numReceived 32 | let numBlocked = await channel.numBlocked 33 | let queueCount = await channel.queueCount 34 | 35 | XCTAssertEqual(numSent, 2) 36 | XCTAssertEqual(numReceived, 1) 37 | XCTAssertEqual(numBlocked, 1) 38 | XCTAssertEqual(queueCount, 1) 39 | } 40 | 41 | func testBidirectionalChannel() async throws { 42 | let (clientChannel, serverChannel) = DataChannel.withDataActor() 43 | let msg = "hello" 44 | try await clientChannel.writeHandler(msg.data(using: .utf8)!) 45 | var it = serverChannel.dataSequence.makeAsyncIterator(); 46 | let receivedData = await it.next() 47 | let receivedMsg = String(data: receivedData!, encoding: .utf8)! 48 | XCTAssertEqual(msg, receivedMsg) 49 | 50 | } 51 | 52 | func testSimpleRPC() { 53 | let (_, serverChannel) = DataChannel.withDataActor() 54 | let _ = JSONRPCSession(channel: serverChannel) 55 | // TODO... 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/JSONRPC/JSONId.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum JSONId: Sendable { 4 | case numericId(Int) 5 | case stringId(String) 6 | 7 | public init(_ value: Int) { 8 | self = .numericId(value) 9 | } 10 | 11 | public init(_ value: String) { 12 | self = .stringId(value) 13 | } 14 | } 15 | 16 | extension JSONId: Codable { 17 | public init(from decoder: Decoder) throws { 18 | let container = try decoder.singleValueContainer() 19 | 20 | if let value = try? container.decode(Int.self) { 21 | self = .numericId(value) 22 | } else if let value = try? container.decode(String.self) { 23 | self = .stringId(value) 24 | } else { 25 | let ctx = DecodingError.Context(codingPath: container.codingPath, debugDescription: "Unknown JSONId Type") 26 | throw DecodingError.typeMismatch(JSONId.self, ctx) 27 | } 28 | } 29 | 30 | public func encode(to encoder: Encoder) throws { 31 | var container = encoder.singleValueContainer() 32 | 33 | switch self { 34 | case .numericId(let value): 35 | try container.encode(value) 36 | case .stringId(let value): 37 | try container.encode(value) 38 | } 39 | } 40 | } 41 | 42 | extension JSONId: Hashable { 43 | } 44 | 45 | extension JSONId: CustomStringConvertible { 46 | public var description: String { 47 | switch self { 48 | case .stringId(let str): 49 | return str 50 | case .numericId(let num): 51 | return String(num) 52 | } 53 | } 54 | } 55 | 56 | extension JSONId: ExpressibleByIntegerLiteral { 57 | public init(integerLiteral value: Int) { 58 | self.init(value) 59 | } 60 | } 61 | 62 | extension JSONId: ExpressibleByStringLiteral { 63 | public init(stringLiteral value: String) { 64 | self.init(value) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/JSONRPC/DataChannel+PredefinedMessages.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if compiler(>=5.9) 4 | 5 | public enum ScriptedMessage: Hashable, Sendable { 6 | case afterWrite(Data) 7 | case immediate(Data) 8 | } 9 | 10 | actor ScriptedMessageRelay { 11 | private var messages: [ScriptedMessage] 12 | private let continuation: DataChannel.DataSequence.Continuation 13 | public nonisolated let sequence: DataChannel.DataSequence 14 | 15 | init(messages: [ScriptedMessage]) async { 16 | self.messages = messages 17 | 18 | (self.sequence, self.continuation) = DataChannel.DataSequence.makeStream() 19 | 20 | deliverNextIfNeeded() 21 | } 22 | 23 | private func deliverNextIfNeeded() { 24 | guard let next = messages.first else { 25 | continuation.finish() 26 | return 27 | } 28 | 29 | switch next { 30 | case .immediate(let data): 31 | messages.removeFirst() 32 | 33 | continuation.yield(data) 34 | deliverNextIfNeeded() 35 | case .afterWrite: 36 | break 37 | } 38 | 39 | } 40 | 41 | func onWrite() { 42 | guard let next = messages.first else { 43 | continuation.finish() 44 | return 45 | } 46 | 47 | switch next { 48 | case let .afterWrite(data): 49 | messages.removeFirst() 50 | 51 | continuation.yield(data) 52 | 53 | deliverNextIfNeeded() 54 | case .immediate: 55 | fatalError("this should never occur") 56 | } 57 | } 58 | } 59 | 60 | extension DataChannel { 61 | /// A channel that delivers a static set of messages. 62 | /// 63 | /// This will delivery messages in order, after each `write` is performed. Useful for testing. 64 | public static func predefinedMessagesChannel(with messages: [ScriptedMessage]) async -> DataChannel { 65 | let relay = await ScriptedMessageRelay(messages: messages) 66 | 67 | return DataChannel( 68 | writeHandler: { _ in 69 | // strong-ref here to keep relay alive 70 | await relay.onWrite() 71 | }, 72 | dataSequence: relay.sequence) 73 | } 74 | } 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /Tests/JSONRPCTests/XCTest+Fulfillment.swift: -------------------------------------------------------------------------------- 1 | // see https://github.com/apple/swift-corelibs-xctest/issues/436 2 | 3 | import Foundation 4 | 5 | #if os(Linux) 6 | import XCTest 7 | 8 | extension XCTestCase { 9 | /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they 10 | /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. 11 | /// 12 | /// - Parameter expectations: The expectations to wait on. 13 | /// - Parameter timeout: The maximum total time duration to wait on all expectations. 14 | /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order 15 | /// they are specified in the `expectations` Array. Default is false. 16 | /// - Parameter file: The file name to use in the error message if 17 | /// expectations are not fulfilled before the given timeout. Default is the file 18 | /// containing the call to this method. It is rare to provide this 19 | /// parameter when calling this method. 20 | /// - Parameter line: The line number to use in the error message if the 21 | /// expectations are not fulfilled before the given timeout. Default is the line 22 | /// number of the call to this method in the calling file. It is rare to 23 | /// provide this parameter when calling this method. 24 | /// 25 | /// - SeeAlso: XCTWaiter 26 | func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false) async { 27 | return await withCheckedContinuation { continuation in 28 | // This function operates by blocking a background thread instead of one owned by libdispatch or by the 29 | // Swift runtime (as used by Swift concurrency.) To ensure we use a thread owned by neither subsystem, use 30 | // Foundation's Thread.detachNewThread(_:). 31 | Thread.detachNewThread { [self] in 32 | wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder) 33 | continuation.resume() 34 | } 35 | } 36 | } 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/JSONRPC/AsyncStreamPolyfill.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if compiler(<5.9) 3 | 4 | // @available(SwiftStdlib 5.1, *) 5 | extension AsyncStream { 6 | /// Initializes a new ``AsyncStream`` and an ``AsyncStream/Continuation``. 7 | /// 8 | /// - Parameters: 9 | /// - elementType: The element type of the stream. 10 | /// - limit: The buffering policy that the stream should use. 11 | /// - Returns: A tuple containing the stream and its continuation. The continuation should be passed to the 12 | /// producer while the stream should be passed to the consumer. 13 | // @backDeployed(before: SwiftStdlib 5.9) 14 | public static func makeStream( 15 | of elementType: Element.Type = Element.self, 16 | bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded 17 | ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { 18 | var continuation: AsyncStream.Continuation! 19 | let stream = AsyncStream(bufferingPolicy: limit) { continuation = $0 } 20 | return (stream: stream, continuation: continuation!) 21 | } 22 | } 23 | 24 | // @available(SwiftStdlib 5.1, *) 25 | extension AsyncThrowingStream { 26 | /// Initializes a new ``AsyncThrowingStream`` and an ``AsyncThrowingStream/Continuation``. 27 | /// 28 | /// - Parameters: 29 | /// - elementType: The element type of the stream. 30 | /// - failureType: The failure type of the stream. 31 | /// - limit: The buffering policy that the stream should use. 32 | /// - Returns: A tuple containing the stream and its continuation. The continuation should be passed to the 33 | /// producer while the stream should be passed to the consumer. 34 | // @backDeployed(before: SwiftStdlib 5.9) 35 | public static func makeStream( 36 | of elementType: Element.Type = Element.self, 37 | throwing failureType: Failure.Type = Failure.self, 38 | bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded 39 | ) -> (stream: AsyncThrowingStream, continuation: AsyncThrowingStream.Continuation) where Failure == Error { 40 | var continuation: AsyncThrowingStream.Continuation! 41 | let stream = AsyncThrowingStream(bufferingPolicy: limit) { continuation = $0 } 42 | return (stream: stream, continuation: continuation!) 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /Tests/JSONRPCTests/JSONValueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import JSONRPC 3 | 4 | final class JSONValueTests: XCTestCase { 5 | func testNullEncoding() throws { 6 | let obj = JSONValue.null 7 | 8 | let data = try JSONEncoder().encode(obj) 9 | let string = try XCTUnwrap(String(data: data, encoding: .utf8)) 10 | 11 | XCTAssertEqual(string, "null") 12 | } 13 | 14 | func testBoolEncoding() throws { 15 | let obj = JSONValue.bool(true) 16 | 17 | let data = try JSONEncoder().encode(obj) 18 | let string = try XCTUnwrap(String(data: data, encoding: .utf8)) 19 | 20 | XCTAssertEqual(string, "true") 21 | } 22 | 23 | func testIntEncoding() throws { 24 | let obj = JSONValue.number(45) 25 | 26 | let data = try JSONEncoder().encode(obj) 27 | let string = try XCTUnwrap(String(data: data, encoding: .utf8)) 28 | 29 | XCTAssertEqual(string, "45") 30 | } 31 | 32 | func testArrayEncoding() throws { 33 | let obj = JSONValue.array([1,2,3]) 34 | 35 | let data = try JSONEncoder().encode(obj) 36 | let string = try XCTUnwrap(String(data: data, encoding: .utf8)) 37 | 38 | XCTAssertEqual(string, "[1,2,3]") 39 | } 40 | 41 | func testNullInDictionary() throws { 42 | let obj = JSONValue.hash(["abc": nil]) 43 | 44 | let data = try JSONEncoder().encode(obj) 45 | let string = try XCTUnwrap(String(data: data, encoding: .utf8)) 46 | 47 | XCTAssertEqual(string, "{\"abc\":null}") 48 | } 49 | 50 | func testDecoding() throws { 51 | let string = """ 52 | { 53 | "string": "abc", 54 | "bool": true, 55 | "null": null, 56 | "int": 145, 57 | "double": 145.0, 58 | "array": [1,2,3] 59 | } 60 | """ 61 | let value = try JSONDecoder().decode(JSONValue.self, from: string.data(using: .utf8)!) 62 | 63 | let expected: JSONValue = [ 64 | "string": "abc", 65 | "bool": true, 66 | "null": nil, 67 | "int": 145, 68 | "double": 145.0, 69 | "array": [1,2,3] 70 | ] 71 | XCTAssertEqual(value, expected) 72 | } 73 | 74 | func testDescriptions() throws { 75 | XCTAssertEqual(JSONValue.null.description, "nil") 76 | XCTAssertEqual(JSONValue.bool(true).description, "true") 77 | XCTAssertEqual(JSONValue.bool(false).description, "false") 78 | XCTAssertEqual(JSONValue.number(1.0).description, "1.0") 79 | XCTAssertEqual(JSONValue.string("yo").description, "yo") 80 | XCTAssertEqual(JSONValue.array([true, false]).description, "[true, false]") 81 | XCTAssertEqual(JSONValue.hash(["a": false]).description, "[\"a\": false]") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/JSONRPC/DataChannel+Actor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ReceiveQueue : Collection where Element == Data { 4 | mutating func append(_ newElement: Element) 5 | mutating func popFirst() -> Element? 6 | } 7 | 8 | extension Array : ReceiveQueue where Element == Data { 9 | public mutating func popFirst() -> Data? { 10 | if isEmpty == true { 11 | return nil 12 | } 13 | 14 | return self.removeFirst() 15 | } 16 | } 17 | 18 | actor DataActor where Queue : ReceiveQueue { 19 | private var queue: Queue 20 | private var continuation: CheckedContinuation? 21 | private(set) var numSent: Int 22 | public var numReceived: Int { numSent - queue.count } 23 | private(set) var numBlocked: Int 24 | public var queueCount: Int { queue.count } 25 | 26 | public init(queueProvider: @Sendable () -> Queue) { 27 | self.queue = queueProvider() 28 | self.numSent = 0 29 | self.numBlocked = 0 30 | } 31 | 32 | public func send(_ data: Data) -> Void { 33 | numSent += 1 34 | if let c = continuation { 35 | assert(queue.isEmpty) 36 | continuation = nil 37 | c.resume(returning: data) 38 | } 39 | else { 40 | queue.append(data) 41 | } 42 | } 43 | 44 | public func recv() async -> Data { 45 | if let data = queue.popFirst() { 46 | return data 47 | } 48 | 49 | numBlocked += 1 50 | 51 | return await withCheckedContinuation { 52 | continuation = $0 53 | } 54 | } 55 | } 56 | 57 | extension DataActor where Queue : Sendable { 58 | init(queue: Queue) { 59 | self.init(queueProvider: { queue }) 60 | } 61 | } 62 | 63 | extension DataChannel { 64 | /// Create a pair of `DataActor` channels. 65 | /// 66 | /// The actor data channel conist of two directional actor data channels with crossover send/receive members. 67 | public static func withDataActor( 68 | queueProvider: @Sendable () -> Queue 69 | ) -> (clientChannel: DataChannel, serverChannel: DataChannel) where Queue : ReceiveQueue { 70 | let clientActor = DataActor(queueProvider: queueProvider) 71 | let serverActor = DataActor(queueProvider: queueProvider) 72 | 73 | let clientChannel = makeChannel(sender: clientActor, reciever: serverActor) 74 | let serverChannel = makeChannel(sender: serverActor, reciever: clientActor) 75 | 76 | return (clientChannel, serverChannel) 77 | } 78 | 79 | // Default actor channel with Array queue storage 80 | public static func withDataActor() -> (clientChannel: DataChannel, serverChannel: DataChannel) { 81 | return withDataActor(queueProvider: { Array() }) 82 | } 83 | 84 | private static func makeChannel( 85 | sender: DataActor, 86 | reciever: DataActor, 87 | onCancel: (@Sendable () -> Void)? = nil 88 | ) -> DataChannel { 89 | let writeHandler = { @Sendable data in 90 | await sender.send(data) 91 | } 92 | 93 | let dataSequence = DataChannel.DataSequence { 94 | await reciever.recv() 95 | } onCancel: { onCancel?() } 96 | 97 | return DataChannel(writeHandler: writeHandler, dataSequence: dataSequence) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/JSONRPC/JSONValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum JSONValue: Codable, Hashable, Sendable { 4 | case null 5 | case bool(Bool) 6 | case number(Double) 7 | case string(String) 8 | case array([JSONValue]) 9 | case hash([String: JSONValue]) 10 | 11 | public func encode(to encoder: Encoder) throws { 12 | var container = encoder.singleValueContainer() 13 | 14 | switch self { 15 | case .null: 16 | try container.encodeNil() 17 | case .bool(let value): 18 | try container.encode(value) 19 | case .number(let value): 20 | try container.encode(value) 21 | case .string(let value): 22 | try container.encode(value) 23 | case .array(let value): 24 | try container.encode(value) 25 | case .hash(let value): 26 | try container.encode(value) 27 | } 28 | } 29 | 30 | public init(from decoder: Decoder) throws { 31 | let single = try? decoder.singleValueContainer() 32 | 33 | if let value = try? single?.decode([String: JSONValue].self) { 34 | self = .hash(value) 35 | return 36 | } 37 | 38 | if let value = try? single?.decode([JSONValue].self) { 39 | self = .array(value) 40 | return 41 | } 42 | 43 | if let value = try? single?.decode(String.self) { 44 | self = .string(value) 45 | return 46 | } 47 | 48 | if let value = try? single?.decode(Double.self) { 49 | self = .number(value) 50 | return 51 | } 52 | 53 | if let value = try? single?.decode(Bool.self) { 54 | self = .bool(value) 55 | return 56 | } 57 | 58 | if single?.decodeNil() == true { 59 | self = .null 60 | return 61 | } 62 | 63 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "failed to decode JSON object")) 64 | } 65 | } 66 | 67 | extension JSONValue: ExpressibleByNilLiteral { 68 | public init(nilLiteral: ()) { 69 | self = .null 70 | } 71 | } 72 | 73 | extension JSONValue: ExpressibleByDictionaryLiteral { 74 | public init(dictionaryLiteral elements: (String, JSONValue)...) { 75 | var hash = [String: JSONValue]() 76 | 77 | for element in elements { 78 | hash[element.0] = element.1 79 | } 80 | 81 | self = .hash(hash) 82 | } 83 | } 84 | 85 | extension JSONValue: ExpressibleByStringLiteral { 86 | public init(stringLiteral: String) { 87 | self = .string(stringLiteral) 88 | } 89 | } 90 | 91 | extension JSONValue: ExpressibleByIntegerLiteral { 92 | public init(integerLiteral value: IntegerLiteralType) { 93 | self = .number(Double(value)) 94 | } 95 | } 96 | 97 | extension JSONValue: ExpressibleByFloatLiteral { 98 | public init(floatLiteral value: FloatLiteralType) { 99 | self = .number(value) 100 | } 101 | } 102 | 103 | extension JSONValue: ExpressibleByArrayLiteral { 104 | public init(arrayLiteral elements: JSONValue...) { 105 | var array = [JSONValue]() 106 | 107 | for element in elements { 108 | array.append(element) 109 | } 110 | 111 | self = .array(array) 112 | } 113 | } 114 | 115 | extension JSONValue: ExpressibleByBooleanLiteral { 116 | public init(booleanLiteral value: BooleanLiteralType) { 117 | self = .bool(value) 118 | } 119 | } 120 | 121 | extension JSONValue: CustomStringConvertible { 122 | public var description: String { 123 | switch self { 124 | case .null: 125 | "nil" 126 | case let .bool(value): 127 | value.description 128 | case let .number(value): 129 | value.description 130 | case let .string(value): 131 | value 132 | case let .array(value): 133 | value.description 134 | case let .hash(value): 135 | value.description 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Build Status][build status badge]][build status] 4 | [![Platforms][platforms badge]][platforms] 5 | [![Documentation][documentation badge]][documentation] 6 | [![Matrix][matrix badge]][matrix] 7 | 8 |
9 | 10 | # JSONRPC 11 | A simple Swift library for [JSON-RPC](https://www.jsonrpc.org) 12 | 13 | Features: 14 | - type-safety 15 | - flexible data transport support 16 | - concurrency support 17 | 18 | ## Integration 19 | 20 | ```swift 21 | dependencies: [ 22 | .package(url: "https://github.com/ChimeHQ/JSONRPC", from: "0.9.0") 23 | ] 24 | ``` 25 | 26 | ## Usage 27 | 28 | The core type you'll use is `JSONRPCSession`. It requires you set up a `DataChannel` object that handles reading and writing raw data. 29 | 30 | ```swift 31 | let channel = DataChannel(...) 32 | let session = JSONRPCSession(channel: channel) 33 | 34 | let params = "hello" // any Encodable 35 | let response: Decodable = try await session.sendRequest(params, method: "my_method") 36 | 37 | Task { 38 | for await event in await session.eventSequence { 39 | switch event { 40 | case .request(let request, let handler, let data): 41 | // inspect request, possibly re-decode with more specific type, 42 | // and reply using the handler 43 | 44 | case .notification(let notification, let data): 45 | // inspect notification 46 | case .error(let error): 47 | print("Error: \(error)") 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | ### DataChannel 54 | 55 | The closures on the `DataChannel` allow different transport mechanisms to be used. The `JSONRPC` package provides a few basic variants: 56 | 57 | - Predefined messages channel 58 | - A channel that delivers a static set of messages 59 | - Usage: `let channel = await DataChannel.predefinedMessagesChannel(with: messages)` 60 | - Stdio channel 61 | - Using stdout + stdin as message transport. 62 | - Note: When using this transport, make sure no non-protocol messages are sent to `stdout`, eg using `print` 63 | - Usage: `let channel = DataChannel.stdioPipe()` 64 | - Actor channel 65 | - Using swift actors to pass messages. 66 | - Can eg be useful for testing, where both client and server are implemented in swift and running in the same process. 67 | - Usage: `let (clientChannel, serverChannel) = DataChannel.withDataActor()` 68 | - WebSocket channel 69 | - Uses `URLSessionWebSocketTask` as a message transport. 70 | - Usage: `let channel = DataChannel.webSocket(url: socketURL, terminationHandler: { print("socket closed" })` 71 | 72 | ## Contributing and Collaboration 73 | 74 | I would love to hear from you! Issues or pull requests work great. Both a [Matrix space][matrix] and [Discord][discord] are available for live help, but I have a strong bias towards answering in the form of documentation. You can also find me on [mastodon](https://mastodon.social/@mattiem). 75 | 76 | I prefer collaboration, and would love to find ways to work together if you have a similar project. 77 | 78 | I prefer indentation with tabs for improved accessibility. But, I'd rather you use the system you want and make a PR than hesitate because of whitespace. 79 | 80 | By participating in this project you agree to abide by the [Contributor Code of Conduct](CODE_OF_CONDUCT.md). 81 | 82 | [build status]: https://github.com/ChimeHQ/JSONRPC/actions 83 | [build status badge]: https://github.com/ChimeHQ/JSONRPC/workflows/CI/badge.svg 84 | [license]: https://opensource.org/licenses/BSD-3-Clause 85 | [license badge]: https://img.shields.io/github/license/ChimeHQ/JSONRPC 86 | [platforms]: https://swiftpackageindex.com/ChimeHQ/JSONRPC 87 | [platforms badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FChimeHQ%2FJSONRPC%2Fbadge%3Ftype%3Dplatforms 88 | [documentation]: https://swiftpackageindex.com/ChimeHQ/JSONRPC/main/documentation 89 | [documentation badge]: https://img.shields.io/badge/Documentation-DocC-blue 90 | [matrix]: https://matrix.to/#/%23chimehq%3Amatrix.org 91 | [matrix badge]: https://img.shields.io/matrix/chimehq%3Amatrix.org?label=Matrix 92 | [discord]: https://discord.gg/esFpX6sErJ 93 | -------------------------------------------------------------------------------- /Tests/JSONRPCTests/ProtocolTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import JSONRPC 3 | 4 | final class ProtocolTests: XCTestCase { 5 | func testResultResponse() throws { 6 | let string = """ 7 | {"jsonrpc":"2.0", "id": 1, "result": "hello"} 8 | """ 9 | let data = try XCTUnwrap(string.data(using: .utf8)) 10 | let response = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 11 | 12 | let expected = JSONRPCResponse(id: 1, result: "hello") 13 | 14 | XCTAssertEqual(response, expected) 15 | } 16 | 17 | func testErrorWithNoDataResponse() throws { 18 | let string = """ 19 | {"jsonrpc":"2.0", "id": 1, "error": {"code": 1, "message": "hello"}} 20 | """ 21 | let data = try XCTUnwrap(string.data(using: .utf8)) 22 | let response = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 23 | 24 | let error = AnyJSONRPCResponseError(code: 1, message: "hello", data: nil) 25 | let expected = JSONRPCResponse(id: 1, content: .failure(error)) 26 | 27 | XCTAssertEqual(response, expected) 28 | } 29 | 30 | func testErrorAndResultResponse() throws { 31 | let string = """ 32 | {"jsonrpc":"2.0", "id": 1, "result": "hello", "error": {"code": 1, "message": "hello"}} 33 | """ 34 | let data = try XCTUnwrap(string.data(using: .utf8)) 35 | 36 | do { 37 | _ = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 38 | 39 | XCTFail() 40 | } catch { 41 | } 42 | } 43 | 44 | func testErrorAndNullResultResponse() throws { 45 | // against spec 46 | 47 | let string = """ 48 | {"jsonrpc":"2.0", "id": 1, "result": null, "error": {"code": 1, "message": "hello"}} 49 | """ 50 | let data = try XCTUnwrap(string.data(using: .utf8)) 51 | let response = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 52 | 53 | let error = AnyJSONRPCResponseError(code: 1, message: "hello", data: nil) 54 | let expected = JSONRPCResponse(id: 1, content: .failure(error)) 55 | 56 | XCTAssertEqual(response, expected) 57 | } 58 | 59 | func testResultAndNullErrorResponse() throws { 60 | // against spec 61 | let string = """ 62 | {"jsonrpc":"2.0", "id": 1, "result": "hello", "error": null} 63 | """ 64 | let data = try XCTUnwrap(string.data(using: .utf8)) 65 | let response = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 66 | 67 | let expected = JSONRPCResponse(id: 1, result: "hello") 68 | 69 | XCTAssertEqual(response, expected) 70 | } 71 | 72 | func testNullIdErrorResponse() throws { 73 | let string = """ 74 | {"jsonrpc":"2.0", "id": null, "error": {"code": 1, "message": "hello"}} 75 | """ 76 | let data = try XCTUnwrap(string.data(using: .utf8)) 77 | 78 | do { 79 | _ = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 80 | 81 | XCTFail() 82 | } catch { 83 | } 84 | } 85 | 86 | func testResultNullAndNullErrorResponse() throws { 87 | // against spec 88 | let string = """ 89 | {"jsonrpc":"2.0", "id": 1, "result": null, "error": null} 90 | """ 91 | let data = try XCTUnwrap(string.data(using: .utf8)) 92 | 93 | do { 94 | _ = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 95 | 96 | XCTFail() 97 | } catch { 98 | } 99 | } 100 | 101 | func testOptionalResultNullAndNullErrorResponse() throws { 102 | // against spec 103 | let string = """ 104 | {"jsonrpc":"2.0", "id": 1, "result": null, "error": null} 105 | """ 106 | let data = try XCTUnwrap(string.data(using: .utf8)) 107 | let response = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 108 | 109 | let expected = JSONRPCResponse(id: 1, result: nil) 110 | 111 | XCTAssertEqual(response, expected) 112 | } 113 | 114 | func testNullIdAndResultResponse() throws { 115 | let string = """ 116 | {"jsonrpc":"2.0", "id": null, "result": "hello", "error": {"code": 1, "message": "hello"}} 117 | """ 118 | let data = try XCTUnwrap(string.data(using: .utf8)) 119 | 120 | do { 121 | _ = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 122 | 123 | XCTFail() 124 | } catch { 125 | } 126 | } 127 | 128 | func testUnsupportedVersionResponse() throws { 129 | let string = """ 130 | {"jsonrpc":"1.0", "id": 1, "result": "hello"} 131 | """ 132 | let data = try XCTUnwrap(string.data(using: .utf8)) 133 | 134 | do { 135 | _ = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 136 | 137 | XCTFail() 138 | } catch { 139 | } 140 | } 141 | 142 | func testEncodeResponse() throws { 143 | let response = JSONRPCResponse(id: 1, result: "hello") 144 | 145 | let data = try JSONEncoder().encode(response) 146 | 147 | let decodedResponse = try JSONDecoder().decode(JSONRPCResponse.self, from: data) 148 | 149 | XCTAssertEqual(decodedResponse, response) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Tests/JSONRPCTests/JSONRPCSessionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import JSONRPC 3 | 4 | #if compiler(>=5.9) 5 | 6 | final class JSONRPCSessionTests: XCTestCase { 7 | typealias TestResponse = JSONRPCResponse 8 | typealias TestNotification = JSONRPCNotification 9 | 10 | func testResultResponse() async throws { 11 | let expectedResponse = TestResponse(id: JSONId(1), result: "goodbye") 12 | 13 | let messages: [ScriptedMessage] = [ 14 | .afterWrite(try JSONEncoder().encode(expectedResponse)), 15 | ] 16 | 17 | let channel = await DataChannel.predefinedMessagesChannel(with: messages) 18 | let session = JSONRPCSession(channel: channel) 19 | 20 | let response: TestResponse = try await session.sendRequest("hello", method: "mymethod") 21 | 22 | XCTAssertEqual(response, expectedResponse) 23 | } 24 | 25 | func testManySendRequestsWithResponses() async throws { 26 | let iterations = 1000 27 | 28 | // be sure to start at 1, to match id generation 29 | let messages = try (1...iterations).map { i in 30 | let responseParam = "goodbye-\(i)" 31 | 32 | let response = TestResponse(id: JSONId(i), result: responseParam) 33 | let data = try JSONEncoder().encode(response) 34 | 35 | return ScriptedMessage.afterWrite(data) 36 | } 37 | 38 | let channel = await DataChannel.predefinedMessagesChannel(with: messages) 39 | let session = JSONRPCSession(channel: channel) 40 | 41 | let params = "hello" 42 | 43 | for i in 1...iterations { 44 | let expectedResponse = "goodbye-\(i)" 45 | 46 | let response: TestResponse = try await session.sendRequest(params, method: "mymethod") 47 | 48 | XCTAssertEqual(try! response.content.get(), expectedResponse) 49 | } 50 | } 51 | 52 | func testSendNotification() async throws { 53 | let pair = DataChannel.DataSequence.makeStream() 54 | 55 | let params = "hello" 56 | let method = "mynotification" 57 | 58 | let result = TestNotification(method: method, params: params) 59 | 60 | let expectation = XCTestExpectation(description: "Notification Message") 61 | 62 | let channel = DataChannel( 63 | writeHandler: { data in 64 | // have to decode this here to make sure json key ordering does not matter 65 | let notification = try JSONDecoder().decode(TestNotification.self, from: data) 66 | XCTAssertEqual(notification, result) 67 | 68 | expectation.fulfill() 69 | }, 70 | dataSequence: pair.stream 71 | ) 72 | 73 | let session = JSONRPCSession(channel: channel) 74 | 75 | try await session.sendNotification(params, method: method) 76 | 77 | await fulfillment(of: [expectation], timeout: 1.0) 78 | } 79 | 80 | func testServerToClientNotification() async throws { 81 | let pair = DataChannel.DataSequence.makeStream() 82 | 83 | let channel = DataChannel(writeHandler: { _ in }, 84 | dataSequence: pair.stream) 85 | 86 | let session = JSONRPCSession(channel: channel) 87 | 88 | let params = "hello" 89 | let method = "mynotification" 90 | 91 | let result = TestNotification(method: method, params: params) 92 | 93 | let expectation = XCTestExpectation(description: "Notification Message") 94 | 95 | Task { 96 | for await event in await session.eventSequence { 97 | guard case let .notification(_, data) = event else { continue } 98 | let notification = try JSONDecoder().decode(TestNotification.self, from: data) 99 | XCTAssertEqual(notification, result) 100 | 101 | expectation.fulfill() 102 | } 103 | } 104 | 105 | let data = try JSONEncoder().encode(result) 106 | pair.continuation.yield(data) 107 | 108 | await fulfillment(of: [expectation], timeout: 1.0) 109 | } 110 | 111 | func testServerToClientResponseThenNotification() async throws { 112 | let expectedResponse = TestResponse(id: JSONId(1), result: nil) 113 | let expectedNotification = TestNotification(method: "note") 114 | 115 | let messages: [ScriptedMessage] = [ 116 | .afterWrite(try JSONEncoder().encode(expectedResponse)), 117 | .immediate(try JSONEncoder().encode(expectedNotification)) 118 | ] 119 | 120 | let channel = await DataChannel.predefinedMessagesChannel(with: messages) 121 | let session = JSONRPCSession(channel: channel) 122 | 123 | let notificationExpectation = XCTestExpectation(description: "Notification Message") 124 | 125 | Task { 126 | for await event in await session.eventSequence { 127 | guard case let .notification(_, data) = event else { continue } 128 | let notification = try JSONDecoder().decode(TestNotification.self, from: data) 129 | XCTAssertEqual(notification, expectedNotification) 130 | 131 | notificationExpectation.fulfill() 132 | } 133 | } 134 | 135 | let response: TestResponse = try await session.sendRequest("hello", method: "myrequest") 136 | XCTAssertEqual(response, expectedResponse) 137 | 138 | await fulfillment(of: [notificationExpectation], timeout: 1.0) 139 | } 140 | 141 | func testNullResultResponse() async throws { 142 | let expectedResponse = TestResponse(id: 1, result: nil) 143 | 144 | let messages: [ScriptedMessage] = [ 145 | .afterWrite(try JSONEncoder().encode(expectedResponse)), 146 | ] 147 | 148 | let channel = await DataChannel.predefinedMessagesChannel(with: messages) 149 | let session = JSONRPCSession(channel: channel) 150 | 151 | let response: TestResponse = try await session.sendRequest("hello", method: "myrequest") 152 | 153 | XCTAssertNil(try response.content.get()) 154 | } 155 | } 156 | 157 | #endif 158 | -------------------------------------------------------------------------------- /Tests/JSONRPCTests/JSONValueDecoderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import JSONRPC 3 | 4 | final class JSONValueDecoderTests: XCTestCase { 5 | struct SimpleStruct: Decodable, Equatable { 6 | var bool: Bool? 7 | var string: String? 8 | var double: Double? 9 | var float: Float? 10 | var int: Int? 11 | var int8: Int8? 12 | var int16: Int16? 13 | var int32: Int32? 14 | var int64: Int64? 15 | var uint: UInt? 16 | var uint8: UInt8? 17 | var uint16: UInt16? 18 | var uint32: UInt32? 19 | var uint64: UInt64? 20 | 21 | var any: JSONValue? 22 | var intArray: [Int]? 23 | 24 | var nested: [SimpleStruct]? 25 | } 26 | 27 | func testDecode() throws { 28 | XCTAssertEqual( 29 | try JSONValueDecoder().decode( 30 | SimpleStruct.self, 31 | from: [ 32 | "bool": true, 33 | "string": "foo", 34 | "double": 101, 35 | "float": 102, 36 | "int": 103, 37 | "int8": 104, 38 | "int16": 105, 39 | "int32": 106, 40 | "int64": 107, 41 | "uint": 108, 42 | "uint8": 109, 43 | "uint16": 110, 44 | "uint32": 111, 45 | "uint64": 112, 46 | 47 | "any": "bar", 48 | "intArray": [11, 22, 33], 49 | 50 | "nested": [["int": 11], ["int": 22]] 51 | ] 52 | ), 53 | SimpleStruct( 54 | bool: true, 55 | string: "foo", 56 | double: 101, 57 | float: 102, 58 | int: 103, 59 | int8: 104, 60 | int16: 105, 61 | int32: 106, 62 | int64: 107, 63 | uint: 108, 64 | uint8: 109, 65 | uint16: 110, 66 | uint32: 111, 67 | uint64: 112, 68 | 69 | any: JSONValue.string("bar"), 70 | intArray: [11, 22, 33], 71 | nested: [SimpleStruct(int: 11), SimpleStruct(int: 22)] 72 | ) 73 | ) 74 | } 75 | 76 | func testDecodeUnkeyedErrorPath() throws { 77 | XCTAssertThrowsError( 78 | try JSONValueDecoder().decode( 79 | SimpleStruct.self, 80 | from: JSONValue.hash(["intArray": .array([.number(0), 81 | .number(1), 82 | .bool(false)])]) 83 | ) 84 | ) { error in 85 | guard case let DecodingError.typeMismatch(_, context) = error else { 86 | XCTFail("Expected typeError") 87 | return 88 | } 89 | XCTAssertEqual( 90 | context.codingPath.count, 91 | 2 92 | ) 93 | XCTAssertEqual( 94 | context.codingPath[0].stringValue, 95 | "intArray" 96 | ) 97 | XCTAssertEqual( 98 | context.codingPath[1].intValue, 99 | 2 100 | ) 101 | } 102 | } 103 | 104 | func testDecodeNotDouble() throws { 105 | XCTAssertThrowsError( 106 | try JSONValueDecoder().decode( 107 | SimpleStruct.self, 108 | from: JSONValue.hash(["double": "string"]) 109 | ) 110 | ) { error in 111 | #if os(Linux) 112 | XCTAssertEqual( 113 | error.localizedDescription, 114 | "The operation could not be completed. The data isn’t in the correct format." 115 | ) 116 | #else 117 | XCTAssertEqual( 118 | error.localizedDescription, 119 | "The data couldn’t be read because it isn’t in the correct format." 120 | ) 121 | #endif 122 | } 123 | } 124 | 125 | func testDecodeOverflow() throws { 126 | XCTAssertThrowsError( 127 | try JSONValueDecoder().decode( 128 | SimpleStruct.self, 129 | from: JSONValue.hash(["int8": 300]) 130 | ) 131 | ) { error in 132 | #if os(Linux) 133 | XCTAssertEqual( 134 | error.localizedDescription, 135 | "The operation could not be completed. The data isn’t in the correct format." 136 | ) 137 | #else 138 | XCTAssertEqual( 139 | error.localizedDescription, 140 | "The data couldn’t be read because it isn’t in the correct format." 141 | ) 142 | #endif 143 | } 144 | } 145 | 146 | func testDecodeFloatOverflow() throws { 147 | XCTAssertThrowsError( 148 | try JSONValueDecoder().decode( 149 | SimpleStruct.self, 150 | from: JSONValue.hash(["float": 1e300]) 151 | ) 152 | ) { error in 153 | #if os(Linux) 154 | XCTAssertEqual( 155 | error.localizedDescription, 156 | "The operation could not be completed. The data isn’t in the correct format." 157 | ) 158 | #else 159 | XCTAssertEqual( 160 | error.localizedDescription, 161 | "The data couldn’t be read because it isn’t in the correct format." 162 | ) 163 | #endif 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | support@chimehq.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /Sources/JSONRPC/JSONRPCSession.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum ProtocolTransportError: Error { 4 | case undecodableMesssage(Data) 5 | case unexpectedResponse(Data) 6 | case abandonedRequest 7 | case dataStreamClosed 8 | } 9 | 10 | private struct JSONRPCRequestReplyEncodableShim: Encodable { 11 | let id: JSONId 12 | let result: JSONRPCEvent.RequestResult 13 | 14 | private enum CodingKeys: String, CodingKey { 15 | case id 16 | case error 17 | case result 18 | case jsonrpc 19 | } 20 | 21 | func encode(to encoder: Encoder) throws { 22 | var container = encoder.container(keyedBy: CodingKeys.self) 23 | 24 | try container.encode("2.0", forKey: .jsonrpc) 25 | 26 | try container.encode(id, forKey: .id) 27 | 28 | switch result { 29 | case .failure(let error): 30 | try container.encode(error, forKey: .error) 31 | case .success(let value): 32 | try container.encode(value, forKey: .result) 33 | } 34 | } 35 | } 36 | 37 | public enum JSONRPCEvent: Sendable { 38 | public typealias RequestResult = Result 39 | public typealias RequestHandler = @Sendable (RequestResult) async -> Void 40 | 41 | case request(AnyJSONRPCRequest, RequestHandler, Data) 42 | case notification(AnyJSONRPCNotification, Data) 43 | case error(Error) 44 | } 45 | 46 | public actor JSONRPCSession { 47 | public typealias EventSequence = AsyncStream 48 | public typealias DataResult = Result<(AnyJSONRPCResponse, Data), Error> 49 | private typealias MessageResponder = @Sendable (DataResult) -> Void 50 | 51 | private var id: Int 52 | private let decoder = JSONDecoder() 53 | private let encoder = JSONEncoder() 54 | private let channel: DataChannel 55 | private var readTask: Task? 56 | private let eventContinuation: EventSequence.Continuation 57 | private var responders = [String: MessageResponder]() 58 | private var channelClosed = false 59 | 60 | public let eventSequence: EventSequence 61 | 62 | public init(channel: DataChannel) { 63 | self.id = 1 64 | self.channel = channel 65 | 66 | // this is annoying, but temporary 67 | //#if compiler(>=5.9) 68 | // (self.eventSequence, self.eventContinuation) = EventSequence.makeStream() 69 | //#else 70 | var escapedEventContinuation: EventSequence.Continuation? 71 | 72 | self.eventSequence = EventSequence { escapedEventContinuation = $0 } 73 | self.eventContinuation = escapedEventContinuation! 74 | //#endif 75 | 76 | Task { 77 | await startMonitoringChannel() 78 | } 79 | } 80 | 81 | deinit { 82 | eventContinuation.finish() 83 | readTask?.cancel() 84 | 85 | for (_, responder) in responders { 86 | responder(.failure(ProtocolTransportError.dataStreamClosed)) 87 | } 88 | } 89 | 90 | private func generateID() -> JSONId { 91 | let issuedId = JSONId.numericId(id) 92 | 93 | id += 1 94 | 95 | return issuedId 96 | } 97 | 98 | private func encodeAndWrite(_ value: T) async throws where T: Encodable { 99 | if channelClosed { 100 | throw ProtocolTransportError.dataStreamClosed 101 | } 102 | 103 | let data = try encoder.encode(value) 104 | 105 | try await channel.writeHandler(data) 106 | } 107 | 108 | private func readSequenceFinished() { 109 | for (_, responder) in responders { 110 | responder(.failure(ProtocolTransportError.dataStreamClosed)) 111 | } 112 | 113 | self.responders.removeAll() 114 | channelClosed = true 115 | } 116 | 117 | private func startMonitoringChannel() { 118 | precondition(readTask == nil) 119 | 120 | let dataSequence = channel.dataSequence 121 | 122 | let task = Task { [weak self] in 123 | for await data in dataSequence { 124 | await self?.handleData(data) 125 | } 126 | 127 | await self?.readSequenceFinished() 128 | } 129 | 130 | self.readTask = task 131 | } 132 | 133 | private func handleData(_ data: Data) { 134 | do { 135 | try self.decodeAndDispatch(data: data) 136 | } catch { 137 | eventContinuation.yield(.error(error)) 138 | } 139 | } 140 | 141 | private func decodeAndDispatch(data: Data) throws { 142 | let msg = try decoder.decode(JSONRPCMessage.self, from: data) 143 | 144 | switch msg { 145 | case .notification(let method, let params): 146 | let note = AnyJSONRPCNotification(method: method, params: params) 147 | 148 | eventContinuation.yield(.notification(note, data)) 149 | case .response(let id): 150 | let resp = AnyJSONRPCResponse(id: id, result: nil) 151 | 152 | try dispatchResponse(resp, originalData: data) 153 | case .request(let id, let method, let params): 154 | let req = AnyJSONRPCRequest(id: id, method: method, params: params) 155 | 156 | let handler: JSONRPCEvent.RequestHandler = { [weak self] in 157 | let resp = JSONRPCRequestReplyEncodableShim(id: id, result: $0) 158 | 159 | do { 160 | try await self?.encodeAndWrite(resp) 161 | } catch { 162 | self?.eventContinuation.yield(.error(error)) 163 | } 164 | } 165 | 166 | eventContinuation.yield(.request(req, handler, data)) 167 | case .undecodableId(let error): 168 | eventContinuation.yield(.error(error)) 169 | } 170 | } 171 | 172 | private func dispatchResponse(_ message: AnyJSONRPCResponse, originalData data: Data) throws { 173 | let key = message.id.description 174 | 175 | guard let responder = responders[key] else { 176 | throw ProtocolTransportError.unexpectedResponse(data) 177 | } 178 | 179 | responder(.success((message, data))) 180 | 181 | responders[key] = nil 182 | } 183 | } 184 | 185 | extension JSONRPCSession { 186 | public func sendDataRequest(_ params: Request, method: String) async throws -> (AnyJSONRPCResponse, Data) 187 | where Request: Encodable { 188 | if channelClosed { 189 | throw ProtocolTransportError.dataStreamClosed 190 | } 191 | 192 | return try await withCheckedThrowingContinuation({ continuation in 193 | // make sure not to capture self 194 | self.sendDataRequest(params, method: method) { [weak self] result in 195 | guard self != nil else { 196 | continuation.resume(throwing: ProtocolTransportError.abandonedRequest) 197 | return 198 | } 199 | 200 | continuation.resume(with: result) 201 | } 202 | }) 203 | } 204 | 205 | public func sendRequest(_ params: Request, method: String) async throws -> JSONRPCResponse 206 | where Request: Encodable, Response: Decodable { 207 | let (_, data) = try await sendDataRequest(params, method: method) 208 | 209 | return try decoder.decode(JSONRPCResponse.self, from: data) 210 | } 211 | 212 | public func sendNotification(_ params: Note, method: String) async throws where Note: Encodable { 213 | if channelClosed { 214 | throw ProtocolTransportError.dataStreamClosed 215 | } 216 | 217 | let notification = JSONRPCNotification(method: method, params: params) 218 | 219 | try await encodeAndWrite(notification) 220 | } 221 | 222 | public func sendNotification(method: String) async throws { 223 | let unusedParams: String? = nil 224 | 225 | try await sendNotification(unusedParams, method: method) 226 | } 227 | 228 | public func response(to method: String, params: Request) async throws -> Response 229 | where Request: Encodable, Response: Decodable { 230 | let (_, data) = try await sendDataRequest(params, method: method) 231 | 232 | let response = try decoder.decode(JSONRPCResponse.self, from: data) 233 | 234 | return try response.content.get() 235 | } 236 | 237 | public func response(to method: String) async throws -> Response 238 | where Response: Decodable { 239 | let unusedParams: String? = nil 240 | 241 | return try await response(to: method, params: unusedParams) 242 | } 243 | } 244 | 245 | extension JSONRPCSession { 246 | private func sendDataRequest( 247 | _ params: Request, method: String, 248 | responseHandler: @escaping MessageResponder 249 | ) where Request: Encodable { 250 | let issuedId = generateID() 251 | 252 | let request = JSONRPCRequest(id: issuedId, method: method, params: params) 253 | 254 | // make sure to store the responser *first*, before sending the message. This prevents a race where the response comes in so fast we aren't yet waiting for it 255 | let key = issuedId.description 256 | 257 | precondition(responders[key] == nil) 258 | 259 | self.responders[key] = responseHandler 260 | 261 | Task { 262 | do { 263 | try await encodeAndWrite(request) 264 | } catch { 265 | responseHandler(.failure(error)) 266 | 267 | self.responders[key] = nil 268 | return 269 | } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /Sources/JSONRPC/Protocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum JSONRPCProtocolError: Error { 4 | case unsupportedVersion(String) 5 | case malformedMessage 6 | } 7 | 8 | public enum JSONRPCMessage { 9 | case notification(String, JSONValue) 10 | case request(JSONId, String, JSONValue?) 11 | case response(JSONId) 12 | case undecodableId(AnyJSONRPCResponseError) 13 | } 14 | 15 | extension JSONRPCMessage: Codable { 16 | private enum CodingKeys: String, CodingKey { 17 | case id 18 | case error 19 | case result 20 | case method 21 | case params 22 | case jsonrpc 23 | } 24 | 25 | public init(from decoder: Decoder) throws { 26 | let container = try decoder.container(keyedBy: CodingKeys.self) 27 | 28 | let version = try container.decode(String.self, forKey: .jsonrpc) 29 | if version != "2.0" { 30 | throw JSONRPCProtocolError.unsupportedVersion(version) 31 | } 32 | 33 | // no id means notification 34 | if container.contains(.id) == false { 35 | let method = try container.decode(String.self, forKey: .method) 36 | let params = try? container.decode(JSONValue.self, forKey: .params) 37 | 38 | self = .notification(method, params ?? .null) 39 | return 40 | } 41 | 42 | // id = null 43 | if (try? container.decodeNil(forKey: .id)) == true { 44 | let error = try container.decode(AnyJSONRPCResponseError.self, forKey: .error) 45 | 46 | self = .undecodableId(error) 47 | return 48 | } 49 | 50 | let id = try container.decode(JSONId.self, forKey: .id) 51 | 52 | if container.contains(.method) { 53 | let method = try container.decode(String.self, forKey: .method) 54 | let params = try? container.decode(JSONValue.self, forKey: .params) 55 | 56 | self = .request(id, method, params) 57 | return 58 | } 59 | 60 | self = .response(id) 61 | } 62 | 63 | public func encode(to encoder: Encoder) throws { 64 | var container = encoder.container(keyedBy: CodingKeys.self) 65 | 66 | try container.encode("2.0", forKey: .jsonrpc) 67 | 68 | switch self { 69 | case .undecodableId(let error): 70 | try container.encodeNil(forKey: .id) 71 | try container.encode(error, forKey: .error) 72 | case .notification(let method, let params): 73 | try container.encode(method, forKey: .method) 74 | 75 | if params != JSONValue.null { 76 | try container.encode(params, forKey: .params) 77 | } 78 | case .request(let id, let method, let params): 79 | try container.encode(id, forKey: .id) 80 | try container.encode(method, forKey: .method) 81 | 82 | if let params = params { 83 | try container.encode(params, forKey: .params) 84 | } 85 | case .response(let id): 86 | try container.encode(id, forKey: .id) 87 | 88 | throw JSONRPCProtocolError.malformedMessage 89 | } 90 | } 91 | 92 | } 93 | 94 | public struct JSONRPCRequest { 95 | public var jsonrpc = "2.0" 96 | public var id: JSONId 97 | public var method: String 98 | public var params: T? 99 | 100 | public init(id: JSONId, method: String, params: T? = nil) { 101 | self.id = id 102 | self.method = method 103 | self.params = params 104 | } 105 | 106 | public init(id: Int, method: String, params: T? = nil) { 107 | self.init(id: .numericId(id), method: method, params: params) 108 | } 109 | } 110 | 111 | extension JSONRPCRequest: Encodable where T: Encodable {} 112 | extension JSONRPCRequest: Decodable where T: Decodable {} 113 | extension JSONRPCRequest: Equatable where T: Equatable {} 114 | extension JSONRPCRequest: Hashable where T: Hashable {} 115 | extension JSONRPCRequest: Sendable where T: Sendable {} 116 | 117 | public typealias AnyJSONRPCRequest = JSONRPCRequest 118 | 119 | public struct JSONRPCNotification { 120 | public var jsonrpc = "2.0" 121 | public var method: String 122 | public var params: T? 123 | 124 | public init(method: String, params: T? = nil) { 125 | self.method = method 126 | self.params = params 127 | } 128 | } 129 | 130 | extension JSONRPCNotification: Encodable where T: Encodable {} 131 | extension JSONRPCNotification: Decodable where T: Decodable {} 132 | extension JSONRPCNotification: Equatable where T: Equatable {} 133 | extension JSONRPCNotification: Hashable where T: Hashable {} 134 | extension JSONRPCNotification: Sendable where T: Sendable {} 135 | 136 | public typealias AnyJSONRPCNotification = JSONRPCNotification 137 | 138 | public struct JSONRPCResponseError: Error { 139 | public var code: Int 140 | public var message: String 141 | public var data: T? 142 | 143 | public init(code: Int, message: String, data: T? = nil) { 144 | self.code = code 145 | self.message = message 146 | self.data = data 147 | } 148 | } 149 | 150 | extension JSONRPCResponseError: Encodable where T: Encodable {} 151 | extension JSONRPCResponseError: Decodable where T: Decodable {} 152 | extension JSONRPCResponseError: Equatable where T: Equatable {} 153 | extension JSONRPCResponseError: Hashable where T: Hashable {} 154 | 155 | public typealias AnyJSONRPCResponseError = JSONRPCResponseError 156 | 157 | public struct JSONRPCResponse { 158 | public let id: JSONId 159 | public let content: Result 160 | 161 | private enum CodingKeys: String, CodingKey { 162 | case id 163 | case error 164 | case result 165 | case jsonrpc 166 | } 167 | 168 | public init(id: JSONId, content: Result) { 169 | self.id = id 170 | self.content = content 171 | } 172 | 173 | public init(id: JSONId, result: T) { 174 | self.id = id 175 | self.content = .success(result) 176 | } 177 | 178 | public var result: T? { 179 | switch content { 180 | case .success(let value): 181 | return value 182 | case .failure: 183 | return nil 184 | } 185 | } 186 | 187 | public var error: AnyJSONRPCResponseError? { 188 | switch content { 189 | case .success: 190 | return nil 191 | case .failure(let error): 192 | return error 193 | } 194 | } 195 | 196 | public var jsonrpc: String { 197 | return "2.0" 198 | } 199 | } 200 | 201 | extension JSONRPCResponse: Decodable where T: Decodable { 202 | public init(from decoder: Decoder) throws { 203 | let container = try decoder.container(keyedBy: CodingKeys.self) 204 | 205 | let version = try container.decode(String.self, forKey: .jsonrpc) 206 | if version != "2.0" { 207 | throw JSONRPCProtocolError.unsupportedVersion(version) 208 | } 209 | 210 | let id = try container.decode(JSONId.self, forKey: .id) 211 | 212 | if container.contains(.error) == false { 213 | let value = try container.decode(T.self, forKey: .result) 214 | self = .init(id: id, result: value) 215 | 216 | return 217 | } 218 | 219 | if container.contains(.result) == false { 220 | let error = try container.decode(AnyJSONRPCResponseError.self, forKey: .error) 221 | self = .init(id: id, content: .failure(error)) 222 | 223 | return 224 | } 225 | 226 | // ok, we have both. This is not allowed by the spec, but we 227 | // don't want to be too strict with what we accept 228 | if try container.decodeNil(forKey: .error) { 229 | let value = try container.decode(T.self, forKey: .result) 230 | self = .init(id: id, result: value) 231 | 232 | return 233 | } 234 | 235 | // in this case, 236 | if try container.decodeNil(forKey: .result) { 237 | let error = try container.decode(AnyJSONRPCResponseError.self, forKey: .error) 238 | self = .init(id: id, content: .failure(error)) 239 | 240 | return 241 | } 242 | 243 | throw JSONRPCProtocolError.malformedMessage 244 | } 245 | } 246 | 247 | extension JSONRPCResponse: Encodable where T: Encodable { 248 | public func encode(to encoder: Encoder) throws { 249 | var container = encoder.container(keyedBy: CodingKeys.self) 250 | 251 | try container.encode("2.0", forKey: .jsonrpc) 252 | 253 | try container.encode(id, forKey: .id) 254 | 255 | switch self.content { 256 | case .failure(let error): 257 | try container.encode(error, forKey: .error) 258 | case .success(let value): 259 | try container.encode(value, forKey: .result) 260 | } 261 | } 262 | } 263 | 264 | extension JSONRPCResponse: Equatable where T: Equatable {} 265 | extension JSONRPCResponse: Hashable where T: Hashable {} 266 | extension JSONRPCResponse: Sendable where T: Sendable {} 267 | 268 | public typealias AnyJSONRPCResponse = JSONRPCResponse 269 | -------------------------------------------------------------------------------- /Sources/JSONRPC/JSONValueDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(Combine) 3 | import Combine 4 | #else 5 | public protocol TopLevelDecoder { 6 | associatedtype Input 7 | 8 | func decode( 9 | _ type: T.Type, 10 | from: JSONValueDecoder.Input 11 | ) throws -> T where T: Decodable 12 | } 13 | #endif 14 | 15 | /// An object that decodes instances of a data type from `JSONValue` objects. 16 | public class JSONValueDecoder: TopLevelDecoder { 17 | public typealias Input = JSONValue 18 | 19 | public init() { 20 | // Do nothing, this definition here just to bump constructor visibility 21 | } 22 | 23 | /// Returns a value of the type you specify, decoded from a `JSONValue` object. 24 | public func decode( 25 | _ type: T.Type, 26 | from: JSONValueDecoder.Input 27 | ) throws -> T where T: Decodable { 28 | return try T(from: JSONValueDecoderImpl(referencing: from)) 29 | } 30 | } 31 | 32 | fileprivate struct JSONKey: CodingKey { 33 | var stringValue: String 34 | var intValue: Int? 35 | 36 | init(stringValue: String) { 37 | self.stringValue = stringValue 38 | self.intValue = nil 39 | } 40 | 41 | init(intValue: Int) { 42 | self.stringValue = "\(intValue)" 43 | self.intValue = intValue 44 | } 45 | 46 | static let `super` = JSONKey(stringValue: "super") 47 | } 48 | 49 | fileprivate class JSONUnkeyedContainer: UnkeyedDecodingContainer { 50 | private let decoder: JSONValueDecoderImpl 51 | private let container: [JSONValue] 52 | private(set) var currentIndex: Int = 0 53 | 54 | internal init(referencing decoder: JSONValueDecoderImpl, wrapping container: [JSONValue]) { 55 | self.decoder = decoder 56 | self.container = container 57 | } 58 | 59 | var codingPath: [CodingKey] { 60 | return decoder.codingPath 61 | } 62 | 63 | var count: Int? { 64 | return container.count 65 | } 66 | 67 | var isAtEnd: Bool { 68 | return currentIndex >= container.count 69 | } 70 | 71 | private func withNextValue(result: (_ value: JSONValue) throws -> T) throws -> T { 72 | decoder.codingPath.append(JSONKey(intValue: self.currentIndex)) 73 | defer { decoder.codingPath.removeLast() } 74 | 75 | if isAtEnd { 76 | throw DecodingError.valueNotFound( 77 | JSONValue?.self, 78 | DecodingError.Context( 79 | codingPath: decoder.codingPath, 80 | debugDescription: "Array exhausted." 81 | ) 82 | ) 83 | } 84 | 85 | let value = container[currentIndex] 86 | currentIndex += 1 87 | 88 | return try result(value) 89 | } 90 | 91 | func decodeNil() throws -> Bool { 92 | return try withNextValue { value in return value == .null } 93 | } 94 | 95 | func decode(_ type: T.Type) throws -> T where T: Decodable { 96 | return try withNextValue { value in 97 | return try decoder.nested(for: value).singleValueContainer().decode(type) 98 | } 99 | } 100 | 101 | func nestedContainer(keyedBy type: NestedKey.Type) throws -> 102 | KeyedDecodingContainer where NestedKey: CodingKey { 103 | return try withNextValue { value in 104 | return try decoder.nested(for: value).container(keyedBy: type) 105 | } 106 | } 107 | 108 | func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 109 | return try withNextValue { value in 110 | return try decoder.nested(for: value).unkeyedContainer() 111 | } 112 | } 113 | 114 | func superDecoder() throws -> Decoder { 115 | throw DecodingError.valueNotFound( 116 | JSONValue?.self, 117 | DecodingError.Context( 118 | codingPath: decoder.codingPath, 119 | debugDescription: "Super not supported for arrays." 120 | ) 121 | ) 122 | } 123 | } 124 | 125 | fileprivate class JSONKeyedContainer: KeyedDecodingContainerProtocol { 126 | typealias Key = K 127 | 128 | let decoder: JSONValueDecoderImpl 129 | let container: [String: JSONValue] 130 | 131 | internal init(referencing decoder: JSONValueDecoderImpl, 132 | wrapping container: [String: JSONValue]) { 133 | self.decoder = decoder 134 | self.container = container 135 | } 136 | 137 | var codingPath: [CodingKey] { 138 | return decoder.codingPath 139 | } 140 | 141 | var allKeys: [Key] { 142 | return container.keys.compactMap { Key(stringValue: $0) } 143 | } 144 | 145 | func contains(_ key: Key) -> Bool { 146 | return container[key.stringValue] != nil 147 | } 148 | 149 | func decodeNil(forKey key: Key) throws -> Bool { 150 | if let entry = container[key.stringValue] { 151 | return entry == .null 152 | } else { 153 | return true 154 | } 155 | } 156 | 157 | private func withValue(forKey key: CodingKey, 158 | result: (_ entry: JSONValue) throws -> T) throws -> T { 159 | guard let value = container[key.stringValue] else { 160 | throw DecodingError.keyNotFound( 161 | key, 162 | DecodingError.Context( 163 | codingPath: decoder.codingPath, 164 | debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\")" 165 | ) 166 | ) 167 | } 168 | 169 | decoder.codingPath.append(key) 170 | defer { decoder.codingPath.removeLast() } 171 | 172 | return try result(value) 173 | } 174 | 175 | func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { 176 | try withValue(forKey: key) { value in 177 | return try decoder.nested(for: value).singleValueContainer().decode(type) 178 | } 179 | } 180 | 181 | func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) 182 | throws -> KeyedDecodingContainer where NestedKey: CodingKey { 183 | try withValue(forKey: key) { value in 184 | return try decoder.nested(for: value).container(keyedBy: type) 185 | } 186 | } 187 | 188 | func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { 189 | try withValue(forKey: key) { value in 190 | return try decoder.nested(for: value).unkeyedContainer() 191 | } 192 | } 193 | 194 | func superDecoder() throws -> Decoder { 195 | try withValue(forKey: JSONKey.super) { value in decoder.nested(for: value) } 196 | } 197 | 198 | func superDecoder(forKey key: Key) throws -> Decoder { 199 | try withValue(forKey: key) { value in decoder.nested(for: value) } 200 | } 201 | } 202 | 203 | fileprivate class JSONValueDecoderImpl: Decoder, SingleValueDecodingContainer { 204 | var codingPath: [CodingKey] 205 | var userInfo: [CodingUserInfoKey: Any] = [:] 206 | private let value: JSONValue 207 | 208 | init(referencing value: JSONValue, at codingPath: [CodingKey] = []) { 209 | self.value = value 210 | self.codingPath = codingPath 211 | } 212 | 213 | internal func nested(for value: JSONValue) -> JSONValueDecoderImpl { 214 | return JSONValueDecoderImpl(referencing: value, at: codingPath) 215 | } 216 | 217 | private func unsatisfiedType(_ type: Any.Type) throws -> Never { 218 | throw DecodingError.typeMismatch( 219 | type, 220 | DecodingError.Context( 221 | codingPath: codingPath, 222 | debugDescription: "\(String(describing: value)) not a \(type)", 223 | underlyingError: nil 224 | ) 225 | ) 226 | } 227 | 228 | private func inconvertibleType(_ type: Any.Type) throws -> Never { 229 | throw DecodingError.dataCorrupted( 230 | DecodingError.Context( 231 | codingPath: codingPath, 232 | debugDescription: "\(String(describing: value)) does not fit in a \(type)", 233 | underlyingError: nil 234 | ) 235 | ) 236 | } 237 | 238 | func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer 239 | where Key: CodingKey { 240 | if case let .hash(dictionary) = value { 241 | return KeyedDecodingContainer(JSONKeyedContainer(referencing: self, wrapping: dictionary)) 242 | } else { 243 | throw DecodingError.typeMismatch( 244 | [String: Any].self, 245 | DecodingError.Context( 246 | codingPath: codingPath, 247 | debugDescription: "\(String(describing: value)) not a hash", 248 | underlyingError: nil 249 | ) 250 | ) 251 | } 252 | } 253 | 254 | func unkeyedContainer() throws -> UnkeyedDecodingContainer { 255 | if case let .array(array) = value { 256 | return JSONUnkeyedContainer(referencing: self, wrapping: array) 257 | } else { 258 | throw DecodingError.typeMismatch( 259 | [String: Any].self, 260 | DecodingError.Context( 261 | codingPath: codingPath, 262 | debugDescription: "\(String(describing: value)) not an array", 263 | underlyingError: nil 264 | ) 265 | ) 266 | } 267 | } 268 | 269 | func singleValueContainer() throws -> SingleValueDecodingContainer { 270 | self 271 | } 272 | 273 | func decodeNil() -> Bool { 274 | return self.value == .null 275 | } 276 | 277 | func decode(_ type: Bool.Type) throws -> Bool { 278 | guard case let .bool(value) = self.value else { try unsatisfiedType(type) } 279 | return value 280 | } 281 | 282 | func decode(_ type: String.Type) throws -> String { 283 | guard case let .string(value) = self.value else { try unsatisfiedType(type) } 284 | return value 285 | } 286 | 287 | func decode(_ type: Double.Type) throws -> Double { 288 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 289 | return value 290 | } 291 | 292 | func decode(_ type: Float.Type) throws -> Float { 293 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 294 | let floatValue = Float(value) 295 | if value.isFinite && floatValue.isInfinite { try inconvertibleType(type) } 296 | return floatValue 297 | } 298 | 299 | func decode(_ type: Int.Type) throws -> Int { 300 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 301 | guard let value = Int(exactly: value) else { try inconvertibleType(type) } 302 | return value 303 | } 304 | 305 | func decode(_ type: Int8.Type) throws -> Int8 { 306 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 307 | guard let value = Int8(exactly: value) else { try inconvertibleType(type) } 308 | return value 309 | } 310 | 311 | func decode(_ type: Int16.Type) throws -> Int16 { 312 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 313 | guard let value = Int16(exactly: value) else { try inconvertibleType(type) } 314 | return value 315 | } 316 | 317 | func decode(_ type: Int32.Type) throws -> Int32 { 318 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 319 | guard let value = Int32(exactly: value) else { try inconvertibleType(type) } 320 | return value 321 | } 322 | 323 | func decode(_ type: Int64.Type) throws -> Int64 { 324 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 325 | guard let value = Int64(exactly: value) else { try inconvertibleType(type) } 326 | return value 327 | } 328 | 329 | func decode(_ type: UInt.Type) throws -> UInt { 330 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 331 | guard let value = UInt(exactly: value) else { try inconvertibleType(type) } 332 | return value 333 | } 334 | 335 | func decode(_ type: UInt8.Type) throws -> UInt8 { 336 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 337 | guard let value = UInt8(exactly: value) else { try inconvertibleType(type) } 338 | return value 339 | } 340 | 341 | func decode(_ type: UInt16.Type) throws -> UInt16 { 342 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 343 | guard let value = UInt16(exactly: value) else { try inconvertibleType(type) } 344 | return value 345 | } 346 | 347 | func decode(_ type: UInt32.Type) throws -> UInt32 { 348 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 349 | guard let value = UInt32(exactly: value) else { try inconvertibleType(type) } 350 | return value 351 | } 352 | 353 | func decode(_ type: UInt64.Type) throws -> UInt64 { 354 | guard case let .number(value) = self.value else { try unsatisfiedType(type) } 355 | guard let value = UInt64(exactly: value) else { try inconvertibleType(type) } 356 | return value 357 | } 358 | 359 | func decode(_ type: T.Type) throws -> T where T: Decodable { 360 | return try T(from: self) 361 | } 362 | } 363 | --------------------------------------------------------------------------------