Skip to content

Commit

Permalink
Merge pull request #382 from HapticX/dev
Browse files Browse the repository at this point in the history
Implement Cached decorator
  • Loading branch information
Ethosa authored Dec 17, 2024
2 parents 5476224 + 59aca19 commit 8c0be20
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 11 deletions.
4 changes: 4 additions & 0 deletions examples/website/src/docs/decorators.nim
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ proc Decorators*(): TagRef =
CodeBlock("nim", nimAuthJWT, "auth_jwt")
CodeBlock("nim", nimAuthBearerJWT, "auth_bearer_jwt")

tP: { translate"You can also use the @Cached decorator to cache the result of your routes." }

CodeBlock("nim", nimCachedDecorator, "cached_decorator")

Tip:
tDiv(class = "flex gap-2"):
tP: { translate"To use JWT, you need to install the library" }
Expand Down
28 changes: 28 additions & 0 deletions examples/website/src/ui/code/nim_ssr.nim
Original file line number Diff line number Diff line change
Expand Up @@ -670,4 +670,32 @@ serve "127.0.0.1", 5123:
if token.hasKey("name"):
return "Hello, " & token["name"].node.str
return "who are you???"
"""
nimCachedDecorator* = """
model TestModel:
username: string
password: string
serve ... :
@Cached # Expires in 60 seconds by default
get "/cached/{i:int}":
await sleepAsync(1000)
if true:
if (query?test) == "hello":
return 100
echo query?one
return i
@Cached(120) # Will expires in 120 seconds
get "/cached/{x}":
await sleepAsync(1000)
if hasKey(query, "key"):
return query["key"]
await sleepAsync(1000)
return x
@Cached(expires = 200) # Will expires in 200 seconds
post "/cached/[m:TestModel]":
await sleepAsync(1000)
return m.username
"""
6 changes: 6 additions & 0 deletions examples/website/src/ui/translations.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2696,6 +2696,12 @@ translatable:
"zh" -> "要使用JWT,您需要安装库"
"fr" -> "Pour utiliser JWT, vous devez installer la bibliothèque"
"ko" -> "JWT를 사용하려면 라이브러리를 설치해야 합니다"
"You can also use the @Cached decorator to cache the result of your routes.":
"ru" -> "Вы также можете использовать декоратор @Cached для того, чтобы кэшировать результат ваших маршрутов."
"ja" -> "また、@Cachedデコレーターを使用して、ルートの結果をキャッシュすることもできます。"
"zh" -> "您还可以使用@Cached装饰器来缓存您的路由结果。"
"fr" -> "Vous pouvez également utiliser le décorateur @Cached pour mettre en cache le résultat de vos routes."
"ko" -> "@Cached 데코레이터를 사용하여 경로의 결과를 캐시할 수도 있습니다."


var spokenLang: cstring
Expand Down
2 changes: 1 addition & 1 deletion happyx.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

description = "Macro-oriented asynchronous web-framework written with ♥"
author = "HapticX"
version = "4.6.5"
version = "4.7.0"
license = "MIT"
srcDir = "src"
installExt = @["nim"]
Expand Down
11 changes: 8 additions & 3 deletions src/happyx/cli/dev_command.nim
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import illwill except


proc devCommand*(host: string = "127.0.0.1", port: int = 5000,
reload: bool = false, browser: bool = false): int =
reload: bool = false, browser: bool = false,
disableApiDoc: bool = false): int =
## Serve
var options: seq[string] = @[]
if disableApiDoc:
options.add "-d:disableApiDoc"
var
project = compileProject()
needReload = false
Expand Down Expand Up @@ -40,8 +44,8 @@ proc devCommand*(host: string = "127.0.0.1", port: int = 5000,
styledEcho "⚡ Server launched at ", fgGreen, styleUnderscore, "http://" & host & ":" & port, fgWhite
if browser:
openDefaultBrowser("http://" & host & ":" & port & "/")
styledEcho fgYellow, "if you want to quit from program, please input [q] char"
while true:
styledEcho fgYellow, "if you want to quit from program, please input [q] char"
if stdin.readChar() == 'q':
break
if not project.process.isNil:
Expand All @@ -59,7 +63,8 @@ proc devCommand*(host: string = "127.0.0.1", port: int = 5000,

# Start server for SPA
styledEcho "⚡ Server launched at ", fgGreen, styleUnderscore, "http://127.0.0.1:", $port, fgWhite
openDefaultBrowser("http://127.0.0.1:" & $port & "/")
if browser:
openDefaultBrowser("http://127.0.0.1:" & $port & "/")

serve host, port:
get "/":
Expand Down
7 changes: 4 additions & 3 deletions src/happyx/core/constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ const
enableUseCompDebugMacro* = defined(useCompDebug) or defined(happyxUseCompDebug) or defined(hpxUseCompDebug)
enableRequestModelDebugMacro* = defined(reqModelDebug) or defined(happyxReqModelDebug) or defined(hpxReqModelDebug)
enableRoutingDebugMacro* = defined(routingDebug) or defined(happyxRoutingDebug) or defined(hpxRoutingDebug)
enableDefaultDecorators* = not (defined(disableDefDeco) or defined(happyxDsableDefDeco) or defined(hpxDisableDefDeco))
enableDefaultDecorators* = not (defined(disableDefDeco) or defined(happyxDisableDefDeco) or defined(hpxDisableDefDeco))
enableCachedRoutes* = not (defined(disableCachedRoutes) or defined(happyxDisableCachedRoutes) or defined(hpxDisabledCachedRoutes))
enableDefaultComponents* = not (defined(disableComp) or defined(happyxDisableComp) or defined(hpxDisableComp))
enableAppRouting* = not (defined(disableRouting) or defined(happyxDisableRouting) or defined(hpxDisableRouting))
enableTemplateEngine* = not (defined(disableTemplateEngine) or defined(happyxTemplateEngine) or defined(hpxTemplateEngine))
Expand Down Expand Up @@ -108,8 +109,8 @@ const
nim_2_0_0* = (NimMajor, NimMinor, NimPatch) >= (2, 0, 0)
# Framework version
HpxMajor* = 4
HpxMinor* = 6
HpxPatch* = 5
HpxMinor* = 7
HpxPatch* = 0
HpxVersion* = $HpxMajor & "." & $HpxMinor & "." & $HpxPatch


Expand Down
3 changes: 2 additions & 1 deletion src/happyx/hpx.nim
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ proc flagsCommandAux(): int = flagsCommand()
proc translateCsvCommandAux(filename: string, output: string = ""): int =
translateCsvCommand(filename, output)
proc devCommandAux(host: string = "127.0.0.1", port: int = 5000,
reload: bool = false, browser: bool = false): int =
reload: bool = false, browser: bool = false,
disableApiDoc: bool = false): int =
devCommand(host, port, reload)
proc createCommandAux(name: string = "", kind: string = "", templates: bool = false,
pathParams: bool = false, useTailwind: bool = false, language: string = ""): int =
Expand Down
20 changes: 20 additions & 0 deletions src/happyx/private/macro_utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ proc isIdentUsed*(body, name: NimNode): bool =
false


proc findAllUses*(body, name: NimNode, uses: var seq[NimNode]) =
## Рекурсивно ищет все использования идентификатора `name` в дереве AST `body`.
for statement in body:
if body.kind in {nnkIdentDefs, nnkExprEqExpr, nnkExprColonExpr} and statement == body[0]:
continue
if body.kind == nnkDotExpr and statement == body[1] and statement != body[0]:
continue
if statement == name:
uses.add(body) # Добавляем узел, где найдено использование
elif statement.kind notin AtomicNodes:
findAllUses(statement, name, uses)


proc getIdentUses*(body, name: NimNode): seq[NimNode] =
## Возвращает все использования идентификатора `name` в дереве AST `body`.
var uses: seq[NimNode] = @[]
findAllUses(body, name, uses)
return uses


proc newCast*(fromType, toType: NimNode): NimNode =
newNimNode(nnkCast).add(toType, fromType)

Expand Down
115 changes: 113 additions & 2 deletions src/happyx/routing/decorators.nim
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,31 @@ import
std/macros,
std/tables,
std/strformat,
std/strutils,
std/base64,
../core/constants
std/httpcore,
../core/constants,
../private/macro_utils,
./routing


export base64


type
DecoratorImpl* = proc(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode])
DecoratorImpl* = proc(
httpMethods: seq[string],
routePath: string,
statementList: NimNode,
arguments: seq[NimNode]
)
CachedResult* = object
data*: string
headers*: HttpHeaders
statusCode*: HttpCode
CachedRoute* = object
create_at*: float
res*: CachedResult


var decorators* {.compileTime.} = newTable[string, DecoratorImpl]()
Expand Down Expand Up @@ -74,6 +90,9 @@ macro decorator*(name, body: untyped): untyped =


when enableDefaultDecorators:
var cachedRoutes* {.threadvar.}: Table[string, CachedRoute]
cachedRoutes = initTable[string, CachedRoute]()

proc authBasicDecoratorImpl(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode]) =
statementList.insert(0, parseStmt"""
var (username, password) = ("", "")
Expand Down Expand Up @@ -123,8 +142,100 @@ var userAgent = navigator.userAgent
)


proc cachedDecoratorImpl(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode]) =
let
route = handleRoute(routePath)
purePath = route.purePath.replace('{', '_').replace('}', '_')

let expiresIn =
if arguments.len == 1 and arguments[0].kind in {nnkIntLit, nnkInt16Lit, nnkInt32Lit, nnkInt64Lit, nnkInt8Lit}:
newLit(arguments[0].intVal.int)
elif arguments.len == 1 and arguments[0].kind == nnkExprEqExpr and arguments[0][0] == ident"expires":
if arguments[0][1].kind in {nnkIntLit, nnkInt16Lit, nnkInt32Lit, nnkInt64Lit, nnkInt8Lit}:
newLit(arguments[0][1].intVal.int)
else:
newLit(60)
else:
newLit(60)

var routeKey = fmt"{purePath}("
for i in route.pathParams:
routeKey &= i.name & "={" & i.name & "}"
for i in route.requestModels:
routeKey &= i.name & "={" & i.name & ".repr}"
routeKey &= ")"

let queryStmt = newStmtList()
var usedVariables: seq[NimNode] = @[]

for identName in ["query", "queryArr"]:
let idnt = ident(identName)
if statementList.isIdentUsed(idnt):
var usages = statementList.getIdentUses(idnt)
for i in usages:
# query?KEY
if i.kind == nnkInfix and i[0] == ident"?" and i[1] == idnt and i[2].kind == nnkIdent:
if i[2] notin usedVariables:
queryStmt.add parseStmt(
fmt"""routeKey &= "{i[2]}" & "=" & {identName}.getOrDefault("{i[2]}", "")"""
)
usedVariables.add i[2]
# query["KEY"]
elif i.kind == nnkBracketExpr and i[0] == idnt and i[1].kind == nnkStrLit:
if i[1] notin usedVariables:
queryStmt.add parseStmt(
fmt"""routeKey &= "{i[1].strVal}" & "=" & {identName}.getOrDefault("{i[1].strVal}", "")"""
)
usedVariables.add i[1]
# query[KEY]
elif i.kind == nnkBracketExpr and i[0] == idnt:
if i[1] notin usedVariables:
queryStmt.add parseStmt(
fmt"""routeKey &= {i[1].toStrLit} & "=" & {identName}.getOrDefault({i[1].toStrLit}, "")"""
)
usedVariables.add i[1]
# hasKey(query, KEY)
elif i.kind == nnkCall and i[0] == ident"hasKey" and i[1] == idnt and i.len == 3:
if i[2] notin usedVariables:
queryStmt.add parseStmt(
fmt"""routeKey &= {i[2].toStrLit} & "=" & {identName}.getOrDefault({i[2].toStrLit}, "")"""
)
usedVariables.add i[2]

let cachedRoutesResult = newNimNode(nnkDotExpr).add(
newNimNode(nnkBracketExpr).add(ident"cachedRoutes", ident"routeKey"), ident"res"
)
let cachedRoutesCreateAt = newNimNode(nnkDotExpr).add(
newNimNode(nnkBracketExpr).add(ident"cachedRoutes", ident"routeKey"), ident"create_at"
)

statementList.insert(0, newStmtList(
newVarStmt(ident"routeKey", newCall("fmt", newLit(fmt"{routeKey}"))),
queryStmt,
newConstStmt(ident"thisRouteCanBeCached", newLit(true)),
newNimNode(nnkIfStmt).add(newNimNode(nnkElifBranch).add(
newCall("hasKey", ident"cachedRoutes", ident"routeKey"),
newNimNode(nnkIfStmt).add(newNimNode(nnkElifBranch).add(
newCall("<", newCall("-", newCall("cpuTime"), cachedRoutesCreateAt), expiresIn),
newStmtList(
newConstStmt(ident"thisIsCachedResponse", newLit(true)),
newCall(
"answer",
ident"req",
newNimNode(nnkDotExpr).add(cachedRoutesResult, ident"data"),
newNimNode(nnkDotExpr).add(cachedRoutesResult, ident"statusCode"),
newNimNode(nnkDotExpr).add(cachedRoutesResult, ident"headers"),
),
newNimNode(nnkBreakStmt).add(ident"__handleRequestBlock")
)
)),
)),
))


static:
regDecorator("AuthBasic", authBasicDecoratorImpl)
regDecorator("AuthBearerJWT", authBearerJwtDecoratorImpl)
regDecorator("AuthJWT", authJwtDecoratorImpl)
regDecorator("GetUserAgent", getUserAgentDecoratorImpl)
regDecorator("Cached", cachedDecoratorImpl)
2 changes: 1 addition & 1 deletion src/happyx/ssr/core.nim
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ else:
import std/posix
import std/osproc

export httpcore
export httpcore, times


func parseHttpMethod*(data: string): Option[HttpMethod] =
Expand Down
31 changes: 31 additions & 0 deletions src/happyx/ssr/server.nim
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import
std/macros,
std/tables,
std/colors,
std/times,
std/json,
std/os,
checksums/md5,
Expand Down Expand Up @@ -99,6 +100,7 @@ export
logging,
cookies,
colors,
times,
utils,
json,
os,
Expand Down Expand Up @@ -286,6 +288,23 @@ template answer*(
when declared(outHeaders):
for key, val in outHeaders.pairs():
h[key] = val

# Cache result
when enableCachedRoutes:
when declared(thisRouteCanBeCached) and declared(routeKey) and not declared(thisIsCachedResponse):
cachedRoutes[routeKey] = CachedRoute(create_at: cpuTime())
when message is string:
cachedRoutes[routeKey].res = CachedResult(data: message)
else:
cachedRoutes[routeKey].res = CachedResult(data: $message)
cachedRoutes[routeKey].res.statusCode = code
when useHeaders:
cachedRoutes[routeKey].res.headers = h
else:
cachedRoutes[routeKey].res.headers = newHttpHeaders([
("Content-Type", "text/plain;charset=utf-8")
])

# HTTPX
when enableHttpx or enableBuiltin:
when useHeaders:
Expand Down Expand Up @@ -395,6 +414,18 @@ template answer*(
when declared(outHeaders):
for key, val in outHeaders.pairs():
h[key] = val

# Cache result
when enableCachedRoutes:
when declared(thisRouteCanBeCached) and declared(routeKey) and not declared(thisIsCachedResponse):
cachedRoutes[routeKey] = CachedRoute(create_at: cpuTime())
when message is string:
cachedRoutes[routeKey].res = CachedResult(data: message)
else:
cachedRoutes[routeKey].res = CachedResult(data: $message)
cachedRoutes[routeKey].res.statusCode = code
cachedRoutes[routeKey].res.headers = h

# HTTPX
when enableHttpx or enableBuiltin:
var headersArr = ""
Expand Down
Loading

0 comments on commit 8c0be20

Please sign in to comment.