diff options
| -rw-r--r-- | example/haberdasherserver.nim | 30 | ||||
| -rw-r--r-- | nimtwirp/errors.nim | 14 | ||||
| -rw-r--r-- | nimtwirp/generator.nim | 39 | ||||
| -rw-r--r-- | nimtwirp/nimtwirp.nim | 101 | ||||
| -rw-r--r-- | nimtwirp/response.nim | 43 |
5 files changed, 154 insertions, 73 deletions
diff --git a/example/haberdasherserver.nim b/example/haberdasherserver.nim index ba85e83..cdb25cb 100644 --- a/example/haberdasherserver.nim +++ b/example/haberdasherserver.nim @@ -2,13 +2,13 @@ import asynchttpserver import asyncdispatch import random +import nimtwirp/nimtwirp import nimtwirp/errors -import nimtwirp/response import service_pb import service_twirp -proc MakeHatImpl(service: Haberdasher, size: twirp_example_haberdasher_Size): twirp_example_haberdasher_Hat = +proc MakeHatImpl(service: Haberdasher, size: twirp_example_haberdasher_Size): Future[twirp_example_haberdasher_Hat] {.async.} = if size.inches <= 0: raise newTwirpError(TwirpInvalidArgument, "I can't make a hat that small!") @@ -17,6 +17,8 @@ proc MakeHatImpl(service: Haberdasher, size: twirp_example_haberdasher_Size): tw result.color = rand(["white", "black", "brown", "red", "blue"]) result.name = rand(["bowler", "baseball cap", "top hat", "derby"]) +# You can do serving this way if you want to customize the process a bit + var server = newAsyncHttpServer() service {.threadvar.}: Haberdasher @@ -24,8 +26,22 @@ var service = newHaberdasher() service.MakeHatImpl = MakeHatImpl -proc cb(req: Request) {.async.} = - var resp = HaberdasherHandler(service, req) - await respond(req, resp) - -waitFor server.serve(Port(8080), cb) +proc handler(req: Request) {.async.} = + # Each service will have a generated handleRequest() proc which takes the + # service object and a asynchttpserver.Request object and returns a + # Future[nimtwirp.Response]. + var fut = handleRequest(service, req) + yield fut + if fut.failed: + await respond(req, nimtwirp.newResponse(fut.readError())) + else: + await respond(req, fut.read()) + +waitFor server.serve(Port(8080), handler) + +# Or this way (idea copied from Jester) if your needs are simple. +# +#var settings = newSettings(8080) +#twirpServices(settings): +# service +#runForever() diff --git a/nimtwirp/errors.nim b/nimtwirp/errors.nim index dba2bd9..0bad607 100644 --- a/nimtwirp/errors.nim +++ b/nimtwirp/errors.nim @@ -1,7 +1,10 @@ -import json import httpcore +import json +import strutils type + TwirpErrorRef* = ref TwirpError + TwirpError* = object of Exception code*: string httpStatus*: HttpCode @@ -52,9 +55,16 @@ template newTwirpError*(T: typedesc, msg: string): untyped = err proc twirpErrorToJson*[T](error: T): JsonNode = + # Get rid of the async tracebacks if any + const header = "\nAsync traceback:\n" + var msg = error.msg + if header in msg: + let start = msg.find(header) + msg = msg[0..<start] + %*{ "code": error.code, - "msg": error.msg + "msg": msg } proc twirpErrorFromJson*(node: JsonNode): ref TwirpError = diff --git a/nimtwirp/generator.nim b/nimtwirp/generator.nim index 8fc0b26..5ea9137 100644 --- a/nimtwirp/generator.nim +++ b/nimtwirp/generator.nim @@ -19,8 +19,8 @@ import strutils import {gen.fileName} +import nimtwirp/nimtwirp import nimtwirp/errors -import nimtwirp/response """ @@ -36,16 +36,16 @@ type for meth in service.methods: result &= &""" - {meth.name}Impl*: proc (service: {service.name}, param: {meth.inputType}): {meth.outputType} + {meth.name}Impl*: proc (service: {service.name}, param: {meth.inputType}): Future[{meth.outputType}] {{.gcsafe, closure.}} """ for meth in service.methods: result &= &""" -proc {meth.name}*(service: {service.name}, param: {meth.inputType}): {meth.outputType} = +proc {meth.name}*(service: {service.name}, param: {meth.inputType}): Future[{meth.outputType}] {{.async.}} = if service.{meth.name}Impl == nil: raise newTwirpError(TwirpUnimplemented, "{meth.name} is not implemented") - result = service.{meth.name}Impl(service, param) + result = await service.{meth.name}Impl(service, param) """ result &= &""" @@ -53,18 +53,17 @@ proc {meth.name}*(service: {service.name}, param: {meth.inputType}): {meth.outpu proc new{service.name}*(): {service.name} = new(result) -proc {service.name}Handler*(service: {service.name}, req: Request): TwirpResponse = - try: - if req.reqMethod != HttpPost: - raise newTwirpError(TwirpBadRoute, "only POST accepted") +proc handleRequest*(service: {service.name}, req: Request): Future[nimtwirp.Response] {{.async.}} = + if req.reqMethod != HttpPost: + raise newTwirpError(TwirpBadRoute, "only POST accepted") - if getOrDefault(req.headers, "Content-Type") != "application/protobuf": - raise newTwirpError(TwirpInternal, "invalid Content-Type") + if getOrDefault(req.headers, "Content-Type") != "application/protobuf": + raise newTwirpError(TwirpInternal, "invalid Content-Type") - if not startsWith(req.url.path, {service.name}Prefix): - raise newTwirpError(TwirpBadRoute, "unknown service") + if not startsWith(req.url.path, {service.name}Prefix): + raise newTwirpError(TwirpBadRoute, "unknown service") - let methodName = req.url.path[len({service.name}Prefix)..^1] + let methodName = req.url.path[len({service.name}Prefix)..^1] """ @@ -73,17 +72,15 @@ proc {service.name}Handler*(service: {service.name}, req: Request): TwirpRespons if index > 0: ifel = "elif" result &= &""" - {ifel} methodName == "{meth.name}": - let inputMsg = new{meth.inputType}(req.body) - let outputMsg = {meth.name}(service, inputMsg) - result = newTwirpResponse(serialize(outputMsg)) + {ifel} methodName == "{meth.name}": + let inputMsg = new{meth.inputType}(req.body) + let outputMsg = await {meth.name}(service, inputMsg) + return nimtwirp.newResponse(serialize(outputMsg)) """ result &= &""" - else: - raise newTwirpError(TwirpBadRoute, "unknown method") - except Exception as exc: - result = newTwirpResponse(exc) + else: + raise newTwirpError(TwirpBadRoute, "unknown method") """ proc genClient(service: Service, prefix: string): string = diff --git a/nimtwirp/nimtwirp.nim b/nimtwirp/nimtwirp.nim new file mode 100644 index 0000000..32921bc --- /dev/null +++ b/nimtwirp/nimtwirp.nim @@ -0,0 +1,101 @@ +import asyncdispatch +import asynchttpserver +import json +import macros +import strformat + +import errors + +type + Settings* = ref object + port*: Port + address*: string + + Response* = ref object + code*: HttpCode + body*: string + headers*: HttpHeaders + + ServeHandlerProc = proc (request: asynchttpserver.Request): Future[nimtwirp.Response] {.gcsafe, closure.} + +proc respond*(req: asynchttpserver.Request, resp: nimtwirp.Response): Future[void] = + req.respond(resp.code, resp.body, resp.headers) + +proc newResponse*(exc: ref Exception): nimtwirp.Response = + var twirpExc: TwirpErrorRef + + if exc of TwirpErrorRef: + twirpExc = TwirpErrorRef(exc) + else: + twirpExc = newTwirpError(TwirpInternal, exc.msg) + + new(result) + result.code = twirpExc.httpStatus + result.body = $twirpErrorToJson(twirpExc) + result.headers = newHttpHeaders({"Content-Type": "application/json"}) + +proc newResponse*(body: string): nimtwirp.Response = + new(result) + result.code = Http200 + result.body = body + result.headers = newHttpHeaders({"Content-Type": "application/protobuf"}) + +proc handleHttpRequest(request: asynchttpserver.Request, handler: ServeHandlerProc) {.async.} = + var fut = handler(request) + + yield fut + + if fut.failed: + await respond(request, newResponse(fut.readError())) + else: + await respond(request, fut.read()) + +proc newSettings*(port = Port(8080), address = ""): Settings = + result = Settings( + port: port, + address: address, + ) + +proc serve*(handler: ServeHandlerProc, settings: Settings = newSettings()) = + var + httpServer = newAsyncHttpServer() + + proc callback(request: asynchttpserver.Request): Future[void] {.gcsafe, closure.} = + handleHttpRequest(request, handler) + + asyncCheck httpServer.serve(settings.port, callback) + +macro twirpServices*(settings: typed, x: untyped): untyped = + expectKind(settings, nnkSym) + + var serviceHandlers = "" + + for service in x: + let serviceName = $service + serviceHandlers.add(&""" + if not done: + fut = handleRequest({serviceName}, request) + yield fut + if fut.failed: + if not (fut.readError() of TwirpBadRoute): + done = true + else: + done = true +""") + + var handlerProc = parseStmt(&""" +proc handler(request: asynchttpserver.Request): Future[nimtwirp.Response] {{.async.}} = + var fut: Future[nimtwirp.Response] + var done = false + +{serviceHandlers} + + if not done: + raise newTwirpError(TwirpBadRoute, "unknown service") + else: + result = fut.read() +""") + + result = newStmtList() + add(result, handlerProc) + add(result, parseStmt(&"nimtwirp.serve(handler, {settings.symbol})")) diff --git a/nimtwirp/response.nim b/nimtwirp/response.nim deleted file mode 100644 index 3d4b943..0000000 --- a/nimtwirp/response.nim +++ /dev/null @@ -1,43 +0,0 @@ -import asyncdispatch -import asynchttpserver -import json - -import nimpb/nimpb - -import errors - -type - TwirpResponse* = ref object - code*: HttpCode - body*: string - headers*: HttpHeaders - - TwirpErrorRef = ref TwirpError - -proc respond*(req: asynchttpserver.Request, resp: TwirpResponse): Future[void] = - req.respond(resp.code, resp.body, resp.headers) - -proc newTwirpResponse*(exc: ref Exception): TwirpResponse = - var twirpExc: TwirpErrorRef - - if exc of TwirpErrorRef: - twirpExc = TwirpErrorRef(exc) - else: - twirpExc = newTwirpError(TwirpInternal, exc.msg) - - new(result) - result.code = twirpExc.httpStatus - result.body = $twirpErrorToJson(twirpExc) - result.headers = newHttpHeaders({"Content-Type": "application/json"}) - -proc newTwirpResponse*(body: string): TwirpResponse = - new(result) - result.code = Http200 - result.body = body - result.headers = newHttpHeaders({"Content-Type": "application/protobuf"}) - -proc newTwirpResponse*(body: JsonNode): TwirpResponse = - new(result) - result.code = Http200 - result.body = $body - result.headers = newHttpHeaders({"Content-Type": "application/json"}) |
