aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOskari Timperi <oskari.timperi@iki.fi>2018-04-19 21:50:51 +0100
committerOskari Timperi <oskari.timperi@iki.fi>2018-04-19 21:53:21 +0100
commitcd061d43cbc7201649bdfd166870a44d37a0588f (patch)
tree57d8a3e505654089bad7233e640d4ac0237c62cc
parent155d1c06db73fc10d88ab39bb6d580494ad8a3f8 (diff)
downloadnimtwirp-cd061d43cbc7201649bdfd166870a44d37a0588f.tar.gz
nimtwirp-cd061d43cbc7201649bdfd166870a44d37a0588f.zip
A bunch of changes
- use parseopt2 in nimtwirp_build - define the prefix when running nimtwirp_build (defaults to `/twirp/`) - add a TwirpResponse type - replace concept with a type having callbacks for each rpc method (a la streams.nim) - check that a request is a POST and that it has a correct Content-Type - generate only a proc for handling the service, let the user handle responding to let them customize e.g. headers
-rw-r--r--example/Makefile2
-rw-r--r--example/haberdasherserver.nim17
-rw-r--r--nimtwirp/generator.nim86
-rw-r--r--nimtwirp/nimtwirp_build.nim35
-rw-r--r--nimtwirp/response.nim43
5 files changed, 125 insertions, 58 deletions
diff --git a/example/Makefile b/example/Makefile
index 63da56f..74f6709 100644
--- a/example/Makefile
+++ b/example/Makefile
@@ -7,4 +7,4 @@ haberdasherclient: haberdasherclient.nim service_pb.nim service_twirp.nim
nim c haberdasherclient.nim
%_pb.nim %_twirp.nim: %.proto
- ../nimtwirp/nimtwirp_build -I. --out=. $^
+ ../nimtwirp/nimtwirp_build -I:. --out:. $^
diff --git a/example/haberdasherserver.nim b/example/haberdasherserver.nim
index a41c3b8..ba85e83 100644
--- a/example/haberdasherserver.nim
+++ b/example/haberdasherserver.nim
@@ -3,14 +3,12 @@ import asyncdispatch
import random
import nimtwirp/errors
+import nimtwirp/response
import service_pb
import service_twirp
-type
- HaberdasherService = object
-
-proc MakeHat(x: HaberdasherService, size: twirp_example_haberdasher_Size): twirp_example_haberdasher_Hat =
+proc MakeHatImpl(service: Haberdasher, size: twirp_example_haberdasher_Size): twirp_example_haberdasher_Hat =
if size.inches <= 0:
raise newTwirpError(TwirpInvalidArgument, "I can't make a hat that small!")
@@ -21,6 +19,13 @@ proc MakeHat(x: HaberdasherService, size: twirp_example_haberdasher_Size): twirp
var
server = newAsyncHttpServer()
- service: HaberdasherService
+ service {.threadvar.}: Haberdasher
+
+service = newHaberdasher()
+service.MakeHatImpl = MakeHatImpl
+
+proc cb(req: Request) {.async.} =
+ var resp = HaberdasherHandler(service, req)
+ await respond(req, resp)
-waitFor server.serve(Port(8080), HaberdasherServer(service, "/"))
+waitFor server.serve(Port(8080), cb)
diff --git a/nimtwirp/generator.nim b/nimtwirp/generator.nim
index c0560b1..8fc0b26 100644
--- a/nimtwirp/generator.nim
+++ b/nimtwirp/generator.nim
@@ -20,29 +20,52 @@ import strutils
import {gen.fileName}
import nimtwirp/errors
+import nimtwirp/response
"""
-proc genServer(service: Service): string =
+proc genServer(service: Service, prefix: string): string =
result = &"""
+const
+ {service.name}Prefix* = "{prefix}{service.fullName}/"
+
type
- {service.name}* = concept x
+ {service.name}* = ref {service.name}Obj
+ {service.name}Obj* = object of RootObj
"""
for meth in service.methods:
result &= &"""
- x.{meth.name}({meth.inputType}) is {meth.outputType}
+ {meth.name}Impl*: proc (service: {service.name}, param: {meth.inputType}): {meth.outputType}
+"""
+
+ for meth in service.methods:
+ result &= &"""
+
+proc {meth.name}*(service: {service.name}, param: {meth.inputType}): {meth.outputType} =
+ if service.{meth.name}Impl == nil:
+ raise newTwirpError(TwirpUnimplemented, "{meth.name} is not implemented")
+ result = service.{meth.name}Impl(service, param)
"""
result &= &"""
-proc {service.name}Server*(service: {service.name}, prefix: string): auto =
- let headers = newHttpHeaders({{"Content-Type": "application/protobuf"}})
- proc cb(req: Request): Future[void] =
- try:
- let servicePrefix = prefix & "{service.fullName}/"
- if startsWith(req.url.path, servicePrefix):
- var methodName = req.url.path[len(servicePrefix)..^1]
+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")
+
+ 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")
+
+ let methodName = req.url.path[len({service.name}Prefix)..^1]
+
"""
for index, meth in service.methods:
@@ -50,30 +73,20 @@ proc {service.name}Server*(service: {service.name}, prefix: string): auto =
if index > 0:
ifel = "elif"
result &= &"""
- {ifel} methodName == "{meth.name}":
- let inputMsg = new{meth.inputType}(req.body)
- let outputMsg = service.{meth.name}(inputMsg)
- let body = serialize(outputMsg)
- result = respond(req, Http200, body, headers)
+ {ifel} methodName == "{meth.name}":
+ let inputMsg = new{meth.inputType}(req.body)
+ let outputMsg = {meth.name}(service, inputMsg)
+ result = newTwirpResponse(serialize(outputMsg))
"""
result &= &"""
- else:
- raise newTwirpError(TwirpNotFound, "method not found")
- else:
- raise newTwirpError(TwirpNotFound, "service not found")
- except TwirpError as exc:
- let headers = newHttpHeaders({{"Content-Type": "application/json"}})
- result = req.respond(exc.httpStatus, $twirpErrorToJson(exc), headers)
- except Exception as exc:
- let headers = newHttpHeaders({{"Content-Type": "application/json"}})
- var err = newTwirpError(TwirpInternal, exc.msg)
- result = req.respond(err.httpStatus, $twirpErrorToJson(err), headers)
- result = cb
-
+ else:
+ raise newTwirpError(TwirpBadRoute, "unknown method")
+ except Exception as exc:
+ result = newTwirpResponse(exc)
"""
-proc genClient(service: Service): string =
+proc genClient(service: Service, prefix: string): string =
result = &"""
@@ -94,7 +107,7 @@ proc new{service.name}Client*(address: string): {service.name}Client =
result &= &"""
proc {meth.name}*(client: {service.name}Client, req: {meth.inputType}): {meth.outputType} =
let body = serialize(req)
- let resp = client.client.request(client.address & "/{service.fullName}/{meth.name}", httpMethod=HttpPost, body=body)
+ let resp = client.client.request(client.address & {service.name}Prefix & "{meth.name}", httpMethod=HttpPost, body=body)
let httpStatus = code(resp)
if httpStatus != Http200:
if contentType(resp) != "application/json":
@@ -106,11 +119,11 @@ proc {meth.name}*(client: {service.name}Client, req: {meth.inputType}): {meth.ou
"""
-proc genService(service: Service): string =
- result = genServer(service)
- result &= genClient(service)
+proc genService(service: Service, prefix: string): string =
+ result = genServer(service, prefix)
+ result &= genClient(service, prefix)
-proc newTwirpServiceGenerator*(): ServiceGenerator =
+proc newTwirpServiceGenerator*(prefix: string): ServiceGenerator =
new(result)
let gen = result
@@ -118,6 +131,9 @@ proc newTwirpServiceGenerator*(): ServiceGenerator =
proc myGenImports(): string =
result = genImports(gen)
+ proc myGenService(service: Service): string =
+ result = genService(service, prefix)
+
result.genImports = myGenImports
- result.genService = genService
+ result.genService = myGenService
result.fileSuffix = "twirp"
diff --git a/nimtwirp/nimtwirp_build.nim b/nimtwirp/nimtwirp_build.nim
index f269d9a..0134ae8 100644
--- a/nimtwirp/nimtwirp_build.nim
+++ b/nimtwirp/nimtwirp_build.nim
@@ -2,6 +2,7 @@ import os
import osproc
import strformat
import strutils
+import parseopt2
import nimpb/compiler/compiler
import generator
@@ -12,27 +13,29 @@ proc usage() {.noreturn.} =
--out The output directory for the generated files
-I Add a path to the set of include paths
+ --prefix The URL prefix used for service (default: /twirp/)
""")
quit(QuitFailure)
var includes: seq[string] = @[]
var protos: seq[string] = @[]
var outdir: string
-
-if paramCount() == 0:
- usage()
-
-for idx in 1..paramCount():
- let param = paramStr(idx)
-
- if param.startsWith("-I"):
- add(includes, param[2..^1])
- elif param.startsWith("--out="):
- outdir = param[6..^1]
- elif param == "--help":
- usage()
- else:
- add(protos, param)
+var prefix: string = "/twirp/"
+
+for kind, key, val in getopt():
+ case kind
+ of cmdArgument:
+ add(protos, key)
+ of cmdLongOption, cmdShortOption:
+ case key
+ of "help", "h": usage()
+ of "prefix": prefix = val
+ of "out": outdir = val
+ of "I": add(includes, val)
+ else:
+ echo("error: unknown option: " & key)
+ usage()
+ of cmdEnd: assert(false)
if outdir == nil:
echo("error: --out is required")
@@ -42,4 +45,4 @@ if len(protos) == 0:
echo("error: no input files")
quit(QuitFailure)
-compileProtos(protos, includes, outdir, newTwirpServiceGenerator())
+compileProtos(protos, includes, outdir, newTwirpServiceGenerator(prefix))
diff --git a/nimtwirp/response.nim b/nimtwirp/response.nim
new file mode 100644
index 0000000..3d4b943
--- /dev/null
+++ b/nimtwirp/response.nim
@@ -0,0 +1,43 @@
+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"})