diff --git a/Cargo.lock b/Cargo.lock index bb405467c..a7369f0a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5445,6 +5445,7 @@ dependencies = [ "async-trait", "facetec-api-client", "hex", + "mockall", "primitives-auth-ticket", "primitives-liveness-data", "rand 0.7.3", @@ -5452,6 +5453,7 @@ dependencies = [ "robonode-crypto", "sc-tracing 3.0.0", "serde", + "serde_json", "sp-application-crypto", "tokio 1.9.0", "tracing", diff --git a/crates/robonode-server/Cargo.toml b/crates/robonode-server/Cargo.toml index 5a7e2997b..d01458303 100644 --- a/crates/robonode-server/Cargo.toml +++ b/crates/robonode-server/Cargo.toml @@ -24,6 +24,8 @@ uuid = { version = "0.8", features = ["v4"] } warp = "0.3" [dev-dependencies] +mockall = "0.10" +serde_json = "1" tracing-test = "0.1" [features] diff --git a/crates/robonode-server/src/http/filters.rs b/crates/robonode-server/src/http/filters.rs index c67eb86d6..9f45cace5 100644 --- a/crates/robonode-server/src/http/filters.rs +++ b/crates/robonode-server/src/http/filters.rs @@ -1,12 +1,16 @@ //! Filters, essentially how [`warp`] implements routes and middlewares. -use std::{convert::TryFrom, sync::Arc}; +use std::sync::Arc; +use serde::Serialize; use warp::Filter; use crate::{ http::handlers, - logic::{op_authenticate, op_enroll, Logic, Signer, Verifier}, + logic::{ + op_authenticate, op_enroll, op_get_facetec_device_sdk_params, op_get_facetec_session_token, + LogicOp, + }, }; /// Pass the [`Arc`] to the handler. @@ -30,12 +34,23 @@ where } /// The root mount point with all the routes. -pub fn root( - logic: Arc>, +pub fn root( + logic: Arc, ) -> impl Filter + Clone where - S: Signer> + Send + Sync + 'static, - PK: Send + Sync + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]> + Verifier> + Into>, + L: LogicOp + + LogicOp + + LogicOp + + LogicOp + + Send + + Sync, + >::Error: warp::reject::Reject, + >::Error: warp::reject::Reject, + >::Response: Serialize, + >::Error: warp::reject::Reject, + >::Response: Serialize, + >::Error: warp::reject::Reject, + >::Response: Serialize, { enroll(Arc::clone(&logic)) .or(authenticate(Arc::clone(&logic))) @@ -44,12 +59,12 @@ where } /// POST /enroll with JSON body. -fn enroll( - logic: Arc>, +fn enroll( + logic: Arc, ) -> impl Filter + Clone where - S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]>, + L: LogicOp + Send + Sync, + L::Error: warp::reject::Reject, { warp::path!("enroll") .and(warp::post()) @@ -59,12 +74,13 @@ where } /// POST /authenticate with JSON body. -fn authenticate( - logic: Arc>, +fn authenticate( + logic: Arc, ) -> impl Filter + Clone where - S: Signer> + Send + Sync + 'static, - PK: Send + Sync + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]> + Verifier> + Into>, + L: LogicOp + Send + Sync, + L::Error: warp::reject::Reject, + L::Response: Serialize, { warp::path!("authenticate") .and(warp::post()) @@ -74,12 +90,13 @@ where } /// GET /facetec-session-token. -fn get_facetec_session_token( - logic: Arc>, +fn get_facetec_session_token( + logic: Arc, ) -> impl Filter + Clone where - S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a [u8]>, + L: LogicOp + Send + Sync, + L::Error: warp::reject::Reject, + L::Response: Serialize, { warp::path!("facetec-session-token") .and(warp::get()) @@ -88,12 +105,13 @@ where } /// GET /facetec-device-sdk-params. -fn get_facetec_device_sdk_params( - logic: Arc>, +fn get_facetec_device_sdk_params( + logic: Arc, ) -> impl Filter + Clone where - S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a [u8]>, + L: LogicOp + Send + Sync, + L::Error: warp::reject::Reject, + L::Response: Serialize, { warp::path!("facetec-device-sdk-params") .and(warp::get()) diff --git a/crates/robonode-server/src/http/handlers.rs b/crates/robonode-server/src/http/handlers.rs index 3896e1a43..8715d45e3 100644 --- a/crates/robonode-server/src/http/handlers.rs +++ b/crates/robonode-server/src/http/handlers.rs @@ -1,40 +1,39 @@ //! Handlers, the HTTP transport coupling for the internal logic. -use std::{convert::TryFrom, sync::Arc}; +use serde::Serialize; +use std::sync::Arc; use warp::hyper::StatusCode; use warp::Reply; use crate::logic::{ op_authenticate, op_enroll, op_get_facetec_device_sdk_params, op_get_facetec_session_token, - Logic, Signer, Verifier, + LogicOp, }; /// Enroll operation HTTP transport coupling. -pub async fn enroll( - logic: Arc>, +pub async fn enroll( + logic: Arc, input: op_enroll::Request, ) -> Result where - S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]>, + L: LogicOp, + L::Error: warp::reject::Reject, { - logic.enroll(input).await.map_err(warp::reject::custom)?; + logic.call(input).await.map_err(warp::reject::custom)?; Ok(StatusCode::CREATED) } /// Authenticate operation HTTP transport coupling. -pub async fn authenticate( - logic: Arc>, +pub async fn authenticate( + logic: Arc, input: op_authenticate::Request, ) -> Result where - S: Signer> + Send + 'static, - PK: Send + Sync + for<'a> TryFrom<&'a [u8]> + Verifier> + Into>, + L: LogicOp, + L::Error: warp::reject::Reject, + L::Response: Serialize, { - let res = logic - .authenticate(input) - .await - .map_err(warp::reject::custom)?; + let res = logic.call(input).await.map_err(warp::reject::custom)?; let reply = warp::reply::json(&res); let reply = warp::reply::with_status(reply, StatusCode::OK); @@ -42,15 +41,16 @@ where } /// Get FaceTec Session Token operation HTTP transport coupling. -pub async fn get_facetec_session_token( - logic: Arc>, +pub async fn get_facetec_session_token( + logic: Arc, ) -> Result where - S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a [u8]>, + L: LogicOp, + L::Error: warp::reject::Reject, + L::Response: Serialize, { let res = logic - .get_facetec_session_token() + .call(op_get_facetec_session_token::Request {}) .await .map_err(warp::reject::custom)?; @@ -60,15 +60,16 @@ where } /// Get FaceTec Device SDK Params operation HTTP transport coupling. -pub async fn get_facetec_device_sdk_params( - logic: Arc>, +pub async fn get_facetec_device_sdk_params( + logic: Arc, ) -> Result where - S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a [u8]>, + L: LogicOp, + L::Error: warp::reject::Reject, + L::Response: Serialize, { let res = logic - .get_facetec_device_sdk_params() + .call(op_get_facetec_device_sdk_params::Request {}) .await .map_err(warp::reject::custom)?; diff --git a/crates/robonode-server/src/http/mod.rs b/crates/robonode-server/src/http/mod.rs index 4b8610cd8..8ecdd7d22 100644 --- a/crates/robonode-server/src/http/mod.rs +++ b/crates/robonode-server/src/http/mod.rs @@ -3,4 +3,7 @@ mod filters; mod handlers; +#[cfg(test)] +mod tests; + pub use filters::root; diff --git a/crates/robonode-server/src/http/tests.rs b/crates/robonode-server/src/http/tests.rs new file mode 100644 index 000000000..6e60d367e --- /dev/null +++ b/crates/robonode-server/src/http/tests.rs @@ -0,0 +1,278 @@ +use std::sync::Arc; + +use mockall::predicate::*; +use mockall::*; +use primitives_auth_ticket::OpaqueAuthTicket; +use primitives_liveness_data::OpaqueLivenessData; +use sp_application_crypto::sp_core::hexdisplay::AsBytesRef; +use warp::hyper::StatusCode; + +use crate::{ + http::root, + logic::{ + op_authenticate, op_enroll, op_get_facetec_device_sdk_params, op_get_facetec_session_token, + LogicOp, + }, +}; + +mock! { + Logic { + fn enroll(&self, req: op_enroll::Request) -> Result; + fn authenticate(&self, req: op_authenticate::Request) -> Result; + fn get_facetec_session_token(&self, req: op_get_facetec_session_token::Request) -> Result; + fn get_facetec_device_sdk_params(&self, req: op_get_facetec_device_sdk_params::Request) -> Result; + } +} + +macro_rules! impl_LogicOp { + ($name:ty, $request:ty, $response:ty, $error:ty, $call: ident) => { + #[async_trait::async_trait] + impl LogicOp<$request> for $name { + type Response = $response; + type Error = $error; + + async fn call(&self, req: $request) -> Result { + self.$call(req) + } + } + }; +} + +impl_LogicOp!( + MockLogic, + op_enroll::Request, + op_enroll::Response, + op_enroll::Error, + enroll +); + +impl_LogicOp!( + MockLogic, + op_authenticate::Request, + op_authenticate::Response, + op_authenticate::Error, + authenticate +); + +impl_LogicOp!( + MockLogic, + op_get_facetec_session_token::Request, + op_get_facetec_session_token::Response, + op_get_facetec_session_token::Error, + get_facetec_session_token +); + +impl_LogicOp!( + MockLogic, + op_get_facetec_device_sdk_params::Request, + op_get_facetec_device_sdk_params::Response, + op_get_facetec_device_sdk_params::Error, + get_facetec_device_sdk_params +); + +fn provide_authenticate_response() -> op_authenticate::Response { + op_authenticate::Response { + auth_ticket: OpaqueAuthTicket(b"ticket".to_vec()), + auth_ticket_signature: b"signature".to_vec(), + } +} + +fn provide_facetec_session_token() -> op_get_facetec_session_token::Response { + op_get_facetec_session_token::Response { + session_token: "token".to_owned(), + } +} + +fn provide_facetec_device_sdk_params() -> op_get_facetec_device_sdk_params::Response { + op_get_facetec_device_sdk_params::Response { + public_face_map_encryption_key: "key".to_owned(), + device_key_identifier: "id".to_owned(), + } +} + +#[tokio::test] +async fn it_works_enroll() { + let input = op_enroll::Request { + public_key: b"key".to_vec(), + liveness_data: OpaqueLivenessData(b"data".to_vec()), + }; + + let mut mock_logic = MockLogic::new(); + mock_logic + .expect_enroll() + .returning(|_| Ok(op_enroll::Response)); + + let logic = Arc::new(mock_logic); + let filter = root(logic); + + let res = warp::test::request() + .method("POST") + .path("/enroll") + .json(&input) + .reply(&filter) + .await; + + assert_eq!(res.status(), StatusCode::CREATED); + assert!(res.body().is_empty()); +} + +#[tokio::test] +async fn it_denies_enroll_with_invalid_public_key() { + let input = op_enroll::Request { + public_key: b"key".to_vec(), + liveness_data: OpaqueLivenessData(b"data".to_vec()), + }; + + let mut mock_logic = MockLogic::new(); + mock_logic + .expect_enroll() + .returning(|_| Err(op_enroll::Error::InvalidPublicKey)); + + let logic = Arc::new(mock_logic); + let filter = root(logic); + + let res = warp::test::request() + .method("POST") + .path("/enroll") + .json(&input) + .reply(&filter) + .await; + + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.body(), "Unhandled rejection: InvalidPublicKey"); +} + +#[tokio::test] +async fn it_works_authenticate() { + let input = op_authenticate::Request { + liveness_data: OpaqueLivenessData(b"data".to_vec()), + liveness_data_signature: b"signature".to_vec(), + }; + + let mut mock_logic = MockLogic::new(); + mock_logic + .expect_authenticate() + .returning(|_| Ok(provide_authenticate_response())); + + let logic = Arc::new(mock_logic); + let filter = root(logic); + + let res = warp::test::request() + .method("POST") + .path("/authenticate") + .json(&input) + .reply(&filter) + .await; + + let expected_response = serde_json::to_string(&provide_authenticate_response()).unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.body().as_bytes_ref(), expected_response.as_bytes()); +} + +#[tokio::test] +async fn it_denies_authenticate() { + let input = op_authenticate::Request { + liveness_data: OpaqueLivenessData(b"data".to_vec()), + liveness_data_signature: b"signature".to_vec(), + }; + + let mut mock_logic = MockLogic::new(); + mock_logic + .expect_authenticate() + .returning(|_| Err(op_authenticate::Error::InternalErrorDbSearchUnsuccessful)); + + let logic = Arc::new(mock_logic); + let filter = root(logic); + + let res = warp::test::request() + .method("POST") + .path("/authenticate") + .json(&input) + .reply(&filter) + .await; + + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + res.body(), + "Unhandled rejection: InternalErrorDbSearchUnsuccessful" + ); +} + +#[tokio::test] +async fn it_works_get_facetec_session_token() { + let input = op_get_facetec_session_token::Request; + + let mut mock_logic = MockLogic::new(); + mock_logic + .expect_get_facetec_session_token() + .returning(|_| Ok(provide_facetec_session_token())); + + let logic = Arc::new(mock_logic); + let filter = root(logic); + + let res = warp::test::request() + .method("GET") + .path("/facetec-session-token") + .json(&input) + .reply(&filter) + .await; + + let expected_response = serde_json::to_string(&provide_facetec_session_token()).unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.body().as_bytes_ref(), expected_response.as_bytes()); +} + +#[tokio::test] +async fn it_denies_get_facetec_session_token() { + let input = op_get_facetec_session_token::Request; + + let mut mock_logic = MockLogic::new(); + mock_logic + .expect_get_facetec_session_token() + .returning(|_| { + Err(op_get_facetec_session_token::Error::InternalErrorSessionTokenUnsuccessful) + }); + + let logic = Arc::new(mock_logic); + let filter = root(logic); + + let res = warp::test::request() + .method("GET") + .path("/facetec-session-token") + .json(&input) + .reply(&filter) + .await; + + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + res.body(), + "Unhandled rejection: InternalErrorSessionTokenUnsuccessful" + ); +} + +#[tokio::test] +async fn it_works_get_facetec_device_sdk_params() { + let input = op_get_facetec_device_sdk_params::Request; + + let mut mock_logic = MockLogic::new(); + mock_logic + .expect_get_facetec_device_sdk_params() + .returning(|_| Ok(provide_facetec_device_sdk_params())); + + let logic = Arc::new(mock_logic); + let filter = root(logic); + + let res = warp::test::request() + .method("GET") + .path("/facetec-device-sdk-params") + .json(&input) + .reply(&filter) + .await; + + let expected_response = serde_json::to_string(&provide_facetec_device_sdk_params()).unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.body().as_bytes_ref(), expected_response.as_bytes()); +} diff --git a/crates/robonode-server/src/logic/op_authenticate.rs b/crates/robonode-server/src/logic/op_authenticate.rs index fed55f365..32a123a3e 100644 --- a/crates/robonode-server/src/logic/op_authenticate.rs +++ b/crates/robonode-server/src/logic/op_authenticate.rs @@ -11,10 +11,10 @@ use serde::{Deserialize, Serialize}; use crate::logic::facetec_utils::{db_search_result_adapter, DbSearchResult}; -use super::{common::*, Logic, Signer, Verifier}; +use super::{common::*, Logic, LogicOp, Signer, Verifier}; /// The request of the authenticate operation. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Request { /// The liveness data that the validator owner provided. @@ -79,13 +79,16 @@ pub enum Error { InternalErrorAuthTicketSigningFailed, } -impl Logic +#[async_trait::async_trait] +impl LogicOp for Logic where - S: Signer> + Send + 'static, + S: Signer> + Send + 'static + Sync, PK: Send + Sync + for<'a> TryFrom<&'a [u8]> + Verifier> + Into>, { - /// An authenticate invocation handler. - pub async fn authenticate(&self, req: Request) -> Result { + type Response = Response; + type Error = Error; + + async fn call(&self, req: Request) -> Result { let liveness_data = LivenessData::try_from(&req.liveness_data).map_err(Error::InvalidLivenessData)?; diff --git a/crates/robonode-server/src/logic/op_enroll.rs b/crates/robonode-server/src/logic/op_enroll.rs index 96893e525..cae64eebd 100644 --- a/crates/robonode-server/src/logic/op_enroll.rs +++ b/crates/robonode-server/src/logic/op_enroll.rs @@ -4,15 +4,15 @@ use std::convert::TryFrom; use facetec_api_client as ft; use primitives_liveness_data::{LivenessData, OpaqueLivenessData}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tracing::{error, trace}; use crate::logic::facetec_utils::{db_search_result_adapter, DbSearchResult}; -use super::{common::*, Logic, Signer}; +use super::{common::*, Logic, LogicOp, Signer}; /// The request for the enroll operation. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Request { /// The public key of the validator. @@ -21,6 +21,11 @@ pub struct Request { pub liveness_data: OpaqueLivenessData, } +/// The response for the enroll operation. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Response; + /// The errors on the enroll operation. #[derive(Debug)] pub enum Error { @@ -54,13 +59,17 @@ pub enum Error { InternalErrorDbEnrollUnsuccessful, } -impl Logic +#[async_trait::async_trait] +impl LogicOp for Logic where S: Signer> + Send + 'static, PK: Send + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]>, { + type Response = (); + type Error = Error; + /// An enroll invocation handler. - pub async fn enroll(&self, req: Request) -> Result<(), Error> { + async fn call(&self, req: Request) -> Result { let public_key = PK::try_from(&req.public_key).map_err(|_| Error::InvalidPublicKey)?; let liveness_data = diff --git a/crates/robonode-server/src/logic/op_get_facetec_device_sdk_params.rs b/crates/robonode-server/src/logic/op_get_facetec_device_sdk_params.rs index 0a780529e..c62c34c98 100644 --- a/crates/robonode-server/src/logic/op_get_facetec_device_sdk_params.rs +++ b/crates/robonode-server/src/logic/op_get_facetec_device_sdk_params.rs @@ -2,9 +2,14 @@ use std::convert::TryFrom; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use super::{Logic, Signer}; +use super::{Logic, LogicOp, Signer}; + +/// The request of the get facetec device sdk params operation. +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Request; /// The response for the get facetec device sdk params operation. #[derive(Debug, Serialize)] @@ -20,13 +25,16 @@ pub struct Response { #[derive(Debug)] pub enum Error {} -impl Logic +#[async_trait::async_trait] +impl LogicOp for Logic where S: Signer> + Send + 'static, PK: Send + for<'a> TryFrom<&'a [u8]>, { - /// Get the FaceTec Device SDK params. - pub async fn get_facetec_device_sdk_params(&self) -> Result { + type Response = Response; + type Error = Error; + + async fn call(&self, _req: Request) -> Result { Ok(Response { device_key_identifier: self.facetec_device_sdk_params.device_key_identifier.clone(), public_face_map_encryption_key: self diff --git a/crates/robonode-server/src/logic/op_get_facetec_session_token.rs b/crates/robonode-server/src/logic/op_get_facetec_session_token.rs index b452ff499..73a4bef87 100644 --- a/crates/robonode-server/src/logic/op_get_facetec_session_token.rs +++ b/crates/robonode-server/src/logic/op_get_facetec_session_token.rs @@ -3,9 +3,14 @@ use std::convert::TryFrom; use facetec_api_client as ft; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use super::{Logic, Signer}; +use super::{Logic, LogicOp, Signer}; + +/// The request of the get facetec session token operation. +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Request; /// The response for the get facetec session token operation. #[derive(Debug, Serialize)] @@ -25,13 +30,16 @@ pub enum Error { InternalErrorSessionTokenUnsuccessful, } -impl Logic +#[async_trait::async_trait] +impl LogicOp for Logic where S: Signer> + Send + 'static, PK: Send + for<'a> TryFrom<&'a [u8]>, { - /// Get a FaceTec Session Token. - pub async fn get_facetec_session_token(&self) -> Result { + type Response = Response; + type Error = Error; + + async fn call(&self, _req: Request) -> Result { let unlocked = self.locked.lock().await; let res = unlocked diff --git a/crates/robonode-server/src/logic/tests.rs b/crates/robonode-server/src/logic/tests.rs index 0d469014f..73cfa877c 100644 --- a/crates/robonode-server/src/logic/tests.rs +++ b/crates/robonode-server/src/logic/tests.rs @@ -9,7 +9,7 @@ use tracing::{info, trace}; use crate::{logic::common::DB_GROUP_NAME, sequence::Sequence}; -use super::{Locked, Logic}; +use super::{Locked, Logic, LogicOp}; struct TestSigner; @@ -179,7 +179,7 @@ async fn standalone_enroll() { let (_guard, test_params, logic) = setup().await; logic - .enroll(super::op_enroll::Request { + .call(super::op_enroll::Request { liveness_data: test_params.enroll_liveness_data, public_key: TEST_PUBLIC_KEY.to_vec(), }) @@ -193,7 +193,7 @@ async fn first_authenticate() { let (_guard, test_params, logic) = setup().await; let err = logic - .authenticate(super::op_authenticate::Request { + .call(super::op_authenticate::Request { liveness_data: test_params.authenticate_liveness_data, liveness_data_signature: b"qwe".to_vec(), }) @@ -209,7 +209,7 @@ async fn enroll_authenticate() { let (_guard, test_params, logic) = setup().await; logic - .enroll(super::op_enroll::Request { + .call(super::op_enroll::Request { liveness_data: test_params.enroll_liveness_data, public_key: TEST_PUBLIC_KEY.to_vec(), }) @@ -219,7 +219,7 @@ async fn enroll_authenticate() { info!("enroll complete, authenticating now"); logic - .authenticate(super::op_authenticate::Request { + .call(super::op_authenticate::Request { liveness_data: test_params.authenticate_liveness_data, liveness_data_signature: b"qwe".to_vec(), }) @@ -233,7 +233,7 @@ async fn double_enroll() { let (_guard, test_params, logic) = setup().await; logic - .enroll(super::op_enroll::Request { + .call(super::op_enroll::Request { liveness_data: test_params.enroll_liveness_data, public_key: b"a".to_vec(), }) @@ -241,7 +241,7 @@ async fn double_enroll() { .unwrap(); let err = logic - .enroll(super::op_enroll::Request { + .call(super::op_enroll::Request { liveness_data: test_params.authenticate_liveness_data, public_key: b"b".to_vec(), }) diff --git a/crates/robonode-server/src/logic/traits.rs b/crates/robonode-server/src/logic/traits.rs index 9c594c451..5752b840b 100644 --- a/crates/robonode-server/src/logic/traits.rs +++ b/crates/robonode-server/src/logic/traits.rs @@ -27,3 +27,15 @@ pub trait Verifier { where D: AsRef<[u8]> + Send + 'a; } + +/// The trait to make logiс operations. +#[async_trait::async_trait] +pub trait LogicOp { + /// Logic operation Response type. + type Response; + /// Logic operation Error type. + type Error; + + /// Process logic operation request. + async fn call(&self, req: Request) -> Result; +}