mirror of
https://github.com/actix/examples
synced 2024-11-30 17:14:35 +01:00
commit
22c9dc39ac
@ -16,6 +16,7 @@ members = [
|
|||||||
"http-proxy",
|
"http-proxy",
|
||||||
"http-full-proxy",
|
"http-full-proxy",
|
||||||
"json",
|
"json",
|
||||||
|
"jsonrpc",
|
||||||
"juniper",
|
"juniper",
|
||||||
"middleware",
|
"middleware",
|
||||||
"multipart",
|
"multipart",
|
||||||
|
17
jsonrpc/Cargo.toml
Normal file
17
jsonrpc/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "jsonrpc-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["mohanson <mohanson@outlook.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
workspace = ".."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = "0.8.0-alpha.2"
|
||||||
|
actix-web = "1.0.0-alpha.4"
|
||||||
|
env_logger = "0.6"
|
||||||
|
futures = "0.1.23"
|
||||||
|
futures-timer = "0.1"
|
||||||
|
log = "0.4"
|
||||||
|
serde = "1.0"
|
||||||
|
serde_derive = "1.0"
|
||||||
|
serde_json = "1.0"
|
34
jsonrpc/README.md
Normal file
34
jsonrpc/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
A simple demo for building a `JSONRPC over HTTP` server in [actix-web](https://github.com/actix/actix-web).
|
||||||
|
|
||||||
|
# Server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ cargo run
|
||||||
|
# Starting server on 127.0.0.1:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
# Client
|
||||||
|
|
||||||
|
**curl**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ping", "params": [], "id": 1}' http://127.0.0.1:8080
|
||||||
|
# {"jsonrpc":"2.0","result":"pong","error":null,"id":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
**python**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ python tests\test_client.py
|
||||||
|
# {'jsonrpc': '2.0', 'result': 'pong', 'error': None, 'id': 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Methods
|
||||||
|
|
||||||
|
- `ping`: Pong immeditely
|
||||||
|
- `wait`: Wait `n` seconds, and then pong
|
||||||
|
- `get`: Get global count
|
||||||
|
- `inc`: Increment global count
|
||||||
|
|
||||||
|
See `tests\test_client.py` to get more information.
|
137
jsonrpc/src/convention.rs
Normal file
137
jsonrpc/src/convention.rs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
//! JSON-RPC 2.0 Specification
|
||||||
|
//! See: https://www.jsonrpc.org/specification
|
||||||
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub static JSONRPC_VERSION: &str = "2.0";
|
||||||
|
|
||||||
|
/// When a rpc call encounters an error, the Response Object MUST contain the
|
||||||
|
/// error member with a value that is a Object with the following members:
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ErrorData {
|
||||||
|
/// A Number that indicates the error type that occurred. This MUST be an integer.
|
||||||
|
pub code: i32,
|
||||||
|
|
||||||
|
/// A String providing a short description of the error. The message SHOULD be
|
||||||
|
/// limited to a concise single sentence.
|
||||||
|
pub message: String,
|
||||||
|
|
||||||
|
/// A Primitive or Structured value that contains additional information
|
||||||
|
/// about the error. This may be omitted. The value of this member is
|
||||||
|
/// defined by the Server (e.g. detailed error information, nested errors
|
||||||
|
/// etc.).
|
||||||
|
pub data: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorData {
|
||||||
|
pub fn new(code: i32, message: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
code,
|
||||||
|
message: String::from(message),
|
||||||
|
data: Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn std(code: i32) -> Self {
|
||||||
|
match code {
|
||||||
|
// Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
|
||||||
|
-32700 => ErrorData::new(-32700, "Parse error"),
|
||||||
|
// The JSON sent is not a valid Request object.
|
||||||
|
-32600 => ErrorData::new(-32600, "Invalid Request"),
|
||||||
|
// The method does not exist / is not available.
|
||||||
|
-32601 => ErrorData::new(-32601, "Method not found"),
|
||||||
|
// Invalid method parameter(s).
|
||||||
|
-32602 => ErrorData::new(-32602, "Invalid params"),
|
||||||
|
// Internal JSON-RPC error.
|
||||||
|
-32603 => ErrorData::new(-32603, "Internal error"),
|
||||||
|
// The error codes from and including -32768 to -32000 are reserved for pre-defined errors. Any code within
|
||||||
|
// this range, but not defined explicitly below is reserved for future use.
|
||||||
|
_ => panic!("Undefined pre-defined error codes"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints out the value as JSON string.
|
||||||
|
pub fn dump(&self) -> String {
|
||||||
|
serde_json::to_string(self).expect("Should never failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for ErrorData {}
|
||||||
|
impl fmt::Display for ErrorData {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "({}, {}, {})", self.code, self.message, self.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rpc call is represented by sending a Request object to a Server.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Request {
|
||||||
|
/// A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
|
||||||
|
pub jsonrpc: String,
|
||||||
|
|
||||||
|
/// A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by
|
||||||
|
/// a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be
|
||||||
|
/// used for anything else.
|
||||||
|
pub method: String,
|
||||||
|
|
||||||
|
/// A Structured value that holds the parameter values to be used during the invocation of the method. This member
|
||||||
|
/// MAY be omitted.
|
||||||
|
pub params: Vec<Value>,
|
||||||
|
|
||||||
|
/// An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is
|
||||||
|
/// not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD
|
||||||
|
/// NOT contain fractional parts.
|
||||||
|
pub id: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Prints out the value as JSON string.
|
||||||
|
pub fn dump(&self) -> String {
|
||||||
|
serde_json::to_string(self).expect("Should never failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When a rpc call is made, the Server MUST reply with a Response, except for in the case of Notifications. The
|
||||||
|
/// Response is expressed as a single JSON Object, with the following members:
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Response {
|
||||||
|
/// A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
|
||||||
|
pub jsonrpc: String,
|
||||||
|
|
||||||
|
/// This member is REQUIRED on success.
|
||||||
|
/// This member MUST NOT exist if there was an error invoking the method.
|
||||||
|
/// The value of this member is determined by the method invoked on the Server.
|
||||||
|
pub result: Value,
|
||||||
|
|
||||||
|
// This member is REQUIRED on error.
|
||||||
|
// This member MUST NOT exist if there was no error triggered during invocation.
|
||||||
|
// The value for this member MUST be an Object as defined in section 5.1.
|
||||||
|
pub error: Option<ErrorData>,
|
||||||
|
|
||||||
|
/// This member is REQUIRED.
|
||||||
|
/// It MUST be the same as the value of the id member in the Request Object.
|
||||||
|
/// If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request),
|
||||||
|
/// it MUST be Null.
|
||||||
|
pub id: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Prints out the value as JSON string.
|
||||||
|
pub fn dump(&self) -> String {
|
||||||
|
serde_json::to_string(self).expect("Should never failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Response {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
jsonrpc: JSONRPC_VERSION.into(),
|
||||||
|
result: Value::Null,
|
||||||
|
error: None,
|
||||||
|
id: Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
158
jsonrpc/src/main.rs
Normal file
158
jsonrpc/src/main.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
use std::error;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
use futures::{future, Future, Stream};
|
||||||
|
use futures_timer::Delay;
|
||||||
|
use serde_json;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod convention;
|
||||||
|
|
||||||
|
/// The main handler for JSONRPC server.
|
||||||
|
fn rpc_handler(
|
||||||
|
req: HttpRequest,
|
||||||
|
payload: web::Payload,
|
||||||
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
|
payload.concat2().from_err().and_then(move |body| {
|
||||||
|
let reqjson: convention::Request = match serde_json::from_slice(body.as_ref()) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(_) => {
|
||||||
|
let r = convention::Response {
|
||||||
|
jsonrpc: String::from(convention::JSONRPC_VERSION),
|
||||||
|
result: Value::Null,
|
||||||
|
error: Some(convention::ErrorData::std(-32700)),
|
||||||
|
id: Value::Null,
|
||||||
|
};
|
||||||
|
return Ok(HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(r.dump()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let app_state = req.app_data().unwrap();
|
||||||
|
let mut result = convention::Response::default();
|
||||||
|
result.id = reqjson.id.clone();
|
||||||
|
|
||||||
|
match rpc_select(&app_state, reqjson.method.as_str(), reqjson.params) {
|
||||||
|
Ok(ok) => result.result = ok,
|
||||||
|
Err(e) => result.error = Some(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(result.dump()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rpc_select(
|
||||||
|
app_state: &AppState,
|
||||||
|
method: &str,
|
||||||
|
params: Vec<Value>,
|
||||||
|
) -> Result<Value, convention::ErrorData> {
|
||||||
|
match method {
|
||||||
|
"ping" => {
|
||||||
|
let r = app_state.network.read().unwrap().ping();
|
||||||
|
Ok(Value::from(r))
|
||||||
|
}
|
||||||
|
"wait" => {
|
||||||
|
if params.len() != 1 || !params[0].is_u64() {
|
||||||
|
return Err(convention::ErrorData::std(-32602));
|
||||||
|
}
|
||||||
|
match app_state
|
||||||
|
.network
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.wait(params[0].as_u64().unwrap())
|
||||||
|
.wait()
|
||||||
|
{
|
||||||
|
Ok(ok) => Ok(Value::from(ok)),
|
||||||
|
Err(e) => Err(convention::ErrorData::new(500, &format!("{:?}", e)[..])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"get" => {
|
||||||
|
let r = app_state.network.read().unwrap().get();
|
||||||
|
Ok(Value::from(r))
|
||||||
|
}
|
||||||
|
"inc" => {
|
||||||
|
app_state.network.write().unwrap().inc();
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
_ => Err(convention::ErrorData::std(-32601)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ImplNetwork {
|
||||||
|
fn ping(&self) -> String;
|
||||||
|
fn wait(&self, d: u64) -> Box<Future<Item = String, Error = Box<error::Error>>>;
|
||||||
|
|
||||||
|
fn get(&self) -> u32;
|
||||||
|
fn inc(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ObjNetwork {
|
||||||
|
c: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjNetwork {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { c: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImplNetwork for ObjNetwork {
|
||||||
|
fn ping(&self) -> String {
|
||||||
|
String::from("pong")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait(&self, d: u64) -> Box<Future<Item = String, Error = Box<error::Error>>> {
|
||||||
|
if let Err(e) = Delay::new(Duration::from_secs(d)).wait() {
|
||||||
|
let e: Box<error::Error> = Box::new(e);
|
||||||
|
return Box::new(future::err(e));
|
||||||
|
};
|
||||||
|
Box::new(future::ok(String::from("pong")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self) -> u32 {
|
||||||
|
self.c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inc(&mut self) {
|
||||||
|
self.c += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
network: Arc<RwLock<ImplNetwork>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(network: Arc<RwLock<ImplNetwork>>) -> Self {
|
||||||
|
Self { network }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
std::env::set_var("RUST_LOG", "info");
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let network = Arc::new(RwLock::new(ObjNetwork::new()));
|
||||||
|
|
||||||
|
let sys = actix::System::new("actix_jrpc");
|
||||||
|
HttpServer::new(move || {
|
||||||
|
let app_state = AppState::new(network.clone());
|
||||||
|
App::new()
|
||||||
|
.data(app_state)
|
||||||
|
.wrap(middleware::Logger::default())
|
||||||
|
.service(web::resource("/").route(web::post().to_async(rpc_handler)))
|
||||||
|
})
|
||||||
|
.bind("127.0.0.1:8080")
|
||||||
|
.unwrap()
|
||||||
|
.workers(1)
|
||||||
|
.start();
|
||||||
|
|
||||||
|
let _ = sys.run();
|
||||||
|
}
|
38
jsonrpc/tests/test_client.py
Normal file
38
jsonrpc/tests/test_client.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
print('ping: pong immediately')
|
||||||
|
r = requests.post('http://127.0.0.1:8080/', json={
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': 'ping',
|
||||||
|
'params': [],
|
||||||
|
'id': 1
|
||||||
|
})
|
||||||
|
print(r.json())
|
||||||
|
|
||||||
|
|
||||||
|
print('ping: pong after 4 secs')
|
||||||
|
r = requests.post('http://127.0.0.1:8080/', json={
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': 'wait',
|
||||||
|
'params': [4],
|
||||||
|
'id': 1
|
||||||
|
})
|
||||||
|
print(r.json())
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
print(f'inc {i:>02}')
|
||||||
|
r = requests.post('http://127.0.0.1:8080/', json={
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': 'inc',
|
||||||
|
'params': [],
|
||||||
|
'id': 1
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f'get')
|
||||||
|
r = requests.post('http://127.0.0.1:8080/', json={
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': 'get',
|
||||||
|
'params': [],
|
||||||
|
'id': 1
|
||||||
|
})
|
||||||
|
print(r.json())
|
Loading…
Reference in New Issue
Block a user