From 9371a3d31eda230d035c260cc4fec0e195c48d51 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 29 Jun 2021 23:21:25 +0800 Subject: [PATCH] add generic for FromRequest and Responder & config user check --- .idea/appflowy_client.iml | 15 ++++ rust-lib/flowy-sys/Cargo.toml | 10 ++- rust-lib/flowy-sys/src/error/error.rs | 13 ++++ rust-lib/flowy-sys/src/request/request.rs | 49 +++++++++++-- rust-lib/flowy-sys/src/response/data.rs | 8 ++- rust-lib/flowy-sys/src/response/responder.rs | 11 +++ rust-lib/flowy-sys/src/response/response.rs | 28 +++++--- rust-lib/flowy-sys/src/stream.rs | 2 +- rust-lib/flowy-user/Cargo.toml | 14 +++- rust-lib/flowy-user/src/domain/mod.rs | 7 ++ rust-lib/flowy-user/src/domain/user.rs | 10 +++ rust-lib/flowy-user/src/domain/user_email.rs | 59 +++++++++++++++ rust-lib/flowy-user/src/domain/user_name.rs | 75 ++++++++++++++++++++ rust-lib/flowy-user/src/handlers/auth.rs | 23 +++++- rust-lib/flowy-user/src/lib.rs | 1 + 15 files changed, 306 insertions(+), 19 deletions(-) create mode 100644 rust-lib/flowy-user/src/domain/mod.rs create mode 100644 rust-lib/flowy-user/src/domain/user.rs create mode 100644 rust-lib/flowy-user/src/domain/user_email.rs create mode 100644 rust-lib/flowy-user/src/domain/user_name.rs diff --git a/.idea/appflowy_client.iml b/.idea/appflowy_client.iml index 42434a43ef..ab4b547fce 100644 --- a/.idea/appflowy_client.iml +++ b/.idea/appflowy_client.iml @@ -36,6 +36,21 @@ + + + + + + + + + + + + + + + diff --git a/rust-lib/flowy-sys/Cargo.toml b/rust-lib/flowy-sys/Cargo.toml index b8dc7ca2bc..f6c7d7c5e1 100644 --- a/rust-lib/flowy-sys/Cargo.toml +++ b/rust-lib/flowy-sys/Cargo.toml @@ -17,13 +17,19 @@ tokio = { version = "1", features = ["full"] } uuid = { version = "0.8", features = ["serde", "v4"] } log = "0.4.14" env_logger = "0.8" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" serde_with = "1.9.4" thread-id = "3.3.0" lazy_static = "1.4.0" dyn-clone = "1.0" +#optional crate +bincode = { version = "1.3", optional = true} +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = {version = "1.0", optional = true} + [dev-dependencies] tokio = { version = "1", features = ["full"] } futures-util = "0.3.15" + +[features] +use_serde = ["bincode", "serde", "serde_json"] diff --git a/rust-lib/flowy-sys/src/error/error.rs b/rust-lib/flowy-sys/src/error/error.rs index 52243567b2..fccb16f53e 100644 --- a/rust-lib/flowy-sys/src/error/error.rs +++ b/rust-lib/flowy-sys/src/error/error.rs @@ -6,6 +6,9 @@ use dyn_clone::DynClone; use std::{fmt, option::NoneError}; use tokio::sync::mpsc::error::SendError; +#[cfg(feature = "use_serde")] +use serde::{Deserialize, Serialize, Serializer}; + pub trait Error: fmt::Debug + fmt::Display + DynClone { fn status_code(&self) -> StatusCode; @@ -94,3 +97,13 @@ where fn as_response(&self) -> EventResponse { EventResponseBuilder::Err().data(format!("{}", self.inner)).build() } } + +#[cfg(feature = "use_serde")] +impl Serialize for SystemError { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_str(&format!("{}", self)) + } +} diff --git a/rust-lib/flowy-sys/src/request/request.rs b/rust-lib/flowy-sys/src/request/request.rs index 68b7800c02..7776a3f65e 100644 --- a/rust-lib/flowy-sys/src/request/request.rs +++ b/rust-lib/flowy-sys/src/request/request.rs @@ -1,16 +1,18 @@ use std::future::Future; use crate::{ - error::SystemError, + error::{InternalError, SystemError}, module::Event, request::payload::Payload, util::ready::{ready, Ready}, }; +use futures_core::ready; use std::{ fmt::{Debug, Display}, hash::Hash, + pin::Pin, + task::{Context, Poll}, }; - #[derive(Clone, Debug)] pub struct EventRequest { id: String, @@ -60,7 +62,46 @@ impl FromRequest for () { #[doc(hidden)] impl FromRequest for String { type Error = SystemError; - type Future = Ready>; + type Future = Ready>; - fn from_request(_req: &EventRequest, _payload: &mut Payload) -> Self::Future { ready(Ok("".to_string())) } + fn from_request(req: &EventRequest, _payload: &mut Payload) -> Self::Future { + match &req.data { + None => ready(Err(InternalError::new("Expected string but request had data").into())), + Some(buf) => ready(Ok(String::from_utf8_lossy(buf).into_owned())), + } + } +} + +#[doc(hidden)] +impl FromRequest for Result +where + T: FromRequest, +{ + type Error = SystemError; + type Future = FromRequestFuture; + + fn from_request(req: &EventRequest, payload: &mut Payload) -> Self::Future { + FromRequestFuture { + fut: T::from_request(req, payload), + } + } +} + +#[pin_project::pin_project] +pub struct FromRequestFuture { + #[pin] + fut: Fut, +} + +impl Future for FromRequestFuture +where + Fut: Future>, +{ + type Output = Result, SystemError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + let res = ready!(this.fut.poll(cx)); + Poll::Ready(Ok(res)) + } } diff --git a/rust-lib/flowy-sys/src/response/data.rs b/rust-lib/flowy-sys/src/response/data.rs index 7602701166..c0402d8ae9 100644 --- a/rust-lib/flowy-sys/src/response/data.rs +++ b/rust-lib/flowy-sys/src/response/data.rs @@ -1,8 +1,10 @@ use bytes::{Buf, Bytes}; +#[cfg(feature = "use_serde")] use serde::{Deserialize, Serialize}; use std::{fmt, fmt::Formatter}; -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Clone)] +#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] pub enum ResponseData { Bytes(Vec), None, @@ -29,6 +31,10 @@ impl std::convert::Into for Bytes { fn into(self) -> ResponseData { ResponseData::Bytes(self.bytes().to_vec()) } } +impl std::convert::Into for Vec { + fn into(self) -> ResponseData { ResponseData::Bytes(self) } +} + impl std::convert::Into for &str { fn into(self) -> ResponseData { self.to_string().into() } } diff --git a/rust-lib/flowy-sys/src/response/responder.rs b/rust-lib/flowy-sys/src/response/responder.rs index 03348d4e95..2e9d5a09c2 100644 --- a/rust-lib/flowy-sys/src/response/responder.rs +++ b/rust-lib/flowy-sys/src/response/responder.rs @@ -34,3 +34,14 @@ where } } } + +// #[cfg(feature = "use_serde")] +// impl Responder for T +// where +// T: serde::Serialize, +// { +// fn respond_to(self, request: &EventRequest) -> EventResponse { +// let bytes = bincode::serialize(&self).unwrap(); +// EventResponseBuilder::Ok().data(bytes).build() +// } +// } diff --git a/rust-lib/flowy-sys/src/response/response.rs b/rust-lib/flowy-sys/src/response/response.rs index 0be953f41f..69c4200937 100644 --- a/rust-lib/flowy-sys/src/response/response.rs +++ b/rust-lib/flowy-sys/src/response/response.rs @@ -3,23 +3,24 @@ use crate::{ request::EventRequest, response::{data::ResponseData, Responder}, }; -use serde::{Deserialize, Serialize, Serializer}; +#[cfg(feature = "use_serde")] +use serde::{Deserialize, Serialize, Serializer}; use std::{fmt, fmt::Formatter}; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] pub enum StatusCode { Ok, Err, } // serde user guide: https://serde.rs/field-attrs.html -#[derive(Serialize, Debug, Clone)] +#[derive(Debug, Clone)] +#[cfg_attr(feature = "use_serde", derive(Serialize))] pub struct EventResponse { - #[serde(serialize_with = "serialize_data")] pub data: ResponseData, pub status: StatusCode, - #[serde(serialize_with = "serialize_error")] pub error: Option, } @@ -35,10 +36,17 @@ impl EventResponse { impl std::fmt::Display for EventResponse { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match serde_json::to_string(self) { - Ok(json) => f.write_fmt(format_args!("{:?}", json))?, - Err(e) => f.write_fmt(format_args!("{:?}", e))?, + f.write_fmt(format_args!("Status_Code: {:?}", self.status))?; + + match &self.data { + ResponseData::Bytes(b) => f.write_fmt(format_args!("Data: {} bytes", b.len()))?, + ResponseData::None => f.write_fmt(format_args!("Data: Empty"))?, } + match &self.error { + Some(e) => f.write_fmt(format_args!("Error: {:?}", e))?, + None => {}, + } + Ok(()) } } @@ -48,6 +56,7 @@ impl Responder for EventResponse { fn respond_to(self, _: &EventRequest) -> EventResponse { self } } +#[cfg(feature = "use_serde")] fn serialize_error(error: &Option, serializer: S) -> Result where S: Serializer, @@ -58,6 +67,9 @@ where } } +// #[cfg_attr(feature = "use_serde", #[serde(serialize_with = +// "serialize_data")])] +#[cfg(feature = "use_serde")] fn serialize_data(data: &ResponseData, serializer: S) -> Result where S: Serializer, diff --git a/rust-lib/flowy-sys/src/stream.rs b/rust-lib/flowy-sys/src/stream.rs index 56cbf2ce55..f6999f6ba8 100644 --- a/rust-lib/flowy-sys/src/stream.rs +++ b/rust-lib/flowy-sys/src/stream.rs @@ -6,7 +6,7 @@ use crate::{ system::ModuleMap, }; use futures_core::{future::LocalBoxFuture, ready, task::Context}; -use std::{future::Future, hash::Hash}; +use std::future::Future; use tokio::{ macros::support::{Pin, Poll}, sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, diff --git a/rust-lib/flowy-user/Cargo.toml b/rust-lib/flowy-user/Cargo.toml index ce7ed6966d..1303c3c075 100644 --- a/rust-lib/flowy-user/Cargo.toml +++ b/rust-lib/flowy-user/Cargo.toml @@ -8,4 +8,16 @@ edition = "2018" [dependencies] derive_more = {version = "0.99", features = ["display"]} flowy-sys = { path = "../flowy-sys" } -flowy-log = { path = "../flowy-log" } \ No newline at end of file +flowy-log = { path = "../flowy-log" } +tracing = { version = "0.1", features = ["log"] } +bytes = "0.5" +serde = { version = "1.0", features = ["derive"] } +validator = "0.12.0" +rand = { version = "0.8", features=["std_rng"] } +unicode-segmentation = "1.7.1" + +[dev-dependencies] +quickcheck = "0.9.2" +quickcheck_macros = "0.9.1" +fake = "~2.3.0" +claim = "0.4.0" \ No newline at end of file diff --git a/rust-lib/flowy-user/src/domain/mod.rs b/rust-lib/flowy-user/src/domain/mod.rs new file mode 100644 index 0000000000..df46cf52fe --- /dev/null +++ b/rust-lib/flowy-user/src/domain/mod.rs @@ -0,0 +1,7 @@ +mod user; +mod user_email; +mod user_name; + +pub use user::*; +pub use user_email::*; +pub use user_name::*; diff --git a/rust-lib/flowy-user/src/domain/user.rs b/rust-lib/flowy-user/src/domain/user.rs new file mode 100644 index 0000000000..a93f998aeb --- /dev/null +++ b/rust-lib/flowy-user/src/domain/user.rs @@ -0,0 +1,10 @@ +use crate::domain::{user_email::UserEmail, user_name::UserName}; + +pub struct User { + name: UserName, + email: UserEmail, +} + +impl User { + pub fn new(name: UserName, email: UserEmail) -> Self { Self { name, email } } +} diff --git a/rust-lib/flowy-user/src/domain/user_email.rs b/rust-lib/flowy-user/src/domain/user_email.rs new file mode 100644 index 0000000000..c82e75fb4e --- /dev/null +++ b/rust-lib/flowy-user/src/domain/user_email.rs @@ -0,0 +1,59 @@ +use validator::validate_email; + +#[derive(Debug)] +pub struct UserEmail(String); + +impl UserEmail { + pub fn parse(s: String) -> Result { + if validate_email(&s) { + Ok(Self(s)) + } else { + Err(format!("{} is not a valid subscriber email.", s)) + } + } +} + +impl AsRef for UserEmail { + fn as_ref(&self) -> &str { &self.0 } +} + +#[cfg(test)] +mod tests { + use super::*; + use claim::assert_err; + use fake::{faker::internet::en::SafeEmail, Fake}; + use quickcheck::Gen; + + #[test] + fn empty_string_is_rejected() { + let email = "".to_string(); + assert_err!(UserEmail::parse(email)); + } + + #[test] + fn email_missing_at_symbol_is_rejected() { + let email = "helloworld.com".to_string(); + assert_err!(UserEmail::parse(email)); + } + + #[test] + fn email_missing_subject_is_rejected() { + let email = "@domain.com".to_string(); + assert_err!(UserEmail::parse(email)); + } + + #[derive(Debug, Clone)] + struct ValidEmailFixture(pub String); + + impl quickcheck::Arbitrary for ValidEmailFixture { + fn arbitrary(g: &mut G) -> Self { + let email = SafeEmail().fake_with_rng(g); + Self(email) + } + } + + #[quickcheck_macros::quickcheck] + fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool { + UserEmail::parse(valid_email.0).is_ok() + } +} diff --git a/rust-lib/flowy-user/src/domain/user_name.rs b/rust-lib/flowy-user/src/domain/user_name.rs new file mode 100644 index 0000000000..38f6339de9 --- /dev/null +++ b/rust-lib/flowy-user/src/domain/user_name.rs @@ -0,0 +1,75 @@ +use unicode_segmentation::UnicodeSegmentation; + +#[derive(Debug)] +pub struct UserName(String); + +impl UserName { + pub fn parse(s: String) -> Result { + let is_empty_or_whitespace = s.trim().is_empty(); + // A grapheme is defined by the Unicode standard as a "user-perceived" + // character: `å` is a single grapheme, but it is composed of two characters + // (`a` and `̊`). + // + // `graphemes` returns an iterator over the graphemes in the input `s`. + // `true` specifies that we want to use the extended grapheme definition set, + // the recommended one. + let is_too_long = s.graphemes(true).count() > 256; + + let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}']; + let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g)); + + if is_empty_or_whitespace || is_too_long || contains_forbidden_characters { + Err(format!("{} is not a valid subscriber name.", s)) + } else { + Ok(Self(s)) + } + } +} + +impl AsRef for UserName { + fn as_ref(&self) -> &str { &self.0 } +} + +#[cfg(test)] +mod tests { + use super::UserName; + use claim::{assert_err, assert_ok}; + + #[test] + fn a_256_grapheme_long_name_is_valid() { + let name = "a̐".repeat(256); + assert_ok!(UserName::parse(name)); + } + + #[test] + fn a_name_longer_than_256_graphemes_is_rejected() { + let name = "a".repeat(257); + assert_err!(UserName::parse(name)); + } + + #[test] + fn whitespace_only_names_are_rejected() { + let name = " ".to_string(); + assert_err!(UserName::parse(name)); + } + + #[test] + fn empty_string_is_rejected() { + let name = "".to_string(); + assert_err!(UserName::parse(name)); + } + + #[test] + fn names_containing_an_invalid_character_are_rejected() { + for name in vec!['/', '(', ')', '"', '<', '>', '\\', '{', '}'] { + let name = name.to_string(); + assert_err!(UserName::parse(name)); + } + } + + #[test] + fn a_valid_name_is_parsed_successfully() { + let name = "nathan".to_string(); + assert_ok!(UserName::parse(name)); + } +} diff --git a/rust-lib/flowy-user/src/handlers/auth.rs b/rust-lib/flowy-user/src/handlers/auth.rs index 50b2b55ab7..d1d4e4b301 100644 --- a/rust-lib/flowy-user/src/handlers/auth.rs +++ b/rust-lib/flowy-user/src/handlers/auth.rs @@ -1,2 +1,21 @@ -// #[tracing::instrument(name = "Adding a new subscriber")] -pub async fn user_check() -> String { "".to_owned() } +use crate::domain::{User, UserEmail, UserName}; +use bytes::Bytes; +use std::convert::TryInto; + +pub struct UserData { + name: String, + email: String, +} + +impl TryInto for UserData { + type Error = String; + + fn try_into(self) -> Result { + let name = UserName::parse(self.name)?; + let email = UserEmail::parse(self.email)?; + Ok(User::new(name, email)) + } +} + +#[tracing::instrument(name = "User check")] +pub async fn user_check(user_name: String) -> Bytes { unimplemented!() } diff --git a/rust-lib/flowy-user/src/lib.rs b/rust-lib/flowy-user/src/lib.rs index 41ae48f870..a5b325481c 100644 --- a/rust-lib/flowy-user/src/lib.rs +++ b/rust-lib/flowy-user/src/lib.rs @@ -1,3 +1,4 @@ +mod domain; mod error; mod event; mod handlers;