├── .gitignore ├── LICENSE ├── README.md ├── example ├── app.nim ├── conf │ ├── Makefile │ └── mongrel2.conf ├── fortunes_tmpl.nim ├── hellocookies.nim ├── helloworld.nim ├── model.nim ├── model_postgre.nim ├── model_redis.nim ├── nim.cfg ├── prepare_postgresql_database.sql └── prepare_redis_database.sh ├── jdump.nim ├── nawak.babel ├── nawak_mongrel.nim └── private ├── jesterpatterns.nim ├── jesterutils.nim ├── nawak_common.nim ├── nawak_mg.nim ├── netstrings.nim ├── optim_strange_loop.nim └── tuple_index_setter.nim /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | 12 | #for fortune in items(fortunes): 13 | 14 | #end for 15 |
idmessage
${fortune.id}${escape(fortune.message)}
16 | 17 | 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/nim.cfg: -------------------------------------------------------------------------------- 1 | path=".." 2 | path="." 3 | nimcache=".nimcache" 4 | hints=off 5 | verbosity="0" 6 | threads=on 7 | #cc = clang 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 .. 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /private/nawak_mg.nim: -------------------------------------------------------------------------------- 1 | import macros, tables, strtabs, parseutils 2 | from strutils import `%`, replace 3 | from xmltree import escape 4 | import jesterpatterns 5 | import tuple_index_setter 6 | 7 | # I can only support one level of magic without going insane. 8 | # Thus, any template/macro that is not user-facing begins with ``inject_`` 9 | # to make crystal clear that it is not a standard function call and more stuff ends up 10 | # in the scope. 11 | # (user-facing: user == the programmer using the framework, not developing it) 12 | 13 | type 14 | THttpCode* = int 15 | TRequest* = tuple[path: string, query: StringTableRef, cookies: StringTableRef] 16 | TResponse* = tuple[code: THttpCode, 17 | headers: StringTableRef, 18 | body: string] 19 | TMatcher = proc(s: string, request: TRequest): 20 | tuple[matched: bool, response: TResponse] 21 | TCallback = proc(request: TRequest): TResponse 22 | TSpecialPageCallback* = proc(msg: string): TResponse 23 | THttpMethod = enum 24 | TGET = "GET", TPOST = "POST" 25 | TNawak* = tuple[gets: seq[tuple[match: TMatcher, path: string]], 26 | posts: seq[tuple[match: TMatcher, path: string]], 27 | custom_pages: Table[int, TSpecialPageCallback] ] 28 | 29 | var nawak* {.global.} : TNawak 30 | 31 | proc default_404_handler(msg: string): TResponse = 32 | return (404, {:}.newStringTable, 33 | "The server says: 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------