From 5fd0561a3aef8faa5c92a0d53c5554cc309972ad Mon Sep 17 00:00:00 2001 From: smihica Date: Thu, 14 Oct 2021 00:42:10 +0900 Subject: [PATCH] Supported CharsMinLength and CharsMaxLength to count chars --- src/validators/mod.rs | 4 +- src/validators/string_validators.rs | 48 ++++++ tests/input_validators.rs | 225 +++++++++++++++++++++++++++- 3 files changed, 274 insertions(+), 3 deletions(-) diff --git a/src/validators/mod.rs b/src/validators/mod.rs index c9ad8bb4..5e2062c3 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -8,7 +8,9 @@ use crate::{Error, Value}; pub use int_validators::{IntEqual, IntGreaterThan, IntLessThan, IntNonZero, IntRange}; pub use list_validators::{List, ListMaxLength, ListMinLength}; -pub use string_validators::{Email, StringMaxLength, StringMinLength, MAC}; +pub use string_validators::{ + CharsMaxLength, CharsMinLength, Email, StringMaxLength, StringMinLength, MAC, +}; /// Input value validator /// diff --git a/src/validators/string_validators.rs b/src/validators/string_validators.rs index c2f83f32..a6982a94 100644 --- a/src/validators/string_validators.rs +++ b/src/validators/string_validators.rs @@ -52,6 +52,54 @@ impl InputValueValidator for StringMaxLength { } } +/// Chars minimum length validator. This supports multibyte character. +pub struct CharsMinLength { + /// Must be greater than or equal to this value. + pub length: i32, +} + +impl InputValueValidator for CharsMinLength { + fn is_valid(&self, value: &Value) -> Result<(), String> { + if let Value::String(s) = value { + if s.chars().count() < self.length as usize { + Err(format!( + "the value chars count is {}, must be greater than or equal to {}", + s.chars().count(), + self.length + )) + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} + +/// Chars maximum length validator. This supports multibyte characters. +pub struct CharsMaxLength { + /// Must be less than or equal to this value. + pub length: i32, +} + +impl InputValueValidator for CharsMaxLength { + fn is_valid(&self, value: &Value) -> Result<(), String> { + if let Value::String(s) = value { + if s.chars().count() > self.length as usize { + Err(format!( + "the value chars count is {}, must be less than or equal to {}", + s.chars().count(), + self.length + )) + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} + static EMAIL_RE: Lazy = Lazy::new(|| { Regex::new("^(([0-9A-Za-z!#$%&'*+-/=?^_`{|}~&&[^@]]+)|(\"([0-9A-Za-z!#$%&'*+-/=?^_`{|}~ \"(),:;<>@\\[\\\\\\]]+)\"))@").unwrap() }); diff --git a/tests/input_validators.rs b/tests/input_validators.rs index 39d4060a..4807903e 100644 --- a/tests/input_validators.rs +++ b/tests/input_validators.rs @@ -1,6 +1,7 @@ use async_graphql::validators::{ - Email, InputValueValidator, IntEqual, IntGreaterThan, IntLessThan, IntNonZero, IntRange, - ListMaxLength, ListMinLength, StringMaxLength, StringMinLength, MAC, + CharsMaxLength, CharsMinLength, Email, InputValueValidator, IntEqual, IntGreaterThan, + IntLessThan, IntNonZero, IntRange, ListMaxLength, ListMinLength, StringMaxLength, + StringMinLength, MAC, }; use async_graphql::*; @@ -230,6 +231,226 @@ pub async fn test_input_validator_string_max_length() { } } +#[tokio::test] +pub async fn test_input_validator_chars_min_length() { + struct QueryRoot; + + #[derive(InputObject)] + struct InputMaxLength { + #[graphql(validator(CharsMinLength(length = "6")))] + pub id: String, + } + + #[Object] + impl QueryRoot { + async fn field_parameter( + &self, + #[graphql(validator(CharsMinLength(length = "6")))] _id: String, + ) -> bool { + true + } + + async fn input_object(&self, _input: InputMaxLength) -> bool { + true + } + } + + let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); + let test_cases = ["一2三4五", "壹2叁4伍6", "一2三4五6七", "壹2叁4伍6Ⅶ8"]; + + let validator_length = 6; + for case in &test_cases { + let field_query = format!("{{fieldParameter(id: \"{}\")}}", case); + let object_query = format!("{{inputObject(input: {{id: \"{}\"}})}}", case); + let case_length = case.chars().count(); + + if case_length < validator_length { + let should_fail_msg = format!( + "CharsMinLength case {} should have failed, but did not", + case + ); + + let field_error_msg = format!( + "Invalid value for argument \"id\", the value chars count is {}, must be greater than or equal to {}", + case_length, validator_length + ); + let object_error_msg = format!( + "Invalid value for argument \"input.id\", the value chars count is {}, must be greater than or equal to {}", + case_length, validator_length + ); + assert_eq!( + schema + .execute(&field_query) + .await + .into_result() + .expect_err(&should_fail_msg), + vec![ServerError { + message: field_error_msg, + locations: vec!(Pos { + line: 1, + column: 17 + }), + path: Vec::new(), + extensions: None, + }] + ); + + assert_eq!( + schema + .execute(&object_query) + .await + .into_result() + .expect_err(&should_fail_msg[..]), + vec![ServerError { + message: object_error_msg, + locations: vec!(Pos { + line: 1, + column: 14 + }), + path: Vec::new(), + extensions: None, + }] + ); + } else { + let error_msg = format!("Schema returned error with test_string = {}", case); + assert_eq!( + schema + .execute(&field_query) + .await + .into_result() + .expect(&error_msg[..]) + .data, + value!({"fieldParameter": true}), + "Failed to validate {} with CharsMinLength", + case + ); + + assert_eq!( + schema + .execute(&object_query) + .await + .into_result() + .expect(&error_msg[..]) + .data, + value!({"inputObject": true}), + "Failed to validate {} with CharsMinLength", + case + ); + } + } +} + +#[tokio::test] +pub async fn test_input_validator_chars_max_length() { + struct QueryRoot; + + #[derive(InputObject)] + struct InputMaxLength { + #[graphql(validator(CharsMaxLength(length = "6")))] + pub id: String, + } + + #[Object] + impl QueryRoot { + async fn field_parameter( + &self, + #[graphql(validator(CharsMaxLength(length = "6")))] _id: String, + ) -> bool { + true + } + + async fn input_object(&self, _input: InputMaxLength) -> bool { + true + } + } + + let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); + let test_cases = ["一2三4五", "壹2叁4伍6", "一2三4五6七", "壹2叁4伍6Ⅶ8"]; + + let validator_length = 6; + for case in &test_cases { + let field_query = format!("{{fieldParameter(id: \"{}\")}}", case); + let object_query = format!("{{inputObject(input: {{id: \"{}\"}})}}", case); + let case_length = case.chars().count(); + + if case_length > validator_length { + let should_fail_msg = format!( + "CharsMaxLength case {} should have failed, but did not", + case + ); + + let field_error_msg = format!( + "Invalid value for argument \"id\", the value chars count is {}, must be less than or equal to {}", + case_length, + validator_length + ); + let object_error_msg = format!( + "Invalid value for argument \"input.id\", the value chars count is {}, must be less than or equal to {}", + case_length, + validator_length + ); + assert_eq!( + schema + .execute(&field_query) + .await + .into_result() + .expect_err(&should_fail_msg[..]), + vec![ServerError { + message: field_error_msg, + locations: vec!(Pos { + line: 1, + column: 17 + }), + path: Vec::new(), + extensions: None, + }] + ); + + assert_eq!( + schema + .execute(&object_query) + .await + .into_result() + .expect_err(&should_fail_msg[..]), + vec![ServerError { + message: object_error_msg, + locations: vec!(Pos { + line: 1, + column: 14 + }), + path: Vec::new(), + extensions: None, + }] + ); + } else { + let error_msg = format!("Schema returned error with test_string = {}", case); + assert_eq!( + schema + .execute(&field_query) + .await + .into_result() + .expect(&error_msg[..]) + .data, + value!({"fieldParameter": true}), + "Failed to validate {} with CharsMaxLength", + case + ); + + assert_eq!( + schema + .execute(&object_query) + .await + .into_result() + .expect(&error_msg[..]) + .data, + value!({"inputObject": true}), + "Failed to validate {} with CharsMaxLength", + case + ); + } + } +} + #[tokio::test] pub async fn test_input_validator_string_email() { struct QueryRoot;