├── sample
├── hello.ru
├── multi.ru
├── accept.ru
├── path.ru
└── sinvsrum
├── lib
└── rum.rb
└── README
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------