akane.nim 9.1 KB

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