Last active
October 30, 2024 19:30
-
-
Save chriseidhof/159d4606fd3986ee7c7d64be38b6b0da to your computer and use it in GitHub Desktop.
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
// | |
// 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