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