diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index c9d0cf7b..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,37 +0,0 @@ -version: 2 - -jobs: - macos: - macos: - xcode: "9.0" - steps: - - run: brew install vapor/tap/vapor - - checkout - - run: swift build - - run: swift test - - linux-3: - docker: - - image: swift:3.1.1 - steps: - - run: apt-get install -yq libssl-dev - - checkout - - run: swift build - - run: swift test - - linux: - docker: - - image: swift:4.0.3 - steps: - - run: apt-get install -yq libssl-dev - - checkout - - run: swift build - - run: swift test - -workflows: - version: 2 - tests: - jobs: - - macos - - linux-3 - - linux diff --git a/.codebeatignore b/.codebeatignore deleted file mode 100644 index 38535d4b..00000000 --- a/.codebeatignore +++ /dev/null @@ -1,2 +0,0 @@ -Sources/Vapor/Core/Generated.swift - diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 383a2897..00000000 --- a/.codecov.yml +++ /dev/null @@ -1,5 +0,0 @@ -coverage: - range: 0...100 - ignore: - - "Sources/TypeSafeRouting/Generated.swift" - - "Sources/TypeSafeGenerator" diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 1fc7e95c..00000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -Sources/Vapor/Error/error.html linguist-vendored diff --git a/Documents/CODE_OF_CONDUCT.md b/Documents/CODE_OF_CONDUCT.md deleted file mode 100644 index 75f6b7b2..00000000 --- a/Documents/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,54 +0,0 @@ -# Qutheory Code of Conduct - -Our open source community strives to: - -- **Be friendly and patient** -- **Be welcoming**: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. -- **Be considerate**: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language. -- **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. -- **Be careful in the words that we choose**: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren’t acceptable. -- **Try to understand why we disagree**: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. - -## Definitions - -**Harassment** includes, but is not limited to: - -- Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, neuro(a)typicality, physical appearance, body size, race, age, regional discrimination, political or religious affiliation -- Unwelcome comments regarding a person’s lifestyle choices and practices, including those related to food, health, parenting, drugs, and employment -- Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a person’s gender identity. You must address people by the name they give you when not addressing them by their username or handle -- Physical contact and simulated physical contact (eg, textual descriptions like “hug” or “backrub”) without consent or after a request to stop -- Threats of violence, both physical and psychological -- Incitement of violence towards any individual, including encouraging a person to commit suicide or to engage in self-harm -- Deliberate intimidation -- Stalking or following -- Harassing photography or recording, including logging online activity for harassment purposes -- Sustained disruption of discussion -- Unwelcome sexual attention, including gratuitous or off-topic sexual images or behaviour -- Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of intimacy with others -- Continued one-on-one communication after requests to cease -- Deliberate “outing” of any aspect of a person’s identity without their consent except as necessary to protect others from intentional abuse -- Publication of non-harassing private communication - -Our open source community prioritizes marginalized people’s safety over privileged people’s comfort. We will not act on complaints regarding: - -- ‘Reverse’ -isms, including ‘reverse racism,’ ‘reverse sexism,’ and ‘cisphobia’ -- Reasonable communication of boundaries, such as “leave me alone,” “go away,” or “I’m not discussing this with you” -- Refusal to explain or debate social justice concepts -- Communicating in a ‘tone’ you don’t find congenial -- Criticizing racist, sexist, cissexist, or otherwise oppressive behavior or assumptions - -## Diversity Statement - -We encourage everyone to participate and are committed to building a community for all. Although we will fail at times, we seek to treat everyone both as fairly and equally as possible. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. - -Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. - -## Reporting Issues - -If you experience or witness unacceptable behavior–or have any other concerns—please report it by contacting us via [tanner@qutheory.io](tanner@qutheory.io) or [logan@qutheory.io](logan@qutheory.io). All reports will be handled with discretion. In your report please include: - -- Your contact information. -- Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. -- Any additional information that may be helpful. - -After filing a report, a representative will contact you personally, review the incident, follow up with any additional questions, and make a decision as to how to respond. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. If the complaint originates from a member of the response team, it will be handled by a different member of the response team. We will respect confidentiality requests for the purpose of protecting victims of abuse. diff --git a/Documents/CONTRIBUTING.md b/Documents/CONTRIBUTING.md deleted file mode 100644 index 583ee984..00000000 --- a/Documents/CONTRIBUTING.md +++ /dev/null @@ -1,42 +0,0 @@ -# Contributing to OSS - -If you've never contributed to an OSS project before, here's a great guide to get you started. - -# Building Vapor - -The Vapor master branch is often ahead of our release, so the tooling can be a bit different. - -## Swift Version - -Check the `.swift-version` file for the version being used for current development - -## Xcode - -In the terminal, `cd` into the repo and build the Xcode Project. - -#### Vapor CLI - -```Swift -cd path/to/my/project -vapor xcode -``` - -#### Native SPM - -```Swift -cd path/to/my/project -swift package generate-xcodeproj -open *.xcodeproj -``` - -## Development - -We have a `Development` target inside of the project that can be used for live testing. - -## Tests - -Pull requests without adequate testing may be delayed. Please add tests alongside your pull requests. - -## Slack - -Join us in the #development channel in slack, for questions and discussions. diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 3dbedfe4..00000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,46 +0,0 @@ -🚀 Thanks for contributing! - -Below are templates for various types of issues (Bugs, Security, and Features) separated by horizontal rules. -Please delete whichever sections of the template you do not need. - -# Bugs - -If you find a bug, please submit a pull request with a failing test case displaying the bug or just create an issue with as much information as possible. - -------------------------------------------------------------------------------- - -# Security Issue - -If you find a security vulnerability, please contact [tanner@qutheory.io](tanner@qutheory.io) as soon as possible. We take these matters seriously. - -------------------------------------------------------------------------------- - -# Feature, Enhancement, or Optimization - -# Name of Feature - -* Author(s): [Developer](https://github.com/) - -## Introduction - -A short description of what the feature is. Try to keep it to a single-paragraph "elevator pitch" so the reader understands what problem this proposal is addressing. - -## Motivation - -Describe the problems that this proposal seeks to address. If the problem is that some common pattern is currently hard to express, show how one can currently get a similar effect and describe its drawbacks. If it's completely new functionality that cannot be emulated, motivate why this new functionality would help Vapor be a better framework. - -## Proposed solution - -Describe your solution to the problem. Provide examples and describe how they work. Show how your solution is better than current workarounds: is it cleaner, safer, or more efficient? - -## Code snippets - -Give us an idea of what this new idea will look like in code. The more code snippets you provide here, the easier it will be for the community to understand what your idea is. - -## Impact - -Describe the impact that this change will have on existing code. Will some Vapor applications stop compiling due to this change? Will applications still compile but produce different behavior than they used to? - -## Alternatives considered - -Describe alternative approaches to addressing the same problem, and why you chose this approach instead. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 69bf7084..dbc42719 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Tanner Nelson +Copyright (c) 2018 Qutheory, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.swift b/Package.swift index 80c16849..a981990c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,18 +1,20 @@ +// swift-tools-version:4.0 import PackageDescription let package = Package( name: "Routing", - targets: [ - // Routing - Target(name: "Branches"), - Target(name: "Routing", dependencies: ["Branches"]), + products: [ + .library(name: "Routing", targets: ["Routing"]), ], dependencies: [ - // Core vapor transport layer - .Package(url: "https://github.com/vapor/engine.git", majorVersion: 2), - .Package(url: "https://github.com/vapor/node.git", majorVersion: 2), + // 🌎 Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging. + .package(url: "https://github.com/vapor/core.git", from: "3.0.0-rc"), + + // 📦 Dependency injection / inversion of control framework. + .package(url: "https://github.com/vapor/service.git", from: "1.0.0-rc"), ], - exclude: [ - "Sources/TypeSafeGenerator" + targets: [ + .target(name: "Routing", dependencies: ["Bits", "Debugging", "Service"]), + .testTarget(name: "RoutingTests", dependencies: ["Routing"]), ] ) diff --git a/Package@swift-4.swift b/Package@swift-4.swift deleted file mode 100644 index 128f35e1..00000000 --- a/Package@swift-4.swift +++ /dev/null @@ -1,22 +0,0 @@ -// swift-tools-version:4.0 -import PackageDescription - -let package = Package( - name: "Routing", - products: [ - .library(name: "Branches", targets: ["Branches"]), - .library(name: "Routing", targets: ["Routing"]), - ], - dependencies: [ - .package(url: "https://github.com/vapor/engine.git", .upToNextMajor(from: "2.2.0")), - .package(url: "https://github.com/vapor/core.git", .upToNextMajor(from: "2.1.2")), - .package(url: "https://github.com/vapor/node.git", .upToNextMajor(from: "2.1.1")), - .package(url: "https://github.com/vapor/debugging.git", .upToNextMajor(from: "1.1.0")), - ], - targets: [ - .target(name: "Branches", dependencies: ["Core", "Node"]), - .testTarget(name: "BranchesTests", dependencies: ["Branches", "HTTP"]), - .target(name: "Routing", dependencies: ["Branches", "HTTP", "WebSockets"]), - .testTarget(name: "RoutingTests", dependencies: ["Routing"]), - ] -) \ No newline at end of file diff --git a/README.md b/README.md index 2b03473a..a77bbc43 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ - + - - + + @@ -15,6 +15,6 @@ - + diff --git a/Sources/Branches/Branch+Paths.swift b/Sources/Branches/Branch+Paths.swift deleted file mode 100644 index 69acd03a..00000000 --- a/Sources/Branches/Branch+Paths.swift +++ /dev/null @@ -1,41 +0,0 @@ - -extension Branch { - public var routes: [String] { - return allBranchesWithOutputIncludingSelf.map { $0.route } - } -} - -extension Branch { - internal var allBranchesWithOutputIncludingSelf: [Branch] { - return allIndividualBranchesInTreeIncludingSelf.filter { $0.output != nil } - } -} - -extension Branch { - // Get all individual branch nodes extending out from, and including self - internal var allIndividualBranchesInTreeIncludingSelf: [Branch] { - var branches: [Branch] = [self] - branches += subBranches.allBranches.flatMap { $0.allIndividualBranchesInTreeIncludingSelf } - return branches - } -} - -extension Branch { - public var allSubBranches: [Branch] { - return subBranches.allBranches - } -} - -extension Branch { - // The individual route leading to the calling branch - internal var route: String { - var route = "" - if !name.isEmpty { - route += "/\(name)" - } - if let parent = parent { - route = parent.route + route - } - return route - } -} diff --git a/Sources/Branches/Branch.swift b/Sources/Branches/Branch.swift deleted file mode 100644 index 6c6b3396..00000000 --- a/Sources/Branches/Branch.swift +++ /dev/null @@ -1,212 +0,0 @@ -import Core -import Node - -/** - When routing requests, different branches will be established, - in a linked list style stemming from their host and request method. - It can be represented as: - - | host | request.method | branch -> branch -> branch -*/ -public class Branch { // TODO: Rename Context - - /** - The name of the branch, ie if we have a path hello/:name, - the branch structure will be: - Branch('hello') (connected to) Branch('name') - - In cases where a slug is used, ie ':name' the slug - will be used as the name and passed as a key in matching. - */ - public let name: String - - /** - The immediate parent of this branch. `nil` if current branch is a terminator - */ - public fileprivate(set) var parent: Branch? - - /* - The leading path that corresponds to this given branch. - */ - public private(set) lazy var path: [String] = { - return self.parent.map { $0.path + [self.name] } ?? [] - }() - - /** - The current depth of a given tree branch. If tip of branch, returns `0` - */ - public private(set) lazy var depth: Int = { - return self.parent.map { $0.depth + 1 } ?? 0 - }() - - /** - A branch with a name beginning with `:` will be considered a `slug` or `parameter` branch. - This means that the branch can match any name, but represents a key value pair with associated path. - This value is used to extract parameters from a path list in an efficient way. - */ - public private(set) lazy var slugIndexes: [(key: String, idx: Int)] = { - var params = self.parent?.slugIndexes ?? [] - guard self.name.hasPrefix(":") else { return params } - let characters = self.name.toCharacterSequence().dropFirst() - let key = String(characters) - params.append((key, self.depth - 1)) - return params - }() - - /** - There are two types of branches, those that support a handler, - and those that are a linker between branches, - for example /users/messages/:id will have 3 connected branches, - only one of which supports a handler. - - Branch('users') -> Branch('messages') -> *Branches('id') - - *indicates a supported branch. - */ - private var value: Output? - - /** - Associated output the branch corresponds to - */ - public var output: Output? { - return value ?? fallback - } - - /** - Some branches are links in a chain, some are a destination that has output. - */ - private var hasValidOutput: Bool { - return output != nil - } - - /// A branch has a singular parent, but multiple children with - /// varying levels of priority - /// named branches match first, followed by slugs, followed by - /// wildcard - internal fileprivate(set) var subBranches = SubBranchMap() - - /** - Fallback routes allow various handlers to "catch" any subsequent paths on its branch that - weren't otherwise matched - */ - private var fallback: Output? { - return subBranches.wildcard?.value - } - - /** - Used to create a new branch - - - parameter name: The name associated with the branch, or the key when dealing with a slug - - parameter handler: The handler to be called if its a valid endpoint, or `nil` if this is a bridging branch - - - returns: an initialized request Branch - */ - required public init(name: String, output: Output? = nil) { - self.name = name - self.value = output - } - - /** - This function will recursively traverse the branch - until the path is fulfilled or the branch ends - - - parameter request: the request to use in matching - - parameter comps: ordered pathway components generator - - - returns: a request handler or nil if not supported - */ - public func fetch(_ path: [String]) -> Branch? { - return fetch(path.makeIterator()) - } - - /** - This function will recursively traverse the branch - until the path is fulfilled or the branch ends - - - parameter request: the request to use in matching - - parameter comps: ordered pathway components generator - - - returns: a request handler or nil if not supported - */ - public func fetch(_ path: IndexingIterator<[String]>) -> Branch? { - var comps = path - guard let key = comps.next() else { return self } - - // first check if direct path exists - if let branch = subBranches.paramBranches[key]?.fetch(comps), branch.hasValidOutput { - return branch - } - - // next attempt to find slug matches if any exist - for slug in subBranches.slugBranches { - guard let branch = slug.fetch(comps), branch.hasValidOutput else { continue } - return branch - } - - // see if wildcard with path exists - if let branch = subBranches.wildcard?.fetch(comps), branch.hasValidOutput { - return branch - } - - // use fallback - if let wildcard = subBranches.wildcard, wildcard.hasValidOutput { - return wildcard - } - - // unmatchable route - return nil - } - - @discardableResult - public func extend(_ path: [String], output: Output?) -> Branch { - return extend(path.makeIterator(), output: output) - } - - /** - If a branch exists that is linked as: - - Branch('one') -> Branch('two') - - This branch will be extended with the given value - - - parameter generator: the generator that will be used to match the path components. /users/messages/:id will return a generator that is 'users' <- 'messages' <- '*id' - - parameter handler: the handler to assign to the end path component - */ - @discardableResult - public func extend(_ path: IndexingIterator<[String]>, output: Output?) -> Branch { - var path = path - guard let key = path.next() else { - self.value = output - return self - } - - let next = subBranches[key] ?? type(of: self).init(name: key, output: nil) - next.parent = self - // trigger lazy loads at extension time -- seek out cleaner way to do this - _ = next.path - _ = next.depth - _ = next.slugIndexes - - subBranches[key] = next - return next.extend(path, output: output) - } -} - -extension Branch { - internal func testableSetBranch(key: String, branch: Branch) { - subBranches.paramBranches[key] = branch - branch.parent = self - } -} - -extension String { - #if swift(>=4.0) - internal func toCharacterSequence() -> String { - return self - } - #else - internal func toCharacterSequence() -> CharacterView { - return self.characters - } - #endif -} diff --git a/Sources/Branches/SubBranchMap.swift b/Sources/Branches/SubBranchMap.swift deleted file mode 100644 index 52107cb1..00000000 --- a/Sources/Branches/SubBranchMap.swift +++ /dev/null @@ -1,46 +0,0 @@ -struct SubBranchMap { - var paramBranches: [String: Branch] = [:] - var slugBranches: [Branch] = [] - var wildcard: Branch? - - var allBranches: [Branch] { - var all = [Branch]() - all += paramBranches.values - all += slugBranches - if let wildcard = wildcard { - all.append(wildcard) - } - return all - } - - init() {} - - subscript(key: String) -> Branch? { - get { - if key == "*" { - return wildcard - } else if key.hasPrefix(":") { - return slugBranches.lazy.filter { $0.name == key } .first - } else { - return paramBranches[key] - } - } - set { - if key == "*" { - wildcard = newValue - } else if key.hasPrefix(":") { - if let existing = slugBranches.index(where: { $0.name == key }) { - if let value = newValue { - slugBranches[existing] = value - } else { - slugBranches.remove(at: existing) - } - } else if let value = newValue { - slugBranches.append(value) - } - } else { - paramBranches[key] = newValue - } - } - } -} diff --git a/Sources/Routing/Method+Wildcard.swift b/Sources/Routing/Method+Wildcard.swift deleted file mode 100644 index 2de4d0a2..00000000 --- a/Sources/Routing/Method+Wildcard.swift +++ /dev/null @@ -1,7 +0,0 @@ -import HTTP - -extension Method { - public static var wildcard: Method { - return .other(method: "*") - } -} diff --git a/Sources/Routing/Parameter.swift b/Sources/Routing/Parameter.swift new file mode 100644 index 00000000..483d1592 --- /dev/null +++ b/Sources/Routing/Parameter.swift @@ -0,0 +1,161 @@ +import Async +import Foundation +import Service + +/// Capable of being used as a route parameter. +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/parameters/#creating-custom-parameters) +public protocol Parameter { + /// the type of this parameter after it has been resolved. + associatedtype ResolvedParameter + + /// 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, using container: Container) throws -> ResolvedParameter +} + +extension Parameter { + /// The path component for this route parameter + public static var parameter: PathComponent { + return .parameter(.string(uniqueSlug)) + } +} + +extension Parameter { + /// See Parameter.uniqueSlug + public static var uniqueSlug: String { + return "\(Self.self)".lowercased() + } +} + +extension String: Parameter { + /// Reads the raw parameter + public static func make(for parameter: String, using container: Container) throws -> String { + return parameter + } +} + +extension Int: Parameter { + + /// Attempts to read the parameter into a `Int` + public static func make(for parameter: String, using container: Container) throws -> Int { + guard let number = Int(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an Int", source: .capture()) + } + + return number + } +} + +extension Double: Parameter { + /// Attempts to read the parameter into a `Double` + public static func make(for parameter: String, using container: Container) throws -> Double { + guard let number = Double(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to a Double", source: .capture()) + } + + return number + } +} + +extension Int8: Parameter { + /// Attempts to read the parameter into a `Int8` + public static func make(for parameter: String, using container: Container) throws -> Int8 { + guard let number = Int8(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an Int8", source: .capture()) + } + + return number + } +} + +extension Int16: Parameter { + /// Attempts to read the parameter into a `Int16` + public static func make(for parameter: String, using container: Container) throws -> Int16 { + guard let number = Int16(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an Int16", source: .capture()) + } + + return number + } +} + +extension Int32: Parameter { + /// Attempts to read the parameter into a `Int32` + public static func make(for parameter: String, using container: Container) throws -> Int32 { + guard let number = Int32(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an Int32", source: .capture()) + } + + return number + } +} + +extension Int64: Parameter { + /// Attempts to read the parameter into a `Int64` + public static func make(for parameter: String, using container: Container) throws -> Int64 { + guard let number = Int64(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an Int64", source: .capture()) + } + + return number + } +} + +extension UInt8: Parameter { + /// Attempts to read the parameter into a `UInt8` + public static func make(for parameter: String, using container: Container) throws -> UInt8 { + guard let number = UInt8(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an UInt8", source: .capture()) + } + + return number + } +} + +extension UInt16: Parameter { + /// Attempts to read the parameter into a `UInt16` + public static func make(for parameter: String, using container: Container) throws -> UInt16 { + guard let number = UInt16(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an UInt16", source: .capture()) + } + + return number + } +} + +extension UInt32: Parameter { + /// Attempts to read the parameter into a `UInt32` + public static func make(for parameter: String, using container: Container) throws -> UInt32 { + guard let number = UInt32(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an UInt32", source: .capture()) + } + + return number + } +} + +extension UInt64: Parameter { + /// Attempts to read the parameter into a `UInt64` + public static func make(for parameter: String, using container: Container) throws -> UInt64 { + guard let number = UInt64(parameter) else { + throw RoutingError(identifier: "parameterNotAnInt", reason: "The parameter was not convertible to an UInt64", source: .capture()) + } + + return number + } +} + +extension UUID: Parameter { + /// Attempts to read the parameter into a `UUID` + public static func make(for parameter: String, using container: Container) throws -> UUID { + guard let uuid = UUID(uuidString: parameter) else { + throw RoutingError(identifier: "parameterNotAUUID", reason: "The parameter was not convertible to a UUID", source: .capture()) + } + + return uuid + } +} + diff --git a/Sources/Routing/ParameterContainer.swift b/Sources/Routing/ParameterContainer.swift new file mode 100644 index 00000000..e96cf62b --- /dev/null +++ b/Sources/Routing/ParameterContainer.swift @@ -0,0 +1,85 @@ +import Async +import Foundation +import Service + +/// A bag for holding parameters resolved during router +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/parameters/) +public protocol ParameterContainer: class { + /// An array of parameters + typealias Parameters = [ParameterValue] + + /// The parameters, not yet resolved + /// so that the `.next()` method can throw any errors. + var parameters: Parameters { get set } +} + +/// MARK: Next + +extension Container where Self: ParameterContainer { + /// Grabs the next parameter from the parameter bag. + /// + /// Note: the parameters _must_ be fetched in the order they + /// appear in the path. + /// + /// For example GET /posts/:post_id/comments/:comment_id + /// must be fetched in this order: + /// + /// let post = try parameters.next(Post.self) + /// let comment = try parameters.next(Comment.self) + /// + public func parameter(_ parameter: P.Type = P.self) throws -> P.ResolvedParameter + where P: Parameter + { + return try self.parameter(P.self, using: self) + } + + /// Infer requested type where the resolved parameter is the parameter type. + public func parameter() throws -> P + where P: Parameter, P.ResolvedParameter == P + { + return try self.parameter(P.self) + } +} + +extension ParameterContainer { + /// Grabs the next parameter from the parameter bag. + /// + /// Note: the parameters _must_ be fetched in the order they + /// appear in the path. + /// + /// For example GET /posts/:post_id/comments/:comment_id + /// must be fetched in this order: + /// + /// let post = try parameters.next(Post.self) + /// let comment = try parameters.next(Comment.self) + /// + public func parameter(_ parameter: P.Type = P.self, using container: Container) throws -> P.ResolvedParameter + where P: Parameter + { + guard parameters.count > 0 else { + throw RoutingError(identifier: "insufficientParameters", reason: "Insufficient parameters", source: .capture()) + } + + let current = parameters[0] + guard current.slug == [UInt8](P.uniqueSlug.utf8) else { + throw RoutingError( + identifier: "invalidParameterType", + reason: "Invalid parameter type. Expected \(P.self) got \(current.slug)", + source: .capture() + ) + } + + guard let string = String(bytes: current.value, encoding: .utf8) else { + throw RoutingError( + identifier: "convertString", + reason: "Could not convert the parameter value to a UTF-8 string.", + source: .capture() + ) + } + + let item = try P.make(for: string, using: container) + parameters = Array(parameters.dropFirst()) + return item + } +} diff --git a/Sources/Routing/ParameterValue.swift b/Sources/Routing/ParameterValue.swift new file mode 100644 index 00000000..6cd8b75a --- /dev/null +++ b/Sources/Routing/ParameterValue.swift @@ -0,0 +1,16 @@ +import Foundation + +/// A parameter and its resolved value. +public struct ParameterValue { + /// The parameter type. + let slug: [UInt8] + + /// The resolved value. + let value: [UInt8] + + /// Create a new lazy parameter. + init(slug: [UInt8], value: [UInt8]) { + self.slug = slug + self.value = value + } +} diff --git a/Sources/Routing/Parameterizable.swift b/Sources/Routing/Parameterizable.swift deleted file mode 100644 index c8fa4d17..00000000 --- a/Sources/Routing/Parameterizable.swift +++ /dev/null @@ -1,48 +0,0 @@ -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 - } -} - -extension String: Parameterizable { - public static var uniqueSlug: String { - return "string" - } - - public static func make(for parameter: String) throws -> String { - return parameter - } -} -extension Int: Parameterizable { - public static var uniqueSlug: String { - return "int" - } - - public static func make(for parameter: String) throws -> Int { - guard let int = Int(parameter) else { - throw RouterError.invalidParameter - } - - return int - } -} diff --git a/Sources/Routing/Parameters.swift b/Sources/Routing/Parameters.swift deleted file mode 100644 index f41e4551..00000000 --- a/Sources/Routing/Parameters.swift +++ /dev/null @@ -1,39 +0,0 @@ -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: 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) -} diff --git a/Sources/Routing/PathComponent.swift b/Sources/Routing/PathComponent.swift new file mode 100644 index 00000000..e21b9c14 --- /dev/null +++ b/Sources/Routing/PathComponent.swift @@ -0,0 +1,114 @@ +import Foundation +import Bits + +/// Components of a router path. +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/parameters/) +public enum PathComponent: ExpressibleByStringLiteral { + public enum Parameter { + case data(Data) + case bytes([UInt8]) + case byteBuffer(ByteBuffer) + case string(String) + case substring(Substring) + + public var count: Int { + switch self { + case .data(let data): return data.count + case .byteBuffer(let byteBuffer): return byteBuffer.count + case .bytes(let bytes): return bytes.count + case .string(let string): return string.utf8.count + case .substring(let substring): return substring.utf8.count + } + } + + func withByteBuffer(do closure: (ByteBuffer) -> (T)) -> T { + switch self { + case .data(let data): return data.withByteBuffer(closure) + case .byteBuffer(let byteBuffer): return closure(byteBuffer) + case .bytes(let bytes): return bytes.withUnsafeBufferPointer(closure) + case .string(let string): + let count = self.count + + return string.withCString { pointer in + return pointer.withMemoryRebound(to: UInt8.self, capacity: count) { pointer in + return closure(ByteBuffer(start: pointer, count: count)) + } + } + case .substring(let substring): + let count = self.count + + return substring.withCString { pointer in + return pointer.withMemoryRebound(to: UInt8.self, capacity: count) { pointer in + return closure(ByteBuffer(start: pointer, count: count)) + } + } + } + } + + public var string: String { + switch self { + case .string(let string): return string + default: + return String(bytes: self.bytes, encoding: .utf8) ?? "" + } + } + + public var bytes: [UInt8] { + switch self { + case .data(let data): return Array(data) + case .byteBuffer(let byteBuffer): return Array(byteBuffer) + case .bytes(let bytes): return bytes + case .string(let string): return [UInt8](string.utf8) + case .substring(let substring): return Array(substring.utf8) + } + } + } + + /// Create a path component from a string + public init(stringLiteral value: String) { + self = .constants(value.split(separator: "/").map { .substring($0) } ) + } + + /// A normal, constant path component. + case constants([Parameter]) + + /// A dynamic parameter component. + case parameter(Parameter) + + /// Any set of components + case anything +} + +/// Capable of being represented by a path component. +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/parameters/) +public protocol PathComponentRepresentable { + /// Convert to path component. + func makePathComponent() -> PathComponent +} + +extension PathComponent: PathComponentRepresentable { + /// See PathComponentRepresentable.makePathComponent() + public func makePathComponent() -> PathComponent { + return self + } +} + +// MARK: Array + +extension Array where Element == PathComponentRepresentable { + /// Convert to array of path components. + public func makePathComponents() -> [PathComponent] { + return map { $0.makePathComponent() } + } +} + +/// Strings are constant path components. +extension String: PathComponentRepresentable { + /// Convert string to constant path component. + /// See PathComponentRepresentable.makePathComponent() + public func makePathComponent() -> PathComponent { + return PathComponent(stringLiteral: self) + } +} diff --git a/Sources/Routing/Request+Routing.swift b/Sources/Routing/Request+Routing.swift deleted file mode 100644 index f24b4219..00000000 --- a/Sources/Routing/Request+Routing.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Branches -import HTTP -import Node - -private let parametersKey = "parameters" - -extension HTTP.Request { - /// when routing a url with slug parameters, ie: - /// foo/:id - /// the request will populate these values before passing to handeler - /// for example: - /// given route: /foo/:id - /// and request with path `/foo/123` - /// parameters will be `["id": 123]` - public var parameters: Parameters { - get { - if let existing = storage[parametersKey] as? Parameters { - return existing - } - - let params = Parameters([:]) - storage[parametersKey] = params - return params - } - set { - storage[parametersKey] = newValue - } - } -} diff --git a/Sources/Routing/Route.swift b/Sources/Routing/Route.swift new file mode 100644 index 00000000..7234b58a --- /dev/null +++ b/Sources/Routing/Route.swift @@ -0,0 +1,22 @@ +import Service + +/// A route. When registered to a router, replies to `Request`s using the `Responder`. +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/route/) +public final class Route: Extendable { + /// The path at which the route is assigned + public var path: [PathComponent] + + /// The responder. Used to respond to a `Request` + public var output: Output + + /// A storage place to extend the `Route` with. + /// Can store metadata like Documentation/Route descriptions + public var extend = Extend() + + /// Creates a new route from a Method, path and responder + public init(path: [PathComponent], output: Output) { + self.path = path + self.output = output + } +} diff --git a/Sources/Routing/RouteBuilder+Grouping.swift b/Sources/Routing/RouteBuilder+Grouping.swift deleted file mode 100644 index 700959ce..00000000 --- a/Sources/Routing/RouteBuilder+Grouping.swift +++ /dev/null @@ -1,80 +0,0 @@ -import HTTP - -extension RouteBuilder { - /// Group all subsequent routes built with this builder - /// under this specified host - /// - /// the last host in the chain will take precedence, for example: - /// - /// if using: - /// grouped(host: "0.0.0.0").grouped(host: "196.08.0.1") - /// - /// will bind subsequent additions to '196.08.0.1' - public func grouped(host: String) -> RouteBuilder { - return RouteGroup(host: host, pathPrefix: [], middleware: [], parent: self) - } - - /// Group all subsequent routes behind a specified path prefix - /// use `,` separated list or `/` separated string - /// for example, the following are all equal - /// - /// "a/path/to/foo" - /// "a", "path", "to", "foo" - /// "a/path", "to/foo" - public func grouped(_ path: String...) -> RouteBuilder { - return grouped(path) - } - - /// - see grouped(_ path: String...) - public func grouped(_ path: [String]) -> RouteBuilder { - let components = path.pathComponents - return RouteGroup(host: nil, pathPrefix: components, middleware: [], parent: self) - } - - /// Group all subsequent routes to pass through specified middleware - /// use `,` separated list for multiple middleware - public func grouped(_ middleware: Middleware...) -> RouteBuilder { - return grouped(middleware) - } - - // FIXME: External arg necessary on middleware groups? - - /// - see grouped(middleware: Middleware...) - public func grouped(_ middleware: [Middleware]) -> RouteBuilder { - return RouteGroup(host: nil, pathPrefix: [], middleware: middleware, parent: self) - } -} - -// MARK: Closures - -extension RouteBuilder { - /// Closure based variant of grouped(host: String) - public func group(host: String, handler: (RouteBuilder) -> ()) { - let builder = grouped(host: host) - handler(builder) - } - - /// Closure based variant of grouped(_ path: String...) - public func group(_ path: String ..., handler: (RouteBuilder) -> ()) { - group(path: path, handler: handler) - } - - /// Closure based variant of grouped(_ path: [String]) - public func group(path: [String], handler: (RouteBuilder) -> ()) { - let path = path.pathComponents - let builder = grouped(path) - handler(builder) - } - - // FIXME: Need external parameter cohesiveness - /// Closure based variant of grouped(middleware: Middleware...) - public func group(_ middleware: Middleware..., handler: (RouteBuilder) -> ()) { - group(middleware: middleware, handler: handler) - } - - /// Closure based variant of grouped(middleware: [Middleware]) - public func group(middleware: [Middleware], handler: (RouteBuilder) -> ()) { - let builder = grouped(middleware) - handler(builder) - } -} diff --git a/Sources/Routing/RouteBuilder.swift b/Sources/Routing/RouteBuilder.swift deleted file mode 100644 index 94f79aba..00000000 --- a/Sources/Routing/RouteBuilder.swift +++ /dev/null @@ -1,101 +0,0 @@ -import HTTP -import WebSockets - -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 { - func register(host: String?, method: Method, path: [String], responder: Responder) -} - -extension RouteBuilder { - public func register( - host: String? = nil, - method: Method = .get, - path: [String] = [], - responder: @escaping RouteHandler - ) { - let re = Request.Handler { try responder($0).makeResponse() } - let path = path.pathComponents - register(host: host, method: method, path: path, responder: re) - } - - public func register(method: Method = .get, path: [String] = [], responder: Responder) { - let path = path.pathComponents - register(host: nil, method: method, path: path, responder: responder) - } - -} - -extension RouteBuilder { - #if swift(>=4) - public func add( - _ method: HTTP.Method, - _ path: String ..., - value: @escaping RouteHandler - ) { - let responder = Request.Handler { try value($0).makeResponse() } - register(method: method, path: path, responder: responder) - } - #else - public func add( - _ method: HTTP.Method, - _ path: String ..., - _ value: @escaping RouteHandler - ) { - let responder = Request.Handler { try value($0).makeResponse() } - register(method: method, path: path, responder: responder) - } - #endif - - 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() - } - } -} diff --git a/Sources/Routing/RouteGroup.swift b/Sources/Routing/RouteGroup.swift deleted file mode 100644 index bf022f97..00000000 --- a/Sources/Routing/RouteGroup.swift +++ /dev/null @@ -1,47 +0,0 @@ -import HTTP - -/// RouteGroup is a step in the RouteBuilder chain that -/// allows users to collect metadata about various endpoints -/// -/// for example, if we have several routes that begin with "some/prefix/path" -/// we might want to group those together so that we can easily append -internal final class RouteGroup: RouteBuilder { - let host: String? - let pathPrefix: [String] - let middleware: [Middleware] - let parent: RouteBuilder - - init(host: String?, pathPrefix: [String], middleware: [Middleware], parent: RouteBuilder) { - self.host = host - self.pathPrefix = pathPrefix - self.middleware = middleware - self.parent = parent - } - - func register(host: String?, method: Method, path: [String], responder: Responder) { - let host = host ?? self.host - let path = self.pathPrefix + path - - let res: Responder - if middleware.isEmpty { - res = responder - } else { - let middleware = self.middleware - res = Request.Handler { request in - return try middleware.chain(to: responder).respond(to: request) - } - } - - parent.register(host: host, method: method, path: path, responder: res) - } -} - -extension Collection where Iterator.Element == Middleware { - fileprivate func chain(to responder: Responder) -> Responder { - return reversed().reduce(responder) { nextResponder, nextMiddleware in - return Request.Handler { request in - return try nextMiddleware.respond(to: request, chainingTo: nextResponder) - } - } - } -} diff --git a/Sources/Routing/Router+Responder.swift b/Sources/Routing/Router+Responder.swift deleted file mode 100644 index f9043a2e..00000000 --- a/Sources/Routing/Router+Responder.swift +++ /dev/null @@ -1,105 +0,0 @@ -import HTTP -import Branches -import Debugging - -public var supportOptionsRequests = true - -extension Router: Responder { - public func respond(to request: Request) throws -> Response { - guard let responder = route(request) else { return try fallbackResponse(for: request) } - return try responder.respond(to: request) - } - - private func fallbackResponse(for request: Request) throws -> Response { - guard supportOptionsRequests, request.method == .options else { throw RouterError.missingRoute(for: request) } - return options(for: request) - } - - private func options(for request: Request) -> Response { - let opts = supportedMethods(for: request) - .map { $0.description } - .joined(separator: ", ") - return Response(status: .ok, headers: ["Allow": opts]) - } - - private func supportedMethods(for request: Request) -> [Method] { - let request = request.copy() - guard let host = self.host(for: request.uri.hostname) else { return [] } - let allOptions = host.allSubBranches - let allPossibleMethods = allOptions.map { Method($0.name) } - return allPossibleMethods.filter { method in - request.method = method - return route(request) != nil - } - } - - private func host(for host: String) -> Branch? { - return base.fetch([host]) - } -} - -extension Request { - public func copy() -> Request { - return Request( - method: method, - uri: uri, - version: version, - headers: headers, - body: body - ) - } -} - -public enum RouterError: Debuggable { - case invalidParameter - case missingRoute(for: Request) - case unspecified(Swift.Error) -} - -extension RouterError { - public var identifier: String { - switch self { - case .missingRoute: - return "missingRoute" - case .unspecified: - return "unspecified" - case .invalidParameter: - return "invalidParameter" - } - } - - public var reason: String { - switch self { - case .invalidParameter: - return "invalid parameter" - case .missingRoute(let request): - return "no route found for \(request)" - case .unspecified(let error): - return "unspecified \(error)" - } - } - - public var suggestedFixes: [String] { - switch self { - case .missingRoute(let request): - return [ - "ensure that a route for path '\(request.uri.path)' exists", - "verify the host and httpmethod for the request are as expected", - "log the routes of your router with `router.routes`" - ] - case .unspecified(_): - return [ - "look into upgrading to a version that expects this error", - "try to understand which module threw this error and where it came from" - ] - case .invalidParameter: - return [] - } - } - - public var possibleCauses: [String] { - return [ - "received a route that is not supported by the router" - ] - } -} diff --git a/Sources/Routing/Router.swift b/Sources/Routing/Router.swift deleted file mode 100644 index 514781b7..00000000 --- a/Sources/Routing/Router.swift +++ /dev/null @@ -1,149 +0,0 @@ -import Branches -import Foundation -import HTTP - -public class Router { - /// The base branch from which all routing stems outward - public final let base = Branch(name: "", output: nil) - - /// Init - public init() {} - - /// Register a given path. Use `*` for host OR method to define wildcards that will be matched - /// if no concrete match exists. - /// - /// - parameter host: the host to match, ie: '0.0.0.0', or `*` to match any - /// - parameter method: the method to match, ie: `GET`, or `*` to match any - /// - parameter path: the path that should be registered - /// - parameter output: the associated output of this path, usually a responder, or `nil` - public func register(host: String?, method: HTTP.Method, path: [String], responder: Responder) { - let host = host ?? "*" - let path = [host, method.description] + path.filter { !$0.isEmpty } - base.extend(path, output: responder) - } - - // caches static route resolutions - private var _cache: [String: Responder?] = [:] - private var _cacheLock = NSLock() - - /// Removes all entries from this router's cache. - /// - public func flushCache() { - _cacheLock.lock() - _cache.removeAll() - _cacheLock.unlock() - } - - /// Removes the cached Responder for a given Request. - /// If there is no cached Responder, returns nil. - /// - /// NOTE: If you do not register a new Responder for the - /// Request, the old Responder will be invoked on a subsequent - /// Request and re-cached. I.e. this function does not prune - /// the Branch. - @discardableResult - public func flushCache(for request: Request) -> Responder? { - _cacheLock.lock() - let maybeCached = _cache.removeValue(forKey: request.routeKey) - _cacheLock.unlock() - - if let cached = maybeCached { - return cached - } else { - return nil - } - } - - /// Routes an incoming request - /// the request will be populated with any found parameters (aka slugs). - /// - /// If a handler is found, it is returned. - public func route(_ request: Request) -> Responder? { - let key = request.routeKey - - // check the static route cache - - _cacheLock.lock() - let maybeCached = _cache[key] - _cacheLock.unlock() - - if let cached = maybeCached { - return cached - } - - let path = request.path() - let result = base.fetch(path) - - request.parameters = result?.slugs(for: path) ?? [:] - - // if there are no dynamic slugs, we can cache - if request.parameters.object?.isEmpty == true { - _cacheLock.lock() - _cache[key] = result?.output - _cacheLock.unlock() - } - - return result?.output - } -} - -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 - fileprivate var routeKey: String { - return uri.hostname - + ";" + method.description - + ";" + uri.path - } - - fileprivate func path() -> [String] { - var host: String = uri.hostname - if host.isEmpty { host = "*" } - let method = self.method.description - let components = uri.path.pathComponents - return [host, method] + components - } -} - -extension Router { - public var routes: [String] { - return base.routes.map { input in - var comps = input.pathComponents.makeIterator() - let host = comps.next() ?? "*" - let method = comps.next() ?? "*" - let path = comps.joined(separator: "/") - return "\(host) \(method) \(path)" - } - } -} - -extension Router: RouteBuilder {} diff --git a/Sources/Routing/RoutingError.swift b/Sources/Routing/RoutingError.swift new file mode 100644 index 00000000..8f52d7b2 --- /dev/null +++ b/Sources/Routing/RoutingError.swift @@ -0,0 +1,22 @@ +import Debugging + +/// Errors that can be thrown while working with TCP sockets. +public struct RoutingError: Debuggable { + public static let readableName = "Routing Error" + public var identifier: String + public var reason: String + public var sourceLocation: SourceLocation? + public var stackTrace: [String] + + /// Create a new TCP error. + init( + identifier: String, + reason: String, + source: SourceLocation + ) { + self.identifier = identifier + self.reason = reason + self.sourceLocation = source + self.stackTrace = RoutingError.makeStackTrace() + } +} diff --git a/Sources/Routing/TrieRouter.swift b/Sources/Routing/TrieRouter.swift new file mode 100644 index 00000000..3b5afc71 --- /dev/null +++ b/Sources/Routing/TrieRouter.swift @@ -0,0 +1,113 @@ +import Async +import Foundation +import Bits + +/// A basic router that can route requests depending on the method and URI +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/router/) +public final class TrieRouter { + /// All routes registered to this router + public private(set) var routes: [Route] = [] + + /// The root node + var root: TrieRouterNode + + /// If a route cannot be found, this is the fallback responder that will be used instead + public var fallback: Output? + + /// If `true`, constants are compared case insensitively + public var caseInsensitive: Bool + + /// Create a new trie router + public init() { + self.caseInsensitive = false + self.root = TrieRouterNode(kind: .root) + } + + /// See Router.register() + public func register(route: Route) { + self.routes.append(route) + var current = root + + for component in route.path { + current = current[component] + } + + current.output = route.output + } + + /// See Router.route() + public func route(path: [PathComponent.Parameter], parameters: ParameterContainer) -> Output? { + // always start at the root node + var current: TrieRouterNode = root + var parameterNode: (TrieRouterNode, [UInt8])? + var fallbackNode: TrieRouterNode? + + // traverse the constant path supplied + nextComponent: for component in path { + // Reset state to ensure a previous resolved path isn't interfering + parameterNode = nil + fallbackNode = nil + + for child in current.children { + switch child.kind { + case .anything: + fallbackNode = child + case .constant(let data, _): + // if we find a constant route path that matches this component, + // then we should use it. + let match = component.withByteBuffer { buffer -> Bool in + if self.caseInsensitive { + return data.caseInsensitiveEquals(to: buffer) + } else { + return data.elementsEqual(buffer) + } + } + + if match { + current = child + continue nextComponent + } + case .parameter(let parameter): + parameterNode = (child, parameter) + case .root: + fatalError("Incorrect nested 'root' routing node") + } + } + + if let (node, parameter) = parameterNode { + // if no constant routes were found that match the path, but + // a dynamic parameter child was found, we can use it + let lazy = ParameterValue(slug: parameter, value: component.bytes) + parameters.parameters.append(lazy) + current = node + continue nextComponent + } + + guard let fallbackNode = fallbackNode else { + // No results found + return fallback + } + + current = fallbackNode + } + + // return the resolved responder if there hasn't + // been an early exit. + return current.output ?? fallback + } +} + +extension TrieRouterNode { + fileprivate func find(constants: [[UInt8]]) -> TrieRouterNode? { + let constant = constants[0] + + let node = self.findConstant(constant) + + if constants.count > 1 { + return node?.find(constants: Array(constants[1...])) + } else { + return node + } + } +} diff --git a/Sources/Routing/TrieRouterNode+Create.swift b/Sources/Routing/TrieRouterNode+Create.swift new file mode 100644 index 00000000..3dab8f3c --- /dev/null +++ b/Sources/Routing/TrieRouterNode+Create.swift @@ -0,0 +1,82 @@ +extension TrieRouterNode { + fileprivate func find(constants: [[UInt8]]) -> TrieRouterNode { + let constant = constants[0] + + let node: TrieRouterNode + + if let found = self.findConstant(constant) { + node = found + } else { + node = TrieRouterNode(kind: .constant(data: constant, dataSize: constant.count)) + self.children.append(node) + } + + if constants.count > 1 { + return node.find(constants: Array(constants[1...])) + } else { + return node + } + } + + fileprivate func find(component: PathComponent) -> TrieRouterNode { + switch component { + case .constants(let constants): + if constants.count == 0 { + return self + } else { + return self.find(constants: constants.map { $0.bytes }) + } + case .parameter(let p): + if let node = self.findParameterNode() { + return node + } else { + let node = TrieRouterNode(kind: .parameter(data: p.bytes)) + self.children.append(node) + return node + } + case .anything: + if let node = findAnyNode() { + return node + } else { + let node = TrieRouterNode(kind: .anything) + self.children.append(node) + return node + } + } + } + + /// Returns the first parameter node + fileprivate func findParameterNode() -> TrieRouterNode? { + for child in children { + if case .parameter = child.kind { + return child + } + } + + return nil + } + + func findConstant(_ buffer: [UInt8]) -> TrieRouterNode? { + for child in children { + if case .constant(let data, _) = child.kind, data == buffer { + return child + } + } + + return nil + } + + fileprivate func findAnyNode() -> TrieRouterNode? { + for child in children { + if case .anything = child.kind { + return child + } + } + + return nil + } + + subscript(path: PathComponent) -> TrieRouterNode { + return self.find(component: path) + } +} diff --git a/Sources/Routing/TrieRouterNode.swift b/Sources/Routing/TrieRouterNode.swift new file mode 100644 index 00000000..056a842d --- /dev/null +++ b/Sources/Routing/TrieRouterNode.swift @@ -0,0 +1,34 @@ +import Foundation +import Bits + +final class TrieRouterNode { + /// Kind of node + var kind: TrieRouterNodeKind + + /// All constant child nodes + var children: [TrieRouterNode] + + /// This node's output + var output: Output? + + init( + kind: TrieRouterNodeKind, + children: [TrieRouterNode] = [], + output: Output? = nil + ) { + self.kind = kind + self.children = children + self.output = output + } +} + +enum TrieRouterNodeKind { + case root + + // Size is separate to save ARC performance, which had a huge impact here + case parameter(data: [UInt8]) + + case constant(data: [UInt8], dataSize: Int) + + case anything +} diff --git a/Sources/Routing/Utilities.swift b/Sources/Routing/Utilities.swift deleted file mode 100644 index 5e2b1131..00000000 --- a/Sources/Routing/Utilities.swift +++ /dev/null @@ -1,30 +0,0 @@ -extension String { - /// Separates a URI path into - /// an array by splitting on `/` - internal var pathComponents: [String] { - return toCharacterSequence() - .split(separator: "/", omittingEmptySubsequences: true) - .map { String($0) } - } -} - -extension Sequence where Iterator.Element == String { - /// Ensures that `/` are interpreted properly on arrays - /// of path components, so `["foo", "bar/dar"]` - /// will expand to `["foo", "bar", "dar"]` - internal var pathComponents: [String] { - return flatMap { $0.pathComponents } .filter { !$0.isEmpty } - } -} - -extension String { - #if swift(>=4.0) - internal func toCharacterSequence() -> String { - return self - } - #else - internal func toCharacterSequence() -> CharacterView { - return self.characters - } - #endif -} diff --git a/Sources/TypeSafeGenerator/Body.swift b/Sources/TypeSafeGenerator/Body.swift deleted file mode 100644 index 28a76f0d..00000000 --- a/Sources/TypeSafeGenerator/Body.swift +++ /dev/null @@ -1,81 +0,0 @@ -struct Body { - var signature: Signature - - init(signature: Signature) { - self.signature = signature - } -} - -extension Body: CustomStringConvertible { - var description: String { - return [ - "self.add(.\(signature.method), \(path)) { request in", - innerBody.indented, - "}", - ].joined(separator: "\n") - } - - var path: String { - return signature.parameters.map { parameter in - switch parameter { - case .path(let path): - return path.name - case .wildcard(let wildcard): - return "\":\(wildcard.name)\"" - } - }.joined(separator: ", ") - } - - var innerBody: String { - return [ - badRequestGuards, - stringInitializeTrys, - invalidParameterGuards, - returns - ].joined(separator: "\n") - } - - var badRequestGuards: String { - return signature.wildcards.map { wildcard in - return [ - "guard let v\(wildcard.name) = request.parameters[\"\(wildcard.name)\"]?.string else {", - " throw TypeSafeRoutingError.missingParameter", - "}" - ].joined(separator: "\n") - }.joined(separator: "\n") - } - - var stringInitializeTrys: String { - return signature.wildcards.map { wildcard in - return "let e\(wildcard.name) = try \(wildcard.generic)(from: v\(wildcard.name))\n" - }.joined(separator: "\n") - } - - var invalidParameterGuards: String { - return signature.wildcards.map { wildcard in - return [ - "guard let c\(wildcard.name) = e\(wildcard.name) else {", - " throw TypeSafeRoutingError.invalidParameterType(\(wildcard.generic).self)", - "}" - ].joined(separator: "\n") - }.joined(separator: "\n") - } - - var returns: String { - let additions: String - if signature.wildcards.count > 0 { - additions = "," + signature.wildcards.map { wildcard in - return "c\(wildcard.name)" - }.joined(separator: ", ") - } else { - additions = "" - } - - switch signature.variant { - case .socket: - return "return try request.upgradeToWebSocket { try handler(request, $0\(additions)) }" - case .base: - return "return try handler(request\(additions)).makeResponse()" - } - } -} diff --git a/Sources/TypeSafeGenerator/Function.swift b/Sources/TypeSafeGenerator/Function.swift deleted file mode 100644 index 27480c28..00000000 --- a/Sources/TypeSafeGenerator/Function.swift +++ /dev/null @@ -1,19 +0,0 @@ -struct Function { - var signature: Signature - var body: Body - - init(variant: Variant, method: Method, parameters: [Parameter]) { - signature = Signature(variant: variant, method: method, parameters: parameters) - body = Body(signature: signature) - } -} - -extension Function: CustomStringConvertible { - var description: String { - return [ - "\(signature.description) {", - body.description.indented, - "}" - ].joined(separator: "\n") - } -} diff --git a/Sources/TypeSafeGenerator/Generator.swift b/Sources/TypeSafeGenerator/Generator.swift deleted file mode 100644 index 480a93d0..00000000 --- a/Sources/TypeSafeGenerator/Generator.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation - -class Generator { - var parameterMax: Int - - init(max parameters: Int) { - parameterMax = parameters - } - - func generate() -> String { - let permutations = generatePermutations() - - var functions: [Function] = [] - - for parameters in permutations { - if parameters.count == 0 { - continue - } - - let function = Function(variant: .socket, method: .get, parameters: parameters) - functions.append(function) - - for method: Method in [.get, .post, .put, .patch, .delete, .options] { - let function = Function(variant: .base, method: method, parameters: parameters) - functions.append(function) - } - } - - var generated = [ - warning, - "import Branches", - "import HTTP", - "import Routing", - "import WebSockets", - " ", - "extension RouteBuilder {", - ] - for function in functions { - generated.append(function.description.indented) - } - generated.append("}") - return generated.joined(separator: "\n") - } - - private func generatePermutations() -> [[Parameter]] { - var permutations: [[Parameter]] = [[]] - - for i in 0 ... parameterMax { - var subPermutations: [[Parameter]] = [[]] - - for _ in 0 ..< i { - subPermutations = permutate(subPermutations) - } - - permutations += subPermutations - } - - return permutations - } - - - private func permutate(_ array: [[Parameter]]) -> [[Parameter]] { - var result: [[Parameter]] = [] - - for subarray in array { - var pathArray = subarray - var wildcardArray = subarray - - let path = Parameter.pathFor(pathArray) - pathArray.append(path) - - let wildcard = Parameter.wildcardFor(wildcardArray) - wildcardArray.append(wildcard) - - result.append(pathArray) - result.append(wildcardArray) - } - - return result - } - -} - -extension Generator { - var warning: String { - return [ - "/*", - " ⚠️ AUTOMATICALLY GENERATED CODE", - " ", - " Generated by Sources/Generator/main.swift.", - " Do not edit this file directly.", - " ", - " Last generated: \(NSDate())", - "*/" - ].joined(separator: "\n") - } -} diff --git a/Sources/TypeSafeGenerator/Method.swift b/Sources/TypeSafeGenerator/Method.swift deleted file mode 100644 index 0386a931..00000000 --- a/Sources/TypeSafeGenerator/Method.swift +++ /dev/null @@ -1,12 +0,0 @@ -enum Method { - case get, post, put, patch, delete, options -} - -extension Method { - var uppercase: String { - return "\(self)".uppercased() - } - var lowercase: String { - return "\(self)".lowercased() - } -} diff --git a/Sources/TypeSafeGenerator/Operators.swift b/Sources/TypeSafeGenerator/Operators.swift deleted file mode 100644 index 2d2f05ad..00000000 --- a/Sources/TypeSafeGenerator/Operators.swift +++ /dev/null @@ -1,16 +0,0 @@ -infix operator <<< - -func <<<(lhs: inout String, rhs: String) { - return lhs.append(rhs) -} - -func <<(lhs: inout String, rhs: String) { - return lhs.append(rhs + "\n") -} - -extension String { - var indented: String { - let split = self.characters.split(separator: "\n") - return split.map { " " + String($0) }.joined(separator: "\n") - } -} diff --git a/Sources/TypeSafeGenerator/Parameter.swift b/Sources/TypeSafeGenerator/Parameter.swift deleted file mode 100644 index fc47dbe2..00000000 --- a/Sources/TypeSafeGenerator/Parameter.swift +++ /dev/null @@ -1,66 +0,0 @@ -enum Parameter { - struct Wildcard { - var name: String - var generic: String - } - - struct Path { - var name: String - } - - case path(Path) - case wildcard(Wildcard) - - var name: String { - switch self { - case .path(let path): - return path.name - case .wildcard(let wildcard): - return wildcard.name - } - } - - var isPath: Bool { - switch self { - case.path(_): - return true - default: - return false - } - } - - var isWildcard: Bool { - switch self { - case.wildcard(_): - return true - default: - return false - } - } - - static func pathFor(_ array: [Parameter]) -> Parameter { - var i = 0 - - for item in array { - if item.isPath { - i += 1 - } - } - - let path = Path(name: "p\(i)") - return .path(path) - } - - static func wildcardFor(_ array: [Parameter]) -> Parameter { - var i = 0 - - for item in array { - if item.isWildcard { - i += 1 - } - } - - let path = Wildcard(name: "w\(i)", generic: "W\(i)") - return .wildcard(path) - } -} \ No newline at end of file diff --git a/Sources/TypeSafeGenerator/Signature+Documentation.swift b/Sources/TypeSafeGenerator/Signature+Documentation.swift deleted file mode 100644 index 32d3d3d5..00000000 --- a/Sources/TypeSafeGenerator/Signature+Documentation.swift +++ /dev/null @@ -1,69 +0,0 @@ -extension Signature { - var documentation: String { - var d = "" - - d << "/**" - - if variant == .socket { - d << " Establishes a WebSocket connection" - d << " at the given path. WebSocket connections" - d << " can be accessed using the `ws://` or `wss://`" - d << " schemes to provide two way information" - d << " transfer between the client and the server." - d << " " - d << " **Body**" - d << " The body closure is given access to the Request" - d << " that started the connection as well as the WebSocket." - d << " " - d << " drop.socket(\"test\") { request, ws in" - d << " " - d << " }" - d << " " - d << " **Sending Data**" - d << " " - d << " Data is sent to the WebSocket stream using `send(_:Data)`" - d << " " - d << " try ws.send(\"Hello, world\")" - d << " " - d << " **Receiving Data**" - d << " " - d << " Data is received from the WebSocket using" - d << " the `onText` callback." - d << " " - d << " ws.onText = { ws, text in" - d << " drop.console.output(\"Received \\(text)\")" - d << " }" - d << " " - d << " **Closing**" - d << " " - d << " Close the Socket when you are done." - - d << " try ws.close()" - d << " " - d << " **Routing**" - d << " " - } - - - d << " This route will run for any \(method.uppercase) request" - d << " to a path that matches:" - d << "" - - d <<< " /" - - for parameter in parameters { - switch parameter { - case .path(_): - d <<< "/" - case .wildcard(_): - d <<< "{wildcard}/" - } - } - - d << " " - - d <<< "*/" - - return d - } -} diff --git a/Sources/TypeSafeGenerator/Signature.swift b/Sources/TypeSafeGenerator/Signature.swift deleted file mode 100644 index 51141d15..00000000 --- a/Sources/TypeSafeGenerator/Signature.swift +++ /dev/null @@ -1,142 +0,0 @@ -struct Signature { - var variant: Variant - var method: Method - var parameters: [Parameter] - - init(variant: Variant, method: Method, parameters: [Parameter]) { - self.variant = variant - self.method = method - self.parameters = parameters - } -} - -extension Signature { - var wildcards: [Parameter.Wildcard] { - return parameters.flatMap { parameter in - if case .wildcard(let wildcard) = parameter { - return wildcard - } - - return nil - } - } - - var paths: [Parameter.Path] { - return parameters.flatMap { parameter in - if case .path(let path) = parameter { - return path - } - - return nil - } - } -} - -/** - Creates the function signature. - - <--- documentation --> - - /** - Blah blah blah ... - */ - - public func get(_ p0: String, _ w0: T, handler: (Request, T) -> ResponseRepresentable) - - <----- generic map ----> - <----- list --------> - handler input - <---------> <-- handler output --> - - <------------------- input -----------------> - - <-------------------------------------------- description ----------------------------------------------------> -*/ -extension Signature: CustomStringConvertible { - var description: String { - return [ - documentation, - "public func \(name)\(generics)(\(input))" - ].joined(separator: "\n") - } -} - -extension Signature { - var input: String { - if parameters.count > 0 { - return "\(list), \(handler)" - } else { - return handler - } - } - - var list: String { - if - parameters.count == 1, - let first = parameters.first, - case .path(let path) = first - { - return "_ \(path.name): String = \"\"" - } else { - return parameters.map { parameter in - var string = "_ \(parameter.name): " - - switch parameter { - case .path(_): - string <<< "String" - case .wildcard(let wildcard): - string <<< "\(wildcard.generic).Type" - } - - return string - }.joined(separator: ", ") - } - } - - var handler: String { - return "handler: @escaping (\(handlerInput)) throws -> \(handlerOutput)" - } - - var handlerInput: String { - var items = ["Request"] - - if variant == .socket { - items.append("WebSocket") - } - - items += wildcards.map { $0.generic } - - return items.joined(separator: ", ") - } - - var handlerOutput: String { - switch variant { - case .socket: - return "()" - case .base: - return "ResponseRepresentable" - } - } - - var generics: String { - if genericMap.characters.count == 0 { - return "" - } - return "<\(genericMap)>" - } - - var genericMap: String { - return wildcards.map { wildcard in - return "\(wildcard.generic): StringInitializable" - }.joined(separator: ", ") - } - - var name: String { - switch variant { - case .socket: - return "socket" - case .base: - return method.lowercase - } - } -} diff --git a/Sources/TypeSafeGenerator/Variant.swift b/Sources/TypeSafeGenerator/Variant.swift deleted file mode 100644 index 7a792af4..00000000 --- a/Sources/TypeSafeGenerator/Variant.swift +++ /dev/null @@ -1,3 +0,0 @@ -enum Variant { - case base, socket -} diff --git a/Sources/TypeSafeGenerator/main.swift b/Sources/TypeSafeGenerator/main.swift deleted file mode 100644 index 11692b45..00000000 --- a/Sources/TypeSafeGenerator/main.swift +++ /dev/null @@ -1,39 +0,0 @@ -#if !os(Linux) -import Foundation - -let generator = Generator(max: 3) -let code = generator.generate() - -if CommandLine.arguments.count < 2 { - print("⚠️ IMPORTANT") - print("To run the Generator, you must pass the $(SRCROOT)") - print("as the first argument to the executable.") - print("") - print("This can be done using 'Edit Scheme'") - print("") - fatalError("$(SRCROOT) must be passed as a parameter") -} - -let path = CommandLine.arguments[1].replacingOccurrences(of: "XcodeProject", with: "") -let url = URL(fileURLWithPath: path + "/Sources/TypeSafeRouting/Generated.swift") - -do{ - let lines = code.characters.split(separator: "\n").count - let functions = code.components(separatedBy: "func").count - 1 - - // writing to disk - try code.write(to: url, atomically: true, encoding: .utf8) - print("✅ Code successfully generated.") - print("Functions: \(functions)") - print("Lines: \(lines)") - print("Location: \(url)") - print("Date: \(NSDate())") -} catch let error as NSError { - print("Error writing generated file at \(url)") - print(error.localizedDescription) -} - -#else - print("Linux not supported by generator.") -#endif - diff --git a/Tests/BranchesTests/BranchTests.swift b/Tests/BranchesTests/BranchTests.swift deleted file mode 100644 index 8d94233b..00000000 --- a/Tests/BranchesTests/BranchTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -import XCTest -import Branches - -class BranchTests: XCTestCase { - static let allTests = [ - ("testSimple", testSimple), - ("testWildcard", testWildcard), - ("testWildcardTrailing", testWildcardTrailing), - ("testLeadingPath", testLeadingPath), - ("testEmpty", testEmpty) - ] - - func testSimple() { - let base = Branch(name: "[base]", output: nil) - base.extend(["a", "b", "c"], output: "abc") - let result = base.fetch(["a", "b", "c"]) - XCTAssert(result?.output == "abc") - } - - func testWildcard() { - let base = Branch(name: "[base]", output: nil) - base.extend(["a", "b", "c", "*"], output: "abc") - let result = base.fetch(["a", "b", "c"]) - XCTAssert(result?.output == "abc") - } - - func testWildcardTrailing() { - let base = Branch(name: "[base]", output: nil) - base.extend(["a", "b", "c", "*"], output: "abc") - guard let result = base.fetch(["a", "b", "c", "d", "e", "f"]) else { - XCTFail("invalid wildcard fetch") - return - } - - XCTAssert(result.output == "abc") - } - - func testLeadingPath() { - let base = Branch(name: "[base]", output: nil) - let subBranch = base.extend([":a", ":b", ":c", "*"], output: "abc") - XCTAssert(subBranch.path == [":a", ":b", ":c", "*"]) - } - - func testEmpty() { - let base = Branch(name: "[base]", output: nil) - base.extend(["a", "b", "c"], output: "abc") - let result = base.fetch(["z"]) - XCTAssert(result == nil) - - } -} diff --git a/Tests/BranchesTests/RouteExtractionTests.swift b/Tests/BranchesTests/RouteExtractionTests.swift deleted file mode 100644 index f4e620df..00000000 --- a/Tests/BranchesTests/RouteExtractionTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// RouteExtractionTests.swift -// Routing -// -// Created by Logan Wright on 11/1/16. -// -// - -import XCTest -@testable import Branches -import HTTP - -class RouteExtractionTests: XCTestCase { - static let allTests = [ - ("testRouteLog", testRouteLog), - ("testIndividualBranches", testIndividualBranches), - ("testIndividualBranchesWithOutput", testIndividualBranchesWithOutput), - ("testBranchRoutes", testBranchRoutes), - ] - - func testRouteLog() throws { - let base = Branch(name: "a") - XCTAssertEqual(base.route, "/a") - - let extended = base.extend(["b", "c"], output: 2) - XCTAssertEqual(extended.route, "/a/b/c") - - let wild = base.extend(["*", ":foo"], output: 3) - XCTAssertEqual(wild.route, "/a/*/:foo") - } - - func testIndividualBranches() throws { - let a = Branch(name: "a") - let b = Branch(name: "b") - let c = Branch(name: "c") - let d = Branch(name: "d") - let e = Branch(name: "e") - - a.testableSetBranch(key: "b", branch: b) - a.testableSetBranch(key: "c", branch: c) - c.testableSetBranch(key: "d", branch: d) - c.testableSetBranch(key: "e", branch: e) - - let allBranches = a.allIndividualBranchesInTreeIncludingSelf.map { $0.name } - XCTAssertEqual(Set(allBranches), Set([a, b, c, d, e].map { $0.name })) - } - - func testIndividualBranchesWithOutput() throws { - let a = Branch(name: "a", output: 1) - let b = Branch(name: "b") - let c = Branch(name: "c", output: 2) - let d = Branch(name: "d") - let e = Branch(name: "e", output: 3) - - a.testableSetBranch(key: "b", branch: b) - a.testableSetBranch(key: "c", branch: c) - c.testableSetBranch(key: "d", branch: d) - c.testableSetBranch(key: "e", branch: e) - - let allBranches = a.allBranchesWithOutputIncludingSelf.map { $0.name } - XCTAssertEqual(allBranches, [a, c, e].map { $0.name }) - } - - func testBranchRoutes() throws { - let a = Branch(name: "a", output: 1) - let b = Branch(name: "b") - let c = Branch(name: "c", output: 2) - let d = Branch(name: "d") - let e = Branch(name: "e", output: 3) - - a.testableSetBranch(key: "b", branch: b) - a.testableSetBranch(key: "c", branch: c) - c.testableSetBranch(key: "d", branch: d) - c.testableSetBranch(key: "e", branch: e) - - let expectation = ["/a", "/a/c", "/a/c/e"] - XCTAssertEqual(a.routes, expectation) - } -} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 08fde9af..30f5f48a 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,20 +2,9 @@ import XCTest @testable import RoutingTests -@testable import BranchesTests XCTMain([ - // Branches - testCase(BranchTests.allTests), - testCase(RouteExtractionTests.allTests), - - // Routing - testCase(AddTests.allTests), - testCase(GroupedTests.allTests), - testCase(GroupTests.allTests), testCase(RouterTests.allTests), - testCase(RouteTests.allTests), - testCase(RouteBuilderTests.allTests), ]) #endif diff --git a/Tests/RoutingTests/AddTests.swift b/Tests/RoutingTests/AddTests.swift deleted file mode 100644 index e84b50dc..00000000 --- a/Tests/RoutingTests/AddTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import XCTest -import HTTP -import Routing - -class AddTests: XCTestCase { - static var allTests = [ - ("testBasic", testBasic), - ("testVariadic", testVariadic), - ("testWithSlash", testWithSlash), - ] - - func testBasic() throws { - let router = Router() - router.add(.get, "ferret") { request in - return "foo" - } - - let request = Request(method: .get, path: "ferret") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "foo".makeBytes()) - } - - func testVariadic() throws { - let router = Router() - router.add(.trace, "foo", "bar", "baz") { request in - return "1337" - } - - let request = Request(method: .trace, path: "foo/bar/baz") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "1337".makeBytes()) - } - - func testWithSlash() throws { - let router = Router() - router.add(.get, "foo/bar") { request in - return "foo" - } - - let request = Request(method: .get, path: "foo/bar") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "foo".makeBytes()) - } -} diff --git a/Tests/RoutingTests/GroupTests.swift b/Tests/RoutingTests/GroupTests.swift deleted file mode 100644 index 8efe63f1..00000000 --- a/Tests/RoutingTests/GroupTests.swift +++ /dev/null @@ -1,101 +0,0 @@ -import XCTest -import HTTP -import Routing - -class GroupTests: XCTestCase { - static var allTests = [ - ("testBasic", testBasic), - ("testVariadic", testVariadic), - ("testHost", testHost), - ("testHostMiss", testHostMiss), - ("testMiddleware", testMiddleware), - ] - - func testBasic() throws { - let router = Router() - - router.group("users") { users in - users.add(.get, ":id") { request in - return "show" - } - } - - let request = Request(method: .get, path: "users/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "show".makeBytes()) - XCTAssertEqual(request.parameters["id"], "5") - } - - func testVariadic() throws { - let router = Router() - - router.group("users", "devices", "etc") { users in - users.add(.get, ":id") { request in - return "show" - } - } - let request = Request(method: .get, path: "users/devices/etc/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "show".makeBytes()) - XCTAssertEqual(request.parameters["id"], "5") - } - - func testHost() throws { - let router = Router() - - router.group(host: "192.168.0.1") { host in - host.add(.get, "host-only") { request in - return "host" - } - } - router.add(.get, "host-only") { req in - return "nothost" - } - - let request = Request(method: .get, path: "host-only", host: "192.168.0.1") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "host".makeBytes()) - } - - func testHostMiss() throws { - let router = Router() - - router.group(host: "192.168.0.1") { host in - host.add(.get, "host-only") { request in - return "host" - } - } - router.add(.get, "host-only") { req in - return "nothost" - } - - let request = Request(method: .get, path: "host-only", host: "BADHOST") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "nothost".makeBytes()) - } - - func testMiddleware() throws { - class Middy: Middleware { - func respond(to request: Request, chainingTo next: Responder) throws -> Response { - request.storage["middleware"] = true - return try next.respond(to: request) - } - } - - let router = Router() - router.group(Middy()) { builder in - builder.register { _ in return "hello" } - } - - let request = Request(method: .get, path: "/") - XCTAssertNil(request.storage["middleware"]) - let response = try router.respond(to: request) - let middleware = request.storage["middleware"] as? Bool - XCTAssertEqual(middleware, true) - XCTAssertEqual(response.body.bytes?.makeString(), "hello") - } -} diff --git a/Tests/RoutingTests/GroupedTests.swift b/Tests/RoutingTests/GroupedTests.swift deleted file mode 100644 index 0a49d363..00000000 --- a/Tests/RoutingTests/GroupedTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -import XCTest -import HTTP -import Routing - -class GroupedTests: XCTestCase { - static let allTests = [ - ("testBasic", testBasic), - ("testVariadic", testVariadic), - ("testHost", testHost), - ("testChained", testChained), - ("testMultiChained", testMultiChained), - ] - - func testBasic() throws { - let router = Router() - - let users = router.grouped("users") - users.register(method: .get, path: [":id"]) { request in - return "show" - } - - let request = Request(method: .get, path: "users/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes.makeString(), "show") - XCTAssertEqual(request.parameters["id"], "5") - } - - func testVariadic() throws { - let router = Router() - - let users = router.grouped("users", "devices", "etc") - users.add(.get, ":id") { request in - return "show" - } - - let request = Request(method: .get, path: "users/devices/etc/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "show".makeBytes()) - XCTAssertEqual(request.parameters["id"], "5") - } - - func testHost() throws { - let router = Router() - let host = router.grouped(host: "192.168.0.1") - host.register(method: .get, path: ["host-only"]) { request in - return "host group found" - } - - router.register(method: .get, path: ["host-only"]) { _ in return "nothost" } - let request = Request(method: .get, path: "host-only", host: "192.168.0.1") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes.makeString(), "host group found") - } - - func testChained() throws { - let router = Router() - - let users = router.grouped("users", "devices", "etc").grouped("even", "deeper") - users.add(.get, ":id") { request in - return "show" - } - - let request = Request(method: .get, path: "users/devices/etc/even/deeper/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes.makeString(), "show") - XCTAssertEqual(request.parameters["id"], "5") - } - - func testMultiChained() throws { - class Middy: Middleware { - func respond(to request: Request, chainingTo next: Responder) throws -> Response { - request.storage["middleware"] = true - return try next.respond(to: request) - } - } - - let router = Router() - let builder = router.grouped("a", "path").grouped(Middy()).grouped(host: "9.9.9.9") - builder.add(.get, "/") { req in - return "got it" - } - - let request = Request(method: .get, path: "a/path", host: "9.9.9.9") - let responder = router.route(request) - let response = try responder?.respond(to: request) - XCTAssertNotNil(response) - XCTAssertEqual(response?.body.bytes?.makeString(), "got it") - let middleware = request.storage["middleware"] as? Bool - XCTAssertEqual(middleware, true) - - let bad = Request(method: .get, path: "a/path", host: "0.0.0.0") - XCTAssertNil(router.route(bad)) - } -} diff --git a/Tests/RoutingTests/RouteBuilderTests.swift b/Tests/RoutingTests/RouteBuilderTests.swift deleted file mode 100644 index 9cde1816..00000000 --- a/Tests/RoutingTests/RouteBuilderTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -import XCTest - -import HTTP -import Routing - -class RouteBuilderTests: XCTestCase { - static let allTests = [ - ("testBasic", testBasic), - ("testVariadic", testVariadic), - ("testMoreThanThreeArgs", testMoreThanThreeArgs), - ("testCustomMethod", testCustomMethod), - ("testAll", testAll), - ] - - func testBasic() throws { - let builder = Dropped() - builder.get("hello") { _ in - return "world!" - } - - let request = Request(method: .get, path: "hello") - let bytes = try request.bytes(running: builder.router) - - XCTAssertEqual(bytes, "world!".makeBytes()) - } - - func testVariadic() throws { - let builder = Dropped() - builder.delete("foo", "bar", "baz") { _ in - return "1337" - } - - let request = Request(method: .delete, path: "foo/bar/baz") - let bytes = try request.bytes(running: builder.router) - - XCTAssertEqual(bytes, "1337".makeBytes()) - } - - func testMoreThanThreeArgs() throws { - let builder = Dropped() - builder.post(":userId", "messages", ":messageId", "read") { _ in - return "Please don't read this" - } - - let request = Request(method: .post, path: "1/messages/10/read") - let bytes = try request.bytes(running: builder.router) - - XCTAssertEqual(bytes, "Please don't read this".makeBytes()) - } - - func testCustomMethod() throws { - let builder = Dropped() - builder.add(.other(method: "custom"), "custom", "method") { _ in - return "Custom method" - } - - let request = Request(method: .other(method: "custom"), path: "custom/method") - let bytes = try request.bytes(running: builder.router) - - XCTAssertEqual(bytes, "Custom method".makeBytes()) - } - - func testAll() throws { - let builder = Dropped() - builder.all("all", "methods") { _ in - return "All around the world (repeat 144)" - } - - let methods: [HTTP.Method] = [ - .delete, .get, .head, .post, .put, .connect, .options, .trace, .patch, .other(method: "other") - ] - - methods.forEach { - let request = Request(method: $0, path: "all/methods") - - do { - let bytes = try request.bytes(running: builder.router) - XCTAssertEqual(bytes, "All around the world (repeat 144)".makeBytes()) - } catch { - XCTFail("Routing failed: \(error) for method: \($0)") - } - } - } -} - -/// A mock for RouteBuilder -final class Dropped: RouteBuilder { - let router = Router() - - public func register(host: String?, method: HTTP.Method, path: [String], responder: Responder) { - router.register(host: host, method: method, path: path, responder: responder) - } -} diff --git a/Tests/RoutingTests/RouteTests.swift b/Tests/RoutingTests/RouteTests.swift deleted file mode 100644 index c0d86221..00000000 --- a/Tests/RoutingTests/RouteTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -import XCTest -import HTTP -import URI -import Routing -import Node - -class RouteTests: XCTestCase { - static let allTests = [ - ("testRoute", testRoute), - ("testRouteParams", testRouteParams), - ("testParameters", testParameters), - ] - - func testRoute() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { req in - return Response(status: .ok, body: "HI") - } - - let request = Request(method: .get, uri: "http://0.0.0.0/hello") - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "HI") - } - - func testRouteParams() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: [":zero", ":one", ":two", "*"]) { req in - let zero = req.parameters["zero"]?.string ?? "[fail]" - let one = req.parameters["one"]?.string ?? "[fail]" - let two = req.parameters["two"]?.string ?? "[fail]" - return Response(status: .ok, body: "\(zero):\(one):\(two)") - } - - let paths: [[String]] = [ - ["a", "b", "c"], - ["1", "2", "3", "4"], - ["x", "y", "z", "should", "be", "in", "wildcard"] - ] - try paths.forEach { path in - let uri = URI( - scheme: "http", - userInfo: nil, - hostname: "0.0.0.0", - port: 80, - path: path.joined(separator: "/"), - query: nil, - fragment: nil - ) - let request = Request(method: .get, uri: uri) - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), path.prefix(3).joined(separator: ":")) - } - } - - func testParameters() throws { - let request = Request(method: .get, path: "") - let params = request.parameters - XCTAssertEqual(params, Parameters([:])) - } -} diff --git a/Tests/RoutingTests/RouterTests.swift b/Tests/RoutingTests/RouterTests.swift index 5abbe90d..69ff320e 100644 --- a/Tests/RoutingTests/RouterTests.swift +++ b/Tests/RoutingTests/RouterTests.swift @@ -1,324 +1,177 @@ -import XCTest -import HTTP -import Branches +import Async +import Dispatch +import Bits import Routing -import URI - -extension String: Swift.Error {} +import Service +import XCTest class RouterTests: XCTestCase { - static let allTests = [ - ("testRouter", testRouter), - ("testWildcardMethod", testWildcardMethod), - ("testWildcardHost", testWildcardHost), - ("testHostMatch", testHostMatch), - ("testMiss", testMiss), - ("testWildcardPath", testWildcardPath), - ("testParameters", testParameters), - ("testEmpty", testEmpty), - ("testNoHostWildcard", testNoHostWildcard), - ("testRouterDualSlugRoutes", testRouterDualSlugRoutes), - ("testRouteLogs", testRouteLogs), - ("testRouterThrows", testRouterThrows), - ("testParams", testParams), - ("testOutOfBoundsParams", testOutOfBoundsParams), - ("testParamsDuplicateKey", testParamsDuplicateKey), - ("testSecondRegistrationIgnored", testSecondRegistrationIgnored), - ("testCanRegisterAfterRemoveResponse", testCanRegisterAfterRemoveResponse), - ] - func testRouter() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - - let request = Request(method: .get, uri: "http://0.0.0.0/hello") - let response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Hello, World!") - } + let router = TrieRouter() - func testWildcardMethod() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .wildcard, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - - let method: [HTTP.Method] = [.get, .post, .put, .patch, .delete, .trace, .head, .options] - try method.forEach { method in - let request = Request(method: method, uri: "http://0.0.0.0/hello") - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, World!") - } - } - - func testWildcardHost() throws { - let router = Router() - router.register(host: "*", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - - let hosts: [String] = ["0.0.0.0", "chat.app.com", "[255.255.255.255.255]", "slack.app.com"] - try hosts.forEach { host in - let request = Request(method: .get, uri: "http://\(host)/hello") - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, World!") - } - } - - func testHostMatch() throws { - let router = Router() - - let hosts: [String] = ["0.0.0.0", "chat.app.com", "255.255.255.255", "slack.app.com"] - hosts.forEach { host in - router.register(host: host, path: ["hello"]) { request in - return Response(status: .ok, body: "Host: \(host)") - } - } - - try hosts.forEach { host in - let request = Request(method: .get, uri: "http://\(host)/hello") - let response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Host: \(host)") - } - } + let path: [PathComponent.Parameter] = [.string("foo"), .string("bar"), .string("baz")] - func testMiss() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - XCTFail("should not be found, wrong host") - return "[fail]" - } + let route = Route(path: [.constants(path), .parameter(.string(User.uniqueSlug))], output: 42) + router.register(route: route) - let request = Request(method: .get, uri: "http://[255.255.255.255.255]/hello") - let handler = router.route(request) - XCTAssert(handler == nil) - } - - func testWildcardPath() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello", "*"]) { request in - return "Hello, World!" - } - - let paths: [String] = [ - "hello", - "hello/zero", - "hello/extended/path", - "hello/very/extended/path.pdf" - ] - - try paths.forEach { path in - let request = Request(method: .get, uri: "http://0.0.0.0/\(path)") - let response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Hello, World!") - } - } - - func testParameters() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello", ":name", ":age"]) { request in - guard let name = request.parameters["name"]?.string else { throw "missing param: name" } - guard let age = request.parameters["age"]?.int else { throw "missing or invalid param: age" } - return "Hello, \(name) aged \(age)." - } - - let namesAndAges: [(String, Int)] = [ - ("a", 12), - ("b", 42), - ("c", 200), - ("d", 1) - ] - - try namesAndAges.forEach { name, age in - let request = Request(method: .get, uri: "http://0.0.0.0/hello/\(name)/\(age)") - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, \(name) aged \(age).") - } - } - - func testEmpty() throws { - let router = Router() - router.register(path: []) { request in - return Response(status: .ok, body: "Hello, Empty!") - } - - let empties: [String] = ["", "/"] - try empties.forEach { emptypath in - let uri = URI(scheme: "http", hostname: "0.0.0.0", path: emptypath) - let request = Request(method: .get, uri: uri) - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, Empty!") - } - } - - func testNoHostWildcard() throws { - let router = Router() - router.register { request in - return Response(status: .ok, body: "Hello, World!") - } - - let uri = URI( - scheme: "", - hostname: "" + let container = try BasicContainer( + config: Config(), + environment: .development, + services: Services(), + on: DefaultEventLoop(label: "unit-test") + ) + let params = Params() + XCTAssertEqual(router.route(path: path + [.string("Tanner")], parameters: params), 42) + try XCTAssertEqual(params.parameter(User.self, using: container).blockingAwait().name, "Tanner") + } + + func testCaseSensitiveRouting() throws { + let router = TrieRouter() + + let path: [PathComponent.Parameter] = [.string("path"), .string("TO"), .string("fOo")] + + let route = Route(path: [.constants(path)], output: 42) + router.register(route: route) + + let params = Params() + XCTAssertEqual(router.route(path: [.string("PATH"), .string("tO"), .string("FOo")], parameters: params), nil) + XCTAssertEqual(router.route(path: [.string("path"), .string("TO"), .string("fOo")], parameters: params), 42) + } + + func testCaseInsensitiveRouting() throws { + let router = TrieRouter() + router.caseInsensitive = true + + let path: [PathComponent.Parameter] = [.string("path"), .string("TO"), .string("fOo")] + + let route = Route(path: [.constants(path)], output: 42) + router.register(route: route) + + let params = Params() + XCTAssertEqual(router.route(path: [.string("PATH"), .string("tO"), .string("FOo")], parameters: params), 42) + } + + func testAnyRouting() throws { + let router = TrieRouter() + + let route0 = Route(path: [ + .constants([.string("a")]), + .anything + ], output: 0) + + let route1 = Route(path: [ + .constants([.string("b")]), + .parameter(.string("1")), + .anything + ], output: 1) + + let route2 = Route(path: [ + .constants([.string("c")]), + .parameter(.string("1")), + .parameter(.string("2")), + .anything + ], output: 2) + + let route3 = Route(path: [ + .constants([.string("d")]), + .parameter(.string("1")), + .parameter(.string("2")), + ], output: 3) + + let route4 = Route(path: [ + .constants([.string("e")]), + .parameter(.string("1")), + .anything, + .constants([.string("a")]) + ], output: 4) + + router.register(route: route0) + router.register(route: route1) + router.register(route: route2) + router.register(route: route3) + router.register(route: route4) + + XCTAssertEqual( + router.route(path: [.string("a"), .string("b")], parameters: Params()), + 0 + ) + + XCTAssertNil(router.route(path: [.string("a")], parameters: Params())) + + XCTAssertEqual( + router.route(path: [.string("a"), .string("a")], parameters: Params()), + 0 + ) + + XCTAssertEqual( + router.route(path: [.string("b"), .string("a"), .string("c")], parameters: Params()), + 1 + ) + + XCTAssertNil(router.route(path: [.string("b")], parameters: Params())) + XCTAssertNil(router.route(path: [.string("b"), .string("a")], parameters: Params())) + + XCTAssertEqual( + router.route(path: [.string("b"), .string("a"), .string("c")], parameters: Params()), + 1 + ) + + XCTAssertNil(router.route(path: [.string("c")], parameters: Params())) + XCTAssertNil(router.route(path: [.string("c"), .string("a")], parameters: Params())) + XCTAssertNil(router.route(path: [.string("c"), .string("b")], parameters: Params())) + + XCTAssertEqual( + router.route(path: [.string("d"), .string("a"), .string("b")], parameters: Params()), + 3 + ) + + XCTAssertNil(router.route(path: [.string("d"), .string("a"), .string("b"), .string("c")], parameters: Params())) + XCTAssertNil(router.route(path: [.string("d"), .string("a")], parameters: Params())) + + XCTAssertEqual( + router.route(path: [.string("e"), .string("a"), .string("b"), .string("a")], parameters: Params()), + 4 ) - let request = Request(method: .get, uri: uri) - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, World!") - } - - func testRouterDualSlugRoutes() throws { - let router = Router() - router.register(path: ["foo", ":a", "one"]) { _ in return "1" } - router.register(path: ["foo", ":b", "two"]) { _ in return "2" } - - let requestOne = Request(method: .get, path: "foo/slug-val/one") - let responseOne = try router.respond(to: requestOne) - XCTAssertEqual(responseOne.body.bytes?.makeString(), "1") - - let requestTwo = Request(method: .get, path: "foo/slug-val/two") - let responseTwo = try router.respond(to: requestTwo) - XCTAssertEqual(responseTwo.body.bytes?.makeString(), "2") } - - func testRouteLogs() throws { - let router = Router() - let responder = Request.Handler { _ in return Response(status: .ok) } - router.register(path: ["foo", "bar", ":id"], responder: responder) - router.register(path: ["foo", "bar", ":id", "zee"], responder: responder) - router.register(path: ["1/2/3/4/5/6/7"], responder: responder) - router.register(method: .post, path: ["multi-path"], responder: responder) - router.register(method: .put, path: ["multi-path"], responder: responder) - - let expectation = [ - "* POST multi-path", - "* PUT multi-path", - "* GET 1/2/3/4/5/6/7", - "* GET foo/bar/:id", - "* GET foo/bar/:id/zee" - ] - - XCTAssertEqual(Set(router.routes), Set(expectation)) - } - - func testRouterThrows() { - let router = Router() - - do { - let request = Request(method: .get, path: "asfd") - _ = try router.respond(to: request) - XCTFail("Should throw missing route") - } catch { - print(error) - } - } - - - func testParams() { - let base = Branch(name: "[base]", output: nil) - base.extend([":a", ":b", ":c", "*"], output: "abc") - let path = ["zero", "one", "two", "d", "e", "f"] - guard let result = base.fetch(path) else { - XCTFail("invalid wildcard fetch") - return - } - - let params = result.slugs(for: path) - XCTAssert(params["a"] == "zero") - XCTAssert(params["b"] == "one") - XCTAssert(params["c"] == "two") - XCTAssert(result.output == "abc") - } - - func testOutOfBoundsParams() { - let base = Branch(name: "[base]", output: nil) - base.extend([":a", ":b", ":c", "*"], output: "abc") - let path = ["zero", "one", "two", "d", "e", "f"] - guard let result = base.fetch(path) else { - XCTFail("invalid wildcard fetch") - return - } - - let params = result.slugs(for: ["zero", "one"]) - XCTAssert(params["a"] == "zero") - XCTAssert(params["b"] == "one") - XCTAssert(params["c"] == nil) - XCTAssert(result.output == "abc") - } - - func testParamsDuplicateKey() { - let base = Branch(name: "[base]", output: nil) - base.extend([":a", ":a", ":a", "*"], output: "abc") - let path = ["zero", "one", "two", "d", "e", "f"] - guard let result = base.fetch(path) else { - XCTFail("invalid wildcard fetch") - return - } - - let params = result.slugs(for: ["zero", "one"]) - XCTAssert(params["a.0"] == "zero") - XCTAssert(params["a.1"] == "one") - XCTAssert(params["a.2"] == nil) - XCTAssert(result.output == "abc") } - func testParameterizable() throws { - let base = Branch(name: "[base]", output: nil) - base.extend([Foo.parameter, Foo.parameter, Foo.parameter, "*"], output: "abc") - let path = ["zero", "one", "two", "d", "e", "f"] - guard let result = base.fetch(path) else { - XCTFail("invalid wildcard fetch") - return - } - - var params = result.slugs(for: ["zero", "one"]) - let one = try params.next(Foo.self) - let two = try params.next(Foo.self) - XCTAssert(one.id == "zero") - XCTAssert(two.id == "one") - } + func testRouterSuffixes() throws { + let router = TrieRouter() + router.caseInsensitive = true - func testSecondRegistrationIgnored() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - let request = Request(method: .get, uri: "http://0.0.0.0/hello") - var response = try router.respond(to: request) - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Ciao mondo!") - } + let path1: [PathComponent.Parameter] = [.string("a")] + let path2: [PathComponent.Parameter] = [.string("aa")] + let route1 = Route(path: [.constants(path1)], output: 1) + let route2 = Route(path: [.constants(path2)], output: 2) + router.register(route: route1) + router.register(route: route2) - response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Hello, World!") + let params = Params() + XCTAssertEqual(router.route(path: [.string("a")], parameters: params), 1) + XCTAssertEqual(router.route(path: [.string("aa")], parameters: params), 2) } - func testCanRegisterAfterRemoveResponse() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - let request = Request(method: .get, uri: "http://0.0.0.0/hello") - var response = try router.respond(to: request) - - router.flushCache(for: request) - - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Ciao, mondo!") - } - - response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Ciao, mondo!") - } + static let allTests = [ + ("testRouter", testRouter), + ("testCaseInsensitiveRouting", testCaseInsensitiveRouting), + ("testCaseSensitiveRouting", testCaseSensitiveRouting), + ("testAnyRouting", testAnyRouting), + ("testRouterSuffixes", testRouterSuffixes), + ] } -struct Foo { - let id: String +final class Params: ParameterContainer { + var parameters: Parameters = [] + init() {} } -extension Foo: Parameterizable { - static let uniqueSlug = "foo-slug" +final class User: Parameter { + var name: String + + init(name: String) { + self.name = name + } - static func make(for parameter: String) throws -> Foo { - return .init(id: parameter) + static func make(for parameter: String, using container: Container) throws -> Future { + return Future(User(name: parameter)) } } diff --git a/Tests/RoutingTests/Utilities.swift b/Tests/RoutingTests/Utilities.swift deleted file mode 100644 index b83768f4..00000000 --- a/Tests/RoutingTests/Utilities.swift +++ /dev/null @@ -1,28 +0,0 @@ -import XCTest -import HTTP -import Routing -import URI - -extension Request { - convenience init(method: HTTP.Method, path: String, host: String = "0.0.0.0") { - let uri = URI(hostname: host, path: path) - self.init(method: method, uri: uri) - } - - enum BytesError: Error { - case routingFailed - case invalidResponse - } - - func bytes(running router: Router) throws -> Bytes { - guard let responder = router.route(self) else { - throw BytesError.routingFailed - } - - guard let bytes = try responder.respond(to: self).body.bytes else { - throw BytesError.invalidResponse - } - - return bytes - } -} diff --git a/app.json b/app.json deleted file mode 100644 index b6f67e4e..00000000 --- a/app.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "vapor", - "scripts": { - }, - "env": { - }, - "formation": { - }, - "addons": [ - - ], - "buildpacks": [ - { - "url": "https://github.com/kylef/heroku-buildpack-swift" - } - ] -}
- + - - + + @@ -15,6 +15,6 @@ - +
(_ parameter: P.Type = P.self) throws -> P.ResolvedParameter + where P: Parameter + { + return try self.parameter(P.self, using: self) + } + + /// Infer requested type where the resolved parameter is the parameter type. + public func parameter
() throws -> P + where P: Parameter, P.ResolvedParameter == P + { + return try self.parameter(P.self) + } +} + +extension ParameterContainer { + /// Grabs the next parameter from the parameter bag. + /// + /// Note: the parameters _must_ be fetched in the order they + /// appear in the path. + /// + /// For example GET /posts/:post_id/comments/:comment_id + /// must be fetched in this order: + /// + /// let post = try parameters.next(Post.self) + /// let comment = try parameters.next(Comment.self) + /// + public func parameter
(_ parameter: P.Type = P.self, using container: Container) throws -> P.ResolvedParameter + where P: Parameter + { + guard parameters.count > 0 else { + throw RoutingError(identifier: "insufficientParameters", reason: "Insufficient parameters", source: .capture()) + } + + let current = parameters[0] + guard current.slug == [UInt8](P.uniqueSlug.utf8) else { + throw RoutingError( + identifier: "invalidParameterType", + reason: "Invalid parameter type. Expected \(P.self) got \(current.slug)", + source: .capture() + ) + } + + guard let string = String(bytes: current.value, encoding: .utf8) else { + throw RoutingError( + identifier: "convertString", + reason: "Could not convert the parameter value to a UTF-8 string.", + source: .capture() + ) + } + + let item = try P.make(for: string, using: container) + parameters = Array(parameters.dropFirst()) + return item + } +} diff --git a/Sources/Routing/ParameterValue.swift b/Sources/Routing/ParameterValue.swift new file mode 100644 index 00000000..6cd8b75a --- /dev/null +++ b/Sources/Routing/ParameterValue.swift @@ -0,0 +1,16 @@ +import Foundation + +/// A parameter and its resolved value. +public struct ParameterValue { + /// The parameter type. + let slug: [UInt8] + + /// The resolved value. + let value: [UInt8] + + /// Create a new lazy parameter. + init(slug: [UInt8], value: [UInt8]) { + self.slug = slug + self.value = value + } +} diff --git a/Sources/Routing/Parameterizable.swift b/Sources/Routing/Parameterizable.swift deleted file mode 100644 index c8fa4d17..00000000 --- a/Sources/Routing/Parameterizable.swift +++ /dev/null @@ -1,48 +0,0 @@ -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 - } -} - -extension String: Parameterizable { - public static var uniqueSlug: String { - return "string" - } - - public static func make(for parameter: String) throws -> String { - return parameter - } -} -extension Int: Parameterizable { - public static var uniqueSlug: String { - return "int" - } - - public static func make(for parameter: String) throws -> Int { - guard let int = Int(parameter) else { - throw RouterError.invalidParameter - } - - return int - } -} diff --git a/Sources/Routing/Parameters.swift b/Sources/Routing/Parameters.swift deleted file mode 100644 index f41e4551..00000000 --- a/Sources/Routing/Parameters.swift +++ /dev/null @@ -1,39 +0,0 @@ -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: 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) -} diff --git a/Sources/Routing/PathComponent.swift b/Sources/Routing/PathComponent.swift new file mode 100644 index 00000000..e21b9c14 --- /dev/null +++ b/Sources/Routing/PathComponent.swift @@ -0,0 +1,114 @@ +import Foundation +import Bits + +/// Components of a router path. +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/parameters/) +public enum PathComponent: ExpressibleByStringLiteral { + public enum Parameter { + case data(Data) + case bytes([UInt8]) + case byteBuffer(ByteBuffer) + case string(String) + case substring(Substring) + + public var count: Int { + switch self { + case .data(let data): return data.count + case .byteBuffer(let byteBuffer): return byteBuffer.count + case .bytes(let bytes): return bytes.count + case .string(let string): return string.utf8.count + case .substring(let substring): return substring.utf8.count + } + } + + func withByteBuffer(do closure: (ByteBuffer) -> (T)) -> T { + switch self { + case .data(let data): return data.withByteBuffer(closure) + case .byteBuffer(let byteBuffer): return closure(byteBuffer) + case .bytes(let bytes): return bytes.withUnsafeBufferPointer(closure) + case .string(let string): + let count = self.count + + return string.withCString { pointer in + return pointer.withMemoryRebound(to: UInt8.self, capacity: count) { pointer in + return closure(ByteBuffer(start: pointer, count: count)) + } + } + case .substring(let substring): + let count = self.count + + return substring.withCString { pointer in + return pointer.withMemoryRebound(to: UInt8.self, capacity: count) { pointer in + return closure(ByteBuffer(start: pointer, count: count)) + } + } + } + } + + public var string: String { + switch self { + case .string(let string): return string + default: + return String(bytes: self.bytes, encoding: .utf8) ?? "" + } + } + + public var bytes: [UInt8] { + switch self { + case .data(let data): return Array(data) + case .byteBuffer(let byteBuffer): return Array(byteBuffer) + case .bytes(let bytes): return bytes + case .string(let string): return [UInt8](string.utf8) + case .substring(let substring): return Array(substring.utf8) + } + } + } + + /// Create a path component from a string + public init(stringLiteral value: String) { + self = .constants(value.split(separator: "/").map { .substring($0) } ) + } + + /// A normal, constant path component. + case constants([Parameter]) + + /// A dynamic parameter component. + case parameter(Parameter) + + /// Any set of components + case anything +} + +/// Capable of being represented by a path component. +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/parameters/) +public protocol PathComponentRepresentable { + /// Convert to path component. + func makePathComponent() -> PathComponent +} + +extension PathComponent: PathComponentRepresentable { + /// See PathComponentRepresentable.makePathComponent() + public func makePathComponent() -> PathComponent { + return self + } +} + +// MARK: Array + +extension Array where Element == PathComponentRepresentable { + /// Convert to array of path components. + public func makePathComponents() -> [PathComponent] { + return map { $0.makePathComponent() } + } +} + +/// Strings are constant path components. +extension String: PathComponentRepresentable { + /// Convert string to constant path component. + /// See PathComponentRepresentable.makePathComponent() + public func makePathComponent() -> PathComponent { + return PathComponent(stringLiteral: self) + } +} diff --git a/Sources/Routing/Request+Routing.swift b/Sources/Routing/Request+Routing.swift deleted file mode 100644 index f24b4219..00000000 --- a/Sources/Routing/Request+Routing.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Branches -import HTTP -import Node - -private let parametersKey = "parameters" - -extension HTTP.Request { - /// when routing a url with slug parameters, ie: - /// foo/:id - /// the request will populate these values before passing to handeler - /// for example: - /// given route: /foo/:id - /// and request with path `/foo/123` - /// parameters will be `["id": 123]` - public var parameters: Parameters { - get { - if let existing = storage[parametersKey] as? Parameters { - return existing - } - - let params = Parameters([:]) - storage[parametersKey] = params - return params - } - set { - storage[parametersKey] = newValue - } - } -} diff --git a/Sources/Routing/Route.swift b/Sources/Routing/Route.swift new file mode 100644 index 00000000..7234b58a --- /dev/null +++ b/Sources/Routing/Route.swift @@ -0,0 +1,22 @@ +import Service + +/// A route. When registered to a router, replies to `Request`s using the `Responder`. +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/route/) +public final class Route: Extendable { + /// The path at which the route is assigned + public var path: [PathComponent] + + /// The responder. Used to respond to a `Request` + public var output: Output + + /// A storage place to extend the `Route` with. + /// Can store metadata like Documentation/Route descriptions + public var extend = Extend() + + /// Creates a new route from a Method, path and responder + public init(path: [PathComponent], output: Output) { + self.path = path + self.output = output + } +} diff --git a/Sources/Routing/RouteBuilder+Grouping.swift b/Sources/Routing/RouteBuilder+Grouping.swift deleted file mode 100644 index 700959ce..00000000 --- a/Sources/Routing/RouteBuilder+Grouping.swift +++ /dev/null @@ -1,80 +0,0 @@ -import HTTP - -extension RouteBuilder { - /// Group all subsequent routes built with this builder - /// under this specified host - /// - /// the last host in the chain will take precedence, for example: - /// - /// if using: - /// grouped(host: "0.0.0.0").grouped(host: "196.08.0.1") - /// - /// will bind subsequent additions to '196.08.0.1' - public func grouped(host: String) -> RouteBuilder { - return RouteGroup(host: host, pathPrefix: [], middleware: [], parent: self) - } - - /// Group all subsequent routes behind a specified path prefix - /// use `,` separated list or `/` separated string - /// for example, the following are all equal - /// - /// "a/path/to/foo" - /// "a", "path", "to", "foo" - /// "a/path", "to/foo" - public func grouped(_ path: String...) -> RouteBuilder { - return grouped(path) - } - - /// - see grouped(_ path: String...) - public func grouped(_ path: [String]) -> RouteBuilder { - let components = path.pathComponents - return RouteGroup(host: nil, pathPrefix: components, middleware: [], parent: self) - } - - /// Group all subsequent routes to pass through specified middleware - /// use `,` separated list for multiple middleware - public func grouped(_ middleware: Middleware...) -> RouteBuilder { - return grouped(middleware) - } - - // FIXME: External arg necessary on middleware groups? - - /// - see grouped(middleware: Middleware...) - public func grouped(_ middleware: [Middleware]) -> RouteBuilder { - return RouteGroup(host: nil, pathPrefix: [], middleware: middleware, parent: self) - } -} - -// MARK: Closures - -extension RouteBuilder { - /// Closure based variant of grouped(host: String) - public func group(host: String, handler: (RouteBuilder) -> ()) { - let builder = grouped(host: host) - handler(builder) - } - - /// Closure based variant of grouped(_ path: String...) - public func group(_ path: String ..., handler: (RouteBuilder) -> ()) { - group(path: path, handler: handler) - } - - /// Closure based variant of grouped(_ path: [String]) - public func group(path: [String], handler: (RouteBuilder) -> ()) { - let path = path.pathComponents - let builder = grouped(path) - handler(builder) - } - - // FIXME: Need external parameter cohesiveness - /// Closure based variant of grouped(middleware: Middleware...) - public func group(_ middleware: Middleware..., handler: (RouteBuilder) -> ()) { - group(middleware: middleware, handler: handler) - } - - /// Closure based variant of grouped(middleware: [Middleware]) - public func group(middleware: [Middleware], handler: (RouteBuilder) -> ()) { - let builder = grouped(middleware) - handler(builder) - } -} diff --git a/Sources/Routing/RouteBuilder.swift b/Sources/Routing/RouteBuilder.swift deleted file mode 100644 index 94f79aba..00000000 --- a/Sources/Routing/RouteBuilder.swift +++ /dev/null @@ -1,101 +0,0 @@ -import HTTP -import WebSockets - -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 { - func register(host: String?, method: Method, path: [String], responder: Responder) -} - -extension RouteBuilder { - public func register( - host: String? = nil, - method: Method = .get, - path: [String] = [], - responder: @escaping RouteHandler - ) { - let re = Request.Handler { try responder($0).makeResponse() } - let path = path.pathComponents - register(host: host, method: method, path: path, responder: re) - } - - public func register(method: Method = .get, path: [String] = [], responder: Responder) { - let path = path.pathComponents - register(host: nil, method: method, path: path, responder: responder) - } - -} - -extension RouteBuilder { - #if swift(>=4) - public func add( - _ method: HTTP.Method, - _ path: String ..., - value: @escaping RouteHandler - ) { - let responder = Request.Handler { try value($0).makeResponse() } - register(method: method, path: path, responder: responder) - } - #else - public func add( - _ method: HTTP.Method, - _ path: String ..., - _ value: @escaping RouteHandler - ) { - let responder = Request.Handler { try value($0).makeResponse() } - register(method: method, path: path, responder: responder) - } - #endif - - 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() - } - } -} diff --git a/Sources/Routing/RouteGroup.swift b/Sources/Routing/RouteGroup.swift deleted file mode 100644 index bf022f97..00000000 --- a/Sources/Routing/RouteGroup.swift +++ /dev/null @@ -1,47 +0,0 @@ -import HTTP - -/// RouteGroup is a step in the RouteBuilder chain that -/// allows users to collect metadata about various endpoints -/// -/// for example, if we have several routes that begin with "some/prefix/path" -/// we might want to group those together so that we can easily append -internal final class RouteGroup: RouteBuilder { - let host: String? - let pathPrefix: [String] - let middleware: [Middleware] - let parent: RouteBuilder - - init(host: String?, pathPrefix: [String], middleware: [Middleware], parent: RouteBuilder) { - self.host = host - self.pathPrefix = pathPrefix - self.middleware = middleware - self.parent = parent - } - - func register(host: String?, method: Method, path: [String], responder: Responder) { - let host = host ?? self.host - let path = self.pathPrefix + path - - let res: Responder - if middleware.isEmpty { - res = responder - } else { - let middleware = self.middleware - res = Request.Handler { request in - return try middleware.chain(to: responder).respond(to: request) - } - } - - parent.register(host: host, method: method, path: path, responder: res) - } -} - -extension Collection where Iterator.Element == Middleware { - fileprivate func chain(to responder: Responder) -> Responder { - return reversed().reduce(responder) { nextResponder, nextMiddleware in - return Request.Handler { request in - return try nextMiddleware.respond(to: request, chainingTo: nextResponder) - } - } - } -} diff --git a/Sources/Routing/Router+Responder.swift b/Sources/Routing/Router+Responder.swift deleted file mode 100644 index f9043a2e..00000000 --- a/Sources/Routing/Router+Responder.swift +++ /dev/null @@ -1,105 +0,0 @@ -import HTTP -import Branches -import Debugging - -public var supportOptionsRequests = true - -extension Router: Responder { - public func respond(to request: Request) throws -> Response { - guard let responder = route(request) else { return try fallbackResponse(for: request) } - return try responder.respond(to: request) - } - - private func fallbackResponse(for request: Request) throws -> Response { - guard supportOptionsRequests, request.method == .options else { throw RouterError.missingRoute(for: request) } - return options(for: request) - } - - private func options(for request: Request) -> Response { - let opts = supportedMethods(for: request) - .map { $0.description } - .joined(separator: ", ") - return Response(status: .ok, headers: ["Allow": opts]) - } - - private func supportedMethods(for request: Request) -> [Method] { - let request = request.copy() - guard let host = self.host(for: request.uri.hostname) else { return [] } - let allOptions = host.allSubBranches - let allPossibleMethods = allOptions.map { Method($0.name) } - return allPossibleMethods.filter { method in - request.method = method - return route(request) != nil - } - } - - private func host(for host: String) -> Branch? { - return base.fetch([host]) - } -} - -extension Request { - public func copy() -> Request { - return Request( - method: method, - uri: uri, - version: version, - headers: headers, - body: body - ) - } -} - -public enum RouterError: Debuggable { - case invalidParameter - case missingRoute(for: Request) - case unspecified(Swift.Error) -} - -extension RouterError { - public var identifier: String { - switch self { - case .missingRoute: - return "missingRoute" - case .unspecified: - return "unspecified" - case .invalidParameter: - return "invalidParameter" - } - } - - public var reason: String { - switch self { - case .invalidParameter: - return "invalid parameter" - case .missingRoute(let request): - return "no route found for \(request)" - case .unspecified(let error): - return "unspecified \(error)" - } - } - - public var suggestedFixes: [String] { - switch self { - case .missingRoute(let request): - return [ - "ensure that a route for path '\(request.uri.path)' exists", - "verify the host and httpmethod for the request are as expected", - "log the routes of your router with `router.routes`" - ] - case .unspecified(_): - return [ - "look into upgrading to a version that expects this error", - "try to understand which module threw this error and where it came from" - ] - case .invalidParameter: - return [] - } - } - - public var possibleCauses: [String] { - return [ - "received a route that is not supported by the router" - ] - } -} diff --git a/Sources/Routing/Router.swift b/Sources/Routing/Router.swift deleted file mode 100644 index 514781b7..00000000 --- a/Sources/Routing/Router.swift +++ /dev/null @@ -1,149 +0,0 @@ -import Branches -import Foundation -import HTTP - -public class Router { - /// The base branch from which all routing stems outward - public final let base = Branch(name: "", output: nil) - - /// Init - public init() {} - - /// Register a given path. Use `*` for host OR method to define wildcards that will be matched - /// if no concrete match exists. - /// - /// - parameter host: the host to match, ie: '0.0.0.0', or `*` to match any - /// - parameter method: the method to match, ie: `GET`, or `*` to match any - /// - parameter path: the path that should be registered - /// - parameter output: the associated output of this path, usually a responder, or `nil` - public func register(host: String?, method: HTTP.Method, path: [String], responder: Responder) { - let host = host ?? "*" - let path = [host, method.description] + path.filter { !$0.isEmpty } - base.extend(path, output: responder) - } - - // caches static route resolutions - private var _cache: [String: Responder?] = [:] - private var _cacheLock = NSLock() - - /// Removes all entries from this router's cache. - /// - public func flushCache() { - _cacheLock.lock() - _cache.removeAll() - _cacheLock.unlock() - } - - /// Removes the cached Responder for a given Request. - /// If there is no cached Responder, returns nil. - /// - /// NOTE: If you do not register a new Responder for the - /// Request, the old Responder will be invoked on a subsequent - /// Request and re-cached. I.e. this function does not prune - /// the Branch. - @discardableResult - public func flushCache(for request: Request) -> Responder? { - _cacheLock.lock() - let maybeCached = _cache.removeValue(forKey: request.routeKey) - _cacheLock.unlock() - - if let cached = maybeCached { - return cached - } else { - return nil - } - } - - /// Routes an incoming request - /// the request will be populated with any found parameters (aka slugs). - /// - /// If a handler is found, it is returned. - public func route(_ request: Request) -> Responder? { - let key = request.routeKey - - // check the static route cache - - _cacheLock.lock() - let maybeCached = _cache[key] - _cacheLock.unlock() - - if let cached = maybeCached { - return cached - } - - let path = request.path() - let result = base.fetch(path) - - request.parameters = result?.slugs(for: path) ?? [:] - - // if there are no dynamic slugs, we can cache - if request.parameters.object?.isEmpty == true { - _cacheLock.lock() - _cache[key] = result?.output - _cacheLock.unlock() - } - - return result?.output - } -} - -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 - fileprivate var routeKey: String { - return uri.hostname - + ";" + method.description - + ";" + uri.path - } - - fileprivate func path() -> [String] { - var host: String = uri.hostname - if host.isEmpty { host = "*" } - let method = self.method.description - let components = uri.path.pathComponents - return [host, method] + components - } -} - -extension Router { - public var routes: [String] { - return base.routes.map { input in - var comps = input.pathComponents.makeIterator() - let host = comps.next() ?? "*" - let method = comps.next() ?? "*" - let path = comps.joined(separator: "/") - return "\(host) \(method) \(path)" - } - } -} - -extension Router: RouteBuilder {} diff --git a/Sources/Routing/RoutingError.swift b/Sources/Routing/RoutingError.swift new file mode 100644 index 00000000..8f52d7b2 --- /dev/null +++ b/Sources/Routing/RoutingError.swift @@ -0,0 +1,22 @@ +import Debugging + +/// Errors that can be thrown while working with TCP sockets. +public struct RoutingError: Debuggable { + public static let readableName = "Routing Error" + public var identifier: String + public var reason: String + public var sourceLocation: SourceLocation? + public var stackTrace: [String] + + /// Create a new TCP error. + init( + identifier: String, + reason: String, + source: SourceLocation + ) { + self.identifier = identifier + self.reason = reason + self.sourceLocation = source + self.stackTrace = RoutingError.makeStackTrace() + } +} diff --git a/Sources/Routing/TrieRouter.swift b/Sources/Routing/TrieRouter.swift new file mode 100644 index 00000000..3b5afc71 --- /dev/null +++ b/Sources/Routing/TrieRouter.swift @@ -0,0 +1,113 @@ +import Async +import Foundation +import Bits + +/// A basic router that can route requests depending on the method and URI +/// +/// [Learn More →](https://docs.vapor.codes/3.0/routing/router/) +public final class TrieRouter { + /// All routes registered to this router + public private(set) var routes: [Route] = [] + + /// The root node + var root: TrieRouterNode + + /// If a route cannot be found, this is the fallback responder that will be used instead + public var fallback: Output? + + /// If `true`, constants are compared case insensitively + public var caseInsensitive: Bool + + /// Create a new trie router + public init() { + self.caseInsensitive = false + self.root = TrieRouterNode(kind: .root) + } + + /// See Router.register() + public func register(route: Route) { + self.routes.append(route) + var current = root + + for component in route.path { + current = current[component] + } + + current.output = route.output + } + + /// See Router.route() + public func route(path: [PathComponent.Parameter], parameters: ParameterContainer) -> Output? { + // always start at the root node + var current: TrieRouterNode = root + var parameterNode: (TrieRouterNode, [UInt8])? + var fallbackNode: TrieRouterNode? + + // traverse the constant path supplied + nextComponent: for component in path { + // Reset state to ensure a previous resolved path isn't interfering + parameterNode = nil + fallbackNode = nil + + for child in current.children { + switch child.kind { + case .anything: + fallbackNode = child + case .constant(let data, _): + // if we find a constant route path that matches this component, + // then we should use it. + let match = component.withByteBuffer { buffer -> Bool in + if self.caseInsensitive { + return data.caseInsensitiveEquals(to: buffer) + } else { + return data.elementsEqual(buffer) + } + } + + if match { + current = child + continue nextComponent + } + case .parameter(let parameter): + parameterNode = (child, parameter) + case .root: + fatalError("Incorrect nested 'root' routing node") + } + } + + if let (node, parameter) = parameterNode { + // if no constant routes were found that match the path, but + // a dynamic parameter child was found, we can use it + let lazy = ParameterValue(slug: parameter, value: component.bytes) + parameters.parameters.append(lazy) + current = node + continue nextComponent + } + + guard let fallbackNode = fallbackNode else { + // No results found + return fallback + } + + current = fallbackNode + } + + // return the resolved responder if there hasn't + // been an early exit. + return current.output ?? fallback + } +} + +extension TrieRouterNode { + fileprivate func find(constants: [[UInt8]]) -> TrieRouterNode? { + let constant = constants[0] + + let node = self.findConstant(constant) + + if constants.count > 1 { + return node?.find(constants: Array(constants[1...])) + } else { + return node + } + } +} diff --git a/Sources/Routing/TrieRouterNode+Create.swift b/Sources/Routing/TrieRouterNode+Create.swift new file mode 100644 index 00000000..3dab8f3c --- /dev/null +++ b/Sources/Routing/TrieRouterNode+Create.swift @@ -0,0 +1,82 @@ +extension TrieRouterNode { + fileprivate func find(constants: [[UInt8]]) -> TrieRouterNode { + let constant = constants[0] + + let node: TrieRouterNode + + if let found = self.findConstant(constant) { + node = found + } else { + node = TrieRouterNode(kind: .constant(data: constant, dataSize: constant.count)) + self.children.append(node) + } + + if constants.count > 1 { + return node.find(constants: Array(constants[1...])) + } else { + return node + } + } + + fileprivate func find(component: PathComponent) -> TrieRouterNode { + switch component { + case .constants(let constants): + if constants.count == 0 { + return self + } else { + return self.find(constants: constants.map { $0.bytes }) + } + case .parameter(let p): + if let node = self.findParameterNode() { + return node + } else { + let node = TrieRouterNode(kind: .parameter(data: p.bytes)) + self.children.append(node) + return node + } + case .anything: + if let node = findAnyNode() { + return node + } else { + let node = TrieRouterNode(kind: .anything) + self.children.append(node) + return node + } + } + } + + /// Returns the first parameter node + fileprivate func findParameterNode() -> TrieRouterNode? { + for child in children { + if case .parameter = child.kind { + return child + } + } + + return nil + } + + func findConstant(_ buffer: [UInt8]) -> TrieRouterNode? { + for child in children { + if case .constant(let data, _) = child.kind, data == buffer { + return child + } + } + + return nil + } + + fileprivate func findAnyNode() -> TrieRouterNode? { + for child in children { + if case .anything = child.kind { + return child + } + } + + return nil + } + + subscript(path: PathComponent) -> TrieRouterNode { + return self.find(component: path) + } +} diff --git a/Sources/Routing/TrieRouterNode.swift b/Sources/Routing/TrieRouterNode.swift new file mode 100644 index 00000000..056a842d --- /dev/null +++ b/Sources/Routing/TrieRouterNode.swift @@ -0,0 +1,34 @@ +import Foundation +import Bits + +final class TrieRouterNode { + /// Kind of node + var kind: TrieRouterNodeKind + + /// All constant child nodes + var children: [TrieRouterNode] + + /// This node's output + var output: Output? + + init( + kind: TrieRouterNodeKind, + children: [TrieRouterNode] = [], + output: Output? = nil + ) { + self.kind = kind + self.children = children + self.output = output + } +} + +enum TrieRouterNodeKind { + case root + + // Size is separate to save ARC performance, which had a huge impact here + case parameter(data: [UInt8]) + + case constant(data: [UInt8], dataSize: Int) + + case anything +} diff --git a/Sources/Routing/Utilities.swift b/Sources/Routing/Utilities.swift deleted file mode 100644 index 5e2b1131..00000000 --- a/Sources/Routing/Utilities.swift +++ /dev/null @@ -1,30 +0,0 @@ -extension String { - /// Separates a URI path into - /// an array by splitting on `/` - internal var pathComponents: [String] { - return toCharacterSequence() - .split(separator: "/", omittingEmptySubsequences: true) - .map { String($0) } - } -} - -extension Sequence where Iterator.Element == String { - /// Ensures that `/` are interpreted properly on arrays - /// of path components, so `["foo", "bar/dar"]` - /// will expand to `["foo", "bar", "dar"]` - internal var pathComponents: [String] { - return flatMap { $0.pathComponents } .filter { !$0.isEmpty } - } -} - -extension String { - #if swift(>=4.0) - internal func toCharacterSequence() -> String { - return self - } - #else - internal func toCharacterSequence() -> CharacterView { - return self.characters - } - #endif -} diff --git a/Sources/TypeSafeGenerator/Body.swift b/Sources/TypeSafeGenerator/Body.swift deleted file mode 100644 index 28a76f0d..00000000 --- a/Sources/TypeSafeGenerator/Body.swift +++ /dev/null @@ -1,81 +0,0 @@ -struct Body { - var signature: Signature - - init(signature: Signature) { - self.signature = signature - } -} - -extension Body: CustomStringConvertible { - var description: String { - return [ - "self.add(.\(signature.method), \(path)) { request in", - innerBody.indented, - "}", - ].joined(separator: "\n") - } - - var path: String { - return signature.parameters.map { parameter in - switch parameter { - case .path(let path): - return path.name - case .wildcard(let wildcard): - return "\":\(wildcard.name)\"" - } - }.joined(separator: ", ") - } - - var innerBody: String { - return [ - badRequestGuards, - stringInitializeTrys, - invalidParameterGuards, - returns - ].joined(separator: "\n") - } - - var badRequestGuards: String { - return signature.wildcards.map { wildcard in - return [ - "guard let v\(wildcard.name) = request.parameters[\"\(wildcard.name)\"]?.string else {", - " throw TypeSafeRoutingError.missingParameter", - "}" - ].joined(separator: "\n") - }.joined(separator: "\n") - } - - var stringInitializeTrys: String { - return signature.wildcards.map { wildcard in - return "let e\(wildcard.name) = try \(wildcard.generic)(from: v\(wildcard.name))\n" - }.joined(separator: "\n") - } - - var invalidParameterGuards: String { - return signature.wildcards.map { wildcard in - return [ - "guard let c\(wildcard.name) = e\(wildcard.name) else {", - " throw TypeSafeRoutingError.invalidParameterType(\(wildcard.generic).self)", - "}" - ].joined(separator: "\n") - }.joined(separator: "\n") - } - - var returns: String { - let additions: String - if signature.wildcards.count > 0 { - additions = "," + signature.wildcards.map { wildcard in - return "c\(wildcard.name)" - }.joined(separator: ", ") - } else { - additions = "" - } - - switch signature.variant { - case .socket: - return "return try request.upgradeToWebSocket { try handler(request, $0\(additions)) }" - case .base: - return "return try handler(request\(additions)).makeResponse()" - } - } -} diff --git a/Sources/TypeSafeGenerator/Function.swift b/Sources/TypeSafeGenerator/Function.swift deleted file mode 100644 index 27480c28..00000000 --- a/Sources/TypeSafeGenerator/Function.swift +++ /dev/null @@ -1,19 +0,0 @@ -struct Function { - var signature: Signature - var body: Body - - init(variant: Variant, method: Method, parameters: [Parameter]) { - signature = Signature(variant: variant, method: method, parameters: parameters) - body = Body(signature: signature) - } -} - -extension Function: CustomStringConvertible { - var description: String { - return [ - "\(signature.description) {", - body.description.indented, - "}" - ].joined(separator: "\n") - } -} diff --git a/Sources/TypeSafeGenerator/Generator.swift b/Sources/TypeSafeGenerator/Generator.swift deleted file mode 100644 index 480a93d0..00000000 --- a/Sources/TypeSafeGenerator/Generator.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation - -class Generator { - var parameterMax: Int - - init(max parameters: Int) { - parameterMax = parameters - } - - func generate() -> String { - let permutations = generatePermutations() - - var functions: [Function] = [] - - for parameters in permutations { - if parameters.count == 0 { - continue - } - - let function = Function(variant: .socket, method: .get, parameters: parameters) - functions.append(function) - - for method: Method in [.get, .post, .put, .patch, .delete, .options] { - let function = Function(variant: .base, method: method, parameters: parameters) - functions.append(function) - } - } - - var generated = [ - warning, - "import Branches", - "import HTTP", - "import Routing", - "import WebSockets", - " ", - "extension RouteBuilder {", - ] - for function in functions { - generated.append(function.description.indented) - } - generated.append("}") - return generated.joined(separator: "\n") - } - - private func generatePermutations() -> [[Parameter]] { - var permutations: [[Parameter]] = [[]] - - for i in 0 ... parameterMax { - var subPermutations: [[Parameter]] = [[]] - - for _ in 0 ..< i { - subPermutations = permutate(subPermutations) - } - - permutations += subPermutations - } - - return permutations - } - - - private func permutate(_ array: [[Parameter]]) -> [[Parameter]] { - var result: [[Parameter]] = [] - - for subarray in array { - var pathArray = subarray - var wildcardArray = subarray - - let path = Parameter.pathFor(pathArray) - pathArray.append(path) - - let wildcard = Parameter.wildcardFor(wildcardArray) - wildcardArray.append(wildcard) - - result.append(pathArray) - result.append(wildcardArray) - } - - return result - } - -} - -extension Generator { - var warning: String { - return [ - "/*", - " ⚠️ AUTOMATICALLY GENERATED CODE", - " ", - " Generated by Sources/Generator/main.swift.", - " Do not edit this file directly.", - " ", - " Last generated: \(NSDate())", - "*/" - ].joined(separator: "\n") - } -} diff --git a/Sources/TypeSafeGenerator/Method.swift b/Sources/TypeSafeGenerator/Method.swift deleted file mode 100644 index 0386a931..00000000 --- a/Sources/TypeSafeGenerator/Method.swift +++ /dev/null @@ -1,12 +0,0 @@ -enum Method { - case get, post, put, patch, delete, options -} - -extension Method { - var uppercase: String { - return "\(self)".uppercased() - } - var lowercase: String { - return "\(self)".lowercased() - } -} diff --git a/Sources/TypeSafeGenerator/Operators.swift b/Sources/TypeSafeGenerator/Operators.swift deleted file mode 100644 index 2d2f05ad..00000000 --- a/Sources/TypeSafeGenerator/Operators.swift +++ /dev/null @@ -1,16 +0,0 @@ -infix operator <<< - -func <<<(lhs: inout String, rhs: String) { - return lhs.append(rhs) -} - -func <<(lhs: inout String, rhs: String) { - return lhs.append(rhs + "\n") -} - -extension String { - var indented: String { - let split = self.characters.split(separator: "\n") - return split.map { " " + String($0) }.joined(separator: "\n") - } -} diff --git a/Sources/TypeSafeGenerator/Parameter.swift b/Sources/TypeSafeGenerator/Parameter.swift deleted file mode 100644 index fc47dbe2..00000000 --- a/Sources/TypeSafeGenerator/Parameter.swift +++ /dev/null @@ -1,66 +0,0 @@ -enum Parameter { - struct Wildcard { - var name: String - var generic: String - } - - struct Path { - var name: String - } - - case path(Path) - case wildcard(Wildcard) - - var name: String { - switch self { - case .path(let path): - return path.name - case .wildcard(let wildcard): - return wildcard.name - } - } - - var isPath: Bool { - switch self { - case.path(_): - return true - default: - return false - } - } - - var isWildcard: Bool { - switch self { - case.wildcard(_): - return true - default: - return false - } - } - - static func pathFor(_ array: [Parameter]) -> Parameter { - var i = 0 - - for item in array { - if item.isPath { - i += 1 - } - } - - let path = Path(name: "p\(i)") - return .path(path) - } - - static func wildcardFor(_ array: [Parameter]) -> Parameter { - var i = 0 - - for item in array { - if item.isWildcard { - i += 1 - } - } - - let path = Wildcard(name: "w\(i)", generic: "W\(i)") - return .wildcard(path) - } -} \ No newline at end of file diff --git a/Sources/TypeSafeGenerator/Signature+Documentation.swift b/Sources/TypeSafeGenerator/Signature+Documentation.swift deleted file mode 100644 index 32d3d3d5..00000000 --- a/Sources/TypeSafeGenerator/Signature+Documentation.swift +++ /dev/null @@ -1,69 +0,0 @@ -extension Signature { - var documentation: String { - var d = "" - - d << "/**" - - if variant == .socket { - d << " Establishes a WebSocket connection" - d << " at the given path. WebSocket connections" - d << " can be accessed using the `ws://` or `wss://`" - d << " schemes to provide two way information" - d << " transfer between the client and the server." - d << " " - d << " **Body**" - d << " The body closure is given access to the Request" - d << " that started the connection as well as the WebSocket." - d << " " - d << " drop.socket(\"test\") { request, ws in" - d << " " - d << " }" - d << " " - d << " **Sending Data**" - d << " " - d << " Data is sent to the WebSocket stream using `send(_:Data)`" - d << " " - d << " try ws.send(\"Hello, world\")" - d << " " - d << " **Receiving Data**" - d << " " - d << " Data is received from the WebSocket using" - d << " the `onText` callback." - d << " " - d << " ws.onText = { ws, text in" - d << " drop.console.output(\"Received \\(text)\")" - d << " }" - d << " " - d << " **Closing**" - d << " " - d << " Close the Socket when you are done." - - d << " try ws.close()" - d << " " - d << " **Routing**" - d << " " - } - - - d << " This route will run for any \(method.uppercase) request" - d << " to a path that matches:" - d << "" - - d <<< " /" - - for parameter in parameters { - switch parameter { - case .path(_): - d <<< "/" - case .wildcard(_): - d <<< "{wildcard}/" - } - } - - d << " " - - d <<< "*/" - - return d - } -} diff --git a/Sources/TypeSafeGenerator/Signature.swift b/Sources/TypeSafeGenerator/Signature.swift deleted file mode 100644 index 51141d15..00000000 --- a/Sources/TypeSafeGenerator/Signature.swift +++ /dev/null @@ -1,142 +0,0 @@ -struct Signature { - var variant: Variant - var method: Method - var parameters: [Parameter] - - init(variant: Variant, method: Method, parameters: [Parameter]) { - self.variant = variant - self.method = method - self.parameters = parameters - } -} - -extension Signature { - var wildcards: [Parameter.Wildcard] { - return parameters.flatMap { parameter in - if case .wildcard(let wildcard) = parameter { - return wildcard - } - - return nil - } - } - - var paths: [Parameter.Path] { - return parameters.flatMap { parameter in - if case .path(let path) = parameter { - return path - } - - return nil - } - } -} - -/** - Creates the function signature. - - <--- documentation --> - - /** - Blah blah blah ... - */ - - public func get(_ p0: String, _ w0: T, handler: (Request, T) -> ResponseRepresentable) - - <----- generic map ----> - <----- list --------> - handler input - <---------> <-- handler output --> - - <------------------- input -----------------> - - <-------------------------------------------- description ----------------------------------------------------> -*/ -extension Signature: CustomStringConvertible { - var description: String { - return [ - documentation, - "public func \(name)\(generics)(\(input))" - ].joined(separator: "\n") - } -} - -extension Signature { - var input: String { - if parameters.count > 0 { - return "\(list), \(handler)" - } else { - return handler - } - } - - var list: String { - if - parameters.count == 1, - let first = parameters.first, - case .path(let path) = first - { - return "_ \(path.name): String = \"\"" - } else { - return parameters.map { parameter in - var string = "_ \(parameter.name): " - - switch parameter { - case .path(_): - string <<< "String" - case .wildcard(let wildcard): - string <<< "\(wildcard.generic).Type" - } - - return string - }.joined(separator: ", ") - } - } - - var handler: String { - return "handler: @escaping (\(handlerInput)) throws -> \(handlerOutput)" - } - - var handlerInput: String { - var items = ["Request"] - - if variant == .socket { - items.append("WebSocket") - } - - items += wildcards.map { $0.generic } - - return items.joined(separator: ", ") - } - - var handlerOutput: String { - switch variant { - case .socket: - return "()" - case .base: - return "ResponseRepresentable" - } - } - - var generics: String { - if genericMap.characters.count == 0 { - return "" - } - return "<\(genericMap)>" - } - - var genericMap: String { - return wildcards.map { wildcard in - return "\(wildcard.generic): StringInitializable" - }.joined(separator: ", ") - } - - var name: String { - switch variant { - case .socket: - return "socket" - case .base: - return method.lowercase - } - } -} diff --git a/Sources/TypeSafeGenerator/Variant.swift b/Sources/TypeSafeGenerator/Variant.swift deleted file mode 100644 index 7a792af4..00000000 --- a/Sources/TypeSafeGenerator/Variant.swift +++ /dev/null @@ -1,3 +0,0 @@ -enum Variant { - case base, socket -} diff --git a/Sources/TypeSafeGenerator/main.swift b/Sources/TypeSafeGenerator/main.swift deleted file mode 100644 index 11692b45..00000000 --- a/Sources/TypeSafeGenerator/main.swift +++ /dev/null @@ -1,39 +0,0 @@ -#if !os(Linux) -import Foundation - -let generator = Generator(max: 3) -let code = generator.generate() - -if CommandLine.arguments.count < 2 { - print("⚠️ IMPORTANT") - print("To run the Generator, you must pass the $(SRCROOT)") - print("as the first argument to the executable.") - print("") - print("This can be done using 'Edit Scheme'") - print("") - fatalError("$(SRCROOT) must be passed as a parameter") -} - -let path = CommandLine.arguments[1].replacingOccurrences(of: "XcodeProject", with: "") -let url = URL(fileURLWithPath: path + "/Sources/TypeSafeRouting/Generated.swift") - -do{ - let lines = code.characters.split(separator: "\n").count - let functions = code.components(separatedBy: "func").count - 1 - - // writing to disk - try code.write(to: url, atomically: true, encoding: .utf8) - print("✅ Code successfully generated.") - print("Functions: \(functions)") - print("Lines: \(lines)") - print("Location: \(url)") - print("Date: \(NSDate())") -} catch let error as NSError { - print("Error writing generated file at \(url)") - print(error.localizedDescription) -} - -#else - print("Linux not supported by generator.") -#endif - diff --git a/Tests/BranchesTests/BranchTests.swift b/Tests/BranchesTests/BranchTests.swift deleted file mode 100644 index 8d94233b..00000000 --- a/Tests/BranchesTests/BranchTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -import XCTest -import Branches - -class BranchTests: XCTestCase { - static let allTests = [ - ("testSimple", testSimple), - ("testWildcard", testWildcard), - ("testWildcardTrailing", testWildcardTrailing), - ("testLeadingPath", testLeadingPath), - ("testEmpty", testEmpty) - ] - - func testSimple() { - let base = Branch(name: "[base]", output: nil) - base.extend(["a", "b", "c"], output: "abc") - let result = base.fetch(["a", "b", "c"]) - XCTAssert(result?.output == "abc") - } - - func testWildcard() { - let base = Branch(name: "[base]", output: nil) - base.extend(["a", "b", "c", "*"], output: "abc") - let result = base.fetch(["a", "b", "c"]) - XCTAssert(result?.output == "abc") - } - - func testWildcardTrailing() { - let base = Branch(name: "[base]", output: nil) - base.extend(["a", "b", "c", "*"], output: "abc") - guard let result = base.fetch(["a", "b", "c", "d", "e", "f"]) else { - XCTFail("invalid wildcard fetch") - return - } - - XCTAssert(result.output == "abc") - } - - func testLeadingPath() { - let base = Branch(name: "[base]", output: nil) - let subBranch = base.extend([":a", ":b", ":c", "*"], output: "abc") - XCTAssert(subBranch.path == [":a", ":b", ":c", "*"]) - } - - func testEmpty() { - let base = Branch(name: "[base]", output: nil) - base.extend(["a", "b", "c"], output: "abc") - let result = base.fetch(["z"]) - XCTAssert(result == nil) - - } -} diff --git a/Tests/BranchesTests/RouteExtractionTests.swift b/Tests/BranchesTests/RouteExtractionTests.swift deleted file mode 100644 index f4e620df..00000000 --- a/Tests/BranchesTests/RouteExtractionTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// RouteExtractionTests.swift -// Routing -// -// Created by Logan Wright on 11/1/16. -// -// - -import XCTest -@testable import Branches -import HTTP - -class RouteExtractionTests: XCTestCase { - static let allTests = [ - ("testRouteLog", testRouteLog), - ("testIndividualBranches", testIndividualBranches), - ("testIndividualBranchesWithOutput", testIndividualBranchesWithOutput), - ("testBranchRoutes", testBranchRoutes), - ] - - func testRouteLog() throws { - let base = Branch(name: "a") - XCTAssertEqual(base.route, "/a") - - let extended = base.extend(["b", "c"], output: 2) - XCTAssertEqual(extended.route, "/a/b/c") - - let wild = base.extend(["*", ":foo"], output: 3) - XCTAssertEqual(wild.route, "/a/*/:foo") - } - - func testIndividualBranches() throws { - let a = Branch(name: "a") - let b = Branch(name: "b") - let c = Branch(name: "c") - let d = Branch(name: "d") - let e = Branch(name: "e") - - a.testableSetBranch(key: "b", branch: b) - a.testableSetBranch(key: "c", branch: c) - c.testableSetBranch(key: "d", branch: d) - c.testableSetBranch(key: "e", branch: e) - - let allBranches = a.allIndividualBranchesInTreeIncludingSelf.map { $0.name } - XCTAssertEqual(Set(allBranches), Set([a, b, c, d, e].map { $0.name })) - } - - func testIndividualBranchesWithOutput() throws { - let a = Branch(name: "a", output: 1) - let b = Branch(name: "b") - let c = Branch(name: "c", output: 2) - let d = Branch(name: "d") - let e = Branch(name: "e", output: 3) - - a.testableSetBranch(key: "b", branch: b) - a.testableSetBranch(key: "c", branch: c) - c.testableSetBranch(key: "d", branch: d) - c.testableSetBranch(key: "e", branch: e) - - let allBranches = a.allBranchesWithOutputIncludingSelf.map { $0.name } - XCTAssertEqual(allBranches, [a, c, e].map { $0.name }) - } - - func testBranchRoutes() throws { - let a = Branch(name: "a", output: 1) - let b = Branch(name: "b") - let c = Branch(name: "c", output: 2) - let d = Branch(name: "d") - let e = Branch(name: "e", output: 3) - - a.testableSetBranch(key: "b", branch: b) - a.testableSetBranch(key: "c", branch: c) - c.testableSetBranch(key: "d", branch: d) - c.testableSetBranch(key: "e", branch: e) - - let expectation = ["/a", "/a/c", "/a/c/e"] - XCTAssertEqual(a.routes, expectation) - } -} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 08fde9af..30f5f48a 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,20 +2,9 @@ import XCTest @testable import RoutingTests -@testable import BranchesTests XCTMain([ - // Branches - testCase(BranchTests.allTests), - testCase(RouteExtractionTests.allTests), - - // Routing - testCase(AddTests.allTests), - testCase(GroupedTests.allTests), - testCase(GroupTests.allTests), testCase(RouterTests.allTests), - testCase(RouteTests.allTests), - testCase(RouteBuilderTests.allTests), ]) #endif diff --git a/Tests/RoutingTests/AddTests.swift b/Tests/RoutingTests/AddTests.swift deleted file mode 100644 index e84b50dc..00000000 --- a/Tests/RoutingTests/AddTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import XCTest -import HTTP -import Routing - -class AddTests: XCTestCase { - static var allTests = [ - ("testBasic", testBasic), - ("testVariadic", testVariadic), - ("testWithSlash", testWithSlash), - ] - - func testBasic() throws { - let router = Router() - router.add(.get, "ferret") { request in - return "foo" - } - - let request = Request(method: .get, path: "ferret") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "foo".makeBytes()) - } - - func testVariadic() throws { - let router = Router() - router.add(.trace, "foo", "bar", "baz") { request in - return "1337" - } - - let request = Request(method: .trace, path: "foo/bar/baz") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "1337".makeBytes()) - } - - func testWithSlash() throws { - let router = Router() - router.add(.get, "foo/bar") { request in - return "foo" - } - - let request = Request(method: .get, path: "foo/bar") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "foo".makeBytes()) - } -} diff --git a/Tests/RoutingTests/GroupTests.swift b/Tests/RoutingTests/GroupTests.swift deleted file mode 100644 index 8efe63f1..00000000 --- a/Tests/RoutingTests/GroupTests.swift +++ /dev/null @@ -1,101 +0,0 @@ -import XCTest -import HTTP -import Routing - -class GroupTests: XCTestCase { - static var allTests = [ - ("testBasic", testBasic), - ("testVariadic", testVariadic), - ("testHost", testHost), - ("testHostMiss", testHostMiss), - ("testMiddleware", testMiddleware), - ] - - func testBasic() throws { - let router = Router() - - router.group("users") { users in - users.add(.get, ":id") { request in - return "show" - } - } - - let request = Request(method: .get, path: "users/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "show".makeBytes()) - XCTAssertEqual(request.parameters["id"], "5") - } - - func testVariadic() throws { - let router = Router() - - router.group("users", "devices", "etc") { users in - users.add(.get, ":id") { request in - return "show" - } - } - let request = Request(method: .get, path: "users/devices/etc/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "show".makeBytes()) - XCTAssertEqual(request.parameters["id"], "5") - } - - func testHost() throws { - let router = Router() - - router.group(host: "192.168.0.1") { host in - host.add(.get, "host-only") { request in - return "host" - } - } - router.add(.get, "host-only") { req in - return "nothost" - } - - let request = Request(method: .get, path: "host-only", host: "192.168.0.1") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "host".makeBytes()) - } - - func testHostMiss() throws { - let router = Router() - - router.group(host: "192.168.0.1") { host in - host.add(.get, "host-only") { request in - return "host" - } - } - router.add(.get, "host-only") { req in - return "nothost" - } - - let request = Request(method: .get, path: "host-only", host: "BADHOST") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "nothost".makeBytes()) - } - - func testMiddleware() throws { - class Middy: Middleware { - func respond(to request: Request, chainingTo next: Responder) throws -> Response { - request.storage["middleware"] = true - return try next.respond(to: request) - } - } - - let router = Router() - router.group(Middy()) { builder in - builder.register { _ in return "hello" } - } - - let request = Request(method: .get, path: "/") - XCTAssertNil(request.storage["middleware"]) - let response = try router.respond(to: request) - let middleware = request.storage["middleware"] as? Bool - XCTAssertEqual(middleware, true) - XCTAssertEqual(response.body.bytes?.makeString(), "hello") - } -} diff --git a/Tests/RoutingTests/GroupedTests.swift b/Tests/RoutingTests/GroupedTests.swift deleted file mode 100644 index 0a49d363..00000000 --- a/Tests/RoutingTests/GroupedTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -import XCTest -import HTTP -import Routing - -class GroupedTests: XCTestCase { - static let allTests = [ - ("testBasic", testBasic), - ("testVariadic", testVariadic), - ("testHost", testHost), - ("testChained", testChained), - ("testMultiChained", testMultiChained), - ] - - func testBasic() throws { - let router = Router() - - let users = router.grouped("users") - users.register(method: .get, path: [":id"]) { request in - return "show" - } - - let request = Request(method: .get, path: "users/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes.makeString(), "show") - XCTAssertEqual(request.parameters["id"], "5") - } - - func testVariadic() throws { - let router = Router() - - let users = router.grouped("users", "devices", "etc") - users.add(.get, ":id") { request in - return "show" - } - - let request = Request(method: .get, path: "users/devices/etc/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes, "show".makeBytes()) - XCTAssertEqual(request.parameters["id"], "5") - } - - func testHost() throws { - let router = Router() - let host = router.grouped(host: "192.168.0.1") - host.register(method: .get, path: ["host-only"]) { request in - return "host group found" - } - - router.register(method: .get, path: ["host-only"]) { _ in return "nothost" } - let request = Request(method: .get, path: "host-only", host: "192.168.0.1") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes.makeString(), "host group found") - } - - func testChained() throws { - let router = Router() - - let users = router.grouped("users", "devices", "etc").grouped("even", "deeper") - users.add(.get, ":id") { request in - return "show" - } - - let request = Request(method: .get, path: "users/devices/etc/even/deeper/5") - let bytes = try request.bytes(running: router) - - XCTAssertEqual(bytes.makeString(), "show") - XCTAssertEqual(request.parameters["id"], "5") - } - - func testMultiChained() throws { - class Middy: Middleware { - func respond(to request: Request, chainingTo next: Responder) throws -> Response { - request.storage["middleware"] = true - return try next.respond(to: request) - } - } - - let router = Router() - let builder = router.grouped("a", "path").grouped(Middy()).grouped(host: "9.9.9.9") - builder.add(.get, "/") { req in - return "got it" - } - - let request = Request(method: .get, path: "a/path", host: "9.9.9.9") - let responder = router.route(request) - let response = try responder?.respond(to: request) - XCTAssertNotNil(response) - XCTAssertEqual(response?.body.bytes?.makeString(), "got it") - let middleware = request.storage["middleware"] as? Bool - XCTAssertEqual(middleware, true) - - let bad = Request(method: .get, path: "a/path", host: "0.0.0.0") - XCTAssertNil(router.route(bad)) - } -} diff --git a/Tests/RoutingTests/RouteBuilderTests.swift b/Tests/RoutingTests/RouteBuilderTests.swift deleted file mode 100644 index 9cde1816..00000000 --- a/Tests/RoutingTests/RouteBuilderTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -import XCTest - -import HTTP -import Routing - -class RouteBuilderTests: XCTestCase { - static let allTests = [ - ("testBasic", testBasic), - ("testVariadic", testVariadic), - ("testMoreThanThreeArgs", testMoreThanThreeArgs), - ("testCustomMethod", testCustomMethod), - ("testAll", testAll), - ] - - func testBasic() throws { - let builder = Dropped() - builder.get("hello") { _ in - return "world!" - } - - let request = Request(method: .get, path: "hello") - let bytes = try request.bytes(running: builder.router) - - XCTAssertEqual(bytes, "world!".makeBytes()) - } - - func testVariadic() throws { - let builder = Dropped() - builder.delete("foo", "bar", "baz") { _ in - return "1337" - } - - let request = Request(method: .delete, path: "foo/bar/baz") - let bytes = try request.bytes(running: builder.router) - - XCTAssertEqual(bytes, "1337".makeBytes()) - } - - func testMoreThanThreeArgs() throws { - let builder = Dropped() - builder.post(":userId", "messages", ":messageId", "read") { _ in - return "Please don't read this" - } - - let request = Request(method: .post, path: "1/messages/10/read") - let bytes = try request.bytes(running: builder.router) - - XCTAssertEqual(bytes, "Please don't read this".makeBytes()) - } - - func testCustomMethod() throws { - let builder = Dropped() - builder.add(.other(method: "custom"), "custom", "method") { _ in - return "Custom method" - } - - let request = Request(method: .other(method: "custom"), path: "custom/method") - let bytes = try request.bytes(running: builder.router) - - XCTAssertEqual(bytes, "Custom method".makeBytes()) - } - - func testAll() throws { - let builder = Dropped() - builder.all("all", "methods") { _ in - return "All around the world (repeat 144)" - } - - let methods: [HTTP.Method] = [ - .delete, .get, .head, .post, .put, .connect, .options, .trace, .patch, .other(method: "other") - ] - - methods.forEach { - let request = Request(method: $0, path: "all/methods") - - do { - let bytes = try request.bytes(running: builder.router) - XCTAssertEqual(bytes, "All around the world (repeat 144)".makeBytes()) - } catch { - XCTFail("Routing failed: \(error) for method: \($0)") - } - } - } -} - -/// A mock for RouteBuilder -final class Dropped: RouteBuilder { - let router = Router() - - public func register(host: String?, method: HTTP.Method, path: [String], responder: Responder) { - router.register(host: host, method: method, path: path, responder: responder) - } -} diff --git a/Tests/RoutingTests/RouteTests.swift b/Tests/RoutingTests/RouteTests.swift deleted file mode 100644 index c0d86221..00000000 --- a/Tests/RoutingTests/RouteTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -import XCTest -import HTTP -import URI -import Routing -import Node - -class RouteTests: XCTestCase { - static let allTests = [ - ("testRoute", testRoute), - ("testRouteParams", testRouteParams), - ("testParameters", testParameters), - ] - - func testRoute() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { req in - return Response(status: .ok, body: "HI") - } - - let request = Request(method: .get, uri: "http://0.0.0.0/hello") - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "HI") - } - - func testRouteParams() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: [":zero", ":one", ":two", "*"]) { req in - let zero = req.parameters["zero"]?.string ?? "[fail]" - let one = req.parameters["one"]?.string ?? "[fail]" - let two = req.parameters["two"]?.string ?? "[fail]" - return Response(status: .ok, body: "\(zero):\(one):\(two)") - } - - let paths: [[String]] = [ - ["a", "b", "c"], - ["1", "2", "3", "4"], - ["x", "y", "z", "should", "be", "in", "wildcard"] - ] - try paths.forEach { path in - let uri = URI( - scheme: "http", - userInfo: nil, - hostname: "0.0.0.0", - port: 80, - path: path.joined(separator: "/"), - query: nil, - fragment: nil - ) - let request = Request(method: .get, uri: uri) - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), path.prefix(3).joined(separator: ":")) - } - } - - func testParameters() throws { - let request = Request(method: .get, path: "") - let params = request.parameters - XCTAssertEqual(params, Parameters([:])) - } -} diff --git a/Tests/RoutingTests/RouterTests.swift b/Tests/RoutingTests/RouterTests.swift index 5abbe90d..69ff320e 100644 --- a/Tests/RoutingTests/RouterTests.swift +++ b/Tests/RoutingTests/RouterTests.swift @@ -1,324 +1,177 @@ -import XCTest -import HTTP -import Branches +import Async +import Dispatch +import Bits import Routing -import URI - -extension String: Swift.Error {} +import Service +import XCTest class RouterTests: XCTestCase { - static let allTests = [ - ("testRouter", testRouter), - ("testWildcardMethod", testWildcardMethod), - ("testWildcardHost", testWildcardHost), - ("testHostMatch", testHostMatch), - ("testMiss", testMiss), - ("testWildcardPath", testWildcardPath), - ("testParameters", testParameters), - ("testEmpty", testEmpty), - ("testNoHostWildcard", testNoHostWildcard), - ("testRouterDualSlugRoutes", testRouterDualSlugRoutes), - ("testRouteLogs", testRouteLogs), - ("testRouterThrows", testRouterThrows), - ("testParams", testParams), - ("testOutOfBoundsParams", testOutOfBoundsParams), - ("testParamsDuplicateKey", testParamsDuplicateKey), - ("testSecondRegistrationIgnored", testSecondRegistrationIgnored), - ("testCanRegisterAfterRemoveResponse", testCanRegisterAfterRemoveResponse), - ] - func testRouter() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - - let request = Request(method: .get, uri: "http://0.0.0.0/hello") - let response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Hello, World!") - } + let router = TrieRouter() - func testWildcardMethod() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .wildcard, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - - let method: [HTTP.Method] = [.get, .post, .put, .patch, .delete, .trace, .head, .options] - try method.forEach { method in - let request = Request(method: method, uri: "http://0.0.0.0/hello") - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, World!") - } - } - - func testWildcardHost() throws { - let router = Router() - router.register(host: "*", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - - let hosts: [String] = ["0.0.0.0", "chat.app.com", "[255.255.255.255.255]", "slack.app.com"] - try hosts.forEach { host in - let request = Request(method: .get, uri: "http://\(host)/hello") - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, World!") - } - } - - func testHostMatch() throws { - let router = Router() - - let hosts: [String] = ["0.0.0.0", "chat.app.com", "255.255.255.255", "slack.app.com"] - hosts.forEach { host in - router.register(host: host, path: ["hello"]) { request in - return Response(status: .ok, body: "Host: \(host)") - } - } - - try hosts.forEach { host in - let request = Request(method: .get, uri: "http://\(host)/hello") - let response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Host: \(host)") - } - } + let path: [PathComponent.Parameter] = [.string("foo"), .string("bar"), .string("baz")] - func testMiss() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - XCTFail("should not be found, wrong host") - return "[fail]" - } + let route = Route(path: [.constants(path), .parameter(.string(User.uniqueSlug))], output: 42) + router.register(route: route) - let request = Request(method: .get, uri: "http://[255.255.255.255.255]/hello") - let handler = router.route(request) - XCTAssert(handler == nil) - } - - func testWildcardPath() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello", "*"]) { request in - return "Hello, World!" - } - - let paths: [String] = [ - "hello", - "hello/zero", - "hello/extended/path", - "hello/very/extended/path.pdf" - ] - - try paths.forEach { path in - let request = Request(method: .get, uri: "http://0.0.0.0/\(path)") - let response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Hello, World!") - } - } - - func testParameters() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello", ":name", ":age"]) { request in - guard let name = request.parameters["name"]?.string else { throw "missing param: name" } - guard let age = request.parameters["age"]?.int else { throw "missing or invalid param: age" } - return "Hello, \(name) aged \(age)." - } - - let namesAndAges: [(String, Int)] = [ - ("a", 12), - ("b", 42), - ("c", 200), - ("d", 1) - ] - - try namesAndAges.forEach { name, age in - let request = Request(method: .get, uri: "http://0.0.0.0/hello/\(name)/\(age)") - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, \(name) aged \(age).") - } - } - - func testEmpty() throws { - let router = Router() - router.register(path: []) { request in - return Response(status: .ok, body: "Hello, Empty!") - } - - let empties: [String] = ["", "/"] - try empties.forEach { emptypath in - let uri = URI(scheme: "http", hostname: "0.0.0.0", path: emptypath) - let request = Request(method: .get, uri: uri) - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, Empty!") - } - } - - func testNoHostWildcard() throws { - let router = Router() - router.register { request in - return Response(status: .ok, body: "Hello, World!") - } - - let uri = URI( - scheme: "", - hostname: "" + let container = try BasicContainer( + config: Config(), + environment: .development, + services: Services(), + on: DefaultEventLoop(label: "unit-test") + ) + let params = Params() + XCTAssertEqual(router.route(path: path + [.string("Tanner")], parameters: params), 42) + try XCTAssertEqual(params.parameter(User.self, using: container).blockingAwait().name, "Tanner") + } + + func testCaseSensitiveRouting() throws { + let router = TrieRouter() + + let path: [PathComponent.Parameter] = [.string("path"), .string("TO"), .string("fOo")] + + let route = Route(path: [.constants(path)], output: 42) + router.register(route: route) + + let params = Params() + XCTAssertEqual(router.route(path: [.string("PATH"), .string("tO"), .string("FOo")], parameters: params), nil) + XCTAssertEqual(router.route(path: [.string("path"), .string("TO"), .string("fOo")], parameters: params), 42) + } + + func testCaseInsensitiveRouting() throws { + let router = TrieRouter() + router.caseInsensitive = true + + let path: [PathComponent.Parameter] = [.string("path"), .string("TO"), .string("fOo")] + + let route = Route(path: [.constants(path)], output: 42) + router.register(route: route) + + let params = Params() + XCTAssertEqual(router.route(path: [.string("PATH"), .string("tO"), .string("FOo")], parameters: params), 42) + } + + func testAnyRouting() throws { + let router = TrieRouter() + + let route0 = Route(path: [ + .constants([.string("a")]), + .anything + ], output: 0) + + let route1 = Route(path: [ + .constants([.string("b")]), + .parameter(.string("1")), + .anything + ], output: 1) + + let route2 = Route(path: [ + .constants([.string("c")]), + .parameter(.string("1")), + .parameter(.string("2")), + .anything + ], output: 2) + + let route3 = Route(path: [ + .constants([.string("d")]), + .parameter(.string("1")), + .parameter(.string("2")), + ], output: 3) + + let route4 = Route(path: [ + .constants([.string("e")]), + .parameter(.string("1")), + .anything, + .constants([.string("a")]) + ], output: 4) + + router.register(route: route0) + router.register(route: route1) + router.register(route: route2) + router.register(route: route3) + router.register(route: route4) + + XCTAssertEqual( + router.route(path: [.string("a"), .string("b")], parameters: Params()), + 0 + ) + + XCTAssertNil(router.route(path: [.string("a")], parameters: Params())) + + XCTAssertEqual( + router.route(path: [.string("a"), .string("a")], parameters: Params()), + 0 + ) + + XCTAssertEqual( + router.route(path: [.string("b"), .string("a"), .string("c")], parameters: Params()), + 1 + ) + + XCTAssertNil(router.route(path: [.string("b")], parameters: Params())) + XCTAssertNil(router.route(path: [.string("b"), .string("a")], parameters: Params())) + + XCTAssertEqual( + router.route(path: [.string("b"), .string("a"), .string("c")], parameters: Params()), + 1 + ) + + XCTAssertNil(router.route(path: [.string("c")], parameters: Params())) + XCTAssertNil(router.route(path: [.string("c"), .string("a")], parameters: Params())) + XCTAssertNil(router.route(path: [.string("c"), .string("b")], parameters: Params())) + + XCTAssertEqual( + router.route(path: [.string("d"), .string("a"), .string("b")], parameters: Params()), + 3 + ) + + XCTAssertNil(router.route(path: [.string("d"), .string("a"), .string("b"), .string("c")], parameters: Params())) + XCTAssertNil(router.route(path: [.string("d"), .string("a")], parameters: Params())) + + XCTAssertEqual( + router.route(path: [.string("e"), .string("a"), .string("b"), .string("a")], parameters: Params()), + 4 ) - let request = Request(method: .get, uri: uri) - let response = try router.respond(to: request) - XCTAssertEqual(response.body.bytes?.makeString(), "Hello, World!") - } - - func testRouterDualSlugRoutes() throws { - let router = Router() - router.register(path: ["foo", ":a", "one"]) { _ in return "1" } - router.register(path: ["foo", ":b", "two"]) { _ in return "2" } - - let requestOne = Request(method: .get, path: "foo/slug-val/one") - let responseOne = try router.respond(to: requestOne) - XCTAssertEqual(responseOne.body.bytes?.makeString(), "1") - - let requestTwo = Request(method: .get, path: "foo/slug-val/two") - let responseTwo = try router.respond(to: requestTwo) - XCTAssertEqual(responseTwo.body.bytes?.makeString(), "2") } - - func testRouteLogs() throws { - let router = Router() - let responder = Request.Handler { _ in return Response(status: .ok) } - router.register(path: ["foo", "bar", ":id"], responder: responder) - router.register(path: ["foo", "bar", ":id", "zee"], responder: responder) - router.register(path: ["1/2/3/4/5/6/7"], responder: responder) - router.register(method: .post, path: ["multi-path"], responder: responder) - router.register(method: .put, path: ["multi-path"], responder: responder) - - let expectation = [ - "* POST multi-path", - "* PUT multi-path", - "* GET 1/2/3/4/5/6/7", - "* GET foo/bar/:id", - "* GET foo/bar/:id/zee" - ] - - XCTAssertEqual(Set(router.routes), Set(expectation)) - } - - func testRouterThrows() { - let router = Router() - - do { - let request = Request(method: .get, path: "asfd") - _ = try router.respond(to: request) - XCTFail("Should throw missing route") - } catch { - print(error) - } - } - - - func testParams() { - let base = Branch(name: "[base]", output: nil) - base.extend([":a", ":b", ":c", "*"], output: "abc") - let path = ["zero", "one", "two", "d", "e", "f"] - guard let result = base.fetch(path) else { - XCTFail("invalid wildcard fetch") - return - } - - let params = result.slugs(for: path) - XCTAssert(params["a"] == "zero") - XCTAssert(params["b"] == "one") - XCTAssert(params["c"] == "two") - XCTAssert(result.output == "abc") - } - - func testOutOfBoundsParams() { - let base = Branch(name: "[base]", output: nil) - base.extend([":a", ":b", ":c", "*"], output: "abc") - let path = ["zero", "one", "two", "d", "e", "f"] - guard let result = base.fetch(path) else { - XCTFail("invalid wildcard fetch") - return - } - - let params = result.slugs(for: ["zero", "one"]) - XCTAssert(params["a"] == "zero") - XCTAssert(params["b"] == "one") - XCTAssert(params["c"] == nil) - XCTAssert(result.output == "abc") - } - - func testParamsDuplicateKey() { - let base = Branch(name: "[base]", output: nil) - base.extend([":a", ":a", ":a", "*"], output: "abc") - let path = ["zero", "one", "two", "d", "e", "f"] - guard let result = base.fetch(path) else { - XCTFail("invalid wildcard fetch") - return - } - - let params = result.slugs(for: ["zero", "one"]) - XCTAssert(params["a.0"] == "zero") - XCTAssert(params["a.1"] == "one") - XCTAssert(params["a.2"] == nil) - XCTAssert(result.output == "abc") } - func testParameterizable() throws { - let base = Branch(name: "[base]", output: nil) - base.extend([Foo.parameter, Foo.parameter, Foo.parameter, "*"], output: "abc") - let path = ["zero", "one", "two", "d", "e", "f"] - guard let result = base.fetch(path) else { - XCTFail("invalid wildcard fetch") - return - } - - var params = result.slugs(for: ["zero", "one"]) - let one = try params.next(Foo.self) - let two = try params.next(Foo.self) - XCTAssert(one.id == "zero") - XCTAssert(two.id == "one") - } + func testRouterSuffixes() throws { + let router = TrieRouter() + router.caseInsensitive = true - func testSecondRegistrationIgnored() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - let request = Request(method: .get, uri: "http://0.0.0.0/hello") - var response = try router.respond(to: request) - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Ciao mondo!") - } + let path1: [PathComponent.Parameter] = [.string("a")] + let path2: [PathComponent.Parameter] = [.string("aa")] + let route1 = Route(path: [.constants(path1)], output: 1) + let route2 = Route(path: [.constants(path2)], output: 2) + router.register(route: route1) + router.register(route: route2) - response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Hello, World!") + let params = Params() + XCTAssertEqual(router.route(path: [.string("a")], parameters: params), 1) + XCTAssertEqual(router.route(path: [.string("aa")], parameters: params), 2) } - func testCanRegisterAfterRemoveResponse() throws { - let router = Router() - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Hello, World!") - } - let request = Request(method: .get, uri: "http://0.0.0.0/hello") - var response = try router.respond(to: request) - - router.flushCache(for: request) - - router.register(host: "0.0.0.0", method: .get, path: ["hello"]) { request in - return Response(status: .ok, body: "Ciao, mondo!") - } - - response = try router.respond(to: request) - XCTAssert(response.body.bytes?.makeString() == "Ciao, mondo!") - } + static let allTests = [ + ("testRouter", testRouter), + ("testCaseInsensitiveRouting", testCaseInsensitiveRouting), + ("testCaseSensitiveRouting", testCaseSensitiveRouting), + ("testAnyRouting", testAnyRouting), + ("testRouterSuffixes", testRouterSuffixes), + ] } -struct Foo { - let id: String +final class Params: ParameterContainer { + var parameters: Parameters = [] + init() {} } -extension Foo: Parameterizable { - static let uniqueSlug = "foo-slug" +final class User: Parameter { + var name: String + + init(name: String) { + self.name = name + } - static func make(for parameter: String) throws -> Foo { - return .init(id: parameter) + static func make(for parameter: String, using container: Container) throws -> Future { + return Future(User(name: parameter)) } } diff --git a/Tests/RoutingTests/Utilities.swift b/Tests/RoutingTests/Utilities.swift deleted file mode 100644 index b83768f4..00000000 --- a/Tests/RoutingTests/Utilities.swift +++ /dev/null @@ -1,28 +0,0 @@ -import XCTest -import HTTP -import Routing -import URI - -extension Request { - convenience init(method: HTTP.Method, path: String, host: String = "0.0.0.0") { - let uri = URI(hostname: host, path: path) - self.init(method: method, uri: uri) - } - - enum BytesError: Error { - case routingFailed - case invalidResponse - } - - func bytes(running router: Router) throws -> Bytes { - guard let responder = router.route(self) else { - throw BytesError.routingFailed - } - - guard let bytes = try responder.respond(to: self).body.bytes else { - throw BytesError.invalidResponse - } - - return bytes - } -} diff --git a/app.json b/app.json deleted file mode 100644 index b6f67e4e..00000000 --- a/app.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "vapor", - "scripts": { - }, - "env": { - }, - "formation": { - }, - "addons": [ - - ], - "buildpacks": [ - { - "url": "https://github.com/kylef/heroku-buildpack-swift" - } - ] -}