Created
February 24, 2020 16:15
-
-
Save DeFrenZ/ea0600ab504baba83fd268cfb683d5ea to your computer and use it in GitHub Desktop.
Type-safe Route
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
import Foundation | |
struct Route<T: PartialConvertible> { | |
let components: [Component] | |
enum Component { | |
case literal(String) | |
case component( | |
parse: (String, inout Partial<T>) -> Bool, | |
resolve: (T) -> String | |
) | |
} | |
} | |
@dynamicMemberLookup | |
struct Partial<T> { | |
private var storage: [PartialKeyPath<T>: Any] = [:] | |
subscript <V> (dynamicMember keyPath: KeyPath<T, V>) -> V? { | |
get { storage[keyPath] as? V } | |
set { storage[keyPath] = newValue } | |
} | |
} | |
protocol PartialConvertible { | |
init?(partial: Partial<Self>) | |
} | |
extension Route { | |
func parse(from string: String) -> T? { | |
var partial = Partial<T>() | |
let segments = string.split(separator: "/", omittingEmptySubsequences: true) | |
for (segment, component) in zip(segments, components) { | |
guard component.parse(from: String(segment), into: &partial) else { return nil } | |
} | |
return T(partial: partial) | |
} | |
func resolve(for value: T) -> String { | |
components | |
.map({ $0.resolve(for: value) }) | |
.map({ "/\($0)" }) | |
.joined() | |
} | |
} | |
extension Route.Component { | |
func parse(from segment: String, into partial: inout Partial<T>) -> Bool { | |
switch self { | |
case .literal(let string): | |
return segment == string | |
case .component(let parse, _): | |
return parse(segment, &partial) | |
} | |
} | |
func resolve(for value: T) -> String { | |
switch self { | |
case .literal(let string): | |
return string | |
case .component(_, let resolve): | |
return resolve(value) | |
} | |
} | |
} | |
extension Route: ExpressibleByStringLiteral { | |
init(stringLiteral: String) { | |
self.components = [.literal(stringLiteral)] | |
} | |
} | |
extension Route: ExpressibleByStringInterpolation { | |
init(stringInterpolation: StringInterpolation) { | |
self.components = stringInterpolation.components | |
} | |
struct StringInterpolation : StringInterpolationProtocol { | |
var components: [Route.Component] = [] | |
init(literalCapacity: Int, interpolationCount: Int) { | |
components.reserveCapacity(2 * interpolationCount + 1) | |
} | |
mutating func appendLiteral(_ string: String) { | |
let trimmed = string.trimmingCharacters(in: CharacterSet(charactersIn: "/")) | |
guard !trimmed.isEmpty else { return } | |
components.append(.literal(trimmed)) | |
} | |
mutating func appendInterpolation <S: LosslessStringConvertible> (_ keyPath: WritableKeyPath<T, S>) { | |
components.append(.component( | |
parse: { segment, partial in | |
guard let value = S(segment) else { return false } | |
partial[dynamicMember: keyPath] = value | |
return true | |
}, | |
resolve: { value in value[keyPath: keyPath].description } | |
)) | |
} | |
} | |
} | |
struct Params { | |
var user: Int | |
var product: Int | |
} | |
extension Params: PartialConvertible { | |
init?(partial: Partial<Self>) { | |
guard | |
let user = partial.user, | |
let product = partial.product | |
else { return nil } | |
self.init( | |
user: user, | |
product: product | |
) | |
} | |
} | |
let route: Route<Params> = "/user/\(\.user)/product/\(\.product)" | |
let path = route.resolve(for: Params(user: 42, product: 1)) | |
print(path) | |
let parsed = route.parse(from: path) | |
print(parsed!) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment