akane.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. # author: Ethosa
  2. # ----- CORE ----- #
  3. import asyncdispatch
  4. import asynchttpserver
  5. # ----- SUPPORT ----- #
  6. import asyncfile # loadtemplate
  7. import strutils # startsWith, endsWith
  8. import tables
  9. import macros
  10. import times
  11. import json # urlParams
  12. import uri # decodeUrl
  13. import os
  14. import re # regex
  15. # ----- EXPORT -----
  16. export asyncdispatch
  17. export asynchttpserver
  18. export strutils
  19. export json
  20. export uri
  21. export re
  22. type
  23. ServerRef* = ref object
  24. port*: uint16
  25. address*: string
  26. server*: AsyncHttpServer
  27. var AKANE_DEBUG_MODE*: bool = false ## change it with `newServer proc<#newServer,string,uint16,bool>`_
  28. proc newServer*(address: string = "127.0.0.1",
  29. port: uint16 = 5000, debug: bool = false): ServerRef =
  30. ## Creates a new ServerRef object.
  31. ##
  32. ## Arguments:
  33. ## - ``address`` - server address, e.g. "127.0.0.1"
  34. ## - ``port`` - server port, e.g. 5000
  35. ## - ``debug`` - debug mode
  36. if not existsDir("templates"):
  37. createDir("templates")
  38. AKANE_DEBUG_MODE = debug
  39. return ServerRef(
  40. address: address, port: port,
  41. server: newAsyncHttpServer()
  42. )
  43. proc loadtemplate*(name: string, json: JsonNode = %*{}): Future[string] {.async, inline.} =
  44. ## Loads HTML template from `templates` folder.
  45. ##
  46. ## Arguments:
  47. ## - ``name`` - template's name, e.g. "index", "api", etc.
  48. ## - ``json`` - Json data, which replaces in the template.
  49. ##
  50. ## Replaces:
  51. ## - $(key) -> value
  52. ## - if $(key) { ... } -> ... (if value is true)
  53. var
  54. file = openAsync(("templates" / name) & ".html")
  55. readed = await file.readAll()
  56. file.close()
  57. for key, value in json.pairs:
  58. # ---- regex patterns ---- #
  59. let
  60. # variable statment, e.g.: $(variable)
  61. variable_stmt = re("(\\$\\s*\\(" & key & "\\))")
  62. # if statment, e.g.: if $(variable) {......}
  63. if_stmt = re("if\\s*(\\$\\s*\\(" & key & "\\))\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
  64. # if not statment, e.g.: if not $(variable) {......}
  65. if_notstmt = re("if\\s*not\\s*(\\$\\s*\\(" & key & "\\))\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
  66. # ---- converts value to bool ---- #
  67. var value_bool = false
  68. case value.kind:
  69. of JBool:
  70. if value.getBool:
  71. value_bool = true
  72. of JInt:
  73. if value.getInt != 0:
  74. value_bool = true
  75. of JFloat:
  76. if value.getFloat != 0.0:
  77. value_bool = true
  78. of JString:
  79. if value.getStr.len > 0:
  80. value_bool = true
  81. of JArray:
  82. if value.len > 0:
  83. value_bool = true
  84. of JObject:
  85. if value.getFields.len > 0:
  86. value_bool = true
  87. else: discard
  88. # ---- replace ----- #
  89. if readed.contains(if_stmt):
  90. if value_bool:
  91. readed = readed.replacef(if_stmt, "$2")
  92. else:
  93. readed = readed.replacef(if_stmt, "")
  94. if readed.contains(if_notstmt):
  95. if value_bool:
  96. readed = readed.replacef(if_notstmt, "")
  97. else:
  98. readed = readed.replacef(if_notstmt, "$2")
  99. readed = readed.replacef(variable_stmt, $value)
  100. return readed
  101. proc parseQuery*(request: Request): Future[JsonNode] {.async.} =
  102. ## Decodes query.
  103. ## e.g.:
  104. ## "a=5&b=10" -> {"a": "5", "b": "10"}
  105. ##
  106. ## This also have debug output, if AKANE_DEBUG_MODE is true.
  107. var data = request.url.query.split("&")
  108. result = %*{}
  109. for i in data:
  110. let timed = i.split("=")
  111. if timed.len > 1:
  112. result[decodeUrl(timed[0])] = %decodeUrl(timed[1])
  113. if AKANE_DEBUG_MODE:
  114. let
  115. now = times.local(times.getTime())
  116. timed_month = ord(now.month)
  117. month = if timed_month > 9: $timed_month else: "0" & $timed_month
  118. day = if now.monthday > 9: $now.monthday else: "0" & $now.monthday
  119. hour = if now.hour > 9: $now.hour else: "0" & $now.hour
  120. minute = if now.minute > 9: $now.minute else: "0" & $now.minute
  121. second = if now.second > 9: $now.second else: "0" & $now.second
  122. host =
  123. if request.headers.hasKey("host") and request.headers["host"].len > 1:
  124. request.headers["host"] & " "
  125. else:
  126. "new "
  127. echo(
  128. host, request.reqMethod,
  129. " at ", now.year, ".", month, ".", day,
  130. " ", hour, ":", minute, ":", second,
  131. " Request from ", request.hostname,
  132. " to url \"", decodeUrl(request.url.path), "\".")
  133. macro pages*(server: ServerRef, body: untyped): untyped =
  134. ## This macro provides convenient page adding.
  135. ##
  136. ## `body` should be StmtList.
  137. ## page type can be:
  138. ## - ``equals``
  139. ## - ``startswith``
  140. ## - ``endswith``
  141. ## - ``regex``
  142. ## - ``notfound`` - this page uses without URL argument.
  143. ##
  144. ## ..code-block::Nim
  145. ## server.pages:
  146. ## equals("/home"):
  147. ## echo url
  148. ## echo urlParams
  149. var
  150. stmtlist = newStmtList()
  151. notfound_declaration = false
  152. stmtlist.add(
  153. newNimNode(nnkLetSection).add( # let urlParams: JsonNode = await parseQuery(request)
  154. newNimNode(nnkIdentDefs).add(
  155. ident("urlParams"),
  156. ident("JsonNode"),
  157. newCall(
  158. "await",
  159. newCall(
  160. "parseQuery",
  161. ident("request")
  162. )
  163. )
  164. )
  165. ),
  166. newNimNode(nnkLetSection).add( # let decode_url: string = decodeUrl(request.url.path)
  167. newNimNode(nnkIdentDefs).add(
  168. ident("decoded_url"),
  169. ident("string"),
  170. newCall(
  171. "decodeUrl",
  172. newNimNode(nnkDotExpr).add(
  173. newNimNode(nnkDotExpr).add(
  174. ident("request"), ident("url")
  175. ),
  176. ident("path")
  177. )
  178. )
  179. )
  180. )
  181. )
  182. stmtlist.add(newNimNode(nnkIfStmt))
  183. var ifstmtlist = stmtlist[2]
  184. for i in body: # for each page in statment list.
  185. let
  186. current = $i[0]
  187. path = if i.len == 3: i[1] else: newEmptyNode()
  188. slist = if i.len == 3: i[2] else: i[1]
  189. if (i.kind == nnkCall and i[0].kind == nnkIdent and
  190. (path.kind == nnkStrLit or path.kind == nnkCallStrLit or path.kind == nnkEmpty) and
  191. slist.kind == nnkStmtList):
  192. if current == "equals":
  193. slist.insert(0, # let url: string = `path`
  194. newNimNode(nnkLetSection).add(
  195. newNimNode(nnkIdentDefs).add(
  196. ident("url"),
  197. ident("string"),
  198. path
  199. )
  200. )
  201. )
  202. ifstmtlist.add( # request.path.url == i[1]
  203. newNimNode(nnkElifBranch).add(
  204. newCall("==", path, ident("decoded_url")),
  205. slist))
  206. elif current == "startswith":
  207. slist.insert(0, # let url = decoded_url[`path`.len..^1]
  208. newNimNode(nnkLetSection).add(
  209. newNimNode(nnkIdentDefs).add(
  210. ident("url"),
  211. ident("string"),
  212. newCall(
  213. "[]",
  214. ident("decoded_url"),
  215. newCall(
  216. "..^",
  217. newCall("len", path),
  218. newLit(1))
  219. )
  220. )
  221. )
  222. )
  223. ifstmtlist.add( # decode_url.startsWith(`path`)
  224. newNimNode(nnkElifBranch).add(
  225. newCall(
  226. "startsWith",
  227. ident("decoded_url"),
  228. path),
  229. slist))
  230. elif current == "endswith":
  231. slist.insert(0, # let url: string = decoded_url[0..^`path`.len]
  232. newNimNode(nnkLetSection).add(
  233. newNimNode(nnkIdentDefs).add(
  234. ident("url"),
  235. ident("string"),
  236. newCall(
  237. "[]",
  238. ident("decoded_url"),
  239. newCall(
  240. "..^",
  241. newLit(0),
  242. newCall("+", newLit(1), newCall("len", path))
  243. )
  244. )
  245. )
  246. )
  247. )
  248. ifstmtlist.add( # decode_url.endsWith(`path`)
  249. newNimNode(nnkElifBranch).add(
  250. newCall(
  251. "endsWith",
  252. ident("decoded_url"),
  253. path),
  254. slist))
  255. elif current == "regex":
  256. slist.insert(0, # discard match(decoded_url, `path`, url)
  257. newNimNode(nnkDiscardStmt).add(
  258. newCall("match", ident("decoded_url"), path, ident("url"))
  259. )
  260. )
  261. slist.insert(0, # var url: array[20, string]
  262. newNimNode(nnkVarSection).add(
  263. newNimNode(nnkIdentDefs).add(
  264. ident("url"),
  265. newNimNode(nnkBracketExpr).add(
  266. ident("array"), newLit(20), ident("string")
  267. ),
  268. newEmptyNode()
  269. )
  270. ))
  271. ifstmtlist.add( # decode_url.match(`path`)
  272. newNimNode(nnkElifBranch).add(
  273. newCall("match", ident("decoded_url"), path),
  274. slist))
  275. elif current == "notfound":
  276. notfound_declaration = true
  277. ifstmtlist.add(newNimNode(nnkElse).add(slist))
  278. if not notfound_declaration:
  279. ifstmtlist.add(
  280. newNimNode(nnkElse).add(
  281. newCall( # await request.respond(Http404, "Not found")
  282. "await",
  283. newCall(
  284. "respond",
  285. ident("request"),
  286. ident("Http404"),
  287. newLit("Not found"))
  288. )
  289. )
  290. )
  291. result = newNimNode(nnkProcDef).add(
  292. ident("receivepages"), # procedure name.
  293. newEmptyNode(), # for template and macros
  294. newEmptyNode(), # generics
  295. newNimNode(nnkFormalParams).add( # proc params
  296. newEmptyNode(), # return type
  297. newNimNode(nnkIdentDefs).add( # param
  298. ident("request"), # param name
  299. ident("Request"), # param type
  300. newEmptyNode() # param default value
  301. )
  302. ),
  303. newNimNode(nnkPragma).add( # pragma declaration
  304. ident("async"),
  305. ident("gcsafe")
  306. ),
  307. newEmptyNode(),
  308. stmtlist)
  309. macro answer*(request, message: untyped, http_code = Http200): untyped =
  310. ## Responds from server with utf-8.
  311. ##
  312. ## Translates to:
  313. ## await request.respond(Http200, "<head><meta charset='utf-8'></head>" & message)
  314. result = newCall(
  315. "respond",
  316. request,
  317. http_code,
  318. newCall("&", newLit("<head><meta charset='utf-8'></head>"), message)
  319. )
  320. macro error*(request, message: untyped, http_code = Http404): untyped =
  321. ## Responds from server with utf-8.
  322. ##
  323. ## Translates to:
  324. ## await request.respond(Http404, "<head><meta charset='utf-8'></head>" & message)
  325. result = newCall(
  326. "respond",
  327. request,
  328. http_code,
  329. newCall("&", newLit("<head><meta charset='utf-8'></head>"), message)
  330. )
  331. macro start*(server: ServerRef): untyped =
  332. ## Starts server.
  333. result = quote do:
  334. if AKANE_DEBUG_MODE:
  335. echo "Server starts on http://", `server`.address, ":", `server`.port
  336. waitFor `server`.server.serve(Port(`server`.port), receivepages)