georgi-kontrol 0.1
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/.gitignore +1 -0
- data/LICENSE +18 -0
- data/README.md +113 -0
- data/lib/kontrol/application.rb +247 -0
- data/lib/kontrol/builder.rb +78 -0
- data/lib/kontrol/helpers.rb +41 -0
- data/lib/kontrol/router.rb +50 -0
- data/lib/kontrol/template.rb +48 -0
- data/lib/kontrol.rb +12 -0
- data/test/application_spec.rb +122 -0
- metadata +62 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*~
|
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,113 @@
|
|
1
|
+
Kontrol - a micro framework
|
2
|
+
===========================
|
3
|
+
|
4
|
+
Kontrol is a small web framework written in Ruby, which runs directly
|
5
|
+
on Rack. It provides a simple pattern matching algorithm for routing
|
6
|
+
and uses GitStore as data storage.
|
7
|
+
|
8
|
+
## Quick Start
|
9
|
+
|
10
|
+
Create a config.ru file and try this little "Hello World" app:
|
11
|
+
|
12
|
+
require 'kontrol'
|
13
|
+
|
14
|
+
run Kontrol::Application.new do
|
15
|
+
get '/' do
|
16
|
+
"Hello World!"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Now run `rackup` and browse to `http://localhost:9292` you will see the "Hello World".
|
21
|
+
|
22
|
+
|
23
|
+
## Routing
|
24
|
+
|
25
|
+
Routing is just as simple as using regular expressions with
|
26
|
+
groups. Each group will be provided as argument to the block.
|
27
|
+
|
28
|
+
Some examples:
|
29
|
+
|
30
|
+
require 'kontrol'
|
31
|
+
|
32
|
+
run Kontrol::Application.new do
|
33
|
+
get '/pages/(.*)' do |name|
|
34
|
+
"This is the page #{name}!"
|
35
|
+
end
|
36
|
+
|
37
|
+
get '/(\d*)/(\d*)', :content_type => 'text/html' do |year, month|
|
38
|
+
"Archive for #{year} #{month}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
The second route has the requirement, that the `content-type` header
|
43
|
+
should be 'text/html'.
|
44
|
+
|
45
|
+
|
46
|
+
## Nested Routes
|
47
|
+
|
48
|
+
You may nest your routes like you want:
|
49
|
+
|
50
|
+
require 'kontrol'
|
51
|
+
|
52
|
+
run Kontrol::Application.new do
|
53
|
+
map '/blog' do
|
54
|
+
get '/archives' do
|
55
|
+
"The archives!"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
This app can be reached at '/blog/archives'.
|
61
|
+
|
62
|
+
|
63
|
+
## Storing your data in a git repository
|
64
|
+
|
65
|
+
Using GitStore for your code and data is a convenient way for
|
66
|
+
developing small scale apps. The whole repository is loaded into the
|
67
|
+
memory and any change in the repository is reflected immediately in
|
68
|
+
you in-memory copy.
|
69
|
+
|
70
|
+
Just init a new repo: `git init`
|
71
|
+
|
72
|
+
Now we create a template named `templates/layout.rhtml`:
|
73
|
+
|
74
|
+
<html>
|
75
|
+
<body>
|
76
|
+
<%= @content %>
|
77
|
+
</body>
|
78
|
+
</html>
|
79
|
+
|
80
|
+
And now another template named `templates/page.rhtml`:
|
81
|
+
|
82
|
+
<h1><%= @title %></h1>
|
83
|
+
<%= @body %>
|
84
|
+
|
85
|
+
Now we create a Markdown file name `pages/index.md`:
|
86
|
+
|
87
|
+
Hello World from **Markdown** !
|
88
|
+
|
89
|
+
|
90
|
+
Create a config.ru file:
|
91
|
+
|
92
|
+
require 'kontrol'
|
93
|
+
|
94
|
+
run Kontrol::Application.new do
|
95
|
+
get '/(.*)' do |name|
|
96
|
+
body = store["pages/#{name}.md"]
|
97
|
+
render "page.rhtml", :title => name.capitalize, :body => body
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
Add all these files to your repo:
|
102
|
+
|
103
|
+
git add templates/layout.rhtml
|
104
|
+
git add templates/page.rhtml
|
105
|
+
git add pages/index.md
|
106
|
+
git add config.ru
|
107
|
+
git commit -m 'added templates and config.ru'
|
108
|
+
|
109
|
+
Now just run `rackup` and browse to `http://localhost:9292/index`
|
110
|
+
|
111
|
+
You will see the rendered template with the inserted content of the
|
112
|
+
Markdown file.
|
113
|
+
|
@@ -0,0 +1,247 @@
|
|
1
|
+
module Kontrol
|
2
|
+
|
3
|
+
class Application
|
4
|
+
|
5
|
+
include Helpers
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def config_reader(file, *names)
|
10
|
+
names.each do |name|
|
11
|
+
name = name.to_s
|
12
|
+
define_method(name) { config[file][name] }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :path, :store, :last_commit
|
18
|
+
|
19
|
+
config_reader 'assets.yml', :javascript_files, :stylesheet_files
|
20
|
+
|
21
|
+
def initialize(options = {}, &block)
|
22
|
+
options.each do |k, v|
|
23
|
+
send "#{k}=", v
|
24
|
+
end
|
25
|
+
|
26
|
+
@mtime = {}
|
27
|
+
@last_mtime = Time.now
|
28
|
+
@path = File.expand_path('.')
|
29
|
+
|
30
|
+
@store = GitStore.new(path)
|
31
|
+
@store.load
|
32
|
+
|
33
|
+
map(&block) if block
|
34
|
+
end
|
35
|
+
|
36
|
+
def assets
|
37
|
+
store['assets'] ||= GitStore::Tree.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def config
|
41
|
+
store['config'] ||= GitStore::Tree.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def templates
|
45
|
+
store['templates'] ||= GitStore::Tree.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def repo
|
49
|
+
@store.repo
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_reload
|
53
|
+
commit = store.repo.commits('master', 1)[0]
|
54
|
+
|
55
|
+
if commit and (last_commit.nil? or last_commit.id != commit.id)
|
56
|
+
@last_commit = commit
|
57
|
+
@last_mtime = last_commit.committed_date
|
58
|
+
@mtime = {}
|
59
|
+
store.load
|
60
|
+
load_store_from_disk
|
61
|
+
elsif ENV['RACK_ENV'] != 'production'
|
62
|
+
load_store_from_disk
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_store_from_disk
|
67
|
+
store.each_with_path do |blob, path|
|
68
|
+
path = "#{self.path}/#{path}"
|
69
|
+
if File.exist?(path)
|
70
|
+
mtime = File.mtime(path)
|
71
|
+
if mtime != @mtime[path]
|
72
|
+
@mtime[path] = mtime
|
73
|
+
@last_mtime = mtime if mtime > @last_mtime
|
74
|
+
blob.load(File.read(path))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def camelize(str)
|
81
|
+
str.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Render template with given variables.
|
85
|
+
def render_template(file, vars)
|
86
|
+
templates[file] or raise "template #{file} not found"
|
87
|
+
Template.render(templates[file], self, file, vars)
|
88
|
+
end
|
89
|
+
|
90
|
+
def render_layout(vars)
|
91
|
+
render_template(vars[:layout] || "layout.rhtml", vars)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Render named template and insert into layout with given variables.
|
95
|
+
def render(name, vars = {})
|
96
|
+
content = render_template(name, vars)
|
97
|
+
if name[0, 1] == '_' || name.match(/\.rxml$/)
|
98
|
+
content
|
99
|
+
else
|
100
|
+
vars.merge!(:content => content)
|
101
|
+
render_layout(vars)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def if_modified_since(date = @last_mtime.httpdate)
|
106
|
+
response['Last-Modified'] = date
|
107
|
+
if request.env['HTTP_IF_MODIFIED_SINCE'] == date
|
108
|
+
response.status = 304
|
109
|
+
else
|
110
|
+
yield
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def render_javascripts
|
115
|
+
if_modified_since do
|
116
|
+
javascript_files.map { |file| assets["javascripts/#{file.strip}.js"] }.join
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def render_stylesheets
|
121
|
+
if_modified_since do
|
122
|
+
stylesheet_files.map { |file| assets["stylesheets/#{file.strip}.css"] }.join
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def request
|
127
|
+
Thread.current['request']
|
128
|
+
end
|
129
|
+
|
130
|
+
def response
|
131
|
+
Thread.current['response']
|
132
|
+
end
|
133
|
+
|
134
|
+
def params
|
135
|
+
request.params
|
136
|
+
end
|
137
|
+
|
138
|
+
def cookies
|
139
|
+
request.cookies
|
140
|
+
end
|
141
|
+
|
142
|
+
def session
|
143
|
+
request.env['rack.session']
|
144
|
+
end
|
145
|
+
|
146
|
+
def redirect(path)
|
147
|
+
response['Location'] = path
|
148
|
+
response.status = 301
|
149
|
+
response.body = "Redirect to: #{path}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def guess_content_type
|
153
|
+
ext = File.extname(request.path_info)[1..-1]
|
154
|
+
MIME_TYPES[ext] || 'text/html'
|
155
|
+
end
|
156
|
+
|
157
|
+
def map(&block)
|
158
|
+
@map = Builder.new(&block)
|
159
|
+
end
|
160
|
+
|
161
|
+
def call(env)
|
162
|
+
Thread.current['request'] = Rack::Request.new(env)
|
163
|
+
Thread.current['response'] = Rack::Response.new([], nil, { 'Content-Type' => '' })
|
164
|
+
|
165
|
+
check_reload
|
166
|
+
|
167
|
+
env['kontrol.app'] = self
|
168
|
+
|
169
|
+
map = @map || store['map.rb'] or raise "no map defined"
|
170
|
+
|
171
|
+
status, header, body = map.call(env)
|
172
|
+
|
173
|
+
response.status = status if response.status.nil?
|
174
|
+
response.header.merge!(header)
|
175
|
+
response.body = body if response.body.empty?
|
176
|
+
response['Content-Type'] = guess_content_type if response['Content-Type'].empty?
|
177
|
+
|
178
|
+
response.finish
|
179
|
+
end
|
180
|
+
|
181
|
+
def inspect
|
182
|
+
"#<Kontrol::Application @path=#{path}>"
|
183
|
+
end
|
184
|
+
|
185
|
+
MIME_TYPES = {
|
186
|
+
"ai" => "application/postscript",
|
187
|
+
"asc" => "text/plain",
|
188
|
+
"avi" => "video/x-msvideo",
|
189
|
+
"bin" => "application/octet-stream",
|
190
|
+
"bmp" => "image/bmp",
|
191
|
+
"class" => "application/octet-stream",
|
192
|
+
"cer" => "application/pkix-cert",
|
193
|
+
"crl" => "application/pkix-crl",
|
194
|
+
"crt" => "application/x-x509-ca-cert",
|
195
|
+
"css" => "text/css",
|
196
|
+
"dms" => "application/octet-stream",
|
197
|
+
"doc" => "application/msword",
|
198
|
+
"dvi" => "application/x-dvi",
|
199
|
+
"eps" => "application/postscript",
|
200
|
+
"etx" => "text/x-setext",
|
201
|
+
"exe" => "application/octet-stream",
|
202
|
+
"gif" => "image/gif",
|
203
|
+
"htm" => "text/html",
|
204
|
+
"html" => "text/html",
|
205
|
+
"ico" => "image/x-icon",
|
206
|
+
"jpe" => "image/jpeg",
|
207
|
+
"jpeg" => "image/jpeg",
|
208
|
+
"jpg" => "image/jpeg",
|
209
|
+
"js" => "text/javascript",
|
210
|
+
"lha" => "application/octet-stream",
|
211
|
+
"lzh" => "application/octet-stream",
|
212
|
+
"mov" => "video/quicktime",
|
213
|
+
"mp3" => "audio/mpeg",
|
214
|
+
"mpe" => "video/mpeg",
|
215
|
+
"mpeg" => "video/mpeg",
|
216
|
+
"mpg" => "video/mpeg",
|
217
|
+
"pbm" => "image/x-portable-bitmap",
|
218
|
+
"pdf" => "application/pdf",
|
219
|
+
"pgm" => "image/x-portable-graymap",
|
220
|
+
"png" => "image/png",
|
221
|
+
"pnm" => "image/x-portable-anymap",
|
222
|
+
"ppm" => "image/x-portable-pixmap",
|
223
|
+
"ppt" => "application/vnd.ms-powerpoint",
|
224
|
+
"ps" => "application/postscript",
|
225
|
+
"qt" => "video/quicktime",
|
226
|
+
"ras" => "image/x-cmu-raster",
|
227
|
+
"rb" => "text/plain",
|
228
|
+
"rd" => "text/plain",
|
229
|
+
"rtf" => "application/rtf",
|
230
|
+
"rss" => "application/rss+xml",
|
231
|
+
"sgm" => "text/sgml",
|
232
|
+
"sgml" => "text/sgml",
|
233
|
+
"tif" => "image/tiff",
|
234
|
+
"tiff" => "image/tiff",
|
235
|
+
"txt" => "text/plain",
|
236
|
+
"xbm" => "image/x-xbitmap",
|
237
|
+
"xls" => "application/vnd.ms-excel",
|
238
|
+
"xml" => "text/xml",
|
239
|
+
"xpm" => "image/x-xpixmap",
|
240
|
+
"xwd" => "image/x-xwindowdump",
|
241
|
+
"zip" => "application/zip",
|
242
|
+
}
|
243
|
+
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
|
@@ -0,0 +1,78 @@
|
|
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
|
@@ -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,50 @@
|
|
1
|
+
module Kontrol
|
2
|
+
|
3
|
+
class Router
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@routing = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def map(pattern, block, options = {})
|
10
|
+
@routing << [pattern, block, options]
|
11
|
+
end
|
12
|
+
|
13
|
+
OPTION_MAPPING = {
|
14
|
+
:method => 'REQUEST_METHOD',
|
15
|
+
:port => 'SERVER_PORT',
|
16
|
+
:host => 'HTTP_HOST',
|
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
|
+
for pattern, app, options in @routing
|
33
|
+
match = path.match(/^#{pattern}/)
|
34
|
+
if match and options_match(env, options)
|
35
|
+
env = env.dup
|
36
|
+
(env['kontrol.args'] ||= []).concat(match.to_a[1..-1])
|
37
|
+
if match[0] == pattern
|
38
|
+
env["SCRIPT_NAME"] += match[0]
|
39
|
+
env["PATH_INFO"] = path[match[0].size..-1]
|
40
|
+
end
|
41
|
+
return app.call(env)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
[404, {"Content-Type" => "text/plain"}, ["Not Found: #{env['REQUEST_URI']}"]]
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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)
|
10
|
+
@__app__ = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def __binding__
|
14
|
+
binding
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.render(erb, app, file, vars)
|
18
|
+
template = Template.new(app)
|
19
|
+
|
20
|
+
for name, value in vars
|
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
|
31
|
+
|
32
|
+
rescue => e
|
33
|
+
e.backtrace.each do |s|
|
34
|
+
s.gsub!('(erb)', file)
|
35
|
+
end
|
36
|
+
raise e
|
37
|
+
end
|
38
|
+
|
39
|
+
def method_missing(id, *args, &block)
|
40
|
+
if @__app__.respond_to?(id)
|
41
|
+
return @__app__.send(id, *args, &block)
|
42
|
+
end
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
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
|
+
require 'git_store'
|
7
|
+
|
8
|
+
require 'kontrol/helpers'
|
9
|
+
require 'kontrol/template'
|
10
|
+
require 'kontrol/application'
|
11
|
+
require 'kontrol/builder'
|
12
|
+
require 'kontrol/router'
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'kontrol'
|
2
|
+
require 'rack/mock'
|
3
|
+
|
4
|
+
describe Kontrol::Builder do
|
5
|
+
|
6
|
+
REPO = File.expand_path(File.dirname(__FILE__) + '/test_repo')
|
7
|
+
|
8
|
+
before do
|
9
|
+
FileUtils.rm_rf REPO
|
10
|
+
Dir.mkdir REPO
|
11
|
+
Dir.chdir REPO
|
12
|
+
`git init`
|
13
|
+
|
14
|
+
@app = Kontrol::Application.new
|
15
|
+
@request = Rack::MockRequest.new(@app)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(*args)
|
19
|
+
@request.get(*args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def post(*args)
|
23
|
+
@request.post(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(*args)
|
27
|
+
@request.delete(*args)
|
28
|
+
end
|
29
|
+
|
30
|
+
def put(*args)
|
31
|
+
@request.put(*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def map(&block)
|
35
|
+
@app.map(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def file(file, data)
|
39
|
+
FileUtils.mkpath(File.dirname(file))
|
40
|
+
open(file, 'w') { |io| io << data }
|
41
|
+
`git add #{file}`
|
42
|
+
`git commit -m 'spec'`
|
43
|
+
File.unlink(file)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should understand all verbs" do
|
47
|
+
map do
|
48
|
+
get '/one' do 'one' end
|
49
|
+
post '/one' do 'two' end
|
50
|
+
delete '/one' do 'three' end
|
51
|
+
put '/two' do 'four' end
|
52
|
+
end
|
53
|
+
|
54
|
+
get("/one").body.should == 'one'
|
55
|
+
post("/one").body.should == 'two'
|
56
|
+
delete("/one").body.should == 'three'
|
57
|
+
put("/two").body.should == 'four'
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should reload after a commit" do
|
61
|
+
file 'file', 'file'
|
62
|
+
|
63
|
+
map do
|
64
|
+
get '/(.*)' do |path|
|
65
|
+
store[path]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
get("/file").body.should == 'file'
|
70
|
+
|
71
|
+
file 'file', 'changed'
|
72
|
+
|
73
|
+
get("/file").body.should == 'changed'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should serve assets" do
|
77
|
+
file 'config/assets.yml', '{ javascript_files: [index], stylesheet_files: [styles] }'
|
78
|
+
file 'assets/javascripts/index.js', 'index'
|
79
|
+
file 'assets/stylesheets/styles.css', 'styles'
|
80
|
+
file 'assets/file', 'file'
|
81
|
+
|
82
|
+
map do
|
83
|
+
get '/assets/javascripts\.js' do
|
84
|
+
render_javascripts
|
85
|
+
end
|
86
|
+
|
87
|
+
get '/assets/stylesheets\.css' do
|
88
|
+
render_stylesheets
|
89
|
+
end
|
90
|
+
|
91
|
+
get '/assets/(.*)' do |path|
|
92
|
+
if_modified_since do
|
93
|
+
assets[path]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
get("/assets/javascripts.js").body.should == 'index'
|
99
|
+
get("/assets/javascripts.js")['Content-Type'].should == 'text/javascript'
|
100
|
+
get("/assets/stylesheets.css").body.should == 'styles'
|
101
|
+
get("/assets/stylesheets.css")['Content-Type'].should == 'text/css'
|
102
|
+
|
103
|
+
get("/assets/file").body.should == 'file'
|
104
|
+
last_mod = get("/assets/file")['Last-Modified']
|
105
|
+
|
106
|
+
get("/assets/file", 'HTTP_IF_MODIFIED_SINCE' => last_mod).status.should == 304
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should render templates" do
|
110
|
+
file 'templates/layout.rhtml', '<%= @content %>'
|
111
|
+
file 'templates/index.rhtml', '<%= @body %>'
|
112
|
+
|
113
|
+
map do
|
114
|
+
get '/' do
|
115
|
+
render 'index.rhtml', :body => 'BODY'
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
get('/').body.should == 'BODY'
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: georgi-kontrol
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthias Georgi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-12-17 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A small web framework running as rack application.
|
17
|
+
email: matti.georgi@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.md
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- LICENSE
|
27
|
+
- README.md
|
28
|
+
- lib/kontrol.rb
|
29
|
+
- lib/kontrol/application.rb
|
30
|
+
- lib/kontrol/builder.rb
|
31
|
+
- lib/kontrol/helpers.rb
|
32
|
+
- lib/kontrol/router.rb
|
33
|
+
- lib/kontrol/template.rb
|
34
|
+
- test/application_spec.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://github.com/georgi/kontrol
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
version:
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.2.0
|
58
|
+
signing_key:
|
59
|
+
specification_version: 2
|
60
|
+
summary: a micro web framework
|
61
|
+
test_files: []
|
62
|
+
|