-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]> * Update Sources/Parsing/Documentation.docc/Articles/Parsers/CaseIterable.md Co-authored-by: Kth <[email protected]> * negatives * Update Sources/Parsing/Documentation.docc/Articles/Parsers/CaseIterable.md Co-authored-by: Kth <[email protected]> * docs Co-authored-by: Brandon Williams <[email protected]> Co-authored-by: Brandon Williams <[email protected]> Co-authored-by: Thomas Grapperon <[email protected]> Co-authored-by: Kth <[email protected]>
- Loading branch information
1 parent
5189e0e
commit 1a0050f
Showing
3 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
73 changes: 73 additions & 0 deletions
73
Sources/Parsing/Documentation.docc/Articles/Parsers/CaseIterable.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# CaseIterable | ||
|
||
A parser that consumes a case-iterable, raw representable value from the beginning of a string. | ||
|
||
Given a type that conforms to `CaseIterable` and `RawRepresentable` with a `RawValue` of `String` | ||
or `Int`, we can incrementally parse a value of it. | ||
|
||
Notably, raw enumerations that conform to `CaseIterable` meet this criteria, so cases of the | ||
following type can be parsed with no extra work: | ||
|
||
```swift | ||
enum Role: String, CaseIterable { | ||
case admin | ||
case guest | ||
case member | ||
} | ||
|
||
try Parse { | ||
Int.parser() | ||
"," | ||
Role.parser() | ||
} | ||
.parse("123,member") // (123, .member) | ||
``` | ||
|
||
This also works with raw enumerations that are backed by integers: | ||
|
||
```swift | ||
enum Role: Int, CaseIterable { | ||
case admin = 1 | ||
case guest = 2 | ||
case member = 3 | ||
} | ||
|
||
try Parse { | ||
Int.parser() | ||
"," | ||
Role.parser() | ||
} | ||
.parse("123,1") // (123, .admin) | ||
``` | ||
|
||
The `parser()` method on `CaseIterable` is overloaded to work on a variety of string representations | ||
in order to be as efficient as possible, including `Substring`, `UTF8View`, and more general | ||
collections of UTF-8 code units (see <doc:StringAbstractions> for more info). | ||
|
||
Typically Swift can choose the correct overload by using type inference based on what other parsers | ||
you are combining `parser()` with. For example, if you use `Role.parser()` with a | ||
`Substring` parser, like the literal "," parser in the above examples, Swift | ||
will choose the overload that works on substrings. | ||
|
||
On the other hand, if `Role.parser()` is used in a context where the input type cannot be inferred, | ||
then you will get an compiler error: | ||
|
||
```swift | ||
let parser = Parse { | ||
Int.parser() | ||
Role.parser() // 🛑 Ambiguous use of 'parser(of:)' | ||
} | ||
|
||
try parser.parse("123member") | ||
``` | ||
|
||
To fix this you can force one of the parsers to be the `Substring` parser, and then the | ||
other will figure it out via type inference: | ||
|
||
```swift | ||
let parser = Parse { | ||
Int.parser(of: Substring.self) | ||
Role.parser() | ||
} | ||
|
||
try parser.parse("123member") // (123, .member) |
143 changes: 143 additions & 0 deletions
143
Sources/Parsing/Parsers/CaseIterableRawRepresentable.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
extension CaseIterable where Self: RawRepresentable, RawValue == Int { | ||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a | ||
/// collection of a substring. | ||
/// | ||
/// See <doc:CaseIterable> for more info. | ||
/// | ||
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the | ||
/// interface that parses any collection of UTF-8 code units. | ||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning | ||
/// of a substring. | ||
@inlinable | ||
public static func parser( | ||
of inputType: Substring.Type = Substring.self | ||
) -> Parsers.CaseIterableRawRepresentableParser<Substring, Self, String> { | ||
.init(toPrefix: { String($0) }, areEquivalent: ==) | ||
} | ||
|
||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a | ||
/// collection of a substring's UTF-8 view. | ||
/// | ||
/// See <doc:CaseIterable> for more info. | ||
/// | ||
/// - Parameter inputType: The `Substring.UTF8View` type. This parameter is included to mirror the | ||
/// interface that parses any collection of UTF-8 code units. | ||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning | ||
/// of a substring's UTF-8 view. | ||
@inlinable | ||
public static func parser( | ||
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self | ||
) -> Parsers.CaseIterableRawRepresentableParser<Substring.UTF8View, Self, String.UTF8View> { | ||
.init(toPrefix: { String($0).utf8 }, areEquivalent: ==) | ||
} | ||
|
||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a | ||
/// collection of UTF-8 code units. | ||
/// | ||
/// - Parameter inputType: The collection type of UTF-8 code units to parse. | ||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning | ||
/// of a collection of UTF-8 code units. | ||
@inlinable | ||
public static func parser<Input>( | ||
of inputType: Input.Type = Input.self | ||
) -> Parsers.CaseIterableRawRepresentableParser<Input, Self, String.UTF8View> | ||
where | ||
Input.SubSequence == Input, | ||
Input.Element == UTF8.CodeUnit | ||
{ | ||
.init(toPrefix: { String($0).utf8 }, areEquivalent: ==) | ||
} | ||
} | ||
|
||
extension CaseIterable where Self: RawRepresentable, RawValue == String { | ||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a | ||
/// collection of a substring. | ||
/// | ||
/// See <doc:CaseIterable> for more info. | ||
/// | ||
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the | ||
/// interface that parses any collection of UTF-8 code units. | ||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning | ||
/// of a substring. | ||
@inlinable | ||
public static func parser( | ||
of inputType: Substring.Type = Substring.self | ||
) -> Parsers.CaseIterableRawRepresentableParser<Substring, Self, String> { | ||
.init(toPrefix: { $0 }, areEquivalent: ==) | ||
} | ||
|
||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a | ||
/// collection of a substring's UTF-8 view. | ||
/// | ||
/// See <doc:CaseIterable> for more info. | ||
/// | ||
/// - Parameter inputType: The `Substring.UTF8View` type. This parameter is included to mirror the | ||
/// interface that parses any collection of UTF-8 code units. | ||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning | ||
/// of a substring's UTF-8 view. | ||
@inlinable | ||
public static func parser( | ||
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self | ||
) -> Parsers.CaseIterableRawRepresentableParser<Substring.UTF8View, Self, String.UTF8View> { | ||
.init(toPrefix: { $0.utf8 }, areEquivalent: ==) | ||
} | ||
|
||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a | ||
/// collection of UTF-8 code units. | ||
/// | ||
/// - Parameter inputType: The collection type of UTF-8 code units to parse. | ||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning | ||
/// of a collection of UTF-8 code units. | ||
@inlinable | ||
public static func parser<Input>( | ||
of inputType: Input.Type = Input.self | ||
) -> Parsers.CaseIterableRawRepresentableParser<Input, Self, String.UTF8View> | ||
where | ||
Input.SubSequence == Input, | ||
Input.Element == UTF8.CodeUnit | ||
{ | ||
.init(toPrefix: { $0.utf8 }, areEquivalent: ==) | ||
} | ||
} | ||
|
||
extension Parsers { | ||
public struct CaseIterableRawRepresentableParser< | ||
Input: Collection, Output: CaseIterable & RawRepresentable, Prefix: Collection | ||
>: Parser | ||
where | ||
Input.SubSequence == Input, | ||
Output.RawValue: Comparable, | ||
Prefix.Element == Input.Element | ||
{ | ||
@usableFromInline | ||
let cases: [(case: Output, prefix: Prefix, count: Int)] | ||
|
||
@usableFromInline | ||
let areEquivalent: (Input.Element, Input.Element) -> Bool | ||
|
||
@usableFromInline | ||
init( | ||
toPrefix: @escaping (Output.RawValue) -> Prefix, | ||
areEquivalent: @escaping (Input.Element, Input.Element) -> Bool | ||
) { | ||
self.areEquivalent = areEquivalent | ||
self.cases = Output.allCases | ||
.map { | ||
let prefix = toPrefix($0.rawValue) | ||
return ($0, prefix, prefix.count) | ||
} | ||
.sorted(by: { $0.count > $1.count }) | ||
} | ||
|
||
@inlinable | ||
public func parse(_ input: inout Input) throws -> Output { | ||
for (`case`, prefix, count) in self.cases { | ||
if input.starts(with: prefix, by: self.areEquivalent) { | ||
input.removeFirst(count) | ||
return `case` | ||
} | ||
} | ||
throw ParsingError.expectedInput("case of \"\(Output.self)\"", at: input) | ||
} | ||
} | ||
} |
127 changes: 127 additions & 0 deletions
127
Tests/ParsingTests/CaseIterableRawRepresentableTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import Parsing | ||
import XCTest | ||
|
||
final class CaseIterableRawRepresentableTests: XCTestCase { | ||
func testParserStringRawValue() throws { | ||
enum Person: String, CaseIterable { | ||
case blob = "Blob" | ||
case blobJr = "Blob Jr" | ||
} | ||
|
||
let peopleParser = Many { | ||
Person.parser() | ||
} separator: { | ||
",".utf8 | ||
} terminator: { | ||
End() | ||
} | ||
|
||
var input = "Blob,Blob Jr"[...].utf8 | ||
XCTAssertEqual(try peopleParser.parse(&input), [.blob, .blobJr]) | ||
|
||
input = "Blob Jr,Blob"[...].utf8 | ||
XCTAssertEqual(try peopleParser.parse(&input), [.blobJr, .blob]) | ||
|
||
input = "Blob,Mr Blob"[...].utf8 | ||
XCTAssertThrowsError(try peopleParser.parse(&input)) { error in | ||
XCTAssertEqual( | ||
""" | ||
error: multiple failures occurred | ||
error: unexpected input | ||
--> input:1:6 | ||
1 | Blob,Mr Blob | ||
| ^ expected case of "Person" | ||
error: unexpected input | ||
--> input:1:5 | ||
1 | Blob,Mr Blob | ||
| ^ expected end of input | ||
""", | ||
"\(error)" | ||
) | ||
} | ||
} | ||
|
||
func testParserIntRawValue() throws { | ||
enum Person: Int, CaseIterable { | ||
case blob = 4 | ||
case blobJr = 42 | ||
} | ||
|
||
let peopleParser = Many { | ||
Person.parser() | ||
} separator: { | ||
",".utf8 | ||
} terminator: { | ||
End() | ||
} | ||
|
||
var input = "4,42"[...].utf8 | ||
XCTAssertEqual(try peopleParser.parse(&input), [.blob, .blobJr]) | ||
|
||
input = "42,4"[...].utf8 | ||
XCTAssertEqual(try peopleParser.parse(&input), [.blobJr, .blob]) | ||
|
||
input = "42,100"[...].utf8 | ||
XCTAssertThrowsError(try peopleParser.parse(&input)) { error in | ||
XCTAssertEqual( | ||
""" | ||
error: multiple failures occurred | ||
error: unexpected input | ||
--> input:1:4 | ||
1 | 42,100 | ||
| ^ expected case of "Person" | ||
error: unexpected input | ||
--> input:1:3 | ||
1 | 42,100 | ||
| ^ expected end of input | ||
""", | ||
"\(error)" | ||
) | ||
} | ||
} | ||
|
||
func testParserNegativeIntRawValue() throws { | ||
enum Person: Int, CaseIterable { | ||
case blob = -4 | ||
case blobJr = -42 | ||
} | ||
|
||
let peopleParser = Many { | ||
Person.parser() | ||
} separator: { | ||
",".utf8 | ||
} terminator: { | ||
End() | ||
} | ||
|
||
var input = "-4,-42"[...].utf8 | ||
XCTAssertEqual(try peopleParser.parse(&input), [.blob, .blobJr]) | ||
|
||
input = "-42,-4"[...].utf8 | ||
XCTAssertEqual(try peopleParser.parse(&input), [.blobJr, .blob]) | ||
|
||
input = "-42,-100"[...].utf8 | ||
XCTAssertThrowsError(try peopleParser.parse(&input)) { error in | ||
XCTAssertEqual( | ||
""" | ||
error: multiple failures occurred | ||
error: unexpected input | ||
--> input:1:5 | ||
1 | -42,-100 | ||
| ^ expected case of "Person" | ||
error: unexpected input | ||
--> input:1:4 | ||
1 | -42,-100 | ||
| ^ expected end of input | ||
""", | ||
"\(error)" | ||
) | ||
} | ||
} | ||
} |