diff --git a/README.md b/README.md index 5e86ed1..53c480b 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,7 @@ struct RegistrationView: View { | `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")` | +| `CharactersValidationRule` | Validates that a string contains only characters from the allowed CharacterSet | `CharactersValidationRule(characterSet: .letters, error: "Invalid characters")` | ## Custom Validators diff --git a/Sources/ValidatorCore/Classes/Rules/CharactersValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/CharactersValidationRule.swift new file mode 100644 index 0000000..dd3f8f1 --- /dev/null +++ b/Sources/ValidatorCore/Classes/Rules/CharactersValidationRule.swift @@ -0,0 +1,33 @@ +// +// Validator +// Copyright © 2025 Space Code. All rights reserved. +// + +import Foundation + +/// A characters validation rule. +public struct CharactersValidationRule: IValidationRule { + // MARK: Types + + public typealias Input = String + + // MARK: Properties + + public let characterSet: CharacterSet + + /// The validation error. + public let error: IValidationError + + // MARK: Initialization + + public init(characterSet: CharacterSet, error: IValidationError) { + self.characterSet = characterSet + self.error = error + } + + // MARK: IValidationRule + + public func validate(input: String) -> Bool { + input.rangeOfCharacter(from: characterSet.inverted) == .none + } +} diff --git a/Tests/ValidatorCoreTests/UnitTests/Rules/CharactersValidationRuleTests.swift b/Tests/ValidatorCoreTests/UnitTests/Rules/CharactersValidationRuleTests.swift new file mode 100644 index 0000000..8461f21 --- /dev/null +++ b/Tests/ValidatorCoreTests/UnitTests/Rules/CharactersValidationRuleTests.swift @@ -0,0 +1,100 @@ +// +// Validator +// Copyright © 2025 Space Code. All rights reserved. +// + +@testable import ValidatorCore +import XCTest + +// MARK: - CharactersValidationRuleTests + +final class CharactersValidationRuleTests: XCTestCase { + // MARK: - Properties + + private var sut: CharactersValidationRule! + + // MARK: - Setup / Teardown + + override func setUp() { + super.setUp() + sut = CharactersValidationRule(characterSet: .letters, error: String.error) + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - Valid Input Tests + + func testValidate_WithOnlyAllowedCharacters_ReturnsTrue() { + // Given + let input = "HelloWorld" + + // When + let result = sut.validate(input: input) + + // Then + XCTAssertTrue(result) + } + + // MARK: - Invalid Input Tests + + func testValidate_WithDisallowedCharacters_ReturnsFalse() { + // Given + let input = "Hello123" + + // When + let result = sut.validate(input: input) + + // Then + XCTAssertFalse(result) + } + + func testValidate_WithSpecialCharacters_ReturnsFalse() { + // Given + let input = "Hi!" + + // When + let result = sut.validate(input: input) + + // Then + XCTAssertFalse(result) + } + + // MARK: - Edge Cases + + func testValidate_EmptyString_ReturnsTrue() { + // Given + let input = "" + + // When + let result = sut.validate(input: input) + + // Then + XCTAssertTrue(result) + } + + func testValidate_UnicodeCharacters_WhenNotAllowed_ReturnsTrue() { + // Given + let input = "Привет" + + // When + let result = sut.validate(input: input) + + // Then + XCTAssertTrue(result) + } + + // MARK: - Error Property + + func testErrorProperty_ReturnsProvidedError() { + XCTAssertEqual(sut.error as? String, String.error) + } +} + +// MARK: Constants + +private extension String { + static let error = "Invalid characters" +}