diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b6621a9
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,40 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ open-pull-requests-limit: 10
+ schedule:
+ interval: daily
+ time: '07:00'
+ timezone: Europe/Berlin
+ assignees:
+ - ns-vasilev
+ reviewers:
+ - ns-vasilev
+ commit-message:
+ prefix: "chore"
+ include: "scope"
+ labels:
+ - "dependencies"
+
+ - package-ecosystem: swift
+ directory: /
+ open-pull-requests-limit: 10
+ schedule:
+ interval: daily
+ time: '07:00'
+ timezone: Europe/Berlin
+ assignees:
+ - ns-vasilev
+ reviewers:
+ - ns-vasilev
+ commit-message:
+ prefix: "chore"
+ include: "scope"
+ labels:
+ - "dependencies"
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 602285e..684e245 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v5
- name: GitHub Action for SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
@@ -80,7 +80,7 @@ jobs:
- uses: actions/checkout@v5
- name: ${{ matrix.name }}
run: xcodebuild test -scheme "Validator-Package" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v5
with:
name: ${{ matrix.name }}
path: test_output
diff --git a/.github/workflows/conventional-pr.yml b/.github/workflows/conventional-pr.yml
new file mode 100644
index 0000000..75569d0
--- /dev/null
+++ b/.github/workflows/conventional-pr.yml
@@ -0,0 +1,20 @@
+name: conventional-pr
+on:
+ pull_request:
+ branches:
+ - main
+ - dev
+ types:
+ - opened
+ - edited
+ - synchronize
+jobs:
+ lint-pr:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: amannn/action-semantic-pull-request@v6
+ with:
+ requireScope: false
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 158ca87..6a0dbaa 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -18,7 +18,7 @@ jobs:
ruby-version: 3.1.4
bundler-cache: true
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v5
- name: Setup gems
run: |
gem install bundler
diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme
deleted file mode 100644
index ea021af..0000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorCore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorCore.xcscheme
deleted file mode 100644
index 57f8c0c..0000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorCore.xcscheme
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorUI.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorUI.xcscheme
deleted file mode 100644
index ef67b36..0000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorUI.xcscheme
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03b5df2..25248a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,29 @@ All notable changes to this project will be documented in this file.
- `1.1.x` Releases - [1.1.0](#110)
- `1.0.x` Releases - [1.0.0](#100) | [1.0.1](#101)
-## [1.1.0](https://github.com/space-code/validator/releases/tag/1.2.0)
+## [Unreleased]
+
+#### Added
+- Add `dependabot.yml`
+ - Added in Pull Request [#21](https://github.com/space-code/validator/pull/21).
+- Add `URLValidationRule`.
+ - Added in Pull Request [#25](https://github.com/space-code/validator/pull/25).
+- Add `CreditCardValidationRule`.
+ - Added in Pull Request [#26](https://github.com/space-code/validator/pull/26).
+- Add `conventional-pr.yml` for PR validation.
+ - Added in Pull Request [#27](https://github.com/space-code/validator/pull/27).
+
+#### Updated
+- Update `Mintfile`
+ - Updated in Pull Request [#22](https://github.com/space-code/validator/pull/22).
+- Update `dependabot.yml`
+ - Updated in Pull Request [#28](https://github.com/space-code/validator/pull/28).
+
+#### Fixed
+- Fix conventional commit script execution in GitHub Actions
+ - Fixed in Pull Request [#29](https://github.com/space-code/validator/pull/29).
+
+## [1.2.0](https://github.com/space-code/validator/releases/tag/1.2.0)
Released on 2025-11-13.
#### Updated
diff --git a/Mintfile b/Mintfile
index ebeb1c1..4ca9786 100644
--- a/Mintfile
+++ b/Mintfile
@@ -1,2 +1,2 @@
-nicklockwood/SwiftFormat@0.54.0
-realm/SwiftLint@0.55.1
+nicklockwood/SwiftFormat@0.58.6
+realm/SwiftLint@0.62.2
\ No newline at end of file
diff --git a/README.md b/README.md
index 9abe793..68351f4 100644
--- a/README.md
+++ b/README.md
@@ -36,14 +36,14 @@ If you need to validate some data, you can use the `Validator` object for this p
import ValidatorCore
let validator = Validator()
-let result = validator.validate(input: "text", rule: LengthValidationRule(min: 4, error: "error text"))
+let result = validator.validate(input: "text", rule: LengthValidationRule(min: 4, error: "Text must be at least 4 characters long"))
switch result {
case .valid:
- print("text is valid")
+ print("Text is valid")
case let .invalid(errors):
// handle validation errors
- print(errors)
+ print("Validation errors: \(errors.map(\.message))")
}
```
@@ -66,16 +66,16 @@ class ViewController: UIViewController {
super.viewDidLoad()
/// Adds validation rule to the text field.
- textField.add(rule: LengthValidationRule(max: 10, error: "error text"))
+ textField.add(rule: LengthValidationRule(max: 10, error: "Text must be at most 10 characters"))
/// Enables automatic validation on input change.
textField.validateOnInputChange(isEnabled: true)
/// Handle the validation result.
textField.validationHandler = { result in
switch result {
case .valid:
- print("text is valid")
+ print("Text is valid")
case let .invalid(errors):
- print(errors)
+ print("Validation errors: \(errors)")
}
}
}
@@ -122,14 +122,14 @@ struct ContentView: View {
@State private var text: String = ""
private let validationRules: [any IValidationRule] = [
- LengthValidationRule(max: 10, error: "Text error")
+ LengthValidationRule(max: 10, error: "Text must be at most 10 characters")
]
var body: some View {
VStack {
- TextField("Text", text: $text)
+ TextField("Enter text", text: $text)
.validate(item: $text, rules: validationRules) { errors in
- Text("Text is bigger than 10 characters")
+ Text("Text exceeds 10 characters")
}
}
}
@@ -149,10 +149,10 @@ class Form: ObservableObject {
@Published
var manager = FormFieldManager()
- @FormField(rules: [LengthValidationRule(max: 20, error: "The first name is very long")])
+ @FormField(rules: [LengthValidationRule(max: 20, error: "First name is too long")])
var firstName: String = ""
- @FormField(rules: [LengthValidationRule(min: 5, error: "The last name is too short")])
+ @FormField(rules: [LengthValidationRule(min: 5, error: "Last name is too short")])
var lastName: String = ""
lazy var firstNameValidationContainer = _firstName.validate(manager: manager)
@@ -180,9 +180,9 @@ struct ContentView: View {
form.manager.$isValid,
perform: { value in
if value {
- print("The form's fields are valid")
+ print("All form fields are valid")
} else {
- print("The form's fields aren't valid")
+ print("Some form fields are invalid")
}
}
)
@@ -199,6 +199,8 @@ struct ContentView: View {
| **PrefixValidationRule** | To validate whether a string contains a prefix |
| **SuffixValidationRule** | To validate whether a string contains a suffix |
| **RegexValidationRule** | To validate if a pattern is matched |
+| **URLValidationRule** | To validate whether a string contains a URL |
+| **CreditCardValidationRule** | To validate whether a string has a valid credit card number |
## Custom Validation Rules
diff --git a/Sources/ValidatorCore/Classes/Rules/CreditCardValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/CreditCardValidationRule.swift
new file mode 100644
index 0000000..76c2c70
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Rules/CreditCardValidationRule.swift
@@ -0,0 +1,76 @@
+//
+// Validator
+// Copyright © 2025 Space Code. All rights reserved.
+//
+
+/// A credit card validation rule.
+public struct CreditCardValidationRule: IValidationRule {
+ // MARK: Types
+
+ public enum CardType: String, Sendable, CaseIterable {
+ case visa, masterCard, amex, jcb, unionPay
+ }
+
+ public typealias Input = String
+
+ // MARK: Properties
+
+ public let types: [CardType]
+
+ /// The validation error.
+ public let error: IValidationError
+
+ // MARK: Initialization
+
+ public init(types: [CardType] = CardType.allCases, error: IValidationError) {
+ self.types = types
+ self.error = error
+ }
+
+ // MARK: IValidationRule
+
+ public func validate(input: String) -> Bool {
+ let sanitized = input.replacingOccurrences(of: " ", with: "")
+
+ guard sanitized.allSatisfy(\.isNumber) else { return false }
+
+ guard types.contains(where: { matches(cardNumber: sanitized, type: $0) }) else { return false }
+
+ return isValidLuhn(sanitized)
+ }
+
+ // MARK: Private
+
+ private func matches(cardNumber: String, type: CardType) -> Bool {
+ switch type {
+ case .visa:
+ cardNumber.hasPrefix("4") && (cardNumber.count == 13 || cardNumber.count == 16 || cardNumber.count == 19)
+ case .masterCard:
+ (cardNumber.hasPrefix("51") || cardNumber.hasPrefix("52") ||
+ cardNumber.hasPrefix("53") || cardNumber.hasPrefix("54") ||
+ cardNumber.hasPrefix("55")) && cardNumber.count == 16
+ case .amex:
+ (cardNumber.hasPrefix("34") || cardNumber.hasPrefix("37")) && cardNumber.count == 15
+ case .jcb:
+ (cardNumber.hasPrefix("3528") || cardNumber.hasPrefix("3589")) && cardNumber.count == 16
+ case .unionPay:
+ cardNumber.hasPrefix("62") && (cardNumber.count >= 16 && cardNumber.count <= 19)
+ }
+ }
+
+ private func isValidLuhn(_ cardNumber: String) -> Bool {
+ let reversedDigits = cardNumber.reversed().map { Int(String($0)) ?? 0 }
+ var sum = 0
+
+ for (index, digit) in reversedDigits.enumerated() {
+ if !index.isMultiple(of: 2) {
+ let doubled = digit * 2
+ sum += doubled > 9 ? doubled - 9 : doubled
+ } else {
+ sum += digit
+ }
+ }
+
+ return sum.isMultiple(of: 10)
+ }
+}
diff --git a/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift
new file mode 100644
index 0000000..c52f6ae
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift
@@ -0,0 +1,31 @@
+//
+// Validator
+// Copyright © 2025 Space Code. All rights reserved.
+//
+
+import Foundation
+
+/// A url validation rule.
+public struct URLValidationRule: 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 {
+ guard let url = URL(string: input) else { return false }
+ return url.isFileURL || (url.host != nil && url.scheme != nil)
+ }
+}
diff --git a/Tests/ValidatorCoreTests/UnitTests/Rules/CreditCardValidationRuleTests.swift b/Tests/ValidatorCoreTests/UnitTests/Rules/CreditCardValidationRuleTests.swift
new file mode 100644
index 0000000..9ac1993
--- /dev/null
+++ b/Tests/ValidatorCoreTests/UnitTests/Rules/CreditCardValidationRuleTests.swift
@@ -0,0 +1,164 @@
+//
+// Validator
+// Copyright © 2025 Space Code. All rights reserved.
+//
+
+@testable import ValidatorCore
+import XCTest
+
+// MARK: - CreditCardValidationRuleTests
+
+final class CreditCardValidationRuleTests: XCTestCase {
+ // MARK: - Visa
+
+ func test_validate_visaCardValid_shouldReturnTrue() {
+ // given
+ let rule = CreditCardValidationRule(types: [.visa], error: String.error)
+ let validVisa = "4111111111111111"
+
+ // when
+ let isValid = rule.validate(input: validVisa)
+
+ // then
+ XCTAssertTrue(isValid)
+ }
+
+ func test_validate_visaCardInvalid_shouldReturnFalse() {
+ // given
+ let rule = CreditCardValidationRule(types: [.visa], error: String.error)
+ let invalidVisa = "411111111111112"
+
+ // when
+ let isValid = rule.validate(input: invalidVisa)
+
+ // then
+ XCTAssertFalse(isValid)
+ }
+
+ // MARK: - MasterCard
+
+ func test_validate_masterCardValid_shouldReturnTrue() {
+ // given
+ let rule = CreditCardValidationRule(types: [.masterCard], error: String.error)
+ let validMaster = "5500000000000004"
+
+ // when
+ let isValid = rule.validate(input: validMaster)
+
+ // then
+ XCTAssertTrue(isValid)
+ }
+
+ func test_validate_masterCardInvalid_shouldReturnFalse() {
+ // given
+ let rule = CreditCardValidationRule(types: [.masterCard], error: String.error)
+ let invalidMaster = "550000000000"
+
+ // when
+ let isValid = rule.validate(input: invalidMaster)
+
+ // then
+ XCTAssertFalse(isValid)
+ }
+
+ // MARK: - American Express
+
+ func test_validate_amexValid_shouldReturnTrue() {
+ // given
+ let rule = CreditCardValidationRule(types: [.amex], error: String.error)
+ let validAmex = "340000000000009"
+
+ // when
+ let isValid = rule.validate(input: validAmex)
+
+ // then
+ XCTAssertTrue(isValid)
+ }
+
+ func test_validate_amexInvalid_shouldReturnFalse() {
+ // given
+ let rule = CreditCardValidationRule(types: [.amex], error: String.error)
+ let invalidAmex = "3400000000000099"
+
+ // when
+ let isValid = rule.validate(input: invalidAmex)
+
+ // then
+ XCTAssertFalse(isValid)
+ }
+
+ // MARK: - JCB
+
+ func test_validate_jcbValid_shouldReturnTrue() {
+ // given
+ let rule = CreditCardValidationRule(types: [.jcb], error: String.error)
+ let validJCB = "3528000000000007"
+
+ // when
+ let isValid = rule.validate(input: validJCB)
+
+ // then
+ XCTAssertTrue(isValid)
+ }
+
+ // MARK: - UnionPay
+
+ func test_validate_unionPayValid_shouldReturnTrue() {
+ // given
+ let rule = CreditCardValidationRule(types: [.unionPay], error: String.error)
+ let validUnionPay = "6221260000000000"
+
+ // when
+ let isValid = rule.validate(input: validUnionPay)
+
+ // then
+ XCTAssertTrue(isValid)
+ }
+
+ // MARK: - Multiple Types
+
+ func test_validate_multipleTypesValid_shouldReturnTrue() {
+ // given
+ let rule = CreditCardValidationRule(types: [.visa, .amex, .masterCard], error: String.error)
+ let validVisa = "4111111111111111"
+
+ // when
+ let isValid = rule.validate(input: validVisa)
+
+ // then
+ XCTAssertTrue(isValid)
+ }
+
+ // MARK: - Unknown Card Type
+
+ func test_validate_unknownCardType_shouldReturnFalse() {
+ // given
+ let rule = CreditCardValidationRule(types: [.visa], error: String.error)
+ let randomCard = "9999999999999999"
+
+ // when
+ let isValid = rule.validate(input: randomCard)
+
+ // then
+ XCTAssertFalse(isValid)
+ }
+
+ // MARK: - Empty Input
+
+ func test_validate_emptyString_shouldReturnFalse() {
+ // given
+ let rule = CreditCardValidationRule(types: [.visa], error: String.error)
+
+ // when
+ let isValid = rule.validate(input: "")
+
+ // then
+ XCTAssertFalse(isValid)
+ }
+}
+
+// MARK: Constants
+
+private extension String {
+ static let error = "Invalid card number"
+}
diff --git a/Tests/ValidatorCoreTests/UnitTests/Rules/URLValidationRuleTests.swift b/Tests/ValidatorCoreTests/UnitTests/Rules/URLValidationRuleTests.swift
new file mode 100644
index 0000000..b95358e
--- /dev/null
+++ b/Tests/ValidatorCoreTests/UnitTests/Rules/URLValidationRuleTests.swift
@@ -0,0 +1,123 @@
+//
+// Validator
+// Copyright © 2025 Space Code. All rights reserved.
+//
+
+import ValidatorCore
+import XCTest
+
+// MARK: - URLValidationRuleTests
+
+final class URLValidationRuleTests: XCTestCase {
+ // MARK: - Properties
+
+ private var sut: URLValidationRule!
+
+ // MARK: - Setup
+
+ override func setUp() {
+ super.setUp()
+ sut = URLValidationRule(error: String.error)
+ }
+
+ override func tearDown() {
+ sut = nil
+ super.tearDown()
+ }
+
+ // MARK: - Tests
+
+ func test_validate_validURL_shouldReturnTrue() {
+ // given
+ let url = "https://google.com"
+
+ // when
+ let result = sut.validate(input: url)
+
+ // then
+ XCTAssertTrue(result)
+ }
+
+ func test_validate_missingScheme_shouldReturnFalse() {
+ // given
+ let url = "google.com"
+
+ // when
+ let result = sut.validate(input: url)
+
+ // then
+ XCTAssertFalse(result)
+ }
+
+ func test_validate_missingHost_shouldReturnFalse() {
+ // given
+ let url = "https://"
+
+ // when
+ let result = sut.validate(input: url)
+
+ // then
+ XCTAssertFalse(result)
+ }
+
+ func test_validate_emptyString_shouldReturnFalse() {
+ // given
+ let url = ""
+
+ // when
+ let result = sut.validate(input: url)
+
+ // then
+ XCTAssertFalse(result)
+ }
+
+ func test_validate_whitespaceString_shouldReturnFalse() {
+ // given
+ let url = " "
+
+ // when
+ let result = sut.validate(input: url)
+
+ // then
+ XCTAssertFalse(result)
+ }
+
+ func test_validate_ipAddressURL_shouldReturnTrue() {
+ // given
+ let url = "http://192.168.0.1"
+
+ // when
+ let result = sut.validate(input: url)
+
+ // then
+ XCTAssertTrue(result)
+ }
+
+ func test_validate_urlWithPort_shouldReturnTrue() {
+ // given
+ let url = "https://localhost:8080"
+
+ // when
+ let result = sut.validate(input: url)
+
+ // then
+ XCTAssertTrue(result)
+ }
+
+ func test_validate_ftpScheme_shouldReturnTrue() {
+ // given
+ let url = "ftp://example.com"
+
+ // when
+ let result = sut.validate(input: url)
+
+ // then
+ XCTAssertTrue(result)
+ }
+}
+
+// MARK: Constants
+
+private extension String {
+ static let error = "URL is invalid"
+}