aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--example/haberdasherserver.nim30
-rw-r--r--nimtwirp/errors.nim14
-rw-r--r--nimtwirp/generator.nim39
-rw-r--r--nimtwirp/nimtwirp.nim101
-rw-r--r--nimtwirp/response.nim43
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"})