diff --git a/.swiftformat b/.swiftformat
new file mode 100644
index 0000000..b743fe2
--- /dev/null
+++ b/.swiftformat
@@ -0,0 +1,64 @@
+# Stream rules
+
+--swiftversion 5.7
+
+# Use 'swiftformat --options' to list all of the possible options
+
+--header "\nValidator\nCopyright © {created.year} Space Code. All rights reserved.\n//"
+
+--enable blankLinesBetweenScopes
+--enable blankLinesAtStartOfScope
+--enable blankLinesAtEndOfScope
+--enable blankLinesAroundMark
+--enable anyObjectProtocol
+--enable consecutiveBlankLines
+--enable consecutiveSpaces
+--enable duplicateImports
+--enable elseOnSameLine
+--enable emptyBraces
+--enable initCoderUnavailable
+--enable leadingDelimiters
+--enable numberFormatting
+--enable preferKeyPath
+--enable redundantBreak
+--enable redundantExtensionACL
+--enable redundantFileprivate
+--enable redundantGet
+--enable redundantInit
+--enable redundantLet
+--enable redundantLetError
+--enable redundantNilInit
+--enable redundantObjc
+--enable redundantParens
+--enable redundantPattern
+--enable redundantRawValues
+--enable redundantReturn
+--enable redundantSelf
+--enable redundantVoidReturnType
+--enable semicolons
+--enable sortedImports
+--enable sortedSwitchCases
+--enable spaceAroundBraces
+--enable spaceAroundBrackets
+--enable spaceAroundComments
+--enable spaceAroundGenerics
+--enable spaceAroundOperators
+--enable spaceInsideBraces
+--enable spaceInsideBrackets
+--enable spaceInsideComments
+--enable spaceInsideGenerics
+--enable spaceInsideParens
+--enable strongOutlets
+--enable strongifiedSelf
+--enable todos
+--enable trailingClosures
+--enable unusedArguments
+--enable void
+--enable markTypes
+--enable isEmpty
+
+# format options
+
+--wraparguments before-first
+--wrapcollections before-first
+--maxwidth 140
\ No newline at end of file
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 0000000..77ee1c7
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,132 @@
+excluded:
+ - Tests
+ - Package.swift
+ - .build
+
+# Rules
+
+disabled_rules:
+ - trailing_comma
+ - todo
+ - opening_brace
+
+opt_in_rules: # some rules are only opt-in
+ - anyobject_protocol
+ - array_init
+ - closure_body_length
+ - closure_end_indentation
+ - closure_spacing
+ - collection_alignment
+ - contains_over_filter_count
+ - contains_over_filter_is_empty
+ - contains_over_first_not_nil
+ - contains_over_range_nil_comparison
+ - convenience_type
+ - discouraged_object_literal
+ - discouraged_optional_boolean
+ - empty_collection_literal
+ - empty_count
+ - empty_string
+ - empty_xctest_method
+ - enum_case_associated_values_count
+ - explicit_init
+ - fallthrough
+ - fatal_error_message
+ - file_name
+ - file_types_order
+ - first_where
+ - flatmap_over_map_reduce
+ - force_unwrapping
+ - ibinspectable_in_extension
+ - identical_operands
+ - implicit_return
+ - inert_defer
+ - joined_default_parameter
+ - last_where
+ - legacy_multiple
+ - legacy_random
+ - literal_expression_end_indentation
+ - lower_acl_than_parent
+ - multiline_arguments
+ - multiline_function_chains
+ - multiline_literal_brackets
+ - multiline_parameters
+ - multiline_parameters_brackets
+ - no_space_in_method_call
+ - operator_usage_whitespace
+ - optional_enum_case_matching
+ - orphaned_doc_comment
+ - overridden_super_call
+ - override_in_extension
+ - pattern_matching_keywords
+ - prefer_self_type_over_type_of_self
+ - prefer_zero_over_explicit_init
+ - prefixed_toplevel_constant
+ - private_action
+ - prohibited_super_call
+ - quick_discouraged_call
+ - quick_discouraged_focused_test
+ - quick_discouraged_pending_test
+ - reduce_into
+ - redundant_nil_coalescing
+ - redundant_objc_attribute
+ - redundant_type_annotation
+ - required_enum_case
+ - single_test_class
+ - sorted_first_last
+ - sorted_imports
+ - static_operator
+ - strict_fileprivate
+ - switch_case_on_newline
+ - toggle_bool
+ - unavailable_function
+ - unneeded_parentheses_in_closure_argument
+ - unowned_variable_capture
+ - untyped_error_in_catch
+ - vertical_parameter_alignment_on_call
+ - vertical_whitespace_closing_braces
+ - vertical_whitespace_opening_braces
+ - xct_specific_matcher
+ - yoda_condition
+
+force_cast: warning
+force_try: warning
+
+identifier_name:
+ excluded:
+ - id
+ - URL
+
+analyzer_rules:
+ - unused_import
+ - unused_declaration
+
+line_length:
+ warning: 130
+ error: 200
+
+type_body_length:
+ warning: 300
+ error: 400
+
+file_length:
+ warning: 500
+ error: 1200
+
+function_body_length:
+ warning: 30
+ error: 50
+
+large_tuple:
+ error: 3
+
+nesting:
+ type_level:
+ warning: 2
+ statement_level:
+ warning: 10
+
+type_name:
+ max_length:
+ warning: 40
+ error: 50
\ No newline at end of file
diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme
new file mode 100644
index 0000000..ec134cc
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorCore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorCore.xcscheme
new file mode 100644
index 0000000..57f8c0c
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorCore.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorUI.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorUI.xcscheme
new file mode 100644
index 0000000..ef67b36
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/ValidatorUI.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2e9885a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,2 @@
+# Change Log
+All notable changes to this project will be documented in this file.
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..856d64b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,19 @@
+all: bootstrap
+
+bootstrap: hook
+ mint bootstrap
+
+hook:
+ ln -sf ../../hooks/pre-commit .git/hooks/pre-commit
+ chmod +x .git/hooks/pre-commit
+
+mint:
+ mint bootstrap
+
+lint:
+ mint run swiftlint
+
+fmt:
+ mint run swiftformat Sources Tests
+
+.PHONY: all bootstrap hook mint lint fmt
\ No newline at end of file
diff --git a/Mintfile b/Mintfile
new file mode 100644
index 0000000..1f32d33
--- /dev/null
+++ b/Mintfile
@@ -0,0 +1,2 @@
+nicklockwood/SwiftFormat@0.47.12
+realm/SwiftLint@0.47.1
\ No newline at end of file
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..7578da7
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,24 @@
+// swift-tools-version: 5.5
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "Validator",
+ platforms: [
+ .iOS(.v13),
+ .macOS(.v10_13),
+ .watchOS(.v7),
+ .tvOS(.v13),
+ ],
+ products: [
+ .library(name: "ValidatorCore", targets: ["ValidatorCore"]),
+ .library(name: "ValidatorUI", targets: ["ValidatorUI"]),
+ ],
+ dependencies: [],
+ targets: [
+ .target(name: "ValidatorCore", dependencies: []),
+ .target(name: "ValidatorUI", dependencies: ["ValidatorCore"]),
+ .testTarget(name: "ValidatorTests", dependencies: ["ValidatorCore"]),
+ ]
+)
diff --git a/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationError.swift b/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationError.swift
new file mode 100644
index 0000000..0863ae1
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationError.swift
@@ -0,0 +1,12 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+/// `IValidationError` is the error type returned by Validator.
+public protocol IValidationError: Error {
+ /// The error message.
+ var message: String { get }
+}
diff --git a/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationRule.swift b/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationRule.swift
new file mode 100644
index 0000000..1fe4726
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationRule.swift
@@ -0,0 +1,22 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+/// A type that verifies the input data.
+public protocol IValidationRule {
+ /// The validation type.
+ associatedtype Input
+
+ /// The validation error.
+ var error: IValidationError { get }
+
+ /// Validates an input value.
+ ///
+ /// - Parameter input: The input value.
+ ///
+ /// - Returns: A validation result.
+ func validate(input: Input) -> Bool
+}
diff --git a/Sources/ValidatorCore/Classes/Core/Models/ValidationResult.swift b/Sources/ValidatorCore/Classes/Core/Models/ValidationResult.swift
new file mode 100644
index 0000000..2730d1b
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Core/Models/ValidationResult.swift
@@ -0,0 +1,16 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+public enum ValidationResult {
+ /// Indicates that the validation was successful.
+ case valid
+
+ /// Indicates that the validation failed with a list of errors.
+ ///
+ /// - Parameter errors: An array of validation errors.
+ case invalid(errors: [IValidationError])
+}
diff --git a/Sources/ValidatorCore/Classes/Extensions/IValidationRule+Erase.swift b/Sources/ValidatorCore/Classes/Extensions/IValidationRule+Erase.swift
new file mode 100644
index 0000000..8283d94
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Extensions/IValidationRule+Erase.swift
@@ -0,0 +1,12 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+public extension IValidationRule {
+ func eraseToAnyValidationRule() -> AnyValidationRule {
+ AnyValidationRule(self)
+ }
+}
diff --git a/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift b/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift
new file mode 100644
index 0000000..74419e4
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift
@@ -0,0 +1,10 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+extension String: IValidationError {
+ public var message: String { self }
+}
diff --git a/Sources/ValidatorCore/Classes/Extensions/ValidationResult+Equatable.swift b/Sources/ValidatorCore/Classes/Extensions/ValidationResult+Equatable.swift
new file mode 100644
index 0000000..a602167
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Extensions/ValidationResult+Equatable.swift
@@ -0,0 +1,19 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+extension ValidationResult: Equatable {
+ public static func == (lhs: ValidationResult, rhs: ValidationResult) -> Bool {
+ switch (lhs, rhs) {
+ case (.valid, .valid):
+ return true
+ case let (.invalid(errors: lhs), .invalid(errors: rhs)):
+ return lhs.map(\.message).joined() == rhs.map(\.message).joined()
+ default:
+ return false
+ }
+ }
+}
diff --git a/Sources/ValidatorCore/Classes/Rules/AnyValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/AnyValidationRule.swift
new file mode 100644
index 0000000..fb6c60d
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Rules/AnyValidationRule.swift
@@ -0,0 +1,27 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+public struct AnyValidationRule: IValidationRule {
+ // MARK: Properties
+
+ private let validationClosure: (Input) -> Bool
+
+ public var error: IValidationError
+
+ // MARK: Initialization
+
+ public init(_ rule: Rule) where Rule.Input == Input {
+ validationClosure = rule.validate
+ error = rule.error
+ }
+
+ // MARK: IValidationRule
+
+ public func validate(input: Input) -> Bool {
+ validationClosure(input)
+ }
+}
diff --git a/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift
new file mode 100644
index 0000000..d59f20c
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift
@@ -0,0 +1,39 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+/// A length validation rule.
+public struct LengthValidationRule: IValidationRule {
+ // MARK: Types
+
+ public typealias Input = String
+
+ // MARK: Properties
+
+ /// The minimum length.
+ public let min: Int
+
+ /// The maximum length.
+ public let max: Int
+
+ /// The validation error.
+ public let error: IValidationError
+
+ // MARK: Initialization
+
+ public init(min: Int, max: Int, error: IValidationError) {
+ self.min = min
+ self.max = max
+ self.error = error
+ }
+
+ // MARK: IValidationRule
+
+ public func validate(input: String) -> Bool {
+ let length = input.count
+ return length >= min && length <= max
+ }
+}
diff --git a/Sources/ValidatorCore/Classes/Rules/RegexValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/RegexValidationRule.swift
new file mode 100644
index 0000000..cc8d799
--- /dev/null
+++ b/Sources/ValidatorCore/Classes/Rules/RegexValidationRule.swift
@@ -0,0 +1,37 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+/// A regular expression validation rule.
+public struct RegexValidationRule: IValidationRule {
+ // MARK: Types
+
+ public typealias Input = String
+
+ // MARK: Properties
+
+ /// The regular expression pattern.
+ public let pattern: String
+ /// The validation error.
+ public let error: IValidationError
+
+ // MARK: Initialization
+
+ public init(pattern: String, error: IValidationError) {
+ self.pattern = pattern
+ self.error = error
+ }
+
+ // MARK: IValidationRule
+
+ public func validate(input: String) -> Bool {
+ let range = NSRange(location: .zero, length: input.count)
+ if let regex = try? NSRegularExpression(pattern: pattern) {
+ return regex.firstMatch(in: input, range: range) != nil
+ }
+ return false
+ }
+}
diff --git a/Sources/ValidatorCore/IValidator.swift b/Sources/ValidatorCore/IValidator.swift
new file mode 100644
index 0000000..c516259
--- /dev/null
+++ b/Sources/ValidatorCore/IValidator.swift
@@ -0,0 +1,48 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+/// A type that can be used to validate the contents of an input value.
+/// Each validator accepts a value as its first argument and a rule or an array of rules as its second argument.
+public protocol IValidator {
+ /// Validates an input value.
+ ///
+ /// - Parameters:
+ /// - input: The input value.
+ /// - rule: The validation rule.
+ ///
+ /// - Returns: A validation result.
+ func validate(input: T.Input, rule: T) -> ValidationResult
+
+ /// Validates an input value.
+ ///
+ /// - Parameters:
+ /// - input: The input value.
+ /// - rules: The validation rules array.
+ ///
+ /// - Returns: A validation result.
+ func validate(input: T, rules: [AnyValidationRule]) -> ValidationResult
+
+ /// Validates an input value.
+ ///
+ /// - Parameters:
+ /// - input: The input value.
+ /// - rule: The validation rule.
+ ///
+ /// - Returns: A validation result.
+ @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
+ func validate(input: T, rule: some IValidationRule) -> ValidationResult
+
+ /// Validates an input value.
+ ///
+ /// - Parameters:
+ /// - input: The input value.
+ /// - rules: The validation rules array.
+ ///
+ /// - Returns: A validation result.
+ @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
+ func validate(input: T, rules: [any IValidationRule]) -> ValidationResult
+}
diff --git a/Sources/ValidatorCore/Validator.swift b/Sources/ValidatorCore/Validator.swift
new file mode 100644
index 0000000..fb1afa5
--- /dev/null
+++ b/Sources/ValidatorCore/Validator.swift
@@ -0,0 +1,42 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+// MARK: - Validator
+
+public final class Validator {
+ // MARK: Initialization
+
+ public init() {}
+}
+
+// MARK: IValidator
+
+extension Validator: IValidator {
+ public func validate(input: T.Input, rule: T) -> ValidationResult where T: IValidationRule {
+ validate(input: input, rules: [rule.eraseToAnyValidationRule()])
+ }
+
+ public func validate(input: T, rules: [AnyValidationRule]) -> ValidationResult {
+ let errors = rules
+ .filter { !$0.validate(input: input) }
+ .map(\.error)
+
+ return errors.isEmpty ? .valid : ValidationResult.invalid(errors: errors)
+ }
+
+ @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
+ public func validate(input: T, rules: [any IValidationRule]) -> ValidationResult {
+ let errors = rules
+ .filter { !self.validate(input: input, rule: $0) }
+ .map(\.error)
+
+ return errors.isEmpty ? .valid : ValidationResult.invalid(errors: errors)
+ }
+
+ @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
+ func validate(input: T, rule: some IValidationRule) -> Bool {
+ rule.validate(input: input)
+ }
+}
diff --git a/Sources/ValidatorUI/ValidatorUI.swift b/Sources/ValidatorUI/ValidatorUI.swift
new file mode 100644
index 0000000..63c2173
--- /dev/null
+++ b/Sources/ValidatorUI/ValidatorUI.swift
@@ -0,0 +1,12 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import Foundation
+
+public struct ValidatorUI {
+ public private(set) var text = "Hello, World!"
+
+ public init() {}
+}
diff --git a/Tests/ValidatorTests/UnitTests/Rules/LengthValidationRuleTests.swift b/Tests/ValidatorTests/UnitTests/Rules/LengthValidationRuleTests.swift
new file mode 100644
index 0000000..18d07bf
--- /dev/null
+++ b/Tests/ValidatorTests/UnitTests/Rules/LengthValidationRuleTests.swift
@@ -0,0 +1,64 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import ValidatorCore
+import XCTest
+
+// MARK: - LengthValidationRuleTests
+
+final class LengthValidationRuleTests: XCTestCase {
+ // MARK: Properties
+
+ private var sut: LengthValidationRule!
+
+ // MARK: XCTestCase
+
+ override func setUp() {
+ super.setUp()
+ sut = LengthValidationRule(min: .min, max: .max, error: String.error)
+ }
+
+ override func tearDown() {
+ sut = nil
+ super.tearDown()
+ }
+
+ // MARK: Tests
+
+ func test_thatLengthValidationRuleSetsProperties() {
+ // then
+ XCTAssertEqual(sut.min, .min)
+ XCTAssertEqual(sut.max, .max)
+ XCTAssertEqual(sut.error.message, .error)
+ }
+
+ func test_thatLengthValidationRuleValidatesInput_whenInputIsCorrectValue() {
+ // when
+ let result = sut.validate(input: String(String.input.prefix(.max)))
+
+ // then
+ XCTAssertTrue(result)
+ }
+
+ func test_thatLengthValidationRuleValidatesInput_whenInputIsIncorrectValue() {
+ // when
+ let result = sut.validate(input: .input)
+
+ // then
+ XCTAssertFalse(result)
+ }
+}
+
+// MARK: - Constants
+
+private extension Int {
+ static let min = 1
+ static let max = 10
+}
+
+private extension String {
+ static let input = "lorem ipsum lorem ipsum lorem ipsum"
+ static let error = "error"
+}
diff --git a/Tests/ValidatorTests/UnitTests/Rules/RegexValidationRuleTests.swift b/Tests/ValidatorTests/UnitTests/Rules/RegexValidationRuleTests.swift
new file mode 100644
index 0000000..466a9d5
--- /dev/null
+++ b/Tests/ValidatorTests/UnitTests/Rules/RegexValidationRuleTests.swift
@@ -0,0 +1,60 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import ValidatorCore
+import XCTest
+
+// MARK: - RegexValidationRuleTests
+
+final class RegexValidationRuleTests: XCTestCase {
+ // MARK: Properties
+
+ private var sut: RegexValidationRule!
+
+ // MARK: XCTestCase
+
+ override func setUp() {
+ super.setUp()
+ sut = RegexValidationRule(pattern: .pattern, error: String.error)
+ }
+
+ override func tearDown() {
+ sut = nil
+ super.tearDown()
+ }
+
+ // MARK: Tests
+
+ func test_thatRegexValidationRuleSetsProperties() {
+ // then
+ XCTAssertEqual(sut.pattern, .pattern)
+ XCTAssertEqual(sut.error.message, .error)
+ }
+
+ func test_thatRegexValidationRuleValidatesInput_whenInputIsCorrectValue() {
+ // when
+ let result = sut.validate(input: .input)
+
+ // then
+ XCTAssertTrue(result)
+ }
+
+ func test_thatRegexValidationRuleValidatesInput_whenInputIsIncorrectValue() {
+ // when
+ let result = sut.validate(input: .invalidInput)
+
+ // then
+ XCTAssertFalse(result)
+ }
+}
+
+// MARK: - Constants
+
+private extension String {
+ static let input = "abbat"
+ static let invalidInput = "abb"
+ static let pattern = "[a-zA-Z]at"
+ static let error = "error"
+}
diff --git a/Tests/ValidatorTests/UnitTests/Validator/ValidationResultTests.swift b/Tests/ValidatorTests/UnitTests/Validator/ValidationResultTests.swift
new file mode 100644
index 0000000..f5cbab9
--- /dev/null
+++ b/Tests/ValidatorTests/UnitTests/Validator/ValidationResultTests.swift
@@ -0,0 +1,53 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import ValidatorCore
+import XCTest
+
+// MARK: - ValidationResultTests
+
+final class ValidationResultTests: XCTestCase {
+ func test_validationResultsEquality_whenValidResultsAreEqual() {
+ // given
+ let result1 = ValidationResult.valid
+ let result2 = ValidationResult.valid
+
+ // when
+ let result = result1 == result2
+
+ // then
+ XCTAssertTrue(result)
+ }
+
+ func test_validationResultsEquality_whenResultsAreNotEqual() {
+ // given
+ let result1 = ValidationResult.valid
+ let result2 = ValidationResult.invalid(errors: [])
+
+ // when
+ let result = result1 == result2
+
+ // then
+ XCTAssertFalse(result)
+ }
+
+ func test_validationResultsEquality_whenInvalidResultsAreEqual() {
+ // given
+ let result1 = ValidationResult.invalid(errors: [String.error])
+ let result2 = ValidationResult.invalid(errors: [String.error])
+
+ // when
+ let result = result1 == result2
+
+ // then
+ XCTAssertTrue(result)
+ }
+}
+
+// MARK: - Constants
+
+private extension String {
+ static let error = "error"
+}
diff --git a/Tests/ValidatorTests/UnitTests/Validator/ValidatorTests.swift b/Tests/ValidatorTests/UnitTests/Validator/ValidatorTests.swift
new file mode 100644
index 0000000..55b8e70
--- /dev/null
+++ b/Tests/ValidatorTests/UnitTests/Validator/ValidatorTests.swift
@@ -0,0 +1,127 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+@testable import ValidatorCore
+import XCTest
+
+// MARK: - ValidatorTests
+
+final class ValidatorTests: XCTestCase {
+ // MARK: Properties
+
+ private var validator: Validator!
+
+ // MARK: XCTestCase
+
+ override func setUp() {
+ super.setUp()
+ validator = Validator()
+ }
+
+ override func tearDown() {
+ validator = nil
+ super.tearDown()
+ }
+
+ // MARK: Tests
+
+ func test_thatValidatorValidatesInput_whenLengthIsLessThanTenCharacters() {
+ // given
+ let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error).eraseToAnyValidationRule()
+
+ // when
+ let validationResult = validator.validate(input: String(String.text.prefix(.max)), rule: validationRule)
+
+ // then
+ XCTAssertEqual(validationResult, .valid)
+ }
+
+ func test_thatValidatorValidatesInput_whenLengthIsGreaterThanTenCharacters() {
+ // given
+ let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error).eraseToAnyValidationRule()
+
+ // when
+ let validationResult = validator.validate(input: String(String.text.prefix(11)), rules: [validationRule])
+
+ // then
+ if case let .invalid(errors) = validationResult {
+ XCTAssertEqual(errors.count, 1)
+ XCTAssertEqual(errors.first?.message, .error)
+ } else {
+ XCTFail("The input string must be greater than 10 characters")
+ }
+ }
+
+ func test_thatValidatorValidatesInput_whenLengthIsLessThanOneCharacter() {
+ // given
+ let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error).eraseToAnyValidationRule()
+
+ // when
+ let validationResult = validator.validate(input: .empty, rules: [validationRule])
+
+ // then
+ if case let .invalid(errors) = validationResult {
+ XCTAssertEqual(errors.count, 1)
+ XCTAssertEqual(errors.first?.message, .error)
+ } else {
+ XCTFail("The input string must be empty")
+ }
+ }
+
+ func test_thatValidatorValidatesInput_whenThereAreCoupleOfRules() {
+ // given
+ let rules: [AnyValidationRule] = [
+ RegexValidationRule(pattern: .pattern, error: String.error).eraseToAnyValidationRule(),
+ LengthValidationRule(min: .min, max: .max, error: String.error).eraseToAnyValidationRule(),
+ ]
+
+ // when
+ let validationResult = validator.validate(input: .text, rules: rules)
+
+ // then
+ if case let .invalid(errors) = validationResult {
+ XCTAssertEqual(errors.count, 2)
+ XCTAssertEqual(errors[0].message, .error)
+ XCTAssertEqual(errors[1].message, .error)
+ } else {
+ XCTFail("The input string must be empty")
+ }
+ }
+
+ @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
+ func test_thatValidatorValidatesInputWithAnyRules_whenThereAreCoupleOfRules() {
+ // given
+ let rules: [any IValidationRule] = [
+ RegexValidationRule(pattern: .pattern, error: String.error),
+ LengthValidationRule(min: .min, max: .max, error: String.error),
+ ]
+
+ // when
+ let validationResult = validator.validate(input: .text, rules: rules)
+
+ // then
+ if case let .invalid(errors) = validationResult {
+ XCTAssertEqual(errors.count, 2)
+ XCTAssertEqual(errors[0].message, .error)
+ XCTAssertEqual(errors[1].message, .error)
+ } else {
+ XCTFail("The input string must be empty")
+ }
+ }
+}
+
+// MARK: - Constants
+
+private extension String {
+ static let error: String = "error description"
+ static let text: String = "lorem ipsum lorem ipsum lorem ipsum"
+ static let empty: String = ""
+ static let pattern: String = ""
+}
+
+private extension Int {
+ static let min: Int = 1
+ static let max: Int = 10
+}
diff --git a/hooks/pre-commit b/hooks/pre-commit
new file mode 100755
index 0000000..956fdcb
--- /dev/null
+++ b/hooks/pre-commit
@@ -0,0 +1,38 @@
+#!/bin/bash
+git diff --diff-filter=d --staged --name-only | grep -e '\.swift$' | while read line; do
+ if [[ $line == *"/Generated"* ]]; then
+ echo "IGNORING GENERATED FILE: " "$line";
+ else
+ mint run swiftformat swiftformat "${line}";
+ git add "$line";
+ fi
+done
+
+LINT=$(which mint)
+if [[ -e "${LINT}" ]]; then
+ # Export files in SCRIPT_INPUT_FILE_$count to lint against later
+ count=0
+ while IFS= read -r file_path; do
+ export SCRIPT_INPUT_FILE_$count="$file_path"
+ count=$((count + 1))
+ done < <(git diff --name-only --cached --diff-filter=d | grep ".swift$")
+ export SCRIPT_INPUT_FILE_COUNT=$count
+
+ if [ "$count" -eq 0 ]; then
+ echo "No files to lint!"
+ exit 0
+ fi
+
+ echo "Found $count lintable files! Linting now.."
+ mint run swiftlint --use-script-input-files --strict --config .swiftlint.yml
+ RESULT=$? # swiftline exit value is number of errors
+
+ if [ $RESULT -eq 0 ]; then
+ echo "🎉 Well done. No violation."
+ fi
+ exit $RESULT
+else
+ echo "⚠️ WARNING: SwiftLint not found"
+ echo "⚠️ You might want to edit .git/hooks/pre-commit to locate your swiftlint"
+ exit 0
+fi
\ No newline at end of file