Skip to content

Instantly share code, notes, and snippets.

@DeFrenZ
Created February 24, 2020 16:15
Show Gist options
  • Save DeFrenZ/ea0600ab504baba83fd268cfb683d5ea to your computer and use it in GitHub Desktop.
Save DeFrenZ/ea0600ab504baba83fd268cfb683d5ea to your computer and use it in GitHub Desktop.
Type-safe Route
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