Skip to content

Commit

Permalink
Add parser for case-iterable, string raw-representable values (#176)
Browse files Browse the repository at this point in the history
* 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
5 people authored Mar 7, 2022
1 parent 5189e0e commit 1a0050f
Show file tree
Hide file tree
Showing 3 changed files with 343 additions and 0 deletions.
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 Sources/Parsing/Parsers/CaseIterableRawRepresentable.swift
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 Tests/ParsingTests/CaseIterableRawRepresentableTests.swift
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)"
)
}
}
}

0 comments on commit 1a0050f

Please sign in to comment.