Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@ fastlane/test_output
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/
./swiftpm
.swiftpm/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ struct RegistrationView: View {
| `RegexValidationRule` | Pattern matching validation | `RegexValidationRule(pattern: "^\\d{3}-\\d{4}$", error: "Invalid phone format")` |
| `URLValidationRule` | Validates URL format | `URLValidationRule(error: "Please enter a valid URL")` |
| `CreditCardValidationRule` | Validates credit card numbers (Luhn algorithm) | `CreditCardValidationRule(error: "Invalid card number")` |
| `EmailValidationRule` | Validates email format | `EmailValidationRule(error: "Please enter a valid email")` |

## Custom Validators

Expand Down
38 changes: 38 additions & 0 deletions Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// Validator
// Copyright © 2023 Space Code. All rights reserved.
//

import Foundation

/// An email validation rule.
public struct EmailValidationRule: IValidationRule {
// MARK: Types

public typealias Input = String

// MARK: Properties

/// The validation error.
public let error: IValidationError

// MARK: Initialization

public init(error: IValidationError) {
self.error = error
}

// MARK: IValidationRule

public func validate(input: String) -> Bool {
let range = NSRange(location: .zero, length: input.count)
if let regex = try? NSRegularExpression(
// swiftlint:disable:next line_length
pattern: "^(?!\\.)(?!.*\\.\\.)[A-Z0-9a-z._%+-]+(?<!\\.)@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\\.[A-Za-z]{2,64}$",
options: [.caseInsensitive]
) {
return regex.firstMatch(in: input, range: range) != nil
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
//
// Validator
// Copyright © 2025 Space Code. All rights reserved.
//

@testable import ValidatorCore
import XCTest

final class EmailValidationRuleTests: XCTestCase {
var sut: EmailValidationRule!

override func setUp() {
super.setUp()
sut = EmailValidationRule(error: "Invalid email")
}

override func tearDown() {
sut = nil
super.tearDown()
}

// MARK: - Valid Email Tests

func testValidateWithValidEmail_ReturnsTrue() {
// Given
let email = "user@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

func testValidateWithValidEmailWithNumbers_ReturnsTrue() {
// Given
let email = "user123@example456.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

func testValidateWithValidEmailWithDots_ReturnsTrue() {
// Given
let email = "first.last@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

func testValidateWithValidEmailWithPlus_ReturnsTrue() {
// Given
let email = "user+tag@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

func testValidateWithValidEmailWithUnderscore_ReturnsTrue() {
// Given
let email = "user_name@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

func testValidateWithValidEmailWithHyphen_ReturnsTrue() {
// Given
let email = "user@my-domain.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

func testValidateWithValidEmailWithSubdomain_ReturnsTrue() {
// Given
let email = "user@mail.example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

func testValidateWithValidEmailWithLongTLD_ReturnsTrue() {
// Given
let email = "user@example.museum"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

func testValidateWithValidEmailWithTwoCharacterTLD_ReturnsTrue() {
// Given
let email = "user@example.io"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

// MARK: - Invalid Email Tests

func testValidateWithInvalidEmailMissingAtSign_ReturnsFalse() {
// Given
let email = "invalid.email.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailMissingDomain_ReturnsFalse() {
// Given
let email = "user@"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailMissingLocalPart_ReturnsFalse() {
// Given
let email = "@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailMissingTLD_ReturnsFalse() {
// Given
let email = "user@example"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailMultipleAtSigns_ReturnsFalse() {
// Given
let email = "user@@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailSpaces_ReturnsFalse() {
// Given
let email = "user @example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailSpecialCharacters_ReturnsFalse() {
// Given
let email = "user#name@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailDotAtStart_ReturnsFalse() {
// Given
let email = ".user@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailDotAtEnd_ReturnsFalse() {
// Given
let email = "user.@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailConsecutiveDots_ReturnsFalse() {
// Given
let email = "user..name@example.com"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailTLDTooLong_ReturnsFalse() {
// Given
let email = "user@example.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithInvalidEmailSingleCharacterTLD_ReturnsFalse() {
// Given
let email = "user@example.c"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

// MARK: - Edge Cases

func testValidateWithEmptyString_ReturnsFalse() {
// Given
let email = ""

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithOnlyAtSign_ReturnsFalse() {
// Given
let email = "@"

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithOnlyDot_ReturnsFalse() {
// Given
let email = "."

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithWhitespaceOnly_ReturnsFalse() {
// Given
let email = " "

// When
let result = sut.validate(input: email)

// Then
XCTAssertFalse(result)
}

func testValidateWithValidEmailWithMaxTLDLength_ReturnsTrue() {
// Given
let email = "user@example.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij"

// When
let result = sut.validate(input: email)

// Then
XCTAssertTrue(result)
}

// MARK: - Error Property Tests

func testErrorPropertyIsSetCorrectly() {
// Given
let expectedError = "Invalid email"

// When
let actualError = sut.error as? String

// Then
XCTAssertEqual(actualError, expectedError)
}
}