@@ -14,7 +14,7 @@ import _StringProcessing
14
14
@testable import RegexBuilder
15
15
16
16
// A nibbler processes a single character from a string
17
- private protocol Nibbler: CustomRegexComponent {
17
+ private protocol Nibbler: CustomMatchingRegexComponent {
18
18
func nibble(_: Character) -> RegexOutput?
19
19
}
20
20
@@ -24,7 +24,7 @@ extension Nibbler {
24
24
_ input: String,
25
25
startingAt index: String.Index,
26
26
in bounds: Range<String.Index>
27
- ) -> (upperBound: String.Index, output: RegexOutput)? {
27
+ ) throws -> (upperBound: String.Index, output: RegexOutput)? {
28
28
guard index != bounds.upperBound, let res = nibble(input[index]) else {
29
29
return nil
30
30
}
@@ -49,6 +49,69 @@ private struct Asciibbler: Nibbler {
49
49
}
50
50
}
51
51
52
+ private struct IntParser: CustomMatchingRegexComponent {
53
+ struct ParseError: Error, Hashable {}
54
+ typealias RegexOutput = Int
55
+ func match(_ input: String,
56
+ startingAt index: String.Index,
57
+ in bounds: Range<String.Index>
58
+ ) throws -> (upperBound: String.Index, output: Int)? {
59
+ guard index != bounds.upperBound else { return nil }
60
+
61
+ let r = Regex {
62
+ Capture(OneOrMore(.digit)) { Int($0) }
63
+ }
64
+
65
+ guard let match = input[index..<bounds.upperBound].prefixMatch(of: r),
66
+ let output = match.1 else {
67
+ throw ParseError()
68
+ }
69
+
70
+ return (match.range.upperBound, output)
71
+ }
72
+ }
73
+
74
+ private struct CurrencyParser: CustomMatchingRegexComponent {
75
+ enum Currency: String, Hashable {
76
+ case usd = "USD"
77
+ case ntd = "NTD"
78
+ case dem = "DEM"
79
+ }
80
+
81
+ enum ParseError: Error, Hashable {
82
+ case unrecognized
83
+ case deprecated
84
+ }
85
+
86
+ typealias RegexOutput = Currency
87
+ func match(_ input: String,
88
+ startingAt index: String.Index,
89
+ in bounds: Range<String.Index>
90
+ ) throws -> (upperBound: String.Index, output: Currency)? {
91
+
92
+ guard index != bounds.upperBound else { return nil }
93
+
94
+ let substr = input[index..<bounds.upperBound]
95
+ guard !substr.isEmpty else { return nil }
96
+
97
+ let currencies: [Currency] = [ .usd, .ntd ]
98
+ let deprecated: [Currency] = [ .dem ]
99
+
100
+ for currency in currencies {
101
+ if substr.hasPrefix(currency.rawValue) {
102
+ return (input.range(of: currency.rawValue)!.upperBound, currency)
103
+ }
104
+ }
105
+
106
+ for dep in deprecated {
107
+ if substr.hasPrefix(dep.rawValue) {
108
+ throw ParseError.deprecated
109
+ }
110
+ }
111
+ throw ParseError.unrecognized
112
+ }
113
+ }
114
+
52
115
enum MatchCall {
53
116
case match
54
117
case firstMatch
@@ -223,4 +286,186 @@ class CustomRegexComponentTests: XCTestCase {
223
286
224
287
225
288
}
289
+
290
+ func testCustomRegexThrows() {
291
+
292
+ func customTest<Match: Equatable, E: Error & Equatable>(
293
+ _ regex: Regex<Match>,
294
+ _ tests: (input: String, match: Match?, expectError: E?)...,
295
+ file: StaticString = #file,
296
+ line: UInt = #line
297
+ ) {
298
+ for (input, match, expectError) in tests {
299
+ do {
300
+ let result = try regex.wholeMatch(in: input)?.output
301
+ XCTAssertEqual(result, match)
302
+ } catch let e as E {
303
+ XCTAssertEqual(e, expectError)
304
+ } catch {
305
+ XCTFail()
306
+ }
307
+ }
308
+ }
309
+
310
+ func customTest<Match: Equatable, Error1: Error & Equatable, Error2: Error & Equatable>(
311
+ _ regex: Regex<Match>,
312
+ _ tests: (input: String, match: Match?, expectError1: Error1?, expectError2: Error2?)...,
313
+ file: StaticString = #file,
314
+ line: UInt = #line
315
+ ) {
316
+ for (input, match, expectError1, expectError2) in tests {
317
+ do {
318
+ let result = try regex.wholeMatch(in: input)?.output
319
+ XCTAssertEqual(result, match)
320
+ } catch let e as Error1 {
321
+ XCTAssertEqual(e, expectError1, input, file: file, line: line)
322
+ } catch let e as Error2 {
323
+ XCTAssertEqual(e, expectError2, input, file: file, line: line)
324
+ } catch {
325
+ XCTFail("caught error: \(error.localizedDescription)")
326
+ }
327
+ }
328
+ }
329
+
330
+ func customTest<Capture: Equatable, Error1: Error & Equatable, Error2: Error & Equatable>(
331
+ _ regex: Regex<(Substring, Capture)>,
332
+ _ tests: (input: String, match: (Substring, Capture)?, expectError1: Error1?, expectError2: Error2?)...,
333
+ file: StaticString = #file,
334
+ line: UInt = #line
335
+ ) {
336
+ for (input, match, expectError1, expectError2) in tests {
337
+ do {
338
+ let result = try regex.wholeMatch(in: input)?.output
339
+ XCTAssertEqual(result?.0, match?.0, file: file, line: line)
340
+ XCTAssertEqual(result?.1, match?.1, file: file, line: line)
341
+ } catch let e as Error1 {
342
+ XCTAssertEqual(e, expectError1, input, file: file, line: line)
343
+ } catch let e as Error2 {
344
+ XCTAssertEqual(e, expectError2, input, file: file, line: line)
345
+ } catch {
346
+ XCTFail("caught error: \(error.localizedDescription)")
347
+ }
348
+ }
349
+ }
350
+
351
+ func customTest<Capture1: Equatable, Capture2: Equatable, Error1: Error & Equatable, Error2: Error & Equatable>(
352
+ _ regex: Regex<(Substring, Capture1, Capture2)>,
353
+ _ tests: (input: String, match: (Substring, Capture1, Capture2)?, expectError1: Error1?, expectError2: Error2?)...,
354
+ file: StaticString = #file,
355
+ line: UInt = #line
356
+ ) {
357
+ for (input, match, expectError1, expectError2) in tests {
358
+ do {
359
+ let result = try regex.wholeMatch(in: input)?.output
360
+ XCTAssertEqual(result?.0, match?.0, file: file, line: line)
361
+ XCTAssertEqual(result?.1, match?.1, file: file, line: line)
362
+ XCTAssertEqual(result?.2, match?.2, file: file, line: line)
363
+ } catch let e as Error1 {
364
+ XCTAssertEqual(e, expectError1, input, file: file, line: line)
365
+ } catch let e as Error2 {
366
+ XCTAssertEqual(e, expectError2, input, file: file, line: line)
367
+ } catch {
368
+ XCTFail("caught error: \(error.localizedDescription)")
369
+ }
370
+ }
371
+ }
372
+
373
+ // No capture, one error
374
+ customTest(
375
+ Regex {
376
+ IntParser()
377
+ },
378
+ ("zzz", nil, IntParser.ParseError()),
379
+ ("x10x", nil, IntParser.ParseError()),
380
+ ("30", 30, nil)
381
+ )
382
+
383
+ customTest(
384
+ Regex {
385
+ CurrencyParser()
386
+ },
387
+ ("USD", .usd, nil),
388
+ ("NTD", .ntd, nil),
389
+ ("NTD USD", nil, nil),
390
+ ("DEM", nil, CurrencyParser.ParseError.deprecated),
391
+ ("XXX", nil, CurrencyParser.ParseError.unrecognized)
392
+ )
393
+
394
+ // No capture, two errors
395
+ customTest(
396
+ Regex {
397
+ IntParser()
398
+ " "
399
+ IntParser()
400
+ },
401
+ ("20304 100", "20304 100", nil, nil),
402
+ ("20304.445 200", nil, IntParser.ParseError(), nil),
403
+ ("20304 200.123", nil, nil, IntParser.ParseError()),
404
+ ("20304.445 200.123", nil, IntParser.ParseError(), IntParser.ParseError())
405
+ )
406
+
407
+ customTest(
408
+ Regex {
409
+ CurrencyParser()
410
+ IntParser()
411
+ },
412
+ ("USD100", "USD100", nil, nil),
413
+ ("XXX100", nil, CurrencyParser.ParseError.unrecognized, nil),
414
+ ("USD100.000", nil, nil, IntParser.ParseError()),
415
+ ("XXX100.0000", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError())
416
+ )
417
+
418
+ // One capture, two errors: One error is thrown from inside a capture,
419
+ // while the other one is thrown from outside
420
+ customTest(
421
+ Regex {
422
+ Capture { CurrencyParser() }
423
+ IntParser()
424
+ },
425
+ ("USD100", ("USD100", .usd), nil, nil),
426
+ ("NTD305.5", nil, nil, IntParser.ParseError()),
427
+ ("DEM200", ("DEM200", .dem), CurrencyParser.ParseError.deprecated, nil),
428
+ ("XXX", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError())
429
+ )
430
+
431
+ customTest(
432
+ Regex {
433
+ CurrencyParser()
434
+ Capture { IntParser() }
435
+ },
436
+ ("USD100", ("USD100", 100), nil, nil),
437
+ ("NTD305.5", nil, nil, IntParser.ParseError()),
438
+ ("DEM200", ("DEM200", 200), CurrencyParser.ParseError.deprecated, nil),
439
+ ("XXX", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError())
440
+ )
441
+
442
+ // One capture, two errors: Both errors are thrown from inside the capture
443
+ customTest(
444
+ Regex {
445
+ Capture {
446
+ CurrencyParser()
447
+ IntParser()
448
+ }
449
+ },
450
+ ("USD100", ("USD100", "USD100"), nil, nil),
451
+ ("NTD305.5", nil, nil, IntParser.ParseError()),
452
+ ("DEM200", ("DEM200", "DEM200"), CurrencyParser.ParseError.deprecated, nil),
453
+ ("XXX", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError())
454
+ )
455
+
456
+ // Two captures, two errors: Different erros are thrown from inside captures
457
+ customTest(
458
+ Regex {
459
+ Capture(CurrencyParser())
460
+ Capture(IntParser())
461
+ },
462
+ ("USD100", ("USD100", .usd, 100), nil, nil),
463
+ ("NTD500", ("NTD500", .ntd, 500), nil, nil),
464
+ ("XXX20", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError()),
465
+ ("DEM500", nil, CurrencyParser.ParseError.deprecated, nil),
466
+ ("DEM500.345", nil, CurrencyParser.ParseError.deprecated, IntParser.ParseError()),
467
+ ("NTD100.345", nil, nil, IntParser.ParseError())
468
+ )
469
+
470
+ }
226
471
}
0 commit comments