Skip to content

Commit 1a0050f

Browse files
stephencelismbrandonwtgrapperonKeithBird
authored
Add parser for case-iterable, string raw-representable values (#176)
* Add parser for case-iterable, string raw-representable values * wip * Update Sources/Parsing/Documentation.docc/Articles/Parsers/CaseIterable.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/Parsing/Documentation.docc/Articles/Parsers/CaseIterable.md Co-authored-by: Kth <K2968220169@outlook.com> * negatives * Update Sources/Parsing/Documentation.docc/Articles/Parsers/CaseIterable.md Co-authored-by: Kth <K2968220169@outlook.com> * docs Co-authored-by: Brandon Williams <mbrandonw@hey.com> Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> Co-authored-by: Kth <K2968220169@outlook.com>
1 parent 5189e0e commit 1a0050f

File tree

3 files changed

+343
-0
lines changed

3 files changed

+343
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# CaseIterable
2+
3+
A parser that consumes a case-iterable, raw representable value from the beginning of a string.
4+
5+
Given a type that conforms to `CaseIterable` and `RawRepresentable` with a `RawValue` of `String`
6+
or `Int`, we can incrementally parse a value of it.
7+
8+
Notably, raw enumerations that conform to `CaseIterable` meet this criteria, so cases of the
9+
following type can be parsed with no extra work:
10+
11+
```swift
12+
enum Role: String, CaseIterable {
13+
case admin
14+
case guest
15+
case member
16+
}
17+
18+
try Parse {
19+
Int.parser()
20+
","
21+
Role.parser()
22+
}
23+
.parse("123,member") // (123, .member)
24+
```
25+
26+
This also works with raw enumerations that are backed by integers:
27+
28+
```swift
29+
enum Role: Int, CaseIterable {
30+
case admin = 1
31+
case guest = 2
32+
case member = 3
33+
}
34+
35+
try Parse {
36+
Int.parser()
37+
","
38+
Role.parser()
39+
}
40+
.parse("123,1") // (123, .admin)
41+
```
42+
43+
The `parser()` method on `CaseIterable` is overloaded to work on a variety of string representations
44+
in order to be as efficient as possible, including `Substring`, `UTF8View`, and more general
45+
collections of UTF-8 code units (see <doc:StringAbstractions> for more info).
46+
47+
Typically Swift can choose the correct overload by using type inference based on what other parsers
48+
you are combining `parser()` with. For example, if you use `Role.parser()` with a
49+
`Substring` parser, like the literal "," parser in the above examples, Swift
50+
will choose the overload that works on substrings.
51+
52+
On the other hand, if `Role.parser()` is used in a context where the input type cannot be inferred,
53+
then you will get an compiler error:
54+
55+
```swift
56+
let parser = Parse {
57+
Int.parser()
58+
Role.parser() // 🛑 Ambiguous use of 'parser(of:)'
59+
}
60+
61+
try parser.parse("123member")
62+
```
63+
64+
To fix this you can force one of the parsers to be the `Substring` parser, and then the
65+
other will figure it out via type inference:
66+
67+
```swift
68+
let parser = Parse {
69+
Int.parser(of: Substring.self)
70+
Role.parser()
71+
}
72+
73+
try parser.parse("123member") // (123, .member)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
extension CaseIterable where Self: RawRepresentable, RawValue == Int {
2+
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
3+
/// collection of a substring.
4+
///
5+
/// See <doc:CaseIterable> for more info.
6+
///
7+
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the
8+
/// interface that parses any collection of UTF-8 code units.
9+
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
10+
/// of a substring.
11+
@inlinable
12+
public static func parser(
13+
of inputType: Substring.Type = Substring.self
14+
) -> Parsers.CaseIterableRawRepresentableParser<Substring, Self, String> {
15+
.init(toPrefix: { String($0) }, areEquivalent: ==)
16+
}
17+
18+
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
19+
/// collection of a substring's UTF-8 view.
20+
///
21+
/// See <doc:CaseIterable> for more info.
22+
///
23+
/// - Parameter inputType: The `Substring.UTF8View` type. This parameter is included to mirror the
24+
/// interface that parses any collection of UTF-8 code units.
25+
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
26+
/// of a substring's UTF-8 view.
27+
@inlinable
28+
public static func parser(
29+
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self
30+
) -> Parsers.CaseIterableRawRepresentableParser<Substring.UTF8View, Self, String.UTF8View> {
31+
.init(toPrefix: { String($0).utf8 }, areEquivalent: ==)
32+
}
33+
34+
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
35+
/// collection of UTF-8 code units.
36+
///
37+
/// - Parameter inputType: The collection type of UTF-8 code units to parse.
38+
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
39+
/// of a collection of UTF-8 code units.
40+
@inlinable
41+
public static func parser<Input>(
42+
of inputType: Input.Type = Input.self
43+
) -> Parsers.CaseIterableRawRepresentableParser<Input, Self, String.UTF8View>
44+
where
45+
Input.SubSequence == Input,
46+
Input.Element == UTF8.CodeUnit
47+
{
48+
.init(toPrefix: { String($0).utf8 }, areEquivalent: ==)
49+
}
50+
}
51+
52+
extension CaseIterable where Self: RawRepresentable, RawValue == String {
53+
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
54+
/// collection of a substring.
55+
///
56+
/// See <doc:CaseIterable> for more info.
57+
///
58+
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the
59+
/// interface that parses any collection of UTF-8 code units.
60+
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
61+
/// of a substring.
62+
@inlinable
63+
public static func parser(
64+
of inputType: Substring.Type = Substring.self
65+
) -> Parsers.CaseIterableRawRepresentableParser<Substring, Self, String> {
66+
.init(toPrefix: { $0 }, areEquivalent: ==)
67+
}
68+
69+
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
70+
/// collection of a substring's UTF-8 view.
71+
///
72+
/// See <doc:CaseIterable> for more info.
73+
///
74+
/// - Parameter inputType: The `Substring.UTF8View` type. This parameter is included to mirror the
75+
/// interface that parses any collection of UTF-8 code units.
76+
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
77+
/// of a substring's UTF-8 view.
78+
@inlinable
79+
public static func parser(
80+
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self
81+
) -> Parsers.CaseIterableRawRepresentableParser<Substring.UTF8View, Self, String.UTF8View> {
82+
.init(toPrefix: { $0.utf8 }, areEquivalent: ==)
83+
}
84+
85+
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
86+
/// collection of UTF-8 code units.
87+
///
88+
/// - Parameter inputType: The collection type of UTF-8 code units to parse.
89+
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
90+
/// of a collection of UTF-8 code units.
91+
@inlinable
92+
public static func parser<Input>(
93+
of inputType: Input.Type = Input.self
94+
) -> Parsers.CaseIterableRawRepresentableParser<Input, Self, String.UTF8View>
95+
where
96+
Input.SubSequence == Input,
97+
Input.Element == UTF8.CodeUnit
98+
{
99+
.init(toPrefix: { $0.utf8 }, areEquivalent: ==)
100+
}
101+
}
102+
103+
extension Parsers {
104+
public struct CaseIterableRawRepresentableParser<
105+
Input: Collection, Output: CaseIterable & RawRepresentable, Prefix: Collection
106+
>: Parser
107+
where
108+
Input.SubSequence == Input,
109+
Output.RawValue: Comparable,
110+
Prefix.Element == Input.Element
111+
{
112+
@usableFromInline
113+
let cases: [(case: Output, prefix: Prefix, count: Int)]
114+
115+
@usableFromInline
116+
let areEquivalent: (Input.Element, Input.Element) -> Bool
117+
118+
@usableFromInline
119+
init(
120+
toPrefix: @escaping (Output.RawValue) -> Prefix,
121+
areEquivalent: @escaping (Input.Element, Input.Element) -> Bool
122+
) {
123+
self.areEquivalent = areEquivalent
124+
self.cases = Output.allCases
125+
.map {
126+
let prefix = toPrefix($0.rawValue)
127+
return ($0, prefix, prefix.count)
128+
}
129+
.sorted(by: { $0.count > $1.count })
130+
}
131+
132+
@inlinable
133+
public func parse(_ input: inout Input) throws -> Output {
134+
for (`case`, prefix, count) in self.cases {
135+
if input.starts(with: prefix, by: self.areEquivalent) {
136+
input.removeFirst(count)
137+
return `case`
138+
}
139+
}
140+
throw ParsingError.expectedInput("case of \"\(Output.self)\"", at: input)
141+
}
142+
}
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import Parsing
2+
import XCTest
3+
4+
final class CaseIterableRawRepresentableTests: XCTestCase {
5+
func testParserStringRawValue() throws {
6+
enum Person: String, CaseIterable {
7+
case blob = "Blob"
8+
case blobJr = "Blob Jr"
9+
}
10+
11+
let peopleParser = Many {
12+
Person.parser()
13+
} separator: {
14+
",".utf8
15+
} terminator: {
16+
End()
17+
}
18+
19+
var input = "Blob,Blob Jr"[...].utf8
20+
XCTAssertEqual(try peopleParser.parse(&input), [.blob, .blobJr])
21+
22+
input = "Blob Jr,Blob"[...].utf8
23+
XCTAssertEqual(try peopleParser.parse(&input), [.blobJr, .blob])
24+
25+
input = "Blob,Mr Blob"[...].utf8
26+
XCTAssertThrowsError(try peopleParser.parse(&input)) { error in
27+
XCTAssertEqual(
28+
"""
29+
error: multiple failures occurred
30+
31+
error: unexpected input
32+
--> input:1:6
33+
1 | Blob,Mr Blob
34+
| ^ expected case of "Person"
35+
36+
error: unexpected input
37+
--> input:1:5
38+
1 | Blob,Mr Blob
39+
| ^ expected end of input
40+
""",
41+
"\(error)"
42+
)
43+
}
44+
}
45+
46+
func testParserIntRawValue() throws {
47+
enum Person: Int, CaseIterable {
48+
case blob = 4
49+
case blobJr = 42
50+
}
51+
52+
let peopleParser = Many {
53+
Person.parser()
54+
} separator: {
55+
",".utf8
56+
} terminator: {
57+
End()
58+
}
59+
60+
var input = "4,42"[...].utf8
61+
XCTAssertEqual(try peopleParser.parse(&input), [.blob, .blobJr])
62+
63+
input = "42,4"[...].utf8
64+
XCTAssertEqual(try peopleParser.parse(&input), [.blobJr, .blob])
65+
66+
input = "42,100"[...].utf8
67+
XCTAssertThrowsError(try peopleParser.parse(&input)) { error in
68+
XCTAssertEqual(
69+
"""
70+
error: multiple failures occurred
71+
72+
error: unexpected input
73+
--> input:1:4
74+
1 | 42,100
75+
| ^ expected case of "Person"
76+
77+
error: unexpected input
78+
--> input:1:3
79+
1 | 42,100
80+
| ^ expected end of input
81+
""",
82+
"\(error)"
83+
)
84+
}
85+
}
86+
87+
func testParserNegativeIntRawValue() throws {
88+
enum Person: Int, CaseIterable {
89+
case blob = -4
90+
case blobJr = -42
91+
}
92+
93+
let peopleParser = Many {
94+
Person.parser()
95+
} separator: {
96+
",".utf8
97+
} terminator: {
98+
End()
99+
}
100+
101+
var input = "-4,-42"[...].utf8
102+
XCTAssertEqual(try peopleParser.parse(&input), [.blob, .blobJr])
103+
104+
input = "-42,-4"[...].utf8
105+
XCTAssertEqual(try peopleParser.parse(&input), [.blobJr, .blob])
106+
107+
input = "-42,-100"[...].utf8
108+
XCTAssertThrowsError(try peopleParser.parse(&input)) { error in
109+
XCTAssertEqual(
110+
"""
111+
error: multiple failures occurred
112+
113+
error: unexpected input
114+
--> input:1:5
115+
1 | -42,-100
116+
| ^ expected case of "Person"
117+
118+
error: unexpected input
119+
--> input:1:4
120+
1 | -42,-100
121+
| ^ expected end of input
122+
""",
123+
"\(error)"
124+
)
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)