Corridor

1.0.3

Corridor lets you easily match URLs and extract their values
Nextdoor/corridor

What's New

Xcode 11 fix

2019-10-02T05:09:09Z

Corridor

Swift compatibility

Spend less time writing custom URL matching and parsing logic. Define any URL format you support using a single string, and let Corridor do the heavy lifting.

Suppose you want to handle a user-profile universal-link like: https://example.com/user/15127128.

(1) Create a route -- a Codable struct that contains all the values you want to extract from the URL. Once the URL is matched, the userId will have a value of 15127128

struct ViewUserProfile: CorridorRoute {
    let userId: Int
}

(2) Register the format: "/user/:userId{int}"

router.register("/user/:userId{int}", { try corridorResponse($0, ViewUserProfile.self) })

Now, for the usage:

(3) Call attemptMatch(_:URL) with the url https://example.com/user/15127128 to get a matched-response

let result = router.attemptMatch(url)

(4) Switch on the result's route:

let route = result.route
switch route {
  case let route as ViewUserProfile:
    print(route.userId)
  default:
    print("url not matched")
}

15127128 Voila!

Overview

See an example router that uses Corridor inside the Example folder

Route

A struct that conforms to CorridorRoute. Its variables correspond to values contained in a URL. It's created when a URL is matched.

Router

Routers facilitate the process of matching a URL to a route:

let router = CorridorRouter()

Let's register a type of URL:

router.register("/user/:userId{int}", { try corridorResponse($0, ViewUserProfile.self) })

ViewUserProfile is the struct that we defined in the first example

"/user/:userId{int}" is the URL format. It matches URLs that start with /user and have an integer value after.

{ try corridorResponse($0, ViewUserProfile.self) } is a block that populates the route ViewUserProfile if the URL is matched.

URL format

A URL format is split into a path and a query:

  • Path: includes all the URL values separated by "/"
  • Query: all the key-value pairs after the URL's last "/"

URL formats must start with a forward slash "/"

Formatting the path

Suppose you want to match URLs in the form /user/12891, that start with user and end with a number.

The URL format for this is: "/user/:userId{int}", where the number after user is represented as :userId{int}.

Each param you want to extract follows the form :paramName{baseType} or :paramName.

  • paramName is the same name as a variable in the route
  • baseType indicates the type of paramName

So :userId without an explicit base type would map to let userId: String in a route, while :userId{int} would map to let userId: Int in a route.

URL definitions must start with a forward slash "/"

Base type 'string' maps to a String

Base type 'int' maps to a Int

Formatting the query

Suppose you want to match URLs in the form /user?userId=12891, that begin with a user path, and have a numeric userId value in the params.

The URL format for this is: "/user/?:userId{int}", where everything after ? is part of the query.

Optionals:

Now suppose that the userId value was optional, so /user&userId=12891 and /user would map to the same route.

The URL format for this is: "/user/?:userId{int?}"

Literals:

Now suppose that we wanted to match URLs in the form /user&userId=12891&sourceType=email, where the ('sourceType', 'email') pair was present in the query.

The URL format for this is: "/user/?:userId{int}&:sourceType{'email'}"

URL formats must separate consecutive query params with an ampersand '&'

Support custom base types

The URL definition "/user/:userId{int}?:nickname{string}" contains built-in base types: "int", "string", and "bool". Each base type maps to a Swift type:

"int" -> Int

"string" -> String

"bool" -> Bool

Any base type can be enclosed by square brackets to represent a comma separated list of base values. Example: "[int]" -> [Int].

What if you want to use a base type that isn't one of the built-in ones? Here's an example:

(1) Define a base type named "uint" that converts values to UInt:

private struct CorridorTypeUInt: CorridorTypeProtocol {
    let name = "uint"
    func convertToType(from value: String) -> Any? {
      return UInt(value)
    }
}

Base type names must be alphabetic, and cannot be identical to any of the existing built-in base type names

(2) Instantiate CorridorTypes with the custom base type:

let baseTypes = CorridorTypes(customTypes: [CorridorTypeUInt()])

(3) Instantiate the router with the baseTypes

let router = CorridorRouter(corridorTypes: baseTypes)

Now, for the usage:

struct ViewUserProfile: CorridorRoute {
    let userId: UInt
    let nickname: String?
}
router.register("/user/:userId{uint}?:nickname{string}", { try corridorResponse($0, ViewUserProfile.self) })

(4) Call attemptMatch(_:URL) with the url www.example.com/8971/?nickname=Bob

let result = router.attemptMatch(url)
let route = routeResponse.route as! ViewUserProfile
print(route.userId)

8971 Voila!

Match global query params

What if a query parameter such as a tracking id, or user id is contained in many of your URLs?

Suppose your URLs contained a query param sourceUserId to track the user who clicked on the URL.

You could include let sourceUserId: String? in all your routes, but doing so is tedious and error-prone. CorridorGlobalParams to the rescue!

(1) Create a struct that conforms to CorridorGlobalParams

struct GlobalParams: CorridorGlobalParams {
  let sourceUserId: String?
}

(2) Create a mapping

let mapping = GlobalQueryOptionalParamsMapping(params: ["sourceUserId"],
                                                       decoder: { try corridorGlobalParamsResponse($0, GlobalParams.self) })

(3) Instantiate the router with the mapping

let router = CorridorRouter(globalQueryOptionalParamsMapping: mapping)

Now, for the usage:

(4) Call attemptMatch(_:URL) with the url www.example.com/news_feed/?sourceUserId=abc123

let result = router.attemptMatch(url)
let globalParams = routeResponse.globalParams as! GlobalParams
print(globalParams.sourceUserId)

abc123 Voila!

Installation

Corridor supports multiple methods for installing the library in a project.

CocoaPods

To integrate Corridor into your Xcode project using CocoaPods, specify it in your Podfile:

pod 'Corridor','~> 1.0.0'

Also include on a separate line in your Podfile:

use_frameworks!

Carthage

To integrate Corridor into your Xcode project using Carthage, specify it in your Cartfile:

github "Nextdoor/corridor" ~> 1.0.0'

Run carthage to build the framework and drag the built Corridor.framework into your Xcode project.

Swift Package Manager

To integrate Corridor into your Xcode project using Swift Package Manager, add it to the dependencies value of your Package.swift:

dependencies: [
    .package(url: "https://github.com/Nextdoor/corridor.git", from: "1.0.0")
]

Contribute

  • If you have a feature request or discover a bug, open an issue
  • If you'd like to contribute changes, submit a pull request

License

Corridor is released under the Apache 2.0 license. See LICENSE for details.

Description

  • Swift Tools 5.0.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sat Oct 19 2024 10:52:05 GMT-0900 (Hawaii-Aleutian Daylight Time)