Add object to standardize the API response data format.
This commit is contained in:
parent
3b1e9b5a8d
commit
9a510389d3
36
README.md
36
README.md
@ -0,0 +1,36 @@
|
|||||||
|
# Buffoonery: Tools for the [Jester Web Framework][jester]
|
||||||
|
|
||||||
|
Buffoonery is primarily an opinionated implementation of JWT-based session
|
||||||
|
management
|
||||||
|
a collection of extensions and patterns built around Jester to
|
||||||
|
facilitate the types of API services I tend to write.
|
||||||
|
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
### JDB Software Packages
|
||||||
|
|
||||||
|
Buffoonery depends on a number of packages which are not yet available in he
|
||||||
|
official Nim repository. The easiest way to get access to these packages is to
|
||||||
|
add a new `PackageList` to your [nimble configuration] for the [JDB Software Nim
|
||||||
|
packages repository]. The url is
|
||||||
|
`https://git.jdb-software.com/jdb/nim-packages/raw/main/packages.json`
|
||||||
|
|
||||||
|
[nimble configuration]: https://github.com/nim-lang/nimble#configuration
|
||||||
|
[JDB Software Nim packages]: https://git.jdb-software.com/jdb/nim-packages
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Buffoonery is available under two licenses depending on usage.
|
||||||
|
|
||||||
|
For private use, non-commercial use, or use in small enterprise (defined as any
|
||||||
|
enterprise bringing in less than $1 million in annual gross profit), Buffoonery
|
||||||
|
is available under the [MIT License][mit-license].
|
||||||
|
|
||||||
|
For commercial use in larger enterprises (more than $1 million in annual
|
||||||
|
gross profit), Buffoonery is available under the [GNU Affero General Public
|
||||||
|
License v3.0][agpl3]
|
||||||
|
|
||||||
|
[jester]: https://github.com/dom96/jester/
|
||||||
|
[mit-license]: https://mit-license.org/
|
||||||
|
[agpl3]: https://www.gnu.org/licenses/agpl-3.0.en.html
|
@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.2.3"
|
version = "0.3.0"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Jonathan's opinionated extensions and auth layer for Jester."
|
description = "Jonathan's opinionated extensions and auth layer for Jester."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import std/json, std/logging, std/options, std/strutils, std/sequtils
|
import std/[json, jsonutils, logging, options, strutils, sequtils]
|
||||||
import jester, namespaced_logging
|
import jester, namespaced_logging
|
||||||
|
|
||||||
import ./apierror
|
import ./apierror
|
||||||
@ -12,6 +12,35 @@ template log(): untyped =
|
|||||||
logNs
|
logNs
|
||||||
|
|
||||||
## Response Utilities
|
## Response Utilities
|
||||||
|
## ------------------
|
||||||
|
|
||||||
|
type ApiResponse*[T] = object
|
||||||
|
details*: Option[string]
|
||||||
|
data*: Option[T]
|
||||||
|
nextOffset*: Option[int]
|
||||||
|
totalItems*: Option[int]
|
||||||
|
nextLink*: Option[string]
|
||||||
|
prevLink*: Option[string]
|
||||||
|
|
||||||
|
func initApiResponse*[T](
|
||||||
|
details = none[string](),
|
||||||
|
data: Option[T] = none[T](),
|
||||||
|
nextOffset = none[int](),
|
||||||
|
totalItems = none[int](),
|
||||||
|
nextLink = none[string](),
|
||||||
|
prevLink = none[string]()): ApiResponse[T] =
|
||||||
|
ApiResponse[T](details: details, data: data, nextOffset: nextOffset,
|
||||||
|
totalItems: totalItems, nextLink: nextLink, prevLink: prevLink)
|
||||||
|
|
||||||
|
func `%`*(r: ApiResponse): JsonNode =
|
||||||
|
result = newJObject()
|
||||||
|
if r.details.isSome: result["details"] = %r.details
|
||||||
|
if r.data.isSome: result["data"] = %r.data
|
||||||
|
if r.nextOffset.isSome: result["nextOffset"] = %r.nextOffset
|
||||||
|
if r.totalItems.isSome: result["totalItems"] = %r.totalItems
|
||||||
|
if r.nextLink.isSome: result["nextLink"] = %r.nextLink
|
||||||
|
if r.prevLink.isSome: result["prevLink"] = %r.prevLink
|
||||||
|
|
||||||
template halt*(
|
template halt*(
|
||||||
code: HttpCode,
|
code: HttpCode,
|
||||||
headers: RawHeaders,
|
headers: RawHeaders,
|
||||||
@ -31,8 +60,8 @@ template halt*(
|
|||||||
template sendJsonResp*(
|
template sendJsonResp*(
|
||||||
body: JsonNode,
|
body: JsonNode,
|
||||||
code: HttpCode = Http200,
|
code: HttpCode = Http200,
|
||||||
knownOrigins: seq[string],
|
knownOrigins: seq[string] = @[],
|
||||||
headersToSend: RawHeaders) =
|
headersToSend: RawHeaders = @{:}) =
|
||||||
## Immediately send a JSON response and stop processing the request.
|
## Immediately send a JSON response and stop processing the request.
|
||||||
let reqOrigin =
|
let reqOrigin =
|
||||||
if headers(request).hasKey("Origin"): $(headers(request)["Origin"])
|
if headers(request).hasKey("Origin"): $(headers(request)["Origin"])
|
||||||
@ -59,26 +88,17 @@ template sendJsonResp*(
|
|||||||
$body
|
$body
|
||||||
)
|
)
|
||||||
|
|
||||||
proc makeDataBody*(
|
template sendResp*[T](
|
||||||
data: JsonNode,
|
resp: ApiResponse[T],
|
||||||
nextOffset = none[int](),
|
code = Http200,
|
||||||
totalItems = none[int](),
|
allowedOrigins = newSeq[string](),
|
||||||
nextLink = none[string](),
|
headersToSend: RawHeaders = @{:}) =
|
||||||
prevLink = none[string]()): JsonNode =
|
sendJsonResp(%resp, code, allowedOrigins, headersToSend)
|
||||||
|
|
||||||
result = %*{"details":"","data":data}
|
|
||||||
|
|
||||||
if nextOffset.isSome: result["nextOffset"] = %nextOffset.get
|
|
||||||
if totalItems.isSome: result["totalItems"] = %totalItems.get
|
|
||||||
if nextLink.isSome: result["next"] = %nextLink.get
|
|
||||||
if prevLink.isSome: result["prev"] = %prevLink.get
|
|
||||||
|
|
||||||
proc makeStatusBody*(details: string): JsonNode = %*{"details":details}
|
|
||||||
|
|
||||||
template sendErrorResp*(err: ref ApiError, knownOrigins: seq[string]): void =
|
template sendErrorResp*(err: ref ApiError, knownOrigins: seq[string]): void =
|
||||||
log().error err.respMsg & ( if err.msg.len > 0: ": " & err.msg else: "")
|
log().error err.respMsg & ( if err.msg.len > 0: ": " & err.msg else: "")
|
||||||
if not err.parent.isNil: log().error " original exception: " & err.parent.msg
|
if not err.parent.isNil: log().error " original exception: " & err.parent.msg
|
||||||
sendJsonResp(makeStatusBody(err.respMsg), err.respCode, knownOrigins, @{:})
|
sendJsonResp( %*{"details":err.respMsg}, err.respCode, knownOrigins)
|
||||||
|
|
||||||
## CORS support
|
## CORS support
|
||||||
template sendOptionsResp*(
|
template sendOptionsResp*(
|
||||||
|
@ -3,14 +3,14 @@ import json, times, timeutils, uuids
|
|||||||
|
|
||||||
const MONTH_FORMAT* = "YYYY-MM"
|
const MONTH_FORMAT* = "YYYY-MM"
|
||||||
|
|
||||||
proc getOrFail*(n: JsonNode, key: string): JsonNode =
|
func getOrFail*(n: JsonNode, key: string): JsonNode =
|
||||||
## convenience method to get a key from a JObject or raise an exception
|
## convenience method to get a key from a JObject or raise an exception
|
||||||
if not n.hasKey(key):
|
if not n.hasKey(key):
|
||||||
raise newException(ValueError, "missing key '" & key & "'")
|
raise newException(ValueError, "missing key '" & key & "'")
|
||||||
|
|
||||||
return n[key]
|
return n[key]
|
||||||
|
|
||||||
proc parseUUID*(n: JsonNode, key: string): UUID =
|
func parseUUID*(n: JsonNode, key: string): UUID =
|
||||||
return parseUUID(n.getOrFail(key).getStr)
|
return parseUUID(n.getOrFail(key).getStr)
|
||||||
|
|
||||||
proc parseIso8601*(n: JsonNode, key: string): DateTime =
|
proc parseIso8601*(n: JsonNode, key: string): DateTime =
|
||||||
@ -19,5 +19,5 @@ proc parseIso8601*(n: JsonNode, key: string): DateTime =
|
|||||||
proc parseMonth*(n: JsonNode, key: string): DateTime =
|
proc parseMonth*(n: JsonNode, key: string): DateTime =
|
||||||
return parse(n.getOrFail(key).getStr, MONTH_FORMAT)
|
return parse(n.getOrFail(key).getStr, MONTH_FORMAT)
|
||||||
|
|
||||||
proc formatMonth*(dt: DateTime): string =
|
func formatMonth*(dt: DateTime): string =
|
||||||
return dt.format(MONTH_FORMAT)
|
return dt.format(MONTH_FORMAT)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user