Skip to content

Releases: RougeWare/Swift-Either

2.0 • Raw value decoding

Choose a tag to compare

@KyNorthstar KyNorthstar released this 23 Dec 05:32
89bdbb8

Breaking Changes

While this update preserves existing decoding paths while adding an additional path, it does remove an error which might have previously been thrown.

In case any users of this library depended on that error to validate encoded data which doesn't match their desired format, this change is considered breaking and requires a MAJOR version bump.

Summary

Now Either can be used to decode payloads which encode different types in the same field, without wrapping those fields in "left"/"right" sub-objects.

The decoder now attempts to interpret a single‑value container as the left type, and if that fails, the right type. Of course, if both fail to decode, then a NeitherLeftNorRightValueWasEncoded error is thrown just as always.

Either<String, Int>
{ "value": "foo" }          // decodes to either.left("foo")
{ "value": 42 }             // decodes to either.right(42)

Usage notes

Since this only depends on which types the field can encode as, this will always decode as .left when the left and right types of the Either are the same:

Either<String, String>
{ "value": "foo" }          // decodes to either.left("foo")
{ "value": "baz" }          // decodes to either.left("baz")

1.1 • `Sendable` autoconformance

Choose a tag to compare

@KyNorthstar KyNorthstar released this 27 Apr 00:57
6a80178

This simple change makes Either declare conformance to Sendable when both Left and Right conform to Sendable.

It ain't much, but it means a lot if you want to use this across concurrency barriers

1.0 • MVP

Choose a tag to compare

@KyNorthstar KyNorthstar released this 22 Sep 21:08
8e38bf1

Yet Another Either Type! 🥳

Did the world need another Swift Either type? No.
Are We suffering from Not-Invented-Here Syndrome? Maybe.
Did We still think this was a good idea? Definitely.

Either is a concept in many functional and strongly-typed languages which allows a value of either one type or another, to be stored in one field:

/// A response for the population query 
struct PopulationResponse {

    /// The list of people in the population
    ///
    /// - Note: In 1.x, this was a list of names as `String`s.
    ///         In 2.x and newer, this is a map of UUIDs to `Person` objects
    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 leftness and rightness 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:

    {
        "either": {
            "left": {
                "name": "Dax",
                "favoriteColor": 6765239
            }
        }
    }

    or:

    {
        "either": {
            "right": 42
        }
    }

Unwrapping

Obviously you gotta eventually get a value out of this, and it offers a few approaches:

  • left – If the Either is a .left, then that value is returned, else nil
  • right – If the Either is a .right, then that value is returned, else nil

When both Left and Right are the same type, then these are also available:

  • value – The current value, disregarding whether that value is .left or .right
  • * – Inspired by the semantics of dereferencing a pointer in C (and because Swift doesn't allow custom postfix !), place this before the Either instance for the same behavior as calling `.value:
    func name(_ user: Either<Person, Person>) -> String {
        return (*user).name
    }
  • Value – Since both positions are the same type, this typealias allows you to reference that type without specifically using Left or Right:
    typealias LegacyOrMigratedUser = Either<User, User>
    
    func account(of user: LegacyOrMigratedUser) -> LegacyOrMigratedUser.Value.Account {
        (*user).account
    }

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<Right>, and vice versa any Optional can be turned into an Either<Void, Wrapped>. Just pass one to the initializer of the other:

    let either = Either<Void, String>.right("I'm valued")
    let optional = Optional(either)
    print(optional!) // Prints `I'm valued`
    var optional: String? = nil
    var either = Either<Void, _>(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:

    let either = Either<Data, Error>.left(Data(base64Encoded: "SG93ZHk=")!)
    let result = Result(either)
    print(result) // Prints `success(5 bytes)`
    var result = Result<Data, Error>(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})`

  • 1.0.1 – Retroactive application of the orignally-intended and accidentally-omitted MIT license