vapor / routing-kit Goto Github PK
View Code? Open in Web Editor NEW🚍 High-performance trie-node router.
License: MIT License
🚍 High-performance trie-node router.
License: MIT License
Will likely copy and adapt the one from vapor/vapor once maintainer application is accepted
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
RoutingKit should ensure paths do not contain slashes nor empty path components.
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
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)
}
}
// 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)!"
}
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.
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.
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!"
}
}
Testing with postman produces:
http://localhost:8080/adsd = Boom!
http://localhost:8080/v1 = { "message": "Not Found", "code": 404 }
I would expect the
http://localhost:8080/v1 = Boom!
as it doesn't match any route.
http://localhost:8080/v1 = { "message": "Not Found", "code": 404 }
Will likely copy the one from vapor/vapor after the maintainer application has been accepted.
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"]),
]
)
Should follow the rules laid out in vapor/vapor#1876 and automatically apply fixes when possible.
Should also be part of CI.
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.
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
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
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).
router.get("cores", String.parameter, "timezone", use: myController.getTimeZone)
The following URL properly matches the configured route:
http://localhost:8080/cores/foo/timezone
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
I expect the route to match, regardless of whether or not GET parameters were supplied.
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.
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 ""
}
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.
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.
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/"
Vapor will correctly route and call testController.int
.
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.
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
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: ¶ms)
$R0: Int? = 0
$R0: Int? = nil
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 notFound
s.
#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.
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 Protocol
s.
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.
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 { ... }
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)
}
}
I expect a String.parameter
to not fail when called with a string :)
The request fails with:
Oops: Invalid parameter type. Expected String got [99, 111, 114, 101]
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)})
}
}
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:
{"hello":"abc1234"}
{"hello":"abc|1234"}
{"hello":"abc|1234"}
In Vapor 3 instead:
{"hello":"abc1234"}
{"hello":"abc%7C1234"}
// Notice that this is automatically encoded by any client before sending{"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)
}
Moving from a factory method to an init method could help.
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.
In trying to build an application that allows you to add and change routes while the app is running, two related problems occur:
Provide a function that allows you to remove the cached responder for a given request.
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)
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.
No alternatives were considered. This seems to be the simplest implementation.
See PR #31
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.
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
.
See above.
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.
N/A
Consider changing extension Array where Element == PathComponent
to extension Sequence where Element == PathComponent
.
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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.