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:
parent
e96c6ded2a
commit
9a7e711e22
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue