gin 1.1.2 → 1.2.0
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/History.rdoc +22 -1
- data/Manifest.txt +7 -0
- data/TODO.rdoc +45 -0
- data/bin/gin +105 -35
- data/lib/gin.rb +7 -1
- data/lib/gin/app.rb +328 -59
- data/lib/gin/asset_manifest.rb +178 -0
- data/lib/gin/asset_pipeline.rb +235 -0
- data/lib/gin/cache.rb +36 -0
- data/lib/gin/config.rb +3 -1
- data/lib/gin/constants.rb +6 -1
- data/lib/gin/controller.rb +180 -17
- data/lib/gin/core_ext/float.rb +10 -0
- data/lib/gin/core_ext/time.rb +41 -0
- data/lib/gin/filterable.rb +5 -5
- data/lib/gin/mountable.rb +100 -0
- data/lib/gin/request.rb +4 -12
- data/lib/gin/response.rb +4 -2
- data/lib/gin/router.rb +110 -37
- data/lib/gin/strict_hash.rb +33 -0
- data/lib/gin/test.rb +8 -4
- data/lib/gin/worker.rb +49 -0
- data/test/mock_app.rb +7 -7
- data/test/test_app.rb +266 -17
- data/test/test_cache.rb +73 -5
- data/test/test_config.rb +4 -4
- data/test/test_controller.rb +158 -32
- data/test/test_filterable.rb +16 -1
- data/test/test_gin.rb +7 -6
- data/test/test_request.rb +6 -1
- data/test/test_response.rb +1 -1
- data/test/test_router.rb +156 -34
- data/test/test_test.rb +51 -45
- metadata +42 -14
- checksums.yaml +0 -7
data/lib/gin/request.rb
CHANGED
@@ -45,9 +45,9 @@ class Gin::Request < Rack::Request
|
|
45
45
|
|
46
46
|
private
|
47
47
|
|
48
|
-
M_BOOLEAN = /^true|false$/
|
49
|
-
M_FLOAT =
|
50
|
-
M_INTEGER =
|
48
|
+
M_BOOLEAN = /^true|false$/ #:nodoc:
|
49
|
+
M_FLOAT = /^-?([^0]\d+|\d)\.\d+$/ #:nodoc:
|
50
|
+
M_INTEGER = /^-?([^0]\d+|\d)$/ #:nodoc:
|
51
51
|
|
52
52
|
##
|
53
53
|
# Enable string or symbol key access to the nested params hash.
|
@@ -56,7 +56,7 @@ class Gin::Request < Rack::Request
|
|
56
56
|
def process_params object
|
57
57
|
case object
|
58
58
|
when Hash
|
59
|
-
new_hash =
|
59
|
+
new_hash = Gin::StrictHash.new
|
60
60
|
object.each { |key, value| new_hash[key] = process_params(value) }
|
61
61
|
new_hash
|
62
62
|
when Array
|
@@ -71,12 +71,4 @@ class Gin::Request < Rack::Request
|
|
71
71
|
object
|
72
72
|
end
|
73
73
|
end
|
74
|
-
|
75
|
-
|
76
|
-
##
|
77
|
-
# Creates a Hash with indifferent access.
|
78
|
-
|
79
|
-
def indifferent_hash
|
80
|
-
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
81
|
-
end
|
82
74
|
end
|
data/lib/gin/response.rb
CHANGED
@@ -8,13 +8,15 @@ class Gin::Response < Rack::Response
|
|
8
8
|
|
9
9
|
def body= value
|
10
10
|
value = value.body while Rack::Response === value
|
11
|
-
@body = value.respond_to?(:each)
|
11
|
+
@body = value.respond_to?(:each) && !value.is_a?(String) ?
|
12
|
+
value : [value.to_s]
|
12
13
|
@body
|
13
14
|
end
|
14
15
|
|
15
16
|
|
16
17
|
def finish
|
17
18
|
body_out = body
|
19
|
+
body_out = [body_out] if String === body_out
|
18
20
|
header[CNT_TYPE] ||= 'text/html;charset=UTF-8'
|
19
21
|
|
20
22
|
if NO_HEADER_STATUSES.include?(status.to_i)
|
@@ -43,7 +45,7 @@ class Gin::Response < Rack::Response
|
|
43
45
|
l + Rack::Utils.bytesize(p)
|
44
46
|
end.to_s
|
45
47
|
when File
|
46
|
-
header[CNT_LENGTH] =
|
48
|
+
header[CNT_LENGTH] = File.size(body.path).to_s
|
47
49
|
end
|
48
50
|
end
|
49
51
|
end
|
data/lib/gin/router.rb
CHANGED
@@ -28,15 +28,8 @@ class Gin::Router
|
|
28
28
|
# Used by the Gin::App.mount DSL.
|
29
29
|
|
30
30
|
class Mount
|
31
|
-
|
32
|
-
|
33
|
-
:show => %w{get /:id},
|
34
|
-
:new => %w{get /new},
|
35
|
-
:create => %w{post /:id},
|
36
|
-
:edit => %w{get /:id/edit},
|
37
|
-
:update => %w{put /:id},
|
38
|
-
:destroy => %w{delete /:id}
|
39
|
-
}
|
31
|
+
|
32
|
+
PATH_NAME_MATCHER = %r{\A[-+\w/]+\Z}m #:nodoc:
|
40
33
|
|
41
34
|
VERBS = %w{get post put delete head options trace}
|
42
35
|
|
@@ -45,28 +38,50 @@ class Gin::Router
|
|
45
38
|
end
|
46
39
|
|
47
40
|
|
48
|
-
def initialize ctrl, base_path, &block
|
41
|
+
def initialize ctrl, base_path=nil, &block
|
42
|
+
raise ArgumentError,
|
43
|
+
"#{ctrl.inspect} must respond to `call'" unless ctrl.respond_to?(:call)
|
44
|
+
|
45
|
+
base_path ||= ctrl.controller_name if Gin::Mountable === ctrl
|
46
|
+
|
47
|
+
if !base_path
|
48
|
+
uname = Gin.underscore(ctrl.to_s)
|
49
|
+
base_path = File.join("", uname) if uname =~ PATH_NAME_MATCHER
|
50
|
+
end
|
51
|
+
|
52
|
+
raise ArgumentError,
|
53
|
+
"Could not deduce base path from #{ctrl.inspect}" unless base_path
|
54
|
+
|
49
55
|
@ctrl = ctrl
|
50
56
|
@routes = []
|
51
57
|
@actions = []
|
52
58
|
@base_path = base_path
|
53
59
|
|
54
|
-
|
55
|
-
|
60
|
+
if block_given?
|
61
|
+
instance_eval(&block)
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
65
|
+
if !(Gin::Mountable === @ctrl)
|
66
|
+
any :call, "/"
|
67
|
+
else
|
68
|
+
defaults
|
69
|
+
end
|
56
70
|
end
|
57
71
|
|
58
72
|
|
59
73
|
##
|
60
74
|
# Create and add default restful routes if they aren't taken already.
|
61
75
|
|
62
|
-
def defaults
|
63
|
-
|
76
|
+
def defaults
|
77
|
+
raise TypeError,
|
78
|
+
"#{@ctrl.inspect} must inherit Gin::Mountable to support defaults" unless
|
79
|
+
Gin::Mountable === @ctrl
|
64
80
|
|
65
81
|
(@ctrl.actions - @actions).each do |action|
|
66
|
-
verb, path =
|
67
|
-
verb, path = [default_verb, "/#{action}"] if verb.nil?
|
82
|
+
verb, path = @ctrl.default_route_for(action)
|
68
83
|
|
69
|
-
add(verb, action, path) unless
|
84
|
+
add(verb, action, path) unless
|
70
85
|
@routes.any?{|route| route === [verb, path] }
|
71
86
|
end
|
72
87
|
end
|
@@ -87,15 +102,25 @@ class Gin::Router
|
|
87
102
|
path = args.shift if String === args[0]
|
88
103
|
name = args.shift.to_sym if args[0]
|
89
104
|
|
90
|
-
|
91
|
-
|
105
|
+
if Gin::Mountable === @ctrl
|
106
|
+
path ||= @ctrl.default_route_for(action)[1]
|
107
|
+
name ||= @ctrl.route_name_for(action)
|
108
|
+
elsif !(path && name) && !action.is_a?(Array) && !action.is_a?(Hash) &&
|
109
|
+
action.to_s =~ PATH_NAME_MATCHER
|
110
|
+
path ||= action.to_s
|
111
|
+
uname = Gin.underscore(@ctrl.to_s).sub(%r{^.*/},'')
|
112
|
+
name = "#{action}_#{uname}" if !name && uname =~ PATH_NAME_MATCHER
|
113
|
+
end
|
114
|
+
|
115
|
+
raise ArgumentError, "No path could be determined for target %s %s" %
|
116
|
+
[@ctrl.inspect, action.inspect] unless path
|
92
117
|
|
93
118
|
path = File.join(@base_path, path)
|
94
119
|
target = [@ctrl, action]
|
95
120
|
|
96
121
|
route = Route.new(verb, path, target, name)
|
97
|
-
@routes
|
98
|
-
@actions << action
|
122
|
+
@routes << route
|
123
|
+
@actions << action
|
99
124
|
end
|
100
125
|
|
101
126
|
|
@@ -117,6 +142,8 @@ class Gin::Router
|
|
117
142
|
# #=> "/foo/123.json"
|
118
143
|
|
119
144
|
class Route
|
145
|
+
include Gin::Constants
|
146
|
+
|
120
147
|
# Parsed out path param key names.
|
121
148
|
attr_reader :param_keys
|
122
149
|
|
@@ -132,24 +159,28 @@ class Gin::Router
|
|
132
159
|
# Arbitrary name of the route.
|
133
160
|
attr_reader :name
|
134
161
|
|
162
|
+
# HTTP verb used by the route.
|
163
|
+
attr_reader :verb
|
164
|
+
|
135
165
|
SEP = "/" # :nodoc:
|
136
166
|
VAR_MATCHER = /:(\w+)/ # :nodoc:
|
137
167
|
PARAM_MATCHER = "(.*?)" # :nodoc:
|
138
168
|
|
139
169
|
|
140
|
-
def initialize verb, path, target, name
|
170
|
+
def initialize verb, path, target=[], name=nil
|
141
171
|
@target = target
|
142
|
-
@name = name
|
172
|
+
@name = name.to_sym if name
|
143
173
|
build verb, path
|
144
174
|
end
|
145
175
|
|
146
176
|
|
147
177
|
##
|
148
178
|
# Render a route path by giving it inline (and other) params.
|
149
|
-
# route.to_path id
|
179
|
+
# route.to_path :id => 123, :format => "json", :foo => "bar"
|
150
180
|
# #=> "/foo/123.json?foo=bar"
|
151
181
|
|
152
182
|
def to_path params={}
|
183
|
+
params ||= {}
|
153
184
|
rendered_path = @path.dup
|
154
185
|
rendered_path = rendered_path % @param_keys.map do |k|
|
155
186
|
val = params.delete(k) || params.delete(k.to_sym)
|
@@ -162,6 +193,27 @@ class Gin::Router
|
|
162
193
|
end
|
163
194
|
|
164
195
|
|
196
|
+
##
|
197
|
+
# Creates a Rack env hash with the given params and headers.
|
198
|
+
|
199
|
+
def to_env params={}, headers={}
|
200
|
+
headers ||= {}
|
201
|
+
params ||= {}
|
202
|
+
|
203
|
+
path_info, query_string = to_path(params).split('?', 2)
|
204
|
+
|
205
|
+
env = headers.merge(
|
206
|
+
PATH_INFO => path_info,
|
207
|
+
REQ_METHOD => @verb,
|
208
|
+
QUERY_STRING => query_string
|
209
|
+
)
|
210
|
+
|
211
|
+
# TODO: implement multipart streams for requests that support a body
|
212
|
+
env[RACK_INPUT] ||= ''
|
213
|
+
env
|
214
|
+
end
|
215
|
+
|
216
|
+
|
165
217
|
##
|
166
218
|
# Returns true if the argument matches the route_id.
|
167
219
|
# The route id is an array with verb and original path.
|
@@ -174,26 +226,28 @@ class Gin::Router
|
|
174
226
|
private
|
175
227
|
|
176
228
|
def build verb, path
|
177
|
-
verb = verb.to_s.
|
229
|
+
@verb = verb.to_s.upcase
|
178
230
|
|
179
231
|
@path = ""
|
180
232
|
@param_keys = []
|
181
233
|
@match_keys = []
|
182
|
-
@route_id = [verb, path]
|
234
|
+
@route_id = [@verb, path]
|
183
235
|
|
184
|
-
parts = [verb].concat path.split(SEP)
|
236
|
+
parts = [@verb].concat path.split(SEP)
|
185
237
|
|
186
238
|
parts.each_with_index do |p, i|
|
187
239
|
next if p.empty?
|
188
240
|
|
241
|
+
is_param = false
|
189
242
|
part = Regexp.escape(p).gsub!(VAR_MATCHER) do
|
190
243
|
@param_keys << $1
|
244
|
+
is_param = true
|
191
245
|
PARAM_MATCHER
|
192
246
|
end
|
193
247
|
|
194
248
|
if part == PARAM_MATCHER
|
195
249
|
part = "%s"
|
196
|
-
elsif
|
250
|
+
elsif is_param
|
197
251
|
part = /^#{part}$/
|
198
252
|
else
|
199
253
|
part = p
|
@@ -243,8 +297,6 @@ class Gin::Router
|
|
243
297
|
# Used by Gin::App.mount.
|
244
298
|
|
245
299
|
def add ctrl, base_path=nil, &block
|
246
|
-
base_path ||= ctrl.controller_name
|
247
|
-
|
248
300
|
mount = Mount.new(ctrl, base_path, &block)
|
249
301
|
|
250
302
|
mount.each_route do |route|
|
@@ -284,7 +336,8 @@ class Gin::Router
|
|
284
336
|
##
|
285
337
|
# Get the path to the given Controller and action combo or route name,
|
286
338
|
# provided with the needed params. Routes with missing path params will raise
|
287
|
-
# MissingParamError.
|
339
|
+
# MissingParamError. Missing routes will raise a RouterError.
|
340
|
+
# Returns a String starting with "/".
|
288
341
|
#
|
289
342
|
# path_to FooController, :show, id: 123
|
290
343
|
# #=> "/foo/123"
|
@@ -296,13 +349,32 @@ class Gin::Router
|
|
296
349
|
# #=> "/foo/123?more=true"
|
297
350
|
|
298
351
|
def path_to *args
|
352
|
+
params = args.pop.dup if Hash === args.last
|
353
|
+
route = route_to(*args)
|
354
|
+
route.to_path(params)
|
355
|
+
end
|
356
|
+
|
357
|
+
|
358
|
+
##
|
359
|
+
# Get the route object to the given Controller and action combo or route name.
|
360
|
+
# MissingParamError. Returns a Gin::Router::Route instance.
|
361
|
+
# Raises a RouterError if no route can be found.
|
362
|
+
#
|
363
|
+
# route_to FooController, :show
|
364
|
+
# route_to :show_foo
|
365
|
+
|
366
|
+
def route_to *args
|
299
367
|
key = Class === args[0] ? args.slice!(0..1) : args.shift
|
300
368
|
route = @routes_lookup[key]
|
301
|
-
raise PathArgumentError, "No route for #{Array(key).join("#")}" unless route
|
302
369
|
|
303
|
-
|
370
|
+
unless route
|
371
|
+
name = Array === key && Gin::Mountable === key[0] ?
|
372
|
+
key[0].display_name(key[1]) : name.inspect
|
304
373
|
|
305
|
-
|
374
|
+
raise Gin::RouterError, "No route for #{name}"
|
375
|
+
end
|
376
|
+
|
377
|
+
route
|
306
378
|
end
|
307
379
|
|
308
380
|
|
@@ -317,10 +389,11 @@ class Gin::Router
|
|
317
389
|
|
318
390
|
def resources_for http_verb, path
|
319
391
|
param_vals = []
|
320
|
-
curr_node = @routes_tree[http_verb.to_s.
|
392
|
+
curr_node = @routes_tree[http_verb.to_s.upcase]
|
321
393
|
return unless curr_node
|
322
394
|
|
323
|
-
path.scan(%r{/([^/]+|$)}) do |
|
395
|
+
path.scan(%r{/([^/]+|$)}) do |matches|
|
396
|
+
key = matches[0]
|
324
397
|
next if key.empty?
|
325
398
|
|
326
399
|
if curr_node[key]
|
@@ -345,6 +418,6 @@ class Gin::Router
|
|
345
418
|
path_params = param_vals.empty? ?
|
346
419
|
{} : route.param_keys.inject({}){|h, name| h[name] = param_vals.shift; h}
|
347
420
|
|
348
|
-
[
|
421
|
+
[route.target, path_params]
|
349
422
|
end
|
350
423
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Gin::StrictHash < Hash
|
2
|
+
|
3
|
+
def [] key
|
4
|
+
super key.to_s
|
5
|
+
end
|
6
|
+
|
7
|
+
def []= key, value
|
8
|
+
super key.to_s, value
|
9
|
+
end
|
10
|
+
|
11
|
+
def delete key
|
12
|
+
super key.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_key? key
|
16
|
+
super key.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
alias key? has_key?
|
20
|
+
|
21
|
+
def merge hash
|
22
|
+
return super if hash.class == self.class
|
23
|
+
new_hash = self.dup
|
24
|
+
new_hash.merge!(hash)
|
25
|
+
new_hash
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge! hash
|
29
|
+
return super if hash.class == self.class
|
30
|
+
hash.each{|k,v| self[k] = v}
|
31
|
+
self
|
32
|
+
end
|
33
|
+
end
|
data/lib/gin/test.rb
CHANGED
@@ -137,7 +137,7 @@ module Gin::Test::Assertions
|
|
137
137
|
# assert_select '**/address/domestic=YES/../value'
|
138
138
|
|
139
139
|
def assert_data path, opts={}, msg=nil
|
140
|
-
assert_select path, opts.merge(selector
|
140
|
+
assert_select path, opts.merge(:selector => :rb_path), msg
|
141
141
|
end
|
142
142
|
|
143
143
|
|
@@ -153,7 +153,7 @@ module Gin::Test::Assertions
|
|
153
153
|
# assert_select '.address[domestic=Yes]'
|
154
154
|
|
155
155
|
def assert_css path, opts={}, msg=nil
|
156
|
-
assert_select path, opts.merge(selector
|
156
|
+
assert_select path, opts.merge(:selector => :css), msg
|
157
157
|
end
|
158
158
|
|
159
159
|
|
@@ -169,7 +169,7 @@ module Gin::Test::Assertions
|
|
169
169
|
# assert_select './/address[@domestic=Yes]'
|
170
170
|
|
171
171
|
def assert_xpath path, opts={}, msg=nil
|
172
|
-
assert_select path, opts.merge(selector
|
172
|
+
assert_select path, opts.merge(:selector => :xpath), msg
|
173
173
|
end
|
174
174
|
|
175
175
|
|
@@ -243,7 +243,7 @@ module Gin::Test::Assertions
|
|
243
243
|
# controller and action.
|
244
244
|
|
245
245
|
def assert_route verb, path, exp_ctrl, exp_action, msg=nil
|
246
|
-
ctrl, action, = app.router.resources_for(verb, path)
|
246
|
+
ctrl, action, = Array(app.router.resources_for(verb, path))[0]
|
247
247
|
expected = "#{exp_ctrl}##{exp_action}"
|
248
248
|
real = "#{ctrl}##{action}"
|
249
249
|
real_msg = ctrl && action ? "got #{real}" : "doesn't exist"
|
@@ -463,6 +463,10 @@ Run the following command and try again: gem install #{gemname}"
|
|
463
463
|
env['REQUEST_METHOD'] = verb.to_s.upcase
|
464
464
|
env['QUERY_STRING'] = query
|
465
465
|
env['PATH_INFO'] = path
|
466
|
+
|
467
|
+
host, port = (app.hostname || "localhost").split(":")
|
468
|
+
env['SERVER_NAME'] = host
|
469
|
+
env['SERVER_PORT'] = port || '80'
|
466
470
|
env.merge! headers
|
467
471
|
|
468
472
|
@rack_response = app.call(env)
|
data/lib/gin/worker.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class Gin::Worker
|
4
|
+
|
5
|
+
def initialize pidfile, &block
|
6
|
+
@pidfile = pidfile
|
7
|
+
@pid = nil
|
8
|
+
@block = block
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def run
|
13
|
+
FileUtils.touch(@pidfile)
|
14
|
+
f = File.open(@pidfile, 'r+')
|
15
|
+
return unless f.flock(File::LOCK_EX | File::LOCK_NB)
|
16
|
+
|
17
|
+
if other_pid = f.read
|
18
|
+
running = Process.kill(0, other_pid) rescue false
|
19
|
+
return if running
|
20
|
+
end
|
21
|
+
|
22
|
+
@pid = fork do
|
23
|
+
begin
|
24
|
+
f.truncate(File.size(f.path))
|
25
|
+
f.write Process.pid.to_s
|
26
|
+
f.flush
|
27
|
+
@block.call
|
28
|
+
rescue Interrupt
|
29
|
+
$stderr.puts "Worker Interrupted: #{@pidfile} (#{Process.pid})"
|
30
|
+
ensure
|
31
|
+
f.close unless f.closed?
|
32
|
+
File.delete(f.path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
ensure
|
37
|
+
f.close if f && !f.closed?
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def kill sig="INT"
|
42
|
+
Process.kill(sig, @pid) if @pid
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def wait
|
47
|
+
Process.waitpid(@pid) if @pid
|
48
|
+
end
|
49
|
+
end
|