Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Router generic over its route 🚏 #240

Merged
merged 1 commit into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions Alicerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
0A9AF8BC1FC3242E0076458E /* SpecializedGenericTestView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0A9AF8BB1FC3242E0076458E /* SpecializedGenericTestView.xib */; };
0A9AF8C01FC336F60076458E /* ReusableViewTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9AF8BF1FC336F60076458E /* ReusableViewTestCase.swift */; };
0A9AF8C21FC33B070076458E /* ReusableViewCollectionViewTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9AF8C11FC33B070076458E /* ReusableViewCollectionViewTestCase.swift */; };
0A9E25BC26ADE3100096D006 /* Routable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9E25BA26ADE1030096D006 /* Routable.swift */; };
0AA061002273B4F80052B4A1 /* TestNIBCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F19A028822664D2A00650D93 /* TestNIBCollectionViewCell.swift */; };
0AA061012273B4F80052B4A1 /* TestNIBTableHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D1F608226620D800CC46B3 /* TestNIBTableHeaderFooterView.swift */; };
0AA061022273B4F80052B4A1 /* TestNIBCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F19A028C22664EC300650D93 /* TestNIBCollectionReusableView.swift */; };
Expand Down Expand Up @@ -551,6 +552,7 @@
0A9AF8BB1FC3242E0076458E /* SpecializedGenericTestView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SpecializedGenericTestView.xib; sourceTree = "<group>"; };
0A9AF8BF1FC336F60076458E /* ReusableViewTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableViewTestCase.swift; sourceTree = "<group>"; };
0A9AF8C11FC33B070076458E /* ReusableViewCollectionViewTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableViewCollectionViewTestCase.swift; sourceTree = "<group>"; };
0A9E25BA26ADE1030096D006 /* Routable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routable.swift; sourceTree = "<group>"; };
0AA4EBA6264598AE00616FB3 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = "<group>"; };
0AA4EBA72645E96900616FB3 /* Gemfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile; sourceTree = "<group>"; };
0AB34A052085385A001F2979 /* UnfairLockTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfairLockTestCase.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -829,6 +831,7 @@
isa = PBXGroup;
children = (
0A3C2C831EA7E18500EFB7D4 /* ApplicationRouter.swift */,
0A9E25BA26ADE1030096D006 /* Routable.swift */,
0A3C2C861EA7E18500EFB7D4 /* Route.swift */,
0A3C2C841EA7E18500EFB7D4 /* Route+Component.swift */,
0AE188C022E49DFF00153A36 /* Route+TrieNode.swift */,
Expand Down Expand Up @@ -1652,7 +1655,7 @@
0A7B505620B72192005A08E7 = {
CreatedOnToolsVersion = 9.3.1;
LastSwiftMigration = 1020;
ProvisioningStyle = Manual;
ProvisioningStyle = Automatic;
};
0A908667217767B200E76280 = {
LastSwiftMigration = 1020;
Expand Down Expand Up @@ -1961,6 +1964,7 @@
0A708F6620E97B6E001784DA /* AnyAnalyticsTracker.swift in Sources */,
1B667A0A20127C1600A8CD5A /* StackOrchestrator+Store.swift in Sources */,
0A708F6220E96CD1001784DA /* Analytics.swift in Sources */,
0A9E25BC26ADE3100096D006 /* Routable.swift in Sources */,
0AD7F24820A9D1D000CC927E /* ServerTrustEvaluator.swift in Sources */,
0A132EB724842EFE00FC108A /* StackOrchestratorStore.swift in Sources */,
0A3C2DBA1EA7E5DD00EFB7D4 /* TableViewHeaderFooterView.swift in Sources */,
Expand Down Expand Up @@ -2342,8 +2346,8 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
Expand Down Expand Up @@ -2380,8 +2384,8 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEVELOPMENT_TEAM = "";
ENABLE_NS_ASSERTIONS = NO;
Expand All @@ -2393,6 +2397,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.mindera.alicerce.DummyHostApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
Expand Down
12 changes: 12 additions & 0 deletions Sources/DeepLinking/Routable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

/// A type representing something that can be routed (via a URL).
public protocol Routable {

var route: URL { get }
}

extension URL: Routable {

public var route: URL { self }
}
22 changes: 11 additions & 11 deletions Sources/DeepLinking/Route+TrieRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,23 @@ extension Route {
/// A URL router that is backed by a **trie** tree and forwards route handling to registered handlers.
///
/// Routes are registered with an associated handler, which on match handles the event and optionally invokes a
/// completion closure with an abitrary payload of type `T`. This allows handlers to perform asynchronous work even
/// completion closure with an arbitrary payload of type `T`. This allows handlers to perform asynchronous work even
/// though route matching is made synchronously.
///
/// - Remark: Access to the backing trie tree data structure *is* synchronized, so all operations can safely be
/// called from different threads.
///
/// - Note: https://en.wikipedia.org/wiki/Trie for more information.
public final class TrieRouter<T>: Router {
public final class TrieRouter<R: Routable, T>: Router {

/// A type representing the router's trie tree node.
fileprivate typealias TrieNode = Route.TrieNode<AnyRouteHandler<T>>
fileprivate typealias TrieNode = Route.TrieNode<AnyRouteHandler<R, T>>

/// A type representing a route to match.
private typealias MatchRoute = (components: [String], queryItems : [URLQueryItem])

/// A type representing a matched route,.
private typealias Match = (parameters: Route.Parameters, handler: AnyRouteHandler<T>)
private typealias Match = (parameters: Route.Parameters, handler: AnyRouteHandler<R, T>)

/// The router's trie tree.
fileprivate var trie: Atomic<TrieNode> = Atomic(TrieNode())
Expand All @@ -84,9 +84,9 @@ extension Route {
/// - route: The route to register.
/// - handler: The handler to associate with the route and handle it on match.
/// - Throws: A `TrieRouterError` error if the route is invalid or a conflict exists.
public func register(_ route: URL, handler: AnyRouteHandler<T>) throws {
public func register(_ route: R, handler: AnyRouteHandler<R, T>) throws {

let routeComponents = try parseAnnotatedRoute(route)
let routeComponents = try parseAnnotatedRoute(route.route)

try trie.modify { node in

Expand Down Expand Up @@ -115,9 +115,9 @@ extension Route {
/// - Throws: A `TrieRouterError` error if the route is invalid or wasn't found.
/// - Returns: The unregistered handler associated with the route.
@discardableResult
public func unregister(_ route: URL) throws -> AnyRouteHandler<T> {
public func unregister(_ route: R) throws -> AnyRouteHandler<R, T> {

let routeComponents = try parseAnnotatedRoute(route)
let routeComponents = try parseAnnotatedRoute(route.route)

return try trie.modify { node in

Expand All @@ -141,9 +141,9 @@ extension Route {
/// - route: The route to route.
/// - handleCompletion: The closure to notify routing success with custom payload from the route handler.
/// - Throws: A `TrieRouterError` error if the route wasn't found.
public func route(_ route: URL, handleCompletion: ((T) -> Void)? = nil) throws {
public func route(_ route: R, handleCompletion: ((T) -> Void)? = nil) throws {

let (pathComponents, queryItems) = try parseMatchRoute(route)
let (pathComponents, queryItems) = try parseMatchRoute(route.route)

let match: Match = try trie.withValue { node in

Expand Down Expand Up @@ -246,4 +246,4 @@ private extension Optional where Wrapped == String {
}

@available(*, unavailable, renamed: "Route.TrieRouter")
public typealias TreeRouter<Handler> = Route.TrieRouter<Handler>
public typealias TreeRouter<Handler> = Route.TrieRouter<URL, Handler>
36 changes: 21 additions & 15 deletions Sources/DeepLinking/Router.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import Foundation

/// A type representing a URL router.
/// A type representing a (URL-based) router.
public protocol Router {

/// A router's route type.
associatedtype R: Routable

/// A router's custom payload type to be passed when a route is routed.
associatedtype T

/// Routes the given route, and optionally notifies routing success with a custom payload.
/// Routes the given route and optionally notifies routing success with a custom payload.
///
/// - Parameters:
/// - route: The route to route.
/// - handleCompletion: The closure to notify routing success with custom payload.
/// - Throws: An error if the route couldn't be routed.
func route(_ route: URL, handleCompletion: ((T) -> Void)?) throws
func route(_ route: R, handleCompletion: ((T) -> Void)?) throws
}

/// A type that handles URL routes form a router.
/// A type that handles (URL-based) routes from a router.
public protocol RouteHandler {

/// A handler's (router) route type.
associatedtype R

/// A handler's custom payload type to be passed when a route is handled.
associatedtype T

Expand All @@ -34,14 +40,19 @@ public protocol RouteHandler {
/// - parameters: The parameters captured by the router.
/// - queryItems: The query items contained in the route.
/// - completion: The closure to notify handle completion with custom payload.
func handle(route: URL, parameters: Route.Parameters, queryItems: [URLQueryItem], completion: ((T) -> Void)?)
func handle(route: R, parameters: Route.Parameters, queryItems: [URLQueryItem], completion: ((T) -> Void)?)
}

extension RouteHandler {

public func eraseToAnyRouteHandler() -> AnyRouteHandler<R, T> { .init(self) }
}

/// A type-erased URL route handler.
public final class AnyRouteHandler<T>: RouteHandler {
/// A type-erased (URL-based) route handler.
public final class AnyRouteHandler<R, T>: RouteHandler {

/// The type-erased handler's wrapped instance `handle` method, stored as a closure.
private let _handle: (URL, Route.Parameters, [URLQueryItem], ((T) -> Void)?) -> Void
private let _handle: (R, Route.Parameters, [URLQueryItem], ((T) -> Void)?) -> Void

/// The type-erased tracker's wrapped instance.
private let _wrapped: Any
Expand All @@ -50,7 +61,7 @@ public final class AnyRouteHandler<T>: RouteHandler {
///
/// - Parameters:
/// - handler: The route handler instance to wrap.
public init<H: RouteHandler>(_ handler: H) where H.T == T {
public init<H: RouteHandler>(_ handler: H) where H.R == R, H.T == T {

_handle = handler.handle
_wrapped = handler
Expand All @@ -70,12 +81,7 @@ public final class AnyRouteHandler<T>: RouteHandler {
/// - parameters: The parameters captured by the router.
/// - queryItems: The query items contained in the route.
/// - completion: The closure to notify handle completion with custom payload.
public func handle(
route: URL,
parameters: Route.Parameters,
queryItems: [URLQueryItem],
completion: ((T) -> Void)?
) {
public func handle(route: R, parameters: Route.Parameters, queryItems: [URLQueryItem], completion: ((T) -> Void)?) {

_handle(route, parameters, queryItems, completion)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import XCTest

class Route_TrieRouter_DescriptionTests: XCTestCase {

struct Payload {}

struct TestHandler: RouteHandler, CustomStringConvertible {

let tag: String
Expand All @@ -11,13 +13,13 @@ class Route_TrieRouter_DescriptionTests: XCTestCase {
route: URL,
parameters: [String : String],
queryItems: [URLQueryItem],
completion: ((String) -> Void)?
completion: ((Payload) -> Void)?
) {}

public var description: String { return tag }
public var description: String { tag }
}

typealias TestRouter = Route.TrieRouter<String>
typealias TestRouter = Route.TrieRouter<URL, Payload>

func testDescription_ShouldMatchValues() {

Expand Down Expand Up @@ -59,87 +61,87 @@ class Route_TrieRouter_DescriptionTests: XCTestCase {
"""
├──┬ schemeb
│ ├──┬ hosta
│ │ └──● AnyRouteHandler<String>(P)
│ │ └──● AnyRouteHandler<URL, Payload>(P)
│ │
│ ├──┬ hostb
│ │ └──┬ path
│ │ ├──┬ *
│ │ │ └──● AnyRouteHandler<String>(R)
│ │ │ └──● AnyRouteHandler<URL, Payload>(R)
│ │ │
│ │ └──● AnyRouteHandler<String>(Q)
│ │ └──● AnyRouteHandler<URL, Payload>(Q)
│ │
│ └──┬ *
│ └──● AnyRouteHandler<String>(O)
│ └──● AnyRouteHandler<URL, Payload>(O)
├──┬ schemea
│ ├──┬ hostc
│ │ └──┬ *
│ │ └──┬ yet
│ │ └──┬ another
│ │ ├──┬ path
│ │ │ └──● AnyRouteHandler<String>(K)
│ │ │ └──● AnyRouteHandler<URL, Payload>(K)
│ │ │
│ │ ├──┬ :parameterA
│ │ │ └──┬ *
│ │ │ └──● AnyRouteHandler<String>(L)
│ │ │ └──● AnyRouteHandler<URL, Payload>(L)
│ │ │
│ │ └──┬ **
│ │ └──● AnyRouteHandler<String>(M)
│ │ └──● AnyRouteHandler<URL, Payload>(M)
│ │
│ └──┬ *
│ └──┬ **catchAll
│ └──● AnyRouteHandler<String>(N)
│ └──● AnyRouteHandler<URL, Payload>(N)
└──┬ *
└──┬ *
├──┬ some
│ └──┬ path
│ ├──┬ *
│ │ └──┬ :parameterA
│ │ └──● AnyRouteHandler<String>(C)
│ │ └──● AnyRouteHandler<URL, Payload>(C)
│ │
│ ├──┬ **
│ │ └──● AnyRouteHandler<String>(B)
│ │ └──● AnyRouteHandler<URL, Payload>(B)
│ │
│ └──● AnyRouteHandler<String>(A)
│ └──● AnyRouteHandler<URL, Payload>(A)
├──┬ host
│ └──┬ another
│ └──┬ :parameterA
│ └──┬ :parameterB
│ └──┬ **parameterC
│ └──● AnyRouteHandler<String>(F)
│ └──● AnyRouteHandler<URL, Payload>(F)
├──┬ hostA
│ └──┬ another
│ ├──┬ path
│ │ └──● AnyRouteHandler<String>(D)
│ │ └──● AnyRouteHandler<URL, Payload>(D)
│ │
│ └──┬ :parameterA
│ └──┬ :parameterB
│ └──● AnyRouteHandler<String>(E)
│ └──● AnyRouteHandler<URL, Payload>(E)
└──┬ hostB
└──┬ :parameterA
├──┬ before
│ └──┬ path
│ └──● AnyRouteHandler<String>(G)
│ └──● AnyRouteHandler<URL, Payload>(G)
├──┬ :parameterB
│ ├──┬ path
│ │ └──┬ *
│ │ └──● AnyRouteHandler<String>(H)
│ │ └──● AnyRouteHandler<URL, Payload>(H)
│ │
│ └──┬ **
│ └──● AnyRouteHandler<String>(I)
│ └──● AnyRouteHandler<URL, Payload>(I)
└──┬ *
└──┬ path
└──┬ *
└──● AnyRouteHandler<String>(J)
└──● AnyRouteHandler<URL, Payload>(J)
"""
)
}

private func testHandler(_ tag: String) -> AnyRouteHandler<String> { return AnyRouteHandler(TestHandler(tag: tag)) }
private func testHandler(_ tag: String) -> AnyRouteHandler<URL, Payload> { AnyRouteHandler(TestHandler(tag: tag)) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ final class TestHandler: RouteHandler {

class Route_TrieRouter_RegisterTests: XCTestCase {

typealias TestRouter = Route.TrieRouter<HandledRoute>
typealias TestRouter = Route.TrieRouter<URL, HandledRoute>
typealias TestRouteTrieNode = Route.TrieNode<TestHandler>

var testHandler = AnyRouteHandler<HandledRoute>(TestHandler())
var testHandler = AnyRouteHandler(TestHandler())

// MARK: - failure

Expand Down
Loading