Skip to content

Commit f8d0a48

Browse files
committed
feat: implement @cached route decorator
1 parent b58bdd8 commit f8d0a48

File tree

5 files changed

+166
-3
lines changed

5 files changed

+166
-3
lines changed

src/happyx/private/macro_utils.nim

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,26 @@ proc isIdentUsed*(body, name: NimNode): bool =
6060
false
6161

6262

63+
proc findAllUses*(body, name: NimNode, uses: var seq[NimNode]) =
64+
## Рекурсивно ищет все использования идентификатора `name` в дереве AST `body`.
65+
for statement in body:
66+
if body.kind in {nnkIdentDefs, nnkExprEqExpr, nnkExprColonExpr} and statement == body[0]:
67+
continue
68+
if body.kind == nnkDotExpr and statement == body[1] and statement != body[0]:
69+
continue
70+
if statement == name:
71+
uses.add(body) # Добавляем узел, где найдено использование
72+
elif statement.kind notin AtomicNodes:
73+
findAllUses(statement, name, uses)
74+
75+
76+
proc getIdentUses*(body, name: NimNode): seq[NimNode] =
77+
## Возвращает все использования идентификатора `name` в дереве AST `body`.
78+
var uses: seq[NimNode] = @[]
79+
findAllUses(body, name, uses)
80+
return uses
81+
82+
6383
proc newCast*(fromType, toType: NimNode): NimNode =
6484
newNimNode(nnkCast).add(toType, fromType)
6585

src/happyx/routing/decorators.nim

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,31 @@ import
3535
std/macros,
3636
std/tables,
3737
std/strformat,
38+
std/strutils,
3839
std/base64,
39-
../core/constants
40+
std/httpcore,
41+
../core/constants,
42+
../private/macro_utils,
43+
./routing
4044

4145

4246
export base64
4347

4448

4549
type
46-
DecoratorImpl* = proc(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode])
50+
DecoratorImpl* = proc(
51+
httpMethods: seq[string],
52+
routePath: string,
53+
statementList: NimNode,
54+
arguments: seq[NimNode]
55+
)
56+
CachedResult* = object
57+
data*: string
58+
headers*: HttpHeaders
59+
statusCode*: HttpCode
60+
CachedRoute* = object
61+
create_at*: float
62+
res*: CachedResult
4763

4864

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

7591

7692
when enableDefaultDecorators:
93+
var cachedRoutes* {.threadvar.}: Table[string, CachedRoute]
94+
cachedRoutes = initTable[string, CachedRoute]()
95+
7796
proc authBasicDecoratorImpl(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode]) =
7897
statementList.insert(0, parseStmt"""
7998
var (username, password) = ("", "")
@@ -123,8 +142,87 @@ var userAgent = navigator.userAgent
123142
)
124143

125144

145+
proc cachedDecoratorImpl(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode]) =
146+
let
147+
route = handleRoute(routePath)
148+
purePath = route.purePath.replace('{', '_').replace('}', '_')
149+
150+
let expiresIn =
151+
if arguments.len == 1:
152+
arguments[0]
153+
else:
154+
newLit(60)
155+
156+
var routeKey = fmt"{purePath}:pp("
157+
for i in route.pathParams:
158+
routeKey &= i.name & "={" & i.name & "}"
159+
routeKey &= ")"
160+
echo routeKey
161+
162+
let
163+
queryStmt = newStmtList()
164+
queryArrStmt = newStmtList()
165+
166+
if statementList.isIdentUsed(ident"query"):
167+
var usages = statementList.getIdentUses(ident"query")
168+
for i in usages:
169+
if i.kind == nnkInfix and i[0] == ident"?" and i[1] == ident"query" and i[2].kind == nnkIdent:
170+
queryStmt.add parseStmt(fmt"""routeKey &= "{i[2]}" & "=" & query.getOrDefault("{i[2]}", "")""")
171+
elif i.kind == nnkBracketExpr and i[0] == ident"query" and i[1].kind == nnkStrLit:
172+
queryStmt.add parseStmt(fmt"""routeKey &= "{i[1].strVal}" & "=" & query.getOrDefault("{i[1].strVal}", "")""")
173+
elif i.kind == nnkBracketExpr and i[0] == ident"query":
174+
queryStmt.add parseStmt(fmt"""routeKey &= {i[1].toStrLit} & "=" & query.getOrDefault({i[1].toStrLit}, "")""")
175+
else:
176+
discard
177+
# echo i.treeRepr
178+
if statementList.isIdentUsed(ident"queryArr"):
179+
var usages = statementList.getIdentUses(ident"queryArr")
180+
for i in usages:
181+
if i.kind == nnkInfix and i[0] == ident"?" and i[1] == ident"queryArr" and i[2].kind == nnkIdent:
182+
queryStmt.add parseStmt(fmt"""routeKey &= "{i[2]}" & "=" & $queryArr.getOrDefault("{i[2]}", "")""")
183+
elif i.kind == nnkBracketExpr and i[0] == ident"queryArr" and i[1].kind == nnkStrLit:
184+
queryStmt.add parseStmt(fmt"""routeKey &= "{i[1].strVal}" & "=" & $queryArr.getOrDefault("{i[1].strVal}", "")""")
185+
elif i.kind == nnkBracketExpr and i[0] == ident"queryArr":
186+
queryStmt.add parseStmt(fmt"""routeKey &= {i[1].toStrLit} & "=" & $queryArr.getOrDefault({i[1].toStrLit}, "")""")
187+
else:
188+
discard
189+
# echo i.treeRepr
190+
191+
let cachedRoutesResult = newNimNode(nnkDotExpr).add(
192+
newNimNode(nnkBracketExpr).add(ident"cachedRoutes", ident"routeKey"), ident"res"
193+
)
194+
let cachedRoutesCreateAt = newNimNode(nnkDotExpr).add(
195+
newNimNode(nnkBracketExpr).add(ident"cachedRoutes", ident"routeKey"), ident"create_at"
196+
)
197+
198+
statementList.insert(0, newStmtList(
199+
newVarStmt(ident"routeKey", newCall("fmt", newLit(fmt"{routeKey}"))),
200+
queryStmt,
201+
queryArrStmt,
202+
newConstStmt(ident"thisRouteCanBeCached", newLit(true)),
203+
newNimNode(nnkIfStmt).add(newNimNode(nnkElifBranch).add(
204+
newCall("hasKey", ident"cachedRoutes", ident"routeKey"),
205+
newNimNode(nnkIfStmt).add(newNimNode(nnkElifBranch).add(
206+
newCall("<", newCall("-", newCall("cpuTime"), cachedRoutesCreateAt), expiresIn),
207+
newStmtList(
208+
newConstStmt(ident"thisIsCachedResponse", newLit(true)),
209+
newCall(
210+
"answer",
211+
ident"req",
212+
newNimNode(nnkDotExpr).add(cachedRoutesResult, ident"data"),
213+
newNimNode(nnkDotExpr).add(cachedRoutesResult, ident"statusCode"),
214+
newNimNode(nnkDotExpr).add(cachedRoutesResult, ident"headers"),
215+
),
216+
newNimNode(nnkBreakStmt).add(ident"__handleRequestBlock")
217+
)
218+
)),
219+
)),
220+
))
221+
222+
126223
static:
127224
regDecorator("AuthBasic", authBasicDecoratorImpl)
128225
regDecorator("AuthBearerJWT", authBearerJwtDecoratorImpl)
129226
regDecorator("AuthJWT", authJwtDecoratorImpl)
130227
regDecorator("GetUserAgent", getUserAgentDecoratorImpl)
228+
regDecorator("Cached", cachedDecoratorImpl)

src/happyx/ssr/core.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ else:
4545
import std/posix
4646
import std/osproc
4747

48-
export httpcore
48+
export httpcore, times
4949

5050

5151
func parseHttpMethod*(data: string): Option[HttpMethod] =

src/happyx/ssr/server.nim

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,22 @@ template answer*(
286286
when declared(outHeaders):
287287
for key, val in outHeaders.pairs():
288288
h[key] = val
289+
290+
# Cache result
291+
when declared(thisRouteCanBeCached) and declared(routeKey) and not declared(thisIsCachedResponse):
292+
cachedRoutes[routeKey] = CachedRoute(create_at: cpuTime())
293+
when message is string:
294+
cachedRoutes[routeKey].res = CachedResult(data: message)
295+
else:
296+
cachedRoutes[routeKey].res = CachedResult(data: $message)
297+
cachedRoutes[routeKey].res.statusCode = code
298+
when useHeaders:
299+
cachedRoutes[routeKey].res.headers = h
300+
else:
301+
cachedRoutes[routeKey].res.headers = newHttpHeaders([
302+
("Content-Type", "text/plain;charset=utf-8")
303+
])
304+
289305
# HTTPX
290306
when enableHttpx or enableBuiltin:
291307
when useHeaders:
@@ -395,6 +411,17 @@ template answer*(
395411
when declared(outHeaders):
396412
for key, val in outHeaders.pairs():
397413
h[key] = val
414+
415+
# Cache result
416+
when declared(thisRouteCanBeCached) and declared(routeKey) and not declared(thisIsCachedResponse):
417+
cachedRoutes[routeKey] = CachedRoute(create_at: cpuTime())
418+
when message is string:
419+
cachedRoutes[routeKey].res = CachedResult(data: message)
420+
else:
421+
cachedRoutes[routeKey].res = CachedResult(data: $message)
422+
cachedRoutes[routeKey].res.statusCode = code
423+
cachedRoutes[routeKey].res.headers = h
424+
398425
# HTTPX
399426
when enableHttpx or enableBuiltin:
400427
var headersArr = ""

tests/testc16.nim

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import
2+
std/asyncdispatch,
23
../src/happyx,
34
jwt
45

@@ -102,6 +103,23 @@ serve "127.0.0.1", 5000:
102103
put "/post/$id:int":
103104
## Edits a post
104105
return "Hello, world!"
106+
107+
@Cached # Expires in 60 seconds by default
108+
get "/cached/{i:int}":
109+
await sleepAsync(1000)
110+
if true:
111+
if (query?test) == "hello":
112+
return 100
113+
echo query?one
114+
return i
115+
116+
@Cached(120) # Expires in 60 seconds by default
117+
get "/cached/{x}":
118+
await sleepAsync(1000)
119+
if query.hasKey("key"):
120+
return query["key"]
121+
await sleepAsync(1000)
122+
return x
105123

106124
@AuthBasic
107125
post "/test/basic-auth":

0 commit comments

Comments
 (0)