├── README ├── lib └── rum.rb └── sample ├── accept.ru ├── hello.ru ├── multi.ru ├── path.ru └── sinvsrum /README: -------------------------------------------------------------------------------- 1 | = Rum, the gRand Unified Mapper 2 | 3 | Rum is a powerful mapper for your Rack applications that can be used 4 | as a microframework. Just throw in a template engine and a data 5 | backend, and get started! 6 | 7 | == Mappings 8 | 9 | Rum apps use a small DSL to set up the mappings: 10 | 11 | MyApp = Rum.new { 12 | on get, path('greet') do 13 | on param("name") do |name| 14 | puts "Hello, #{Rack::Utils.escape_html name}!" 15 | end 16 | on default do 17 | puts "Hello, world!" 18 | end 19 | end 20 | } 21 | 22 | This will map GET /greet to the hello world, and GET /greet?name=X to 23 | a personal greeting. 24 | 25 | Mappings are declared by nested "on"-calls. When one matched, the 26 | block is called and other "on"-calls are ignored on that level. (But 27 | note you can use "also" to reset this.) 28 | 29 | Multiple "on"-calls can be collapsed to one: 30 | 31 | on a do |x| 32 | on b do |y| 33 | ... 34 | end 35 | end 36 | 37 | is the same as 38 | 39 | on a, b do |x, y| 40 | ... 41 | end 42 | 43 | Every predicate returns data which is passed to the block. Use _ to 44 | ignore data you don't need when using the collapsed calling method: 45 | 46 | on get, path('foo'), param('bar') do |_, _, bar| ... end 47 | 48 | == Predicates 49 | 50 | These predicates are predefined for your mappings: 51 | 52 | path(rx): match rx against the PATH_INFO beginning and try to match a 53 | path segment. (path('foo') will match '/foo/bar', but not '/foobar'). 54 | PATH_INFO and SCRIPT_NAME are adjusted appropriately. 55 | 56 | number: match a number segment in path. 57 | 58 | segment: match any single segment in path. (i.e. no '/'.) 59 | 60 | extension(e=...): match (if given) and yield the file extension. 61 | 62 | param(p, default=...): check if parameter p is given, else use default 63 | if passed. 64 | 65 | header(p, default=...): check if HTTP header h exists, else yield default 66 | if passed. 67 | 68 | default: always match. 69 | 70 | host(h): check if the request was meant for host (useful for virtual 71 | servers). 72 | 73 | method(m): check if the request used m as HTTP method. 74 | 75 | get, post, put, delete: shortcuts for method(m). 76 | 77 | accept(m): check if the request accepts MIME-type m. This is a very 78 | simple check that doesn't handle parameters or globs for now. 79 | 80 | check{block}: general check whether block returns a trueish value. 81 | 82 | any(...): meta-predicate to check if any argument matches. 83 | 84 | == Helpers 85 | 86 | For convenience, Rum provides a few helpers: 87 | 88 | env, req, res: The current Rack env, Rack::Request, Rack::Response. 89 | 90 | also: Reset state of matching, that is, the next on() will be checked, 91 | even if one before already matched. 92 | 93 | run(app): directly transfer to the Rack application app (which can be 94 | another Rum app.) 95 | 96 | print, puts: wrappers for res.write. 97 | 98 | == Copyright 99 | 100 | Copyright (C) 2008, 2009 Christian Neukirchen 101 | 102 | Permission is hereby granted, free of charge, to any person obtaining a copy 103 | of this software and associated documentation files (the "Software"), to 104 | deal in the Software without restriction, including without limitation the 105 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 106 | sell copies of the Software, and to permit persons to whom the Software is 107 | furnished to do so, subject to the following conditions: 108 | 109 | The above copyright notice and this permission notice shall be included in 110 | all copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 113 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 114 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 115 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 116 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 117 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 118 | 119 | == Links 120 | 121 | Rack:: 122 | Christian Neukirchen:: 123 | 124 | 125 | -------------------------------------------------------------------------------- /lib/rum.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | 3 | class Rack::Response 4 | # 301 Moved Permanently 5 | # 302 Found 6 | # 303 See Other 7 | # 307 Temporary Redirect 8 | def redirect(target, status=302) 9 | self.status = status 10 | self["Location"] = target 11 | end 12 | end 13 | 14 | class Rum 15 | attr_reader :env, :req, :res 16 | 17 | def initialize(&blk) 18 | @blk = blk 19 | end 20 | 21 | def call(env) 22 | dup._call(env) 23 | end 24 | 25 | def _call(env) 26 | @env = env 27 | @req = Rack::Request.new(env) 28 | @res = Rack::Response.new 29 | @matched = false 30 | catch(:rum_run_next_app) { 31 | instance_eval(&@blk) 32 | @res.status = 404 unless @matched || !@res.empty? 33 | return @res.finish 34 | }.call(env) 35 | end 36 | 37 | def on(*arg, &block) 38 | return if @matched 39 | s, p = env["SCRIPT_NAME"], env["PATH_INFO"] 40 | yield *arg.map { |a| a == true || (a != false && a.call) || return } 41 | env["SCRIPT_NAME"], env["PATH_INFO"] = s, p 42 | @matched = true 43 | end 44 | 45 | def any(*args) 46 | args.any? { |a| a == true || (a != false && a.call) } 47 | end 48 | 49 | def also 50 | @matched = false 51 | end 52 | 53 | def path(p) 54 | lambda { 55 | if env["PATH_INFO"] =~ /\A\/(#{p})(\/|\z)/ #/ 56 | env["SCRIPT_NAME"] += "/#{$1}" 57 | env["PATH_INFO"] = $2 + $' 58 | $1 59 | end 60 | } 61 | end 62 | 63 | def number 64 | path("\\d+") 65 | end 66 | 67 | def segment 68 | path("[^\\/]+") 69 | end 70 | 71 | def extension(e="\\w+") 72 | lambda { env["PATH_INFO"] =~ /\.(#{e})\z/ && $1 } 73 | end 74 | 75 | def param(p, default=nil) 76 | lambda { req[p] || default } 77 | end 78 | 79 | def header(p, default=nil) 80 | lambda { env[p.upcase.tr('-','_')] || default } 81 | end 82 | 83 | def default 84 | true 85 | end 86 | 87 | def host(h) 88 | req.host == h 89 | end 90 | 91 | def method(m) 92 | req.request_method = m 93 | end 94 | 95 | def get; req.get?; end 96 | def post; req.post?; end 97 | def put; req.put?; end 98 | def delete; req.delete?; end 99 | 100 | def accept(mimetype) 101 | lambda { 102 | env['HTTP_ACCEPT'].split(',').any? { |s| s.strip == mimetype } and 103 | res['Content-Type'] = mimetype 104 | } 105 | end 106 | 107 | def check(&block) 108 | block 109 | end 110 | 111 | def run(app) 112 | throw :rum_run_next_app, app 113 | end 114 | 115 | def puts(*args) 116 | args.each { |s| 117 | res.write s 118 | res.write "\n" 119 | } 120 | end 121 | 122 | def print(*args) 123 | args.each { |s| res.write s } 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /sample/accept.ru: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | require '../lib/rum' 3 | 4 | use Rack::ShowStatus 5 | 6 | run Rum.new { 7 | on param("name") do |name| 8 | on any(accept("text/plain"), extension("txt")) do 9 | res.write "Hello, #{name}!" 10 | end 11 | on default do 12 | res.write "

Hello, #{Rack::Utils.escape_html name}!

" 13 | end 14 | end 15 | on default do 16 | res.write "Hello, world!" 17 | end 18 | } 19 | -------------------------------------------------------------------------------- /sample/hello.ru: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | require '../lib/rum' 3 | 4 | use Rack::ShowStatus 5 | 6 | run Rum.new { 7 | on param("name") do |name| 8 | puts "Hello, #{Rack::Utils.escape_html name}!" 9 | end 10 | on default do 11 | puts "Hello, world!" 12 | end 13 | } 14 | -------------------------------------------------------------------------------- /sample/multi.ru: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | require '../lib/rum' 3 | 4 | use Rack::ShowStatus 5 | 6 | greeter = Rum.new { 7 | on param("name") do |name| 8 | res.write "Hello, #{Rack::Utils.escape_html name}!" 9 | end 10 | on default do 11 | res.write "Hello, world!" 12 | end 13 | } 14 | 15 | run Rum.new { 16 | on path('greet') do 17 | run greeter 18 | end 19 | } 20 | -------------------------------------------------------------------------------- /sample/path.ru: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | require '../lib/rum' 3 | 4 | use Rack::ShowStatus 5 | 6 | module Kernel 7 | def info(title, r) 8 | r.res['Content-Type'] = "text/plain" 9 | r.res.write "At #{title}\n" 10 | r.res.write " SCRIPT_NAME: #{r.req.script_name}\n" 11 | r.res.write " PATH_INFO: #{r.req.path_info}\n" 12 | end 13 | end 14 | 15 | run Rum.new { 16 | on path('foo') do 17 | info("foo", self) 18 | on path('bar') do 19 | info("foo/bar", self) 20 | end 21 | end 22 | on get, path('say'), segment, path('to'), segment do |_, _, what, _, whom, _| 23 | info("say/#{what}/to/#{whom}", self) 24 | end 25 | also 26 | on default do 27 | info("default", self) 28 | end 29 | } 30 | -------------------------------------------------------------------------------- /sample/sinvsrum: -------------------------------------------------------------------------------- 1 | # A quick comparison of Sinatra and Rum. 2 | 3 | # get '/hi' do 4 | on get, path('hi') do 5 | 6 | # get '/:name' do 7 | on get, segment do |_, name| 8 | 9 | # get '/say/*/to/*' do 10 | on get, path('say'), segment, path('to'), segment do |_, _, what, _, whom, _| 11 | 12 | # get '/download/*.*' do 13 | on get, path('download'), segment, extension do |_, _, filename, ext| 14 | 15 | # get '/foo', :agent => /Songbird (\d\.\d)[\d\/]*?/ do 16 | on get, path('foo'), check{env["HTTP_USER_AGENT"] =~ /Songbird (\d\.\d)[\d\/]*?/} do 17 | 18 | # post '/foo' do 19 | on post, path('foo'), param("bar") do |bar| 20 | 21 | # redirect '/' 22 | res.redirect '/' 23 | 24 | # redirect '/', 307 25 | res.redirect '/', 307 26 | 27 | # status 404 28 | res.status = 404 29 | --------------------------------------------------------------------------------