Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Last active August 26, 2018 00:16
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chriseidhof/159d4606fd3986ee7c7d64be38b6b0da to your computer and use it in GitHub Desktop.
Save chriseidhof/159d4606fd3986ee7c7d64be38b6b0da to your computer and use it in GitHub Desktop.
//
// main.swift
// RoutingApproaches
//
// Created by Chris Eidhof on 01.08.18.
// Copyright © 2018 objc.io. All rights reserved.
//
import Foundation
// A *description* of a choice between two routes A and B
struct Sum<A, B> {
let l: A
let r: B
init(_ l: A, _ r: B) {
self.l = l
self.r = r
}
}
// A *description* of a two parts of a route in sequence
struct Prod<A, B> {
let a: A
let b: B
init(_ a: A, _ b: B) {
self.a = a
self.b = b
}
}
// A description of a constant label
struct Constant {
let name: String
init(_ name: String) {
self.name = name
}
}
// A description of a single parameter A
struct P<A> { }
// A route representing ()
struct Unit {}
enum Either<A, B> { case left(A); case right(B) }
// All of Sum, Prod, Label, K and Unit conform to Endpoint
protocol Endpoint {
associatedtype Result
func parse(_: inout [String]) -> Result?
func pretty(_: Result) -> [String]
}
extension Sum: Endpoint where A: Endpoint, B: Endpoint {
typealias Result = Either<A.Result, B.Result>
func parse(_ p: inout [String]) -> Either<A.Result, B.Result>? {
let saved = p
if let x = l.parse(&p), p.isEmpty {
return .left(x)
} else {
p = saved
return r.parse(&p).map(Either.right)
}
}
func pretty(_ x: Either<A.Result, B.Result>) -> [String] {
switch x {
case let .left(x): return l.pretty(x)
case let .right(x): return r.pretty(x)
}
}
}
extension Constant: Endpoint {
typealias Result = ()
func parse(_ p: inout [String]) -> Result? {
guard p.first == name else { return nil }
p.removeFirst()
return ()
}
func pretty(_ x: ()) -> [String] {
return [name]
}
}
extension P: Endpoint where A: Param {
typealias Result = A
func parse(_ p: inout [String]) -> Result? {
return A.parse(&p)
}
func pretty(_ x: A) -> [String] {
return x.pretty()
}
}
extension Prod: Endpoint where A: Endpoint, B: Endpoint {
typealias Result = (A.Result, B.Result)
func parse(_ p: inout [String]) -> Result? {
let state = p
guard let x = a.parse(&p), let y = b.parse(&p) else {
p = state
return nil
}
return (x,y)
}
func pretty(_ x: Result) -> [String] {
return a.pretty(x.0) + b.pretty(x.1)
}
}
extension Unit: Endpoint {
typealias Result = ()
func parse(_: inout [String]) -> ()? {
return ()
}
func pretty(_: ()) -> [String] {
return []
}
}
// Parameters
protocol Param {
static func parse(_ p: inout [String]) -> Self?
func pretty() -> [String]
}
extension Int: Param {
static func parse(_ p: inout [String]) -> Int? {
guard let x = p.first, let value = Int(x) else { return nil }
p.removeFirst()
return value
}
func pretty() -> [String] {
return ["\(self)"]
}
}
extension String: Param {
static func parse(_ p: inout [String]) -> String? {
guard let x = p.first else { return nil }
p.removeFirst()
return x
}
func pretty() -> [String] {
return [self] // todo escape
}
}
// In order for a type (e.g. YourRoute) to be Routable, it needs a Repr, and a way to convert to and from the Repr.Result
protocol Routable {
associatedtype Repr: Endpoint
static var repr: Repr { get }
var to: Repr.Result { get }
init(_ from: Repr.Result)
}
// Some convenience extensions
extension Routable {
var pretty: String {
return Self.repr.pretty(to).joined(separator: "/")
}
init?(_ p: String) {
var c = p.split(separator: "/", omittingEmptySubsequences: false).map(String.init)
guard let x = Self.repr.parse(&c) else { return nil }
self.init(x)
}
}
// Combinators:
precedencegroup Sum {
associativity: right
}
precedencegroup Prod {
higherThan: Sum
associativity: right
}
infix operator <|>: Sum
func <|><A,B>(lhs: A, rhs: B) -> Sum<A, B> {
return Sum(lhs, rhs)
}
infix operator </>: Prod
func </><A,B>(lhs: A, rhs: B) -> Prod<A, B> {
return Prod(lhs, rhs)
}
let string = P<String>()
let int = P<Int>()
// User-defined type describing the routes in an app
enum Route {
case home
case episodes
case episode(String)
case two(Int, Int)
}
extension Route: Routable {
// The routes, one per endpoint
static var repr: Repr {
return Constant("home") <|>
Constant("episodes") <|>
Constant("episodes") </> string <|>
Constant("two") </> int </> Constant("test") </> int
}
// This type can be inferred from `repr`. It's a description of the routes.
typealias Repr = Sum<Constant, Sum<Constant, Sum<Prod<Constant, P<String>>, Prod<Constant, Prod<P<Int>, Prod<Constant, P<Int>>>>>>>
// The `to` and `init` are mechanical to write, but I don't think writing them can be automated...
var to: Repr.Result {
switch self {
case .home: return .left(())
case .episodes: return .right(.left(()))
case let .episode(x): return .right(.right(.left(((), x))))
case let .two(x,y): return .right(.right(.right(((), (x, ((), y))))))
}
}
init(_ from: Repr.Result) {
switch from {
case .left(_): self = .home
case .right(.left(_)): self = .episodes
case let .right(.right(.left(((), x)))): self = .episode(x)
case let .right(.right(.right(((), (x,((), y)))))): self = .two(x, y)
}
}
}
print(Route.home.pretty)
print(Route.episodes.pretty)
print(Route.episode("one").pretty)
print(Route.two(5,6).pretty)
print(Route("home"))
print(Route("episodes"))
print(Route("episodes/5"))
print(Route("two/5/test/6"))
// Swift can use type-inference to figure out the type of z
let z = Constant("home") <|>
Constant("episodes") <|>
Constant("episodes") </> string <|>
Constant("two") </> int </> int </> P<Int>()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment