kontrol 0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +18 -0
- data/README.md +201 -0
- data/examples/git_app.ru +19 -0
- data/examples/hello_world.ru +16 -0
- data/examples/routing.ru +15 -0
- data/examples/templates/layout.rhtml +5 -0
- data/examples/templates/page.rhtml +2 -0
- data/examples/templates.ru +11 -0
- data/lib/kontrol/application.rb +155 -0
- data/lib/kontrol/helpers.rb +41 -0
- data/lib/kontrol/mime_types.rb +62 -0
- data/lib/kontrol/route.rb +30 -0
- data/lib/kontrol/router.rb +35 -0
- data/lib/kontrol/template.rb +42 -0
- data/lib/kontrol.rb +12 -0
- data/test/application_spec.rb +121 -0
- data/test/route_spec.rb +50 -0
- data/test/router_spec.rb +53 -0
- metadata +72 -0
data/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2008 Matthias Georgi <http://www.matthias-georgi.de>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
|
5
|
+
deal in the Software without restriction, including without limitation the
|
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Kontrol - a micro framework
|
|
2
|
+
===========================
|
|
3
|
+
|
|
4
|
+
Kontrol is a small web framework written in Ruby, which runs directly
|
|
5
|
+
on [Rack][1]. Basically you can mount a class as rack handler and
|
|
6
|
+
attach a set of named routes, which will be used for route recognition
|
|
7
|
+
and generation.
|
|
8
|
+
|
|
9
|
+
All examples can be found in the examples folder of the kontrol
|
|
10
|
+
project, which is hosted on [github][2].
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
We will create a simple Kontrol application with exactly one route
|
|
15
|
+
named 'root'. Routes are defined within a block insider your
|
|
16
|
+
application class. Each route has a name, a pattern and a block. The
|
|
17
|
+
name must be defined to generate paths pointing to this route.
|
|
18
|
+
|
|
19
|
+
`hello_world.ru`:
|
|
20
|
+
|
|
21
|
+
class HelloWorld < Kontrol::Application
|
|
22
|
+
|
|
23
|
+
def time
|
|
24
|
+
Time.now.strftime "%H:%M:%S"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
map do
|
|
28
|
+
root '/' do
|
|
29
|
+
text "<h1>Hello World at #{time}</h1>"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
run HelloWorld.new
|
|
35
|
+
|
|
36
|
+
Now run:
|
|
37
|
+
|
|
38
|
+
rackup hello_world.ru
|
|
39
|
+
|
|
40
|
+
Browse to `http://localhost:9292` and you will see "Hello World".
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Basics
|
|
44
|
+
|
|
45
|
+
A Kontrol application is a class, which provides some context to the
|
|
46
|
+
defined actions. You will probably use these methods:
|
|
47
|
+
|
|
48
|
+
- __request__: the Rack request object
|
|
49
|
+
- __response__: the Rack response object
|
|
50
|
+
- __params__: union of GET and POST parameters
|
|
51
|
+
- __cookies__: shortcut to request.cookies
|
|
52
|
+
- __session__: shortcut to `request.env['rack.session']`
|
|
53
|
+
- __redirect(path)__: renders a redirect response to specified path
|
|
54
|
+
- __render(file, variables)__: render a template with specified variables
|
|
55
|
+
- __text(string)__: render a string
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Routing
|
|
59
|
+
|
|
60
|
+
Routing is just as simple as using regular expressions with
|
|
61
|
+
groups. Each group will be provided as argument to the block.
|
|
62
|
+
|
|
63
|
+
Create a file named `routing.ru`:
|
|
64
|
+
|
|
65
|
+
require 'kontrol'
|
|
66
|
+
|
|
67
|
+
class Routing < Kontrol::Application
|
|
68
|
+
map do
|
|
69
|
+
pages '/pages/(.*)' do |name|
|
|
70
|
+
text "The path is #{ pages_path name }! "
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
archive '/(\d*)/(\d*)' do |year, month|
|
|
74
|
+
text "The path is #{ archive_path year, month }! "
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
run Routing.new
|
|
80
|
+
|
|
81
|
+
Now run this application:
|
|
82
|
+
|
|
83
|
+
rackup routing.ru
|
|
84
|
+
|
|
85
|
+
You will now see, how regex groups and parameters are related. For
|
|
86
|
+
example if you browse to `localhost:9292/2008/12`, the app will
|
|
87
|
+
display `The path is /2008/12`.
|
|
88
|
+
|
|
89
|
+
The inverse operation to route recognition is route generation. That
|
|
90
|
+
means a route with one or more groups can generate a url, which will
|
|
91
|
+
be recognized this very route.
|
|
92
|
+
|
|
93
|
+
For example the route `/page/(.*)` named page will recognize the path
|
|
94
|
+
`/page/about`, which can be generated by using `page_path('about')`.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
## Templates
|
|
98
|
+
|
|
99
|
+
Rendering templates is as simple as calling a template file with some
|
|
100
|
+
parameters, which are accessible inside the template as instance
|
|
101
|
+
variables. Additionally you will need a layout template.
|
|
102
|
+
|
|
103
|
+
Create a template named `templates/layout.rhtml`:
|
|
104
|
+
|
|
105
|
+
<html>
|
|
106
|
+
<body>
|
|
107
|
+
<%= @content %>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
|
|
111
|
+
And now another template named `templates/page.rhtml`:
|
|
112
|
+
|
|
113
|
+
<h1><%= @title %></h1>
|
|
114
|
+
<%= @body %>
|
|
115
|
+
|
|
116
|
+
Create a templates.ru file:
|
|
117
|
+
|
|
118
|
+
require 'kontrol'
|
|
119
|
+
|
|
120
|
+
class Templates < Kontrol::Application
|
|
121
|
+
map do
|
|
122
|
+
page '/(.*)' do |name|
|
|
123
|
+
render "page.rhtml", :title => name.capitalize, :body => "This is the body!"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
run Templates.new
|
|
129
|
+
|
|
130
|
+
Now run this example:
|
|
131
|
+
|
|
132
|
+
rackup templates.ru
|
|
133
|
+
|
|
134
|
+
If you browse to any path on `localhost:9292`, you will see the
|
|
135
|
+
rendered template. Note that the title and body parameters have been
|
|
136
|
+
passed to the `render` call.
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## Using GitStore
|
|
140
|
+
|
|
141
|
+
[GitStore][3] is another library, which allows you to store code and
|
|
142
|
+
data in a convenient way in a git repository. The repository is
|
|
143
|
+
checked out into memory and any data may be saved back into the
|
|
144
|
+
repository.
|
|
145
|
+
|
|
146
|
+
Install [GitStore][] by:
|
|
147
|
+
|
|
148
|
+
$ gem sources -a http://gems.github.com
|
|
149
|
+
$ sudo gem install georgi-git_store
|
|
150
|
+
|
|
151
|
+
We create a Markdown file name `index.md`:
|
|
152
|
+
|
|
153
|
+
Hello World
|
|
154
|
+
===========
|
|
155
|
+
|
|
156
|
+
This is the **Index** page!
|
|
157
|
+
|
|
158
|
+
We have now a simple page, which should be rendered as response. We
|
|
159
|
+
create a simple app in a file `git_app.ru`:
|
|
160
|
+
|
|
161
|
+
require 'kontrol'
|
|
162
|
+
require 'bluecloth'
|
|
163
|
+
require 'git_store'
|
|
164
|
+
|
|
165
|
+
class GitApp < Kontrol::Application
|
|
166
|
+
|
|
167
|
+
def initialize(path)
|
|
168
|
+
super
|
|
169
|
+
@store = GitStore.new(path)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
map do
|
|
173
|
+
page '/(.*)' do |name|
|
|
174
|
+
text BlueCloth.new(@store[name + '.md']).to_html
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
run GitApp.new
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
Add all the page to your git repository:
|
|
183
|
+
|
|
184
|
+
git init
|
|
185
|
+
git add index.md
|
|
186
|
+
git commit -m 'init'
|
|
187
|
+
|
|
188
|
+
Run the app:
|
|
189
|
+
|
|
190
|
+
rackup git_app.ru
|
|
191
|
+
|
|
192
|
+
Browse to `http://localhost:9292/index` and you will see the rendered
|
|
193
|
+
page generated from the markdown file.
|
|
194
|
+
|
|
195
|
+
This application runs straight from the git repository. You can even
|
|
196
|
+
delete the page and it will still show up over the web.
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
[1]: http://github.com/chneukirchen/rack
|
|
200
|
+
[2]: http://github.com/georgi/kontrol
|
|
201
|
+
[3]: http://github.com/georgi/git_store
|
data/examples/git_app.ru
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'kontrol'
|
|
2
|
+
require 'bluecloth'
|
|
3
|
+
require 'git_store'
|
|
4
|
+
|
|
5
|
+
class GitApp < Kontrol::Application
|
|
6
|
+
|
|
7
|
+
def initialize(path)
|
|
8
|
+
super
|
|
9
|
+
@store = GitStore.new(path)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
map do
|
|
13
|
+
page '/(.*)' do |name|
|
|
14
|
+
text BlueCloth.new(@store[name]).to_html
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
run GitApp.new
|
data/examples/routing.ru
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'kontrol'
|
|
2
|
+
|
|
3
|
+
class Routing < Kontrol::Application
|
|
4
|
+
map do
|
|
5
|
+
pages '/pages/(.*)' do |name|
|
|
6
|
+
text "The path is #{ pages_path name }! "
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
archive '/(\d*)/(\d*)' do |year, month|
|
|
10
|
+
text "The path is #{ archive_path year, month }! "
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
run Routing.new
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
require 'digest/sha1'
|
|
2
|
+
|
|
3
|
+
module Kontrol
|
|
4
|
+
|
|
5
|
+
class Application
|
|
6
|
+
include Helpers
|
|
7
|
+
|
|
8
|
+
attr_reader :path
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_accessor :router
|
|
12
|
+
|
|
13
|
+
def map(&block)
|
|
14
|
+
@router = Router.new(&block)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(path = '.')
|
|
19
|
+
@path = File.expand_path(path)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def load_template(file)
|
|
23
|
+
ERB.new(File.read("#{self.path}/templates/#{file}"))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Render template with given variables.
|
|
27
|
+
def render_template(file, vars)
|
|
28
|
+
template = load_template(file) or raise "not found: #{path}"
|
|
29
|
+
Template.render(template, self, "#{self.path}/templates/#{file}", vars)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Render named template and insert into layout with given variables.
|
|
33
|
+
def render(name, options = {})
|
|
34
|
+
options = options.merge(:request => request, :params => params)
|
|
35
|
+
content = render_template(name, options)
|
|
36
|
+
layout = options.delete(:layout)
|
|
37
|
+
|
|
38
|
+
if name[0, 1] == '_'
|
|
39
|
+
return content
|
|
40
|
+
|
|
41
|
+
elsif layout == false
|
|
42
|
+
response.body = content
|
|
43
|
+
else
|
|
44
|
+
options.merge!(:content => content)
|
|
45
|
+
response.body = render_template(layout || "layout.rhtml", options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
response['Content-Length'] = response.body.size.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def etag(string)
|
|
52
|
+
Digest::SHA1.hexdigest(string)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def if_modified_since(time)
|
|
56
|
+
date = time.respond_to?(:httpdate) ? time.httpdate : time
|
|
57
|
+
response['Last-Modified'] = date
|
|
58
|
+
|
|
59
|
+
if request.env['HTTP_IF_MODIFIED_SINCE'] == date
|
|
60
|
+
response.status = 304
|
|
61
|
+
else
|
|
62
|
+
yield
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def if_none_match(etag)
|
|
67
|
+
response['Etag'] = etag
|
|
68
|
+
if request.env['HTTP_IF_NONE_MATCH'] == etag
|
|
69
|
+
response.status = 304
|
|
70
|
+
else
|
|
71
|
+
yield
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def request ; Thread.current['request'] end
|
|
76
|
+
def response; Thread.current['response'] end
|
|
77
|
+
def params ; request.params end
|
|
78
|
+
def cookies ; request.cookies end
|
|
79
|
+
def session ; request.env['rack.session'] end
|
|
80
|
+
def post? ; request.post? end
|
|
81
|
+
def get? ; request.get? end
|
|
82
|
+
def put? ; request.put? end
|
|
83
|
+
def delete? ; request.delete? end
|
|
84
|
+
def post ; request.post? and yield end
|
|
85
|
+
def get ; request.get? and yield end
|
|
86
|
+
def put ; request.put? and yield end
|
|
87
|
+
def delete ; request.delete? and yield end
|
|
88
|
+
|
|
89
|
+
def text(s)
|
|
90
|
+
response.body = s
|
|
91
|
+
response['Content-Length'] = response.body.size.to_s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def redirect(path)
|
|
95
|
+
response['Location'] = path
|
|
96
|
+
response.status = 301
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def guess_content_type
|
|
100
|
+
ext = File.extname(request.path_info)[1..-1]
|
|
101
|
+
MIME_TYPES[ext] || 'text/html'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def router
|
|
105
|
+
self.class.router
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def call(env)
|
|
109
|
+
Thread.current['request'] = Rack::Request.new(env)
|
|
110
|
+
Thread.current['response'] = Rack::Response.new([], 200, { 'Content-Type' => '' })
|
|
111
|
+
|
|
112
|
+
route, match = router.__recognize__(request)
|
|
113
|
+
|
|
114
|
+
if route
|
|
115
|
+
method = "process_#{route.name}"
|
|
116
|
+
self.class.send(:define_method, method, &route.block)
|
|
117
|
+
send(method, *match.to_a[1..-1])
|
|
118
|
+
else
|
|
119
|
+
response.body = "<h1>404 - Page Not Found</h1>"
|
|
120
|
+
response['Content-Length'] = response.body.size.to_s
|
|
121
|
+
response.status = 404
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
response['Content-Type'] = guess_content_type if response['Content-Type'].empty?
|
|
125
|
+
response.finish
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def inspect
|
|
129
|
+
"#<#{self.class.name} @path=#{path}>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def respond_to?(name)
|
|
133
|
+
if match = name.to_s.match(/^(.*)_path$/)
|
|
134
|
+
router.__find__(match[1])
|
|
135
|
+
else
|
|
136
|
+
super
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def method_missing(name, *args, &block)
|
|
141
|
+
if match = name.to_s.match(/^(.*)_path$/)
|
|
142
|
+
if route = router.__find__(match[1])
|
|
143
|
+
route.generate(*args)
|
|
144
|
+
else
|
|
145
|
+
super
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
super
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
end
|
|
155
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Kontrol
|
|
2
|
+
|
|
3
|
+
module Helpers
|
|
4
|
+
|
|
5
|
+
# Render a HTML tag with given name.
|
|
6
|
+
# The last argument specifies the attributes of the tag.
|
|
7
|
+
# The second argument may be the content of the tag.
|
|
8
|
+
def tag(name, *args)
|
|
9
|
+
text, attr = args.first.is_a?(Hash) ? [nil, args.first] : args
|
|
10
|
+
attributes = attr.map { |k, v| %Q{#{k}="#{v}"} }.join(' ')
|
|
11
|
+
"<#{name} #{attributes}>#{text}</#{name}>"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Render a link
|
|
15
|
+
def link_to(text, path, options = {})
|
|
16
|
+
tag :a, text, options.merge(:href => path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def markdown(text, *args)
|
|
20
|
+
BlueCloth.new(text, *args).to_html
|
|
21
|
+
rescue => e
|
|
22
|
+
"#{text}<br/><br/><strong style='color:red'>#{e.message}</strong>"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def strip_tags(str)
|
|
26
|
+
str.to_s.gsub(/<\/?[^>]*>/, "")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def urlify(string)
|
|
30
|
+
string.downcase.gsub(/[ -]+/, '-').gsub(/[^-a-z0-9_]+/, '')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
HTML_ESCAPE = { '&' => '&', '"' => '"', '>' => '>', '<' => '<', ' ' => ' ' }
|
|
34
|
+
|
|
35
|
+
def h(s)
|
|
36
|
+
s.to_s.gsub(/[ &"><]/) { |special| HTML_ESCAPE[special] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Kontrol
|
|
2
|
+
|
|
3
|
+
MIME_TYPES = {
|
|
4
|
+
"ai" => "application/postscript",
|
|
5
|
+
"asc" => "text/plain",
|
|
6
|
+
"avi" => "video/x-msvideo",
|
|
7
|
+
"bin" => "application/octet-stream",
|
|
8
|
+
"bmp" => "image/bmp",
|
|
9
|
+
"class" => "application/octet-stream",
|
|
10
|
+
"cer" => "application/pkix-cert",
|
|
11
|
+
"crl" => "application/pkix-crl",
|
|
12
|
+
"crt" => "application/x-x509-ca-cert",
|
|
13
|
+
"css" => "text/css",
|
|
14
|
+
"dms" => "application/octet-stream",
|
|
15
|
+
"doc" => "application/msword",
|
|
16
|
+
"dvi" => "application/x-dvi",
|
|
17
|
+
"eps" => "application/postscript",
|
|
18
|
+
"etx" => "text/x-setext",
|
|
19
|
+
"exe" => "application/octet-stream",
|
|
20
|
+
"gif" => "image/gif",
|
|
21
|
+
"htm" => "text/html",
|
|
22
|
+
"html" => "text/html",
|
|
23
|
+
"ico" => "image/x-icon",
|
|
24
|
+
"jpe" => "image/jpeg",
|
|
25
|
+
"jpeg" => "image/jpeg",
|
|
26
|
+
"jpg" => "image/jpeg",
|
|
27
|
+
"js" => "text/javascript",
|
|
28
|
+
"lha" => "application/octet-stream",
|
|
29
|
+
"lzh" => "application/octet-stream",
|
|
30
|
+
"mov" => "video/quicktime",
|
|
31
|
+
"mp3" => "audio/mpeg",
|
|
32
|
+
"mpe" => "video/mpeg",
|
|
33
|
+
"mpeg" => "video/mpeg",
|
|
34
|
+
"mpg" => "video/mpeg",
|
|
35
|
+
"pbm" => "image/x-portable-bitmap",
|
|
36
|
+
"pdf" => "application/pdf",
|
|
37
|
+
"pgm" => "image/x-portable-graymap",
|
|
38
|
+
"png" => "image/png",
|
|
39
|
+
"pnm" => "image/x-portable-anymap",
|
|
40
|
+
"ppm" => "image/x-portable-pixmap",
|
|
41
|
+
"ppt" => "application/vnd.ms-powerpoint",
|
|
42
|
+
"ps" => "application/postscript",
|
|
43
|
+
"qt" => "video/quicktime",
|
|
44
|
+
"ras" => "image/x-cmu-raster",
|
|
45
|
+
"rb" => "text/plain",
|
|
46
|
+
"rd" => "text/plain",
|
|
47
|
+
"rtf" => "application/rtf",
|
|
48
|
+
"rss" => "application/rss+xml",
|
|
49
|
+
"sgm" => "text/sgml",
|
|
50
|
+
"sgml" => "text/sgml",
|
|
51
|
+
"tif" => "image/tiff",
|
|
52
|
+
"tiff" => "image/tiff",
|
|
53
|
+
"txt" => "text/plain",
|
|
54
|
+
"xbm" => "image/x-xbitmap",
|
|
55
|
+
"xls" => "application/vnd.ms-excel",
|
|
56
|
+
"xml" => "text/xml",
|
|
57
|
+
"xpm" => "image/x-xpixmap",
|
|
58
|
+
"xwd" => "image/x-xwindowdump",
|
|
59
|
+
"zip" => "application/zip",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Kontrol
|
|
2
|
+
|
|
3
|
+
class Route
|
|
4
|
+
attr_accessor :name, :pattern, :options, :block
|
|
5
|
+
|
|
6
|
+
def initialize(name, pattern, options, block)
|
|
7
|
+
@name = name
|
|
8
|
+
@pattern = pattern
|
|
9
|
+
@block = block
|
|
10
|
+
@options = options || {}
|
|
11
|
+
@format = pattern.gsub(/\(.*?\)/, '%s')
|
|
12
|
+
@regexp = /^#{pattern}/
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def recognize(request)
|
|
16
|
+
match = request.path_info.match(@regexp)
|
|
17
|
+
valid = @options.all? { |key, val| request.send(key).match(val) }
|
|
18
|
+
|
|
19
|
+
return match if match and valid
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate(*args)
|
|
23
|
+
@format % args.map { |arg|
|
|
24
|
+
arg.respond_to?(:to_param) ? arg.to_param : arg.to_s
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Kontrol
|
|
2
|
+
|
|
3
|
+
class Router
|
|
4
|
+
|
|
5
|
+
def initialize(&block)
|
|
6
|
+
@routes = []
|
|
7
|
+
@map = {}
|
|
8
|
+
|
|
9
|
+
instance_eval(&block) if block
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def __find__(name)
|
|
13
|
+
@map[name.to_sym]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def __recognize__(request)
|
|
17
|
+
@routes.each do |route|
|
|
18
|
+
if match = route.recognize(request)
|
|
19
|
+
return route, match
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def method_missing(name, pattern, *args, &block)
|
|
27
|
+
route = Route.new(name, pattern, args.first, block)
|
|
28
|
+
|
|
29
|
+
@routes << route
|
|
30
|
+
@map[name] = route
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Kontrol
|
|
2
|
+
|
|
3
|
+
# This class renders an ERB template for a set of attributes, which
|
|
4
|
+
# are accessible as instance variables.
|
|
5
|
+
class Template
|
|
6
|
+
include Helpers
|
|
7
|
+
|
|
8
|
+
# Initialize this template with an ERB instance.
|
|
9
|
+
def initialize(app, vars)
|
|
10
|
+
@__app__ = app
|
|
11
|
+
|
|
12
|
+
vars.each do |k, v|
|
|
13
|
+
instance_variable_set "@#{k}", v
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def __binding__
|
|
18
|
+
binding
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.render(erb, app, file, vars)
|
|
22
|
+
template = Template.new(app, vars)
|
|
23
|
+
|
|
24
|
+
return erb.result(template.__binding__)
|
|
25
|
+
|
|
26
|
+
rescue => e
|
|
27
|
+
e.backtrace.each do |s|
|
|
28
|
+
s.gsub!('(erb)', file)
|
|
29
|
+
end
|
|
30
|
+
raise e
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def method_missing(id, *args, &block)
|
|
34
|
+
if @__app__.respond_to?(id)
|
|
35
|
+
return @__app__.send(id, *args, &block)
|
|
36
|
+
end
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
data/lib/kontrol.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rack'
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'logger'
|
|
6
|
+
|
|
7
|
+
require 'kontrol/helpers'
|
|
8
|
+
require 'kontrol/mime_types'
|
|
9
|
+
require 'kontrol/template'
|
|
10
|
+
require 'kontrol/application'
|
|
11
|
+
require 'kontrol/router'
|
|
12
|
+
require 'kontrol/route'
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require 'kontrol'
|
|
2
|
+
require 'rack/mock'
|
|
3
|
+
|
|
4
|
+
describe Kontrol::Application do
|
|
5
|
+
|
|
6
|
+
before do
|
|
7
|
+
@class = Class.new(Kontrol::Application)
|
|
8
|
+
@app = @class.new
|
|
9
|
+
@request = Rack::MockRequest.new(@app)
|
|
10
|
+
|
|
11
|
+
def @app.load_template(file)
|
|
12
|
+
if file == "layout.rhtml"
|
|
13
|
+
ERB.new '<html><%= @content %></html>'
|
|
14
|
+
else
|
|
15
|
+
ERB.new '<p><%= @body %></p>'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get(*args)
|
|
21
|
+
@request.get(*args)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def map(&block)
|
|
25
|
+
@class.map(&block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "should do simple pattern matching" do
|
|
29
|
+
map do
|
|
30
|
+
one '/one' do
|
|
31
|
+
response.body = 'one'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
two '/two' do
|
|
35
|
+
response.body = 'two'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
get("/one").body.should == 'one'
|
|
40
|
+
get("/two").body.should == 'two'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "should have a router" do
|
|
44
|
+
map do
|
|
45
|
+
root '/'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@class.router.should_not be_nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "should generate paths" do
|
|
52
|
+
map do
|
|
53
|
+
root '/'
|
|
54
|
+
about '/about'
|
|
55
|
+
page '/page/(.*)'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@app.root_path.should == '/'
|
|
59
|
+
@app.about_path.should == '/about'
|
|
60
|
+
@app.page_path('world').should == '/page/world'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "should redirect" do
|
|
64
|
+
map do
|
|
65
|
+
root '/' do
|
|
66
|
+
redirect 'x'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
get('/')['Location'].should == 'x'
|
|
71
|
+
get('/').status.should == 301
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "should respond with not modified" do
|
|
75
|
+
map do
|
|
76
|
+
assets '/assets/(.*)' do
|
|
77
|
+
script = "script"
|
|
78
|
+
if_none_match(etag(script)) do
|
|
79
|
+
text script
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
get("/assets/test.js").body.should == 'script'
|
|
85
|
+
|
|
86
|
+
etag = get("/assets/file")['Etag']
|
|
87
|
+
get("/assets/file", 'HTTP_IF_NONE_MATCH' => etag).status.should == 304
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "should render text" do
|
|
91
|
+
map do
|
|
92
|
+
index '/' do
|
|
93
|
+
text "Hello"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
get('/').body.should == 'Hello'
|
|
98
|
+
get('/')['Content-Length'].should == '5'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "should render no layout" do
|
|
102
|
+
map do
|
|
103
|
+
index '/' do
|
|
104
|
+
render 'index.rhtml', :body => 'BODY', :layout => false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
get('/').body.should == '<p>BODY</p>'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "should render templates" do
|
|
112
|
+
map do
|
|
113
|
+
index '/' do
|
|
114
|
+
render 'index.rhtml', :body => 'BODY'
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
get('/').body.should == '<html><p>BODY</p></html>'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
end
|
data/test/route_spec.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'kontrol'
|
|
2
|
+
|
|
3
|
+
describe Kontrol::Route do
|
|
4
|
+
|
|
5
|
+
it "should recognize a request" do
|
|
6
|
+
route = Kontrol::Route.new(:test, "/test", nil, nil)
|
|
7
|
+
request = Rack::Request.new('PATH_INFO' => '/test')
|
|
8
|
+
|
|
9
|
+
match = route.recognize(request)
|
|
10
|
+
|
|
11
|
+
match.should_not be_nil
|
|
12
|
+
match[0].should == '/test'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "should recognize a request by options" do
|
|
16
|
+
route = Kontrol::Route.new(:test, "/test", { :request_method => 'GET' }, nil)
|
|
17
|
+
request = Rack::Request.new('PATH_INFO' => '/test', 'REQUEST_METHOD' => 'GET')
|
|
18
|
+
|
|
19
|
+
match = route.recognize(request)
|
|
20
|
+
|
|
21
|
+
match.should_not be_nil
|
|
22
|
+
match[0].should == '/test'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "should recognize a request with groups" do
|
|
26
|
+
route = Kontrol::Route.new(:test, "/test/(.*)/(.*)", nil, nil)
|
|
27
|
+
request = Rack::Request.new('PATH_INFO' => '/test/me/here')
|
|
28
|
+
|
|
29
|
+
match = route.recognize(request)
|
|
30
|
+
|
|
31
|
+
match.should_not be_nil
|
|
32
|
+
match[0].should == '/test/me/here'
|
|
33
|
+
match[1].should == 'me'
|
|
34
|
+
match[2].should == 'here'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "should generate a path" do
|
|
38
|
+
route = Kontrol::Route.new(:test, "/test", nil, nil)
|
|
39
|
+
|
|
40
|
+
route.generate.should == '/test'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "should generate a path with groups" do
|
|
44
|
+
route = Kontrol::Route.new(:test, "/test/(.*)/me/(\d\d)", nil, nil)
|
|
45
|
+
|
|
46
|
+
route.generate(1, 22).should == '/test/1/me/22'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
|
data/test/router_spec.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require 'kontrol'
|
|
2
|
+
|
|
3
|
+
describe Kontrol::Router do
|
|
4
|
+
|
|
5
|
+
before :each do
|
|
6
|
+
@router = Kontrol::Router.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def request(env)
|
|
10
|
+
Rack::Request.new(env)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "should find a route" do
|
|
14
|
+
@router.test '/test'
|
|
15
|
+
route = @router.__find__(:test)
|
|
16
|
+
|
|
17
|
+
route.name.should == :test
|
|
18
|
+
route.pattern.should == '/test'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "should recognize a route" do
|
|
22
|
+
request = request('PATH_INFO' => '/test')
|
|
23
|
+
|
|
24
|
+
@router.test '/test'
|
|
25
|
+
route, match = @router.__recognize__(request)
|
|
26
|
+
|
|
27
|
+
route.name.should == :test
|
|
28
|
+
route.pattern.should == '/test'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "should recognize routes in right order" do
|
|
32
|
+
request = request('PATH_INFO' => '/test')
|
|
33
|
+
|
|
34
|
+
@router.root '/'
|
|
35
|
+
@router.test '/test'
|
|
36
|
+
|
|
37
|
+
route, match = @router.__recognize__(request)
|
|
38
|
+
|
|
39
|
+
route.name.should == :root
|
|
40
|
+
route.pattern.should == '/'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "should not recognize a not matching route" do
|
|
44
|
+
request = request('PATH_INFO' => '/test')
|
|
45
|
+
|
|
46
|
+
@router.root '/other'
|
|
47
|
+
|
|
48
|
+
route, match = @router.__recognize__(request)
|
|
49
|
+
|
|
50
|
+
route.should be_nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kontrol
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: "0.3"
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matthias Georgi
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2010-02-07 00:00:00 +01:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: Small web framework running on top of rack.
|
|
17
|
+
email: matti.georgi@gmail.com
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files:
|
|
23
|
+
- README.md
|
|
24
|
+
files:
|
|
25
|
+
- LICENSE
|
|
26
|
+
- README.md
|
|
27
|
+
- examples/git_app.ru
|
|
28
|
+
- examples/hello_world.ru
|
|
29
|
+
- examples/routing.ru
|
|
30
|
+
- examples/templates.ru
|
|
31
|
+
- examples/templates/layout.rhtml
|
|
32
|
+
- examples/templates/page.rhtml
|
|
33
|
+
- lib/kontrol.rb
|
|
34
|
+
- lib/kontrol/application.rb
|
|
35
|
+
- lib/kontrol/helpers.rb
|
|
36
|
+
- lib/kontrol/mime_types.rb
|
|
37
|
+
- lib/kontrol/route.rb
|
|
38
|
+
- lib/kontrol/router.rb
|
|
39
|
+
- lib/kontrol/template.rb
|
|
40
|
+
- test/application_spec.rb
|
|
41
|
+
- test/route_spec.rb
|
|
42
|
+
- test/router_spec.rb
|
|
43
|
+
has_rdoc: true
|
|
44
|
+
homepage: http://github.com/georgi/kontrol
|
|
45
|
+
licenses: []
|
|
46
|
+
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: "0"
|
|
57
|
+
version:
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: "0"
|
|
63
|
+
version:
|
|
64
|
+
requirements: []
|
|
65
|
+
|
|
66
|
+
rubyforge_project:
|
|
67
|
+
rubygems_version: 1.3.5
|
|
68
|
+
signing_key:
|
|
69
|
+
specification_version: 3
|
|
70
|
+
summary: a micro framework
|
|
71
|
+
test_files: []
|
|
72
|
+
|