├── example
├── nim.cfg
├── conf
│ ├── Makefile
│ └── mongrel2.conf
├── model.nim
├── helloworld.nim
├── fortunes_tmpl.nim
├── model_redis.nim
├── prepare_redis_database.sh
├── model_postgre.nim
├── hellocookies.nim
├── prepare_postgresql_database.sql
└── app.nim
├── .gitignore
├── nawak.babel
├── private
├── optim_strange_loop.nim
├── jesterutils.nim
├── nawak_common.nim
├── tuple_index_setter.nim
├── netstrings.nim
├── jesterpatterns.nim
└── nawak_mg.nim
├── LICENSE
├── jdump.nim
├── README.md
└── nawak_mongrel.nim
/example/nim.cfg:
--------------------------------------------------------------------------------
1 | path=".."
2 | path="."
3 | nimcache=".nimcache"
4 | hints=off
5 | verbosity="0"
6 | threads=on
7 | #cc = clang
8 |
--------------------------------------------------------------------------------
/example/conf/Makefile:
--------------------------------------------------------------------------------
1 |
2 | all: run
3 |
4 | run:
5 | mkdir -p run logs tmp
6 | m2sh load
7 | sudo m2sh start -every
8 |
9 | .PHONY: run
10 |
--------------------------------------------------------------------------------
/example/model.nim:
--------------------------------------------------------------------------------
1 |
2 | type THello* = tuple[message: string]
3 | type TWorld* = tuple[id: int, randomNumber: int]
4 | type TFortune* = tuple[id: int, message: string]
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | nimcache
2 | .nimcache
3 | example/conf/run
4 | example/conf/logs
5 | example/conf/tmp
6 | example/conf/config.sqlite
7 | example/helloworld
8 | example/app_postgresql
9 | example/app_redis
10 |
--------------------------------------------------------------------------------
/example/helloworld.nim:
--------------------------------------------------------------------------------
1 | import nawak_mongrel, strutils
2 |
3 | get "/":
4 | return response("Hello World!")
5 |
6 | get "/user/@username/?":
7 | return response("Hello $1!" % url_params.username)
8 |
9 | run()
10 |
--------------------------------------------------------------------------------
/nawak.babel:
--------------------------------------------------------------------------------
1 | [Package]
2 | name = "nawak"
3 | version = "0.3.0"
4 | author = "Erwan Ameil"
5 | description = "A web micro-framework in Nimrod, heavily inspired by jester, flask and the like."
6 | license = "MIT"
7 |
8 | [Deps]
9 | Requires: "nimrod >= 0.9.6"
10 | Requires: "uuid >= 0.1.0"
11 | Requires: "zmq >= 0.2.1"
12 |
--------------------------------------------------------------------------------
/example/fortunes_tmpl.nim:
--------------------------------------------------------------------------------
1 | #! stdtmpl | standard
2 | #from xmltree import escape
3 | #import model
4 | #proc fortunes_tmpl*(fortunes: openArray[TFortune]): string =
5 | # result = ""
6 |
7 |
8 |
Fortunes
9 |
10 |
11 | | id | message |
12 | #for fortune in items(fortunes):
13 | | ${fortune.id} | ${escape(fortune.message)} |
14 | #end for
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/example/model_redis.nim:
--------------------------------------------------------------------------------
1 | import strutils, redis
2 | import model
3 |
4 | var db {.threadvar.}: TRedis
5 |
6 | proc init_db*() {.procvar.} =
7 | db = open(host="localhost")
8 |
9 | proc getWorld*(id: int): TWorld =
10 | let s = redis.get(db, "world:" & $id)
11 | if s == redisNil:
12 | raise newException(ERedis, "World Not Found")
13 | return (id, s.parseInt)
14 |
15 | proc updateWorld*(w: TWorld) =
16 | db.setk("world:" & $w.id, $w.randomNumber)
17 |
18 | proc getAllFortunes*(): seq[TFortune] =
19 | result = @[]
20 | var i = 1
21 | for s in db.lrange("fortunes", 0, -1):
22 | result.add((id: i, message: s))
23 | inc(i)
24 |
--------------------------------------------------------------------------------
/example/prepare_redis_database.sh:
--------------------------------------------------------------------------------
1 | RANGE=10000
2 | for i in {1..10000}
3 | do
4 | value=$RANDOM
5 | let "value = (value % $RANGE) + 1"
6 | echo "SET world:$i $value" | redis-cli > /dev/null
7 | done
8 |
9 | echo "DEL fortunes" | redis-cli
10 | echo "RPUSH fortunes 'fortune: No such file or directory' \
11 | \"A computer scientist is someone who fixes things that aren''t broken.\" \
12 | 'After enough decimal places, nobody gives a damn.' \
13 | 'A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1' \
14 | 'A computer program does what you tell it to do, not what you want it to do.' \
15 | 'Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen' \
16 | 'Any program that runs right is obsolete.' \
17 | 'A list is only as strong as its weakest link. — Donald Knuth' \
18 | 'Feature: A bug with seniority.' \
19 | 'Computers make very fast, very accurate mistakes.' \
20 | '' \
21 | 'フレームワークのベンチマーク'" | redis-cli
22 |
--------------------------------------------------------------------------------
/private/optim_strange_loop.nim:
--------------------------------------------------------------------------------
1 | # From Araq's talk:
2 | # http://nimrod-lang.org/talk01/slides.html
3 | # http://nimrod-lang.org/talk01/corrections.html
4 |
5 | import strutils
6 | import macros
7 |
8 | proc invalidFormatString() {.noinline.} =
9 | raise newException(ValueError, "invalid format string")
10 |
11 | template optAdd1*{x = y; add(x, z)}(x, y, z: string) =
12 | x = y & z
13 |
14 | template optAdd2*{add(x, y); add(x, z)}(x, y, z: string) =
15 | add(x, y & z)
16 |
17 | macro optFormat*{`%`(f, a)}(f: string{lit}, a: openArray[string]): expr =
18 | result = newNimNode(nnkBracket)
19 | let f = f.strVal
20 | var i = 0
21 | while i < f.len:
22 | if f[i] == '$':
23 | case f[i+1]
24 | of '1'..'9':
25 | var j = 0
26 | i += 1
27 | while f[i] in {'0'..'9'}:
28 | j = j * 10 + ord(f[i]) - ord('0'); i += 1
29 | result.add(a[j-1])
30 | else:
31 | invalidFormatString()
32 | else:
33 | result.add(newLit(f[i])); i += 1
34 |
35 | result = nestList(!"&", result)
36 | #echo toStrLit(result)
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Erwan Ameil
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/example/conf/mongrel2.conf:
--------------------------------------------------------------------------------
1 | # this is the minimal config required for mongrel2
2 | micro_framework = Handler(
3 | send_spec='tcp://127.0.0.1:9999',
4 | #send_spec='ipc:///tmp/requests.mongrel.ipc',
5 | send_ident='0138a43-micro-nimrod',
6 | recv_spec='tcp://127.0.0.1:9998',
7 | #recv_spec='ipc:///tmp/responses.mongrel.ipc',
8 | #protocol='tnetstring', # default to json
9 | recv_ident='')
10 |
11 | main = Server(
12 | uuid="bd81667c-505a-41da-952c-1e672338db8a",
13 | access_log="/logs/access.log",
14 | error_log="/logs/error.log",
15 | chroot="./",
16 | default_host="localhost",
17 | name="test",
18 | pid_file="/run/mongrel2.pid",
19 | port=6767,
20 | hosts = [
21 | Host(name="localhost", routes={
22 | '/': micro_framework
23 | })
24 | ]
25 | )
26 |
27 | settings = {"zeromq.threads": 1,
28 | "disable.access_logging": 1,
29 | "limits.min_ping": 0,
30 | "limits.min_write_rate": 0,
31 | "limits.min_read_rate": 0,
32 | "limits.kill_limit": 2}
33 |
34 | servers = [main]
35 |
--------------------------------------------------------------------------------
/private/jesterutils.nim:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012 Dominik Picheta
2 | # with modifications by Erwan Ameil
3 | # MIT License
4 | import parseutils, strtabs
5 | from cgi import decodeUrl
6 |
7 | proc parseUrlQuery*(query: string, query_params: var StringTableRef) =
8 | query_params = {:}.newStringTable
9 | try:
10 | var i = 0
11 | let j = query.find('?')
12 | if j > 0:
13 | i = j + 1
14 | while i < query.len()-1:
15 | var key = ""
16 | var val = ""
17 | i += query.parseUntil(key, '=', i)
18 | if query[i] != '=':
19 | raise newException(ValueError, "Expected '=' at " & $i)
20 | inc(i) # Skip =
21 | i += query.parseUntil(val, '&', i)
22 | inc(i) # Skip &
23 | query_params[decodeUrl(key)] = decodeUrl(val)
24 | except ValueError: discard
25 |
26 |
27 | when isMainModule:
28 | var r = {:}.newStringTable
29 | parseUrlQuery("FirstName=Mickey", r)
30 | echo r
31 | r = {:}.newStringTable
32 | parseUrlQuery("asdf?FirstName=Mickey", r)
33 | echo r
34 | r = {:}.newStringTable
35 | parseUrlQuery("/my/path?FirstName=Mickey", r)
36 | echo r
37 | r = {:}.newStringTable
38 | parseUrlQuery("/my/path?FirstName=", r)
39 | echo r
40 | r = {:}.newStringTable
41 | parseUrlQuery("/my/path?=", r)
42 | echo r
43 |
--------------------------------------------------------------------------------
/example/model_postgre.nim:
--------------------------------------------------------------------------------
1 | import strutils, db_postgres
2 |
3 | import model
4 |
5 | const qworld = sql"SELECT id, randomNumber FROM World WHERE id = $1"
6 | const qfortunes = sql"SELECT id, message FROM Fortune"
7 | const qupdates = sql"UPDATE World SET randomNumber = $1 WHERE id = $2"
8 |
9 | var db {.threadvar.}: TDbConn
10 | var qworld_prepared {.threadvar.}: TSqlPrepared
11 | var qfortunes_prepared {.threadvar.}: TSqlPrepared
12 | var qupdates_prepared {.threadvar.}: TSqlPrepared
13 |
14 | proc init_db*() {.procvar.} =
15 | db = open("", "benchmarkdbuser", "benchmarkdbpass",
16 | "host=localhost port=5432 dbname=hello_world")
17 | # prepare queries
18 | qworld_prepared = db.prepare("world", qworld, 1)
19 | qfortunes_prepared = db.prepare("fortunes", qfortunes, 0)
20 | qupdates_prepared = db.prepare("updates", qupdates, 2)
21 |
22 |
23 | proc getWorld*(n: int): TWorld =
24 | #let row = db.getRow(qworld, n)
25 | ## Yes, prepared queries are faster than unprepared ones
26 | let row = db.getRow(qworld_prepared, n)
27 | result.id = parseInt(row[0])
28 | result.randomNumber = parseInt(row[1])
29 |
30 | proc updateWorld*(w: TWorld) =
31 | db.exec(qupdates_prepared, $w.randomNumber, $w.id)
32 |
33 | proc getAllFortunes*(): seq[TFortune] =
34 | let rows = db.getAllRows(qfortunes_prepared)
35 | result.newSeq(rows.len)
36 | for j, row in rows.pairs:
37 | result[j] = (row[0].parseInt, row[1])
38 |
--------------------------------------------------------------------------------
/private/nawak_common.nim:
--------------------------------------------------------------------------------
1 | import strtabs, times
2 | import cookies
3 |
4 | proc addCookie*(headers: var StringTableRef, key, value: string;
5 | expires: TimeInfo, domain = "", path = "",
6 | secure = false, httpOnly = false) =
7 | if headers.hasKey("Set-Cookie"):
8 | headers.mget("Set-Cookie").add "\c\L" & setCookie(key, value,
9 | expires, domain, path, noName=false, secure, httpOnly)
10 | else:
11 | headers["Set-Cookie"] = setCookie(key, value, expires, domain, path,
12 | noName=true, secure, httpOnly)
13 |
14 | proc addCookie*(headers: var StringTableRef; key, value: string; domain="",
15 | path="", secure=false, httpOnly=false) =
16 | if headers.hasKey("Set-Cookie"):
17 | headers.mget("Set-Cookie").add "\c\L" &
18 | setCookie(key, value, domain=domain, path=path, noName=false,
19 | secure=secure, httpOnly=httpOnly)
20 | else:
21 | headers["Set-Cookie"] = setCookie(key, value, domain=domain,
22 | path=path, noName=true, secure=secure, httpOnly=httpOnly)
23 |
24 | proc deleteCookie*(headers: var StringTableRef, key: string, domain="", path="",
25 | secure=false, httpOnly=false) =
26 | var tim = Time(0).getGMTime()
27 | addCookie(headers, key, "deleted", expires=tim, domain, path, secure, httpOnly)
28 |
29 | proc daysFromNow*(days: int): TimeInfo =
30 | return Time(int(getTime()) + days * (60*60*24)).getGMTime()
31 |
--------------------------------------------------------------------------------
/jdump.nim:
--------------------------------------------------------------------------------
1 | import strutils, unicode
2 | import private/optim_strange_loop
3 |
4 | # from the standard lib json
5 | proc escapeJson(s: string): string =
6 | ## Converts a string `s` to its JSON representation.
7 | result = newStringOfCap(s.len + s.len shr 3)
8 | result.add("\"")
9 | for x in runes(s):
10 | var r = int(x)
11 | if r >= 32 and r <= 127:
12 | var c = chr(r)
13 | case c
14 | of '"': result.add("\\\"")
15 | of '\\': result.add("\\\\")
16 | else: result.add(c)
17 | else:
18 | result.add("\\u")
19 | result.add(toHex(r, 4))
20 | result.add("\"")
21 |
22 |
23 | proc jdump*(x: string): string = escapeJson(x)
24 | proc jdump*(x: int): string = $x
25 | proc jdump*(x: float): string = $x
26 | #proc jdump*[T](x: seq[T] | openarray[T]): string =
27 | proc jdump*[T](x: seq[T]): string =
28 | result = "["
29 | for i in 0.. x.len - 2:
30 | result.add jdump(x[i])
31 | result.add ","
32 | result.add jdump(x[x.len - 1])
33 | result.add "]"
34 |
35 | proc jdump*[T: tuple](x: T): string =
36 | result = "{"
37 | for name, value in fieldPairs(x):
38 | #result.add("\"$1\":$2," % [name, jdump(value)])
39 | result.add(optFormat("\"$1\":$2,", [name, jdump(value)]))
40 | result[result.len - 1] = '}'
41 |
42 | when isMainModule:
43 | type TTest = tuple[id: string, randomNumber: string]
44 | let t, t2: TTest = ("sdfg328asd", "10472")
45 | echo jdump(t)
46 | echo jdump(@[1, 2, 3])
47 | let t_seq = @[t, t2]
48 | echo jdump(t_seq)
49 |
--------------------------------------------------------------------------------
/example/hellocookies.nim:
--------------------------------------------------------------------------------
1 | import strtabs, times
2 | import nawak_mongrel, cookies
3 |
4 |
5 | get "/mmm_cookies":
6 | var headers = {:}.newStringTable
7 | headers.addCookie("a_cookie", "simple session cookie")
8 | headers.addCookie("another_cookie", "Persistent cookie that will stay a week",
9 | 7.daysFromNow)
10 | headers.addCookie("other_cookie",
11 | "Only sent back on https and unaccessible from javascript",
12 | secure=true, httpOnly=true)
13 |
14 | echo headers
15 | return response("Hi! I gave you cookies. Go back.", headers)
16 |
17 | get "/":
18 | var msg = ""
19 | if request.cookies.hasKey("a_cookie"):
20 | # Here is how you get the value of a cookie
21 | msg = "Ha! I see you already got something for me (like a " &
22 | request.cookies["a_cookie"] & ").
"
23 |
24 | return response(msg & "Here are the cookies you sent me:
" & $request.cookies &
25 | """
Join the Dark Side, we have cookies.
26 |
To destroy the cookies, go here""")
27 |
28 | get "/remove_cookies":
29 | var headers = {:}.newStringTable
30 | for key in request.cookies.keys:
31 | headers.deleteCookie(key)
32 |
33 | # If you're not on https, the loop won't have deleted the following cookie
34 | headers.deleteCookie("other_cookie")
35 |
36 | return response("I have eaten all your cookies. Go back.",
37 | headers)
38 |
39 | run()
40 |
--------------------------------------------------------------------------------
/private/tuple_index_setter.nim:
--------------------------------------------------------------------------------
1 | import macros
2 | import jesterpatterns
3 |
4 | macro inject_tuple_setter_by_index*(index_var, tuple_to_set, new_val: expr,
5 | path: string): stmt {.immediate, closure.} =
6 | # count the number of fields in the url_params tuple
7 | var fields_len = 0
8 | var path_str = $toStrLit(path)
9 | path_str = path_str[1 .. path_str.len - 2]
10 | var pattern = parsePattern(path_str)
11 | for node in pattern:
12 | case node.typ
13 | of TNodeField:
14 | inc(fields_len)
15 | else:
16 | discard
17 |
18 | if fields_len == 0:
19 | result = newEmptyNode()
20 | return
21 |
22 | result = newNimNode(nnkCaseStmt)
23 | result.add(newIdentNode($toStrLit(index_var)))
24 |
25 | for i in 0..fields_len-1:
26 | if i == fields_len-1: # the last position will be the default case
27 | break
28 | var node = newNimNode(nnkOfBranch)
29 | node.add newIntLitNode(i)
30 |
31 | var bracket_expr = newNimNode(nnkBracketExpr)
32 | bracket_expr.add newIdentNode($toStrLit(tuple_to_set))
33 | bracket_expr.add newIntLitNode(i)
34 |
35 | node.add(newStmtList(
36 | newAssignment( bracket_expr, newIdentNode($toStrLit(new_val)) )
37 | ))
38 |
39 | result.add node
40 |
41 | # default case (the else)
42 | result.add(newNimNode(nnkElse).add(
43 | newAssignment(
44 | newNimNode(nnkBracketExpr).add(
45 | newIdentNode($toStrLit(tuple_to_set))
46 | ).add(
47 | newIntLitNode(fields_len-1)
48 | ),
49 | newIdentNode($toStrLit(new_val))
50 | )
51 | ))
52 |
--------------------------------------------------------------------------------
/example/prepare_postgresql_database.sql:
--------------------------------------------------------------------------------
1 | CREATE USER benchmarkdbuser WITH PASSWORD 'benchmarkdbpass';
2 |
3 | DROP DATABASE IF EXISTS hello_world;
4 | CREATE DATABASE hello_world WITH ENCODING 'UTF8';
5 |
6 | GRANT ALL PRIVILEGES ON DATABASE hello_world to benchmarkdbuser;
7 |
8 |
9 | DROP TABLE IF EXISTS World;
10 | CREATE TABLE World (
11 | id integer NOT NULL,
12 | randomNumber integer NOT NULL default 0,
13 | PRIMARY KEY (id)
14 | );
15 |
16 | INSERT INTO World (id, randomNumber)
17 | SELECT x.id, random() * 10000 + 1 FROM generate_series(1,10000) as x(id);
18 |
19 | DROP TABLE IF EXISTS Fortune;
20 | CREATE TABLE Fortune (
21 | id integer NOT NULL,
22 | message varchar(2048) NOT NULL,
23 | PRIMARY KEY (id)
24 | );
25 |
26 | INSERT INTO Fortune (id, message) VALUES (1, 'fortune: No such file or directory');
27 | INSERT INTO Fortune (id, message) VALUES (2, 'A computer scientist is someone who fixes things that aren''t broken.');
28 | INSERT INTO Fortune (id, message) VALUES (3, 'After enough decimal places, nobody gives a damn.');
29 | INSERT INTO Fortune (id, message) VALUES (4, 'A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1');
30 | INSERT INTO Fortune (id, message) VALUES (5, 'A computer program does what you tell it to do, not what you want it to do.');
31 | INSERT INTO Fortune (id, message) VALUES (6, 'Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen');
32 | INSERT INTO Fortune (id, message) VALUES (7, 'Any program that runs right is obsolete.');
33 | INSERT INTO Fortune (id, message) VALUES (8, 'A list is only as strong as its weakest link. — Donald Knuth');
34 | INSERT INTO Fortune (id, message) VALUES (9, 'Feature: A bug with seniority.');
35 | INSERT INTO Fortune (id, message) VALUES (10, 'Computers make very fast, very accurate mistakes.');
36 | INSERT INTO Fortune (id, message) VALUES (11, '');
37 | INSERT INTO Fortune (id, message) VALUES (12, 'フレームワークのベンチマーク');
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nawak
2 |
3 | A web micro-framework in Nimrod, heavily inspired by jester, flask and the like.
4 | It is only compatible with the `Mongrel2` server for now.
5 |
6 | ## Minimal example
7 | ```nimrod
8 | # helloworld.nim
9 | import nawak_mongrel, strutils
10 |
11 | get "/":
12 | return response("Hello World!")
13 |
14 | get "/user/@username/":
15 | return response("Hello $1!" % url_params.username)
16 |
17 | run()
18 | ```
19 |
20 | ## Installation
21 | Install `Mongrel2` v1.8.1 (`download page `_) by following the instructions in the [manual](http://mongrel2.org/manual/book-finalch3.html). The bindings for `ZeroMQ` may only work with `ZeroMQ` version 4.
22 |
23 | Start `Mongrel2` either with the provided Makefile in the `example/conf/` folder, or manually:
24 |
25 | $ cd example/conf
26 | $ mkdir -p run logs tmp
27 | $ m2sh load
28 | $ sudo m2sh start -every
29 |
30 | Please check that you use a recent compiler version of `Nimrod`/`Nim`. *nawak* only works with a fresh Nimrod compiler.
31 |
32 | You can now compile and execute the examples from the `example` folder:
33 |
34 | $ cd example
35 | $ nimrod c -d:release helloworld.nim
36 | $ ./helloworld
37 | $ firefox http://localhost:6767/
38 |
39 | The [nawak_app.nim](https://github.com/idlewan/nawak/blob/master/example/nawak_app.nim) example answers the requirements of the [web framework benchmarks](http://www.techempower.com/benchmarks/). You will want to install PostgreSQL and create the [database and tables](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/config) to test it out.
40 |
41 | ## Performance test
42 | If you want to make changes and see how it performs, you can use `wrk` to have a preview of the performance.
43 | The command-line options you will want to use are (from the web framework benchmarks):
44 |
45 | $ wrk -H 'Host: localhost' -H 'Accept: application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7' -H 'Connection: keep-alive' -d 15 -c 256 -t 1 http://localhost:6767/json
46 |
--------------------------------------------------------------------------------
/private/netstrings.nim:
--------------------------------------------------------------------------------
1 | import strutils, json
2 |
3 | type
4 | TMongrelMsg* = tuple[uuid: string, id: string, path: string,
5 | headers: JsonNode, body: string]
6 |
7 | proc parse_netstring(ns: string, split_idx: var int): string =
8 | var len_s: string
9 | for s in ns.split(':'):
10 | len_s = s
11 | break
12 | let header_len = parseInt(len_s)
13 | split_idx = len_s.len + 1 + header_len
14 |
15 | assert(ns[split_idx] == ',')
16 |
17 | return ns[len_s.len + 1 .. split_idx - 1]
18 |
19 | proc parse*(msg: string): TMongrelMsg =
20 | #let msg_splitted = msg.split(' ')
21 | #result.uuid = msg_splitted[0]
22 | #result.id = msg_splitted[1]
23 | #result.path = msg_splitted[2]
24 | var i = 0
25 | for str in msg.split(' '):
26 | case i
27 | of 0: result.uuid = str
28 | of 1: result.id = str
29 | of 2: result.path = str
30 | else: break
31 | inc(i)
32 |
33 | var split_idx = 0
34 | ## rest != msg_splitted[3] because the rest can contain spaces
35 | let rest = msg[result.uuid.len + result.id.len + result.path.len + 3 .. msg.len-1]
36 | let head = parse_netstring(rest, split_idx)
37 | result.headers = parseJson(head)
38 |
39 | let body_ns = rest[split_idx + 1 .. rest.len]
40 | result.body = parse_netstring(body_ns, split_idx)
41 |
42 | #proc `$`(s: seq[string]): string =
43 | # result = "["
44 | # for item in s.items:
45 | # result.add($item & ", ")
46 | # result.add "]"
47 |
48 | when isMainModule:
49 | let msg = "18510ea21k 123 /index.html 20:{\"Content-Length\":5},5:Hello,"
50 | var msg_parsed = parse(msg)
51 | echo msg_parsed
52 | echo ""
53 | echo ""
54 |
55 | let msg2 = "0138a43-micro-nimrod 3 / 426:{\"PATH\":\"/\",\"x-forwarded-for\":\"127.0.0.1\",\"accept-language\":\"en-US,en;q=0.5\",\"connection\":\"keep-alive\",\"accept-encoding\":\"gzip, deflate\",\"accept\":\"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\"user-agent\":\"Mozilla/5.0 (X11; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0\",\"host\":\"localhost:6767\",\"METHOD\":\"GET\",\"VERSION\":\"HTTP/1.1\",\"URI\":\"/\",\"PATTERN\":\"/\",\"URL_SCHEME\":\"http\",\"REMOTE_ADDR\":\"127.0.0.1\"},0:,"
56 | var msg2_parsed = parse(msg2)
57 | echo msg2_parsed
58 |
--------------------------------------------------------------------------------
/example/app.nim:
--------------------------------------------------------------------------------
1 | import strtabs, strutils, math, algorithm
2 | import nawak_mongrel, jdump
3 | import model, fortunes_tmpl
4 | when not defined(postgre_model) xor defined(redis_model):
5 | {.error: "please pass either -d:postgre_model or -d:redis_model to the compiler".}
6 | when defined(postgre_model):
7 | import model_postgre
8 | when defined(redis_model):
9 | import model_redis
10 |
11 | get "/json":
12 | var j: THello
13 | j.message = "Hello, World!"
14 | # jdump serialize the tuples of the model as json
15 | return response(jdump(j), "application/json")
16 |
17 | get "/plaintext":
18 | return response("Hello, World!", "text/plain")
19 |
20 | get "/db":
21 | let w = getWorld(random(10_000)+1)
22 | return response(jdump(w), "application/json")
23 |
24 | get "/fortunes":
25 | var fortunes = getAllFortunes()
26 | let new_fortune: TFortune =
27 | (id: 0,
28 | message: "Additional fortune added at request time.")
29 | fortunes.add new_fortune
30 | sort(fortunes, proc(x, y: TFortune): int =
31 | return cmp(x.message, y.message))
32 |
33 | return response(fortunes_tmpl(fortunes), "text/html; charset=utf-8")
34 |
35 |
36 | proc limit_queries(query_params: PStringTable): int =
37 | result = 1
38 | if query_params.hasKey("queries"):
39 | try:
40 | result = parseInt(query_params["queries"])
41 | except EInvalidValue: discard
42 | # clamp the number of queries
43 | if result < 1: result = 1
44 | elif result > 500: result = 500
45 |
46 | get "/queries":
47 | let queries = limit_queries(request.query)
48 |
49 | var world: seq[TWorld]
50 | world.newSeq(queries)
51 | for i in 0..
70 | Here's a 404 Page Not Found error for you.""")
71 |
72 | run(init=init_db, nb_threads=256)
73 |
--------------------------------------------------------------------------------
/private/jesterpatterns.nim:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012 Dominik Picheta
2 | # with modifications by Erwan Ameil
3 | # MIT License
4 | import parseutils, strtabs
5 | type
6 | TNodeType* = enum
7 | TNodeText, TNodeField
8 | TNode* = object
9 | typ*: TNodeType
10 | text*: string
11 | optional*: bool
12 |
13 | TPattern* = seq[TNode]
14 |
15 | # for debug purposes:
16 | proc `$`*(n: TNode): string =
17 | result = "{" & $n.typ & ", " & n.text & ", " & $n.optional & "}"
18 | proc `$`*(p: TPattern): string =
19 | result = "["
20 | for item in p.items:
21 | result.add($item & ", ")
22 | result.add "]"
23 |
24 |
25 | #/show/@id/?
26 | proc parsePattern*(pattern: string): TPattern =
27 | result = @[]
28 | template addNode(result: var TPattern, theT: TNodeType, theText: string,
29 | isOptional: bool): stmt =
30 | block:
31 | var newNode: TNode
32 | newNode.typ = theT
33 | newNode.text = theText
34 | newNode.optional = isOptional
35 | result.add(newNode)
36 |
37 | var i = 0
38 | var text = ""
39 | while i < pattern.len():
40 | case pattern[i]
41 | of '@':
42 | # Add the stored text.
43 | if text != "":
44 | result.addNode(TNodeText, text, false)
45 | text = ""
46 | # Parse named parameter.
47 | inc(i) # Skip @
48 | var nparam = ""
49 | i += pattern.parseUntil(nparam, {'/', '?'}, i)
50 | var optional = pattern[i] == '?'
51 | result.addNode(TNodeField, nparam, optional)
52 | if pattern[i] == '?': inc(i) # Only skip ?. / should not be skipped.
53 | of '?':
54 | var optionalChar = text[text.len-1]
55 | setLen(text, text.len-1) # Truncate ``text``.
56 | # Add the stored text.
57 | if text != "":
58 | result.addNode(TNodeText, text, false)
59 | text = ""
60 | # Add optional char.
61 | inc(i) # Skip ?
62 | result.addNode(TNodeText, $optionalChar, true)
63 | of '\\':
64 | inc i # Skip \
65 | if pattern[i] notin {'?', '@', '\\'}:
66 | raise newException(ValueError,
67 | "This character does not require escaping: " & pattern[i])
68 | text.add(pattern[i])
69 | inc i # Skip ``pattern[i]``
70 |
71 |
72 |
73 | else:
74 | text.add(pattern[i])
75 | inc(i)
76 |
77 | if text != "":
78 | result.addNode(TNodeText, text, false)
79 |
80 | proc findNextText*(pattern: TPattern, i: int, toNode: var TNode): bool =
81 | ## Finds the next TNodeText in the pattern, starts looking from ``i``.
82 | result = false
83 | for n in i..pattern.len()-1:
84 | if pattern[n].typ == TNodeText:
85 | toNode = pattern[n]
86 | return true
87 |
88 | proc check*(n: TNode, s: string, i: int): bool =
89 | let cutTo = (n.text.len-1)+i
90 | if cutTo > s.len-1: return false
91 | return s.substr(i, cutTo) == n.text
92 |
93 | proc match(pattern: TPattern, s: string): tuple[matched: bool, params: StringTableRef] =
94 | var i = 0 # Location in ``s``.
95 |
96 | result.matched = true
97 | result.params = {:}.newStringTable()
98 |
99 | for ncount, node in pattern:
100 | case node.typ
101 | of TNodeText:
102 | if node.optional:
103 | if check(node, s, i):
104 | inc(i, node.text.len) # Skip over this optional character.
105 | else:
106 | # If it's not there, we have nothing to do. It's optional after all.
107 | discard
108 | else:
109 | if check(node, s, i):
110 | inc(i, node.text.len) # Skip over this
111 | else:
112 | # No match.
113 | result.matched = false
114 | return
115 | of TNodeField:
116 | var nextTxtNode: TNode
117 | var stopChar = '/'
118 | if findNextText(pattern, ncount, nextTxtNode):
119 | stopChar = nextTxtNode.text[0]
120 | var matchNamed = ""
121 | i += s.parseUntil(matchNamed, stopChar, i)
122 | if matchNamed != "":
123 | result.params[node.text] = matchNamed
124 | elif matchNamed == "" and not node.optional:
125 | result.matched = false
126 | return
127 |
128 | if s.len != i:
129 | result.matched = false
130 |
131 | when isMainModule:
132 | let f = parsePattern("/show/@id/test/@show?/?")
133 | doAssert match(f, "/show/12/test/hallo/").matched
134 | doAssert match(f, "/show/2131726/test/jjjuuwąąss").matched
135 | doAssert(not match(f, "/").matched)
136 | doAssert(not match(f, "/show//test//").matched)
137 | doAssert(match(f, "/show/asd/test//").matched)
138 | doAssert(not match(f, "/show/asd/asd/test/jjj/").matched)
139 | doAssert(match(f, "/show/@łę¶ŧ←/test/asd/").params["id"] == "@łę¶ŧ←")
140 |
141 | let f2 = parsePattern("/test42/somefile.?@ext?/?")
142 | doAssert(match(f2, "/test42/somefile/").params["ext"] == "")
143 | doAssert(match(f2, "/test42/somefile.txt").params["ext"] == "txt")
144 | doAssert(match(f2, "/test42/somefile.txt/").params["ext"] == "txt")
145 |
146 | let f3 = parsePattern(r"/test32/\@\\\??")
147 | doAssert(match(f3, r"/test32/@\").matched)
148 | doAssert(not match(f3, r"/test32/@\\").matched)
149 | doAssert(match(f3, r"/test32/@\?").matched)
150 |
--------------------------------------------------------------------------------
/nawak_mongrel.nim:
--------------------------------------------------------------------------------
1 | import os, posix, tables, strutils, strtabs, json, cookies
2 | import zmq, uuid
3 | import private/optim_strange_loop, private/netstrings, private/jesterutils
4 | import private/nawak_mg as n_mg,
5 | private/nawak_common as n_cm
6 | export n_mg.get, n_mg.post, n_mg.response, n_mg.redirect, n_mg.custom_page
7 | export n_cm.addCookie, n_cm.deleteCookie, n_cm.daysFromNow
8 |
9 |
10 | const HTTP_FORMAT = "HTTP/1.1 $1 $2\r\n$3\r\n\r\n$4"
11 |
12 | var context {.global.} : PContext
13 |
14 |
15 | var interrupted {.global.} = false
16 | proc signal_handler(signal_value: cint) {.noconv.} =
17 | interrupted = true
18 | echo "Interruption captured, force quit in 2 seconds"
19 | sleep(2000)
20 | if ctx_term(context) != 0:
21 | zmqError()
22 |
23 | # register interrupt callbacks
24 | var action: TSigAction
25 | var nilaction: TSigAction
26 | action.sa_handler = signal_handler
27 | action.sa_flags = 0
28 | discard sigemptyset(action.sa_mask)
29 | discard sigaction(SIGINT, action, action)
30 | discard sigaction(SIGTERM, action, action)
31 |
32 |
33 | proc set_linger(to_mongrel: PSocket) =
34 | var delay = 1500
35 | discard setsockopt(to_mongrel, LINGER, addr delay, sizeof(delay))
36 |
37 |
38 | proc http_response(body:string, code: int, status: string,
39 | headers: var StringTableRef): string =
40 | ## formats the http part of the response payload to send on the zeromq socket
41 | headers["Content-Length"] = $len(body)
42 | if not headers.hasKey("Content-Type"):
43 | headers["Content-Type"] = "text/html"
44 |
45 | var headers_strs: seq[string] = @[]
46 | for k, v in headers.pairs:
47 | #headers_strs.add("$1: $2" % [k, $v])
48 | headers_strs.add(optFormat("$1: $2", [k, $v]))
49 |
50 | return HTTP_FORMAT % [$code, status, headers_strs.join("\r\n"), body]
51 | #return optFormat("HTTP/1.1 $1 $2\r\n$3\r\n\r\n$4", [$code, status, headers_strs.join("\r\n"), body])
52 |
53 | proc send*(to_mongrel: TConnection, uuid, conn_id, msg: string) =
54 | let payload = "$1 $2:$3, $4" % [uuid, $conn_id.len, conn_id, msg]
55 | #let payload = optFormat("$1 $2:$3, $4", [uuid, $conn_id.len, conn_id, msg])
56 | zmq.send(to_mongrel, payload)
57 |
58 | #proc deliver(uuid: string, idents: openArray[string], data: string) =
59 | # send(uuid, idents.join(" "), data)
60 |
61 | proc deliver(to_mongrel: TConnection, uuid, conn_id, data: string) =
62 | send(to_mongrel, uuid, conn_id, data)
63 |
64 |
65 | proc prepare_request_obj(req: TMongrelMsg): TRequest =
66 | result.path = req.headers["PATH"].str
67 | result.query = {:}.newStringTable
68 | if req.headers.hasKey("QUERY"):
69 | parseUrlQuery(req.headers["QUERY"].str, result.query)
70 | if req.headers.hasKey("cookie"):
71 | result.cookies = parseCookies(req.headers["cookie"].str)
72 | else:
73 | result.cookies = {:}.newStringTable
74 |
75 | template try_match(): stmt {.immediate.} =
76 | try:
77 | let (matched, res) = it.match(request.path, request)
78 | if matched:
79 | has_matched = matched
80 | resp = res
81 | break
82 | except:
83 | let
84 | e = getCurrentException()
85 | msg = getCurrentExceptionMsg()
86 | stacktrace = getStackTrace(e)
87 | has_matched = true
88 | resp = arg.nawak.custom_pages[500](msg & "\L" & stacktrace)
89 | break
90 |
91 | type ThrArgs = tuple
92 | init: proc()
93 | from_addr, to_addr: string
94 | nawak: TNawak
95 |
96 | proc run_thread*(arg: ThrArgs) {.thread.} =
97 | var my_uuid: Tuuid
98 | uuid_generate_random(my_uuid)
99 | let sender_uuid = my_uuid.to_hex
100 | #echo "I am: ", sender_uuid
101 |
102 | var from_mongrel = connect(arg.from_addr, PULL, context)
103 | var to_mongrel = connect(arg.to_addr, PUB, context)
104 | discard setsockopt(to_mongrel.s, IDENTITY, cstring(sender_uuid),
105 | sender_uuid.len)
106 |
107 | arg.init()
108 |
109 | while not interrupted:
110 | #echo ""
111 | var msg: string
112 | try:
113 | msg = receive(from_mongrel)
114 | except EZmq:
115 | set_linger(to_mongrel.s)
116 | break
117 |
118 | var req = parse(msg)
119 | #echo req.headers["PATH"].str
120 | #echo pretty(req.headers)
121 |
122 | var resp: TResponse
123 | var has_matched = false
124 |
125 | case req.headers["METHOD"].str
126 | of "JSON":
127 | if req.body == "{\"type\":\"disconnect\"}":
128 | #echo "DISCONNECT: a client didn't wait for the answer\n\tThe server did some work for nothing! Intolerable!"
129 | continue
130 |
131 | of "GET":
132 | let request = prepare_request_obj(req)
133 | for it in arg.nawak.gets:
134 | try_match()
135 |
136 | of "POST":
137 | let request = prepare_request_obj(req)
138 | for it in arg.nawak.posts:
139 | try_match()
140 |
141 | else:
142 | echo "METHOD NOT AVAILABLE"
143 | to_mongrel.send(req.uuid, req.id, "") # disconnect
144 |
145 | if not has_matched:
146 | resp = arg.nawak.custom_pages[404]("""
147 | "And they took the road less traveled.
148 | Unfortunately, there was nothing there."
149 | """)
150 |
151 | if resp.headers == nil:
152 | resp.headers = {:}.newStringTable
153 | if resp.body == nil or resp.code == 0:
154 | resp = arg.nawak.custom_pages[500]("""
155 | The programmer forgot to add a status code or to return some data
156 | in the body of the response.""")
157 |
158 | if interrupted:
159 | set_linger(to_mongrel.s)
160 |
161 | try:
162 | to_mongrel.send(req.uuid, req.id, http_response(
163 | resp.body, resp.code, "OK", resp.headers
164 | ))
165 | except EZmq:
166 | set_linger(to_mongrel.s)
167 | break
168 |
169 | #var thread_id = cast[int](myThreadId[type(arg)]())
170 | #echo "Quit thread $#" % $thread_id
171 |
172 | let status_from = close(from_mongrel.s)
173 | let status_to = close(to_mongrel.s)
174 | if status_from != 0 or status_to != 0:
175 | zmqError()
176 |
177 | proc run*(init: proc(), from_addr="tcp://localhost:9999", to_addr="tcp://localhost:9998", nb_threads=32) =
178 | context = ctx_new()
179 | if context == nil:
180 | zmqError()
181 |
182 | ## the following only executes (number of cpu cores) threads at a time maximum
183 | #let nb_cpu = 4
184 | #for i in 0 .. nb_cpu:
185 | # spawn run_thread(from_addr, to_addr)
186 | #system.sync()
187 |
188 | var thr: seq[TThread[ThrArgs]]
189 | thr.newSeq(nb_threads)
190 |
191 | for i in 0 .. 404 not found.
" & msg)
34 |
35 | proc default_500_handler(msg: string): TResponse =
36 | echo msg
37 | let msg_html = msg.replace("\n", "
\L")
38 | return (500, {:}.newStringTable,
39 | "The server says: 500 internal error.
" & msg_html)
40 |
41 | proc init_nawak(nawak: ptr TNawak) =
42 | nawak.gets = @[]
43 | nawak.posts = @[]
44 | nawak.custom_pages = initTable[int, TSpecialPageCallback]()
45 | nawak.custom_pages[404] = default_404_handler
46 | nawak.custom_pages[500] = default_500_handler
47 |
48 | init_nawak(addr nawak)
49 |
50 | proc register_custom_page(code: THttpCode, callback: TSpecialPageCallback) =
51 | nawak.custom_pages[code] = callback
52 |
53 | template custom_page*(code: int, body: stmt): stmt {.immediate.} =
54 | bind register_custom_page
55 | register_custom_page(code, proc(msg: string): TResponse =
56 | body
57 | )
58 |
59 | proc response*(body: string): TResponse =
60 | #result.code = 200
61 | #result.headers = nil
62 | #shallowCopy(result.body, body)
63 | return (200, nil, body)
64 |
65 | proc response*(body: string, content_type: string): TResponse =
66 | return (200, {"Content-Type": content_type}.newStringTable, body)
67 |
68 | proc response*(code: THttpCode, body: string): TResponse =
69 | return (code, nil, body)
70 |
71 | proc response*(code: THttpCode, body: string, headers: StringTableRef): TResponse =
72 | return (code, headers, body)
73 |
74 | proc response*(body: string, headers: StringTableRef): TResponse =
75 | return (200, headers, body)
76 |
77 | proc redirect*(path: string, code = 303): TResponse =
78 | let path = escape(path)
79 | result.code = code
80 | result.headers = {:}.newStringTable
81 | result.headers["Location"] = path
82 | result.body = "Redirecting to $1." % [path]
83 |
84 | proc register(http_method: THttpMethod, matcher: TMatcher, path: string) =
85 | case http_method
86 | of TGET:
87 | nawak.gets.add((matcher, path))
88 | of TPOST:
89 | nawak.posts.add((matcher, path))
90 |
91 | # debug purposes:
92 | echo "registered: ", http_method, " ", path
93 |
94 | template inject_matcher(path: string, pattern: TPattern,
95 | callback: proc(r: TRequest):TResponse,
96 | url_params: tuple) {.dirty, immediate.} =
97 | # (if not dirty, macros inside don't compile)
98 | ## injects the proc ``match`` in the current scope.
99 | ## The variables ``pattern``, ``callback`` and ``url_params`` are closed over,
100 | ## and are supposed to be declared before this template is called.
101 | bind TRequest, TResponse, TNode, TNodeText, TNodeField, check, findNextText,
102 | parseUntil, inject_tuple_setter_by_index
103 | proc match(s: string, request: TRequest):
104 | tuple[matched: bool, response: TResponse] {.closure.} =
105 | var i = 0 # Location in ``s``.
106 |
107 | result.matched = true
108 | var field_count = 0
109 |
110 | for ncount, node in pattern:
111 | case node.typ
112 | of TNodeText:
113 | if node.optional:
114 | if check(node, s, i):
115 | inc(i, node.text.len) # Skip over this optional character.
116 | else:
117 | # If it's not there, we have nothing to do. It's optional after all.
118 | discard
119 | else:
120 | if check(node, s, i):
121 | inc(i, node.text.len) # Skip over this
122 | else:
123 | # No match.
124 | result.matched = false
125 | return
126 | of TNodeField:
127 | var nextTxtNode: TNode
128 | var stopChar = '/'
129 | if findNextText(pattern, ncount, nextTxtNode):
130 | stopChar = nextTxtNode.text[0]
131 | var matchNamed = ""
132 | i += s.parseUntil(matchNamed, stopChar, i)
133 | if matchNamed != "":
134 | #url_params[field_count] = matchNamed
135 | ## this line doesn't work cause tuple index cannot be dynamic.
136 | ## solution: case switch macro injecter!
137 | inject_tuple_setter_by_index(field_count, url_params, matchNamed, path)
138 | elif matchNamed == "" and not node.optional:
139 | result.matched = false
140 | return
141 | inc(field_count)
142 |
143 | if s.len != i:
144 | result.matched = false
145 | else:
146 | result.response = callback(request)
147 | result.matched = true
148 | # cleanup the url parameters for the next request
149 | # (because of optional params that could find
150 | # themselves filled even if they shouldn't)
151 | let emptystring = ""
152 | for j in 0..field_count-1:
153 | ## same impossibility with tuple index that cannot be dynamic
154 | #url_params[j] = ""
155 | #url_params[0] = ""
156 | inject_tuple_setter_by_index(j, url_params, emptystring, path)
157 |
158 |
159 | macro inject_urlparams_tuple*(path: string): stmt =
160 | ## for a path like "/show/@user/@id/?"
161 | ## it will injects the following lines in the current scope:
162 | ## var url_params {.threadvar.}: tuple[user: string, id: string]
163 | var path_str = $toStrLit(path)
164 | # remove the quotes at the beginning and end
165 | path_str = path_str[1 .. path_str.len - 2]
166 |
167 | var pattern = parsePattern(path_str)
168 | var fields_total = 0
169 |
170 | var pragmaexpr = newNimNode(nnkPragmaExpr)
171 | var pragma = newNimNode(nnkPragma)
172 | pragma.add(newIdentNode("threadvar"))
173 | pragmaexpr.add(newIdentNode("url_params"))
174 | pragmaexpr.add(pragma)
175 |
176 | var tuple_ty = newNimNode(nnkTupleTy)
177 | for i, node in pattern:
178 | case node.typ
179 | of TNodeField:
180 | inc(fields_total)
181 | tuple_ty.add(
182 | newIdentDefs(
183 | newIdentNode( node.text ),
184 | newIdentNode("string")
185 | )
186 | )
187 | else:
188 | discard
189 |
190 | if fields_total == 0:
191 | result = newEmptyNode()
192 | return
193 |
194 | result = newNimNode(nnkVarSection)
195 | result.add(newIdentDefs(
196 | pragmaexpr,
197 | tuple_ty
198 | ))
199 |
200 | # debug the constructed macro:
201 | #echo toStrLit(result)
202 | #echo result.treeRepr
203 |
204 |
205 | template register_route(http_method: THttpMethod, path: string,
206 | body: stmt): stmt {.immediate, dirty.} =
207 | bind parsePattern, TRequest, TResponse, inject_matcher,
208 | inject_urlparams_tuple, register
209 | block:
210 | let pattern = parsePattern(path)
211 | inject_urlparams_tuple(path)
212 |
213 | proc callback(request: TRequest): TResponse =
214 | body
215 |
216 | inject_matcher(path, pattern, callback, url_params)
217 | register(http_method, match, path)
218 |
219 |
220 | template get*(path: string, body: stmt): stmt {.immediate, dirty.} =
221 | bind register_route, TGET
222 | register_route(TGET, path, body)
223 |
224 | template post*(path: string, body: stmt): stmt {.immediate, dirty.} =
225 | bind register_route, TPOST
226 | register_route(TPOST, path, body)
227 |
228 |
--------------------------------------------------------------------------------