kontrol 0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|