Compare commits

...

14 Commits

Author SHA1 Message Date
David Patterson b9b9a65b74 Merge pull request #98 from davepatterson/remote-validation-tests
Remote validation tests
2016-03-05 03:00:31 -06:00
David Patterson 7bc7067515 more minor edits 2016-03-03 23:27:21 -06:00
David Patterson 87a47a3107 minor edits 2016-03-03 23:22:58 -06:00
David Patterson f9fad02c3d added tests for remote-validation methods and a little refactoring 2016-03-03 23:20:52 -06:00
David Patterson 0bd79451d2 Merge pull request #97 from jpotts18/remote-validation
Remote validation
2016-03-02 21:55:54 -06:00
David Patterson 0d2b50c9a6 Added remote validation and updated README accordingly 2016-03-02 21:48:30 -06:00
David Patterson d4aa8e594f Merge pull request #3 from davepatterson/remote-validation
Added Remote validation to validator.
2016-03-01 19:40:44 -06:00
David Patterson d359bf0683 remote validation added to Demo on email field 2016-03-01 19:33:45 -06:00
David Patterson b57e924b26 beta version of remote validation 2016-03-01 12:47:58 -06:00
David Patterson 7c6bf284c4 updated podspec to 3.0.4 2016-03-01 09:58:14 -06:00
David Patterson 3f5fb305c3 starting on validation with completion handlers 2016-02-29 23:17:39 -06:00
David Patterson 3a7d4956b9 Update README.md 2016-02-16 01:40:31 -06:00
David Patterson cb94d71119 Update README.md 2016-02-16 01:39:55 -06:00
David Patterson 3bab916c05 Update README.md 2016-02-15 23:54:57 -06:00
7 changed files with 268 additions and 24 deletions

View File

@ -1,6 +1,5 @@
SwiftValidator SwiftValidator
=============== =======
[![Build Status](https://travis-ci.org/jpotts18/SwiftValidator.svg?branch=travis-ci)](https://travis-ci.org/jpotts18/SwiftValidator) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![codecov.io](https://codecov.io/github/jpotts18/SwiftValidator/coverage.svg?branch=master)](https://codecov.io/github/jpotts18/SwiftValidator?branch=master) [![Build Status](https://travis-ci.org/jpotts18/SwiftValidator.svg?branch=travis-ci)](https://travis-ci.org/jpotts18/SwiftValidator) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![codecov.io](https://codecov.io/github/jpotts18/SwiftValidator/coverage.svg?branch=master)](https://codecov.io/github/jpotts18/SwiftValidator?branch=master)
Swift Validator is a rule-based validation library for Swift. Swift Validator is a rule-based validation library for Swift.
@ -146,6 +145,33 @@ class SSNVRule: RegexRule {
} }
``` ```
## Remote Validation
Register field to `validator` with `remoteInfo` parameter set
`validator.registerField(emailTextField, errorLabel: emailErrorLabel, rules: [RequiredRule(), EmailRule()], remoteInfo: (urlString: "http://localhost:8000/emails/", error: "Email already in use"))`
Implement `ValidationDelegate`'s `remoteValidationRequest` method
```swift
func remoteValidationRequest(text: String, urlString: String, completion: (result: Bool) -> Void) {
// Add email to urlString
let newUrlString = "\(urlString)?email=\(text)"
YourNetworkingLibrary.request(.GET, newUrlString) { data -> Void in
if data.httpResponse.statusCode == 404 {
// resource was not found, therefore the text (username, email, etc) is available
completion(result: true)
}
if data.httpResponse.statusCode == 200 {
// resource was found, therefore the text (username, email, etc) is unavailable
completion(result: false)
}
}
// end of remoteValidationRequest method
}
```
Credits Credits
------- -------

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = "SwiftValidator" s.name = "SwiftValidator"
s.version = "3.0.1" s.version = "3.0.4"
s.summary = "A UITextField Validation library for Swift" s.summary = "A UITextField Validation library for Swift"
s.homepage = "https://github.com/jpotts18/SwiftValidator" s.homepage = "https://github.com/jpotts18/SwiftValidator"
s.screenshots = "https://raw.githubusercontent.com/jpotts18/SwiftValidator/master/swift-validator-v2.gif" s.screenshots = "https://raw.githubusercontent.com/jpotts18/SwiftValidator/master/swift-validator-v2.gif"
@ -9,7 +9,7 @@ Pod::Spec.new do |s|
s.social_media_url = "http://twitter.com/jpotts18" s.social_media_url = "http://twitter.com/jpotts18"
s.platform = :ios s.platform = :ios
s.ios.deployment_target = '8.0' s.ios.deployment_target = '8.0'
s.source = { :git => "https://github.com/jpotts18/SwiftValidator.git", :tag => "3.0.1" } s.source = { :git => "https://github.com/jpotts18/SwiftValidator.git", :tag => "3.0.4" }
s.source_files = "SwiftValidator/**/*.swift" s.source_files = "SwiftValidator/**/*.swift"
s.exclude_files = "Validator/AppDelegate.swift" s.exclude_files = "Validator/AppDelegate.swift"
s.frameworks = ['Foundation', 'UIKit'] s.frameworks = ['Foundation', 'UIKit']

View File

@ -13,13 +13,21 @@ import UIKit
*/ */
@objc public protocol ValidationDelegate { @objc public protocol ValidationDelegate {
/** /**
This method will be called on delegate object when validation is successful. This delegate method will be called on delegate object when validation is successful.
- returns: No return value. - returns: No return value.
*/ */
func validationSuccessful() func validationSuccessful()
/** /**
This method will be called on delegate object when validation fails. This delegae method will be called on delegate object when validation fails.
- returns: No return value. - returns: No return value.
*/ */
func validationFailed(errors: [UITextField:ValidationError]) func validationFailed(errors: [UITextField:ValidationError])
/**
This delegate method is called on fields that require remote validation.
- parameter text: String to is sent to server to be validated.
- parameter urlString: String of url endpoint that will be used to validate text.
- parameter completion: closure that holds the result of the server validation request. Should be set to true if server validation was a success. Should return false if server validation failed.
- returns: No return value.
*/
optional func remoteValidationRequest(text: String, urlString: String, completion:(result: Bool) -> Void)
} }

View File

@ -11,17 +11,19 @@ import UIKit
/** /**
Class that makes `Validator` objects. Should be added as a parameter to ViewController that will display Class that makes `Validator` objects. Should be added as a parameter to ViewController that will display
validation fields. validation fields.
*/ */
public class Validator { public class Validator {
/// Dictionary to hold all fields (and accompanying rules) that will undergo validation. /// Dictionary to hold all fields (and accompanying rules) that will undergo validation.
public var validations = [UITextField:ValidationRule]() public var validations = [UITextField:ValidationRule]()
/// Dictionary to hold fields (and accompanying errors) that were unsuccessfully validated. /// Dictionary to hold fields (and accompanying errors) that were unsuccessfully validated.
public var errors = [UITextField:ValidationError]() public var errors = [UITextField:ValidationError]()
/// Variable that holds success closure to display positive status of field. /// Variable that holds success closure to display positive status of field.
public var delegate: ValidationDelegate?
private var successStyleTransform:((validationRule:ValidationRule)->Void)? private var successStyleTransform:((validationRule:ValidationRule)->Void)?
/// Variable that holds error closure to display negative status of field. /// Variable that holds error closure to display negative status of field.
private var errorStyleTransform:((validationError:ValidationError)->Void)? private var errorStyleTransform:((validationError:ValidationError)->Void)?
/// - returns: An initialized object, or nil if an object could not be created for some reason that would not result in an exception. /// - returns: An initialized object, or nil if an object could not be created for some reason that would not result in an exception.
private var completedValidationsCount: Int = 0
public init(){} public init(){}
// MARK: Private functions // MARK: Private functions
@ -53,6 +55,94 @@ public class Validator {
} }
} }
/**
This method is used to validate all fields registered to Validator. If validation is unsuccessful,
field gets added to errors dictionary. Completion closure is used to validator know when all fields
have undergone validation attempt.
- parameter completion: Bool that is set to true when all fields have experienced validation attempt.
- returns: No return value.
*/
private func validateAllFields(completion: (finished: Bool) -> Void) {
errors = [:]
for (textField, rule) in validations {
if rule.remoteInfo != nil {
validateRemoteField(textField, callback: { error -> Void in
self.completedValidationsCount = self.completedValidationsCount + 1
if self.completedValidationsCount == self.validations.count {
// Sends validation back to validate()
completion(finished: true)
}
})
} else {
validateRegularField(textField)
self.completedValidationsCount = self.completedValidationsCount + 1
if completedValidationsCount == validations.count {
// Sends validation back to validate()
completion(finished: true)
}
}
}
}
/**
This method is used to validate a field that will need to also be validated via remote request.
- parameter textField: TextField of field that is being validated.
- parameter completion: Closure that holds the status of textField's validation. Is set to true
after remote validation has ended, regardless of whether the validation was a success or failure.
- returns: No return value.
*/
public func validateRemoteField(textField: UITextField, callback: (error: ValidationError?) -> Void) {
if let fieldRule = validations[textField] {
// Carry on with validation only if regular validation passed
if self.validateRegularField(fieldRule.textField) {
delegate!.remoteValidationRequest?(textField.text!, urlString: fieldRule.remoteInfo!.urlString, completion: { result -> Void in
if result {
if let transform = self.successStyleTransform {
transform(validationRule: fieldRule)
}
callback(error: nil)
} else {
// Stop validation because remote validation failed
// Validation Failed on remote call
let error = ValidationError(textField: fieldRule.textField, errorLabel: fieldRule.errorLabel, error: fieldRule.remoteInfo!.error)
self.errors[fieldRule.textField] = error
if let transform = self.errorStyleTransform {
transform(validationError: error)
}
callback(error: error)
}
})
}
}
}
/**
Method used to validate a regular field (non-remote).
- parameter: TextField of field that is undergoing validation
- returns: A Bool that represents whether the validation was a success or failure, returns true for the
former and false for the latter.
*/
private func validateRegularField(textField: UITextField) -> Bool {
if let fieldRule = validations[textField] {
if let error = fieldRule.validateField() {
errors[textField] = error
if let transform = self.errorStyleTransform {
transform(validationError: error)
}
return false
} else {
if let transform = self.successStyleTransform {
if fieldRule.remoteInfo == nil {
transform(validationRule: fieldRule)
}
}
return true
}
}
return false
}
// MARK: Public functions // MARK: Public functions
/** /**
@ -100,9 +190,9 @@ public class Validator {
- parameter textField: field that is to be validated. - parameter textField: field that is to be validated.
- parameter Rule: An array which holds different rules to validate against textField. - parameter Rule: An array which holds different rules to validate against textField.
- returns: No return value - returns: No return value
*/ */
public func registerField(textField:UITextField, rules:[Rule]) { public func registerField(textField:UITextField, rules:[Rule], remoteInfo: (String, String)? = nil) {
validations[textField] = ValidationRule(textField: textField, rules: rules, errorLabel: nil) validations[textField] = ValidationRule(textField: textField, rules: rules, errorLabel: nil, remoteInfo: remoteInfo)
} }
/** /**
@ -112,9 +202,9 @@ public class Validator {
- parameter errorLabel: A UILabel that holds error label data - parameter errorLabel: A UILabel that holds error label data
- parameter rules: A Rule array that holds different rules that apply to said textField. - parameter rules: A Rule array that holds different rules that apply to said textField.
- returns: No return value - returns: No return value
*/ */
public func registerField(textField:UITextField, errorLabel:UILabel, rules:[Rule]) { public func registerField(textField:UITextField, errorLabel:UILabel, rules:[Rule], remoteInfo: (String, String)? = nil) {
validations[textField] = ValidationRule(textField: textField, rules:rules, errorLabel:errorLabel) validations[textField] = ValidationRule(textField: textField, rules:rules, errorLabel:errorLabel, remoteInfo: remoteInfo)
} }
/** /**
@ -122,7 +212,7 @@ public class Validator {
- parameter textField: field used to locate and remove textField from validator. - parameter textField: field used to locate and remove textField from validator.
- returns: No return value - returns: No return value
*/ */
public func unregisterField(textField:UITextField) { public func unregisterField(textField:UITextField) {
validations.removeValueForKey(textField) validations.removeValueForKey(textField)
errors.removeValueForKey(textField) errors.removeValueForKey(textField)
@ -132,7 +222,7 @@ public class Validator {
This method checks to see if all fields in validator are valid. This method checks to see if all fields in validator are valid.
- returns: No return value. - returns: No return value.
*/ */
public func validate(delegate:ValidationDelegate) { public func validate(delegate:ValidationDelegate) {
self.validateAllFields() self.validateAllFields()
@ -146,12 +236,30 @@ public class Validator {
} }
/** /**
This method validates all fields in validator and sets any errors to errors parameter of callback. This method attempts to validate all fields registered to Validator().
- returns: No return value.
*/
public func validate() {
self.validateAllFields { finished -> Void in
if self.errors.isEmpty {
// call success method if it's implemented
self.delegate!.validationSuccessful()
} else {
// call failure method if it's implemented
self.delegate!.validationFailed(self.errors)
}
// set number of completed validations back to 0
self.completedValidationsCount = 0
}
}
/**
This method validates all fields in validator and sets any errors to errors parameter of closure.
- parameter callback: A closure which is called with errors, a dictionary of type UITextField:ValidationError. - parameter callback: A closure which is called with errors, a dictionary of type UITextField:ValidationError.
- returns: No return value. - returns: No return value.
*/ */
public func validate(callback:(errors:[UITextField:ValidationError])->Void) -> Void { public func validate(callback:(errors:[UITextField:ValidationError])->Void) -> Void {
self.validateAllFields() self.validateAllFields()

View File

@ -18,6 +18,9 @@ public class ValidationRule {
public var errorLabel:UILabel? public var errorLabel:UILabel?
/// the rules of the field /// the rules of the field
public var rules:[Rule] = [] public var rules:[Rule] = []
/// tuple that holds remote validatin info
public var remoteInfo: (urlString: String, error: String)?
//public var remoteURLString: String?
/** /**
Initializes `ValidationRule` instance with text field, rules, and errorLabel. Initializes `ValidationRule` instance with text field, rules, and errorLabel.
@ -27,10 +30,12 @@ public class ValidationRule {
- parameter rules: array of Rule objects, which text field will be validated against. - parameter rules: array of Rule objects, which text field will be validated against.
- returns: An initialized `ValidationRule` object, or nil if an object could not be created for some reason that would not result in an exception. - returns: An initialized `ValidationRule` object, or nil if an object could not be created for some reason that would not result in an exception.
*/ */
public init(textField: UITextField, rules:[Rule], errorLabel:UILabel?){
public init(textField: UITextField, rules:[Rule], errorLabel:UILabel? = nil, remoteInfo: (String, String)? = nil){
self.textField = textField self.textField = textField
self.errorLabel = errorLabel self.errorLabel = errorLabel
self.rules = rules self.rules = rules
self.remoteInfo = remoteInfo
} }
/** /**

View File

@ -48,6 +48,40 @@ class SwiftValidatorTests: XCTestCase {
let ERROR_LABEL = UILabel() let ERROR_LABEL = UILabel()
let URL_STRING = "http://localhost:8000/emails/"
let EMAIL_TAKEN_MESSAGE = "Email already taken"
class FailureRemoteValidationViewController: UIViewController, ValidationDelegate {
func validationSuccessful() {
}
func validationFailed(errors: [UITextField : ValidationError]) {
}
func remoteValidationRequest(text: String, urlString: String, completion: (result: Bool) -> Void) {
completion(result: false)
}
}
class SuccessRemoteValidationViewController: UIViewController, ValidationDelegate {
func validationSuccessful() {
}
func validationFailed(errors: [UITextField : ValidationError]) {
}
func remoteValidationRequest(text: String, urlString: String, completion: (result: Bool) -> Void) {
completion(result: true)
}
}
let REGISTER_FAILURE_REMOTE_VALIDATION_DELEGATE = FailureRemoteValidationViewController()
let REGISTER_SUCCESS_REMOTE_VALIDATION_DELEGATE = SuccessRemoteValidationViewController()
override func setUp() { override func setUp() {
super.setUp() super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class. // Put setup code here. This method is called before the invocation of each test method in the class.
@ -370,6 +404,44 @@ class SwiftValidatorTests: XCTestCase {
} }
} }
/// Used to test validation success on a single field that has remote validation
func testValidateSuccessSingleRemoteField() {
REGISTER_TXT_FIELD.text = VALID_EMAIL
REGISTER_VALIDATOR.registerField(REGISTER_TXT_FIELD, errorLabel: ERROR_LABEL, rules: [EmailRule()], remoteInfo: (urlString: URL_STRING, error: EMAIL_TAKEN_MESSAGE))
REGISTER_VALIDATOR.delegate = REGISTER_SUCCESS_REMOTE_VALIDATION_DELEGATE
REGISTER_VALIDATOR.validateRemoteField(REGISTER_TXT_FIELD) { error -> Void in
XCTAssertNil(error)
}
}
/// Used to test validation failure on a single field that has remote validation
func testValidateFailureSingleRemoteField() {
REGISTER_TXT_FIELD.text = VALID_EMAIL
REGISTER_VALIDATOR.registerField(REGISTER_TXT_FIELD, errorLabel: ERROR_LABEL, rules: [EmailRule()], remoteInfo: (urlString: URL_STRING, error: EMAIL_TAKEN_MESSAGE))
REGISTER_VALIDATOR.delegate = REGISTER_FAILURE_REMOTE_VALIDATION_DELEGATE
REGISTER_VALIDATOR.validateRemoteField(REGISTER_TXT_FIELD) { error -> Void in
XCTAssertNotNil(error)
}
}
/// Used to test remote validation success
func testValidationSuccessOnRemoteSuccess() {
REGISTER_TXT_FIELD.text = VALID_EMAIL
REGISTER_VALIDATOR.registerField(REGISTER_TXT_FIELD, errorLabel: ERROR_LABEL, rules: [EmailRule()], remoteInfo: (urlString: URL_STRING, error: EMAIL_TAKEN_MESSAGE))
REGISTER_VALIDATOR.delegate = REGISTER_SUCCESS_REMOTE_VALIDATION_DELEGATE
REGISTER_VALIDATOR.validate()
XCTAssert(REGISTER_VALIDATOR.errors.count == 0)
}
/// Used to test remote validation failure
func testValidationFailOnRemoteFailure() {
REGISTER_TXT_FIELD.text = VALID_EMAIL
REGISTER_VALIDATOR.registerField(REGISTER_TXT_FIELD, errorLabel: ERROR_LABEL, rules: [EmailRule()], remoteInfo: (urlString: URL_STRING, error: EMAIL_TAKEN_MESSAGE))
REGISTER_VALIDATOR.delegate = REGISTER_FAILURE_REMOTE_VALIDATION_DELEGATE
REGISTER_VALIDATOR.validate()
XCTAssert(REGISTER_VALIDATOR.errors.count == 1, "There is at least one error")
}
// MARK: Validate error field gets it's text set to the error, if supplied // MARK: Validate error field gets it's text set to the error, if supplied
func testNoErrorMessageSet() { func testNoErrorMessageSet() {

View File

@ -50,33 +50,58 @@ class ViewController: UIViewController , ValidationDelegate, UITextFieldDelegate
}) })
validator.registerField(fullNameTextField, errorLabel: fullNameErrorLabel , rules: [RequiredRule(), FullNameRule()]) validator.registerField(fullNameTextField, errorLabel: fullNameErrorLabel , rules: [RequiredRule(), FullNameRule()])
validator.registerField(emailTextField, errorLabel: emailErrorLabel, rules: [RequiredRule(), EmailRule()]) validator.registerField(emailTextField, errorLabel: emailErrorLabel, rules: [RequiredRule(), EmailRule()], remoteInfo: (urlString: "http://localhost:8000/emails/", error: "Email already in use"))
validator.registerField(emailConfirmTextField, errorLabel: emailConfirmErrorLabel, rules: [RequiredRule(), ConfirmationRule(confirmField: emailTextField)]) validator.registerField(emailConfirmTextField, errorLabel: emailConfirmErrorLabel, rules: [RequiredRule(), ConfirmationRule(confirmField: emailTextField)])
validator.registerField(phoneNumberTextField, errorLabel: phoneNumberErrorLabel, rules: [RequiredRule(), MinLengthRule(length: 9)]) validator.registerField(phoneNumberTextField, errorLabel: phoneNumberErrorLabel, rules: [RequiredRule(), MinLengthRule(length: 9)])
validator.registerField(zipcodeTextField, errorLabel: zipcodeErrorLabel, rules: [RequiredRule(), ZipCodeRule()]) validator.registerField(zipcodeTextField, errorLabel: zipcodeErrorLabel, rules: [RequiredRule(), ZipCodeRule()])
validator.delegate = self
} }
@IBAction func submitTapped(sender: AnyObject) { @IBAction func submitTapped(sender: AnyObject) {
print("Validating...") print("Validating...")
validator.validate(self) validator.validate()
}
func simulateRemoteRequest(seconds: Int64, completion: (result: Bool) -> Void) {
print("Simulating \(seconds) second server request...")
// Set number of seconds before "request" is finished
let triggerTime = (Int64(NSEC_PER_SEC) * seconds)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, triggerTime), dispatch_get_main_queue(), { () -> Void in
// For example purposes, if user does not enter David Patterson as full name,
// the email they tried will be already taken
if self.fullNameTextField.text! == "David Patterson" {
completion(result: true)
} else {
completion(result: false)
}
})
}
func hideKeyboard(){
self.view.endEditing(true)
} }
// MARK: ValidationDelegate Methods // MARK: ValidationDelegate Methods
func validationSuccessful() { func validationSuccessful() {
print("Validation Success!") print("Validation Success!")
let alert = UIAlertController(title: "Success", message: "You are validated!", preferredStyle: UIAlertControllerStyle.Alert) let alert = UIAlertController(title: "Success", message: "You are validated, \(fullNameTextField.text!)!", preferredStyle: UIAlertControllerStyle.Alert)
let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil) let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alert.addAction(defaultAction) alert.addAction(defaultAction)
self.presentViewController(alert, animated: true, completion: nil) self.presentViewController(alert, animated: true, completion: nil)
} }
func validationFailed(errors:[UITextField:ValidationError]) { func validationFailed(errors:[UITextField:ValidationError]) {
print("Validation FAILED!") print("Validation FAILED!", validator.errors.count)
} }
func hideKeyboard(){ func remoteValidationRequest(text: String, urlString: String, completion: (result: Bool) -> Void) {
self.view.endEditing(true) simulateRemoteRequest(2) { result -> Void in
// Set result to true if field was validated server-side
// Set to false if field was not validated server-side
completion(result: result)
}
} }
// MARK: Validate single field // MARK: Validate single field