Add Accept handling to REST server and client. (#12)

* Add Accept handling to REST server and client.
Add proper tests to server and client's test suites.
Fix some warnings.
Bump some nimble dependencies.

* Remove debugging echo.
This commit is contained in:
Eugene Kabanov 2021-09-07 13:28:56 +03:00 committed by GitHub
parent e96c6ded2a
commit 9a7e711e22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 190 additions and 6 deletions

View File

@ -1,14 +1,14 @@
mode = ScriptMode.Verbose
packageName = "presto"
version = "0.0.3"
version = "0.0.4"
author = "Status Research & Development GmbH"
description = "REST API implementation"
license = "MIT"
skipDirs = @["tests", "examples"]
requires "nim >= 1.2.0",
"chronos >= 3.0.3",
"chronos >= 3.0.6",
"chronicles",
"stew"

View File

@ -15,6 +15,7 @@ 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
@ -49,6 +50,7 @@ type
Status, PlainResponse, GenericResponse, Value
const
DefaultAcceptContentType = "application/json"
RestContentTypeArg = "restContentType"
RestAcceptTypeArg = "restAcceptType"
RestClientArg = "restClient"
@ -158,6 +160,15 @@ proc getMethodOrDefault(prc: NimNode,
return node[1]
return default
proc getAcceptOrDefault(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 == "accept":
return node[1].strVal()
return default
proc getAsyncPragma(prc: NimNode): NimNode {.compileTime.} =
let pragmaNode = prc.pragma()
for node in pragmaNode.items():
@ -255,6 +266,7 @@ proc isPostMethod(node: NimNode): bool {.compileTime.} =
proc transformProcDefinition(prc: NimNode, clientIdent: NimNode,
contentIdent: NimNode,
acceptIdent: NimNode,
acceptValue: NimNode,
stmtList: NimNode): NimNode {.compileTime.} =
var procdef = copyNimTree(prc)
var parameters = copyNimTree(prc.findChild(it.kind == nnkFormalParams))
@ -267,7 +279,7 @@ proc transformProcDefinition(prc: NimNode, clientIdent: NimNode,
newStrLitNode("application/json"))
let acceptTypeArg =
newTree(nnkIdentDefs, acceptIdent, newIdentNode("string"),
newStrLitNode("application/json"))
acceptValue)
let asyncPragmaArg = newIdentNode("async")
@ -607,6 +619,14 @@ proc restSingleProc(prc: NimNode): NimNode {.compileTime.} =
prc.pragma())
res
let accept =
block:
let res = prc.getAcceptOrDefault(DefaultAcceptContentType)
if len(res) == 0:
error("REST procedure should have non-empty {.accept.} pragma",
prc.pragma())
res
let meth = prc.getMethodOrDefault(newDotExpr(ident("HttpMethod"),
ident("MethodGet")))
let spath = SegmentedPath.init(HttpMethod.MethodGet, endpoint, nil)
@ -863,7 +883,8 @@ proc restSingleProc(prc: NimNode): NimNode {.compileTime.} =
return `responseResultIdent`
let res = transformProcDefinition(prc, clientIdent, contentTypeIdent,
acceptTypeIdent, statements)
acceptTypeIdent, newStrLitNode(accept),
statements)
res
macro rest*(prc: untyped): untyped =

View File

@ -327,7 +327,9 @@ macro api*(router: RestRouter, meth: static[HttpMethod],
proc `doMain`(`requestParam`: HttpRequestRef, `pathParams`: HttpTable,
`queryParams`: HttpTable, `bodyParam`: Option[ContentBody]
): Future[RestApiResponse] {.raises: [Defect], async.} =
template preferredContentType(
t: varargs[string]): Result[string, cstring] {.used.} =
`requestParam`.preferredContentType(t)
`pathDecoder`
`optDecoder`
`respDecoder`

View File

@ -1051,6 +1051,88 @@ suite "REST API client test suite":
await server.stop()
await server.closeWait()
asyncTest "Accept test":
var router = RestRouter.init(testValidate)
router.api(MethodPost, "/test/accept") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
let obody =
if contentBody.isSome():
let b = contentBody.get()
b.contentType & "," & bytesToString(b.data)
else:
"nobody"
let preferred = preferredContentType("app/type1", "app/type2")
return
if preferred.isOk():
case preferred.get()
of "app/type1":
RestApiResponse.response("type1[" & obody & "]")
of "app/type2":
RestApiResponse.response("type2[" & obody & "]")
else:
# This MUST not be happened.
RestApiResponse.error(Http407, "")
else:
RestApiResponse.error(Http406, "")
var sres = RestServerRef.new(router, serverAddress)
let server = sres.get()
server.start()
proc testAccept1(body: string): RestPlainResponse {.
rest, endpoint: "/test/accept", meth: MethodPost,
accept: "*/*".}
proc testAccept2(body: string): RestPlainResponse {.
rest, endpoint: "/test/accept", meth: MethodPost,
accept: "app/type1,app/type2".}
proc testAccept3(body: string): RestPlainResponse {.
rest, endpoint: "/test/accept", meth: MethodPost,
accept: "app/type2".}
proc testAccept4(body: string): RestPlainResponse {.
rest, endpoint: "/test/accept", meth: MethodPost,
accept: "app/type2;q=0.5,app/type1;q=0.7".}
proc testAccept5(body: string): RestPlainResponse {.
rest, endpoint: "/test/accept", meth: MethodPost,
accept: "app/type2".}
proc testAccept6(body: string): RestPlainResponse {.
rest, endpoint: "/test/accept", meth: MethodPost.}
var client = RestClientRef.new(serverAddress, HttpClientScheme.NonSecure)
let res1 = await client.testAccept1("accept1")
let res2 = await client.testAccept2("accept2")
let res3 = await client.testAccept3("accept3")
let res4 = await client.testAccept4("accept4")
let res5 = await client.testAccept5("accept5")
# This procedure is missing `accept` pragma in definition, so default
# accept will be used `application/json`.
let res6 = await client.testAccept6("accept6")
let res7 = await client.testAccept6("accept7",
restAcceptType = "app/type1;q=1.0,app/type2;q=0.1")
let res8 = await client.testAccept6("accept8",
restAcceptType = "")
check:
res1.status == 200
res2.status == 200
res3.status == 200
res4.status == 200
res5.status == 200
res6.status == 406
res7.status == 200
res8.status == 200
res1.data.bytesToString() == "type1[application/json,accept1]"
res2.data.bytesToString() == "type1[application/json,accept2]"
res3.data.bytesToString() == "type2[application/json,accept3]"
res4.data.bytesToString() == "type1[application/json,accept4]"
res5.data.bytesToString() == "type2[application/json,accept5]"
res7.data.bytesToString() == "type1[application/json,accept7]"
res8.data.bytesToString() == "type1[application/json,accept8]"
await client.closeWait()
await server.stop()
await server.closeWait()
test "Leaks test":
proc getTrackerLeaks(tracker: string): bool =
let tracker = getTracker(tracker)

View File

@ -12,12 +12,15 @@ type
data*: string
proc httpClient(server: TransportAddress, meth: HttpMethod, url: string,
body: string, ctype = ""): Future[ClientResponse] {.async.} =
body: string, ctype = "",
accept = ""): Future[ClientResponse] {.async.} =
var request = $meth & " " & $parseUri(url) & " HTTP/1.1\r\n"
request.add("Host: " & $server & "\r\n")
request.add("Content-Length: " & $len(body) & "\r\n")
if len(ctype) > 0:
request.add("Content-Type: " & ctype & "\r\n")
if len(accept) > 0:
request.add("Accept: " & accept & "\r\n")
request.add("\r\n")
if len(body) > 0:
@ -528,6 +531,82 @@ suite "REST API server test suite":
finally:
await server.closeWait()
asyncTest "preferredContentType() test":
const PostVectors = [
(
("/test/post", "somebody0908", "text/html",
"app/type1;q=0.9,app/type2;q=0.8"),
ClientResponse(status: 200, data: "type1[text/html,somebody0908]")
),
(
("/test/post", "somebody0908", "text/html",
"app/type2;q=0.8,app/type1;q=0.9"),
ClientResponse(status: 200, data: "type1[text/html,somebody0908]")
),
(
("/test/post", "somebody09", "text/html",
"app/type2,app/type1;q=0.9"),
ClientResponse(status: 200, data: "type2[text/html,somebody09]")
),
(
("/test/post", "somebody09", "text/html", "app/type1;q=0.9,app/type2"),
ClientResponse(status: 200, data: "type2[text/html,somebody09]")
),
(
("/test/post", "somebody", "text/html", "*/*"),
ClientResponse(status: 200, data: "type1[text/html,somebody]")
),
(
("/test/post", "somebody", "text/html", ""),
ClientResponse(status: 200, data: "type1[text/html,somebody]")
),
(
("/test/post", "somebody", "text/html", "app/type2"),
ClientResponse(status: 200, data: "type2[text/html,somebody]")
),
(
("/test/post", "somebody", "text/html", "app/type3"),
ClientResponse(status: 406, data: "")
)
]
var router = RestRouter.init(testValidate)
router.api(MethodPost, "/test/post") do (
body: Option[ContentBody], resp: HttpResponseRef) -> RestApiResponse:
let obody =
if body.isSome():
let b = body.get()
b.contentType & "," & bytesToString(b.data)
else:
"nobody"
let preferred = preferredContentType("app/type1", "app/type2")
return
if preferred.isOk():
case preferred.get()
of "app/type1":
RestApiResponse.response("type1[" & obody & "]")
of "app/type2":
RestApiResponse.response("type2[" & obody & "]")
else:
# This MUST not be happened.
RestApiResponse.error(Http407, "")
else:
RestApiResponse.error(Http406, "")
var sres = RestServerRef.new(router, serverAddress)
let server = sres.get()
server.start()
try:
for item in PostVectors:
let res = await httpClient(serverAddress, MethodPost, item[0][0],
item[0][1], item[0][2], item[0][3])
check:
res.status == item[1].status
res.data == item[1].data
finally:
await server.closeWait()
test "Leaks test":
check:
getTracker("async.stream.reader").isLeaked() == false