From 262df0af0694edbdae0c9d7a728eb56c10ba4227 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sat, 18 Aug 2012 19:10:32 +0200 Subject: [PATCH 01/62] Port to CoffeeScript. --- .gitignore | 1 + README.md | 106 +----------------------------------------- almost_sinatra.coffee | 28 +++++++++++ almost_sinatra.rb | 8 ---- example.coffee | 35 ++++++++++++++ example.rb | 53 --------------------- package.json | 5 ++ 7 files changed, 70 insertions(+), 166 deletions(-) create mode 100644 almost_sinatra.coffee delete mode 100644 almost_sinatra.rb create mode 100644 example.coffee delete mode 100644 example.rb create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 14fac93..ca9fd0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ annoying people +node_modules diff --git a/README.md b/README.md index de89030..a43328d 100644 --- a/README.md +++ b/README.md @@ -3,108 +3,4 @@ "until programmers stop acting like obfuscation is morally hazardous, they’re not artists, just kids who don’t want their food to touch." - _why -A Sinatra implementation in less than 10 lines. -Dependencies: Tilt and Rack (like Sinatra) and [Backports](https://github.com/marcandre/backports/). -Only works on Ruby 1.9. - -Features: - -* all template engines Sinatra supports -* get/post/put/delete -* before filters -* configure/set/enable/disable/... -* session/params -* support for helpers -* stand alone usage -* inline templates -* thread safe -* works with Padrino - -It is considered fully compatible with Sinatra. If your app does not run with -Almost Sinatra, please open a [Sinatra issue](https://github.com/sinatra/sinatra/issues). - -## Installation - -Copy the contents of `almost_sinatra.rb` into your app file (at the top), that -way you also avoid running the wrong version by accident. - -## Try it - - $ ruby example.rb - What Sinatra implementation should I use? - [R]eal Sinatra - [A]lmost Sinatra - >> a - -## Modular style - -When pasting the Almost Sinatra code into your file, replace `Application=$a` -with `Base=$a` (second line atm, but don't expect me to update the readme if -I move stuff around). - -## Performance - -It's just amazing. No code is faster than no code. - - $ sloccount alomst_sinatra.rb - Total Physical Source Lines of Code (SLOC) = 8 - Development Effort Estimate, Person-Years (Person-Months) = 0.00 (0.02) - (Basic COCOMO model, Person-Months = 2.4 * (KSLOC**1.05)) - Schedule Estimate, Years (Months) = 0.04 (0.51) - (Basic COCOMO model, Months = 2.5 * (person-months**0.38)) - Estimated Average Number of Developers (Effort/Schedule) = 0.03 - Total Estimated Cost to Develop = $ 170 - (average salary = $56,286/year, overhead = 2.40). - - $ sloccount ../sinatra - Total Physical Source Lines of Code (SLOC) = 5,771 - Development Effort Estimate, Person-Years (Person-Months) = 1.26 (15.12) - (Basic COCOMO model, Person-Months = 2.4 * (KSLOC**1.05)) - Schedule Estimate, Years (Months) = 0.58 (7.02) - (Basic COCOMO model, Months = 2.5 * (person-months**0.38)) - Estimated Average Number of Developers (Effort/Schedule) = 2.15 - Total Estimated Cost to Develop = $ 170,198 - (average salary = $56,286/year, overhead = 2.40). - -As you can see Sinatra is 1000000% more expensive than Almost Sinatra! - -Generated using David A. Wheeler's 'SLOCCount'. - -## Coding guidelines - -* keep code under ten lines, this includes require and whatnot - -* avoid spaces and newlines if possible - -* wrap at 200 characters to make it readable on a terminal - -* newlines may be used instead of `;` to improve readability - -* use `map` instead of `each`, it's shorter - -* store constants in global variables to dry up your code (like `$f = File`) - -* if you have one loop, reuse it. instead of this: - - ['a','b'].map{|e|...} - [Rack::Something].map{|e|...} - - do this: - - ['a','b',Rack::Something].map{|e|(e==e.to_s)?(...):(...)} - - Saves a line! - -* `e.to_s==e` is shorter than `e.is_a? String` or `String===e` - -* use `->{}` instead of `proc` or `lambda` (this is why it depends on 1.9) - -* add methods for class scope to `$o` and for instance scope to `$a` (or `$o`), - see [Scopes And Binding](http://www.sinatrarb.com/intro#Scopes%20and%20Binding) - -* don't include tests. tests just bloat the code base. just commit, the users - will complain if you break anything. - -## About Versioning - -Versions are to Software what Subversion is to Git. +A Sinatra is ROFLscale. diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee new file mode 100644 index 0000000..c30f909 --- /dev/null +++ b/almost_sinatra.coffee @@ -0,0 +1,28 @@ +[M,w,e]=[Math,((s)->s.split(/\s+/)),(d,s)->d[k]=v for k,v of s];[http,url,haml,ejs]=w('http url haml ejs').map require + +class Session + constructor:(id)->@__id__=id + @s={} + +Session.get=(r)-> + [h,c]=[{},(r.headers.cookie||'').split(/\s*;\s*/)];c.map (p)->q=p.split('=');h[q[0]]=q[1] + i=h.sessid||M.floor(M.random()*M.pow(2,52)).toString 36;@s[i]||=new @(i) + +class App + [@b,@r,@h,@t]=[[],[],{},{}] + constructor:(@req, @res)->@_url=url.parse(@req.url,true);@params=@_url.query;@session=Session.get(@req);e @,App.h + render:(s)->h='Content-Type':'text/html','Set-Cookie':'sessid='+@session.__id__;@res.writeHead 200,h;@res.end s + haml:(n)->@render haml(App.t[n])(@) + ejs:(n,o)->@render ejs.render(App.t[n],o?.locals) + puts:(s)->console.log s + +w('get post put delete patch').map (v)->App[v]=(p,f)->App.r.push [v.toUpperCase(),p,f] + +e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen(4567) + +App.handle=(req,res)-> + a=new App(req,res);p=url.parse(req.url).pathname;App.b.map (b)->b.call a + b=App.r.filter((r)->r[0]==req.method&&r[1]==p)[0] + if b then (b[2].call a) else (res.writeHead 400,{};res.end()) + +module.exports = App diff --git a/almost_sinatra.rb b/almost_sinatra.rb deleted file mode 100644 index e2c7e02..0000000 --- a/almost_sinatra.rb +++ /dev/null @@ -1,8 +0,0 @@ -%w.rack tilt backports INT TERM..map{|l|trap(l){$r.stop}rescue require l} -$n=Sinatra=Module.new{extend Rack;a,D,S,$p,q,Application=Builder.new,Object.method(:define_method),/@@ *([^\n]+)\n(((?!@@)[^\n]*\n)*)/m,4567,a -%w[get post put delete].map{|m|D.(m){|u,&b|a.map(u){run->(e){[200,{"Content-Type"=>"text/html"},[a.instance_eval(&b)]]}}}} -Tilt.mappings.map{|k,v|D.(k){|n,*o|$t||=(h={};File.read(caller[0][/^[^:]+/]).scan(S){|a,b|h[a]=b};h);v[0].new(*o){n.to_s==n ?n:$t[n.to_s]}.render(a,o[0].try(:[],:locals)||{})}} -%w[set enable disable configure helpers use register].map{|m|D.(m){|*_,&b|b.try :[]}};END{Handler.get("webrick").run(a,Port:$p){|s|$r=s}} -%w[params session].map{|m|D.(m){q.send m}};a.use Session::Cookie;a.use Lock -D.(:before){|&b|a.use Rack::Config,&b};before{|e|q=Request.new e;q.params.dup.map{|k,v|params[k.to_sym]=v}}} -puts "== almost #$n/No Version has taken the stage on #$p for development with backup from Webrick" diff --git a/example.coffee b/example.coffee new file mode 100644 index 0000000..6258037 --- /dev/null +++ b/example.coffee @@ -0,0 +1,35 @@ +app = require './almost_sinatra' + +app.helpers + inc_counter: -> + @session.counter ||= 0 + @session.counter += 1 + +app.before -> + @puts "yay! got a request!" + +app.get '/', -> + @title = 'Almost Sinatra' + @haml 'index' + +app.get '/hello', -> + @ejs 'hello', locals: {name: @params.name} + +app.get '/counter', -> + @inc_counter() + @render @session.counter.toString() + +app.template 'index', """ +%html + %head + %title= title + %body + %a{href: '/hello?name=World'} Say hello! + %a{href: '/counter'} Show Counter +""" + +app.template 'hello', """ +Hello <%= name %>! +""" + +app.run() diff --git a/example.rb b/example.rb deleted file mode 100644 index bacc7c9..0000000 --- a/example.rb +++ /dev/null @@ -1,53 +0,0 @@ -until defined? Sinatra - puts "What Sinatra implementation should I use?", - "[R]eal Sinatra", "[A]lmost Sinatra" - print ">> " - case gets.strip.downcase - when 'a' then require_relative "almost_sinatra" - when 'r' then require 'sinatra' - else puts "invalid input!!" - end -end - -configure do - enable :sessions -end - -helpers do - def inc_counter - session[:counter] ||= 0 - session[:counter] += 1 - end -end - -before do - puts " yay! got a request ".center(80, "=") -end - -get '/' do - @title = "Almost Sinatra" - haml :index -end - -# /hello?name=world -get '/hello' do - erb :hello, locals: { name: params[:name] } -end - -get '/counter' do - inc_counter - session[:counter].to_s -end - -__END__ - -@@ index -%html - %head - %title= @title - %body - %a{:href => '/hello?name=World'} Say hello! - %a{:href => '/counter'} Show Counter - -@@ hello -Hello <%= name %>! diff --git a/package.json b/package.json new file mode 100644 index 0000000..e6d2e74 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "name": "almost_sinatra", + "version": "1.0.0", + "dependencies": {"coffee-script":"", "haml":"", "ejs":""} +} \ No newline at end of file From 72631f8d29f4255c5d8f32c8c550deb6ecd94eb2 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sat, 18 Aug 2012 19:13:56 +0200 Subject: [PATCH 02/62] Return 404 on route not found. --- almost_sinatra.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index c30f909..b7d544f 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -23,6 +23,6 @@ e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),ru App.handle=(req,res)-> a=new App(req,res);p=url.parse(req.url).pathname;App.b.map (b)->b.call a b=App.r.filter((r)->r[0]==req.method&&r[1]==p)[0] - if b then (b[2].call a) else (res.writeHead 400,{};res.end()) + if b then (b[2].call a) else (res.writeHead 404,{};res.end()) module.exports = App From b90b0727f598512a292bf0665c570c43b995a2a8 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sat, 18 Aug 2012 19:45:36 +0200 Subject: [PATCH 03/62] Parse form-urlencoded bodies. --- almost_sinatra.coffee | 11 ++++++++--- example.coffee | 3 +++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index b7d544f..2c8a14e 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,4 +1,4 @@ -[M,w,e]=[Math,((s)->s.split(/\s+/)),(d,s)->d[k]=v for k,v of s];[http,url,haml,ejs]=w('http url haml ejs').map require +[M,w,e]=[Math,((s)->s.split(/\s+/)),(d,s)->d[k]=v for k,v of s];[http,url,qs,haml,ejs]=w('http url querystring haml ejs').map require class Session constructor:(id)->@__id__=id @@ -10,7 +10,12 @@ Session.get=(r)-> class App [@b,@r,@h,@t]=[[],[],{},{}] - constructor:(@req, @res)->@_url=url.parse(@req.url,true);@params=@_url.query;@session=Session.get(@req);e @,App.h + constructor:(@req, @res)->@_url=url.parse(@req.url,true);@session=Session.get(@req);e @,App.h + parse:(c)-> + if /application\/x-www-form-urlencoded/.test @req.headers['content-type'] + @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=>@params=qs.parse b;c() + else + @params=@_url.query;c() render:(s)->h='Content-Type':'text/html','Set-Cookie':'sessid='+@session.__id__;@res.writeHead 200,h;@res.end s haml:(n)->@render haml(App.t[n])(@) ejs:(n,o)->@render ejs.render(App.t[n],o?.locals) @@ -23,6 +28,6 @@ e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),ru App.handle=(req,res)-> a=new App(req,res);p=url.parse(req.url).pathname;App.b.map (b)->b.call a b=App.r.filter((r)->r[0]==req.method&&r[1]==p)[0] - if b then (b[2].call a) else (res.writeHead 404,{};res.end()) + if b then (a.parse ()->b[2].call a) else (res.writeHead 404,{};res.end()) module.exports = App diff --git a/example.coffee b/example.coffee index 6258037..0541899 100644 --- a/example.coffee +++ b/example.coffee @@ -19,6 +19,9 @@ app.get '/counter', -> @inc_counter() @render @session.counter.toString() +app.post '/', -> + @render JSON.stringify(@params) + app.template 'index', """ %html %head From 20395a687fe9b62e1935342346abeeb5b5e74dc3 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 01:54:28 +0200 Subject: [PATCH 04/62] Allow user to set headers and status codes. --- almost_sinatra.coffee | 15 +++++++++------ example.coffee | 4 +++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 2c8a14e..5d1e0a2 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,4 +1,5 @@ -[M,w,e]=[Math,((s)->s.split(/\s+/)),(d,s)->d[k]=v for k,v of s];[http,url,qs,haml,ejs]=w('http url querystring haml ejs').map require +[M,e,w,z]=[Math,((d,s)->d[k]=v for k,v of s),((s)->s.split(/\s+/)),(d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s] +[http,url,qs,haml,ejs]=w('http url querystring haml ejs').map require class Session constructor:(id)->@__id__=id @@ -10,13 +11,15 @@ Session.get=(r)-> class App [@b,@r,@h,@t]=[[],[],{},{}] - constructor:(@req, @res)->@_url=url.parse(@req.url,true);@session=Session.get(@req);e @,App.h + constructor:(@req,@res,@_h={})->@_u=url.parse(@req.url,true);@session=Session.get(@req);z @_h,'Set-Cookie':['sessid='+@session.__id__];e @,App.h parse:(c)-> if /application\/x-www-form-urlencoded/.test @req.headers['content-type'] @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=>@params=qs.parse b;c() else - @params=@_url.query;c() - render:(s)->h='Content-Type':'text/html','Set-Cookie':'sessid='+@session.__id__;@res.writeHead 200,h;@res.end s + @params=@_u.query;c() + headers:(o)->z @_h,o + status:(n)->@_s=parseInt(n,10) + render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;z h,@_h;@res.writeHead @_s||200,h;@res.end s haml:(n)->@render haml(App.t[n])(@) ejs:(n,o)->@render ejs.render(App.t[n],o?.locals) puts:(s)->console.log s @@ -28,6 +31,6 @@ e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),ru App.handle=(req,res)-> a=new App(req,res);p=url.parse(req.url).pathname;App.b.map (b)->b.call a b=App.r.filter((r)->r[0]==req.method&&r[1]==p)[0] - if b then (a.parse ()->b[2].call a) else (res.writeHead 404,{};res.end()) + if b then (a.parse ()->r=b[2].call a;if typeof r=='string' then a.render r) else (res.writeHead 404,{};res.end()) -module.exports = App +module.exports=App diff --git a/example.coffee b/example.coffee index 0541899..53c3b5a 100644 --- a/example.coffee +++ b/example.coffee @@ -20,7 +20,9 @@ app.get '/counter', -> @render @session.counter.toString() app.post '/', -> - @render JSON.stringify(@params) + @status 201 + @headers 'Content-Type': 'application/json' + JSON.stringify(@params) app.template 'index', """ %html From 075919823f38ffebdabc18b1b127e4f872cb697f Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 02:04:55 +0200 Subject: [PATCH 05/62] Make cookies safer. --- almost_sinatra.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 5d1e0a2..fc7ccb5 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -7,11 +7,11 @@ class Session Session.get=(r)-> [h,c]=[{},(r.headers.cookie||'').split(/\s*;\s*/)];c.map (p)->q=p.split('=');h[q[0]]=q[1] - i=h.sessid||M.floor(M.random()*M.pow(2,52)).toString 36;@s[i]||=new @(i) + i=h.sessid||[1..4].map(->M.floor(M.random()*M.pow(2,40)).toString 36).join '';@s[i]||=new @(i) class App [@b,@r,@h,@t]=[[],[],{},{}] - constructor:(@req,@res,@_h={})->@_u=url.parse(@req.url,true);@session=Session.get(@req);z @_h,'Set-Cookie':['sessid='+@session.__id__];e @,App.h + constructor:(@req,@res,@_h={})->@_u=url.parse(@req.url,true);@session=Session.get(@req);z @_h,'Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h parse:(c)-> if /application\/x-www-form-urlencoded/.test @req.headers['content-type'] @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=>@params=qs.parse b;c() From e99065e008bb2a19f74586ef6f30dd700852631f Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 02:06:38 +0200 Subject: [PATCH 06/62] Remove an unneeded space. --- almost_sinatra.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index fc7ccb5..38498b1 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -31,6 +31,6 @@ e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),ru App.handle=(req,res)-> a=new App(req,res);p=url.parse(req.url).pathname;App.b.map (b)->b.call a b=App.r.filter((r)->r[0]==req.method&&r[1]==p)[0] - if b then (a.parse ()->r=b[2].call a;if typeof r=='string' then a.render r) else (res.writeHead 404,{};res.end()) + if b then(a.parse ()->r=b[2].call a;if typeof r=='string'then a.render r)else(res.writeHead 404,{};res.end()) module.exports=App From 5ad7a6557f51e08ab4b89ac5f37384bc4055c4dc Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 02:14:21 +0200 Subject: [PATCH 07/62] Incorporate map() call into w(). --- almost_sinatra.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 38498b1..f9cdcdc 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,5 +1,5 @@ -[M,e,w,z]=[Math,((d,s)->d[k]=v for k,v of s),((s)->s.split(/\s+/)),(d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s] -[http,url,qs,haml,ejs]=w('http url querystring haml ejs').map require +[M,e,w,z]=[Math,((d,s)->d[k]=v for k,v of s),((s,c)->s.split(/\s+/).map c),(d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s] +[http,url,qs,haml,ejs]=w 'http url querystring haml ejs',require class Session constructor:(id)->@__id__=id @@ -24,7 +24,7 @@ class App ejs:(n,o)->@render ejs.render(App.t[n],o?.locals) puts:(s)->console.log s -w('get post put delete patch').map (v)->App[v]=(p,f)->App.r.push [v.toUpperCase(),p,f] +w 'get post put delete patch',(v)->App[v]=(p,f)->App.r.push [v.toUpperCase(),p,f] e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen(4567) From b18205d6d0ad110ba09349735da1d72c07888d6f Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 02:17:04 +0200 Subject: [PATCH 08/62] Respond to HEAD and OPTIONS. --- almost_sinatra.coffee | 2 +- example.coffee | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index f9cdcdc..e19b16b 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -24,7 +24,7 @@ class App ejs:(n,o)->@render ejs.render(App.t[n],o?.locals) puts:(s)->console.log s -w 'get post put delete patch',(v)->App[v]=(p,f)->App.r.push [v.toUpperCase(),p,f] +w 'get post put delete patch head options',(v)->App[v]=(p,f)->App.r.push [v.toUpperCase(),p,f] e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen(4567) diff --git a/example.coffee b/example.coffee index 53c3b5a..bd47701 100644 --- a/example.coffee +++ b/example.coffee @@ -24,6 +24,10 @@ app.post '/', -> @headers 'Content-Type': 'application/json' JSON.stringify(@params) +app.options '/', -> + @headers 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' + @render '' + app.template 'index', """ %html %head From cb5b527760a7d3926306c8aafb378f9aede8d128 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 02:30:49 +0200 Subject: [PATCH 09/62] Cleaner params parsing. --- almost_sinatra.coffee | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index e19b16b..7276c52 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,4 +1,4 @@ -[M,e,w,z]=[Math,((d,s)->d[k]=v for k,v of s),((s,c)->s.split(/\s+/).map c),(d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s] +[u,M,e,w,z]=[/\bapplication\/x-www-form-urlencoded\b/,Math,((d,s)->d[k]=v for k,v of s),((s,c)->s.split(/\s+/).map c),(d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s] [http,url,qs,haml,ejs]=w 'http url querystring haml ejs',require class Session @@ -13,10 +13,8 @@ class App [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_h={})->@_u=url.parse(@req.url,true);@session=Session.get(@req);z @_h,'Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h parse:(c)-> - if /application\/x-www-form-urlencoded/.test @req.headers['content-type'] - @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=>@params=qs.parse b;c() - else - @params=@_u.query;c() + @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> + @body=b;@params=(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c() headers:(o)->z @_h,o status:(n)->@_s=parseInt(n,10) render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;z h,@_h;@res.writeHead @_s||200,h;@res.end s @@ -24,7 +22,7 @@ class App ejs:(n,o)->@render ejs.render(App.t[n],o?.locals) puts:(s)->console.log s -w 'get post put delete patch head options',(v)->App[v]=(p,f)->App.r.push [v.toUpperCase(),p,f] +w 'get post put delete patch head options',(v)->App[v]=(p,f)->@r.push [v.toUpperCase(),p,f] e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen(4567) From c7973a50731721f70ecadefb31cc01b5bb08f320 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 02:34:19 +0200 Subject: [PATCH 10/62] Remove a redundant helper function. --- almost_sinatra.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 7276c52..7ee6aff 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,4 +1,4 @@ -[u,M,e,w,z]=[/\bapplication\/x-www-form-urlencoded\b/,Math,((d,s)->d[k]=v for k,v of s),((s,c)->s.split(/\s+/).map c),(d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s] +[u,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,Math,((d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s),(s,c)->s.split(/\s+/).map c] [http,url,qs,haml,ejs]=w 'http url querystring haml ejs',require class Session @@ -11,13 +11,13 @@ Session.get=(r)-> class App [@b,@r,@h,@t]=[[],[],{},{}] - constructor:(@req,@res,@_h={})->@_u=url.parse(@req.url,true);@session=Session.get(@req);z @_h,'Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h + constructor:(@req,@res,@_h={})->@_u=url.parse(@req.url,true);@session=Session.get(@req);e @_h,'Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h parse:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> @body=b;@params=(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c() - headers:(o)->z @_h,o + headers:(o)->e @_h,o status:(n)->@_s=parseInt(n,10) - render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;z h,@_h;@res.writeHead @_s||200,h;@res.end s + render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s haml:(n)->@render haml(App.t[n])(@) ejs:(n,o)->@render ejs.render(App.t[n],o?.locals) puts:(s)->console.log s From a29b9407cb3ea0da4ba078c2fece1a29c1b3a0e3 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 02:36:44 +0200 Subject: [PATCH 11/62] Remove .rvmrc. --- .rvmrc | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 .rvmrc diff --git a/.rvmrc b/.rvmrc deleted file mode 100644 index 137b9bb..0000000 --- a/.rvmrc +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash - -# This is an RVM Project .rvmrc file, used to automatically load the ruby -# development environment upon cd'ing into the directory - -# First we specify our desired [@], the @gemset name is optional. -environment_id="ruby-1.9.2-p180" - -# -# First we attempt to load the desired environment directly from the environment -# file. This is very fast and efficicent compared to running through the entire -# CLI and selector. If you want feedback on which environment was used then -# insert the word 'use' after --create as this triggers verbose mode. -# -if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \ - && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]] ; then - \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id" -else - # If the environment file has not yet been created, use the RVM CLI to select. - rvm --create use "$environment_id" -fi - -# -# If you use an RVM gemset file to install a list of gems (*.gems), you can have -# it be automatically loaded. Uncomment the following and adjust the filename if -# necessary. -# -# filename=".gems" -# if [[ -s "$filename" ]] ; then -# rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d' -# fi - -# -# If you use bundler and would like to run bundle each time you enter the -# directory, you can uncomment the following code. -# -# # Ensure that Bundler is installed. Install it if it is not. -# if ! command -v bundle >/dev/null; then -# printf "The rubygem 'bundler' is not installed. Installing it now.\n" -# gem install bundler -# fi -# -# # Bundle while reducing excess noise. -# printf "Bundling your gems. This may take a few minutes on a fresh clone.\n" -# bundle | grep -v '^Using ' | grep -v ' is complete' | sed '/^$/d' -# - -echo "Many girls want to be carnal with me... because I'm such a premium dancer!" From 83c7f879311a96adb412773c37756cbac5b265a1 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 02:50:46 +0200 Subject: [PATCH 12/62] Remove some unneeded parens. --- almost_sinatra.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 7ee6aff..ca85174 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,4 +1,4 @@ -[u,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,Math,((d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s),(s,c)->s.split(/\s+/).map c] +[u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s),(s,c)->s.split(/\s+/).map c] [http,url,qs,haml,ejs]=w 'http url querystring haml ejs',require class Session @@ -18,13 +18,13 @@ class App headers:(o)->e @_h,o status:(n)->@_s=parseInt(n,10) render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s - haml:(n)->@render haml(App.t[n])(@) - ejs:(n,o)->@render ejs.render(App.t[n],o?.locals) + haml:(n)->@render haml App.t[n],@ + ejs:(n,o)->@render ejs.render App.t[n],o?.locals puts:(s)->console.log s w 'get post put delete patch head options',(v)->App[v]=(p,f)->@r.push [v.toUpperCase(),p,f] -e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen(4567) +e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen p App.handle=(req,res)-> a=new App(req,res);p=url.parse(req.url).pathname;App.b.map (b)->b.call a From 9359f132089e63ca3780d650fb0cc582fb714182 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 03:01:41 +0200 Subject: [PATCH 13/62] Move parens in a helper function. --- almost_sinatra.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index ca85174..ceb3e53 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,4 +1,4 @@ -[u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->(d[k]=if d[k]instanceof Array then d[k].concat v else v) for k,v of s),(s,c)->s.split(/\s+/).map c] +[u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s),(s,c)->s.split(/\s+/).map c] [http,url,qs,haml,ejs]=w 'http url querystring haml ejs',require class Session From e8d6c9084a0c8c48df51f3265cecb5e15daa221b Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 04:03:05 +0200 Subject: [PATCH 14/62] Add path-parameter parsing. --- almost_sinatra.coffee | 17 +++++++++++------ example.coffee | 3 +++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index ceb3e53..7101549 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -12,23 +12,28 @@ Session.get=(r)-> class App [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_h={})->@_u=url.parse(@req.url,true);@session=Session.get(@req);e @_h,'Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h - parse:(c)-> + parse:(p,c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> - @body=b;@params=(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c() + [@params,d]=[{},@_u.pathname.match p[2]];@params[k]=decodeURIComponent d[i+1]for k,i in p[1];@body=b + e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c() headers:(o)->e @_h,o status:(n)->@_s=parseInt(n,10) render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s - haml:(n)->@render haml App.t[n],@ + haml:(n)->@render haml(App.t[n]) @ ejs:(n,o)->@render ejs.render App.t[n],o?.locals puts:(s)->console.log s -w 'get post put delete patch head options',(v)->App[v]=(p,f)->@r.push [v.toUpperCase(),p,f] +w 'get post put delete patch head options',(v)->App[v]=(p,f)-> + o=(p.match(/[\/\.]:[a-z\_\$][a-z0-9\_\$]*/g)||[]).map (s)->s.substr 2 + x=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/:[a-z\_\$][a-z0-9\_\$]*/ig,'([^\\/]+?)')+'$') + @r.push [v.toUpperCase(),o,x,f] e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen p App.handle=(req,res)-> a=new App(req,res);p=url.parse(req.url).pathname;App.b.map (b)->b.call a - b=App.r.filter((r)->r[0]==req.method&&r[1]==p)[0] - if b then(a.parse ()->r=b[2].call a;if typeof r=='string'then a.render r)else(res.writeHead 404,{};res.end()) + b=App.r.filter((r)->r[0]==req.method&&r[2].test p)[0] + if b then(a.parse b,->r=b[3].call a;if typeof r=='string'then a.render r)else(res.writeHead 404,{};res.end()) module.exports=App + diff --git a/example.coffee b/example.coffee index bd47701..a097543 100644 --- a/example.coffee +++ b/example.coffee @@ -15,6 +15,9 @@ app.get '/', -> app.get '/hello', -> @ejs 'hello', locals: {name: @params.name} +app.get '/words/:category/:id.:format', -> + JSON.stringify @params + app.get '/counter', -> @inc_counter() @render @session.counter.toString() From 4cc3c45c747406e32a807e76cfe61533b4df52f1 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 04:08:19 +0200 Subject: [PATCH 15/62] Don't parse URLs twice. --- almost_sinatra.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 7101549..b9fb060 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -31,8 +31,7 @@ w 'get post put delete patch head options',(v)->App[v]=(p,f)-> e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen p App.handle=(req,res)-> - a=new App(req,res);p=url.parse(req.url).pathname;App.b.map (b)->b.call a - b=App.r.filter((r)->r[0]==req.method&&r[2].test p)[0] + a=new App(req,res);App.b.map((b)->b.call a);b=App.r.filter((r)->r[0]==req.method&&r[2].test a._u.pathname)[0] if b then(a.parse b,->r=b[3].call a;if typeof r=='string'then a.render r)else(res.writeHead 404,{};res.end()) module.exports=App From 208f16fd60deeb555cc86630c9be5f5296aa6975 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 04:14:32 +0200 Subject: [PATCH 16/62] Make helper functions visible in HAML and EJS. --- almost_sinatra.coffee | 4 ++-- example.coffee | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index b9fb060..fc8f50d 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,4 +1,4 @@ -[u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s),(s,c)->s.split(/\s+/).map c] +[u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] [http,url,qs,haml,ejs]=w 'http url querystring haml ejs',require class Session @@ -20,7 +20,7 @@ class App status:(n)->@_s=parseInt(n,10) render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s haml:(n)->@render haml(App.t[n]) @ - ejs:(n,o)->@render ejs.render App.t[n],o?.locals + ejs:(n,o)->@render ejs.render App.t[n],e o?.locals||{},App.h puts:(s)->console.log s w 'get post put delete patch head options',(v)->App[v]=(p,f)-> diff --git a/example.coffee b/example.coffee index a097543..b3766b3 100644 --- a/example.coffee +++ b/example.coffee @@ -4,6 +4,9 @@ app.helpers inc_counter: -> @session.counter ||= 0 @session.counter += 1 + + site_name: -> + "Awesome.net" app.before -> @puts "yay! got a request!" @@ -36,12 +39,13 @@ app.template 'index', """ %head %title= title %body + %p= site_name() %a{href: '/hello?name=World'} Say hello! %a{href: '/counter'} Show Counter """ app.template 'hello', """ -Hello <%= name %>! +Hello <%= name %>, welcome to <%= site_name() %>! """ app.run() From a1907544133e647aa1b459b03bbc560e539c6121 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 04:17:01 +0200 Subject: [PATCH 17/62] Let the user pick a port. --- almost_sinatra.coffee | 2 +- example.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index fc8f50d..5605276 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -28,7 +28,7 @@ w 'get post put delete patch head options',(v)->App[v]=(p,f)-> x=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/:[a-z\_\$][a-z0-9\_\$]*/ig,'([^\\/]+?)')+'$') @r.push [v.toUpperCase(),o,x,f] -e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:->http.createServer(@handle).listen p +e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(_p)->http.createServer(@handle).listen _p||p App.handle=(req,res)-> a=new App(req,res);App.b.map((b)->b.call a);b=App.r.filter((r)->r[0]==req.method&&r[2].test a._u.pathname)[0] diff --git a/example.coffee b/example.coffee index b3766b3..3bbb723 100644 --- a/example.coffee +++ b/example.coffee @@ -48,4 +48,4 @@ app.template 'hello', """ Hello <%= name %>, welcome to <%= site_name() %>! """ -app.run() +app.run 4567 From 8698e7be5651e908ef38d18e5330911c1c5e71f8 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 04:17:57 +0200 Subject: [PATCH 18/62] Change port variable name. --- almost_sinatra.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 5605276..311ac46 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -28,7 +28,7 @@ w 'get post put delete patch head options',(v)->App[v]=(p,f)-> x=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/:[a-z\_\$][a-z0-9\_\$]*/ig,'([^\\/]+?)')+'$') @r.push [v.toUpperCase(),o,x,f] -e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(_p)->http.createServer(@handle).listen _p||p +e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@handle).listen q||p App.handle=(req,res)-> a=new App(req,res);App.b.map((b)->b.call a);b=App.r.filter((r)->r[0]==req.method&&r[2].test a._u.pathname)[0] From 0b2412eb9efddfbfcb8eec34d82d848ed9f4d1b7 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 04:31:16 +0200 Subject: [PATCH 19/62] Remove some parens we don't need. --- almost_sinatra.coffee | 4 ++-- example.coffee | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 311ac46..ae8107e 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -6,7 +6,7 @@ class Session @s={} Session.get=(r)-> - [h,c]=[{},(r.headers.cookie||'').split(/\s*;\s*/)];c.map (p)->q=p.split('=');h[q[0]]=q[1] + [h,c]=[{},(r.headers.cookie||'').split /\s*;\s*/];c.map (p)->q=p.split '=';h[q[0]]=q[1] i=h.sessid||[1..4].map(->M.floor(M.random()*M.pow(2,40)).toString 36).join '';@s[i]||=new @(i) class App @@ -17,7 +17,7 @@ class App [@params,d]=[{},@_u.pathname.match p[2]];@params[k]=decodeURIComponent d[i+1]for k,i in p[1];@body=b e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c() headers:(o)->e @_h,o - status:(n)->@_s=parseInt(n,10) + status:(n)->@_s=parseInt n,10 render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s haml:(n)->@render haml(App.t[n]) @ ejs:(n,o)->@render ejs.render App.t[n],e o?.locals||{},App.h diff --git a/example.coffee b/example.coffee index 3bbb723..a3b14b8 100644 --- a/example.coffee +++ b/example.coffee @@ -6,10 +6,10 @@ app.helpers @session.counter += 1 site_name: -> - "Awesome.net" + 'Awesome.net' app.before -> - @puts "yay! got a request!" + @puts 'yay! got a request!' app.get '/', -> @title = 'Almost Sinatra' From efb030427eadd573929323671bcbb40dc3bc9ec7 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 10:49:20 +0200 Subject: [PATCH 20/62] Add WebSocket support. --- almost_sinatra.coffee | 30 +++++++++++++++++------------- example.coffee | 22 ++++++++++++++++++++++ package.json | 2 +- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index ae8107e..1c55beb 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,5 +1,5 @@ -[u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] -[http,url,qs,haml,ejs]=w 'http url querystring haml ejs',require +[u,p,x,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,((r)->r.writeHead 404,{};r.end()),Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] +[http,url,qs,WS,haml,ejs]=w 'http url querystring faye-websocket haml ejs',require class Session constructor:(id)->@__id__=id @@ -11,11 +11,12 @@ Session.get=(r)-> class App [@b,@r,@h,@t]=[[],[],{},{}] - constructor:(@req,@res,@_h={})->@_u=url.parse(@req.url,true);@session=Session.get(@req);e @_h,'Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h - parse:(p,c)-> + constructor:(@req,@res,@_m)-> + @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h + @_b=App.r.filter((r)=>r[0]==(@_m||@req.method)&&r[2].test @_u.pathname)[0];if @_b then([@params,d]=[{},@_u.pathname.match @_b[2]];@params[k]=decodeURIComponent d[i+1]for k,i in @_b[1]) + parse:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> - [@params,d]=[{},@_u.pathname.match p[2]];@params[k]=decodeURIComponent d[i+1]for k,i in p[1];@body=b - e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c() + e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ headers:(o)->e @_h,o status:(n)->@_s=parseInt n,10 render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s @@ -23,16 +24,19 @@ class App ejs:(n,o)->@render ejs.render App.t[n],e o?.locals||{},App.h puts:(s)->console.log s -w 'get post put delete patch head options',(v)->App[v]=(p,f)-> +w 'get post put delete patch head options websocket',(v)->App[v]=(p,f)-> o=(p.match(/[\/\.]:[a-z\_\$][a-z0-9\_\$]*/g)||[]).map (s)->s.substr 2 - x=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/:[a-z\_\$][a-z0-9\_\$]*/ig,'([^\\/]+?)')+'$') - @r.push [v.toUpperCase(),o,x,f] + m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/:[a-z\_\$][a-z0-9\_\$]*/ig,'([^\\/]+?)')+'$') + @r.push [v.toUpperCase(),o,m,f] -e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@handle).listen q||p +e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p -App.handle=(req,res)-> - a=new App(req,res);App.b.map((b)->b.call a);b=App.r.filter((r)->r[0]==req.method&&r[2].test a._u.pathname)[0] - if b then(a.parse b,->r=b[3].call a;if typeof r=='string'then a.render r)else(res.writeHead 404,{};res.end()) +App.call=(req,res)-> + a=new App(req,res);App.b.map((b)->b.call a) + if a._b then(a.parse ->r=@_b[3].call @;if typeof r=='string'then @render r)else x res + +App.ws=(r,s,h)-> + a=new App(r,s,'WEBSOCKET');if a._b then(w=new WS(r,s,h);a._b[3].call a,w;w.addEventListener 'close',->w=null)else x r module.exports=App diff --git a/example.coffee b/example.coffee index a3b14b8..fe855d9 100644 --- a/example.coffee +++ b/example.coffee @@ -15,6 +15,10 @@ app.get '/', -> @title = 'Almost Sinatra' @haml 'index' +app.get '/js/socket.js', -> + @headers 'Content-Type': 'text/javascript' + @ejs 'socket' + app.get '/hello', -> @ejs 'hello', locals: {name: @params.name} @@ -34,6 +38,10 @@ app.options '/', -> @headers 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' @render '' +app.websocket '/ws/:name', (ws) -> + ws.onmessage = (e)=> + ws.send @params.name + ': ' + e.data + app.template 'index', """ %html %head @@ -42,6 +50,20 @@ app.template 'index', """ %p= site_name() %a{href: '/hello?name=World'} Say hello! %a{href: '/counter'} Show Counter + %ul{id: 'log'} + %script{src: '/js/socket.js'} +""" + +app.template 'socket', """ +var ws = new WebSocket('ws://localhost:4567/ws/awesome'); +ws.onmessage = function(e) { + var log = document.getElementById('log'), + li = document.createElement('li'); + li.innerHTML = e.data; + log.appendChild(li); + setTimeout(function() { ws.send('Loop') }, 2000) +}; +ws.onopen = function() { ws.send('Ping!') }; """ app.template 'hello', """ diff --git a/package.json b/package.json index e6d2e74..2664006 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "almost_sinatra", "version": "1.0.0", - "dependencies": {"coffee-script":"", "haml":"", "ejs":""} + "dependencies": {"coffee-script":"", "faye-websocket":"", "haml":"", "ejs":""} } \ No newline at end of file From aa563b7ab1a7ce388704fda70a5d484c9a1d01c1 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 11:24:47 +0200 Subject: [PATCH 21/62] Documentation. --- README.md | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a43328d..ae439b8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,147 @@ # Almost Sinatra - "until programmers stop acting like obfuscation is morally hazardous, - they’re not artists, just kids who don’t want their food to touch." - _why + “until programmers stop acting like obfuscation is morally hazardous, + they’re not artists, just kids who don’t want their food to touch.” - _why -A Sinatra is ROFLscale. +I ported @rkh's `almost-sinatra` to CoffeeScript, for the lolz, and then I got +carried away. Berlin is a strange and wonderful place. + +So, what can this version do? + +* Route requests based on method and pathname +* Respond to `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` +* Handle WebSocket connections +* Construct params from path data, query strings and entity bodies +* Has in-memory session support +* Set status code and headers +* Render EJS, HAML and raw text +* Define helper functions for request handlers and templates + + +## Run the example + + git clone git://github.com/jcoglan/almost-sinatra.git + cd almost-sinatra + git checkout coffee + npm install + ./node_modules/.bin/coffee example.coffee + + +### Hello world + + app = require './almost_sinatra' + + app.get '/', -> 'Hello, world!' + + app.run 4567 + + +### Handling HTTP methods, statuses, headers, etc. + + app.get '/confs/:name', -> + JSON.stringify @params + + app.post '/confs', -> + @status 201 + @headers 'Content-Type: application/json' + JSON.stringify @params + + app.options '/', -> + @headers + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' + @render '' + +This app responds like so: + + $ curl 'localhost:4567/confs/eurucamp?bears=awesome' + {"name":"eurucamp","bears":"awesome"} + + $ curl -iX POST localhost:4567/confs -d 'horses=fake' + HTTP/1.1 201 Created + Content-Type: application/json + Content-Length: 17 + Set-Cookie: sessid=cb3npo8wcb66t62o88nps16onxl2g3k; Path=/; HttpOnly + Date: Sun, 19 Aug 2012 09:13:00 GMT + Connection: keep-alive + + {"horses":"fake"} + + $ curl -iX OPTIONS localhost:4567/ + HTTP/1.1 200 OK + Content-Type: text/html + Content-Length: 0 + Set-Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s; Path=/; HttpOnly + Access-Control-Allow-Origin: * + Access-Control-Allow-Methods: GET, PUT, DELETE + Date: Sun, 19 Aug 2012 09:13:42 GMT + Connection: keep-alive + + +### Wait, are those session cookies? + + app.get '/counter', -> + @session.counter ||= 0 + @session.counter += 1 + @render @session.counter.toString() + +Try it out: + + $ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' + 1 + $ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' + 2 + $ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' + 3 + + +### Helpers, EJS and HAML + + app.helpers + site_name: -> 'Awesome.net' + + app.template 'hello', """ + Hello <%= name %>, welcome to <%= site_name() %>! + """ + + app.get '/hello', -> + @ejs 'hello', locals: {name: @params.name} + +You can use `@ejs` or `@haml` to render a template. `@render` just takes a +string and writes it to the response body. If your route handler returns a +string, that string will be the body. Otherwise, you must call a rendering +function. + + $ curl 'localhost:4567/hello?name=_why' + Hello _why, welcome to Awesome.net! + + +### WebSockets! + + app.websocket '/ws/:name', (ws) -> + ws.onmessage = (e)=> + ws.send @params.name + ': ' + e.data + + +## License + +(The MIT License) + +Copyright (c) 2012 James Coglan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 083c14d419a18f4b79875be13b1b7d2059eb4916 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 11:44:26 +0200 Subject: [PATCH 22/62] Add EventSource support. --- README.md | 8 +++++++- almost_sinatra.coffee | 13 ++++++++----- example.coffee | 18 +++++++++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ae439b8..6485d87 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ So, what can this version do? * Route requests based on method and pathname * Respond to `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` -* Handle WebSocket connections +* Handle WebSocket and EventSource connections * Construct params from path data, query strings and entity bodies * Has in-memory session support * Set status code and headers @@ -123,6 +123,12 @@ function. ws.send @params.name + ': ' + e.data +### EventSource! + + app.eventsource '/ws/:name', (es) -> + setInterval (=> es.send @params.name + ': PUSH!'), 5000 + + ## License (The MIT License) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 1c55beb..04c4cf4 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -13,7 +13,7 @@ class App [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_m)-> @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h - @_b=App.r.filter((r)=>r[0]==(@_m||@req.method)&&r[2].test @_u.pathname)[0];if @_b then([@params,d]=[{},@_u.pathname.match @_b[2]];@params[k]=decodeURIComponent d[i+1]for k,i in @_b[1]) + @_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0];if @_b then([@params,d]=[{},@_u.pathname.match @_b[2]];@params[k]=decodeURIComponent d[i+1]for k,i in @_b[1]) parse:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ @@ -24,7 +24,7 @@ class App ejs:(n,o)->@render ejs.render App.t[n],e o?.locals||{},App.h puts:(s)->console.log s -w 'get post put delete patch head options websocket',(v)->App[v]=(p,f)-> +w 'get post put delete patch head options websocket eventsource',(v)->App[v]=(p,f)-> o=(p.match(/[\/\.]:[a-z\_\$][a-z0-9\_\$]*/g)||[]).map (s)->s.substr 2 m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/:[a-z\_\$][a-z0-9\_\$]*/ig,'([^\\/]+?)')+'$') @r.push [v.toUpperCase(),o,m,f] @@ -32,11 +32,14 @@ w 'get post put delete patch head options websocket',(v)->App[v]=(p,f)-> e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p App.call=(req,res)-> - a=new App(req,res);App.b.map((b)->b.call a) - if a._b then(a.parse ->r=@_b[3].call @;if typeof r=='string'then @render r)else x res + ES=WS.EventSource;k=ES.isEventSource req;a=new App(req,res,if k then 'EVENTSOURCE' else req.method);App.b.map((b)->b.call a) + if k + if a._b then(s=new ES req,res;a._b[3].call a,s;s.addEventListener 'close',->s=null)else x res + else + if a._b then(a.parse ->r=@_b[3].call @;if typeof r=='string'then @render r)else x res App.ws=(r,s,h)-> - a=new App(r,s,'WEBSOCKET');if a._b then(w=new WS(r,s,h);a._b[3].call a,w;w.addEventListener 'close',->w=null)else x r + a=new App(r,s,'WEBSOCKET');if a._b then(w=new WS r,s,h;a._b[3].call a,w;w.addEventListener 'close',->w=null)else x r module.exports=App diff --git a/example.coffee b/example.coffee index fe855d9..a9217b7 100644 --- a/example.coffee +++ b/example.coffee @@ -42,6 +42,9 @@ app.websocket '/ws/:name', (ws) -> ws.onmessage = (e)=> ws.send @params.name + ': ' + e.data +app.eventsource '/ws/:name', (es) -> + setInterval (=> es.send @params.name + ': PUSH!'), 5000 + app.template 'index', """ %html %head @@ -55,15 +58,24 @@ app.template 'index', """ """ app.template 'socket', """ -var ws = new WebSocket('ws://localhost:4567/ws/awesome'); -ws.onmessage = function(e) { +var write = function(data) { var log = document.getElementById('log'), li = document.createElement('li'); - li.innerHTML = e.data; + li.innerHTML = data; log.appendChild(li); +}; + +var ws = new WebSocket('ws://localhost:4567/ws/awesome'); +ws.onmessage = function(e) { + write(e.data); setTimeout(function() { ws.send('Loop') }, 2000) }; ws.onopen = function() { ws.send('Ping!') }; + +var es = new EventSource('http://localhost:4567/ws/awesome'); +es.onmessage = function(e) { + write(e.data); +}; """ app.template 'hello', """ From 31b023564940e2263fd344720d83d5448ee860c9 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 11:51:57 +0200 Subject: [PATCH 23/62] Include query string params in WebSocket and EventSource handlers. --- almost_sinatra.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 04c4cf4..c9f26a9 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -13,7 +13,7 @@ class App [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_m)-> @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h - @_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0];if @_b then([@params,d]=[{},@_u.pathname.match @_b[2]];@params[k]=decodeURIComponent d[i+1]for k,i in @_b[1]) + @_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0];if @_b then([@params,d]=[{},@_u.pathname.match @_b[2]];@params[k]=decodeURIComponent d[i+1]for k,i in @_b[1]);e @params,@_u.query parse:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ From 79e872680135ccedf5f5a43f8fba5ea25843d93a Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 12:08:59 +0200 Subject: [PATCH 24/62] Golf the request-dispatching code a bit. --- almost_sinatra.coffee | 9 +++++---- example.coffee | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index c9f26a9..9962b84 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,4 +1,4 @@ -[u,p,x,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,((r)->r.writeHead 404,{};r.end()),Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] +[u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] [http,url,qs,WS,haml,ejs]=w 'http url querystring faye-websocket haml ejs',require class Session @@ -17,6 +17,7 @@ class App parse:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ + _x:(c)->if @_b then c.call @,((a)=>@_b[3].call @,a) else(@res.writeHead 404,{};@res.end()) headers:(o)->e @_h,o status:(n)->@_s=parseInt n,10 render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s @@ -34,12 +35,12 @@ e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),ru App.call=(req,res)-> ES=WS.EventSource;k=ES.isEventSource req;a=new App(req,res,if k then 'EVENTSOURCE' else req.method);App.b.map((b)->b.call a) if k - if a._b then(s=new ES req,res;a._b[3].call a,s;s.addEventListener 'close',->s=null)else x res + a._x (h)->s=new ES req,res;h s;s.addEventListener 'close',->s=null else - if a._b then(a.parse ->r=@_b[3].call @;if typeof r=='string'then @render r)else x res + a._x (h)->a.parse ->r=h();if typeof r=='string'then @render r App.ws=(r,s,h)-> - a=new App(r,s,'WEBSOCKET');if a._b then(w=new WS r,s,h;a._b[3].call a,w;w.addEventListener 'close',->w=null)else x r + a=new App(r,s,'WEBSOCKET');a._x (H)->w=new WS r,s,h;H w;w.addEventListener 'close',->w=null module.exports=App diff --git a/example.coffee b/example.coffee index a9217b7..60a1f61 100644 --- a/example.coffee +++ b/example.coffee @@ -32,7 +32,7 @@ app.get '/counter', -> app.post '/', -> @status 201 @headers 'Content-Type': 'application/json' - JSON.stringify(@params) + JSON.stringify @params app.options '/', -> @headers 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' @@ -72,7 +72,7 @@ ws.onmessage = function(e) { }; ws.onopen = function() { ws.send('Ping!') }; -var es = new EventSource('http://localhost:4567/ws/awesome'); +var es = new EventSource('http://localhost:4567/ws/eurucamp'); es.onmessage = function(e) { write(e.data); }; From 9a69b43e8c749fb206a9bab26472c43b4d84ca73 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 13:48:56 +0200 Subject: [PATCH 25/62] Correct Content-Type header in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6485d87..fa9fac3 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ So, what can this version do? app.post '/confs', -> @status 201 - @headers 'Content-Type: application/json' + @headers 'Content-Type': 'application/json' JSON.stringify @params app.options '/', -> From 125926d622a567be2205cb57a22cab79ba611a58 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 14:46:17 +0200 Subject: [PATCH 26/62] Support params.splat. --- README.md | 7 +++++++ almost_sinatra.coffee | 8 ++++---- example.coffee | 3 +++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fa9fac3..6a17904 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ So, what can this version do? * Route requests based on method and pathname * Respond to `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` * Handle WebSocket and EventSource connections +* Path-based pattern matching with `:named` and `*` matchers * Construct params from path data, query strings and entity bodies * Has in-memory session support * Set status code and headers @@ -41,6 +42,9 @@ So, what can this version do? app.get '/confs/:name', -> JSON.stringify @params + app.get '/download/*.*', -> + @params.splat.join ', ' + app.post '/confs', -> @status 201 @headers 'Content-Type': 'application/json' @@ -57,6 +61,9 @@ This app responds like so: $ curl 'localhost:4567/confs/eurucamp?bears=awesome' {"name":"eurucamp","bears":"awesome"} + $ curl localhost:4567/download/foo.js + foo, js + $ curl -iX POST localhost:4567/confs -d 'horses=fake' HTTP/1.1 201 Created Content-Type: application/json diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 9962b84..7f28ea7 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -12,8 +12,8 @@ Session.get=(r)-> class App [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_m)-> - @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h - @_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0];if @_b then([@params,d]=[{},@_u.pathname.match @_b[2]];@params[k]=decodeURIComponent d[i+1]for k,i in @_b[1]);e @params,@_u.query + @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h;@_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] + if @_b then([@params,d]=[{splat:[]},@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1]);e @params,@_u.query parse:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ @@ -26,8 +26,8 @@ class App puts:(s)->console.log s w 'get post put delete patch head options websocket eventsource',(v)->App[v]=(p,f)-> - o=(p.match(/[\/\.]:[a-z\_\$][a-z0-9\_\$]*/g)||[]).map (s)->s.substr 2 - m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/:[a-z\_\$][a-z0-9\_\$]*/ig,'([^\\/]+?)')+'$') + o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map (s)->s.replace /^[^a-z0-9\_\$\*]*/, '' + m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$') @r.push [v.toUpperCase(),o,m,f] e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p diff --git a/example.coffee b/example.coffee index 60a1f61..df21b9a 100644 --- a/example.coffee +++ b/example.coffee @@ -15,6 +15,9 @@ app.get '/', -> @title = 'Almost Sinatra' @haml 'index' +app.get '/download/*.*', -> + @params.splat.join ', ' + app.get '/js/socket.js', -> @headers 'Content-Type': 'text/javascript' @ejs 'socket' From 2f4b339ecbbd0458c834322cd614eaed617a8524 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 15:14:16 +0200 Subject: [PATCH 27/62] Pass params into the request handlers as arguments. --- README.md | 16 +++++++++++----- almost_sinatra.coffee | 8 ++++---- example.coffee | 13 ++++++++----- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6a17904..b0f3a0e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ So, what can this version do? app.get '/confs/:name', -> JSON.stringify @params + app.put '/confs/:name', (name) -> + name + app.get '/download/*.*', -> @params.splat.join ', ' @@ -61,6 +64,9 @@ This app responds like so: $ curl 'localhost:4567/confs/eurucamp?bears=awesome' {"name":"eurucamp","bears":"awesome"} + $ curl -X PUT localhost:4567/confs/jsconf + jsconf + $ curl localhost:4567/download/foo.js foo, js @@ -125,15 +131,15 @@ function. ### WebSockets! - app.websocket '/ws/:name', (ws) -> - ws.onmessage = (e)=> - ws.send @params.name + ': ' + e.data + app.websocket '/ws/:name', -> + @socket.onmessage = (e)=> + @socket.send @params.name + ': ' + e.data ### EventSource! - app.eventsource '/ws/:name', (es) -> - setInterval (=> es.send @params.name + ': PUSH!'), 5000 + app.eventsource '/ws/:name', -> + setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 ## License diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 7f28ea7..9c1c8b0 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -13,11 +13,11 @@ class App [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_m)-> @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h;@_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] - if @_b then([@params,d]=[{splat:[]},@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1]);e @params,@_u.query + if @_b then([@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1]);e @params,@_u.query parse:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ - _x:(c)->if @_b then c.call @,((a)=>@_b[3].call @,a) else(@res.writeHead 404,{};@res.end()) + _x:(c)->if @_b then c.call @,(=>@_b[3].apply @,@_a) else(@res.writeHead 404,{};@res.end()) headers:(o)->e @_h,o status:(n)->@_s=parseInt n,10 render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s @@ -35,12 +35,12 @@ e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),ru App.call=(req,res)-> ES=WS.EventSource;k=ES.isEventSource req;a=new App(req,res,if k then 'EVENTSOURCE' else req.method);App.b.map((b)->b.call a) if k - a._x (h)->s=new ES req,res;h s;s.addEventListener 'close',->s=null + a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null else a._x (h)->a.parse ->r=h();if typeof r=='string'then @render r App.ws=(r,s,h)-> - a=new App(r,s,'WEBSOCKET');a._x (H)->w=new WS r,s,h;H w;w.addEventListener 'close',->w=null + a=new App(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null module.exports=App diff --git a/example.coffee b/example.coffee index df21b9a..f9be29b 100644 --- a/example.coffee +++ b/example.coffee @@ -18,6 +18,9 @@ app.get '/', -> app.get '/download/*.*', -> @params.splat.join ', ' +app.put '/download/*.*', (path, ext) -> + [path, ext].join ' // ' + app.get '/js/socket.js', -> @headers 'Content-Type': 'text/javascript' @ejs 'socket' @@ -41,12 +44,12 @@ app.options '/', -> @headers 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' @render '' -app.websocket '/ws/:name', (ws) -> - ws.onmessage = (e)=> - ws.send @params.name + ': ' + e.data +app.websocket '/ws/:name', -> + @socket.onmessage = (e)=> + @socket.send @params.name + ': ' + e.data -app.eventsource '/ws/:name', (es) -> - setInterval (=> es.send @params.name + ': PUSH!'), 5000 +app.eventsource '/ws/:name', -> + setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 app.template 'index', """ %html From b3749898ca60a42a8a4d12d81dd1f0cffbec34dc Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sun, 19 Aug 2012 15:48:12 +0200 Subject: [PATCH 28/62] Lose 4 characters. --- almost_sinatra.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 9c1c8b0..40d52f4 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -13,11 +13,11 @@ class App [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_m)-> @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h;@_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] - if @_b then([@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1]);e @params,@_u.query + if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,@_u.query parse:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ - _x:(c)->if @_b then c.call @,(=>@_b[3].apply @,@_a) else(@res.writeHead 404,{};@res.end()) + _x:(c)->if @_b then c.call @,(=>@_b[3].apply @,@_a)else @res.writeHead 404,{};@res.end() headers:(o)->e @_h,o status:(n)->@_s=parseInt n,10 render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s From 6bbf935f5b5437c77fa1b8d2991043efe0c48080 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 03:29:17 +0200 Subject: [PATCH 29/62] Highlight code in the readme appropriately. --- README.md | 186 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 103 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index b0f3a0e..b810261 100644 --- a/README.md +++ b/README.md @@ -21,125 +21,145 @@ So, what can this version do? ## Run the example - git clone git://github.com/jcoglan/almost-sinatra.git - cd almost-sinatra - git checkout coffee - npm install - ./node_modules/.bin/coffee example.coffee +```bash +git clone git://github.com/jcoglan/almost-sinatra.git +cd almost-sinatra +git checkout coffee +npm install +./node_modules/.bin/coffee example.coffee +``` ### Hello world - app = require './almost_sinatra' - - app.get '/', -> 'Hello, world!' - - app.run 4567 +```coffee +app = require './almost_sinatra' + +app.get '/', -> 'Hello, world!' + +app.run 4567 +``` ### Handling HTTP methods, statuses, headers, etc. - app.get '/confs/:name', -> - JSON.stringify @params - - app.put '/confs/:name', (name) -> - name - - app.get '/download/*.*', -> - @params.splat.join ', ' - - app.post '/confs', -> - @status 201 - @headers 'Content-Type': 'application/json' - JSON.stringify @params - - app.options '/', -> - @headers - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' - @render '' +```coffee +app.get '/confs/:name', -> + JSON.stringify @params + +app.put '/confs/:name', (name) -> + name + +app.get '/download/*.*', -> + @params.splat.join ', ' + +app.post '/confs', -> + @status 201 + @headers 'Content-Type': 'application/json' + JSON.stringify @params + +app.options '/', -> + @headers + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' + @render '' +``` This app responds like so: - $ curl 'localhost:4567/confs/eurucamp?bears=awesome' - {"name":"eurucamp","bears":"awesome"} - - $ curl -X PUT localhost:4567/confs/jsconf - jsconf - - $ curl localhost:4567/download/foo.js - foo, js - - $ curl -iX POST localhost:4567/confs -d 'horses=fake' - HTTP/1.1 201 Created - Content-Type: application/json - Content-Length: 17 - Set-Cookie: sessid=cb3npo8wcb66t62o88nps16onxl2g3k; Path=/; HttpOnly - Date: Sun, 19 Aug 2012 09:13:00 GMT - Connection: keep-alive - - {"horses":"fake"} - - $ curl -iX OPTIONS localhost:4567/ - HTTP/1.1 200 OK - Content-Type: text/html - Content-Length: 0 - Set-Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s; Path=/; HttpOnly - Access-Control-Allow-Origin: * - Access-Control-Allow-Methods: GET, PUT, DELETE - Date: Sun, 19 Aug 2012 09:13:42 GMT - Connection: keep-alive +```bash +$ curl 'localhost:4567/confs/eurucamp?bears=awesome' +{"name":"eurucamp","bears":"awesome"} + +$ curl -X PUT localhost:4567/confs/jsconf +jsconf + +$ curl localhost:4567/download/foo.js +foo, js + +$ curl -iX POST localhost:4567/confs -d 'horses=fake' +HTTP/1.1 201 Created +Content-Type: application/json +Content-Length: 17 +Set-Cookie: sessid=cb3npo8wcb66t62o88nps16onxl2g3k; Path=/; HttpOnly +Date: Sun, 19 Aug 2012 09:13:00 GMT +Connection: keep-alive + +{"horses":"fake"} + +$ curl -iX OPTIONS localhost:4567/ +HTTP/1.1 200 OK +Content-Type: text/html +Content-Length: 0 +Set-Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s; Path=/; HttpOnly +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, PUT, DELETE +Date: Sun, 19 Aug 2012 09:13:42 GMT +Connection: keep-alive +``` ### Wait, are those session cookies? - app.get '/counter', -> - @session.counter ||= 0 - @session.counter += 1 - @render @session.counter.toString() +```coffee +app.get '/counter', -> + @session.counter ||= 0 + @session.counter += 1 + @render @session.counter.toString() +``` Try it out: - $ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' - 1 - $ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' - 2 - $ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' - 3 +```bash +$ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' +1 +$ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' +2 +$ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' +3 +``` ### Helpers, EJS and HAML - app.helpers - site_name: -> 'Awesome.net' - - app.template 'hello', """ - Hello <%= name %>, welcome to <%= site_name() %>! - """ - - app.get '/hello', -> - @ejs 'hello', locals: {name: @params.name} +```coffee +app.helpers + site_name: -> 'Awesome.net' + +app.template 'hello', """ +Hello <%= name %>, welcome to <%= site_name() %>! +""" + +app.get '/hello', -> + @ejs 'hello', locals: {name: @params.name} +``` You can use `@ejs` or `@haml` to render a template. `@render` just takes a string and writes it to the response body. If your route handler returns a string, that string will be the body. Otherwise, you must call a rendering function. - $ curl 'localhost:4567/hello?name=_why' - Hello _why, welcome to Awesome.net! +```bash +$ curl 'localhost:4567/hello?name=_why' +Hello _why, welcome to Awesome.net! +``` ### WebSockets! - app.websocket '/ws/:name', -> - @socket.onmessage = (e)=> - @socket.send @params.name + ': ' + e.data +```coffee +app.websocket '/ws/:name', -> + @socket.onmessage = (e)=> + @socket.send @params.name + ': ' + e.data +``` ### EventSource! - app.eventsource '/ws/:name', -> - setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 +```coffee +app.eventsource '/ws/:name', -> + setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 +``` ## License From 305e4693fa02fe26da9b7ebf0f93562f300878a8 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 03:37:07 +0200 Subject: [PATCH 30/62] Example code for accessing the session in a WebSocket handler. --- example.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example.coffee b/example.coffee index f9be29b..7bf40d6 100644 --- a/example.coffee +++ b/example.coffee @@ -46,7 +46,8 @@ app.options '/', -> app.websocket '/ws/:name', -> @socket.onmessage = (e)=> - @socket.send @params.name + ': ' + e.data + @inc_counter() + @socket.send @params.name + ': ' + e.data + ': ' + @session.counter app.eventsource '/ws/:name', -> setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 From dd04d84fa2db0613893c3e9873eecd5fbbfad6ef Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 10:53:01 +0200 Subject: [PATCH 31/62] Reduce line count by 10. --- almost_sinatra.coffee | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 40d52f4..9df7ea8 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -5,42 +5,32 @@ class Session constructor:(id)->@__id__=id @s={} -Session.get=(r)-> - [h,c]=[{},(r.headers.cookie||'').split /\s*;\s*/];c.map (p)->q=p.split '=';h[q[0]]=q[1] - i=h.sessid||[1..4].map(->M.floor(M.random()*M.pow(2,40)).toString 36).join '';@s[i]||=new @(i) +Session.get=(r)->[h,c]=[{},(r.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);i=h.sessid||[1..4].map(->M.floor(M.random()*M.pow(2,40)).toString 36).join '';@s[i]||=new @(i) class App [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_m)-> @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h;@_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,@_u.query - parse:(c)-> + _p:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ _x:(c)->if @_b then c.call @,(=>@_b[3].apply @,@_a)else @res.writeHead 404,{};@res.end() - headers:(o)->e @_h,o - status:(n)->@_s=parseInt n,10 render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s - haml:(n)->@render haml(App.t[n]) @ - ejs:(n,o)->@render ejs.render App.t[n],e o?.locals||{},App.h - puts:(s)->console.log s + +e App.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(App.t[n]) @),ejs:((n,o)->@render ejs.render App.t[n],e o?.locals||{},App.h),puts:(s)->console.log s w 'get post put delete patch head options websocket eventsource',(v)->App[v]=(p,f)-> o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map (s)->s.replace /^[^a-z0-9\_\$\*]*/, '' - m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$') - @r.push [v.toUpperCase(),o,m,f] + m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p App.call=(req,res)-> ES=WS.EventSource;k=ES.isEventSource req;a=new App(req,res,if k then 'EVENTSOURCE' else req.method);App.b.map((b)->b.call a) - if k - a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null - else - a._x (h)->a.parse ->r=h();if typeof r=='string'then @render r + if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->a._p ->r=h();if typeof r=='string'then @render r) -App.ws=(r,s,h)-> - a=new App(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null +App.ws=(r,s,h)->a=new App(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null module.exports=App From 86ece348ac19757ec400a9aaedc4ebe63716c5fb Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 11:18:23 +0200 Subject: [PATCH 32/62] Single-letter class names. --- almost_sinatra.coffee | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 9df7ea8..1995dd1 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,16 +1,16 @@ [u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] [http,url,qs,WS,haml,ejs]=w 'http url querystring faye-websocket haml ejs',require -class Session +class S constructor:(id)->@__id__=id @s={} -Session.get=(r)->[h,c]=[{},(r.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);i=h.sessid||[1..4].map(->M.floor(M.random()*M.pow(2,40)).toString 36).join '';@s[i]||=new @(i) +S.get=(r)->[h,c]=[{},(r.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);i=h.sessid||[1..4].map(->M.floor(M.random()*M.pow(2,40)).toString 36).join '';@s[i]||=new @(i) -class App +class A [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_m)-> - @_u=url.parse(@req.url,true);@session=Session.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,App.h;@_b=App.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] + @_u=url.parse(@req.url,true);@session=S.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,@_u.query _p:(c)-> @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> @@ -18,19 +18,19 @@ class App _x:(c)->if @_b then c.call @,(=>@_b[3].apply @,@_a)else @res.writeHead 404,{};@res.end() render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s -e App.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(App.t[n]) @),ejs:((n,o)->@render ejs.render App.t[n],e o?.locals||{},App.h),puts:(s)->console.log s +e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s -w 'get post put delete patch head options websocket eventsource',(v)->App[v]=(p,f)-> +w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map (s)->s.replace /^[^a-z0-9\_\$\*]*/, '' m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] -e App,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p +e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p -App.call=(req,res)-> - ES=WS.EventSource;k=ES.isEventSource req;a=new App(req,res,if k then 'EVENTSOURCE' else req.method);App.b.map((b)->b.call a) +A.call=(req,res)-> + ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);A.b.map((b)->b.call a) if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->a._p ->r=h();if typeof r=='string'then @render r) -App.ws=(r,s,h)->a=new App(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null +A.ws=(r,s,h)->a=new A(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null -module.exports=App +module.exports=A From 0a691f6e1940a07bd84eac0cb9baab8612eb92eb Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 12:59:07 +0200 Subject: [PATCH 33/62] Use self-contained sessions. --- README.md | 15 ++------------- almost_sinatra.coffee | 27 ++++++++++----------------- example.coffee | 5 +++-- package.json | 2 +- 4 files changed, 16 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index b810261..23f587f 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ $ curl -iX POST localhost:4567/confs -d 'horses=fake' HTTP/1.1 201 Created Content-Type: application/json Content-Length: 17 -Set-Cookie: sessid=cb3npo8wcb66t62o88nps16onxl2g3k; Path=/; HttpOnly +Set-Cookie: session=XABfKjq2xvCavSitaxu0BC9XSl...; Path=/; HttpOnly Date: Sun, 19 Aug 2012 09:13:00 GMT Connection: keep-alive @@ -91,7 +91,7 @@ $ curl -iX OPTIONS localhost:4567/ HTTP/1.1 200 OK Content-Type: text/html Content-Length: 0 -Set-Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s; Path=/; HttpOnly +Set-Cookie: session=h7HwIkvXA0xcZk7JhmoVK25qNH...; Path=/; HttpOnly Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, PUT, DELETE Date: Sun, 19 Aug 2012 09:13:42 GMT @@ -108,17 +108,6 @@ app.get '/counter', -> @render @session.counter.toString() ``` -Try it out: - -```bash -$ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' -1 -$ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' -2 -$ curl localhost:4567/counter -H 'Cookie: sessid=6xbt2xogcbo6f5z48h153k74dvy4j01s' -3 -``` - ### Helpers, EJS and HAML diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 1995dd1..d5a68db 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,22 +1,14 @@ -[u,p,M,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] -[http,url,qs,WS,haml,ejs]=w 'http url querystring faye-websocket haml ejs',require - -class S - constructor:(id)->@__id__=id - @s={} - -S.get=(r)->[h,c]=[{},(r.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);i=h.sessid||[1..4].map(->M.floor(M.random()*M.pow(2,40)).toString 36).join '';@s[i]||=new @(i) +[u,p,M,J,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,JSON,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] +[http,url,qs,WS,E,haml,ejs]=w 'http url querystring faye-websocket vault/node/aes haml ejs',require class A [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@req,@res,@_m)-> - @_u=url.parse(@req.url,true);@session=S.get(@req);@_h='Set-Cookie':['sessid='+@session.__id__+'; Path=/; HttpOnly'];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] - if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,@_u.query - _p:(c)-> - @req.setEncoding('utf8');b='';@req.on('data',(s)->b+=s);@req.on 'end',=> - e @params,(if u.test @req.headers['content-type']then qs.parse b else @_u.query);c.call @ - _x:(c)->if @_b then c.call @,(=>@_b[3].apply @,@_a)else @res.writeHead 404,{};@res.end() - render:(s)->h='Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s + [h,c]=[{},(@req.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);@cookies=h;new E(A.session_secret).decrypt h.session||'',(x,j)=> + @session=(if x then{}else J.parse(j));@_u=url.parse(@req.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] + if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if u.test @req.headers['content-type']then qs.parse @req.body else @_u.query);@_c() + _x:(c)->@_c= =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @res.writeHead 404,{};@res.end() + render:(s)->h=new E(A.session_secret).encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s @@ -27,8 +19,9 @@ w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f) e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p A.call=(req,res)-> - ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);A.b.map((b)->b.call a) - if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->a._p ->r=h();if typeof r=='string'then @render r) + req.setEncoding 'utf8';b='';req.on('data',(s)->b+=s);req.on 'end',-> + req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);A.b.map((b)->b.call a) + if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) A.ws=(r,s,h)->a=new A(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null diff --git a/example.coffee b/example.coffee index 7bf40d6..50867dc 100644 --- a/example.coffee +++ b/example.coffee @@ -1,5 +1,7 @@ app = require './almost_sinatra' +app.session_secret = 'abc123' + app.helpers inc_counter: -> @session.counter ||= 0 @@ -46,8 +48,7 @@ app.options '/', -> app.websocket '/ws/:name', -> @socket.onmessage = (e)=> - @inc_counter() - @socket.send @params.name + ': ' + e.data + ': ' + @session.counter + @socket.send @params.name + ': ' + e.data app.eventsource '/ws/:name', -> setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 diff --git a/package.json b/package.json index 2664006..b55c1a7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "almost_sinatra", "version": "1.0.0", - "dependencies": {"coffee-script":"", "faye-websocket":"", "haml":"", "ejs":""} + "dependencies": {"coffee-script":"", "faye-websocket":"", "vault": "", "haml":"", "ejs":""} } \ No newline at end of file From 95b06b6fc1bb15d7c7dab5140b469e3297d3a726 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 12:59:49 +0200 Subject: [PATCH 34/62] Correct description of sessions in readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23f587f..4678dc1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ So, what can this version do? * Handle WebSocket and EventSource connections * Path-based pattern matching with `:named` and `*` matchers * Construct params from path data, query strings and entity bodies -* Has in-memory session support +* Self-contained encrypted session cookies * Set status code and headers * Render EJS, HAML and raw text * Define helper functions for request handlers and templates From f044309c3dd2a4dad53ac42a046e04d31d52108a Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 13:16:50 +0200 Subject: [PATCH 35/62] Make before-filters happen when the request is fully processed, and make them async-chained. --- almost_sinatra.coffee | 6 +++--- example.coffee | 3 ++- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index d5a68db..6074797 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,5 +1,5 @@ [u,p,M,J,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,JSON,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] -[http,url,qs,WS,E,haml,ejs]=w 'http url querystring faye-websocket vault/node/aes haml ejs',require +[http,url,qs,async,WS,E,haml,ejs]=w 'http url querystring async faye-websocket vault/node/aes haml ejs',require class A [@b,@r,@h,@t]=[[],[],{},{}] @@ -7,7 +7,7 @@ class A [h,c]=[{},(@req.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);@cookies=h;new E(A.session_secret).decrypt h.session||'',(x,j)=> @session=(if x then{}else J.parse(j));@_u=url.parse(@req.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if u.test @req.headers['content-type']then qs.parse @req.body else @_u.query);@_c() - _x:(c)->@_c= =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @res.writeHead 404,{};@res.end() + _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @res.writeHead 404,{};@res.end() render:(s)->h=new E(A.session_secret).encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s @@ -20,7 +20,7 @@ e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run: A.call=(req,res)-> req.setEncoding 'utf8';b='';req.on('data',(s)->b+=s);req.on 'end',-> - req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);A.b.map((b)->b.call a) + req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method) if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) A.ws=(r,s,h)->a=new A(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null diff --git a/example.coffee b/example.coffee index 50867dc..4a4e8d2 100644 --- a/example.coffee +++ b/example.coffee @@ -10,8 +10,9 @@ app.helpers site_name: -> 'Awesome.net' -app.before -> +app.before (next) -> @puts 'yay! got a request!' + next() app.get '/', -> @title = 'Almost Sinatra' diff --git a/package.json b/package.json index b55c1a7..04ac6b2 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "almost_sinatra", "version": "1.0.0", - "dependencies": {"coffee-script":"", "faye-websocket":"", "vault": "", "haml":"", "ejs":""} + "dependencies": {"coffee-script":"", "async":"", "faye-websocket":"", "vault": "", "haml":"", "ejs":""} } \ No newline at end of file From 699c133802f149bb2550e4fb0c335e8cb9518b26 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 13:19:44 +0200 Subject: [PATCH 36/62] Use full words @request and @response. --- almost_sinatra.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 6074797..396baec 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -3,12 +3,12 @@ class A [@b,@r,@h,@t]=[[],[],{},{}] - constructor:(@req,@res,@_m)-> - [h,c]=[{},(@req.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);@cookies=h;new E(A.session_secret).decrypt h.session||'',(x,j)=> - @session=(if x then{}else J.parse(j));@_u=url.parse(@req.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] - if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if u.test @req.headers['content-type']then qs.parse @req.body else @_u.query);@_c() - _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @res.writeHead 404,{};@res.end() - render:(s)->h=new E(A.session_secret).encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@res.writeHead @_s||200,h;@res.end s + constructor:(@request,@response,@_m)-> + [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);@cookies=h;new E(A.session_secret).decrypt h.session||'',(x,j)=> + @session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] + if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if u.test @request.headers['content-type']then qs.parse @request.body else @_u.query);@_c() + _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @response.writeHead 404,{};@response.end() + render:(s)->h=new E(A.session_secret).encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s From d06b9c154941855a711fcb1cb5c567b4d6b8aa51 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 13:21:36 +0200 Subject: [PATCH 37/62] Merge two lines. --- almost_sinatra.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 396baec..f530fbb 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -13,8 +13,7 @@ class A e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> - o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map (s)->s.replace /^[^a-z0-9\_\$\*]*/, '' - m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] + o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/, '');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p From c23db3cae2aceb20d83c5b9fdc30c7ec3b32b351 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 13:24:12 +0200 Subject: [PATCH 38/62] Shouldn't be a space between args. --- almost_sinatra.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index f530fbb..c3d493d 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -13,7 +13,7 @@ class A e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> - o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/, '');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] + o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p From 92cf0126e11e91e0c94e2939596d5d96bff1f296 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 14:04:53 +0200 Subject: [PATCH 39/62] Add cookie support. --- README.md | 12 ++++++++++++ almost_sinatra.coffee | 19 ++++++------------- example.coffee | 15 ++++++++------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4678dc1..b077765 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ So, what can this version do? * Handle WebSocket and EventSource connections * Path-based pattern matching with `:named` and `*` matchers * Construct params from path data, query strings and entity bodies +* Read and create cookies * Self-contained encrypted session cookies * Set status code and headers * Render EJS, HAML and raw text @@ -109,6 +110,17 @@ app.get '/counter', -> ``` +### Reading and setting cookies + +```coffee +app.get '/', -> + @cookie my_cookie: 'hello' + @cookie another_cookie: {value: 'something', path: '/welcome', expires: new Date(2012,11,25), http: true} + +app.get '/welcome', -> + @render @cookies.my_cookie + + ### Helpers, EJS and HAML ```coffee diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index c3d493d..c8ccbf6 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,28 +1,21 @@ [u,p,M,J,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,JSON,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] [http,url,qs,async,WS,E,haml,ejs]=w 'http url querystring async faye-websocket vault/node/aes haml ejs',require - class A [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@request,@response,@_m)-> - [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);@cookies=h;new E(A.session_secret).decrypt h.session||'',(x,j)=> - @session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] + [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);new E(A.session_secret).decrypt h.session||'',(x,j)=> + delete h.session;@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if u.test @request.headers['content-type']then qs.parse @request.body else @_u.query);@_c() _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @response.writeHead 404,{};@response.end() - render:(s)->h=new E(A.session_secret).encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s - + render:(s)->s=String s;h=new E(A.session_secret).encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s + cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s - +e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] - -e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p - A.call=(req,res)-> req.setEncoding 'utf8';b='';req.on('data',(s)->b+=s);req.on 'end',-> - req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method) - if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) - + req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) A.ws=(r,s,h)->a=new A(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null - module.exports=A diff --git a/example.coffee b/example.coffee index 4a4e8d2..b84f1e2 100644 --- a/example.coffee +++ b/example.coffee @@ -15,28 +15,29 @@ app.before (next) -> next() app.get '/', -> + @cookie message: {value: 'Sinatra', path: '/hello', http: true} @title = 'Almost Sinatra' @haml 'index' +app.get '/js/socket.js', -> + @headers 'Content-Type': 'text/javascript' + @ejs 'socket' + app.get '/download/*.*', -> @params.splat.join ', ' app.put '/download/*.*', (path, ext) -> [path, ext].join ' // ' -app.get '/js/socket.js', -> - @headers 'Content-Type': 'text/javascript' - @ejs 'socket' - app.get '/hello', -> - @ejs 'hello', locals: {name: @params.name} + @ejs 'hello', locals: {name: @params.name, message: @cookies.message} app.get '/words/:category/:id.:format', -> JSON.stringify @params app.get '/counter', -> @inc_counter() - @render @session.counter.toString() + @render @session.counter app.post '/', -> @status 201 @@ -88,7 +89,7 @@ es.onmessage = function(e) { """ app.template 'hello', """ -Hello <%= name %>, welcome to <%= site_name() %>! +Hello <%= name %>, welcome to <%= site_name() %>! Cookie: <%= message %> """ app.run 4567 From cb807e42cc81b393b6f8c90634e9dd877d348164 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 14:11:59 +0200 Subject: [PATCH 40/62] Fix 404 handling. --- almost_sinatra.coffee | 4 ++-- example.coffee | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index c8ccbf6..d50a292 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -4,8 +4,8 @@ class A [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@request,@response,@_m)-> [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);new E(A.session_secret).decrypt h.session||'',(x,j)=> - delete h.session;@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] - if @_b then[@params,@_a,d]=[{splat:[]},[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if u.test @request.headers['content-type']then qs.parse @request.body else @_u.query);@_c() + delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] + @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if u.test @request.headers['content-type']then qs.parse @request.body else @_u.query) _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @response.writeHead 404,{};@response.end() render:(s)->s=String s;h=new E(A.session_secret).encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c diff --git a/example.coffee b/example.coffee index b84f1e2..651e8c8 100644 --- a/example.coffee +++ b/example.coffee @@ -11,7 +11,7 @@ app.helpers 'Awesome.net' app.before (next) -> - @puts 'yay! got a request!' + @puts @request.method + ' ' + @request.url next() app.get '/', -> From e5bc95225fea4515fccddccbf4c8cc71fd889cb2 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 17:45:58 +0200 Subject: [PATCH 41/62] Shave a few characters off. --- almost_sinatra.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index d50a292..970d8ef 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,16 +1,16 @@ -[u,p,M,J,e,w]=[/\bapplication\/x-www-form-urlencoded\b/,4567,Math,JSON,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] +[J,M,e,w]=[JSON,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] [http,url,qs,async,WS,E,haml,ejs]=w 'http url querystring async faye-websocket vault/node/aes haml ejs',require class A [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@request,@response,@_m)-> - [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);new E(A.session_secret).decrypt h.session||'',(x,j)=> + [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);A.aes().decrypt h.session||'',(x,j)=> delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] - @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if u.test @request.headers['content-type']then qs.parse @request.body else @_u.query) + @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @response.writeHead 404,{};@response.end() - render:(s)->s=String s;h=new E(A.session_secret).encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s + render:(s)->s=String s;A.aes().encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s -e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:(q)->http.createServer(@call).on('upgrade',@ws).listen q||p +e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] A.call=(req,res)-> From 41d1fc6ac74db40f30e097a4be497957c760348d Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 18:24:53 +0200 Subject: [PATCH 42/62] Serve static files, and render views from files. --- README.md | 27 ++++++++++-- almost_sinatra.coffee | 9 ++-- example.coffee | 95 ------------------------------------------- package.json | 2 +- 4 files changed, 28 insertions(+), 105 deletions(-) delete mode 100644 example.coffee diff --git a/README.md b/README.md index b077765..f96fb52 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ So, what can this version do? * Self-contained encrypted session cookies * Set status code and headers * Render EJS, HAML and raw text +* Serve static files * Define helper functions for request handlers and templates @@ -42,6 +43,13 @@ app.run 4567 ``` +### Settings + + app.public = __dirname + '/public' # where to find static files + app.views = __dirname + '/views' # where to find view templates + app.session_secret = 'abcde12345' # key for encrypting sessions + + ### Handling HTTP methods, statuses, headers, etc. ```coffee @@ -124,17 +132,20 @@ app.get '/welcome', -> ### Helpers, EJS and HAML ```coffee +app.views = __dirname + '/views' + app.helpers site_name: -> 'Awesome.net' -app.template 'hello', """ -Hello <%= name %>, welcome to <%= site_name() %>! -""" - app.get '/hello', -> @ejs 'hello', locals: {name: @params.name} ``` +```erb +# views/hello.ejs +Hello <%= name %>, welcome to <%= site_name() %>! +``` + You can use `@ejs` or `@haml` to render a template. `@render` just takes a string and writes it to the response body. If your route handler returns a string, that string will be the body. Otherwise, you must call a rendering @@ -145,6 +156,14 @@ $ curl 'localhost:4567/hello?name=_why' Hello _why, welcome to Awesome.net! ``` +You can also register templates in the app code itself: + +```coffee +app.template 'hello.ejs', """ +Hello <%= name %>, welcome to <%= site_name() %>! +""" +``` + ### WebSockets! diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 970d8ef..79a8a11 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,16 +1,15 @@ [J,M,e,w]=[JSON,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] -[http,url,qs,async,WS,E,haml,ejs]=w 'http url querystring async faye-websocket vault/node/aes haml ejs',require +[http,url,qs,fs,async,WS,E,haml,ejs,mime]=w 'http url querystring fs async faye-websocket vault/node/aes haml ejs mime',require class A - [@b,@r,@h,@t]=[[],[],{},{}] constructor:(@request,@response,@_m)-> [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);A.aes().decrypt h.session||'',(x,j)=> delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) - _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else @response.writeHead 404,{};@response.end() + _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else p=@_u.pathname;fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,{'Content-Type':mime.lookup p};@response.end f render:(s)->s=String s;A.aes().encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c -e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->@render haml(A.t[n]) @),ejs:((n,o)->@render ejs.render A.t[n],e o?.locals||{},A.h),puts:(s)->console.log s -e A,before:((b)->@b.push b),helpers:((o)->e @h,o),template:((n,t)->@t[n]=t),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) +e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s +e A,r:[],b:[],before:((b)->@b.push b),h:{},helpers:((o)->e @h,o),t:{},template:((n,t)->@t[n]=t),v:((t,n,c)->if f=@t[n+'.'+t]then c null,f else fs.readFile (@views||'./views')+'/'+n+'.'+t,(x,f)->c null,f.toString()),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] A.call=(req,res)-> diff --git a/example.coffee b/example.coffee deleted file mode 100644 index 651e8c8..0000000 --- a/example.coffee +++ /dev/null @@ -1,95 +0,0 @@ -app = require './almost_sinatra' - -app.session_secret = 'abc123' - -app.helpers - inc_counter: -> - @session.counter ||= 0 - @session.counter += 1 - - site_name: -> - 'Awesome.net' - -app.before (next) -> - @puts @request.method + ' ' + @request.url - next() - -app.get '/', -> - @cookie message: {value: 'Sinatra', path: '/hello', http: true} - @title = 'Almost Sinatra' - @haml 'index' - -app.get '/js/socket.js', -> - @headers 'Content-Type': 'text/javascript' - @ejs 'socket' - -app.get '/download/*.*', -> - @params.splat.join ', ' - -app.put '/download/*.*', (path, ext) -> - [path, ext].join ' // ' - -app.get '/hello', -> - @ejs 'hello', locals: {name: @params.name, message: @cookies.message} - -app.get '/words/:category/:id.:format', -> - JSON.stringify @params - -app.get '/counter', -> - @inc_counter() - @render @session.counter - -app.post '/', -> - @status 201 - @headers 'Content-Type': 'application/json' - JSON.stringify @params - -app.options '/', -> - @headers 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' - @render '' - -app.websocket '/ws/:name', -> - @socket.onmessage = (e)=> - @socket.send @params.name + ': ' + e.data - -app.eventsource '/ws/:name', -> - setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 - -app.template 'index', """ -%html - %head - %title= title - %body - %p= site_name() - %a{href: '/hello?name=World'} Say hello! - %a{href: '/counter'} Show Counter - %ul{id: 'log'} - %script{src: '/js/socket.js'} -""" - -app.template 'socket', """ -var write = function(data) { - var log = document.getElementById('log'), - li = document.createElement('li'); - li.innerHTML = data; - log.appendChild(li); -}; - -var ws = new WebSocket('ws://localhost:4567/ws/awesome'); -ws.onmessage = function(e) { - write(e.data); - setTimeout(function() { ws.send('Loop') }, 2000) -}; -ws.onopen = function() { ws.send('Ping!') }; - -var es = new EventSource('http://localhost:4567/ws/eurucamp'); -es.onmessage = function(e) { - write(e.data); -}; -""" - -app.template 'hello', """ -Hello <%= name %>, welcome to <%= site_name() %>! Cookie: <%= message %> -""" - -app.run 4567 diff --git a/package.json b/package.json index 04ac6b2..6eac38d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "almost_sinatra", "version": "1.0.0", - "dependencies": {"coffee-script":"", "async":"", "faye-websocket":"", "vault": "", "haml":"", "ejs":""} + "dependencies": {"coffee-script":"", "async":"", "faye-websocket":"", "vault":"", "haml":"", "ejs":"", "mime":""} } \ No newline at end of file From 185820a384a1dc847e4d2db83e6112c9843ea491 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 18:26:56 +0200 Subject: [PATCH 43/62] Fix readme code formatting. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f96fb52..62c521d 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ app.get '/', -> app.get '/welcome', -> @render @cookies.my_cookie +``` ### Helpers, EJS and HAML From 17959950797207e31cd959e2333af9ecd8774a51 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 18:33:32 +0200 Subject: [PATCH 44/62] Merge documentation for sockets. --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 62c521d..1f3285b 100644 --- a/README.md +++ b/README.md @@ -166,18 +166,13 @@ Hello <%= name %>, welcome to <%= site_name() %>! ``` -### WebSockets! +### Sockets! ```coffee app.websocket '/ws/:name', -> @socket.onmessage = (e)=> @socket.send @params.name + ': ' + e.data -``` - -### EventSource! - -```coffee app.eventsource '/ws/:name', -> setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 ``` From fdc79b889318a58695de1596230cc71d1d8b9f08 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Mon, 20 Aug 2012 18:33:49 +0200 Subject: [PATCH 45/62] Commit missing example app. --- example/app.coffee | 64 ++++++++++++++++++++++++++++++++++++++++ example/public/socket.js | 20 +++++++++++++ example/views/index.haml | 9 ++++++ 3 files changed, 93 insertions(+) create mode 100644 example/app.coffee create mode 100644 example/public/socket.js create mode 100644 example/views/index.haml diff --git a/example/app.coffee b/example/app.coffee new file mode 100644 index 0000000..4971cdb --- /dev/null +++ b/example/app.coffee @@ -0,0 +1,64 @@ +app = require '../almost_sinatra' + +app.public = __dirname + '/public' +app.views = __dirname + '/views' +app.session_secret = 'abc123' + +app.helpers + inc_counter: -> + @session.counter ||= 0 + @session.counter += 1 + + site_name: -> + 'Awesome.net' + +app.before (next) -> + @puts @request.method + ' ' + @request.url + next() + +app.get '/', -> + @cookie message: {value: 'Sinatra', path: '/hello', http: true} + @title = 'Almost Sinatra' + @haml 'index' + +app.get '/js/socket.js', -> + @headers 'Content-Type': 'text/javascript' + @ejs 'socket' + +app.get '/download/*.*', -> + @params.splat.join ', ' + +app.put '/download/*.*', (path, ext) -> + [path, ext].join ' // ' + +app.get '/hello', -> + @ejs 'hello', locals: {name: @params.name, message: @cookies.message} + +app.get '/words/:category/:id.:format', -> + JSON.stringify @params + +app.get '/counter', -> + @inc_counter() + @render @session.counter + +app.post '/', -> + @status 201 + @headers 'Content-Type': 'application/json' + JSON.stringify @params + +app.options '/', -> + @headers 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' + @render '' + +app.websocket '/ws/:name', -> + @socket.onmessage = (e)=> + @socket.send @params.name + ': ' + e.data + +app.eventsource '/ws/:name', -> + setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 + +app.template 'hello.ejs', """ +Hello <%= name %>, welcome to <%= site_name() %>! Cookie: <%= message %> +""" + +app.run 4567 diff --git a/example/public/socket.js b/example/public/socket.js new file mode 100644 index 0000000..23161c8 --- /dev/null +++ b/example/public/socket.js @@ -0,0 +1,20 @@ +var write = function(data) { + var log = document.getElementById('log'), + li = document.createElement('li'); + + li.innerHTML = data; + log.appendChild(li); +}; + +var ws = new WebSocket('ws://localhost:4567/ws/awesome'); +ws.onmessage = function(e) { + write(e.data); + setTimeout(function() { ws.send('Loop') }, 2000) +}; +ws.onopen = function() { ws.send('Ping!') }; + +var es = new EventSource('http://localhost:4567/ws/eurucamp'); +es.onmessage = function(e) { + write(e.data); +}; + diff --git a/example/views/index.haml b/example/views/index.haml new file mode 100644 index 0000000..d1f5206 --- /dev/null +++ b/example/views/index.haml @@ -0,0 +1,9 @@ +%html + %head + %title= title + %body + %p= site_name() + %a{href: '/hello?name=World'} Say hello! + %a{href: '/counter'} Show Counter + %ul{id: 'log'} + %script{src: '/socket.js'} From f9d2ef06e2a3539bc047aa199b0a4b019ee669ba Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 02:53:59 +0200 Subject: [PATCH 46/62] Remove an unneeded route handler from the example app. --- example/app.coffee | 4 ---- 1 file changed, 4 deletions(-) diff --git a/example/app.coffee b/example/app.coffee index 4971cdb..df485f4 100644 --- a/example/app.coffee +++ b/example/app.coffee @@ -21,10 +21,6 @@ app.get '/', -> @title = 'Almost Sinatra' @haml 'index' -app.get '/js/socket.js', -> - @headers 'Content-Type': 'text/javascript' - @ejs 'socket' - app.get '/download/*.*', -> @params.splat.join ', ' From 552903441341c24142167b0d7317b46cbdea1b84 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 02:54:44 +0200 Subject: [PATCH 47/62] Place all the browser-accessible route handlers up top. --- example/app.coffee | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/example/app.coffee b/example/app.coffee index df485f4..0e65886 100644 --- a/example/app.coffee +++ b/example/app.coffee @@ -21,22 +21,22 @@ app.get '/', -> @title = 'Almost Sinatra' @haml 'index' -app.get '/download/*.*', -> - @params.splat.join ', ' - -app.put '/download/*.*', (path, ext) -> - [path, ext].join ' // ' - app.get '/hello', -> @ejs 'hello', locals: {name: @params.name, message: @cookies.message} -app.get '/words/:category/:id.:format', -> - JSON.stringify @params - app.get '/counter', -> @inc_counter() @render @session.counter +app.get '/words/:category/:id.:format', -> + JSON.stringify @params + +app.get '/download/*.*', -> + @params.splat.join ', ' + +app.put '/download/*.*', (path, ext) -> + [path, ext].join ' // ' + app.post '/', -> @status 201 @headers 'Content-Type': 'application/json' From b632b3ef5ff45122fc27e6423260cb2b01d319c9 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 14:56:22 +0200 Subject: [PATCH 48/62] Accommodate route matchers with no leading slash, or with a trailing slash. --- almost_sinatra.coffee | 4 ++-- example/app.coffee | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 79a8a11..0bfd416 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -6,12 +6,12 @@ class A delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else p=@_u.pathname;fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,{'Content-Type':mime.lookup p};@response.end f - render:(s)->s=String s;A.aes().encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s + render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s e A,r:[],b:[],before:((b)->@b.push b),h:{},helpers:((o)->e @h,o),t:{},template:((n,t)->@t[n]=t),v:((t,n,c)->if f=@t[n+'.'+t]then c null,f else fs.readFile (@views||'./views')+'/'+n+'.'+t,(x,f)->c null,f.toString()),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> - o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] + o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/^\/?/,'/').replace(/\/*$/,'').replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] A.call=(req,res)-> req.setEncoding 'utf8';b='';req.on('data',(s)->b+=s);req.on 'end',-> req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) diff --git a/example/app.coffee b/example/app.coffee index 0e65886..c6ac320 100644 --- a/example/app.coffee +++ b/example/app.coffee @@ -21,7 +21,7 @@ app.get '/', -> @title = 'Almost Sinatra' @haml 'index' -app.get '/hello', -> +app.get 'hello', -> @ejs 'hello', locals: {name: @params.name, message: @cookies.message} app.get '/counter', -> From 8ed84b85966ee4a8b21a663701c85e45a25aa091 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 15:36:13 +0200 Subject: [PATCH 49/62] Add context groups for shorter routes and contextual before-blocks. --- README.md | 28 ++++++++++++++++++++++++++++ almost_sinatra.coffee | 8 ++++---- example/app.coffee | 15 +++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1f3285b..05447b8 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,34 @@ Hello <%= name %>, welcome to <%= site_name() %>! ``` +### Context groups + +You can set up a set of routes with a common prefix and add before-filters that +only apply to that context. + +```coffee +app.get '/', -> 'Hello!' + +app.context '/auth', (auth) -> + auth.before (next) -> + if @request.headers.authorization + next() + else + @status 401 + @render 'Authorization required' + + auth.get '/', -> 'Secret' +``` + +```bash +$ curl localhost:4567/ +Hello! +$ curl localhost:4567/auth +Authorization required +$ curl localhost:4567/auth -H 'Authorization: foo' +Secret +``` + ### Sockets! ```coffee diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 0bfd416..7c938ad 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,17 +1,17 @@ -[J,M,e,w]=[JSON,Math,((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] +[J,M,n,e,w]=[JSON,Math,((p)->p.replace(/\/*$/,'').replace /^\/?/,'/'),((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] [http,url,qs,fs,async,WS,E,haml,ejs,mime]=w 'http url querystring fs async faye-websocket vault/node/aes haml ejs mime',require class A constructor:(@request,@response,@_m)-> [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);A.aes().decrypt h.session||'',(x,j)=> delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) - _x:(c)->@_c= =>async.series A.b.map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else p=@_u.pathname;fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,{'Content-Type':mime.lookup p};@response.end f + _x:(c)->@_c= =>async.series A.b.filter((f)=>@_u.pathname.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else p=@_u.pathname;fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,{'Content-Type':mime.lookup p};@response.end f render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s -e A,r:[],b:[],before:((b)->@b.push b),h:{},helpers:((o)->e @h,o),t:{},template:((n,t)->@t[n]=t),v:((t,n,c)->if f=@t[n+'.'+t]then c null,f else fs.readFile (@views||'./views')+'/'+n+'.'+t,(x,f)->c null,f.toString()),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) +e A,r:[],c:[''],context:((p,f)->@c.push n p;f @;@c.pop()),b:[],before:((b)->b.c=@c.join '';@b.push b),h:{},helpers:((o)->e @h,o),t:{},template:((n,t)->@t[n]=t),v:((t,n,c)->if f=@t[n+'.'+t]then c null,f else fs.readFile (@views||'./views')+'/'+n+'.'+t,(x,f)->c null,f.toString()),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> - o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/^\/?/,'/').replace(/\/*$/,'').replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] + p=n(@c.join('')+n p);o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] A.call=(req,res)-> req.setEncoding 'utf8';b='';req.on('data',(s)->b+=s);req.on 'end',-> req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) diff --git a/example/app.coffee b/example/app.coffee index c6ac320..e08f0ce 100644 --- a/example/app.coffee +++ b/example/app.coffee @@ -28,6 +28,21 @@ app.get '/counter', -> @inc_counter() @render @session.counter +app.context '/auth', (auth) -> + auth.before (next) -> + if @request.headers.authorization + next() + else + @status 401 + @render 'Authorization required' + + auth.get '/', -> + 'Secret stuff' + + auth.delete 'info', -> + @status 418 + 'I am a teapot' + app.get '/words/:category/:id.:format', -> JSON.stringify @params From 2806c99f060b5cbe692cec9be385d7f29002d535 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 15:40:51 +0200 Subject: [PATCH 50/62] Add Content-Length to static file responses, and golf things down a bit. --- almost_sinatra.coffee | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 7c938ad..a19696b 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -5,16 +5,13 @@ class A [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);A.aes().decrypt h.session||'',(x,j)=> delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) - _x:(c)->@_c= =>async.series A.b.filter((f)=>@_u.pathname.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else p=@_u.pathname;fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,{'Content-Type':mime.lookup p};@response.end f + _x:(c)->@_c= =>async.series A.b.filter((f)=>@_u.pathname.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else p=@_u.pathname;fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,'Content-Length':f.length,'Content-Type':mime.lookup p;@response.end f render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s e A,r:[],c:[''],context:((p,f)->@c.push n p;f @;@c.pop()),b:[],before:((b)->b.c=@c.join '';@b.push b),h:{},helpers:((o)->e @h,o),t:{},template:((n,t)->@t[n]=t),v:((t,n,c)->if f=@t[n+'.'+t]then c null,f else fs.readFile (@views||'./views')+'/'+n+'.'+t,(x,f)->c null,f.toString()),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) -w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)-> - p=n(@c.join('')+n p);o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] -A.call=(req,res)-> - req.setEncoding 'utf8';b='';req.on('data',(s)->b+=s);req.on 'end',-> - req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) +w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)->p=n(@c.join('')+n p);o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] +A.call=(req,res)->req.setEncoding 'utf8';b='';req.on('data',(s)->b+=s);req.on 'end',->req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) A.ws=(r,s,h)->a=new A(r,s,'WEBSOCKET');a._x (H)->w=a.socket=new WS r,s,h;H();w.addEventListener 'close',->w=null module.exports=A From d79277ce921827c4becb6390a3a51111a08ff4c6 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 18:53:28 +0200 Subject: [PATCH 51/62] Don't allow '..' as a path segment. --- almost_sinatra.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index a19696b..99064aa 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -5,7 +5,7 @@ class A [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);A.aes().decrypt h.session||'',(x,j)=> delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) - _x:(c)->@_c= =>async.series A.b.filter((f)=>@_u.pathname.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else p=@_u.pathname;fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,'Content-Length':f.length,'Content-Type':mime.lookup p;@response.end f + _x:(c)->@_c= =>p=@_u.pathname;if p.split('/').indexOf('..')>=0 then @response.writeHead 400,{};@response.end() else async.series A.b.filter((f)=>p.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,'Content-Length':f.length,'Content-Type':mime.lookup p;@response.end f render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s From cc9588d193a3db586734393dee887ec5a4543da4 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 19:14:06 +0200 Subject: [PATCH 52/62] Add redirect() method. --- README.md | 12 ++++++++++-- almost_sinatra.coffee | 4 ++-- example/app.coffee | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 05447b8..b4db41a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ So, what can this version do? * Read and create cookies * Self-contained encrypted session cookies * Set status code and headers +* Perform redirects * Render EJS, HAML and raw text * Serve static files * Define helper functions for request handlers and templates @@ -67,6 +68,9 @@ app.post '/confs', -> @headers 'Content-Type': 'application/json' JSON.stringify @params +app.get '/legacy', -> + @redirect '/hello' + app.options '/', -> @headers 'Access-Control-Allow-Origin': '*', @@ -91,11 +95,16 @@ HTTP/1.1 201 Created Content-Type: application/json Content-Length: 17 Set-Cookie: session=XABfKjq2xvCavSitaxu0BC9XSl...; Path=/; HttpOnly -Date: Sun, 19 Aug 2012 09:13:00 GMT Connection: keep-alive {"horses":"fake"} +$ curl -i localhost:4567/legacy +HTTP/1.1 303 See Other +Location: /hello +Connection: keep-alive +Transfer-Encoding: chunked + $ curl -iX OPTIONS localhost:4567/ HTTP/1.1 200 OK Content-Type: text/html @@ -103,7 +112,6 @@ Content-Length: 0 Set-Cookie: session=h7HwIkvXA0xcZk7JhmoVK25qNH...; Path=/; HttpOnly Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, PUT, DELETE -Date: Sun, 19 Aug 2012 09:13:42 GMT Connection: keep-alive ``` diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 99064aa..9d78b57 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -6,9 +6,9 @@ class A delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) _x:(c)->@_c= =>p=@_u.pathname;if p.split('/').indexOf('..')>=0 then @response.writeHead 400,{};@response.end() else async.series A.b.filter((f)=>p.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,'Content-Length':f.length,'Content-Type':mime.lookup p;@response.end f - render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h='Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length;e h,@_h;@response.writeHead @_s||200,h;@response.end s + render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h={};e h,'Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length if s;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c -e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s +e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),redirect:((u,s=303)->@status s;@headers 'Location':u;@render()),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s e A,r:[],c:[''],context:((p,f)->@c.push n p;f @;@c.pop()),b:[],before:((b)->b.c=@c.join '';@b.push b),h:{},helpers:((o)->e @h,o),t:{},template:((n,t)->@t[n]=t),v:((t,n,c)->if f=@t[n+'.'+t]then c null,f else fs.readFile (@views||'./views')+'/'+n+'.'+t,(x,f)->c null,f.toString()),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)->p=n(@c.join('')+n p);o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] A.call=(req,res)->req.setEncoding 'utf8';b='';req.on('data',(s)->b+=s);req.on 'end',->req.body=b;ES=WS.EventSource;k=ES.isEventSource req;a=new A(req,res,if k then 'EVENTSOURCE' else req.method);if k then(a._x (h)->s=a.socket=new ES req,res;h();s.addEventListener 'close',->s=null)else(a._x (h)->r=h();if typeof r=='string'then @render r) diff --git a/example/app.coffee b/example/app.coffee index e08f0ce..c76a10b 100644 --- a/example/app.coffee +++ b/example/app.coffee @@ -57,6 +57,9 @@ app.post '/', -> @headers 'Content-Type': 'application/json' JSON.stringify @params +app.get '/legacy', -> + @redirect '/hello' + app.options '/', -> @headers 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' @render '' From 8a8b9224ed86509b35e6d4c84a522d539348cfcc Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 19:23:47 +0200 Subject: [PATCH 53/62] Cookies should go through URI codec on the wire. --- almost_sinatra.coffee | 12 ++++++------ example/app.coffee | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 9d78b57..236ac13 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -1,13 +1,13 @@ -[J,M,n,e,w]=[JSON,Math,((p)->p.replace(/\/*$/,'').replace /^\/?/,'/'),((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] +[J,M,duc,euc,n,e,w]=[JSON,Math,decodeURIComponent,encodeURIComponent,((p)->p.replace(/\/*$/,'').replace /^\/?/,'/'),((d,s)->d[k]=(if d[k]instanceof Array then d[k].concat v else v)for k,v of s;d),(s,c)->s.split(/\s+/).map c] [http,url,qs,fs,async,WS,E,haml,ejs,mime]=w 'http url querystring fs async faye-websocket vault/node/aes haml ejs mime',require class A constructor:(@request,@response,@_m)-> - [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[q[0]]=q[1]);A.aes().decrypt h.session||'',(x,j)=> + [h,c]=[{},(@request.headers.cookie||'').split /\s*;\s*/];c.map((p)->q=p.split '=';h[duc q[0]]=duc q[1]);A.aes().decrypt h.session||'',(x,j)=> delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] - @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=decodeURIComponent d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) - _x:(c)->@_c= =>p=@_u.pathname;if p.split('/').indexOf('..')>=0 then @response.writeHead 400,{};@response.end() else async.series A.b.filter((f)=>p.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,'Content-Length':f.length,'Content-Type':mime.lookup p;@response.end f - render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h={};e h,'Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html','Content-Length':new Buffer(s,'utf8').length if s;e h,@_h;@response.writeHead @_s||200,h;@response.end s - cookie:(c)->(v=if typeof v=='string'then value:v else v;s=k+'='+v.value+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c + @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=duc d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) + _x:(c)->@_c= =>p=@_u.pathname;if p.split('/').indexOf('..')>=0 then @response.writeHead 400,{};@response.end() else async.series A.b.filter((f)=>p.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,'Content-Length':f.length,'Content-Type':mime.lookup(p)+'; charset=utf-8';@response.end f + render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h={};e h,'Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html; charset=utf-8','Content-Length':new Buffer(s,'utf8').length if s;e h,@_h;@response.writeHead @_s||200,h;@response.end s + cookie:(c)->(v=if typeof v=='string'then value:v else v;s=euc(k)+'='+euc(v.value)+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),redirect:((u,s=303)->@status s;@headers 'Location':u;@render()),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s e A,r:[],c:[''],context:((p,f)->@c.push n p;f @;@c.pop()),b:[],before:((b)->b.c=@c.join '';@b.push b),h:{},helpers:((o)->e @h,o),t:{},template:((n,t)->@t[n]=t),v:((t,n,c)->if f=@t[n+'.'+t]then c null,f else fs.readFile (@views||'./views')+'/'+n+'.'+t,(x,f)->c null,f.toString()),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) w 'get post put delete patch head options websocket eventsource',(v)->A[v]=(p,f)->p=n(@c.join('')+n p);o=(p.match(/[\/\.](\*|:[a-z\_\$][a-z0-9\_\$]*)/g)||[]).map((s)->s.replace /^[^a-z0-9\_\$\*]*/,'');m=new RegExp('^'+p.replace(/([\/\.])/g,'\\$1').replace(/\*|:[a-z\_\$][a-z0-9\_\$]*/ig,'(.+?)')+'$');@r.push [v.toUpperCase(),o,m,f] diff --git a/example/app.coffee b/example/app.coffee index c76a10b..d30167a 100644 --- a/example/app.coffee +++ b/example/app.coffee @@ -17,11 +17,12 @@ app.before (next) -> next() app.get '/', -> - @cookie message: {value: 'Sinatra', path: '/hello', http: true} + @cookie message: {value: 'Sinatra; it’s a framework', path: '/hello', http: true} @title = 'Almost Sinatra' @haml 'index' app.get 'hello', -> + console.log @request.headers.cookie @ejs 'hello', locals: {name: @params.name, message: @cookies.message} app.get '/counter', -> @@ -72,7 +73,7 @@ app.eventsource '/ws/:name', -> setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 app.template 'hello.ejs', """ -Hello <%= name %>, welcome to <%= site_name() %>! Cookie: <%= message %> +Hello <%= name %>, welcome to <%= site_name() %>! Cookie: “<%= message %>” """ app.run 4567 From 9bbc67f0e955f6b90b73b7cca94e9f0fbabd8112 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 18:32:54 +0100 Subject: [PATCH 54/62] URI-encode session cookies, otherwise they decode badly. --- almost_sinatra.coffee | 2 +- example/app.coffee | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/almost_sinatra.coffee b/almost_sinatra.coffee index 236ac13..f8c369c 100644 --- a/almost_sinatra.coffee +++ b/almost_sinatra.coffee @@ -6,7 +6,7 @@ class A delete h.session;@params={splat:[]};@cookies=h;@session=(if x then{}else J.parse(j));@_u=url.parse(@request.url,true);@_h='Set-Cookie':[];e @,A.h;@_b=A.r.filter((r)=>r[0]==@_m&&r[2].test @_u.pathname)[0] @_c if @_b then[@_a,d]=[[],@_u.pathname.match @_b[2]];(v=duc d[i+1];@_a.push v;if k=='*' then @params.splat.push v else @params[k]=v)for k,i in @_b[1];e @params,(if /\bapplication\/x-www-form-urlencoded\b/.test @request.headers['content-type']then qs.parse @request.body else @_u.query) _x:(c)->@_c= =>p=@_u.pathname;if p.split('/').indexOf('..')>=0 then @response.writeHead 400,{};@response.end() else async.series A.b.filter((f)=>p.substr(0,f.c.length)==f.c).map((f)=> =>f.apply @,arguments).concat =>if @_b then c.call @,(=>@_b[3].apply @,@_a)else fs.readFile (A.public||'./public')+p,(x,f)=>if x then @response.writeHead 404,{};@response.end()else @response.writeHead 200,'Content-Length':f.length,'Content-Type':mime.lookup(p)+'; charset=utf-8';@response.end f - render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h={};e h,'Set-Cookie':['session=' + j + '; Path=/; HttpOnly'],'Content-Type':'text/html; charset=utf-8','Content-Length':new Buffer(s,'utf8').length if s;e h,@_h;@response.writeHead @_s||200,h;@response.end s + render:(s)->s=(if s==undefined then ''else String s);A.aes().encrypt J.stringify(@session),(x,j)=>h={};e h,'Set-Cookie':['session=' + euc(j) + '; Path=/; HttpOnly'],'Content-Type':'text/html; charset=utf-8','Content-Length':new Buffer(s,'utf8').length if s;e h,@_h;@response.writeHead @_s||200,h;@response.end s cookie:(c)->(v=if typeof v=='string'then value:v else v;s=euc(k)+'='+euc(v.value)+'; Path='+v.path||'/';s+='; Domain='+v.domain if v.domain;s+='; Expires='+v.expires.toGMTString() if v.expires;s+='; HttpOnly' if v.http;s+='; Secure' if v.secure;e @_h,'Set-Cookie':s)for k,v of c e A.prototype,headers:((o)->e @_h,o),status:((n)->@_s=parseInt n,10),redirect:((u,s=303)->@status s;@headers 'Location':u;@render()),haml:((n)->A.v 'haml',n,(x,t)=>@render haml(t) @),ejs:((n,o)->A.v 'ejs',n,(x,t)=>@render ejs.render t,e o?.locals||{},A.h),puts:(s)->console.log s e A,r:[],c:[''],context:((p,f)->@c.push n p;f @;@c.pop()),b:[],before:((b)->b.c=@c.join '';@b.push b),h:{},helpers:((o)->e @h,o),t:{},template:((n,t)->@t[n]=t),v:((t,n,c)->if f=@t[n+'.'+t]then c null,f else fs.readFile (@views||'./views')+'/'+n+'.'+t,(x,f)->c null,f.toString()),run:((q)->http.createServer(@call).on('upgrade',@ws).listen q||4567),aes:->new E(@session_secret) diff --git a/example/app.coffee b/example/app.coffee index d30167a..be46332 100644 --- a/example/app.coffee +++ b/example/app.coffee @@ -17,12 +17,11 @@ app.before (next) -> next() app.get '/', -> - @cookie message: {value: 'Sinatra; it’s a framework', path: '/hello', http: true} + @cookie message: {value: 'Sinatra; it’s a framework', path: '/hello'} @title = 'Almost Sinatra' @haml 'index' app.get 'hello', -> - console.log @request.headers.cookie @ejs 'hello', locals: {name: @params.name, message: @cookies.message} app.get '/counter', -> From 0b288895a0c0e1f8a7a328b4520eb967163f963e Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 21 Aug 2012 23:20:59 +0100 Subject: [PATCH 55/62] Make a docs website, change the name of the project. --- .npmignore | 6 + README.markdown | 26 +++ README.md | 238 ----------------------- almost_sinatra.coffee => berliner.coffee | 0 example/app.coffee | 2 +- package.json | 27 ++- site/index.html | 217 +++++++++++++++++++++ site/prettify.css | 6 + site/prettify.js | 23 +++ 9 files changed, 301 insertions(+), 244 deletions(-) create mode 100644 .npmignore create mode 100644 README.markdown delete mode 100644 README.md rename almost_sinatra.coffee => berliner.coffee (100%) create mode 100644 site/index.html create mode 100644 site/prettify.css create mode 100644 site/prettify.js diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..7879b59 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +.git +.gitignore +.npmignore +.redcar +node_modules +site diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..dbb0a49 --- /dev/null +++ b/README.markdown @@ -0,0 +1,26 @@ +# Berliner + +A lightweight web framework. [Read the online docs.](http://berliner.jcoglan.com) + +## License + +(The MIT License) + +Copyright (c) 2012 James Coglan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index b4db41a..0000000 --- a/README.md +++ /dev/null @@ -1,238 +0,0 @@ -# Almost Sinatra - - “until programmers stop acting like obfuscation is morally hazardous, - they’re not artists, just kids who don’t want their food to touch.” - _why - -I ported @rkh's `almost-sinatra` to CoffeeScript, for the lolz, and then I got -carried away. Berlin is a strange and wonderful place. - -So, what can this version do? - -* Route requests based on method and pathname -* Respond to `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` -* Handle WebSocket and EventSource connections -* Path-based pattern matching with `:named` and `*` matchers -* Construct params from path data, query strings and entity bodies -* Read and create cookies -* Self-contained encrypted session cookies -* Set status code and headers -* Perform redirects -* Render EJS, HAML and raw text -* Serve static files -* Define helper functions for request handlers and templates - - -## Run the example - -```bash -git clone git://github.com/jcoglan/almost-sinatra.git -cd almost-sinatra -git checkout coffee -npm install -./node_modules/.bin/coffee example.coffee -``` - - -### Hello world - -```coffee -app = require './almost_sinatra' - -app.get '/', -> 'Hello, world!' - -app.run 4567 -``` - - -### Settings - - app.public = __dirname + '/public' # where to find static files - app.views = __dirname + '/views' # where to find view templates - app.session_secret = 'abcde12345' # key for encrypting sessions - - -### Handling HTTP methods, statuses, headers, etc. - -```coffee -app.get '/confs/:name', -> - JSON.stringify @params - -app.put '/confs/:name', (name) -> - name - -app.get '/download/*.*', -> - @params.splat.join ', ' - -app.post '/confs', -> - @status 201 - @headers 'Content-Type': 'application/json' - JSON.stringify @params - -app.get '/legacy', -> - @redirect '/hello' - -app.options '/', -> - @headers - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, PUT, DELETE' - @render '' -``` - -This app responds like so: - -```bash -$ curl 'localhost:4567/confs/eurucamp?bears=awesome' -{"name":"eurucamp","bears":"awesome"} - -$ curl -X PUT localhost:4567/confs/jsconf -jsconf - -$ curl localhost:4567/download/foo.js -foo, js - -$ curl -iX POST localhost:4567/confs -d 'horses=fake' -HTTP/1.1 201 Created -Content-Type: application/json -Content-Length: 17 -Set-Cookie: session=XABfKjq2xvCavSitaxu0BC9XSl...; Path=/; HttpOnly -Connection: keep-alive - -{"horses":"fake"} - -$ curl -i localhost:4567/legacy -HTTP/1.1 303 See Other -Location: /hello -Connection: keep-alive -Transfer-Encoding: chunked - -$ curl -iX OPTIONS localhost:4567/ -HTTP/1.1 200 OK -Content-Type: text/html -Content-Length: 0 -Set-Cookie: session=h7HwIkvXA0xcZk7JhmoVK25qNH...; Path=/; HttpOnly -Access-Control-Allow-Origin: * -Access-Control-Allow-Methods: GET, PUT, DELETE -Connection: keep-alive -``` - - -### Wait, are those session cookies? - -```coffee -app.get '/counter', -> - @session.counter ||= 0 - @session.counter += 1 - @render @session.counter.toString() -``` - - -### Reading and setting cookies - -```coffee -app.get '/', -> - @cookie my_cookie: 'hello' - @cookie another_cookie: {value: 'something', path: '/welcome', expires: new Date(2012,11,25), http: true} - -app.get '/welcome', -> - @render @cookies.my_cookie -``` - - -### Helpers, EJS and HAML - -```coffee -app.views = __dirname + '/views' - -app.helpers - site_name: -> 'Awesome.net' - -app.get '/hello', -> - @ejs 'hello', locals: {name: @params.name} -``` - -```erb -# views/hello.ejs -Hello <%= name %>, welcome to <%= site_name() %>! -``` - -You can use `@ejs` or `@haml` to render a template. `@render` just takes a -string and writes it to the response body. If your route handler returns a -string, that string will be the body. Otherwise, you must call a rendering -function. - -```bash -$ curl 'localhost:4567/hello?name=_why' -Hello _why, welcome to Awesome.net! -``` - -You can also register templates in the app code itself: - -```coffee -app.template 'hello.ejs', """ -Hello <%= name %>, welcome to <%= site_name() %>! -""" -``` - - -### Context groups - -You can set up a set of routes with a common prefix and add before-filters that -only apply to that context. - -```coffee -app.get '/', -> 'Hello!' - -app.context '/auth', (auth) -> - auth.before (next) -> - if @request.headers.authorization - next() - else - @status 401 - @render 'Authorization required' - - auth.get '/', -> 'Secret' -``` - -```bash -$ curl localhost:4567/ -Hello! -$ curl localhost:4567/auth -Authorization required -$ curl localhost:4567/auth -H 'Authorization: foo' -Secret -``` - -### Sockets! - -```coffee -app.websocket '/ws/:name', -> - @socket.onmessage = (e)=> - @socket.send @params.name + ': ' + e.data - -app.eventsource '/ws/:name', -> - setInterval (=> @socket.send @params.name + ': PUSH!'), 5000 -``` - - -## License - -(The MIT License) - -Copyright (c) 2012 James Coglan - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the 'Software'), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/almost_sinatra.coffee b/berliner.coffee similarity index 100% rename from almost_sinatra.coffee rename to berliner.coffee diff --git a/example/app.coffee b/example/app.coffee index be46332..98f4e4b 100644 --- a/example/app.coffee +++ b/example/app.coffee @@ -1,4 +1,4 @@ -app = require '../almost_sinatra' +app = require '../berliner' app.public = __dirname + '/public' app.views = __dirname + '/views' diff --git a/package.json b/package.json index 6eac38d..39f5da5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,22 @@ -{ - "name": "almost_sinatra", - "version": "1.0.0", - "dependencies": {"coffee-script":"", "async":"", "faye-websocket":"", "vault":"", "haml":"", "ejs":"", "mime":""} -} \ No newline at end of file +{ "name" : "berliner" +, "description" : "The lightweight web framework" +, "homepage" : "http://berliner.jcoglan.com/" +, "author" : "James Coglan (http://jcoglan.com/)" +, "keywords" : ["web", "framework", "lightweight"] + +, "version" : "1.0.0" +, "engines" : {"node": ">=0.4.0"} +, "main" : "./berliner" +, "dependencies" : {"coffee-script":"", "async":"", "faye-websocket":"", "vault":"", "haml":"", "ejs":"", "mime":""} + +, "licenses" : [ { "type" : "MIT" + , "url" : "http://www.opensource.org/licenses/mit-license.php" + } + ] + +, "repositories" : [ { "type" : "git" + , "url" : "git://github.com/jcoglan/berliner.git" + } + ] +} + diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..855905f --- /dev/null +++ b/site/index.html @@ -0,0 +1,217 @@ + + + + + + Berliner + + + + + + + +

Berliner.

+

The lightweight web framework

+ +

Do you like Sinatra, but find it a little heavy? Bloated with things like + simple error handling and CSRF protection; sick of the impedence mismatch + of using one language on the server and another on the client; it’s + not even asynchronous?!

+ +

Berliner is the framework for you! In just 16 lines of delicious + CoffeeScript, it gives you all this, using the infinitely scalable horse + power of Node.js:

+ +
    +
  • Route requests based on method and pathname
  • +
  • Respond to GET, POST, PUT, + DELETE, PATCH, HEAD, and + OPTIONS
  • +
  • Handle WebSocket and EventSource connections
  • +
  • Path-based pattern matching with :named and + * matchers
  • +
  • Construct params from path data, query strings and entity bodies
  • +
  • Read and create cookies
  • +
  • Self-contained encrypted session cookies
  • +
  • Set status code and headers
  • +
  • Perform redirects
  • +
  • Render EJS, HAML and raw text
  • +
  • Serve static files
  • +
  • Define helper functions for request handlers and templates
  • +
+ +

Install it

+ +
$ npm install berliner
+ +

Hello, world

+ +
app = require 'berliner'
+
+app.get '/', -> 'Hello, world!'
+
+app.run 4567
+ +

Settings

+ +
app.public = __dirname + '/public' # where to find static files
+app.views  = __dirname + '/views'  # where to find view templates
+app.session_secret = 'abcde12345'  # key for encrypting sessions
+ +

Handling HTTP methods, statuses, headers, etc.

+ +
app.get '/confs/:name', ->
+  JSON.stringify @params
+
+app.put '/confs/:name', (name) ->
+  name
+
+app.get '/download/*.*', ->
+  @params.splat.join ', '
+
+app.post '/confs', ->
+  @status 201
+  @headers 'Content-Type': 'application/json'
+  JSON.stringify @params
+
+app.get '/legacy', ->
+  @redirect '/hello'
+
+app.options '/', ->
+  @headers
+    'Access-Control-Allow-Origin': '*',
+    'Access-Control-Allow-Methods': 'GET, PUT, DELETE'
+  @render ''
+ +

This app responds like so:

+ +
$ curl 'localhost:4567/confs/eurucamp?bears=awesome'
+{"name":"eurucamp","bears":"awesome"}
+
+$ curl -X PUT localhost:4567/confs/jsconf
+jsconf
+
+$ curl localhost:4567/download/foo.js
+foo, js
+
+$ curl -iX POST localhost:4567/confs -d 'horses=fake'
+HTTP/1.1 201 Created
+Content-Type: application/json
+Content-Length: 17
+Set-Cookie: session=XABfKjq2xvCavSitaxu0BC9XSl...; Path=/; HttpOnly
+Connection: keep-alive
+
+{"horses":"fake"}
+
+$ curl -i localhost:4567/legacy
+HTTP/1.1 303 See Other
+Location: /hello
+Connection: keep-alive
+Transfer-Encoding: chunked
+
+$ curl -iX OPTIONS localhost:4567/
+HTTP/1.1 200 OK
+Content-Type: text/html
+Content-Length: 0
+Set-Cookie: session=h7HwIkvXA0xcZk7JhmoVK25qNH...; Path=/; HttpOnly
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Methods: GET, PUT, DELETE
+Connection: keep-alive
+ +

WebSocket and EventSource

+ +
app.websocket '/ws/:name', ->
+  @socket.onmessage = (e)=>
+    @socket.send @params.name + ': ' + e.data
+
+app.eventsource '/ws/:name', ->
+  setInterval (=> @socket.send @params.name + ': PUSH!'), 5000
+ +

Sessions

+ +
app.get '/counter', ->
+  @session.counter ||= 0
+  @session.counter += 1
+  @render @session.counter.toString()
+ +

Cookies

+ +
app.get '/', ->
+  @cookie my_cookie: 'hello'
+  @cookie another_cookie: {value: 'something', path: '/welcome', expires: new Date(2012,11,25), http: true}
+
+app.get '/welcome', ->
+  @render @cookies.my_cookie
+ +

Helpers, EJS and HAML

+ +
app.views = __dirname + '/views'
+
+app.helpers
+  site_name: -> 'Awesome.net'
+
+app.get '/hello', ->
+  @ejs 'hello', locals: {name: @params.name}
+ +
# views/hello.ejs
+
+Hello <%= name %>, welcome to <%= site_name() %>!
+ +

You can use @ejs or @haml to render a template. + @render just takes a string and writes it to the response + body. If your route handler returns a string, that string will be the + body. Otherwise, you must call a rendering function.

+ +
$ curl 'localhost:4567/hello?name=_why'
+Hello _why, welcome to Awesome.net!
+ +

You can also register templates in the app code itself:

+ +
app.template 'hello.ejs', """
+Hello <%= name %>, welcome to <%= site_name() %>!
+"""
+ +

Context groups

+ +

You can set up a set of routes with a common prefix and add + before-filters that only apply to that context.

+ +
app.get '/', -> 'Hello!'
+
+app.context '/auth', (auth) ->
+  auth.before (next) ->
+    if @request.headers.authorization
+      next()
+    else
+      @status 401
+      @render 'Authorization required'
+  
+  auth.get '/', -> 'Secret'
+ +
$ curl localhost:4567/
+Hello!
+$ curl localhost:4567/auth
+Authorization required
+$ curl localhost:4567/auth -H 'Authorization: foo'
+Secret
+ +

Credits

+ +

Created by James Coglan at Eurucamp 2012, inspired by Konstantin + Haase. Released under the MIT license.

+ +

If you find bugs, you’re probably holding it wrong. Since this is + such a small module, it’s almost certainly bug-free; in fact I + didn’t bother writing tests because it’s obviously correct and + test frameworks are ugly and stupid.

+ + + diff --git a/site/prettify.css b/site/prettify.css new file mode 100644 index 0000000..b75d36d --- /dev/null +++ b/site/prettify.css @@ -0,0 +1,6 @@ +/* Defaults */ +.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun{color:#660}.pln{color:#000}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec{color:#606}@media print{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun{color:#440}.pln{color:#000}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}} + +/* Overrides */ +.str{color:#d14}.kwd{color:#000;font-weight:bold}.com{color:#998;font-style:italic}.typ{color:#458}.lit{color:#099}.pun{color:#666}.pln{color:#000}.tag{color:#000080}.atn{color:#008080}.atv{color:#008080}.dec{color:#000;font-weight:bold} + diff --git a/site/prettify.js b/site/prettify.js new file mode 100644 index 0000000..8fc4c93 --- /dev/null +++ b/site/prettify.js @@ -0,0 +1,23 @@ +function H(){var x=navigator&&navigator.userAgent&&/\bMSIE 6\./.test(navigator.userAgent);H=function(){return x};return x}(function(){function x(b){b=b.split(/ /g);var a={};for(var c=b.length;--c>=0;){var d=b[c];if(d)a[d]=null}return a}var y="break continue do else for if return while ",U=y+"auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile ",D=U+"catch class delete false import new operator private protected public this throw true try ", +I=D+"alignof align_union asm axiom bool concept concept_map const_cast constexpr decltype dynamic_cast explicit export friend inline late_check mutable namespace nullptr reinterpret_cast static_assert static_cast template typeid typename typeof using virtual wchar_t where ",J=D+"boolean byte extends final finally implements import instanceof null native package strictfp super synchronized throws transient ",V=J+"as base by checked decimal delegate descending event fixed foreach from group implicit in interface internal into is lock object out override orderby params readonly ref sbyte sealed stackalloc string select uint ulong unchecked unsafe ushort var ", +K=D+"debugger eval export function get null set undefined var with Infinity NaN ",L="caller delete die do dump elsif eval exit foreach for goto if import last local my next no our print package redo require sub undef unless until use wantarray while BEGIN END ",M=y+"and as assert class def del elif except exec finally from global import in is lambda nonlocal not or pass print raise try with yield False True None ",N=y+"alias and begin case class def defined elsif end ensure false in module next nil not or redo rescue retry self super then true undef unless until when yield BEGIN END ", +O=y+"case done elif esac eval fi function in local set then until ",W=I+V+K+L+M+N+O;function X(b){return b>="a"&&b<="z"||b>="A"&&b<="Z"}function u(b,a,c,d){b.unshift(c,d||0);try{a.splice.apply(a,b)}finally{b.splice(0,2)}}var Y=(function(){var b=["!","!=","!==","#","%","%=","&","&&","&&=","&=","(","*","*=","+=",",","-=","->","/","/=",":","::",";","<","<<","<<=","<=","=","==","===",">",">=",">>",">>=",">>>",">>>=","?","@","[","^","^=","^^","^^=","{","|","|=","||","||=","~","break","case","continue", +"delete","do","else","finally","instanceof","return","throw","try","typeof"],a="(?:(?:(?:^|[^0-9.])\\.{1,3})|(?:(?:^|[^\\+])\\+)|(?:(?:^|[^\\-])-)";for(var c=0;c:&])/g,"\\$1")}a+="|^)\\s*$";return new RegExp(a)})(),P=/&/g,Q=//g,Z=/\"/g;function $(b){return b.replace(P,"&").replace(Q,"<").replace(R,">").replace(Z,""")}function E(b){return b.replace(P,"&").replace(Q,"<").replace(R,">")}var aa= +/</g,ba=/>/g,ca=/'/g,da=/"/g,ea=/&/g,fa=/ /g;function ga(b){var a=b.indexOf("&");if(a<0)return b;for(--a;(a=b.indexOf("&#",a+1))>=0;){var c=b.indexOf(";",a);if(c>=0){var d=b.substring(a+3,c),g=10;if(d&&d.charAt(0)==="x"){d=d.substring(1);g=16}var e=parseInt(d,g);if(!isNaN(e))b=b.substring(0,a)+String.fromCharCode(e)+b.substring(c+1)}}return b.replace(aa,"<").replace(ba,">").replace(ca,"'").replace(da,'"').replace(ea,"&").replace(fa," ")}function S(b){return"XMP"===b.tagName} +function z(b,a){switch(b.nodeType){case 1:var c=b.tagName.toLowerCase();a.push("<",c);for(var d=0;d");for(var e=b.firstChild;e;e=e.nextSibling)z(e,a);if(b.firstChild||!/^(?:br|link|img)$/.test(c))a.push("");break;case 2:a.push(b.name.toLowerCase(),'="',$(b.value),'"');break;case 3:case 4:a.push(E(b.nodeValue));break}}var F=null;function ha(b){if(null===F){var a=document.createElement("PRE"); +a.appendChild(document.createTextNode('\n'));F=!/=0;i-=" ".length)d.push(" ".substring(0,i));g=e+1;break; +case "\n":a=0;break;default:++a}}if(!d)return c;d.push(c.substring(g));return d.join("")}}var ja=/(?:[^<]+|