Module no_proto::rpc[][src]

Remote Procedure Call APIs

You can optionally omit all the RPC related code with features = [] in your cargo.toml

The NoProto RPC framework builds on top of NoProto’s format and Rust’s conventions to provide a flexible, powerful and safe RPC protocol.

This RPC framework has zero transport code and is transport agnostic. You can send bytes between the server/client using any method you’d like.

It’s also possible to send messages in either direction, the Client & Server both have the ability to encode/decode requests and responses.

RPC JSON Spec

Before you can send bytes between servers and clients, you must let NoProto know the shape and format of your endpoints, requests and responses. Like schemas, RPC specs are written as JSON.

Any fields in your spec not required by the library will simply be ignored.

Required Fields

id, version

The id property should be a V4 UUID you’ve generated yourself. This website can help generate a UUID for you. The version property should be a semver string like 0.1.0 or 1.0.0 or 0.0.23.

The id and version data is encoded in every request and response. If you attempt to open a request or response that does not match the version and id of the specification you’re using, the request/response will fail to open.

name

The name property is the title for your specification. Should be something appropriate like “Todo App RPC” or “User Account RPC”.

author

The author property is a string and can contain any value. You can put your name here, your companies name, your email, whatever you’d like.

spec

Is an array of RPC specifications described below, this is the root of your specifications. The array should be at property spec.

RPC Specifications

There are 4 different kinds of values allowed in the spec array. They can be in any order and you can have as many of each type as you’d like.

1. Message

RPC messages are named NoProto schemas. They must have a msg property with the name of the schema, then a type property for the schema type. The messages MUST be valid NoProto schemas.

// Some valid messages
{"msg": "user_id", "type": "u32"}
 
{"msg": "address", "type": "struct", "fields": [
    ["street", {"type": "string"}],
    ["city", {"type": "string"}]
]}
 
{"msg": "tags", "type": "list", "of": {"type": "string"}}

2. RPC Method

Methods are named endpoints with arguments and responses. The arguments and responses MUST reference messages. They always contain a rpc property and an fn property which describes the endpoint arguments and return types.

RPC methods can have between 0 and 1 arguments and can return nothing, a value T, an option<T> or, Result<T,E>

// Some valid RPC methods
{"rpc": "get_count", "fn": "() -> self::count"}
 
{"rpc": "get_user", "fn": "(self::user_id) -> Option<self::user>"}
 
{"rpc": "del_user", "fn": "(self::user_id) -> Result<(), self::error>"}
 
{"rpc": "add_one", "fn": "(self::add_arg) -> Result<self::add_arg, self::error>"}
 
{"rpc": "trigger_action", "fn": "() -> ()"}

3. RPC Module

You can create nested namespaces inside your specification that contain their own specification. Namespaces require a mod property and spec property.

// a valid RPC module
{"mod": "user", "spec": [
    {"msg": "user_id", "type": "u32"},
    {"msg": "user_name", "type": "string"},
    {"rpc": "get_username", "fn": "(self::user_id) -> Option<self::user_name>"}
]}

4. Comments

You can insert string comments anywhere in your spec.

RPC Namespaces & Modules

I’m sure you’ve noticed the self being used above in the function definitions. You can create messages anywhere in your specification and they can be accessed by any RPC method in any namespace using the namespace syntax.

Methods can always access messages in their own namespace using self. Otherwise, the top of the name space is mod and messages in other namespaces can be used by their names. For example, let’s say we had a message named delete inside the modify RPC module inside the user RPC module. That message could be accessed by any RPC method with mod::user::modify::delete.

That might be confusing so here’s an example RPC spec with some fancy namespacing.

Example RPC JSON SPEC

{
    "name": "TEST API",
    "author": "Jeb Kermin",
    "id": "cc419a66-9bbe-48db-ad1c-e0ffa2a2376f",
    "version": "1.0.0",
    "spec": [
        {"msg": "Error", "type": "string" },
        {"msg": "Count", "type": "u32" },
        "this is a comment",
        {"rpc": "get_count", "fn": "() -> self::Count"},
        {"mod": "user", "spec": [
            {"msg": "username", "type": "string"},
            {"msg": "user_id", "type": "u32"},
            {"rpc": "get_user_id", "fn": "(self::username) -> Option<self::user_id>"},
            {"rpc": "del_user", "fn": "(self::user_id) -> Result<self::user_id, mod::Error>"},
            {"mod": "admin", "spec": [
                {"rpc": "update_user", "fn": "(mod::user::user_id) -> Result<(), mod::Error>"}
            ]}
        ]}
    ]
}

Using the RPC Framework

use no_proto::rpc::{NP_RPC_Factory, NP_ResponseKinds, NP_RPC_Response, NP_RPC_Request};
use no_proto::error::NP_Error;
 
// You can generate an RPC Factory with this method.
// Like NoProto Factories, this RPC factory can be used to encode/decode any number of requests/responses.
 
let rpc_factory = NP_RPC_Factory::new(r#"{
    "name": "Test API",
    "author": "Jeb Kermin",
    "id": "cc419a66-9bbe-48db-ad1c-e0ffa2a2376f",
    "version": "1.0.0",
    "spec": [
        {"msg": "Error", "type": "string" },
        {"msg": "Count", "type": "u32" },
        {"rpc": "get_count", "fn": "() -> self::Count"},
        {"mod": "user", "spec": [
            {"msg": "username", "type": "string"},
            {"msg": "user_id", "type": "u32"},
            {"rpc": "get_user_id", "fn": "(self::username) -> Option<self::user_id>"},
            {"rpc": "del_user", "fn": "(self::user_id) -> Result<self::user_id, mod::Error>"},
        ]}
    ]
}"#)?;
 
// rpc_factory should be initilized on server and client using an identical JSON RPC SPEC
// Both server and client can encode/decode responses and requests so the examples below are only a convention.
 
 
 
// SIMPLE EXAMPLE
 
// === CLIENT ===
// generate request
let get_count: NP_RPC_Request = rpc_factory.new_request("get_count")?;
// close request (request has no arguments)
let count_req_bytes: Vec<u8> = get_count.rpc_close();

// === SEND count_req_bytes to SERVER ===

// === SERVER ===
// ingest request
let a_request: NP_RPC_Request = rpc_factory.open_request(count_req_bytes)?;
assert_eq!(a_request.rpc_name(), "get_count");
// generate a response
let mut count_response: NP_RPC_Response = a_request.new_response()?;
// set response data
count_response.data.set(&[], 20u32)?;
// set response kind
count_response.kind = NP_ResponseKinds::Ok;
// close response
let respond_bytes = count_response.rpc_close()?;

// === SEND respond_bytes to CLIENT ====

// === CLIENT ===
let count_response = rpc_factory.open_response(respond_bytes)?;
// confirm our response matches the same request RPC we sent
assert_eq!(count_response.rpc_name(), "get_count");
// confirm that we got data in the response
assert_eq!(count_response.kind, NP_ResponseKinds::Ok);
// confirm it's the same data the server sent
assert_eq!(count_response.data.get(&[])?, Some(20u32));
 
 
 
// RESULT EXAMPLE
 
// === CLIENT ===
// generate request
let mut del_user: NP_RPC_Request = rpc_factory.new_request("user.del_user")?;
del_user.data.set(&[], 50u32)?;
let del_user_bytes: Vec<u8> = del_user.rpc_close();

// === SEND del_user_bytes to SERVER ===

// === SERVER ===
// ingest request
let a_request: NP_RPC_Request = rpc_factory.open_request(del_user_bytes)?;
assert_eq!(a_request.rpc_name(), "user.del_user");
// generate a response
let mut del_response: NP_RPC_Response = a_request.new_response()?;
// set response as ok with data
del_response.data.set(&[], 50u32)?;
del_response.kind = NP_ResponseKinds::Ok;
// close response
let respond_bytes = del_response.rpc_close()?;

// === SEND respond_bytes to CLIENT ====

// === CLIENT ===
let del_response = rpc_factory.open_response(respond_bytes)?;
// confirm our response matches the same request RPC we sent
assert_eq!(del_response.rpc_name(), "user.del_user");
// confirm that we got data in the response
assert_eq!(del_response.kind, NP_ResponseKinds::Ok);
// confirm it's the same data set on the server
assert_eq!(del_response.data.get(&[])?, Some(50u32));
 
 
 
// RESULT EXAMPLE 2
 
// === CLIENT ===
// generate request
let mut del_user: NP_RPC_Request = rpc_factory.new_request("user.del_user")?;
del_user.data.set(&[], 50u32)?;
let del_user_bytes: Vec<u8> = del_user.rpc_close();
 
// === SEND del_user_bytes to SERVER ===
 
// === SERVER ===
// ingest request
let a_request: NP_RPC_Request = rpc_factory.open_request(del_user_bytes)?;
assert_eq!(a_request.rpc_name(), "user.del_user");
// generate a response
let mut del_response: NP_RPC_Response = a_request.new_response()?;
// set response as error
del_response.error.set(&[], "Can't find user.")?;
del_response.kind = NP_ResponseKinds::Error;
// close response
let respond_bytes = del_response.rpc_close()?;
 
// === SEND respond_bytes to CLIENT ====
 
// === CLIENT ===
let del_response = rpc_factory.open_response(respond_bytes)?;
// confirm our response matches the same request RPC we sent
assert_eq!(del_response.rpc_name(), "user.del_user");
// confirm we recieved error response
assert_eq!(del_response.kind, NP_ResponseKinds::Error);
// get the error information
assert_eq!(del_response.error.get(&[])?, Some("Can't find user."));
 
 
 
// OPTION EXAMPLE
 
// === CLIENT ===
// generate request
let mut get_user: NP_RPC_Request = rpc_factory.new_request("user.get_user_id")?;
get_user.data.set(&[], "username")?;
let get_user_bytes: Vec<u8> = get_user.rpc_close();
 
// === SEND get_user_bytes to SERVER ===
 
// === SERVER ===
// ingest request
let a_request: NP_RPC_Request = rpc_factory.open_request(get_user_bytes)?;
assert_eq!(a_request.rpc_name(), "user.get_user_id");
// generate a response
let mut del_response: NP_RPC_Response = a_request.new_response()?;
// set response as none
del_response.kind = NP_ResponseKinds::None;
// close response
let respond_bytes = del_response.rpc_close()?;
 
// === SEND respond_bytes to CLIENT ====
 
// === CLIENT ===
let del_response = rpc_factory.open_response(respond_bytes)?;
// confirm our response matches the same request RPC we sent
assert_eq!(del_response.rpc_name(), "user.get_user_id");
// confirm that we got data in the response
assert_eq!(del_response.kind, NP_ResponseKinds::None);
// with NONE response there is no data
 

Structs

NP_RPC_Factory

RPC Factory

NP_RPC_Request

RPC Request object

NP_RPC_Response

RPC Response object

Enums

NP_ResponseKinds

The different kinds of responses