Add `url`, `regex` and `ip` validators.
This commit is contained in:
parent
f40c4ca3e6
commit
502bfb8670
|
@ -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>`.
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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()))?;
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue