From 9df7ee5ace822d290912eac41af65068fc995c14 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 23 Nov 2025 20:43:49 +0400 Subject: [PATCH 1/4] feat: add email validation rule --- .../Classes/Rules/EmailValidationRule.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift diff --git a/Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift new file mode 100644 index 0000000..ff264fd --- /dev/null +++ b/Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift @@ -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._%+-]+(? Date: Sun, 23 Nov 2025 20:44:05 +0400 Subject: [PATCH 2/4] test: add unit tests for the email validation rule --- .../Rules/EmailValidationRuleTests.swift | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 Tests/ValidatorCoreTests/UnitTests/Rules/EmailValidationRuleTests.swift diff --git a/Tests/ValidatorCoreTests/UnitTests/Rules/EmailValidationRuleTests.swift b/Tests/ValidatorCoreTests/UnitTests/Rules/EmailValidationRuleTests.swift new file mode 100644 index 0000000..0c3ec3f --- /dev/null +++ b/Tests/ValidatorCoreTests/UnitTests/Rules/EmailValidationRuleTests.swift @@ -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) + } +} From 729f17e4725cb498345ba6bd3be65d92a6f9ad36 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 23 Nov 2025 20:44:48 +0400 Subject: [PATCH 3/4] chore: update `.gitignore` --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5da418c..0649719 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,4 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ -./swiftpm \ No newline at end of file +.swiftpm/ \ No newline at end of file From fcb07eb7ce36706f1ee9b8e29a6953fdc17de506 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 23 Nov 2025 20:48:46 +0400 Subject: [PATCH 4/4] docs: add email validation rule to docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9207ee1..5e86ed1 100644 --- a/README.md +++ b/README.md @@ -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