Skip to content

Commit

Permalink
Merge pull request #19 from vapor/typesafe-rework
Browse files Browse the repository at this point in the history
Parameterizable and type safe routing rework
  • Loading branch information
loganwright authored May 9, 2017
2 parents e8cea1b + 82400d5 commit 11db47f
Show file tree
Hide file tree
Showing 14 changed files with 331 additions and 2,404 deletions.
4 changes: 0 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ let package = Package(
// Routing
Target(name: "Branches"),
Target(name: "Routing", dependencies: ["Branches"]),

// Type Safe
Target(name: "TypeSafeRouting", dependencies: ["Branches", "Routing"]),
// Target(name: "TypeSafeGenerator"),
],
dependencies: [
// Core vapor transport layer
Expand Down
21 changes: 0 additions & 21 deletions Sources/Branches/Branch.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,6 @@
import Core
import Node

extension Branch {
/**
It is not uncommon to place slugs along our branches representing keys that will
match for the path given. When this happens, the path can be laid across here to extract
slug values efficiently.
Branches: `path/to/:name`
Given Path: `path/to/joe`
let slugs = branch.slugs(for: givenPath) // ["name": "joe"]
*/
public func slugs(for path: [String]) -> Node {
var slugs: [String: Node] = [:]
slugIndexes.forEach { key, index in
guard let val = path[safe: index].flatMap({ $0.removingPercentEncoding }) else { return }
slugs[key] = Node.string(val)
}
return Node.object(slugs)
}
}

/**
When routing requests, different branches will be established,
in a linked list style stemming from their host and request method.
Expand Down
25 changes: 25 additions & 0 deletions Sources/Routing/Parameterizable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
public protocol Parameterizable {
/// the unique key to use as a slug in route building
static var uniqueSlug: String { get }

// returns the found model for the resolved url parameter
static func make(for parameter: String) throws -> Self
}

extension Parameterizable {
/// The key to be used when a result of this type is extracted from a route.
///
/// Given the following example:
///
/// ```
/// drop.get("users", User.parameter) { req in
/// let user = try req.parameters.get(User.self)
/// }
///
/// ```
///
/// the generated route will be /users/**:user**
public static var parameter: String {
return ":" + uniqueSlug
}
}
39 changes: 39 additions & 0 deletions Sources/Routing/Parameters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Node

public final class ParametersContext: Context {
internal static let shared = ParametersContext()
fileprivate init() {}
}

public let parametersContext = ParametersContext.shared

public struct Parameters: StructuredDataWrapper {
public static var defaultContext: Context? = parametersContext
public var wrapped: StructuredData
public let context: Context

public init(_ wrapped: StructuredData, in context: Context? = defaultContext) {
self.wrapped = wrapped
self.context = context ?? parametersContext
}
}

extension Parameters {
public mutating func next<P: Parameterizable>(_ p: P.Type = P.self) throws -> P {
let error = ParametersError.noMoreParametersFound(forKey: P.uniqueSlug)
guard let param = self[P.uniqueSlug] else { throw error }

var array = param.array ?? [param]
guard !array.isEmpty else { throw error }

let rawValue = array.remove(at: 0)
guard let value = rawValue.string else { throw error }

self[P.uniqueSlug] = .array(array)
return try P.make(for: value)
}
}

public enum ParametersError: Swift.Error {
case noMoreParametersFound(forKey: String)
}
10 changes: 5 additions & 5 deletions Sources/Routing/Request+Routing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ extension HTTP.Request {
/// given route: /foo/:id
/// and request with path `/foo/123`
/// parameters will be `["id": 123]`
public var parameters: Node {
public var parameters: Parameters {
get {
if let existing = storage[parametersKey] as? Node {
if let existing = storage[parametersKey] as? Parameters {
return existing
}

let node = Node([:])
storage[parametersKey] = node
return node
let params = Parameters([:])
storage[parametersKey] = params
return params
}
set {
storage[parametersKey] = newValue
Expand Down
62 changes: 58 additions & 4 deletions Sources/Routing/RouteBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import HTTP
import WebSockets

public typealias RequestHandler = (Request) throws -> ResponseRepresentable
public typealias RouteHandler = (Request) throws -> ResponseRepresentable
public typealias WebSocketRouteHandler = (Request, WebSocket) throws -> Void

/// Used to define behavior of objects capable of building routes
public protocol RouteBuilder: class {
Expand All @@ -12,7 +14,7 @@ extension RouteBuilder {
host: String? = nil,
method: Method = .get,
path: [String] = [],
responder: @escaping RequestHandler
responder: @escaping RouteHandler
) {
let re = Request.Handler { try responder($0).makeResponse() }
let path = path.pathComponents
Expand All @@ -24,14 +26,66 @@ extension RouteBuilder {
register(host: nil, method: method, path: path, responder: responder)
}

}

extension RouteBuilder {
// FIXME: This function feels like it might not fit
public func add(
_ method: HTTP.Method,
_ path: String ...,
_ value: @escaping RequestHandler
_ value: @escaping RouteHandler
) {
let responder = Request.Handler { try value($0).makeResponse() }
register(method: method, path: path, responder: responder)
}


public func socket(_ segments: String..., handler: @escaping WebSocketRouteHandler) {
register(method: .get, path: segments) { req in
return try req.upgradeToWebSocket {
try handler(req, $0)
}
}
}

public func all(_ segments: String..., handler: @escaping RouteHandler) {
register(method: .other(method: "*"), path: segments) {
try handler($0).makeResponse()
}
}

public func get(_ segments: String..., handler: @escaping RouteHandler) {
register(method: .get, path: segments) {
try handler($0).makeResponse()
}
}

public func post(_ segments: String..., handler: @escaping RouteHandler) {
register(method: .post, path: segments) {
try handler($0).makeResponse()
}
}

public func put(_ segments: String..., handler: @escaping RouteHandler) {
register(method: .put, path: segments) {
try handler($0).makeResponse()
}
}

public func patch(_ segments: String..., handler: @escaping RouteHandler) {
register(method: .patch, path: segments) {
try handler($0).makeResponse()
}
}

public func delete(_ segments: String..., handler: @escaping RouteHandler) {
register(method: .delete, path: segments) {
try handler($0).makeResponse()
}
}

public func options(_ segments: String..., handler: @escaping RouteHandler) {
register(method: .options, path: segments) {
try handler($0).makeResponse()
}
}
}
29 changes: 29 additions & 0 deletions Sources/Routing/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,35 @@ public class Router {
}
}

extension Branch {
/// It is not uncommon to place slugs along our branches representing keys that will
/// match for the path given. When this happens, the path can be laid across here to extract
/// slug values efficiently.
///
/// Branches: `path/to/:name`
/// Given Path: `path/to/joe`
///
/// let slugs = branch.slugs(for: givenPath) // ["name": "joe"]
public func slugs(for path: [String]) -> Parameters {
var slugs: [String: Parameters] = [:]
slugIndexes.forEach { key, index in
guard let val = path[safe: index]
.flatMap({ $0.removingPercentEncoding })
.flatMap({ Parameters.string($0) })
else { return }

if let existing = slugs[key] {
var array = existing.array ?? [existing]
array.append(val)
slugs[key] = .array(array)
} else {
slugs[key] = val
}
}
return .object(slugs)
}
}


extension Request {
// unique routing key for this request
Expand Down
Loading

0 comments on commit 11db47f

Please sign in to comment.