Skip to content

Commit

Permalink
Develop (#1)
Browse files Browse the repository at this point in the history
* Add more regexps

* Fix MultiRegex right error return

* Fix validated failure case comparison

* Clean up extended regexps enums

* Add remained tests

* Update README.md

* Make regex protocol api more concise

* Complete test cover for regex cases
  • Loading branch information
pridees authored Nov 13, 2021
1 parent 6925a06 commit afa6fad
Show file tree
Hide file tree
Showing 11 changed files with 357 additions and 105 deletions.
11 changes: 8 additions & 3 deletions Sources/CombineValidate/Publishers/Publishers+Validators.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Foundation
import Combine

public func NotEmptyValidator(
Expand All @@ -7,6 +8,7 @@ public func NotEmptyValidator(
) -> ValidationPublisher {
publisher
.dropFirst()
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.map(\.isEmpty)
.map { !$0 ? .success(.none) : .failure(reason: message, tableName: tableName) }
.eraseToAnyPublisher()
Expand All @@ -20,7 +22,8 @@ public func RegexValidator<Pattern>(
) -> ValidationPublisher where Pattern: RegexProtocol {
publisher
.dropFirst()
.map { $0.range(of: pattern.pattern, options: .regularExpression) != nil }
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.map { pattern.test($0) }
.map { $0 ? .success(.none) : .failure(reason: message, tableName: tableName) }
.eraseToAnyPublisher()
}
Expand All @@ -33,9 +36,10 @@ public func OneOfRegexValidator<Pattern>(
) -> RichValidationPublisher<Pattern> where Pattern: RegexProtocol {
publisher
.dropFirst()
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.map { value in
for pattern in patterns {
if value.range(of: pattern.pattern, options: .regularExpression) != nil {
if pattern.test(value) {
return .success(pattern)
}
}
Expand All @@ -52,11 +56,12 @@ public func MultiRegexValidator<Pattern>(
) -> ValidationPublisher where Pattern: RegexProtocol {
publisher
.dropFirst()
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.map { (value: String) -> [Validated<Void>] in
var validationResults: [Validated<Void>] = .init(repeating: .untouched, count: patterns.count)

for (index, pattern) in patterns.enumerated() {
if value.range(of: pattern.pattern, options: .regularExpression) != nil {
if pattern.test(value) {
validationResults[index] = .success(.none)
} else {
validationResults[index] = .failure(reason: messages[index], tableName: tableName)
Expand Down
39 changes: 33 additions & 6 deletions Sources/CombineValidate/Regex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,33 @@ import Combine
public typealias RegexPattern = String

/// Describes enum instances when RawValues is ``RegexPattern``
public protocol RegexProtocol where Self: RawRepresentable, Self.RawValue == RegexPattern {
public protocol RegexProtocol
where
Self: RawRepresentable,
Self.RawValue == RegexPattern
{

var pattern: RegexPattern { get }

/// Test provided string by regex within (it has default implementation)
/// - Parameters:
/// - string: StringProtocol
/// - Returns:
/// Boolean value with True if testing finished successfully
func test<T: StringProtocol>(_ string: T) -> Bool
}

/// Default implementations for protocol API
public extension RegexProtocol {

/// Returns the RawValue
var pattern: RegexPattern { self.rawValue }


func test<T: StringProtocol>(_ string: T) -> Bool {
string.range(of: self.pattern, options: .regularExpression) != nil
}
}

/// Extensions with predefined popular regular expressions
///
/// Conforming to ``RegexPattern`` & ``RegexProtocol``
Expand All @@ -25,19 +44,27 @@ public enum RegularPattern: RegexPattern, RegexProtocol {
/// - 1 special symbol
/// - 1 Uppercase
/// - 1 number
case strongPassword = #"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$"#
case strongPassword = #"^(?=.*?[0-9])(?=.*?[a-z])(?=.*?[A-Z])(?=.*?[^\w\s]).{8,}$"#

case notEmpty = #"[\S\s]+[\S]+"#
case notEmpty = #"([[:graph:]])+"#

/// String must include either 1 or more capital letter.
/// Cannot be empty.
case mustIncludeCapitalLetters = #"(?=.*[A-Z])"#

/// String must include either 1 or more spectial symbol. % ! \ : @ [ { ` ~
/// Cannot be empty.
case mustIncludeSpecialSymbols = #"(?=.*[\%\!-\/\:-\@\[-\`\{-\~])"#

/// String must include either 1 or more digit symbol.
/// Cannot be empty.
case mustIncludeNumbers = #"(?=.*[0-9])"#

/// String must include either 1 or more lowercased letter.
/// Cannot be empty.
case mustIncludeSmallLetters = #"(?=.*[a-z])"#

/// String must be consist from words or digits without whitespaces and new lines
/// Cannot be empty.
case wordAndDigitsOnly = #"^(\d|\w|\S){1,}$"#

case anyInOneLine = #"^.+$"#
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ public enum CreditCardPattern: RegexPattern, RegexProtocol {
case cardExpireDate = #"^(0[1-9]|1[0-2])(\/|-)([0-9]{4})$"#
}


/// Extended number validation patterns
public enum NumberPattern: RegexPattern, RegexProtocol {
/// any numer
case anyNumber = #"^(-)?[0-9]{1,18}$"#

case positiveNumber = #"^\d+$"#
Expand Down
92 changes: 43 additions & 49 deletions Tests/CombineValidateExtendedTests/CreditCardPatternsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ import XCTest
class CreditCardPatternTests: XCTestCase {
func testAmericanExpressPattern() {
let americanExpressCard = cleanCardNumber("3700 0000 0000 002")

XCTAssertNotNil(
americanExpressCard.range(of: CreditCardPattern.Amex.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.Amex.test(americanExpressCard),
"AmericanExpress: \(americanExpressCard). Positive case failed"
)

let unExistedNumber = cleanCardNumber("0000 0000 0000 000")

XCTAssertNil(
unExistedNumber.range(of: CreditCardPattern.Amex.pattern, options: .regularExpression),
XCTAssertFalse(
CreditCardPattern.Amex.test(unExistedNumber),
"AmericanExpress: \(unExistedNumber). Negative case failed"
)
}
Expand All @@ -23,32 +21,32 @@ class CreditCardPatternTests: XCTestCase {

for IIR in mastercardIIRRange {
let cardNumber = cleanCardNumber("\(IIR)00 0000 0000 4321")
XCTAssertNotNil(
cardNumber.range(of: CreditCardPattern.Mastercard.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.Mastercard.test(cardNumber),
"Mastercard: \(cardNumber). Positive case failed"
)
}

let unExistedNumber = cleanCardNumber("5600 0000 0000 0000")

XCTAssertNil(
unExistedNumber.range(of: CreditCardPattern.Mastercard.pattern, options: .regularExpression),
XCTAssertFalse(
CreditCardPattern.Mastercard.test(unExistedNumber),
"Mastercard: \(unExistedNumber). Negative case failed"
)
}

func testVisaPattern() {
let visaCardNumber = cleanCardNumber("4377 0000 0000 0000")

XCTAssertNotNil(
visaCardNumber.range(of: CreditCardPattern.Visa.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.Visa.test(visaCardNumber),
"Mastercard: \(visaCardNumber). Positive case failed"
)

let unExistedNumber = cleanCardNumber("0000 0000 0000 0000")

XCTAssertNil(
unExistedNumber.range(of: CreditCardPattern.Visa.pattern, options: .regularExpression),
XCTAssertFalse(
CreditCardPattern.Visa.test(unExistedNumber),
"Mastercard: \(unExistedNumber). Negative case failed"
)
}
Expand All @@ -58,38 +56,36 @@ class CreditCardPatternTests: XCTestCase {

for IIR in maestroIIRRange {
let cardNumber = cleanCardNumber("\(IIR) 0000 0000 4321")
XCTAssertNotNil(
cardNumber.range(of: CreditCardPattern.Maestro.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.Maestro.test(cardNumber),
"Maestro: \(cardNumber). Positive case failed"
)
}

let unExistedNumber = cleanCardNumber("0000 0000 0000 0000")

XCTAssertNil(
unExistedNumber.range(of: CreditCardPattern.Maestro.pattern, options: .regularExpression),
XCTAssertFalse(
CreditCardPattern.Maestro.test(unExistedNumber),
"Mastercard: \(unExistedNumber). Negative case failed"
)
}

func testDiscoverPattern() {
let discoverUS = cleanCardNumber("6011 0000 0000 0000")
let discoverGB = cleanCardNumber("6445 0000 0000 0000")

XCTAssertNotNil(
discoverUS.range(of: CreditCardPattern.Discover.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.Discover.test(discoverUS),
"Discover US: \(discoverUS). Positive case failed"
)

XCTAssertNotNil(
discoverGB.range(of: CreditCardPattern.Discover.pattern, options: .regularExpression),
let discoverGB = cleanCardNumber("6445 0000 0000 0000")
XCTAssertTrue(
CreditCardPattern.Discover.test(discoverGB),
"Discover GB: \(discoverGB). Positive case failed"
)

let unExistedNumber = cleanCardNumber("0000 0000 0000 0000")

XCTAssertNil(
unExistedNumber.range(of: CreditCardPattern.Discover.pattern, options: .regularExpression),
XCTAssertFalse(
CreditCardPattern.Discover.test(unExistedNumber),
"Discover: \(unExistedNumber). Negative case failed"
)
}
Expand All @@ -98,51 +94,49 @@ class CreditCardPatternTests: XCTestCase {
let jsbUsCard = cleanCardNumber("3569 9900 1009 5841")
let jsbNativeIRRs = ["2131", "1800"]

XCTAssertNotNil(
jsbUsCard.range(of: CreditCardPattern.JSB.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.JSB.test(jsbUsCard),
"JSB GB: \(jsbUsCard). Positive case failed"
)

for IIR in jsbNativeIRRs {
let cardNumber = cleanCardNumber("\(IIR) 0000 0000 000")
XCTAssertNotNil(
cardNumber.range(of: CreditCardPattern.JSB.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.JSB.test(cardNumber),
"JSB GB: \(cardNumber). Positive case failed"
)
}

let unExistedNumber = cleanCardNumber("0000 0000 0000 0000")

XCTAssertNil(
unExistedNumber.range(of: CreditCardPattern.JSB.pattern, options: .regularExpression),
XCTAssertFalse(
CreditCardPattern.JSB.test(unExistedNumber),
"JSB: \(unExistedNumber). Negative case failed"
)
}

func testUnionPayPattern() {
let unionPay = cleanCardNumber("6200 0000 0000 0000")
let unionPayAlt1 = cleanCardNumber("8171 0000 0000 0000")
let unionPayAlt2 = cleanCardNumber("8171 0000 0000 0000 000")

XCTAssertNotNil(
unionPay.range(of: CreditCardPattern.UnionPay.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.UnionPay.test(unionPay),
"UnionPay: \(unionPay). Positive case failed"
)

XCTAssertNotNil(
unionPayAlt1.range(of: CreditCardPattern.UnionPay.pattern, options: .regularExpression),
let unionPayAlt1 = cleanCardNumber("8171 0000 0000 0000")
XCTAssertTrue(
CreditCardPattern.UnionPay.test(unionPayAlt1),
"UnionPay: \(unionPayAlt1). Positive case failed"
)

XCTAssertNotNil(
unionPayAlt2.range(of: CreditCardPattern.UnionPay.pattern, options: .regularExpression),
let unionPayAlt2 = cleanCardNumber("8171 0000 0000 0000 000")
XCTAssertTrue(
CreditCardPattern.UnionPay.test(unionPayAlt2),
"UnionPay: \(unionPayAlt2). Positive case failed"
)

let unExistedNumber = cleanCardNumber("0000 0000 0000 0000")

XCTAssertNil(
unExistedNumber.range(of: CreditCardPattern.UnionPay.pattern, options: .regularExpression),
XCTAssertFalse(
CreditCardPattern.UnionPay.test(unExistedNumber),
"UnionPay: \(unExistedNumber). Negative case failed"
)
}
Expand All @@ -152,16 +146,16 @@ class CreditCardPatternTests: XCTestCase {

for IIR in mirIRRs {
let cardNumber = cleanCardNumber("\(IIR) 0000 0000 0000")
XCTAssertNotNil(
cardNumber.range(of: CreditCardPattern.Mir.pattern, options: .regularExpression),
XCTAssertTrue(
CreditCardPattern.Mir.test(cardNumber),
"Mir: \(cardNumber). Positive case failed"
)
}

let unExistedNumber = cleanCardNumber("0000 0000 0000 0000")

XCTAssertNil(
unExistedNumber.range(of: CreditCardPattern.Mir.pattern, options: .regularExpression),
XCTAssertFalse(
CreditCardPattern.Mir.test(unExistedNumber),
"Mir: \(unExistedNumber). Negative case failed"
)
}
Expand Down
8 changes: 4 additions & 4 deletions Tests/CombineValidateExtendedTests/RegexPatternsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import XCTest
@testable import CombineValidateExtended

final class RegexPatternsTests: XCTestCase {
func testUrlPattern() throws {
func testUrlPattern() {
let url = "https://google.com"
let nonUrl = "Some string"
XCTAssertTrue(URLPatterns.url.test(url), "Simple URL. Positive case failed")

XCTAssertNotNil(url.range(of: URLPatterns.url.pattern, options: .regularExpression), "Simple URL. Positive case failed")
XCTAssertNil(nonUrl.range(of: URLPatterns.url.pattern, options: .regularExpression), "Simple URL. Negative case failed")
let nonUrl = "Some string"
XCTAssertFalse(URLPatterns.url.test(nonUrl), "Simple URL. Negative case failed")
}
}
Loading

0 comments on commit afa6fad

Please sign in to comment.