diff options
| author | Oskari Timperi <oskari.timperi@iki.fi> | 2018-04-19 21:50:51 +0100 |
|---|---|---|
| committer | Oskari Timperi <oskari.timperi@iki.fi> | 2018-04-19 21:53:21 +0100 |
| commit | cd061d43cbc7201649bdfd166870a44d37a0588f (patch) | |
| tree | 57d8a3e505654089bad7233e640d4ac0237c62cc | |
| parent | 155d1c06db73fc10d88ab39bb6d580494ad8a3f8 (diff) | |
| download | nimtwirp-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/Makefile | 2 | ||||
| -rw-r--r-- | example/haberdasherserver.nim | 17 | ||||
| -rw-r--r-- | nimtwirp/generator.nim | 86 | ||||
| -rw-r--r-- | nimtwirp/nimtwirp_build.nim | 35 | ||||
| -rw-r--r-- | nimtwirp/response.nim | 43 |
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"}) |
