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 |