diff --git a/Examples/Core/Content.playground/Contents.swift b/Examples/Core/Content.playground/Contents.swift index ccbd049..a563b38 100644 --- a/Examples/Core/Content.playground/Contents.swift +++ b/Examples/Core/Content.playground/Contents.swift @@ -5,7 +5,7 @@ import ValidatorCore -// 1. LengthValidationRule - validates string length +/// 1. LengthValidationRule - validates string length let lengthRule = LengthValidationRule(min: 3, max: 20, error: "Length must be 3-20 characters") let shortString = "ab" @@ -18,7 +18,7 @@ print("'\(validString)' is valid: \(lengthRule.validate(input: validString))") / print("'\(longString)' is valid: \(lengthRule.validate(input: longString))") // false print() -// 2. NonEmptyValidationRule - checks if string is not empty +/// 2. NonEmptyValidationRule - checks if string is not empty let nonEmptyRule = NonEmptyValidationRule(error: "Field is required") let emptyString = "" @@ -31,7 +31,7 @@ print("'\(whitespaceString)' is valid: \(nonEmptyRule.validate(input: whitespace print("'\(filledString)' is valid: \(nonEmptyRule.validate(input: filledString))") // true print() -// 3. PrefixValidationRule - validates string prefix +/// 3. PrefixValidationRule - validates string prefix let prefixRule = PrefixValidationRule(prefix: "https://", error: "URL must start with https://") let httpURL = "http://example.com" @@ -44,7 +44,7 @@ print("'\(httpsURL)' is valid: \(prefixRule.validate(input: httpsURL))") // true print("'\(noProtocol)' is valid: \(prefixRule.validate(input: noProtocol))") // false print() -// 4. SuffixValidationRule - validates string suffix +/// 4. SuffixValidationRule - validates string suffix let suffixRule = SuffixValidationRule(suffix: ".com", error: "Domain must end with .com") let comDomain = "example.com" @@ -57,7 +57,7 @@ print("'\(orgDomain)' is valid: \(suffixRule.validate(input: orgDomain))") // fa print("'\(noDomain)' is valid: \(suffixRule.validate(input: noDomain))") // false print() -// 5. RegexValidationRule - validates using regular expression +/// 5. RegexValidationRule - validates using regular expression let phoneRule = RegexValidationRule(pattern: "^\\d{3}-\\d{4}$", error: "Invalid phone format") let validPhone = "123-4567" @@ -70,7 +70,7 @@ print("'\(invalidPhone1)' is valid: \(phoneRule.validate(input: invalidPhone1))" print("'\(invalidPhone2)' is valid: \(phoneRule.validate(input: invalidPhone2))") // false print() -// 6. URLValidationRule - validates URL format +/// 6. URLValidationRule - validates URL format let urlRule = URLValidationRule(error: "Please enter a valid URL") let validURL = "https://www.apple.com" @@ -83,7 +83,7 @@ print("'\(invalidURL)' is valid: \(urlRule.validate(input: invalidURL))") // fal print("'\(localURL)' is valid: \(urlRule.validate(input: localURL))") // true print() -// 7. CreditCardValidationRule - validates credit card number (Luhn algorithm) +/// 7. CreditCardValidationRule - validates credit card number (Luhn algorithm) let cardRule = CreditCardValidationRule(error: "Invalid card number") let validCard = "4532015112830366" // Valid Visa test number @@ -96,7 +96,7 @@ print("'\(invalidCard)' is valid: \(cardRule.validate(input: invalidCard))") // print("'\(shortCard)' is valid: \(cardRule.validate(input: shortCard))") // false print() -// 8. EmailValidationRule - validates email format +/// 8. EmailValidationRule - validates email format let emailRule = EmailValidationRule(error: "Please enter a valid email") let validEmail = "user@example.com" @@ -109,7 +109,7 @@ print("'\(invalidEmail1)' is valid: \(emailRule.validate(input: invalidEmail1))" print("'\(invalidEmail2)' is valid: \(emailRule.validate(input: invalidEmail2))") // false print() -// 9. CharactersValidationRule - validates allowed characters +/// 9. CharactersValidationRule - validates allowed characters let lettersRule = CharactersValidationRule(characterSet: .letters, error: "Invalid characters") let onlyLetters = "HelloWorld" @@ -122,7 +122,7 @@ print("'\(withNumbers)' is valid: \(lettersRule.validate(input: withNumbers))") print("'\(withSpaces)' is valid: \(lettersRule.validate(input: withSpaces))") // false print() -// 10. NilValidationRule - validates that value is nil +/// 10. NilValidationRule - validates that value is nil let nilRule = NilValidationRule(error: "Value must be nil") let nilValue: String? = nil diff --git a/Examples/Core/PlaygroundDependencies/Tests/PlaygroundDependenciesTests/PlaygroundDependenciesTests.swift b/Examples/Core/PlaygroundDependencies/Tests/PlaygroundDependenciesTests/PlaygroundDependenciesTests.swift index df2b80e..21d5a0c 100644 --- a/Examples/Core/PlaygroundDependencies/Tests/PlaygroundDependenciesTests/PlaygroundDependenciesTests.swift +++ b/Examples/Core/PlaygroundDependencies/Tests/PlaygroundDependenciesTests/PlaygroundDependenciesTests.swift @@ -6,6 +6,6 @@ @testable import PlaygroundDependencies import Testing -@Test func example() async throws { +@Test func example() { // Write your test here and use APIs like `#expect(...)` to check expected conditions. } diff --git a/Examples/UIKit/UIKitExample/ViewControllers/LoginTextFieldExampleViewController.swift b/Examples/UIKit/UIKitExample/ViewControllers/LoginTextFieldExampleViewController.swift index 936913d..faec82f 100644 --- a/Examples/UIKit/UIKitExample/ViewControllers/LoginTextFieldExampleViewController.swift +++ b/Examples/UIKit/UIKitExample/ViewControllers/LoginTextFieldExampleViewController.swift @@ -10,7 +10,7 @@ import ValidatorUI final class LoginTextFieldExampleViewController: UIViewController { // MARK: - Properties - // UI + /// UI private let scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false @@ -67,7 +67,7 @@ final class LoginTextFieldExampleViewController: UIViewController { return stackView }() - // Private properties + /// Private properties private var isValid: Bool { [firstNameTextField, lastNameTextField, emailTextField] .allSatisfy { $0.validationResult == .valid } diff --git a/README.md b/README.md index 674c2f0..4b573be 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ struct RegistrationView: View { | `IBANValidationRule` | Validates that a string is a valid IBAN (International Bank Account Number) | `IBANValidationRule(error: "Invalid IBAN")` | `IPAddressValidationRule` | Validates that a string is a valid IPv4 or IPv6 address | `IPAddressValidationRule(version: .v4, error: ValidationError("Invalid IPv4"))` | `PostalCodeValidationRule` | Validates postal/ZIP codes for different countries | `PostalCodeValidationRule(country: .uk, error: "Invalid post code")` +| `Base64ValidationRule` | | `Base64ValidationRule(error: "The input is not valid Base64.")` ## Custom Validators diff --git a/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift b/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift index 5822a16..6b44fca 100644 --- a/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift +++ b/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift @@ -22,5 +22,7 @@ import Foundation /// ``` extension String: IValidationError { /// Returns the string itself as the error message. - public var message: String { self } + public var message: String { + self + } } diff --git a/Sources/ValidatorCore/Classes/Rules/Base64ValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/Base64ValidationRule.swift new file mode 100644 index 0000000..c9cb579 --- /dev/null +++ b/Sources/ValidatorCore/Classes/Rules/Base64ValidationRule.swift @@ -0,0 +1,52 @@ +// +// Validator +// Copyright © 2026 Space Code. All rights reserved. +// + +import Foundation + +/// Validates that a string represents valid Base64-encoded data. +/// +/// # Example: +/// ```swift +/// let rule = Base64ValidationRule(error: "Invalid Base64") +/// rule.validate(input: "SGVsbG8gV29ybGQ=") // true +/// rule.validate(input: "not_base64!") // false +/// ``` +public struct Base64ValidationRule: IValidationRule { + // MARK: Types + + public typealias Input = String + + // MARK: Properties + + /// The validation error returned if the input is not valid Base64. + public let error: IValidationError + + // MARK: Initialization + + /// Initializes a Base64 validation rule. + /// + /// - Parameter error: The validation error returned if input fails validation. + public init(error: IValidationError) { + self.error = error + } + + // MARK: IValidationRule + + public func validate(input: String) -> Bool { + guard !input.isEmpty else { return false } + + let cleanedInput = input.replacingOccurrences(of: "\\s", with: "", options: .regularExpression) + + guard !cleanedInput.isEmpty else { return false } + + guard cleanedInput.count.isMultiple(of: 4) else { return false } + + guard Data(base64Encoded: cleanedInput) != nil else { return false } + + let base64Pattern = "^[A-Za-z0-9+/]*={0,2}$" + let predicate = NSPredicate(format: "SELF MATCHES %@", base64Pattern) + return predicate.evaluate(with: cleanedInput) + } +} diff --git a/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift index 02dbee2..f44ebf6 100644 --- a/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift @@ -5,14 +5,14 @@ import Foundation -/// Validates that a string represents a valid URL. -/// -/// # Example: -/// ```swift -/// let rule = URLValidationRule(error: "Invalid URL") -/// rule.validate(input: "https://example.com") // true -/// rule.validate(input: "not_a_url") // false -/// ``` +// Validates that a string represents a valid URL. +// +// # Example: +// ```swift +// let rule = URLValidationRule(error: "Invalid URL") +// rule.validate(input: "https://example.com") // true +// rule.validate(input: "not_a_url") // false +// ``` public struct URLValidationRule: IValidationRule { // MARK: Types diff --git a/Sources/ValidatorCore/Validator.docc/Overview.md b/Sources/ValidatorCore/Validator.docc/Overview.md index 410e44d..5e09009 100644 --- a/Sources/ValidatorCore/Validator.docc/Overview.md +++ b/Sources/ValidatorCore/Validator.docc/Overview.md @@ -40,6 +40,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for - ``IBANValidationRule`` - ``IPAddressValidationRule`` - ``PostalCodeValidationRule`` +- ``Base64ValidationRule`` ### Articles diff --git a/Sources/ValidatorUI/Classes/AppKit/Extensions/NSTextField+Validation.swift b/Sources/ValidatorUI/Classes/AppKit/Extensions/NSTextField+Validation.swift index aacecf7..d5e1e67 100644 --- a/Sources/ValidatorUI/Classes/AppKit/Extensions/NSTextField+Validation.swift +++ b/Sources/ValidatorUI/Classes/AppKit/Extensions/NSTextField+Validation.swift @@ -9,7 +9,9 @@ extension NSTextField: IUIValidatable { /// The value of the text field to validate. /// Returns an empty string if `text` is nil. - public var inputValue: String { stringValue } + public var inputValue: String { + stringValue + } /// The type of input for validation. public typealias Input = String diff --git a/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextField+Validation.swift b/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextField+Validation.swift index 790b385..9aeff82 100644 --- a/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextField+Validation.swift +++ b/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextField+Validation.swift @@ -9,7 +9,9 @@ extension UITextField: IUIValidatable { /// The value of the text field to validate. /// Returns an empty string if `text` is nil. - public var inputValue: String { text ?? "" } + public var inputValue: String { + text ?? "" + } /// The type of input for validation. public typealias Input = String diff --git a/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextView+Validation.swift b/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextView+Validation.swift index deef938..a2f6d1b 100644 --- a/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextView+Validation.swift +++ b/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextView+Validation.swift @@ -9,7 +9,9 @@ extension UITextView: IUIValidatable { /// The value of the text view to validate. /// Returns an empty string if `text` is nil. - public var inputValue: String { text ?? "" } + public var inputValue: String { + text ?? "" + } /// The type of input for validation. public typealias Input = String diff --git a/Tests/ValidatorCoreTests/UnitTests/Rules/Base64ValidationRuleTests.swift b/Tests/ValidatorCoreTests/UnitTests/Rules/Base64ValidationRuleTests.swift new file mode 100644 index 0000000..9e02b35 --- /dev/null +++ b/Tests/ValidatorCoreTests/UnitTests/Rules/Base64ValidationRuleTests.swift @@ -0,0 +1,189 @@ +// +// Validator +// Copyright © 2026 Space Code. All rights reserved. +// + +import ValidatorCore +import XCTest + +// MARK: - Base64ValidationRuleTests + +final class Base64ValidationRuleTests: XCTestCase { + // MARK: - Properties + + private var sut: Base64ValidationRule! + + // MARK: - Setup + + override func setUp() { + super.setUp() + sut = Base64ValidationRule(error: String.error) + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - Tests + + func test_validate_validBase64_shouldReturnTrue() { + // given + let base64 = "SGVsbG8gV29ybGQ=" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertTrue(result) + } + + func test_validate_validBase64WithoutPadding_shouldReturnTrue() { + // given + let base64 = "SGVsbG8=" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertTrue(result) + } + + func test_validate_validBase64NoPadding_shouldReturnTrue() { + // given + let base64 = "YWJj" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertTrue(result) + } + + func test_validate_validBase64WithPlus_shouldReturnTrue() { + // given + let base64 = "YWJjZGVmZ2hpamts+A==" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertTrue(result) + } + + func test_validate_validBase64WithSlash_shouldReturnTrue() { + // given + let base64 = "YWJjZGVmZ2hpamts/w==" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertTrue(result) + } + + func test_validate_invalidCharacters_shouldReturnFalse() { + // given + let base64 = "Hello@World!" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertFalse(result) + } + + func test_validate_invalidLength_shouldReturnFalse() { + // given + let base64 = "SGVsbG" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertFalse(result) + } + + func test_validate_tooManyPaddingCharacters_shouldReturnFalse() { + // given + let base64 = "SGVsbG8===" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertFalse(result) + } + + func test_validate_emptyString_shouldReturnFalse() { + // given + let base64 = "" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertFalse(result) + } + + func test_validate_whitespaceString_shouldReturnFalse() { + // given + let base64 = " " + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertFalse(result) + } + + func test_validate_plainText_shouldReturnFalse() { + // given + let base64 = "not base64" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertFalse(result) + } + + func test_validate_base64WithWhitespace_shouldReturnTrue() { + // given + let base64 = "SGVs bG8g V29y bGQ=" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertTrue(result) + } + + func test_validate_base64WithNewlines_shouldReturnTrue() { + // given + let base64 = "SGVs\nbG8g\nV29y\nbGQ=" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertTrue(result) + } + + func test_validate_paddingInMiddle_shouldReturnFalse() { + // given + let base64 = "SGVs=bG8=" + + // when + let result = sut.validate(input: base64) + + // then + XCTAssertFalse(result) + } +} + +// MARK: Constants + +private extension String { + static let error = "Base64 string is invalid" +}