Skip to content

Commit 83dfd9f

Browse files
authored
feat: add email validation rule (#63)
1 parent 7238231 commit 83dfd9f

File tree

4 files changed

+366
-1
lines changed

4 files changed

+366
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,4 @@ fastlane/test_output
8888
# https://github.com/johnno1962/injectionforxcode
8989

9090
iOSInjectionProject/
91-
./swiftpm
91+
.swiftpm/

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ struct RegistrationView: View {
302302
| `RegexValidationRule` | Pattern matching validation | `RegexValidationRule(pattern: "^\\d{3}-\\d{4}$", error: "Invalid phone format")` |
303303
| `URLValidationRule` | Validates URL format | `URLValidationRule(error: "Please enter a valid URL")` |
304304
| `CreditCardValidationRule` | Validates credit card numbers (Luhn algorithm) | `CreditCardValidationRule(error: "Invalid card number")` |
305+
| `EmailValidationRule` | Validates email format | `EmailValidationRule(error: "Please enter a valid email")` |
305306

306307
## Custom Validators
307308

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Validator
3+
// Copyright © 2023 Space Code. All rights reserved.
4+
//
5+
6+
import Foundation
7+
8+
/// An email validation rule.
9+
public struct EmailValidationRule: IValidationRule {
10+
// MARK: Types
11+
12+
public typealias Input = String
13+
14+
// MARK: Properties
15+
16+
/// The validation error.
17+
public let error: IValidationError
18+
19+
// MARK: Initialization
20+
21+
public init(error: IValidationError) {
22+
self.error = error
23+
}
24+
25+
// MARK: IValidationRule
26+
27+
public func validate(input: String) -> Bool {
28+
let range = NSRange(location: .zero, length: input.count)
29+
if let regex = try? NSRegularExpression(
30+
// swiftlint:disable:next line_length
31+
pattern: "^(?!\\.)(?!.*\\.\\.)[A-Z0-9a-z._%+-]+(?<!\\.)@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\\.[A-Za-z]{2,64}$",
32+
options: [.caseInsensitive]
33+
) {
34+
return regex.firstMatch(in: input, range: range) != nil
35+
}
36+
return false
37+
}
38+
}
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
//
2+
// Validator
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
@testable import ValidatorCore
7+
import XCTest
8+
9+
final class EmailValidationRuleTests: XCTestCase {
10+
var sut: EmailValidationRule!
11+
12+
override func setUp() {
13+
super.setUp()
14+
sut = EmailValidationRule(error: "Invalid email")
15+
}
16+
17+
override func tearDown() {
18+
sut = nil
19+
super.tearDown()
20+
}
21+
22+
// MARK: - Valid Email Tests
23+
24+
func testValidateWithValidEmail_ReturnsTrue() {
25+
// Given
26+
let email = "user@example.com"
27+
28+
// When
29+
let result = sut.validate(input: email)
30+
31+
// Then
32+
XCTAssertTrue(result)
33+
}
34+
35+
func testValidateWithValidEmailWithNumbers_ReturnsTrue() {
36+
// Given
37+
let email = "user123@example456.com"
38+
39+
// When
40+
let result = sut.validate(input: email)
41+
42+
// Then
43+
XCTAssertTrue(result)
44+
}
45+
46+
func testValidateWithValidEmailWithDots_ReturnsTrue() {
47+
// Given
48+
let email = "first.last@example.com"
49+
50+
// When
51+
let result = sut.validate(input: email)
52+
53+
// Then
54+
XCTAssertTrue(result)
55+
}
56+
57+
func testValidateWithValidEmailWithPlus_ReturnsTrue() {
58+
// Given
59+
let email = "user+tag@example.com"
60+
61+
// When
62+
let result = sut.validate(input: email)
63+
64+
// Then
65+
XCTAssertTrue(result)
66+
}
67+
68+
func testValidateWithValidEmailWithUnderscore_ReturnsTrue() {
69+
// Given
70+
let email = "user_name@example.com"
71+
72+
// When
73+
let result = sut.validate(input: email)
74+
75+
// Then
76+
XCTAssertTrue(result)
77+
}
78+
79+
func testValidateWithValidEmailWithHyphen_ReturnsTrue() {
80+
// Given
81+
let email = "user@my-domain.com"
82+
83+
// When
84+
let result = sut.validate(input: email)
85+
86+
// Then
87+
XCTAssertTrue(result)
88+
}
89+
90+
func testValidateWithValidEmailWithSubdomain_ReturnsTrue() {
91+
// Given
92+
let email = "user@mail.example.com"
93+
94+
// When
95+
let result = sut.validate(input: email)
96+
97+
// Then
98+
XCTAssertTrue(result)
99+
}
100+
101+
func testValidateWithValidEmailWithLongTLD_ReturnsTrue() {
102+
// Given
103+
let email = "user@example.museum"
104+
105+
// When
106+
let result = sut.validate(input: email)
107+
108+
// Then
109+
XCTAssertTrue(result)
110+
}
111+
112+
func testValidateWithValidEmailWithTwoCharacterTLD_ReturnsTrue() {
113+
// Given
114+
let email = "user@example.io"
115+
116+
// When
117+
let result = sut.validate(input: email)
118+
119+
// Then
120+
XCTAssertTrue(result)
121+
}
122+
123+
// MARK: - Invalid Email Tests
124+
125+
func testValidateWithInvalidEmailMissingAtSign_ReturnsFalse() {
126+
// Given
127+
let email = "invalid.email.com"
128+
129+
// When
130+
let result = sut.validate(input: email)
131+
132+
// Then
133+
XCTAssertFalse(result)
134+
}
135+
136+
func testValidateWithInvalidEmailMissingDomain_ReturnsFalse() {
137+
// Given
138+
let email = "user@"
139+
140+
// When
141+
let result = sut.validate(input: email)
142+
143+
// Then
144+
XCTAssertFalse(result)
145+
}
146+
147+
func testValidateWithInvalidEmailMissingLocalPart_ReturnsFalse() {
148+
// Given
149+
let email = "@example.com"
150+
151+
// When
152+
let result = sut.validate(input: email)
153+
154+
// Then
155+
XCTAssertFalse(result)
156+
}
157+
158+
func testValidateWithInvalidEmailMissingTLD_ReturnsFalse() {
159+
// Given
160+
let email = "user@example"
161+
162+
// When
163+
let result = sut.validate(input: email)
164+
165+
// Then
166+
XCTAssertFalse(result)
167+
}
168+
169+
func testValidateWithInvalidEmailMultipleAtSigns_ReturnsFalse() {
170+
// Given
171+
let email = "user@@example.com"
172+
173+
// When
174+
let result = sut.validate(input: email)
175+
176+
// Then
177+
XCTAssertFalse(result)
178+
}
179+
180+
func testValidateWithInvalidEmailSpaces_ReturnsFalse() {
181+
// Given
182+
let email = "user @example.com"
183+
184+
// When
185+
let result = sut.validate(input: email)
186+
187+
// Then
188+
XCTAssertFalse(result)
189+
}
190+
191+
func testValidateWithInvalidEmailSpecialCharacters_ReturnsFalse() {
192+
// Given
193+
let email = "user#name@example.com"
194+
195+
// When
196+
let result = sut.validate(input: email)
197+
198+
// Then
199+
XCTAssertFalse(result)
200+
}
201+
202+
func testValidateWithInvalidEmailDotAtStart_ReturnsFalse() {
203+
// Given
204+
let email = ".user@example.com"
205+
206+
// When
207+
let result = sut.validate(input: email)
208+
209+
// Then
210+
XCTAssertFalse(result)
211+
}
212+
213+
func testValidateWithInvalidEmailDotAtEnd_ReturnsFalse() {
214+
// Given
215+
let email = "user.@example.com"
216+
217+
// When
218+
let result = sut.validate(input: email)
219+
220+
// Then
221+
XCTAssertFalse(result)
222+
}
223+
224+
func testValidateWithInvalidEmailConsecutiveDots_ReturnsFalse() {
225+
// Given
226+
let email = "user..name@example.com"
227+
228+
// When
229+
let result = sut.validate(input: email)
230+
231+
// Then
232+
XCTAssertFalse(result)
233+
}
234+
235+
func testValidateWithInvalidEmailTLDTooLong_ReturnsFalse() {
236+
// Given
237+
let email = "user@example.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm"
238+
239+
// When
240+
let result = sut.validate(input: email)
241+
242+
// Then
243+
XCTAssertFalse(result)
244+
}
245+
246+
func testValidateWithInvalidEmailSingleCharacterTLD_ReturnsFalse() {
247+
// Given
248+
let email = "user@example.c"
249+
250+
// When
251+
let result = sut.validate(input: email)
252+
253+
// Then
254+
XCTAssertFalse(result)
255+
}
256+
257+
// MARK: - Edge Cases
258+
259+
func testValidateWithEmptyString_ReturnsFalse() {
260+
// Given
261+
let email = ""
262+
263+
// When
264+
let result = sut.validate(input: email)
265+
266+
// Then
267+
XCTAssertFalse(result)
268+
}
269+
270+
func testValidateWithOnlyAtSign_ReturnsFalse() {
271+
// Given
272+
let email = "@"
273+
274+
// When
275+
let result = sut.validate(input: email)
276+
277+
// Then
278+
XCTAssertFalse(result)
279+
}
280+
281+
func testValidateWithOnlyDot_ReturnsFalse() {
282+
// Given
283+
let email = "."
284+
285+
// When
286+
let result = sut.validate(input: email)
287+
288+
// Then
289+
XCTAssertFalse(result)
290+
}
291+
292+
func testValidateWithWhitespaceOnly_ReturnsFalse() {
293+
// Given
294+
let email = " "
295+
296+
// When
297+
let result = sut.validate(input: email)
298+
299+
// Then
300+
XCTAssertFalse(result)
301+
}
302+
303+
func testValidateWithValidEmailWithMaxTLDLength_ReturnsTrue() {
304+
// Given
305+
let email = "user@example.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij"
306+
307+
// When
308+
let result = sut.validate(input: email)
309+
310+
// Then
311+
XCTAssertTrue(result)
312+
}
313+
314+
// MARK: - Error Property Tests
315+
316+
func testErrorPropertyIsSetCorrectly() {
317+
// Given
318+
let expectedError = "Invalid email"
319+
320+
// When
321+
let actualError = sut.error as? String
322+
323+
// Then
324+
XCTAssertEqual(actualError, expectedError)
325+
}
326+
}

0 commit comments

Comments
 (0)