diff --git a/.gitignore b/.gitignore index 14fac93..ca9fd0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ annoying people +node_modules 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/.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!" 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 de89030..0000000 --- a/README.md +++ /dev/null @@ -1,110 +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 - -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. 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/berliner.coffee b/berliner.coffee new file mode 100644 index 0000000..1837e32 --- /dev/null +++ b/berliner.coffee @@ -0,0 +1,17 @@ +[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-cipher 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[duc q[0]]=duc q[1]);A.cipher().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=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.cipher().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),cipher:->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) +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.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/example/app.coffee b/example/app.coffee new file mode 100644 index 0000000..98f4e4b --- /dev/null +++ b/example/app.coffee @@ -0,0 +1,78 @@ +app = require '../berliner' + +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; it’s a framework', path: '/hello'} + @title = 'Almost Sinatra' + @haml 'index' + +app.get 'hello', -> + @ejs 'hello', locals: {name: @params.name, message: @cookies.message} + +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 + +app.get '/download/*.*', -> + @params.splat.join ', ' + +app.put '/download/*.*', (path, ext) -> + [path, ext].join ' // ' + +app.post '/', -> + @status 201 + @headers 'Content-Type': 'application/json' + JSON.stringify @params + +app.get '/legacy', -> + @redirect '/hello' + +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'} diff --git a/package.json b/package.json new file mode 100644 index 0000000..357bbe7 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ "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.1" +, "engines" : {"node": ">=0.4.0"} +, "main" : "./berliner" +, "dependencies" : {"async":"", "faye-websocket":"", "vault-cipher":"", "haml":"", "ejs":"", "mime":""} +, "devDependencies" : {"coffee-script":""} + +, "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/analytics.html b/site/analytics.html new file mode 100644 index 0000000..d94892a --- /dev/null +++ b/site/analytics.html @@ -0,0 +1,11 @@ + diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..f19643e --- /dev/null +++ b/site/index.html @@ -0,0 +1,216 @@ + + + + + + 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
  • +
  • Works great with MongoDB
  • +
+ +

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
+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=/(?:[^<]+|