Browse Source

add generic for FromRequest and Responder & config user check

appflowy 3 years ago
parent
commit
9371a3d31e

+ 15 - 0
.idea/appflowy_client.iml

@@ -36,6 +36,21 @@
       <excludeFolder url="file://$MODULE_DIR$/app_flowy/packages/flowy_protobuf/.pub" />
       <excludeFolder url="file://$MODULE_DIR$/app_flowy/packages/flowy_protobuf/build" />
       <excludeFolder url="file://$MODULE_DIR$/rust-lib/target" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/path_provider_macos/example/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/path_provider_macos/example/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/path_provider_macos/example/build" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/path_provider_macos/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/path_provider_macos/build" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/path_provider_macos/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/flowy_sdk/example/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/flowy_sdk/example/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/flowy_sdk/example/build" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/flowy_sdk/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/flowy_sdk/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/flowy_sdk/build" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/window_size/build" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/window_size/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/app_flowy/macos/Flutter/ephemeral/.symlinks/plugins/window_size/.pub" />
     </content>
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />

+ 8 - 2
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"]

+ 13 - 0
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<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&format!("{}", self))
+    }
+}

+ 45 - 4
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<Result<String, SystemError>>;
+    type Future = Ready<Result<Self, Self::Error>>;
+
+    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<T> FromRequest for Result<T, T::Error>
+where
+    T: FromRequest,
+{
+    type Error = SystemError;
+    type Future = FromRequestFuture<T::Future>;
 
-    fn from_request(_req: &EventRequest, _payload: &mut Payload) -> Self::Future { ready(Ok("".to_string())) }
+    fn from_request(req: &EventRequest, payload: &mut Payload) -> Self::Future {
+        FromRequestFuture {
+            fut: T::from_request(req, payload),
+        }
+    }
+}
+
+#[pin_project::pin_project]
+pub struct FromRequestFuture<Fut> {
+    #[pin]
+    fut: Fut,
+}
+
+impl<Fut, T, E> Future for FromRequestFuture<Fut>
+where
+    Fut: Future<Output = Result<T, E>>,
+{
+    type Output = Result<Result<T, E>, SystemError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let this = self.project();
+        let res = ready!(this.fut.poll(cx));
+        Poll::Ready(Ok(res))
+    }
 }

+ 7 - 1
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<u8>),
     None,
@@ -29,6 +31,10 @@ impl std::convert::Into<ResponseData> for Bytes {
     fn into(self) -> ResponseData { ResponseData::Bytes(self.bytes().to_vec()) }
 }
 
+impl std::convert::Into<ResponseData> for Vec<u8> {
+    fn into(self) -> ResponseData { ResponseData::Bytes(self) }
+}
+
 impl std::convert::Into<ResponseData> for &str {
     fn into(self) -> ResponseData { self.to_string().into() }
 }

+ 11 - 0
rust-lib/flowy-sys/src/response/responder.rs

@@ -34,3 +34,14 @@ where
         }
     }
 }
+
+// #[cfg(feature = "use_serde")]
+// impl<T> 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()
+//     }
+// }

+ 20 - 8
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<SystemError>,
 }
 
@@ -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<S>(error: &Option<SystemError>, serializer: S) -> Result<S::Ok, S::Error>
 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<S>(data: &ResponseData, serializer: S) -> Result<S::Ok, S::Error>
 where
     S: Serializer,

+ 1 - 1
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},

+ 13 - 1
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" }
+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"

+ 7 - 0
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::*;

+ 10 - 0
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 } }
+}

+ 59 - 0
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<UserEmail, String> {
+        if validate_email(&s) {
+            Ok(Self(s))
+        } else {
+            Err(format!("{} is not a valid subscriber email.", s))
+        }
+    }
+}
+
+impl AsRef<str> 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: quickcheck::Gen>(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()
+    }
+}

+ 75 - 0
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<UserName, String> {
+        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<str> 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));
+    }
+}

+ 21 - 2
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<User> for UserData {
+    type Error = String;
+
+    fn try_into(self) -> Result<User, Self::Error> {
+        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!() }

+ 1 - 0
rust-lib/flowy-user/src/lib.rs

@@ -1,3 +1,4 @@
+mod domain;
 mod error;
 mod event;
 mod handlers;