diff --git a/jester.nim b/jester.nim index 1a6abd6..b23d0b9 100644 --- a/jester.nim +++ b/jester.nim @@ -14,6 +14,7 @@ export request export strtabs export tables export httpcore +export options export MultiData export HttpMethod export asyncdispatch @@ -42,6 +43,14 @@ type request: Request, error: RouteError ): Future[ResponseData] {.gcsafe, closure.} + MatchPair* = tuple + matcher: MatchProc + errorHandler: ErrorProc + + MatchPairSync* = tuple + matcher: MatchProcSync + errorHandler: ErrorProc + Jester* = object when not useHttpBeast: httpServer*: AsyncHttpServer @@ -451,18 +460,20 @@ proc initJester*( result.errorHandlers = @[] proc initJester*( - matcher: MatchProc, + pair: MatchPair, settings: Settings = newSettings() ): Jester = result = initJester(settings) - result.register(matcher) + result.register(pair.matcher) + result.register(pair.errorHandler) proc initJester*( - matcher: MatchProcSync, # TODO: Annoying nim bug: `MatchProc | MatchProcSync` doesn't work. + pair: MatchPairSync, # TODO: Annoying nim bug: `MatchPair | MatchPairSync` doesn't work. settings: Settings = newSettings() ): Jester = result = initJester(settings) - result.register(matcher) + result.register(pair.matcher) + result.register(pair.errorHandler) proc serve*( self: var Jester @@ -538,7 +549,7 @@ template resp*(code: HttpCode, content: string): typed = ## Sets ``(code, headers, content)`` as the response. bind TCActionSend - result = (TCActionSend, code, none[RawHeaders](), content, true) + result = (TCActionSend, code, result[2], content, true) for header in headers: setHeader(result[2], header[0], header[1]) break route @@ -725,6 +736,8 @@ template uri*(address = "", absolute = true, addScriptName = true): untyped = template responseHeaders*(): var ResponseHeaders = ## Access the Option[RawHeaders] response headers + if result[2].isNone: + result[2] = some[RawHeaders](@[]) result[2] proc daysForward*(days: int): DateTime = @@ -1303,7 +1316,7 @@ proc routesEx(name: string, body: NimNode): NimNode = `afterRoutes` ) - let matchIdent = newIdentNode(name) + let matchIdent = newIdentNode(name & "Matcher") let reqIdent = newIdentNode("request") let needsAsync = needsAsync(body) case needsAsync @@ -1363,6 +1376,26 @@ proc routesEx(name: string, body: NimNode): NimNode = errorHandlerProc[6][0][1][^1][2][1][0] = stmts result.add(errorHandlerProc) + # Pair the matcher and error matcher + let pairIdent = newIdentNode(name) + let matchProcVarIdent = newIdentNode(name & "MatchProc") + let errorProcVarIdent = newIdentNode(name & "ErrorProc") + if needsAsync in {ImplicitTrue, ExplicitTrue}: + # TODO: I don't understand why I have to assign these procs to intermediate + # variables in order to get them into the tuple. It would be nice if it could + # just be: + # let `pairIdent`: MatchPair = (`matchIdent`, `errorHandlerIdent`) + result.add quote do: + let `matchProcVarIdent`: MatchProc = `matchIdent` + let `errorProcVarIdent`: ErrorProc = `errorHandlerIdent` + let `pairIdent`: MatchPair = (`matchProcVarIdent`, `errorProcVarIdent`) + else: + result.add quote do: + let `matchProcVarIdent`: MatchProcSync = `matchIdent` + let `errorProcVarIdent`: ErrorProc = `errorHandlerIdent` + let `pairIdent`: MatchPairSync = (`matchProcVarIdent`, `errorProcVarIdent`) + + # TODO: Replace `body`, `headers`, `code` in routes with `result[i]` to # get these shortcuts back without sacrificing usability. # TODO2: Make sure you replace what `guessAction` used to do for this. @@ -1373,13 +1406,11 @@ proc routesEx(name: string, body: NimNode): NimNode = macro routes*(body: untyped) = result = routesEx("match", body) let jesIdent = genSym(nskVar, "jes") - let matchIdent = newIdentNode("match") - let errorHandlerIdent = newIdentNode("matchErrorHandler") + let pairIdent = newIdentNode("match") let settingsIdent = newIdentNode("settings") result.add( quote do: - var `jesIdent` = initJester(`matchIdent`, `settingsIdent`) - `jesIdent`.register(`errorHandlerIdent`) + var `jesIdent` = initJester(`pairIdent`, `settingsIdent`) ) result.add( quote do: diff --git a/tests/alltest.nim b/tests/alltest.nim index f45ed9f..d29d74e 100644 --- a/tests/alltest.nim +++ b/tests/alltest.nim @@ -45,6 +45,12 @@ routes: get "/halt": resp "

Not halted!

" + + before re"/halt-before/.*?": + halt Http502, "Halted!" + + get "/halt-before/@something": + resp "Should never reach this" before re"/halt-before/.*?": halt Http502, "Halted!" @@ -61,6 +67,16 @@ routes: get "/redirect/@url/?": redirect(uri(@"url")) + + get "/redirect-halt/@url/?": + redirect(uri(@"url")) + resp "ok" + + before re"/redirect-before/.*?": + redirect(uri("/nowhere")) + + get "/redirect-before/@url/?": + resp "should not get here" get "/redirect-halt/@url/?": redirect(uri(@"url")) @@ -223,3 +239,8 @@ routes: get "/issue157": resp(Http200, [("Content-Type","text/css")] , "foo") + + get "/manyheaders": + setHeader(responseHeaders, "foo", "foo") + setHeader(responseHeaders, "bar", "bar") + resp Http200, {"Content-Type": "text/plain"}, "result" diff --git a/tests/customRouter.nim b/tests/customRouter.nim new file mode 100644 index 0000000..5f4f141 --- /dev/null +++ b/tests/customRouter.nim @@ -0,0 +1,20 @@ +import jester + +router myrouter: + get "/": + resp "Hello world" + + get "/raise": + raise newException(Exception, "Foobar") + + error Exception: + resp Http500, "Something bad happened: " & exception.msg + +when isMainModule: + let s = newSettings( + Port(5454), + bindAddr="127.0.0.1", + ) + var jest = initJester(myrouter, s) + # jest.register(myrouterErrorHandler) + jest.serve() diff --git a/tests/tester.nim b/tests/tester.nim index c2222a4..24a1440 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -94,6 +94,11 @@ proc allTest(useStdLib: bool) = let resp = waitFor client.get(address & "/foo/halt") check resp.status.startsWith("502") check (waitFor resp.body) == "I'm sorry, this page has been halted." + + test "/halt-before": + let resp = waitFor client.request(address & "/foo/halt-before/something", HttpGet) + let body = waitFor resp.body + check body == "Halted!" test "/halt-before": let resp = waitFor client.request(address & "/foo/halt-before/something", HttpGet) @@ -109,6 +114,17 @@ proc allTest(useStdLib: bool) = test "/redirect": let resp = waitFor client.request(address & "/foo/redirect/halt", HttpGet) check resp.headers["location"] == "http://localhost:5454/foo/halt" + + test "/redirect-halt": + let resp = waitFor client.request(address & "/foo/redirect-halt/halt", HttpGet) + check resp.headers["location"] == "http://localhost:5454/foo/halt" + check (waitFor resp.body) == "" + + test "/redirect-before": + let resp = waitFor client.request(address & "/foo/redirect-before/anywhere", HttpGet) + check resp.headers["location"] == "http://localhost:5454/foo/nowhere" + let body = waitFor resp.body + check body == "" test "/redirect-halt": let resp = waitFor client.request(address & "/foo/redirect-halt/halt", HttpGet) @@ -154,6 +170,13 @@ proc allTest(useStdLib: bool) = let resp = waitFor client.get(address & "/foo/issue157") let headers = resp.headers check headers["Content-Type"] == "text/css" + + test "resp doesn't overwrite headers": + let resp = waitFor client.get(address & "/foo/manyheaders") + let headers = resp.headers + check headers["foo"] == "foo" + check headers["bar"] == "bar" + check headers["Content-Type"] == "text/plain" suite "static": test "index.html": @@ -253,12 +276,26 @@ proc issue150(useStdLib: bool) = check resp.code == Http500 check (waitFor resp.body).startsWith("Something bad happened") +proc customRouterTest(useStdLib: bool) = + waitFor startServer("customRouter.nim", useStdLib) + var client = newAsyncHttpClient(maxRedirects = 0) + + suite "customRouter useStdLib=" & $useStdLib: + test "error handler": + let resp = waitFor client.get(address & "/raise") + check resp.code == Http500 + let body = (waitFor resp.body) + checkpoint body + check body.startsWith("Something bad happened: Foobar") + when isMainModule: try: allTest(useStdLib=false) # Test HttpBeast. allTest(useStdLib=true) # Test asynchttpserver. issue150(useStdLib=false) issue150(useStdLib=true) + customRouterTest(useStdLib=false) + customRouterTest(useStdLib=true) # Verify that Nim in Action Tweeter still compiles. test "Nim in Action - Tweeter":