add generic for FromRequest and Responder & config user check

This commit is contained in:
appflowy 2021-06-29 23:21:25 +08:00
parent 4e45b1bdfe
commit 9371a3d31e
15 changed files with 306 additions and 19 deletions

View File

@ -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" />

View File

@ -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"]

View File

@ -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))
}
}

View File

@ -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 { 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<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 {
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))
}
}

View File

@ -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() }
}

View File

@ -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()
// }
// }

View File

@ -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,

View File

@ -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},

View File

@ -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"

View File

@ -0,0 +1,7 @@
mod user;
mod user_email;
mod user_name;
pub use user::*;
pub use user_email::*;
pub use user_name::*;

View File

@ -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 } }
}

View File

@ -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()
}
}

View File

@ -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 = "".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));
}
}

View File

@ -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!() }

View File

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