# # REST API framework implementation # (c) Copyright 2021-Present # Status Research & Development GmbH # # Licensed under either of # Apache License, version 2.0, (LICENSE-APACHEv2) # MIT license (LICENSE-MIT) import std/[macros, options, uri, sequtils] import chronos, chronos/apps/http/[httpcommon, httptable, httpclient] import chronicles except error import httputils, stew/base10 import segpath, common, macrocommon, agent export httpclient, httptable, httpcommon, options, agent, httputils template endpoint*(v: string) {.pragma.} template meth*(v: HttpMethod) {.pragma.} template accept*(v: string) {.pragma.} type RestClient* = object of RootObj session*: HttpSessionRef address*: HttpAddress agent: string flags: RestClientFlags RestClientRef* = ref RestClient RestPlainResponse* = object status*: int contentType*: string data*: seq[byte] RestResponse*[T] = object status*: int contentType*: string data*: T RestStatus* = distinct int RestClientFlag* {.pure.} = enum CommaSeparatedArray RestClientFlags* = set[RestClientFlag] RestRequestFlag* {.pure.} = enum ConsumeBody RestReturnKind {.pure.} = enum Status, PlainResponse, GenericResponse, Value const DefaultAcceptContentType = "application/json" RestContentTypeArg = "restContentType" RestAcceptTypeArg = "restAcceptType" RestClientArg = "restClient" ExtraHeadersArg = "extraHeaders" NotAllowedArgumentNames = [RestClientArg, RestContentTypeArg, RestAcceptTypeArg] chronicles.expandIt(HttpAddress): remote = it.hostname & ":" & Base10.toString(it.port) request = if len(it.query) == 0: it.path else: it.path & "?" & it.query chronicles.formatIt(HttpClientConnectionRef): if isNil(it): Base10.toString(0'u64) else: Base10.toString(it.id) proc `==`*(x, y: RestStatus): bool {.borrow.} proc `<=`*(x, y: RestStatus): bool {.borrow.} proc `<`*(x, y: RestStatus): bool {.borrow.} proc `$`*(x: RestStatus): string {.borrow.} proc new*(t: typedesc[RestClientRef], url: string, flags: RestClientFlags = {}, httpFlags: HttpClientFlags = {}, maxConnections: int = -1, maxRedirections: int = HttpMaxRedirections, connectTimeout = HttpConnectTimeout, headersTimeout = HttpHeadersTimeout, bufferSize: int = 4096, userAgent = PrestoIdent ): RestResult[RestClientRef] = let session = HttpSessionRef.new(httpFlags, maxRedirections, connectTimeout, headersTimeout, bufferSize, maxConnections) var uri = parseUri(url) uri.path = "" uri.query = "" uri.anchor = "" let address = block: let res = session.getAddress(uri) if res.isErr(): return err("Unable to resolve remote hostname") res.get() ok(RestClientRef(session: session, address: address, agent: userAgent, flags: flags)) proc new*(t: typedesc[RestClientRef], ta: TransportAddress, scheme: HttpClientScheme = HttpClientScheme.NonSecure, flags: RestClientFlags = {}, httpFlags: HttpClientFlags = {}, maxConnections: int = -1, maxRedirections: int = HttpMaxRedirections, connectTimeout = HttpConnectTimeout, headersTimeout = HttpHeadersTimeout, bufferSize: int = 4096, userAgent = PrestoIdent ): RestClientRef = let session = HttpSessionRef.new(httpFlags, maxRedirections, connectTimeout, headersTimeout, bufferSize, maxConnections) let address = ta.getAddress(scheme, "") RestClientRef(session: session, address: address, agent: userAgent, flags: flags) proc closeWait*(client: RestClientRef) {.async.} = await client.session.closeWait() proc createPostRequest*(client: RestClientRef, path: string, query: string, contentType: string, acceptType: string, extraHeaders: openArray[HttpHeaderTuple], httpMethod: HttpMethod, contentLength: uint64): HttpClientRequestRef = var address = client.address address.path = path address.query = query var headers = newSeqOfCap[HttpHeaderTuple](4 + extraHeaders.len) headers.add(("content-type", contentType)) headers.add(("content-length", Base10.toString(contentLength))) headers.add(("accept", acceptType)) headers.add(("user-agent", client.agent)) headers.add extraHeaders HttpClientRequestRef.new(client.session, address, httpMethod, headers = headers) proc createGetRequest*(client: RestClientRef, path: string, query: string, contentType: string, acceptType: string, extraHeaders: openArray[HttpHeaderTuple], httpMethod: HttpMethod): HttpClientRequestRef = var address = client.address address.path = path address.query = query var headers = newSeqOfCap[HttpHeaderTuple](2 + extraHeaders.len) headers.add(("accept", acceptType)) headers.add(("user-agent", client.agent)) headers.add extraHeaders HttpClientRequestRef.new(client.session, address, httpMethod, headers = headers) proc getEndpointOrDefault(prc: NimNode, default: string): string {.compileTime.} = let pragmaNode = prc.pragma() for node in pragmaNode.items(): if node.kind == nnkExprColonExpr: if (node[0].kind == nnkIdent) and (node[0].strVal() == "endpoint"): if node[1].kind != nnkStrLit: error("REST procedure {.endpoint.} pragma's value should be " & "string literal only", node[1]) if len(node[1].strVal) == 0: error("REST procedure should have non-empty {.endpoint.} pragma", node[1]) return node[1].strVal return default proc getMethodOrDefault(prc: NimNode, default: NimNode): NimNode {.compileTime.} = let pragmaNode = prc.pragma() for node in pragmaNode.items(): if node.kind == nnkExprColonExpr: if node[0].kind == nnkIdent and node[0].strVal == "meth": return copyNimTree(node[1]) return default proc getAcceptOrDefault(prc: NimNode, default: string): NimNode {.compileTime.} = let pragmaNode = prc.pragma() for node in pragmaNode.items(): if node.kind == nnkExprColonExpr: if (node[0].kind == nnkIdent) and (node[0].strVal == "accept"): case node[1].kind of nnkStrLit: if len(node[1].strVal) > 0: return copyNimTree(node[1]) error("REST procedure should have non-empty {.accept.} pragma", node[1]) else: return copyNimTree(node[1]) return newStrLitNode(default) proc getAsyncPragma(prc: NimNode): NimNode {.compileTime.} = let pragmaNode = prc.pragma() for node in pragmaNode.items(): if node.kind == nnkIdent and node.strVal == "async": return node proc raiseRestEncodingStringError*(field: static string) {. noreturn, noinline.} = var msg = "Unable to encode object to string, field " msg.add("[") msg.add(field) msg.add("]") var error = newException(RestEncodingError, msg) error.field = field raise error proc raiseRestEncodingBytesError*(field: static string) {. noreturn, noinline.} = var msg = "Unable to encode object to bytes, field " msg.add("[") msg.add(field) msg.add("]") var error = newException(RestEncodingError, msg) error.field = field raise error proc raiseRestCommunicationError*(exc: ref HttpError) {. noreturn, noinline.} = var msg = "Communication failed while sending/receiving request" msg.add(", http error [") msg.add(exc.name) msg.add("]: ") msg.add(exc.msg) var error = newException(RestCommunicationError, msg) error.exc = exc raise error proc raiseRestCommunicationError*(exc: ref AsyncStreamError) {. noreturn, noinline.} = var msg = "Communication failed while sending request's body" msg.add(", stream error [") msg.add(exc.name) msg.add("]: ") msg.add(exc.msg) var error = newException(RestCommunicationError, msg) error.exc = exc raise error proc raiseRestResponseError*(resp: RestPlainResponse) {. noreturn, noinline.} = var msg = "Unsuccessfull response received" msg.add(", http code [") msg.add(Base10.toString(uint64(resp.status))) msg.add("]") var error = newException(RestResponseError, msg) error.status = resp.status error.contentType = resp.contentType error.message = bytesToString(resp.data) raise error proc raiseRestRedirectionError*(msg: string) {. noreturn, noinline.} = var msg = "Unable to follow redirect location, " msg.add(msg) raise (ref RestRedirectionError)(msg: msg) proc raiseRestDecodingBytesError*(message: cstring) {.noreturn, noinline.} = var msg = "Unable to decode REST response" msg.add(", error [") msg.add(message) msg.add("]") raise (ref RestDecodingError)(msg: msg) proc newArrayNode(nodes: openarray[NimNode]): NimNode = newTree(nnkBracket, @nodes) proc isPostMethod(node: NimNode): bool {.compileTime.} = let methodName = if node.kind == nnkDotExpr: node.expectLen(2) node[1].expectKind(nnkIdent) node[1].strVal elif node.kind == nnkIdent: node.strVal else: "" case methodName of "MethodGet", "MethodHead", "MethodTrace", "MethodOptions", "MethodConnect": false of "MethodPost", "MethodPut", "MethodPatch", "MethodDelete": true else: false proc transformProcDefinition(prc: NimNode, clientIdent: NimNode, contentIdent: NimNode, acceptIdent: NimNode, extraHeadersIdent: NimNode, acceptValue: NimNode, stmtList: NimNode): NimNode {.compileTime.} = var procdef = copyNimTree(prc) var parameters = copyNimTree(prc.findChild(it.kind == nnkFormalParams)) var pragmas = copyNimTree(prc.pragma()) let clientArg = newTree(nnkIdentDefs, clientIdent, newIdentNode("RestClientRef"), newEmptyNode()) let contentTypeArg = newTree(nnkIdentDefs, contentIdent, newIdentNode("string"), newStrLitNode("application/json")) let extraHeadersArg = newTree(nnkIdentDefs, extraHeadersIdent, newTree(nnkBracketExpr, ident"seq", ident"HttpHeaderTuple"), newTree(nnkPrefix, ident"@", newTree(nnkBracket))) let acceptTypeArg = newTree(nnkIdentDefs, acceptIdent, newIdentNode("string"), acceptValue) let asyncPragmaArg = newIdentNode("async") var newParams = block: var res: seq[NimNode] for item in parameters: let includeParam = case item.kind of nnkIdentDefs: item[0].expectKind(nnkIdent) case item[0].strVal().toLowerAscii() of RestContentTypeArg, RestAcceptTypeArg, RestClientArg: false else: true else: true if includeParam: res.add(item) res[0] = newTree(nnkBracketExpr, newIdentNode("Future"), res[0]) res.insert(clientArg, 1) res.add(contentTypeArg) res.add(acceptTypeArg) res.add(extraHeadersArg) res var newPragmas = block: var res: seq[NimNode] res.add(asyncPragmaArg) for item in pragmas: let includePragma = case item.kind of nnkIdent: case item.strVal().toLowerAscii() of "rest", "async", "endpoint", "meth": false else: true of nnkExprColonExpr: item[0].expectKind(nnkIdent) case item[0].strVal().toLowerAscii() of "endpoint", "meth": false else: true else: true if includePragma: # We do not copy here, because we already copied original tree with # copyNimTree(). res.add(item) res for index, item in procdef.pairs(): case item.kind of nnkFormalParams: procdef[index] = newTree(nnkFormalParams, newParams) of nnkPragma: procdef[index] = newTree(nnkPragma, newPragmas) else: discard # We accept only `nnkProcDef` definitions, so we can use numeric index here procdef[6] = stmtList procdef template closeObjects(o1, o2, o3: untyped): untyped = if not(isNil(o1)): await o1.closeWait() o1 = nil if not(isNil(o2)): await o2.closeWait() o2 = nil if not(isNil(o3)): await o3.closeWait() o3 = nil template closeObjects(o1, o2, o3, o4: untyped): untyped = if not(isNil(o1)): await o1.closeWait() o1 = nil if not(isNil(o2)): await o2.closeWait() o2 = nil if not(isNil(o3)): await o3.closeWait() o3 = nil if not(isNil(o4)): await o4.closeWait() o4 = nil proc requestWithoutBody*(req: HttpClientRequestRef, flags: set[RestRequestFlag] ): Future[RestPlainResponse] {. async.} = var request = req redirect: HttpClientRequestRef = nil response: HttpClientResponseRef = nil address = request.address while true: try: debug "Sending REST request to remote server", address, http_method = $request.meth response = await request.send() debug "Got REST response headers from remote server", status = response.status, http_method = $request.meth, address, connection = request.connection if response.status >= 300 and response.status < 400: redirect = block: if "location" in response.headers: let location = response.headers.getString("location") if len(location) > 0: let res = request.redirect(parseUri(location)) if res.isErr(): raiseRestRedirectionError(res.error()) res.get() else: raiseRestRedirectionError("Location header with an empty value") else: raiseRestRedirectionError("Location header missing") discard await response.consumeBody() let redirectAddress = redirect.address debug "Got HTTP redirection from remote server", status = response.status, http_method = $request.meth, connection = request.connection, redirectAddress await request.closeWait() request = nil await response.closeWait() response = nil request = redirect redirect = nil else: let res = block: let status = response.status let contentType = response.headers.getString("content-type") let data = block: var default: seq[byte] if RestRequestFlag.ConsumeBody in flags: discard await response.consumeBody() default else: await response.getBodyBytes() debug "Received REST response body from remote server", status = response.status, http_method = $request.meth, address, connection = request.connection, contentType = contentType, size = len(data) await request.closeWait() request = nil await response.closeWait() response = nil RestPlainResponse(status: status, contentType: contentType, data: data) return res except CancelledError as exc: # TODO: when `finally` proved to work inside loops, move closeWait() logic # to `finally` handler. debug "REST client request was interrupted", address, connection = request.connection closeObjects(request, redirect, response) raise exc except RestError as exc: debug "REST client redirection error", address, connection = request.connection, errorName = exc.name, errorMsg = exc.msg closeObjects(request, redirect, response) raise exc except HttpError as exc: debug "REST client communication error", address, connection = request.connection, errorName = exc.name, errorMsg = exc.msg closeObjects(request, redirect, response) raiseRestCommunicationError(exc) proc requestWithBody*(req: HttpClientRequestRef, pbytes: pointer, nbytes: uint64, chunkSize: int, flags: set[RestRequestFlag]): Future[RestPlainResponse] {. async.} = doAssert(chunkSize > 0 and chunkSize <= high(int)) var request = req redirect: HttpClientRequestRef = nil response: HttpClientResponseRef = nil writer: HttpBodyWriter = nil address = request.address pbuffer = cast[ptr UncheckedArray[byte]](pbytes) while true: try: debug "Sending REST request to remote server", address, http_method = $request.meth # Sending HTTP request headers and obtain HTTP request body writer writer = await request.open() debug "Opened connection to remote server", address, http_method = $request.meth, connection = request.connection # Sending HTTP request body var offset = 0'u64 while offset < nbytes: let toWrite = int(min(nbytes - offset, uint64(chunkSize))) await writer.write(unsafeAddr pbuffer[offset], toWrite) offset = offset + uint64(toWrite) # Finishing HTTP request body debug "REST request body has been sent", address, size = nbytes, http_method = $request.meth, connection = request.connection await writer.finish() await writer.closeWait() writer = nil # Waiting for response headers response = await request.finish() debug "Got REST response headers from remote server", status = response.status, http_method = $request.meth, address, connection = request.connection if response.status >= 300 and response.status < 400: # Handling redirection redirect = block: if "location" in response.headers: let location = response.headers.getString("location") if len(location) > 0: let res = request.redirect(parseUri(location)) if res.isErr(): raiseRestRedirectionError(res.error()) res.get() else: raiseRestRedirectionError("Location header with an empty value") else: raiseRestRedirectionError("Location header missing") # We do not care about response body in redirection. discard await response.consumeBody() await request.closeWait() request = nil await response.closeWait() response = nil request = redirect redirect = nil else: let res = block: let status = response.status let contentType = response.headers.getString("content-type") let data = block: var default: seq[byte] if RestRequestFlag.ConsumeBody in flags: discard await response.consumeBody() default else: await response.getBodyBytes() debug "Received REST response body from remote server", contentType = contentType, size = len(data), address, connection = request.connection await request.closeWait() request = nil await response.closeWait() response = nil RestPlainResponse(status: status, contentType: contentType, data: data) return res except CancelledError as exc: # TODO: when `finally` proved to work inside loops, move closeWait() logic # to `finally` handler. debug "REST request was interrupted", address, connection = request.connection closeObjects(writer, request, redirect, response) raise exc except RestError as exc: debug "REST client redirection error", address, connection = request.connection, errorName = exc.name, errorMsg = exc.msg closeObjects(writer, request, redirect, response) raise exc except HttpError as exc: debug "REST client communication error", address, connection = request.connection, errorName = exc.name, errorMsg = exc.msg closeObjects(writer, request, redirect, response) raiseRestCommunicationError(exc) except AsyncStreamError as exc: # Because `HttpBodyWriter` is actually `AsyncStream` it could raise # `AsyncStreamError` exception. This can happen when we sending request's # body. debug "REST client communication error", address, connection = request.connection, errorName = exc.name, errorMsg = exc.msg closeObjects(writer, request, redirect, response) raiseRestCommunicationError(exc) except CatchableError as exc: debug "REST client got an unexpected error", address, connection = request.connection, errorName = exc.name, errorMsg = exc.msg closeObjects(writer, request, redirect, response) raise(exc) proc restSingleProc(prc: NimNode): NimNode {.compileTime.} = if prc.kind notin {nnkProcDef}: error("Cannot transform this node kind into an REST client procedure." & " Only `proc` definition expected.") let parameters = prc.findChild(it.kind == nnkFormalParams) requestPath = newIdentNode("requestPath") requestQuery = newIdentNode("requestQuery") requestIdent = newIdentNode("request") requestFlagsIdent = newIdentNode("requestFlags") responseResultIdent = newIdentNode("responseResult") responseObjectIdent = newIdentNode("responseObject") clientIdent = newIdentNode(RestClientArg) contentTypeIdent = newIdentNode(RestContentTypeArg) acceptTypeIdent = newIdentNode(RestAcceptTypeArg) extraHeadersIdent = newIdentNode(ExtraHeadersArg) var statements = newStmtList() block: let ares = prc.getAsyncPragma() if not(isNil(ares)): error("REST procedure should not have {.async.} pragma", ares) block: let bres = prc.findChild(it.kind == nnkStmtList) if not(isNil(bres)): error("REST procedure should not have body code", prc) let (returnType, returnKind) = block: parameters.expectMinLen(1) if parameters[0].kind == nnkEmpty: error("REST procedure should non have empty return value", parameters) let node = copyNimTree(parameters[0]) case node.kind of nnkIdent: case node.strVal() of "RestStatus": (node, RestReturnKind.Status) of "RestPlainResponse": (node, RestReturnKind.PlainResponse) else: (node, RestReturnKind.Value) of nnkBracketExpr: case node[0].strVal() of "RestResponse": (node[1], RestReturnKind.GenericResponse) else: (node, RestReturnKind.Value) else: (node, RestReturnKind.Value) let endpointValue = prc.getEndpointOrDefault("") let acceptValue = prc.getAcceptOrDefault(DefaultAcceptContentType) let methodValue = prc.getMethodOrDefault(newDotExpr(ident("HttpMethod"), ident("MethodGet"))) let spath = SegmentedPath.init(HttpMethod.MethodGet, endpointValue, nil) var patterns = spath.getPatterns() let isPostMethod = methodValue.isPostMethod() let (bodyArgument, optionalArguments, pathArguments) = block: var bodyRes: Option[tuple[name, ntype, ename, literal: NimNode]] var optionalRes: seq[tuple[name, ntype, ename, literal: NimNode]] var pathRes: seq[tuple[name, ntype, ename, literal: NimNode]] for paramName, paramType in parameters.paramsIter(): if $paramName in NotAllowedArgumentNames: error("Argument name is reserved name, please choose another one", paramName) let literal = newStrLitNode($paramName) let index = patterns.find($paramName) if index >= 0: if isOptionalArg(paramType): error("Path argument could not be of Option[T] type", paramName) if isSequenceArg(paramType) and not(isBytesArg(paramType)): error("Path argument could not be of iterable type") patterns.delete(index) let decodedName = newIdentNode($paramName & "PathEncoded") pathRes.add((paramName, paramType, decodedName, literal)) else: let name = $paramName if name.startsWith("body"): if bodyRes.isSome(): error("More then one body argument (starts with `body`) present", paramName) let decodedName = newIdentNode($paramName & "BodyEncoded") bodyRes = some((paramName, paramType, decodedName, literal)) else: let decodedName = newIdentNode($paramName & "OptEncoded") optionalRes.add((paramName, paramType, decodedName, literal)) (bodyRes, optionalRes, pathRes) if len(patterns) != 0: error("Some of the arguments that are present in the path are missing: [" & patterns.join(", ") & "]", parameters) for item in pathArguments: let paramName = item.name let paramLiteral = item.literal let encodedName = item.ename statements.add quote do: let `encodedName` = block: let res = encodeString(`paramName`) if res.isErr(): raiseRestEncodingStringError(`paramLiteral`) res.get() for item in optionalArguments: let paramName = item.name let paramLiteral = item.literal let encodedName = item.ename if isOptionalArg(item.ntype): statements.add quote do: let `encodedName` = block: if `paramName`.isSome(): let res = encodeString(`paramName`.get()) if res.isErr(): raiseRestEncodingStringError(`paramLiteral`) var sres = `paramLiteral` sres.add('=') sres.add(encodeUrl(res.get(), true)) sres else: "" elif isSequenceArg(item.ntype): if isBytesArg(item.ntype): statements.add quote do: let `encodedName` = block: let res = encodeString(`paramName`) if res.isErr(): raiseRestEncodingStringError(`paramLiteral`) var sres = `paramLiteral` sres.add('=') sres.add(encodeUrl(res.get(), true)) sres else: statements.add quote do: let `encodedName` = block: if RestClientFlag.CommaSeparatedArray in `clientIdent`.flags: var res: seq[string] for item in `paramName`.items(): let eres = encodeString(item) if eres.isErr(): raiseRestEncodingStringError(`paramLiteral`) res.add(encodeUrl(eres.get(), true)) if len(res) > 0: var sres = `paramLiteral` sres.add('=') sres.add(res.join(",")) sres else: "" else: var res: seq[string] for item in `paramName`.items(): let eres = encodeString(item) if eres.isErr(): raiseRestEncodingStringError(`paramLiteral`) var sres = `paramLiteral` sres.add('=') sres.add(encodeUrl(eres.get(), true)) res.add(sres) res.join("&") else: statements.add quote do: let `encodedName` = block: let res = encodeString(`paramName`) if res.isErr(): raiseRestEncodingStringError(`paramLiteral`) var sres = `paramLiteral` sres.add('=') sres.add(encodeUrl(res.get(), true)) sres if bodyArgument.isSome(): let bodyItem = bodyArgument.get() let paramName = bodyItem.name let paramLiteral = bodyItem.literal let encodedName = bodyItem.ename if not(isPostMethod): error("Non-post method should not contain `body` argument", paramName) statements.add quote do: let `encodedName` = block: let res = encodeBytes(`paramName`, `contentTypeIdent`) if res.isErr(): raiseRestEncodingBytesError(`paramLiteral`) res.get() else: if isPostMethod: error("POST/PUT/PATCH/DELETE requests must have `body` argument", parameters) if len(pathArguments) > 0: let pathLiteral = newStrLitNode(endpointValue) let arrayItems = newArrayNode( pathArguments.mapIt(newPar(it.literal, it.ename)) ) statements.add quote do: let `requestPath` = createPath(`pathLiteral`, `arrayItems`) else: let pathLiteral = newStrLitNode(endpointValue) statements.add quote do: let `requestPath` = `pathLiteral` if len(optionalArguments) > 0: let arrayItems = newArrayNode(optionalArguments.mapIt(it.ename)) statements.add quote do: let `requestQuery` = block: let queryArgs = `arrayItems` var res: string for item in queryArgs: if len(item) > 0: if len(res) > 0: res.add("&") res.add(item) res else: let optionLiteral = newStrLitNode("") statements.add quote do: let `requestQuery` = `optionLiteral` case returnKind of RestReturnKind.Status: statements.add quote do: let `requestFlagsIdent`: set[RestRequestFlag] = { RestRequestFlag.ConsumeBody } else: statements.add quote do: let `requestFlagsIdent`: set[RestRequestFlag] = {} if isPostMethod: let bodyIdent = bodyArgument.get().ename statements.add quote do: let `responseObjectIdent` = block: let chunkSize = `clientIdent`.session.connectionBufferSize let `requestIdent` = createPostRequest( `clientIdent`, `requestPath`, `requestQuery`, `contentTypeIdent`, `acceptTypeIdent`, `extraHeadersIdent`, `methodValue`, uint64(len(`bodyIdent`)) ) await requestWithBody(`requestIdent`, cast[pointer](unsafeAddr `bodyIdent`[0]), uint64(len(`bodyIdent`)), chunkSize, `requestFlagsIdent`) else: statements.add quote do: let `responseObjectIdent` = block: let `requestIdent` = createGetRequest( `clientIdent`, `requestPath`, `requestQuery`, `contentTypeIdent`, `acceptTypeIdent`, `extraHeadersIdent`, `methodValue` ) await requestWithoutBody(`requestIdent`, `requestFlagsIdent`) case returnKind of RestReturnKind.Status: # Result will contain only HTTP status. statements.add quote do: return RestStatus(`responseObjectIdent`.status) of RestReturnKind.PlainResponse: # Result will contain HTTP status, HTTP content-type and sequence of bytes. statements.add quote do: return `responseObjectIdent` of RestReturnKind.GenericResponse: # Result will contain HTTP status, HTTP content-type and decoded value. statements.add quote do: let `responseResultIdent` = block: let res = decodeBytes(`returnType`, `responseObjectIdent`.data, `responseObjectIdent`.contentType) if res.isErr(): raiseRestDecodingBytesError(res.error()) res.get() return RestResponse[`returnType`]( status: `responseObjectIdent`.status, contentType: `responseObjectIdent`.contentType, data: `responseResultIdent` ) of RestReturnKind.Value: # Result will be only decoded value, if HTTP status is not in [200, 299] # exception `RestResponseError` will be raised. statements.add quote do: if `responseObjectIdent`.status < 200 or `responseObjectIdent`.status >= 300: raiseRestResponseError(`responseObjectIdent`) let `responseResultIdent` = block: let res = decodeBytes(`returnType`, `responseObjectIdent`.data, `responseObjectIdent`.contentType) if res.isErr(): raiseRestDecodingBytesError(res.error()) res.get() return `responseResultIdent` let res = transformProcDefinition(prc, clientIdent, contentTypeIdent, acceptTypeIdent, extraHeadersIdent, acceptValue, statements) res macro rest*(prc: untyped): untyped = let res = if prc.kind == nnkStmtList: var statements = newStmtList() for oneProc in prc: statements.add restSingleProc(oneProc) statements else: restSingleProc(prc) when defined(nimDumpRest): echo repr res res