Giter Club home page Giter Club logo

routing-kit's Introduction

RoutingKit

Documentation Team Chat MIT License Continuous Integration Swift 5.7 - 5.9


routing-kit's People

Contributors

0xtim avatar adriencanterot avatar artkay avatar bennydebock avatar brettrtoomey avatar czechboy0 avatar damuellen avatar evertt avatar grundoon avatar gwynne avatar jaapwijnen avatar joannis avatar ketzusaka avatar kylebshr avatar loganwright avatar nathanflurry avatar obbut avatar profburke avatar proggeramlug avatar rugaard avatar sarbogast avatar shnhrrsn avatar skreutzberger avatar stevapple avatar svanimpe avatar tanner0101 avatar technikyle avatar timominous avatar twof avatar vzsg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

routing-kit's Issues

Parameters should be percent escaped

In Vapor 2 the following code:

get("hello", ":id") { req in
            let id = req.parameters["id"]?.string

            var json = JSON()
            try json.set("hello", id)
            return json
        }

Would produce the following output:

  • GET /hello/abc1234 {"hello":"abc1234"}
  • GET /hello/abc|1234 {"hello":"abc|1234"}
  • GET /hello/abc%7C1234 {"hello":"abc|1234"}

In Vapor 3 instead:

  • GET /hello/abc1234 {"hello":"abc1234"}
  • GET /hello/abc|1234 {"hello":"abc%7C1234"} // Notice that this is automatically encoded by any client before sending
  • GET /hello/abc%7C1234 {"hello":"abc%7C1234"}

This to me seems like a regression.

The code that was taking care of this can be found on Router.swift on tag 2.2.1.

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)
    }

Allow access to parameters

It is normal for a web framework to allow access to the request parameters without knowing the expected names of the parameters. RoutingKit.Parameters does not allow accessing parameters without knowing their names.

A list of the parameter names could be returned from Parameters. Then the client code can query for the value of the parameter.

I would consider this standard behaviour for a web-container and vapor to be non-standard.

This restriction is preventing vapor from being dropped in as a replacement web container for our current solution.

how to get Anything parameter

// responds to GET /hello/foo
// responds to GET /hello/foo/bar
// ...
app.get("hello", "**") { req -> String in
    let name = req.parameters.getCatchall().joined(separator: " ")
    return "Hello, \(name)!"
}

how

// responds to GET /hello/foo
// responds to GET /hello/bar
// ...
app.get("hello", "*") { req -> String in
    let name = req.parameters.get("*")!
    return "Hello, \(name)!"
}

Passing Request object to StringInitializable

I was working with type safe routing and found myself needing the Request to properly parse path parameter. I was wondering why the StringInitializable protocol does not include the Request in the init method.

Would it be an idea to add this parameter, allowing the Request to be used?

In my case I needed the Request, because I need the logged in user for determining the parameter.

Have to name all params the same

The routing engine is too restrictive on what the parameter fields can be. For example, I tried to create these two routes for a DELETE

orders/:id
orders/:orderID/work/:workID

That fails because the router wants the ':orderID' in the second line to also be called ':id'.

Yes, I can do that of course, but it makes the names not as clear when working with multiple fields.

move from generic to protocol based type safe routing

Type safe routing is currently limited to 3 methods.

// doesn't work
drop.get("users", User.self, "posts", Post.self) { req, user, post in
    return "User #\(user.id) and post #(post.id)"
}

This is due to limitations with variadics and generics.

I propose moving to a protocol-based approach.

drop.get("users", User.parameter, "posts", Post.parameter) { req in
    let user = try req.parameters.get(User.self)
    let user = try req.parameters.get(User.self)
    
    return "User #\(user.id) and post #(post.id)"
}

This could be implemented as a Parameterizable protocol. Model would conform to this by default.

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 {
    static var parameter: String {
        return ":" + uniqueParameterKey
    }
}
extension Parameters {
    func get<P: Parameterizable>(_ p: P.Type = P.self) throws -> P {
         let parameter = try get(P.uniqueSlug) as String
         return try P.make(for: parameter)
    }
}

PathComponent.catchall not working as expected

I've got a simple test system with just two routes, one to return a counter structure and the other is a catch all, the counter route works fine but the catchall looks to not be called if there is a partial match to the counter route.

public func routes(_ router: Router) throws {

    let version = "v1"
   let counterController = CounterController()
    router.get(version, "counter", String.parameter, String.parameter, use: counterController.getCounter)

    router.get(PathComponent.catchall) { req in
        return "Boom!"
    }
}

Steps to reproduce

Testing with postman produces:

http://localhost:8080/adsd = Boom!
http://localhost:8080/v1 = { "message": "Not Found", "code": 404 }

Expected behaviour

I would expect the

http://localhost:8080/v1 = Boom!

as it doesn't match any route.

Actual behaviour

http://localhost:8080/v1 = { "message": "Not Found", "code": 404 }

Environment

  • Vapor Framework version: 3.3.0
  • Vapor Toolbox version: 3.1.10
  • OS version: 10.14.5

Avoid print in TrieRouter.swift

Is your feature request related to a problem? Please describe.
Yes.
I purposefully override a route because I want to handle some HEAD requests manually, but also want to handle the GET requests for the same path.
When a GET route is registered, the default responder in Vapor will also register the corresponding HEAD route (if the path components are static, which is my case).

Every time the server launches, I get a warning printed: [Routing] Warning: Overriding route output at: HEAD/….
This is 1/ annoying, but I’d be fine with it, but 2/ problematic as the print does not use the logger I have configured. If I use a structured logger to be able to parse the logs, I get this line out of nowhere which is unparseable (I can deal but it’s not great).

Describe the solution you'd like
The message should be logged using a logger, w/ a log level set to notice presumable, or even debug (it’s important to know, but not a warning IMHO, because overriding a route can be normal, as it is in my case).

Describe alternatives you've considered
Vapor could let the client decide whether the HEAD route should be registered for a given GET route; or even just not register it at all.
This would allow me to avoid having the warning, though it would not fix the fact that logging w/o a logger is a bad practice.

Additional context
N/A

warn if mis-match dynamic node registered

Per @tanner0101 request I'm creating an issue that this seeming valid looking code doesn't work:

    func boot(router: Router) {
        router.get(Order.parameter, "edit", use: editViewHandler)
        router.get(Order.ID.parameter, "delete", use: deleteViewHandler)
    }

When trying to call delete and pull out an Order.ID.parameter you get an error about int != order

Deadlock happens when two theads concurrently read from _cache dictionary

Hello,

Version: Vapor 2.1.2, Routing 2.0.0
Xcode 8.3.2/Debug

Very frequently deadlock happens, when two threads at same time read from _cache dictionary. Probably it is Foundation issue, as multiple reads should be allowed. Currently read and write operations for _cache are not synchronized. Probably _cache is corrupted after two threads modify it at same time.

Here is stack:

Thread 2 Queue : codes.vapor.server (concurrent)
#0	0x00000001013476f9 in specialized static String.== infix(String, String) -> Bool ()
#1	0x00000001013052c1 in protocol witness for static Equatable.== infix(A, A) -> Bool in conformance String ()
#2	0x00000001012696ff in _NativeDictionaryBuffer<A, B where ...>._find(A, startBucket : Int) -> (pos : _NativeDictionaryIndex<A, B>, found : Bool) ()
#3	0x0000000101277859 in _NativeDictionaryBuffer<A, B where ...>.maybeGet(A) -> B? ()
#4	0x0000000101265c36 in _VariantDictionaryBuffer.maybeGet(A) -> B? ()
#5	0x000000010123a4be in Dictionary.subscript.getter ()
#6	0x0000000100857662 in Router.route(Request) -> Responder? at /Users/Igor/Projects/TennisCup/.build/checkouts/routing.git-5366657101075133678/Sources/Routing/Router.swift:35
#7	0x0000000100853744 in Router.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/routing.git-5366657101075133678/Sources/Routing/Router+Responder.swift:9
#8	0x000000010085501b in protocol witness for Responder.respond(to : Request) throws -> Response in conformance Router ()
#9	0x0000000100917062 in SessionsMiddleware.respond(to : Request, chainingTo : Responder) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Sessions/SessionsMiddleware.swift:52
#10	0x0000000100917e53 in protocol witness for Middleware.respond(to : Request, chainingTo : Responder) throws -> Response in conformance SessionsMiddleware ()
#11	0x00000001009f23a3 in Collection<A where ...>.(chain(to : Responder) -> Responder).(closure #1).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Middleware/Chaining.swift:15
#12	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#13	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#14	0x00000001009ce3fe in FileMiddleware.respond(to : Request, chainingTo : Responder) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/File/FileMiddleware.swift:21
#15	0x00000001009cf1a3 in protocol witness for Middleware.respond(to : Request, chainingTo : Responder) throws -> Response in conformance FileMiddleware ()
#16	0x00000001009f23a3 in Collection<A where ...>.(chain(to : Responder) -> Responder).(closure #1).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Middleware/Chaining.swift:15
#17	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#18	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#19	0x00000001009a57bc in DateMiddleware.respond(to : Request, chainingTo : Responder) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Date/DateMiddleware.swift:36
#20	0x00000001009a6e63 in protocol witness for Middleware.respond(to : Request, chainingTo : Responder) throws -> Response in conformance DateMiddleware ()
#21	0x00000001009f23a3 in Collection<A where ...>.(chain(to : Responder) -> Responder).(closure #1).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Middleware/Chaining.swift:15
#22	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#23	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#24	0x00000001009bfc3b in ErrorMiddleware.respond(to : Request, chainingTo : Responder) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Error/ErrorMiddleware.swift:17
#25	0x00000001009c27b3 in protocol witness for Middleware.respond(to : Request, chainingTo : Responder) throws -> Response in conformance ErrorMiddleware ()
#26	0x00000001009f23a3 in Collection<A where ...>.(chain(to : Responder) -> Responder).(closure #1).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Middleware/Chaining.swift:15
#27	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#28	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#29	0x00000001009b12bb in Droplet.(init(config : Config?, router : Router?, server : ServerFactoryProtocol?, client : ClientFactoryProtocol?, middleware : [Middleware]?, console : ConsoleProtocol?, log : LogProtocol?, hash : HashProtocol?, cipher : CipherProtocol?, commands : [Command]?, view : ViewRenderer?, cache : CacheProtocol?, mail : MailProtocol?) throws -> Droplet).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Droplet/Droplet.swift:146
#30	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#31	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#32	0x00000001005a56f3 in BasicServer.respond(stream : A.Client, responder : Responder) throws -> () at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Server/BasicServer.swift:96
#33	0x00000001005a4f91 in BasicServer.(start(Responder, errors : (ServerError) -> ()) throws -> ()).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Server/BasicServer.swift:53
Thread 4 Queue : codes.vapor.server (concurrent)
#0	0x000000010140bb4a in _swift_retain_ ()
#1	0x00000001013c5704 in initializeWithCopy for String ()
#2	0x00000001012696da in _NativeDictionaryBuffer<A, B where ...>._find(A, startBucket : Int) -> (pos : _NativeDictionaryIndex<A, B>, found : Bool) ()
#3	0x0000000101277859 in _NativeDictionaryBuffer<A, B where ...>.maybeGet(A) -> B? ()
#4	0x0000000101265c36 in _VariantDictionaryBuffer.maybeGet(A) -> B? ()
#5	0x000000010123a4be in Dictionary.subscript.getter ()
#6	0x0000000100857662 in Router.route(Request) -> Responder? at /Users/Igor/Projects/TennisCup/.build/checkouts/routing.git-5366657101075133678/Sources/Routing/Router.swift:35
#7	0x0000000100853744 in Router.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/routing.git-5366657101075133678/Sources/Routing/Router+Responder.swift:9
#8	0x000000010085501b in protocol witness for Responder.respond(to : Request) throws -> Response in conformance Router ()
#9	0x0000000100917062 in SessionsMiddleware.respond(to : Request, chainingTo : Responder) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Sessions/SessionsMiddleware.swift:52
#10	0x0000000100917e53 in protocol witness for Middleware.respond(to : Request, chainingTo : Responder) throws -> Response in conformance SessionsMiddleware ()
#11	0x00000001009f23a3 in Collection<A where ...>.(chain(to : Responder) -> Responder).(closure #1).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Middleware/Chaining.swift:15
#12	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#13	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#14	0x00000001009ce3fe in FileMiddleware.respond(to : Request, chainingTo : Responder) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/File/FileMiddleware.swift:21
#15	0x00000001009cf1a3 in protocol witness for Middleware.respond(to : Request, chainingTo : Responder) throws -> Response in conformance FileMiddleware ()
#16	0x00000001009f23a3 in Collection<A where ...>.(chain(to : Responder) -> Responder).(closure #1).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Middleware/Chaining.swift:15
#17	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#18	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#19	0x00000001009a57bc in DateMiddleware.respond(to : Request, chainingTo : Responder) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Date/DateMiddleware.swift:36
#20	0x00000001009a6e63 in protocol witness for Middleware.respond(to : Request, chainingTo : Responder) throws -> Response in conformance DateMiddleware ()
#21	0x00000001009f23a3 in Collection<A where ...>.(chain(to : Responder) -> Responder).(closure #1).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Middleware/Chaining.swift:15
#22	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#23	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#24	0x00000001009bfc3b in ErrorMiddleware.respond(to : Request, chainingTo : Responder) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Error/ErrorMiddleware.swift:17
#25	0x00000001009c27b3 in protocol witness for Middleware.respond(to : Request, chainingTo : Responder) throws -> Response in conformance ErrorMiddleware ()
#26	0x00000001009f23a3 in Collection<A where ...>.(chain(to : Responder) -> Responder).(closure #1).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Middleware/Chaining.swift:15
#27	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#28	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#29	0x00000001009b12bb in Droplet.(init(config : Config?, router : Router?, server : ServerFactoryProtocol?, client : ClientFactoryProtocol?, middleware : [Middleware]?, console : ConsoleProtocol?, log : LogProtocol?, hash : HashProtocol?, cipher : CipherProtocol?, commands : [Command]?, view : ViewRenderer?, cache : CacheProtocol?, mail : MailProtocol?) throws -> Droplet).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/vapor.git-5492988889259800272/Sources/Vapor/Droplet/Droplet.swift:146
#30	0x0000000100597d02 in BasicResponder.respond(to : Request) throws -> Response at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Responder/Responder.swift:13
#31	0x0000000100597da4 in protocol witness for Responder.respond(to : Request) throws -> Response in conformance BasicResponder ()
#32	0x00000001005a56f3 in BasicServer.respond(stream : A.Client, responder : Responder) throws -> () at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Server/BasicServer.swift:96
#33	0x00000001005a4f91 in BasicServer.(start(Responder, errors : (ServerError) -> ()) throws -> ()).(closure #1) at /Users/Igor/Projects/TennisCup/.build/checkouts/engine.git-3311994267206676365/Sources/HTTP/Server/BasicServer.swift:53

anything and parameter should be the same node

In the current Trie tree, anything (aka. *) and parameter (aka. :name) are treated as different nodes, while the latter has a higher priority. However, they have exactly the same matching strategy and behavior, except that parameter will set a parameter while anything doesn’t have one.

Shouldn’t they be treated the same as routes like test/* and test/:name matches exactly the same cases?

Possible solution: treat .anything as .parameter("") and unset parameters[""] after matching completed.

elementsEqual

Using Sequence.elementsEqual for comparing route path segments.

extension Sequence where Self.Element == UInt8 {
    /// Compares the collection of `UInt8`s to a case insensitive collection.
    ///
    /// This collection could be get from applying the `UTF8View`
    ///   property on the string protocol.
    ///
    /// - Parameter bytes: The string constant in the form of a collection of `UInt8`
    /// - Returns: Whether the collection contains **EXACTLY** this array or no, but by ignoring case.
    internal func compareCaseInsensitiveASCIIBytes<T: Sequence>(to bytes: T) -> Bool
        where T.Element == UInt8 {
            return self.elementsEqual(bytes, by: {return ($0 & 0xdf) == ($1 & 0xdf)})
    }
}

Incorrect Documentation for PathCompents

Describe the bug

Vapor defines the following extension for an array of path components in routing-kit/Sources/RoutingKit/PathComponents.swift

extension Array where Element == PathComponent {
    /// Converts an array of `PathComponent` into a readable path string.
    ///
    ///     /galaxies/:galaxyID/planets
    ///
    public var string: String {
        return self.map(\.description).joined(separator: "/")
    }
}

Given the following array of path components:

let pathComponents: [PathComponent] = ["galaxies", ":galaxyID", "planets"]

You would expect pathComponents.string to produce /galaxies/:galaxyID/planets, as specified in the documentation comment above, but instead it produces galaxies/:galaxyID/planets.

To Reproduce

See above.

Expected behavior

The code in the documentation should match the actual behavior of the method. Whether the documentation is changed or the behavior of the method is changed is up to you guys.

Environment

N/A

Additional Context

Consider changing extension Array where Element == PathComponent to extension Sequence where Element == PathComponent.

3.0 RC 1: String parameter type does not work

Using a String.parameter does not work.

router.get("cores", String.parameter, "solar", use: solarController.getSolarForCoreByDeviceID)

This seems to be related to this guard statement (also see this discussion with @0xTim):

guard current.slug == [UInt8](P.uniqueSlug.utf8) else { ... }

Steps to reproduce

Create a route that takes a string parameter:

router.get("cores", String.parameter, "solar", use: solarController.getSolarForCoreByDeviceID)

Calling the endpoint (e.g. {{ url }}/cores/somestring/solar) will lead to the following error:

[ ERROR ] RoutingError.invalidParameterType: Invalid parameter type. Expected String got [99, 111, 114, 101] (ParameterContainer.swift:69)

Setting a breakpoint on the parameter line (let deviceID = try req.parameter(String.self)) in the controller's implementation shows that this guard statement fails.

Dumping what slug and uniqueSlug are:

        print("----")
        dump("slug: \(String(bytes: current.slug, encoding: .utf8))")
        dump("unique slug: \(P.uniqueSlug.utf8)")

results in the following output when requesting {{ url }}/cores/somestring/solar:

----
- "slug: Optional(\"core\")"
- "unique slug: string"
[ ERROR ] RoutingError.invalidParameterType: Invalid parameter type. Expected String got [99, 111, 114, 101] (ParameterContainer.swift:73)

Controller:

import Foundation
import Vapor
import Fluent

final class SolarController {
    func getSolarForCoreByDeviceID(_ req: Request) throws -> Future<SolarInfo> {
//        let deviceID = try req.parameters.next(String.self)
        let deviceID = try req.parameter(String.self)
        print("device id: \(deviceID)")
        
        return Core.query(on: req).filter(\Core.deviceID == deviceID).first().flatMap(to: SolarInfo.self) { (core) -> Future<SolarInfo> in
            guard let core = core else {
                throw Abort(.notFound, reason: "No such core")
            }
            
            let solarInfo = SolarInfo(sunrise: core.sunrise, sunset: core.sunset)
            return Future<SolarInfo>(solarInfo)
        }
    }
}

struct SolarInfo: Content {
    var sunrise: Date
    var sunriseTime: String
    var sunset: Date
    var sunsetTime: String
    
    init(sunrise: Date, sunset: Date) {
        self.sunrise = sunrise
        self.sunset = sunset
        
        let timeFormatter = DateFormatter()
        timeFormatter.locale = Locale(identifier: "nl-NL")
        timeFormatter.dateStyle = .none
        timeFormatter.timeStyle = .short
        
        self.sunriseTime = timeFormatter.string(from: sunrise)
        self.sunsetTime = timeFormatter.string(from: sunset)
    }
}

Expected behavior

I expect a String.parameter to not fail when called with a string :)

Actual behavior

The request fails with:

Oops: Invalid parameter type. Expected String got [99, 111, 114, 101]

Environment

  • Vapor Framework version: 3.0 RC
  • Vapor Toolbox version: n/a
  • OS version: High Sierra

Only first route registration works

Steps to reproduce

I've created fresh test repository: https://github.com/Sorix/RouterIssue

We have 2 paths:

let testPath = router.grouped("test")

// /test/:string
testPath.get(String.parameter, use: testController.string)

// /test/:int/secondComponent
testPath.get(Int.parameter, "secondComponent", use: testController.int)

Let's try to call second route:

curl "http://localhost:8080/test/12/secondComponent/"

Expected behavior

Vapor will correctly route and call testController.int.

Actual behavior

Vapor always calls firstly registered route (controller.string). That's why we get the error:

$ curl "http://localhost:8080/test/12/secondComponent/"
{"error":true,"reason":"Invalid parameter type: int != string"}

If we switch registrations, another route will work, but both will never work.

I don't think that my issue is relevant to #45 because I use different count of pathComponents.

Environment

  • Vapor Framework version: 3.1.0
  • Vapor Toolbox version: 3.1.10
  • OS version: macOS 10.14

add iOS platform in Package.swift

I am using Vapor on iOS and needs to add iOS platform in Package.swift

let package = Package(
    name: "routing-kit",
    platforms: [
       .macOS(.v10_15),
       .iOS(.v11)
    ],
    products: [
        .library(name: "RoutingKit", targets: ["RoutingKit"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-log.git", from: "1.4.2")
    ],
    targets: [
        .target(name: "RoutingKit", dependencies: [
            .product(name: "Logging", package: "swift-log"),
        ]),
        .testTarget(name: "RoutingKitTests", dependencies: ["RoutingKit"]),
    ]
)

Move percent encoding mechanism to Vapor

Percent encoding is mostly HTTP specific and feels out out of place in this package. I suggest we move the mechanism to Vapor. There are a few other options.

  • Make percent encoding an option just like case sensitivity
  • See if we can create a more general mechanism so that users of the package can create and use their own encodings.

Along with providing more flexibility, making it so that we can, at the very least, opt out of percent encoding, will help with performance benchmarks.

`any` Path Component only Matches a Single Character

The any component only matches a single character, so v, 1, *, c, and anything else 1 character long will work, but v1 gets a 404.

For example, if I have a route:

router.get("hello", any, "world") {req in //... }

This path matches:

/hello/o/world

But this doesn't match:

/hello/oo/world

Trie Tree matching problem

Steps to reproduce

import RoutingKit
let router = Router<Int>()
router.register(0, at: [.constant("b"), .anything, .constant("c")])
router.register(1, at: [.constant("b"), .parameter("a")])
var params = Parameters()
router.route(path: ["b", "c", "c"], parameters: &params)

Expected behavior

$R0: Int? = 0

Actual behavior

$R0: Int? = nil

Environment

  • vapor/router version: 4.0.0
  • OS version: macOS 10.5.4

Reason

Different from lexicographic matching strategy, routing should follow a strategy like BFS to match the longest defined route. This issue is directly related to the following ones:

#48 Actually the same case. As for developers that really have such needs, vapor ought to provide an alternative, despite performance losses compared to the existing one.

#90 #91 Following the current model, RoutingKit is treating nodes with the same matching cases differently, which directly leads to unexpected notFounds.

#74 This bug is also caused by the matching stratergy, where .parameter is always prior to .catchall. The current solution is far from elegant as it’s using an awkward way to patch the matching solution. Neither extensible, nor clear; hardly Swifty.

Suggestion

If RoutingKit itself would not change its matching strategy, vapor does need to provide an alternative which has a more reliable and accurate model.

For RoutingKit, it means abstracting its APIs with Protocols.
For Vapor, it should allow developers to use their own routing backends in configure.swift.

Once it becomes necessary or performance-qualified, such module may become a vapor-community project.

Traditional routing failing with a comma-separated list

Having more than three parts in a route, traditional routing fails (in Vapor 1.x) when using a comma separated list of strings. E.g:

drop.post(":userId", "messages", ":messageId", "read") { request in
    return ""
}

The above code will fail with an error: Ambiguous reference to member 'post(_:handler:)'.

However, not separating by commas does work:

drop.post(":userId/messages/:messageId/read") { request in
    return ""
}

Add function to remove cached Responders

Feature: Add Function to Flush Router Cache

Introduction

Router objects cache Responders to Requests (as long as the URI has no parameters). If your application updates routes while running, the cache prevents the new responder from being used. This feature adds a function that allows you to flush the cache for a given request.

Motivation

In trying to build an application that allows you to add and change routes while the app is running, two related problems occur:

  1. For an existing route, if you register a new responder, the cached one prevents the new one from being used.
  2. If a request is made for a non-existent route, and later on a responder is added for the route, the new responder is not used because the cache continues to return a 404 responder.

Proposed solution

Provide a function that allows you to remove the cached responder for a given request.

Code snippets

let router = Router()
router.register(host: "0.0.0.0", method: .wildcard, 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)

// do stuff

router.flushCache(for: request)

Impact

I believe this feature will have zero impact on existing code. Applications that do not invoke the new function will see no change in behavior.

Alternatives considered

No alternatives were considered. This seems to be the simplest implementation.

Proposed Implementation

See PR #31

3.0 RC 2: Route not matching if GET parameters are supplied

After updating to 3.0 RC 2 I am noticing that GET routes stopped working / matching when the request has GET parameters (I didn't try POST/PUT/PATCH/DELETE).

Example route:

router.get("cores", String.parameter, "timezone", use: myController.getTimeZone)

Works:

The following URL properly matches the configured route:

http://localhost:8080/cores/foo/timezone

Doesn't work:

However, the same route -but this time with GET parameters- does not match anymore (since RC 2):

http://localhost:8080/cores/foo/timezone?bar=foo

Result:

$ curl -v http://localhost:8080/cores/foo/timezone?bar=foo
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 8080 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /cores/foo/timezone?bar=foo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< content-length: 9
< date: Thu, 22 Mar 2018 09:33:23 GMT
< 
* Connection #0 to host localhost left intact
Not found

Expected behaviour:

I expect the route to match, regardless of whether or not GET parameters were supplied.

Context:

Devices are not talking directly to the endpoint, communication goes through a third party that integrates using web hooks. These web hooks, however, send additional GET parameters that are out of my control.

Not working with localhost

Looks like when I add a grouping Vapor stop allowing "localhost" and I have to explicitly say 127.0.0.1

[griffon:/tmp] scott% curl localhost:8080/clients/test
{"error":true,"reason":"Not Found"}
[griffon:/tmp] scott% curl 127.0.0.1:8080/clients/test
1
[griffon:/tmp] scott% ping localhost
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.062 ms
^C
--- localhost ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.062/0.062/0.062/0.000 ms

bug.zip

Generalize route grouping

Currently EngineRouter inside vapor/vapor has the means to indicate that a group of routes has a specific middleware or path prefix. Neither of those are necessarily specific to HTTP routing, so I think it'd be good to drop that functionality down to this repo in a generic way, and then build the HTTP specific features on top of it within EngineRouter

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.