diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f88dba --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fa3228d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "minirpc" +] diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..50e72c6 --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Pierre Brouca + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ca6868 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# minirpc diff --git a/minirpc/Cargo.toml b/minirpc/Cargo.toml new file mode 100644 index 0000000..71a5c63 --- /dev/null +++ b/minirpc/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "minirpc" +version = "0.1.0" +authors = ["broucz "] +edition = "2018" +description = "A minimalist RPC framework." +documentation = "https://docs.rs/minirpc/" +homepage = "https://github.com/broucz/minirpc" +repository = "https://github.com/broucz/minirpc" +keywords = ["rpc"] +readme = "README.md" +license = "MIT" + +[dependencies] +serde = "1.0.92" +serde_derive = "1.0.92" +serde_json = "1.0.39" diff --git a/minirpc/src/call.rs b/minirpc/src/call.rs new file mode 100644 index 0000000..5c319e1 --- /dev/null +++ b/minirpc/src/call.rs @@ -0,0 +1,41 @@ +use crate::{Id, Method, Params}; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Call { + pub id: Id, + pub method: Method, + pub params: Params, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{self, Value}; + + #[test] + fn call_deserialization() { + let input = r#"{"id":1,"method":"test_method","params":[1,2,3]}"#; + let expected = Call { + id: Id::Number(1), + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + }; + + let result: Call = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn call_serialization() { + let input = Call { + id: Id::Number(1), + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + }; + let expected = r#"{"id":1,"method":"test_method","params":[1,2,3]}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/error.rs b/minirpc/src/error.rs new file mode 100644 index 0000000..05f0e5f --- /dev/null +++ b/minirpc/src/error.rs @@ -0,0 +1,223 @@ +use serde::de::{Deserialize, Deserializer}; +use serde::ser::{Serialize, Serializer}; +use std::fmt; + +#[derive(Debug, PartialEq)] +pub enum Code { + ParseError, + InvalidRequest, + MethodNotFound, + InvalidParams, + InternalError, + ServerError(i64), +} + +impl Code { + pub fn message(&self) -> &str { + match *self { + Code::ParseError => "Parse error", + Code::InvalidRequest => "Invalid request", + Code::MethodNotFound => "Method not found", + Code::InvalidParams => "Invalid params", + Code::InternalError => "Internal error", + Code::ServerError(_) => "Server error", + } + } +} + +impl<'a> Deserialize<'a> for Code { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + Ok(match i64::deserialize(deserializer)? { + -32700 => Code::ParseError, + -32600 => Code::InvalidRequest, + -32601 => Code::MethodNotFound, + -32602 => Code::InvalidParams, + -32603 => Code::InternalError, + code => Code::ServerError(code), + }) + } +} + +impl Serialize for Code { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_i64(match *self { + Code::ParseError => -32700, + Code::InvalidRequest => -32600, + Code::MethodNotFound => -32601, + Code::InvalidParams => -32602, + Code::InternalError => -32603, + Code::ServerError(code) => code, + }) + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Error { + pub code: Code, + pub message: String, +} + +impl Error { + pub fn new(code: Code) -> Self { + let message = code.message().to_owned(); + Self { code, message } + } + + pub fn new_parse_error() -> Self { + Self::new(Code::ParseError) + } + + pub fn new_invalid_request() -> Self { + Self::new(Code::InvalidRequest) + } + + pub fn new_method_not_found() -> Self { + Self::new(Code::MethodNotFound) + } + + pub fn new_invalid_params() -> Self { + Self::new(Code::InvalidParams) + } + + pub fn new_internal_error() -> Self { + Self::new(Code::InternalError) + } + + pub fn new_server_error(code: i64, message: &str) -> Self { + Self { + code: Code::ServerError(code), + message: message.to_owned() + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn code_deserialization() { + let input = r#"-32700"#; + let expected = Code::ParseError; + + let result: Code = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn code_serialization() { + let input = Code::ParseError; + let expected = r#"-32700"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn error_deserialization() { + let input = r#"{"code":-32700,"message":"Parse error"}"#; + let expected = Error::new(Code::ParseError); + + let result: Error = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn error_serialization() { + let input = Error::new(Code::ParseError); + let expected = r#"{"code":-32700,"message":"Parse error"}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn error_new() { + let result = Error::new(Code::ParseError); + let expected = Error { + code: Code::ParseError, + message: "Parse error".to_owned() + }; + + assert_eq!(result, expected); + } + + #[test] + fn error_new_parse_error() { + let result = Error::new_parse_error(); + let expected = Error { + code: Code::ParseError, + message: "Parse error".to_owned() + }; + + assert_eq!(result, expected); + } + + #[test] + fn error_new_invalid_request() { + let result = Error::new_invalid_request(); + let expected = Error { + code: Code::InvalidRequest, + message: "Invalid request".to_owned() + }; + + assert_eq!(result, expected); + } + + #[test] + fn error_new_method_not_found() { + let result = Error::new_method_not_found(); + let expected = Error { + code: Code::MethodNotFound, + message: "Method not found".to_owned() + }; + + assert_eq!(result, expected); + } + + #[test] + fn error_new_invalid_params() { + let result = Error::new_invalid_params(); + let expected = Error { + code: Code::InvalidParams, + message: "Invalid params".to_owned() + }; + + assert_eq!(result, expected); + } + + #[test] + fn error_new_internal_error() { + let result = Error::new_internal_error(); + let expected = Error { + code: Code::InternalError, + message: "Internal error".to_owned() + }; + + assert_eq!(result, expected); + } + + #[test] + fn error_new_server_error() { + let result = Error::new_server_error(-32000, "Test error"); + let expected = Error { + code: Code::ServerError(-32000), + message: "Test error".to_owned() + }; + + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/failure.rs b/minirpc/src/failure.rs new file mode 100644 index 0000000..adf66ce --- /dev/null +++ b/minirpc/src/failure.rs @@ -0,0 +1,38 @@ +use crate::{Error, Id}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Failure { + pub error: Error, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn failure_deserialization() { + let input = r#"{"error":{"code":-32700,"message":"Parse error"},"id":1}"#; + let expected = Failure { + error: Error::new_parse_error(), + id: Some(Id::Number(1)), + }; + + let result: Failure = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn failure_serialization() { + let input = Failure { + error: Error::new_parse_error(), + id: Some(Id::Number(1)), + }; + let expected = r#"{"error":{"code":-32700,"message":"Parse error"},"id":1}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/id.rs b/minirpc/src/id.rs new file mode 100644 index 0000000..dc8c710 --- /dev/null +++ b/minirpc/src/id.rs @@ -0,0 +1,29 @@ +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Id { + Number(u64), +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn id_deserialization() { + let input = r#"1"#; + let expected = Id::Number(1); + + let result: Id = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn id_serialization() { + let input = Id::Number(1); + let expected = r#"1"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/lib.rs b/minirpc/src/lib.rs new file mode 100644 index 0000000..e9e6582 --- /dev/null +++ b/minirpc/src/lib.rs @@ -0,0 +1,27 @@ +#[macro_use] +extern crate serde_derive; + +pub mod call; +pub mod error; +pub mod failure; +pub mod id; +pub mod method; +pub mod notification; +pub mod params; +pub mod request; +pub mod response; +pub mod success; + +pub use self::call::Call; +pub use self::error::Error; +pub use self::failure::Failure; +pub use self::id::Id; +pub use self::method::Method; +pub use self::notification::Notification; +pub use self::params::Params; +pub use self::request::Payload as RequestPayload; +pub use self::request::Request; +pub use self::response::Payload as ResponsePayload; +pub use self::response::Response; +pub use self::success::Success; +pub use serde_json::{Map, Value}; diff --git a/minirpc/src/method.rs b/minirpc/src/method.rs new file mode 100644 index 0000000..81f623e --- /dev/null +++ b/minirpc/src/method.rs @@ -0,0 +1,39 @@ +use std::fmt; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Method { + String(String), +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Method::String(string) => write!(f, "{}", string), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn method_deserialization() { + let input = r#""text_method""#; + let expected = Method::String("text_method".to_owned()); + + let result: Method = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn method_serialization() { + let input = Method::String("text_method".to_owned()); + let expected = r#""text_method""#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/notification.rs b/minirpc/src/notification.rs new file mode 100644 index 0000000..b8fa450 --- /dev/null +++ b/minirpc/src/notification.rs @@ -0,0 +1,38 @@ +use crate::{Method, Params}; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Notification { + pub method: Method, + pub params: Params, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{self, Value}; + + #[test] + fn notification_deserialization() { + let input = r#"{"method":"test_method","params":[1,2,3]}"#; + let expected = Notification { + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + }; + + let result: Notification = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn notification_serialization() { + let input = Notification { + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + }; + let expected = r#"{"method":"test_method","params":[1,2,3]}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/params.rs b/minirpc/src/params.rs new file mode 100644 index 0000000..15bc650 --- /dev/null +++ b/minirpc/src/params.rs @@ -0,0 +1,51 @@ +use serde_json::{Map, Value}; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Params { + Array(Vec), + Object(Map), +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn params_deserialization() { + // Array. + let input = r#"[1,true]"#; + let expected = Params::Array(vec![Value::from(1), Value::Bool(true)]); + + let result: Params = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + + // Object. + let input = r#"{"foo":"bar"}"#; + let mut map = Map::new(); + map.insert("foo".to_string(), "bar".into()); + let expected = Params::Object(map); + + let result: Params = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn params_serialization() { + // Array. + let input = Params::Array(vec![Value::from(1), Value::Bool(true)]); + let expected = r#"[1,true]"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + + // Object. + let mut input = Map::new(); + input.insert("foo".to_string(), "bar".into()); + let expected = r#"{"foo":"bar"}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/request.rs b/minirpc/src/request.rs new file mode 100644 index 0000000..1e8ae20 --- /dev/null +++ b/minirpc/src/request.rs @@ -0,0 +1,106 @@ +use crate::{Call, Notification}; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Request { + Batch(Vec), + Single(Payload), +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Payload { + Notification(Notification), + Call(Call), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Call, Id, Method, Notification, Params}; + use serde_json::{self, Value}; + + #[test] + fn request_deserialization() { + // Single Notification. + let input = r#"{"method":"test_method","params":[1,2,3]}"#; + let expected = Request::Single(Payload::Notification(Notification { + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + })); + + let result: Request = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + + // Single Call. + let input = r#"{"id":1,"method":"test_method","params":[1,2,3]}"#; + let expected = Request::Single(Payload::Call(Call { + id: Id::Number(1), + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + })); + + let result: Request = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + + // Batch Notification and Call. + let input = + r#"[{"method":"test_method","params":[1,2,3]},{"id":1,"method":"test_method","params":[1,2,3]}]"#; + let expected = Request::Batch(vec![ + Payload::Notification(Notification { + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + }), + Payload::Call(Call { + id: Id::Number(1), + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + }), + ]); + + let result: Request = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn request_serialization() { + // Single Notification. + let input = Request::Single(Payload::Notification(Notification { + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + })); + let expected = r#"{"method":"test_method","params":[1,2,3]}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + + // Single Call. + let input = Request::Single(Payload::Call(Call { + id: Id::Number(1), + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + })); + let expected = r#"{"id":1,"method":"test_method","params":[1,2,3]}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + + // Batch Notification and Call. + let input = Request::Batch(vec![ + Payload::Notification(Notification { + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + }), + Payload::Call(Call { + id: Id::Number(1), + method: Method::String("test_method".to_owned()), + params: Params::Array(vec![Value::from(1), Value::from(2), Value::from(3)]), + }), + ]); + let expected = + r#"[{"method":"test_method","params":[1,2,3]},{"id":1,"method":"test_method","params":[1,2,3]}]"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/response.rs b/minirpc/src/response.rs new file mode 100644 index 0000000..f2b737b --- /dev/null +++ b/minirpc/src/response.rs @@ -0,0 +1,102 @@ +use crate::{Failure, Success}; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Payload { + Failure(Failure), + Success(Success), +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Response { + Batch(Vec), + Single(Payload), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Error, Id}; + use serde_json::{self, Value}; + + #[test] + fn response_deserialization() { + // Single Failure. + let input = r#"{"error":{"code":-32700,"message":"Parse error"},"id":1}"#; + let expected = Response::Single(Payload::Failure(Failure { + error: Error::new_parse_error(), + id: Some(Id::Number(1)), + })); + + let result: Response = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + + // Single Success. + let input = r#"{"id":1,"result":true}"#; + let expected = Response::Single(Payload::Success(Success { + id: Id::Number(1), + result: Value::Bool(true), + })); + + let result: Response = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + + // Batch Failure and Success. + let input = + r#"[{"error":{"code":-32700,"message":"Parse error"},"id":1},{"id":1,"result":true}]"#; + let expected = Response::Batch(vec![ + Payload::Failure(Failure { + error: Error::new_parse_error(), + id: Some(Id::Number(1)), + }), + Payload::Success(Success { + id: Id::Number(1), + result: Value::Bool(true), + }), + ]); + + let result: Response = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn response_serialization() { + // Single Failure. + let input = Response::Single(Payload::Failure(Failure { + error: Error::new_parse_error(), + id: Some(Id::Number(1)), + })); + let expected = r#"{"error":{"code":-32700,"message":"Parse error"},"id":1}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + + // Single Success. + let input = Response::Single(Payload::Success(Success { + id: Id::Number(1), + result: Value::Bool(true), + })); + let expected = r#"{"id":1,"result":true}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + + // Batch Failure and Success. + let input = Response::Batch(vec![ + Payload::Failure(Failure { + error: Error::new_parse_error(), + id: Some(Id::Number(1)), + }), + Payload::Success(Success { + id: Id::Number(1), + result: Value::Bool(true), + }), + ]); + let expected = + r#"[{"error":{"code":-32700,"message":"Parse error"},"id":1},{"id":1,"result":true}]"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/minirpc/src/success.rs b/minirpc/src/success.rs new file mode 100644 index 0000000..47d1e6c --- /dev/null +++ b/minirpc/src/success.rs @@ -0,0 +1,38 @@ +use crate::Id; +use serde_json::Value; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Success { + pub id: Id, + pub result: Value, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{self, Value}; + + #[test] + fn success_deserialization() { + let input = r#"{"id":1,"result":true}"#; + let expected = Success { + id: Id::Number(1), + result: Value::Bool(true), + }; + + let result: Success = serde_json::from_str(input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn success_serialization() { + let input = Success { + id: Id::Number(1), + result: Value::Bool(true), + }; + let expected = r#"{"id":1,"result":true}"#; + + let result = serde_json::to_string(&input).unwrap(); + assert_eq!(result, expected); + } +}