akane.nim 8.5 KB

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