diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c0c8fb4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,11 @@ +# These are supported funding model platforms + +github: KyLeggiero # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +open_collective: # Replace with a single Open Collective username +ko_fi: BlueHuskyStudios # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://PayPal.me/KyLeggiero # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..99204ad --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,15 @@ +name: Tests + +on: [push, pull_request] + +jobs: + build: + + runs-on: macOS-latest + + steps: + - uses: actions/checkout@v1 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v diff --git a/README.md b/README.md index c103f98..6542234 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +[![Tested on GitHub Actions](https://github.com/RougeWare/swift-either/actions/workflows/swift.yml/badge.svg)](https://github.com/RougeWare/swift-either/actions/workflows/swift.yml) [![](https://www.codefactor.io/repository/github/rougeware/swift-either/badge)](https://www.codefactor.io/repository/github/rougeware/swift-either) + +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRougeWare%2FSwift-Either%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/RougeWare/Swift-Either) [![swift package](https://img.shields.io/badge/swift%20package-brightgreen.svg)](https://swift.org/package-manager) [![Supports macOS, iOS, tvOS, watchOS, Linux, & Windows](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRougeWare%2FSwift-Either%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/RougeWare/Swift-Either) +[![](https://img.shields.io/github/release-date/rougeware/swift-either?label=latest%20release)](https://github.com/RougeWare/Swift-Either/releases/latest) + + + # Yet Another `Either` Type! 🥳 Did the world need another Swift `Either` type? No. @@ -17,3 +24,96 @@ struct PopulationResponse { let people: Either<[String], [UUID: Person]> } ``` + +This implementation brings a few advantages: + + + +## Automatic Conformance + +This automatically conforms an instance of `Either` to various protocols if its `Left` and `Right` types also conform to them. + +Currently, these are supported: + +- **`Equatable` – Brings `==` and `!=`.** When `Left` and `Right` are unequal types, this considers `left` to never equal `right`. When those types are equal, this ignores the `left`ness and `right`ness positions + +- **`Comparable` – Brings `<`, `<=`, `>=`, and `>`.** When the positions are unequal types, this considers `left` to never be less than nor greater than `right`. When those types are equal, this ignores the positions + +- **`Hashable`** – Allows an instance of `Either` to transparently be given the same hash as whatever value it contains + +- **`CustomStringConvertible` – Provides a `.description`** field with the same value as the `Either`'s contained value's `.description` field + +- **`CustomDebugStringConvertible` – Provides a `.debugDescription`** field with the same value as the `Either`'s contained value's `.debugDescription` field + +- **`Codable` – Allows `Either` instances to be encoded.** This results in a multi-value keyed container which only ever contains one key-value pair where the key is `"left"` or `"right"`, and the value is whatever the instance's value encodes to: + ```json + { + "either": { + "left": { + "name": "Dax", + "color": 6765239 + } + } + } + ``` + or: + ```json + { + "either": { + "right": 42 + } + } + ``` + + + +## Mapping + +This provides various approaches for mapping an `Either`. Generally these consider it a collection of exactly one element, similarly to how `Optional` is treated as a collection of exactly 0 or 1 elements. + + +- **`map(left:right:)`** — Map both positions of this `either` to different values/types, regardless of its current value. Only one of these callbacks is called each time this function is called (the one mapping a value), but this allows you to reuse the same call many times to map both sides depending on which one is set. + +- **`map(left:)`** – Map only the `Left` position of this `either` to a different value/type. The callback is only called when this `either` is a `.left` + +- **`map(right:)`** – Map only the `Right` position. Inverse of `map(left:)` + + + +## Conversions + +This allows you to convert instances of some types into `Either` and back: + +- **`Optional`** – Any `Either` whose `Left` is `Void` can be turned into an `Optional`, and vice versa any `Optional` can be turned into an `Either`. Just pass one to the initializer of the other: + ```swift + let either = Either.right("I'm valued") + let optional = Optional(either) + print(optional!) // Prints `I'm valued` + ``` + + ```swift + var optional: String? = nil + var either = Either(optional) + print(either) // Prints `left()` + + optional = "I'm not sorry" + either = .init(optional) + print(either) // Prints `right("I\'m not sorry")` + ``` + +- **`Result`** – When `Either`'s `Rigth` is an `Error`, you can convert it to and from a `Result` similarly to the above `Optional` conversions: + ```swift + let either = Either.left(Data(base64Encoded: "SG93ZHk=")!) + let result = Result(either) + print(result) // Prints `success(5 bytes)` + ``` + + ```swift + var result = Result(catching: { try Data(contentsOf: URL(string: "https://example.com")!) }) + var either = Either<_, Error>(result) + print(either) // Prints `left(1256 bytes)` + + result = .init(catching: { try Data(contentsOf: URL(string: "https://fakeDomain.fakeTld")!) }) + either = .init(result) + print(either) // Prints `right(Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." UserInfo={NSURL=https://fakeDomain.fakeTld})` + ```