Add `url`, `regex` and `ip` validators.

This commit is contained in:
Sunli 2021-11-17 11:08:34 +08:00
parent f40c4ca3e6
commit 502bfb8670
12 changed files with 183 additions and 4 deletions

View File

@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.0.2] 2021-11-16
- Add `url`, `regex` and `ip` validators.
## [3.0.1] 2021-11-17
- Remove the `ctx` parameter of `CustomValidator::check`. [#710](https://github.com/async-graphql/async-graphql/issues/710)
## [3.0.0-alpha.2] 2021-11-16
- Change the signature of the `connection::query` function to allow the callback to use any type that implements `Into<Error>`.

View File

@ -58,9 +58,9 @@ secrecy = { version = "0.7.0", optional = true }
tracinglib = { version = "0.1.25", optional = true, package = "tracing" }
tracing-futures = { version = "0.2.5", optional = true, features = ["std-future", "futures-03"] }
opentelemetry = { version = "0.16.0", optional = true, default-features = false, features = ["trace"] }
url = { version = "2.2.1", optional = true }
uuid = { version = "0.8.2", optional = true, features = ["v4", "serde"] }
rust_decimal = { version = "1.14.3", optional = true }
url = { version = "2.2.1", optional = true }
# Non-feature optional dependencies
blocking = { version = "1.0.2", optional = true }

View File

@ -51,6 +51,12 @@ pub struct Validators {
chars_min_length: Option<usize>,
#[darling(default)]
email: bool,
#[darling(default)]
url: bool,
#[darling(default)]
ip: bool,
#[darling(default)]
regex: Option<String>,
#[darling(default, multiple)]
custom: Vec<SpannedValue<String>>,
#[darling(default)]
@ -134,6 +140,24 @@ impl Validators {
});
}
if self.url {
codes.push(quote! {
#crate_name::validators::url(#value)
});
}
if self.ip {
codes.push(quote! {
#crate_name::validators::ip(#value)
});
}
if let Some(re) = &self.regex {
codes.push(quote! {
#crate_name::validators::regex(#value, #re)
});
}
for s in &self.custom {
let expr: Expr =
syn::parse_str(s).map_err(|err| Error::new(s.span(), err.to_string()))?;

View File

@ -12,6 +12,9 @@
- **chars_max_length=N** the count of the unicode chars cannot be greater than `N`.
- **chars_min_length=N** the count of the unicode chars cannot be less than `N`.
- **email** is valid email.
- **url** is valid url.
- **ip** is valid ip address.
- **regex=RE** is match for the regex.
```rust
use async_graphql::*;

View File

@ -12,6 +12,9 @@
- **chars_max_length=N** 字符串中unicode字符的的数量不能小于`N`
- **chars_min_length=N** 字符串中unicode字符的的数量不能大于`N`
- **email** 有效的email
- **url** 有效的url
- **ip** 有效的ip地址
- **regex=RE** 匹配正则表达式
```rust
use async_graphql::*;

View File

@ -10,7 +10,7 @@ pub fn email<T: AsRef<str> + InputType>(value: &T) -> Result<(), InputValueError
if EMAIL_RE.is_match(value.as_ref()) {
Ok(())
} else {
Err("invalid email format".into())
Err("invalid email".into())
}
}

27
src/validators/ip.rs Normal file
View File

@ -0,0 +1,27 @@
use std::net::IpAddr;
use std::str::FromStr;
use crate::{InputType, InputValueError};
pub fn ip<T: AsRef<str> + InputType>(value: &T) -> Result<(), InputValueError<T>> {
if IpAddr::from_str(value.as_ref()).is_ok() {
Ok(())
} else {
Err("invalid ip".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ip() {
assert!(ip(&"1.1.1.1".to_string()).is_ok());
assert!(ip(&"255.0.0.0".to_string()).is_ok());
assert!(ip(&"256.1.1.1".to_string()).is_err());
assert!(ip(&"fe80::223:6cff:fe8a:2e8a".to_string()).is_ok());
assert!(ip(&"::ffff:254.42.16.14".to_string()).is_ok());
assert!(ip(&"2a02::223:6cff :fe8a:2e8a".to_string()).is_err());
}
}

View File

@ -1,6 +1,7 @@
mod chars_max_length;
mod chars_min_length;
mod email;
mod ip;
mod max_items;
mod max_length;
mod maximum;
@ -8,10 +9,15 @@ mod min_items;
mod min_length;
mod minimum;
mod multiple_of;
mod regex;
mod url;
pub use self::regex::regex;
pub use self::url::url;
pub use chars_max_length::chars_max_length;
pub use chars_min_length::chars_min_length;
pub use email::email;
pub use ip::ip;
pub use max_items::max_items;
pub use max_length::max_length;
pub use maximum::maximum;

View File

@ -10,9 +10,22 @@ where
T: AsPrimitive<N> + InputType,
N: Rem<Output = N> + Zero + Display + Copy + PartialEq + 'static,
{
if value.as_() % n == N::zero() {
let value = value.as_();
if !value.is_zero() && value % n == N::zero() {
Ok(())
} else {
Err(format!("the value must be a multiple of {}.", n).into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiple_of() {
assert!(multiple_of(&5, 3).is_err());
assert!(multiple_of(&6, 3).is_ok());
assert!(multiple_of(&0, 3).is_err());
}
}

25
src/validators/regex.rs Normal file
View File

@ -0,0 +1,25 @@
use regex::Regex;
use crate::{InputType, InputValueError};
pub fn regex<T: AsRef<str> + InputType>(
value: &T,
regex: &'static str,
) -> Result<(), InputValueError<T>> {
if let Ok(true) = Regex::new(regex).map(|re| re.is_match(value.as_ref())) {
Ok(())
} else {
Err("value is valid".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_url() {
assert!(regex(&"123".to_string(), "^[0-9]+$").is_ok());
assert!(regex(&"12a3".to_string(), "^[0-9]+$").is_err());
}
}

26
src/validators/url.rs Normal file
View File

@ -0,0 +1,26 @@
use std::str::FromStr;
use crate::{InputType, InputValueError};
pub fn url<T: AsRef<str> + InputType>(value: &T) -> Result<(), InputValueError<T>> {
if let Ok(true) = http::uri::Uri::from_str(value.as_ref())
.map(|uri| uri.scheme().is_some() && uri.authority().is_some())
{
Ok(())
} else {
Err("invalid url".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_url() {
assert!(url(&"http".to_string()).is_err());
assert!(url(&"https://google.com".to_string()).is_ok());
assert!(url(&"http://localhost:80".to_string()).is_ok());
assert!(url(&"ftp://localhost:80".to_string()).is_ok());
}
}

View File

@ -54,6 +54,18 @@ pub async fn test_all_validator() {
todo!()
}
async fn url(&self, #[graphql(validator(url))] n: String) -> i32 {
todo!()
}
async fn ip(&self, #[graphql(validator(ip))] n: String) -> i32 {
todo!()
}
async fn regex(&self, #[graphql(validator(regex = "^[0-9]+$"))] n: String) -> i32 {
todo!()
}
async fn list_email(&self, #[graphql(validator(list, email))] n: Vec<String>) -> i32 {
todo!()
}
@ -310,7 +322,7 @@ pub async fn test_custom_validator() {
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let schema = Schema::new(Query, EmptyMutation, Subscription);
assert_eq!(
schema
.execute("{ value(n: 100) }")
@ -367,6 +379,38 @@ pub async fn test_custom_validator() {
extensions: None
}]
);
assert_eq!(
schema
.execute_stream("subscription { value(n: 100 ) }")
.next()
.await
.unwrap()
.into_result()
.unwrap()
.data,
value!({ "value": 100 })
);
assert_eq!(
schema
.execute_stream("subscription { value(n: 11 ) }")
.next()
.await
.unwrap()
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "Int": expect 100, actual 11"#.to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 25
}],
path: vec![PathSegment::Field("value".to_string())],
extensions: None
}]
);
}
#[tokio::test]