georgi-kontrol 0.1.6 → 0.2
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/README.md +59 -90
- data/examples/git_app.ru +3 -3
- data/examples/hello_world.ru +7 -2
- data/examples/routing.ru +4 -4
- data/examples/templates.ru +1 -1
- data/lib/kontrol.rb +1 -1
- data/lib/kontrol/application.rb +49 -51
- data/lib/kontrol/mime_types.rb +62 -0
- data/lib/kontrol/route.rb +28 -0
- data/lib/kontrol/router.rb +19 -33
- data/lib/kontrol/template.rb +8 -14
- data/test/application_spec.rb +59 -83
- data/test/route_spec.rb +50 -0
- data/test/router_spec.rb +53 -0
- metadata +13 -15
- data/.gitignore +0 -2
- data/examples/nested.ru +0 -23
- data/examples/pages/index.md +0 -5
- data/lib/kontrol/builder.rb +0 -78
data/README.md
CHANGED
@@ -2,21 +2,31 @@ Kontrol - a micro framework
|
|
2
2
|
===========================
|
3
3
|
|
4
4
|
Kontrol is a small web framework written in Ruby, which runs directly
|
5
|
-
on [Rack][
|
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.
|
6
8
|
|
7
|
-
All examples can be found in the
|
8
|
-
project, which is hosted on [
|
9
|
+
All examples can be found in the examples folder of the kontrol
|
10
|
+
project, which is hosted on [github][2].
|
9
11
|
|
10
12
|
## Quick Start
|
11
13
|
|
12
|
-
|
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`:
|
13
20
|
|
14
|
-
require 'kontrol'
|
15
|
-
|
16
21
|
class HelloWorld < Kontrol::Application
|
22
|
+
|
23
|
+
def time
|
24
|
+
Time.now.strftime "%H:%M:%S"
|
25
|
+
end
|
26
|
+
|
17
27
|
map do
|
18
|
-
|
19
|
-
"Hello World
|
28
|
+
root '/' do
|
29
|
+
text "<h1>Hello World at #{time}</h1>"
|
20
30
|
end
|
21
31
|
end
|
22
32
|
end
|
@@ -30,21 +40,19 @@ Now run:
|
|
30
40
|
Browse to `http://localhost:9292` and you will see "Hello World".
|
31
41
|
|
32
42
|
|
33
|
-
##
|
34
|
-
|
35
|
-
Kontrol is just a thin layer on top of Rack. It provides a routing
|
36
|
-
algorithm, a simple template mechanism and some convenience stuff to
|
37
|
-
work with [GitStore][1].
|
43
|
+
## Basics
|
38
44
|
|
39
45
|
A Kontrol application is a class, which provides some context to the
|
40
46
|
defined actions. You will probably use these methods:
|
41
47
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
48
56
|
|
49
57
|
|
50
58
|
## Routing
|
@@ -58,12 +66,12 @@ Create a file named `routing.ru`:
|
|
58
66
|
|
59
67
|
class Routing < Kontrol::Application
|
60
68
|
map do
|
61
|
-
|
62
|
-
"
|
69
|
+
pages '/pages/(.*)' do |name|
|
70
|
+
text "The path is #{ pages_path name }! "
|
63
71
|
end
|
64
|
-
|
65
|
-
|
66
|
-
"
|
72
|
+
|
73
|
+
archive '/(\d*)/(\d*)' do |year, month|
|
74
|
+
text "The path is #{ archive_path year, month }! "
|
67
75
|
end
|
68
76
|
end
|
69
77
|
end
|
@@ -74,52 +82,17 @@ Now run this application:
|
|
74
82
|
|
75
83
|
rackup routing.ru
|
76
84
|
|
77
|
-
|
78
85
|
You will now see, how regex groups and parameters are related. For
|
79
86
|
example if you browse to `localhost:9292/2008/12`, the app will
|
80
|
-
display `
|
87
|
+
display `The path is /2008/12`.
|
81
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.
|
82
92
|
|
83
|
-
|
93
|
+
For example the route `/page/(.*)` named page will recognize the path
|
94
|
+
`/page/about`, which can be generated by using `page_path('about')`.
|
84
95
|
|
85
|
-
Routes can be nested. This way you can avoid repeating patterns and
|
86
|
-
define handlers for a set of HTTP verbs. Each handler will be called
|
87
|
-
with the same arguments.
|
88
|
-
|
89
|
-
require 'kontrol'
|
90
|
-
|
91
|
-
class Nested < Kontrol::Application
|
92
|
-
map do
|
93
|
-
map '/blog' do
|
94
|
-
get '/archives' do
|
95
|
-
"The archives!"
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
map '/(.*)' do
|
100
|
-
get do |path|
|
101
|
-
"<form method='post'><input type='submit'/></form>"
|
102
|
-
end
|
103
|
-
|
104
|
-
post do |path|
|
105
|
-
"You posted to #{path}"
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
run Nested.new
|
112
|
-
|
113
|
-
Now run this app like:
|
114
|
-
|
115
|
-
rackup nested.ru
|
116
|
-
|
117
|
-
The second route catches all paths except the `/blog` route. Inside
|
118
|
-
the second route there are two different handlers for `GET` and `POST`
|
119
|
-
actions.
|
120
|
-
|
121
|
-
So if you browse to `/something`, you will see a submit button. After
|
122
|
-
submitting you will see the result of the second handler.
|
123
96
|
|
124
97
|
## Templates
|
125
98
|
|
@@ -146,7 +119,7 @@ Create a templates.ru file:
|
|
146
119
|
|
147
120
|
class Templates < Kontrol::Application
|
148
121
|
map do
|
149
|
-
|
122
|
+
page '/(.*)' do |name|
|
150
123
|
render "page.rhtml", :title => name.capitalize, :body => "This is the body!"
|
151
124
|
end
|
152
125
|
end
|
@@ -165,17 +138,17 @@ passed to the `render` call.
|
|
165
138
|
|
166
139
|
## Using GitStore
|
167
140
|
|
168
|
-
[GitStore][
|
141
|
+
[GitStore][3] is another library, which allows you to store code and
|
169
142
|
data in a convenient way in a git repository. The repository is
|
170
143
|
checked out into memory and any data may be saved back into the
|
171
144
|
repository.
|
172
145
|
|
173
|
-
Install [GitStore][
|
146
|
+
Install [GitStore][] by:
|
174
147
|
|
175
|
-
$ gem sources -a http://gems.github.com
|
176
|
-
$ sudo gem install
|
148
|
+
$ gem sources -a http://gems.github.com
|
149
|
+
$ sudo gem install georgi-git_store
|
177
150
|
|
178
|
-
We create a Markdown file name `
|
151
|
+
We create a Markdown file name `index.md`:
|
179
152
|
|
180
153
|
Hello World
|
181
154
|
===========
|
@@ -185,23 +158,24 @@ We create a Markdown file name `pages/index.md`:
|
|
185
158
|
We have now a simple page, which should be rendered as response. We
|
186
159
|
create a simple app in a file `git_app.ru`:
|
187
160
|
|
188
|
-
require 'kontrol'
|
189
|
-
require 'bluecloth'
|
190
|
-
|
191
161
|
class GitApp < Kontrol::Application
|
162
|
+
|
163
|
+
def initialize(path)
|
164
|
+
super
|
165
|
+
@store = GitStore.new(path)
|
166
|
+
end
|
167
|
+
|
192
168
|
map do
|
193
|
-
|
194
|
-
BlueCloth.new(store[
|
169
|
+
page '/(.*)' do |name|
|
170
|
+
text BlueCloth.new(@store[name + '.md']).to_html
|
195
171
|
end
|
196
172
|
end
|
197
173
|
end
|
198
|
-
|
199
|
-
run GitApp.new
|
200
174
|
|
201
|
-
Add all
|
175
|
+
Add all the page to your git repository:
|
202
176
|
|
203
177
|
git init
|
204
|
-
git add
|
178
|
+
git add index.md
|
205
179
|
git commit -m 'init'
|
206
180
|
|
207
181
|
Run the app:
|
@@ -211,15 +185,10 @@ Run the app:
|
|
211
185
|
Browse to `http://localhost:9292/index` and you will see the rendered
|
212
186
|
page generated from the markdown file.
|
213
187
|
|
214
|
-
This application runs straight from the git repository. You can
|
215
|
-
|
216
|
-
from your repo.
|
188
|
+
This application runs straight from the git repository. You can even
|
189
|
+
delete the page and it will still show up over the web.
|
217
190
|
|
218
191
|
|
219
|
-
[1]: http://github.com/
|
220
|
-
[2]: http://github.com/
|
221
|
-
[3]: http://github.com/georgi/
|
222
|
-
[4]: http://github.com/georgi/kontrol
|
223
|
-
[5]: http://github.com/chneukirchen/rack
|
224
|
-
[6]: http://github.com/chneukirchen/rack/tree/master/lib/rack/request.rb
|
225
|
-
[7]: http://github.com/chneukirchen/rack/tree/master/lib/rack/response.rb
|
192
|
+
[1]: http://github.com/chneukirchen/rack
|
193
|
+
[2]: http://github.com/georgi/kontrol
|
194
|
+
[3]: http://github.com/georgi/git_store
|
data/examples/git_app.ru
CHANGED
@@ -7,10 +7,10 @@ class GitApp < Kontrol::Application
|
|
7
7
|
super
|
8
8
|
@store = GitStore.new(path)
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
map do
|
12
|
-
|
13
|
-
BlueCloth.new(@store[
|
12
|
+
page '/(.*)' do |name|
|
13
|
+
text BlueCloth.new(@store[name + '.md']).to_html
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
data/examples/hello_world.ru
CHANGED
data/examples/routing.ru
CHANGED
@@ -2,12 +2,12 @@ require 'kontrol'
|
|
2
2
|
|
3
3
|
class Routing < Kontrol::Application
|
4
4
|
map do
|
5
|
-
|
6
|
-
"
|
5
|
+
pages '/pages/(.*)' do |name|
|
6
|
+
text "The path is #{ pages_path name }! "
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
"
|
9
|
+
archive '/(\d*)/(\d*)' do |year, month|
|
10
|
+
text "The path is #{ archive_path year, month }! "
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
data/examples/templates.ru
CHANGED
data/lib/kontrol.rb
CHANGED
data/lib/kontrol/application.rb
CHANGED
@@ -3,25 +3,18 @@ require 'digest/sha1'
|
|
3
3
|
module Kontrol
|
4
4
|
|
5
5
|
class Application
|
6
|
+
include Helpers
|
7
|
+
|
8
|
+
attr_reader :path
|
6
9
|
|
7
10
|
class << self
|
11
|
+
attr_accessor :router
|
12
|
+
|
8
13
|
def map(&block)
|
9
|
-
|
10
|
-
@map = Builder.new(&block)
|
11
|
-
else
|
12
|
-
@map
|
13
|
-
end
|
14
|
+
@router = Router.new(&block)
|
14
15
|
end
|
15
|
-
|
16
|
-
def call(env)
|
17
|
-
@map.call(env)
|
18
|
-
end
|
19
16
|
end
|
20
17
|
|
21
|
-
include Helpers
|
22
|
-
|
23
|
-
attr_reader :path
|
24
|
-
|
25
18
|
def initialize(path = '.')
|
26
19
|
@path = File.expand_path(path)
|
27
20
|
end
|
@@ -35,20 +28,20 @@ module Kontrol
|
|
35
28
|
Template.render(load_template(file), self, file, vars)
|
36
29
|
end
|
37
30
|
|
38
|
-
def render_layout(vars)
|
39
|
-
render_template(vars[:layout] || "layout.rhtml", vars)
|
40
|
-
end
|
41
|
-
|
42
31
|
# Render named template and insert into layout with given variables.
|
43
32
|
def render(name, vars = {})
|
44
33
|
content = render_template(name, vars)
|
34
|
+
layout = vars[:layout] || "layout.rhtml"
|
45
35
|
|
46
|
-
if name[0, 1] == '_'
|
47
|
-
content
|
36
|
+
if name[0, 1] == '_'
|
37
|
+
return content
|
38
|
+
elsif layout == false
|
39
|
+
response.body = content
|
48
40
|
else
|
49
|
-
vars.merge
|
50
|
-
render_layout(vars)
|
41
|
+
response.body = render_template(layout, vars.merge(:content => content))
|
51
42
|
end
|
43
|
+
|
44
|
+
response['Content-Length'] = response.body.size.to_s
|
52
45
|
end
|
53
46
|
|
54
47
|
def etag(string)
|
@@ -75,30 +68,24 @@ module Kontrol
|
|
75
68
|
end
|
76
69
|
end
|
77
70
|
|
78
|
-
def request
|
79
|
-
|
80
|
-
end
|
81
|
-
|
82
|
-
def
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
|
-
def
|
87
|
-
request.params
|
88
|
-
end
|
89
|
-
|
90
|
-
def cookies
|
91
|
-
request.cookies
|
92
|
-
end
|
71
|
+
def request ; Thread.current['request'] end
|
72
|
+
def response; Thread.current['response'] end
|
73
|
+
def params ; request.params end
|
74
|
+
def cookies ; request.cookies end
|
75
|
+
def session ; request.env['rack.session'] end
|
76
|
+
def post? ; request.post? end
|
77
|
+
def get? ; request.get? end
|
78
|
+
def put? ; request.put? end
|
79
|
+
def delete? ; request.delete? end
|
93
80
|
|
94
|
-
def
|
95
|
-
|
81
|
+
def text(s)
|
82
|
+
response.body = s
|
83
|
+
response['Content-Length'] = response.body.size.to_s
|
96
84
|
end
|
97
85
|
|
98
86
|
def redirect(path)
|
99
87
|
response['Location'] = path
|
100
88
|
response.status = 301
|
101
|
-
response.body = "Redirect to: #{path}"
|
102
89
|
end
|
103
90
|
|
104
91
|
def guess_content_type
|
@@ -106,31 +93,42 @@ module Kontrol
|
|
106
93
|
MIME_TYPES[ext] || 'text/html'
|
107
94
|
end
|
108
95
|
|
96
|
+
def router
|
97
|
+
self.class.router
|
98
|
+
end
|
99
|
+
|
109
100
|
def call(env)
|
110
101
|
Thread.current['request'] = Rack::Request.new(env)
|
111
|
-
Thread.current['response'] = Rack::Response.new([],
|
112
|
-
|
113
|
-
env['kontrol.app'] = self
|
102
|
+
Thread.current['response'] = Rack::Response.new([], 200, { 'Content-Type' => '' })
|
114
103
|
|
115
|
-
|
104
|
+
route, match = router.__recognize__(request)
|
116
105
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
response.body
|
106
|
+
if route
|
107
|
+
method = "process_#{route.name}"
|
108
|
+
self.class.send(:define_method, method, &route.block)
|
109
|
+
send(method, *match.to_a[1..-1])
|
110
|
+
else
|
111
|
+
response.body = "<h1>404 - Page Not Found</h1>"
|
123
112
|
response['Content-Length'] = response.body.size.to_s
|
113
|
+
response.status = 404
|
124
114
|
end
|
125
|
-
|
126
|
-
response['Content-Type'] = guess_content_type if response['Content-Type'].empty?
|
127
115
|
|
116
|
+
response['Content-Type'] = guess_content_type if response['Content-Type'].empty?
|
128
117
|
response.finish
|
129
118
|
end
|
130
119
|
|
131
120
|
def inspect
|
132
121
|
"#<#{self.class.name} @path=#{path}>"
|
133
122
|
end
|
123
|
+
|
124
|
+
def method_missing(name, *args, &block)
|
125
|
+
if match = name.to_s.match(/^(.*)_path$/)
|
126
|
+
router.__find__(match[1]).generate(*args)
|
127
|
+
else
|
128
|
+
super
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
134
132
|
end
|
135
133
|
|
136
134
|
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,28 @@
|
|
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
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/lib/kontrol/router.rb
CHANGED
@@ -1,47 +1,33 @@
|
|
1
1
|
module Kontrol
|
2
2
|
|
3
3
|
class Router
|
4
|
+
|
5
|
+
def initialize(&block)
|
6
|
+
@routes = []
|
7
|
+
@map = {}
|
4
8
|
|
5
|
-
|
6
|
-
@routing = []
|
9
|
+
instance_eval(&block) if block
|
7
10
|
end
|
8
11
|
|
9
|
-
def
|
10
|
-
@
|
12
|
+
def __find__(name)
|
13
|
+
@map[name.to_sym]
|
11
14
|
end
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
:accept => 'HTTP_ACCEPT',
|
18
|
-
:query => 'QUERY_STRING',
|
19
|
-
:content_type => 'CONTENT_TYPE'
|
20
|
-
}
|
21
|
-
|
22
|
-
def options_match(env, options)
|
23
|
-
options.all? do |name, pattern|
|
24
|
-
value = env[OPTION_MAPPING[name] || name]
|
25
|
-
value and pattern.match(value)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def call(env)
|
30
|
-
path = env["PATH_INFO"].to_s.squeeze("/")
|
31
|
-
|
32
|
-
@routing.each do |pattern, app, options|
|
33
|
-
if (match = path.match(/^#{pattern}/)) && options_match(env, options)
|
34
|
-
env = env.dup
|
35
|
-
(env['kontrol.args'] ||= []).concat(match.to_a[1..-1])
|
36
|
-
if match[0] == pattern
|
37
|
-
env["SCRIPT_NAME"] += match[0]
|
38
|
-
env["PATH_INFO"] = path[match[0].size..-1]
|
39
|
-
end
|
40
|
-
return app.call(env)
|
16
|
+
def __recognize__(request)
|
17
|
+
@routes.each do |route|
|
18
|
+
if match = route.recognize(request)
|
19
|
+
return route, match
|
41
20
|
end
|
42
21
|
end
|
22
|
+
|
23
|
+
return nil
|
24
|
+
end
|
43
25
|
|
44
|
-
|
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
|
45
31
|
end
|
46
32
|
|
47
33
|
end
|
data/lib/kontrol/template.rb
CHANGED
@@ -6,8 +6,12 @@ module Kontrol
|
|
6
6
|
include Helpers
|
7
7
|
|
8
8
|
# Initialize this template with an ERB instance.
|
9
|
-
def initialize(app)
|
9
|
+
def initialize(app, vars)
|
10
10
|
@__app__ = app
|
11
|
+
|
12
|
+
vars.each do |k, v|
|
13
|
+
instance_variable_set "@#{k}", v
|
14
|
+
end
|
11
15
|
end
|
12
16
|
|
13
17
|
def __binding__
|
@@ -15,19 +19,9 @@ module Kontrol
|
|
15
19
|
end
|
16
20
|
|
17
21
|
def self.render(erb, app, file, vars)
|
18
|
-
template = Template.new(app)
|
19
|
-
|
20
|
-
|
21
|
-
template.send :instance_variable_set, "@#{name}", value
|
22
|
-
end
|
23
|
-
|
24
|
-
result = erb.result(template.__binding__)
|
25
|
-
|
26
|
-
for name in template.instance_variables
|
27
|
-
vars[name[1..-1]] = template.send(:instance_variable_get, name) unless name == '@__app__'
|
28
|
-
end
|
29
|
-
|
30
|
-
return result
|
22
|
+
template = Template.new(app, vars)
|
23
|
+
|
24
|
+
return erb.result(template.__binding__)
|
31
25
|
|
32
26
|
rescue => e
|
33
27
|
e.backtrace.each do |s|
|
data/test/application_spec.rb
CHANGED
@@ -1,135 +1,111 @@
|
|
1
1
|
require 'kontrol'
|
2
|
-
require 'git_store'
|
3
2
|
require 'rack/mock'
|
4
3
|
|
5
|
-
describe Kontrol::
|
6
|
-
|
7
|
-
REPO = File.exist?('/tmp') ? '/tmp/test' : File.expand_path(File.dirname(__FILE__) + '/repo')
|
4
|
+
describe Kontrol::Application do
|
8
5
|
|
9
6
|
before do
|
10
|
-
FileUtils.rm_rf REPO
|
11
|
-
Dir.mkdir REPO
|
12
|
-
Dir.chdir REPO
|
13
|
-
`git init`
|
14
|
-
|
15
|
-
ENV['RACK_ENV'] = 'production'
|
16
|
-
|
17
7
|
@class = Class.new(Kontrol::Application)
|
18
|
-
@app = @class.new
|
8
|
+
@app = @class.new
|
19
9
|
@request = Rack::MockRequest.new(@app)
|
20
10
|
end
|
21
11
|
|
22
12
|
def get(*args)
|
23
13
|
@request.get(*args)
|
24
14
|
end
|
25
|
-
|
26
|
-
def post(*args)
|
27
|
-
@request.post(*args)
|
28
|
-
end
|
29
|
-
|
30
|
-
def delete(*args)
|
31
|
-
@request.delete(*args)
|
32
|
-
end
|
33
|
-
|
34
|
-
def put(*args)
|
35
|
-
@request.put(*args)
|
36
|
-
end
|
37
15
|
|
38
16
|
def map(&block)
|
39
17
|
@class.map(&block)
|
40
18
|
end
|
41
|
-
|
42
|
-
def file(file, data)
|
43
|
-
FileUtils.mkpath(File.dirname(file))
|
44
|
-
open(file, 'w') { |io| io << data }
|
45
|
-
`git add #{file}`
|
46
|
-
`git commit -m 'spec'`
|
47
|
-
File.unlink(file)
|
48
|
-
end
|
49
19
|
|
50
|
-
it "should
|
51
|
-
map do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
20
|
+
it "should do simple pattern matching" do
|
21
|
+
map do
|
22
|
+
one '/one' do
|
23
|
+
response.body = 'one'
|
24
|
+
end
|
25
|
+
|
26
|
+
two '/two' do
|
27
|
+
response.body = 'two'
|
28
|
+
end
|
56
29
|
end
|
57
30
|
|
58
31
|
get("/one").body.should == 'one'
|
59
|
-
|
60
|
-
delete("/one").body.should == 'three'
|
61
|
-
put("/two").body.should == 'four'
|
32
|
+
get("/two").body.should == 'two'
|
62
33
|
end
|
63
34
|
|
64
|
-
it "should
|
35
|
+
it "should have a router" do
|
65
36
|
map do
|
66
|
-
|
37
|
+
root '/'
|
67
38
|
end
|
68
39
|
|
69
|
-
|
70
|
-
get('/').status.should == 301
|
40
|
+
@class.router.should_not be_nil
|
71
41
|
end
|
42
|
+
|
43
|
+
it "should generate paths" do
|
44
|
+
map do
|
45
|
+
root '/'
|
46
|
+
about '/about'
|
47
|
+
page '/page/(.*)'
|
48
|
+
end
|
72
49
|
|
73
|
-
|
74
|
-
|
50
|
+
@app.root_path.should == '/'
|
51
|
+
@app.about_path.should == '/about'
|
52
|
+
@app.page_path('world').should == '/page/world'
|
53
|
+
end
|
75
54
|
|
55
|
+
it "should redirect" do
|
76
56
|
map do
|
77
|
-
|
78
|
-
|
57
|
+
root '/' do
|
58
|
+
redirect 'x'
|
79
59
|
end
|
80
60
|
end
|
81
61
|
|
82
|
-
get(
|
83
|
-
|
84
|
-
file 'file', 'changed'
|
85
|
-
|
86
|
-
get("/file").body.should == 'changed'
|
62
|
+
get('/')['Location'].should == 'x'
|
63
|
+
get('/').status.should == 301
|
87
64
|
end
|
88
65
|
|
89
|
-
it "should
|
90
|
-
file 'assets/javascripts/index.js', 'index'
|
91
|
-
file 'assets/stylesheets/styles.css', 'styles'
|
92
|
-
file 'assets/file', 'file'
|
93
|
-
|
66
|
+
it "should respond with not modified" do
|
94
67
|
map do
|
95
|
-
|
96
|
-
|
97
|
-
if_none_match(etag(
|
68
|
+
assets '/assets/(.*)' do
|
69
|
+
script = "script"
|
70
|
+
if_none_match(etag(script)) do
|
71
|
+
text script
|
72
|
+
end
|
98
73
|
end
|
74
|
+
end
|
99
75
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
76
|
+
get("/assets/test.js").body.should == 'script'
|
77
|
+
|
78
|
+
etag = get("/assets/file")['Etag']
|
79
|
+
get("/assets/file", 'HTTP_IF_NONE_MATCH' => etag).status.should == 304
|
80
|
+
end
|
104
81
|
|
105
|
-
|
106
|
-
|
107
|
-
|
82
|
+
it "should render text" do
|
83
|
+
map do
|
84
|
+
index '/' do
|
85
|
+
text "Hello"
|
108
86
|
end
|
109
87
|
end
|
110
88
|
|
111
|
-
get(
|
112
|
-
get(
|
113
|
-
get("/assets/stylesheets.css").body.should == 'styles'
|
114
|
-
get("/assets/stylesheets.css")['Content-Type'].should == 'text/css'
|
115
|
-
|
116
|
-
get("/assets/file").body.should == 'file'
|
117
|
-
etag = get("/assets/file")['Etag']
|
118
|
-
|
119
|
-
get("/assets/file", 'HTTP_IF_NONE_MATCH' => etag).status.should == 304
|
89
|
+
get('/').body.should == 'Hello'
|
90
|
+
get('/')['Content-Length'].should == '5'
|
120
91
|
end
|
121
92
|
|
122
93
|
it "should render templates" do
|
123
|
-
|
124
|
-
|
125
|
-
|
94
|
+
def @app.load_template(file)
|
95
|
+
if file == "layout.rhtml"
|
96
|
+
ERB.new '<html><%= @content %></html>'
|
97
|
+
else
|
98
|
+
ERB.new '<p><%= @body %></p>'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
126
102
|
map do
|
127
|
-
|
103
|
+
index '/' do
|
128
104
|
render 'index.rhtml', :body => 'BODY'
|
129
105
|
end
|
130
106
|
end
|
131
107
|
|
132
|
-
get('/').body.should == 'BODY'
|
108
|
+
get('/').body.should == '<html><p>BODY</p></html>'
|
133
109
|
end
|
134
110
|
|
135
111
|
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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: georgi-kontrol
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: "0.2"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthias Georgi
|
@@ -9,11 +9,11 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-04-
|
12
|
+
date: 2008-04-13 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
16
|
-
description:
|
16
|
+
description: Small web framework running on top of rack.
|
17
17
|
email: matti.georgi@gmail.com
|
18
18
|
executables: []
|
19
19
|
|
@@ -22,26 +22,24 @@ extensions: []
|
|
22
22
|
extra_rdoc_files:
|
23
23
|
- README.md
|
24
24
|
files:
|
25
|
-
- .gitignore
|
26
25
|
- LICENSE
|
27
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
|
28
33
|
- lib/kontrol.rb
|
29
34
|
- lib/kontrol/application.rb
|
30
|
-
- lib/kontrol/builder.rb
|
31
35
|
- lib/kontrol/helpers.rb
|
36
|
+
- lib/kontrol/mime_types.rb
|
37
|
+
- lib/kontrol/route.rb
|
32
38
|
- lib/kontrol/router.rb
|
33
39
|
- lib/kontrol/template.rb
|
34
40
|
- test/application_spec.rb
|
35
|
-
-
|
36
|
-
-
|
37
|
-
- examples/pages/index.md
|
38
|
-
- examples/git_app.ru
|
39
|
-
- examples/templates.ru
|
40
|
-
- examples/templates
|
41
|
-
- examples/templates/page.rhtml
|
42
|
-
- examples/templates/layout.rhtml
|
43
|
-
- examples/nested.ru
|
44
|
-
- examples/hello_world.ru
|
41
|
+
- test/route_spec.rb
|
42
|
+
- test/router_spec.rb
|
45
43
|
has_rdoc: true
|
46
44
|
homepage: http://github.com/georgi/kontrol
|
47
45
|
post_install_message:
|
data/.gitignore
DELETED
data/examples/nested.ru
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
require 'kontrol'
|
2
|
-
|
3
|
-
class Nested < Kontrol::Application
|
4
|
-
map do
|
5
|
-
map '/blog' do
|
6
|
-
get '/archives' do
|
7
|
-
"The archives!"
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
map '(.*)' do
|
12
|
-
get do |path|
|
13
|
-
"<form method='post'><input type='submit'/></form>"
|
14
|
-
end
|
15
|
-
|
16
|
-
post do |path|
|
17
|
-
"You called #{path}"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
run Nested.new
|
data/examples/pages/index.md
DELETED
data/lib/kontrol/builder.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
module Kontrol
|
2
|
-
|
3
|
-
def self.map(&block)
|
4
|
-
Builder.new(&block)
|
5
|
-
end
|
6
|
-
|
7
|
-
class Builder
|
8
|
-
|
9
|
-
def initialize(&block)
|
10
|
-
@router = Router.new
|
11
|
-
@ins = []
|
12
|
-
instance_eval(&block) if block
|
13
|
-
end
|
14
|
-
|
15
|
-
def use(middleware, *args, &block)
|
16
|
-
@ins << lambda { |app| middleware.new(app, *args, &block) }
|
17
|
-
end
|
18
|
-
|
19
|
-
def run(app)
|
20
|
-
@ins << app
|
21
|
-
end
|
22
|
-
|
23
|
-
def get(*args, &block)
|
24
|
-
map_method(:get, *args, &block)
|
25
|
-
end
|
26
|
-
|
27
|
-
def put(*args, &block)
|
28
|
-
map_method(:put, *args, &block)
|
29
|
-
end
|
30
|
-
|
31
|
-
def post(*args, &block)
|
32
|
-
map_method(:post, *args, &block)
|
33
|
-
end
|
34
|
-
|
35
|
-
def delete(*args, &block)
|
36
|
-
map_method(:delete, *args, &block)
|
37
|
-
end
|
38
|
-
|
39
|
-
def map(pattern, &block)
|
40
|
-
@router.map(pattern, Builder.new(&block))
|
41
|
-
end
|
42
|
-
|
43
|
-
def call(env)
|
44
|
-
to_app.call(env)
|
45
|
-
end
|
46
|
-
|
47
|
-
def to_app
|
48
|
-
@ins.reverse.inject(@router) { |a, e| e.call(a) }
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def method_from_proc(obj, proc)
|
54
|
-
name = "proc_#{proc.object_id}"
|
55
|
-
unless obj.respond_to?(name)
|
56
|
-
singleton = class << obj; self; end
|
57
|
-
singleton.send(:define_method, name, &proc)
|
58
|
-
end
|
59
|
-
obj.method(name)
|
60
|
-
end
|
61
|
-
|
62
|
-
def map_method(method, pattern = '.*', options = {}, &block)
|
63
|
-
on(pattern, options.merge(:method => method.to_s.upcase), &block)
|
64
|
-
end
|
65
|
-
|
66
|
-
def on(pattern = '.*', options = {}, &block)
|
67
|
-
wrap = lambda do |env|
|
68
|
-
env['kontrol.app'] or raise "no kontrol.app given"
|
69
|
-
meth = method_from_proc(env['kontrol.app'], block)
|
70
|
-
body = meth.call(*env['kontrol.args'])
|
71
|
-
[200, {}, body]
|
72
|
-
end
|
73
|
-
@router.map(pattern, wrap, options)
|
74
|
-
end
|
75
|
-
|
76
|
-
end
|
77
|
-
|
78
|
-
end
|