Skip to content

neildavis/json-rpc-proxy

 
 

Repository files navigation

JSON RPC Proxy

Library Examples Cocoapods platforms Cocoapods Cocoapods

Overview

A JSON-RPC 2.0 proxy for protocols in Objective-C and Swift (Objective-C compatible protocols marked @objc only)

This allows you to write your RPC function declarations in Swift or Objective-C using native types. The proxy will create the JSON-RPC request payload object, and convert the JSON-RPC response into a callback via a block/closure, marshalling all parameter and return types between Objective-C/Swift & JSON automagically.

Design Considerations

Transport

Like the JSON-RPC 2.0 specification itself, the proxy is 'transport independent'. A client provides a conformant 'transport' component that can take the request object from the proxy, provide it to the JSON-RPC server, take the response and return it to the proxy. The transport may optionally perform the JSON [de]serialization, or opt to have the proxy perform this duty and deal only with the serialized data.

A simple HTTP[S] transport component using URLSession is provided by the library for convenience and/or reference.

Language Choice ('Why Objective-C and not Swift?')

The proxy library itself is (currently) neccessarily implemented in Objective-C in order to benefit from the Objective-C Runtime's dynamic method dispatch via message passing. In particularly, it uses the 'Message Forwarding' capabilities of NSProxy which allows an object to respond to method/function invocations (e.g. from protocols) at runtime without any written & compiled code actually implementing them, which is pretty cool.

Whilst this limits application in Swift to compatible @objc types, this is unlikely to cause an issue for JSON-RPC services due to the primitive type system of JSON which has equivalent Foundation types in Objective-C which are bridged into Swift standard library types.

The proxy can be easily used in Swift, and if necessary wrapped up by other 'Swifty' constructs. E.g. some of the example applications demonstrate wrapping a JSON-RPC service to produce Rx style observable streams.

Terminology

  • This document will use the terms method and function interchangeably as is common between Objective-C and Swift.
  • This document will use the terms parameter and argument interchangeably in reference to methods/functions as is common between Objective-C and Swift.

Getting Started

After installing follow the steps in this section to get your JSON-RPC calls up & running, and/or take a look at the example applications.

Step 1: Write your protocol methods following convention

In order for the proxy to be able to successfully translate your RPC method names, parameters and return types to and from JSON-RPC, you must ensure your protocol methods adhere to some basic conventions:

  • Since JSON-RPC is asynchronous by nature, the return type of ALL proxied methods should ALWAYS be void. Return values (and errors) are passed in completion blocks or escaping closures which MUST be the last parameter of the method.
  • In Swift, all methods (or the protocol definition itself) should be marked @objc and use compatible/bridged types. This is required since the data marshalling is performed through the Objective-C runtime.
  • For by-position, parameters should NOT be named. In Objective-C, the first component of the selector is the method name. All other components of the selector should NOT be named. In Swift all function arguments should not have labels associated with them (_)
  • For by-name, parameters should be named. In Objective-C the first component of the selector should separate the method name from the first parameter name by the word With. This is analagous to the Objective-C/Swift compatibility conventions (e.g. #selector). In Swift, all function arguments should have labels associated with them.

Example using BY-POSITION Parameters

// Swift
@objc protocol MyProxiedProtocol {
    func addIntegers(_ first:Int, _ second:Int, _ completion: @escaping (Int?, NSError) -> Void) -> Void
}
// Objective-C
@protocol MyProxiedProtocol
- (void) addIntegers:(NSInteger)first :(NSInteger)second :(void (^)(NSInteger result, NSError *error))completion;
@end

Example using BY-NAME Parameters

// Swift
@objc protocol MyProxiedProtocol {
    func addIntegers(firstValue first:Int, secondValue second:Int, completion completion: @escaping (Int?, NSError) -> Void) -> Void
}
// Objective-C
@protocol MyProxiedProtocol
- (void) addIntegersWithFirstValue:(NSInteger)first secondValue:(NSInteger)second completion:(void (^)(NSInteger result, NSError *error))completion;
@end

Step 2: Create a transport component

The proxy is transport indpendent. This means it does not impose a particular method of sending the request to, and receiving the response from the server. The transport could be via HTTP[S], WebSocket, MQTT etc. However, since HTTP[S] is so common, a basic transport utilizing URLSession is also provided by the library.

The client must provide an object conforming to the JRPCProxyTransport protocol to the proxy during intialization. This object is responsible for sending the request to and receiving the response from the server, and may optionally perform JSON serialization.

// Swift
@objc class MyJSONRPCTransport : NSObject, JRPCProxyTransport {
// ...
}
// Objective-C
@interface MyJSONRPCTransport : NSObject<JRPCProxyTransport>
// ...
@end

You choose your serialization strategy by implementing ONE of the two optional methods of JRPCProxyTransport. If you implement both methods, the proxy will choose to delegate JSON serialiazation to the transport. If you implement neither, or do not supply a transport, the proxy will throw an exception when initialized.

Transports that do not perform JSON serialization

You may choose this strategy if you prefer to delegate the serialization to/from JSON to the proxy. This is suitable for e.g. HTTP[S] transport where the HTTP protocol & library can easily match the request & response for multiple concurrent requests. Your transport will deal only with opaque Data objects.

To choose this strategy, implement the following method:

// Swift
func sendJSONRPCPayload(withRequest payload: Data, completionQueue: DispatchQueue, completion: @escaping JRPCTransportDataCompletion) -> Void
// Objective-C
- (void) sendJSONRPCPayloadWithRequestData:(NSData*)payload
                completionQueue:(dispatch_queue_t)completionQueue
                     completion:(JRPCTransportDataCompletion)completion;

Transports that perform JSON serialization

You may choose this strategy if you prefer to perform the serialization to/from JSON in your own code, and/or require visibility of the JSON-RPC request & response objects. This is suitable for e.g. a duplex web socket transport where you need access to the JSON-RPC request id to match a response to the corresponding request when sending multiple concurrent requests that may return in any order.

To choose this strategy, implement the following method:

// Swift
func sendJSONRPCPayload(withRequestObject payload: Dictionary<String, AnyObject>, completionQueue: DispatchQueue, completion: @escaping JRPCTransportObjectCompletion) -> Void
// Objective-C
- (void) sendJSONRPCPayloadWithRequestObject:(NSDictionary*)jsonRPCRequest
                completionQueue:(dispatch_queue_t)completionQueue
                     completion:(JRPCTransportObjectCompletion)completion;

Your transport component may use the methods and key constants in the NSDictionary+JSONRPC category to access the JSON-RPC request & response dictionary objects.

Basic HTTP transport

If your app only requires simple HTTP transport using URLSession you can use the JRPCURLSessionTransport class provided, which uses the data task convenience methods of URLSession, and delegates JSON serialization duties to the proxy.

Step 3: Create a proxy for your protocol using your transport and invoke your methods

// Swift
// - create a transport
let transport = JRPCURLSessionTransport(urlSession: nil, url: URL(string: "http://example.com/")!)
// - create the proxy
let proxy = JRPCAbstractProxy.proxy(for: MyProxiedProtocol.self, paramStructure: .byName, transport: transport) as AnyObject
// - invoke methods
proxy.addIntegers(firstValue: 10, secondValue: 32) { (result, error) in
    // ...
}
// Objective-C
// - create a transport
id<JRPCProxyTransport> transport = [JRPCURLSessionTransport transportWithURLSession:nil url:@"http://example.com/"];
// - create the proxy
JRPCAbstractProxy *proxy = [JRPCAbstractProxy proxyForProtocol:@protocol(MyProxiedProtocol)
  paramStructure:JRPCParameterStructureByName
       transport:transport];
// - invoke methods
[proxy addIntegersWithFirstValue:10 secondValue:32 completion:^(NSInteger result, NSError *error) {
    // ...
}];

Examples

RandomLottery

An application to demonstrate the use of the random.org API to randomly pick six lottery numbers from a user specified range.

There are several versions of this example (all written in Swift) to demonstrate use with different architectures and 'Functional Reactive Programming (FRP)' frameworks:

  • MVC-Simple: A basic UIKit example which eschews architecture and best practices for simplicity. All of the code is in the view controller.
  • MVVM-RxSwift Another UIKit example, but utilizing the MVVM architecture and using the open source RxSwift FRP library for bindings.
  • MVVM-Combine Similar to the MVVM-RxSwift version. MVVM architecture using Apple's Combine FRP library for bindings, but still using UIKit for the main app views.
  • MVVM-SwiftUI An example in SwiftUI with MVVM architecture, again using Apple's Combine FRP library for bindings. This example also demonstrates use of the Swift Package Manager to import the library.

Note: You should obtain your own API key from random.org and use it, either by editing the scheme and setting it's value in the RANDOM_ORG_API_KEY environment variable (effective in debugger only), and/or by editing the code to replace the default value of '00000000-0000-0000-0000-000000000000'.

Installing

The easiest way to install JRPCProxy into your project is via CocoaPods or Carthage

CocoaPods

Add 'JRPCProxy' as a dependency in your Podfile

pod 'JRPCProxy'

If you are using the recommended use_frameworks! attribute for your target you can simply import the module:

// Swift
import JRPCProxy
// Objective-C
@import JRPCProxy;

Alternatively, for a static library, import the umbrella header into either your source files or pre-compiled header (Objective-C only)

import "JRPCProxy.h"

Swift Package Manager

Add a Swift Package Dependency using Xcode linking to this repo (https://github.com/neildavis/json-rpc-proxy)

Or, add the package as a dependency in your Package.swift:

// swift-tools-version:5.4

import PackageDescription

let package = Package(
  name: "MyApp",
  dependencies: [
    .package(url: "https://github.com/neildavis/json-rpc-proxy.git", .exact("1.1.2"))
  ],
  targets: [
    .target(name: "MyApp", dependencies: ["JRPCProxy"])
  ]
)

Carthage

Add 'JRPCProxy' as a dependency in your Cartfile ...

github "neildavis/json-rpc-proxy" "1.1.2"

... and update dependencies with Carthage

carthage update

Link JRPCProxy.framework in your project and import the module into your code:

// Swift
import JRPCProxy
// Objective-C
@import JRPCProxy;

Manual

Copy all the files in the JRPCProxy folder into your project.

Import the umbrella header JRPCProxy.h into your bridging header for Swift, or pre-compiled header for Objective-C

Scope

Supported Features

  • by-name and by-position parameter structures
  • Optional JSON serialization of request object and deserialization of the response object.
  • Automatic unique request id synthesis.
  • Automatic marshalling of basic data types between native and JSON types.
  • Support for extending marshalling to custom data types.
  • JSON-RPC errors are mapped to native error types.

Limitations & Omissions

  • Only version 2.0 of JSON-RPC is supported (not compatible with version 1.0)
  • Notifications are not supported.
  • Batch requests are not supported.
  • Server or 'symmetric' roles are not supported. Client role only.

Contributing

Please read CONTRIBUTING.md

Authors

  • Neil Davis - Initial open source work for YouView, since continued in a personal capacity.

License

This project is licensed under the MIT License - see the LICENSE file for details

Acknowledgments

Packages

No packages published

Languages

  • Swift 56.7%
  • Objective-C 41.8%
  • Other 1.5%