From 9a510389d3b57638a6973341ac4f91494c9a6b3a Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Wed, 9 Aug 2023 09:18:29 -0500 Subject: [PATCH] Add object to standardize the API response data format. --- README.md | 36 ++++++++++++++++++++++ buffoonery.nimble | 2 +- src/buffoonery/apiutils.nim | 58 ++++++++++++++++++++++++------------ src/buffoonery/jsonutils.nim | 6 ++-- 4 files changed, 79 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e69de29..a9455c5 100644 --- a/README.md +++ b/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 diff --git a/buffoonery.nimble b/buffoonery.nimble index 8946df9..dcbc085 100644 --- a/buffoonery.nimble +++ b/buffoonery.nimble @@ -1,6 +1,6 @@ # Package -version = "0.2.3" +version = "0.3.0" author = "Jonathan Bernard" description = "Jonathan's opinionated extensions and auth layer for Jester." license = "MIT" diff --git a/src/buffoonery/apiutils.nim b/src/buffoonery/apiutils.nim index 68b2546..214f159 100644 --- a/src/buffoonery/apiutils.nim +++ b/src/buffoonery/apiutils.nim @@ -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 ./apierror @@ -12,6 +12,35 @@ template log(): untyped = logNs ## 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*( code: HttpCode, headers: RawHeaders, @@ -31,8 +60,8 @@ template halt*( template sendJsonResp*( body: JsonNode, code: HttpCode = Http200, - knownOrigins: seq[string], - headersToSend: RawHeaders) = + knownOrigins: seq[string] = @[], + headersToSend: RawHeaders = @{:}) = ## Immediately send a JSON response and stop processing the request. let reqOrigin = if headers(request).hasKey("Origin"): $(headers(request)["Origin"]) @@ -59,26 +88,17 @@ template sendJsonResp*( $body ) -proc makeDataBody*( - data: JsonNode, - nextOffset = none[int](), - totalItems = none[int](), - nextLink = none[string](), - prevLink = none[string]()): JsonNode = - - 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 sendResp*[T]( + resp: ApiResponse[T], + code = Http200, + allowedOrigins = newSeq[string](), + headersToSend: RawHeaders = @{:}) = + sendJsonResp(%resp, code, allowedOrigins, headersToSend) template sendErrorResp*(err: ref ApiError, knownOrigins: seq[string]): void = log().error err.respMsg & ( if err.msg.len > 0: ": " & err.msg else: "") 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 template sendOptionsResp*( diff --git a/src/buffoonery/jsonutils.nim b/src/buffoonery/jsonutils.nim index 08c2c2d..1621057 100644 --- a/src/buffoonery/jsonutils.nim +++ b/src/buffoonery/jsonutils.nim @@ -3,14 +3,14 @@ import json, times, timeutils, uuids 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 if not n.hasKey(key): raise newException(ValueError, "missing key '" & key & "'") return n[key] -proc parseUUID*(n: JsonNode, key: string): UUID = +func parseUUID*(n: JsonNode, key: string): UUID = return parseUUID(n.getOrFail(key).getStr) 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 = return parse(n.getOrFail(key).getStr, MONTH_FORMAT) -proc formatMonth*(dt: DateTime): string = +func formatMonth*(dt: DateTime): string = return dt.format(MONTH_FORMAT)