gin 0.0.0 → 1.0.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/.autotest +3 -3
- data/.gitignore +7 -0
- data/History.rdoc +3 -6
- data/Manifest.txt +36 -2
- data/README.rdoc +24 -14
- data/Rakefile +2 -9
- data/lib/gin.rb +122 -1
- data/lib/gin/app.rb +595 -0
- data/lib/gin/config.rb +50 -0
- data/lib/gin/controller.rb +602 -0
- data/lib/gin/core_ext/cgi.rb +15 -0
- data/lib/gin/core_ext/gin_class.rb +10 -0
- data/lib/gin/errorable.rb +113 -0
- data/lib/gin/filterable.rb +200 -0
- data/lib/gin/reloadable.rb +90 -0
- data/lib/gin/request.rb +76 -0
- data/lib/gin/response.rb +51 -0
- data/lib/gin/router.rb +222 -0
- data/lib/gin/stream.rb +56 -0
- data/public/400.html +14 -0
- data/public/404.html +13 -0
- data/public/500.html +14 -0
- data/public/error.html +38 -0
- data/public/favicon.ico +0 -0
- data/public/gin.css +61 -0
- data/public/gin_sm.png +0 -0
- data/test/app/app_foo.rb +15 -0
- data/test/app/controllers/app_controller.rb +16 -0
- data/test/app/controllers/foo_controller.rb +3 -0
- data/test/mock_config/backend.yml +7 -0
- data/test/mock_config/memcache.yml +10 -0
- data/test/mock_config/not_a_config.txt +0 -0
- data/test/test_app.rb +592 -0
- data/test/test_config.rb +33 -0
- data/test/test_controller.rb +808 -0
- data/test/test_errorable.rb +221 -0
- data/test/test_filterable.rb +126 -0
- data/test/test_gin.rb +59 -0
- data/test/test_helper.rb +5 -0
- data/test/test_request.rb +81 -0
- data/test/test_response.rb +68 -0
- data/test/test_router.rb +193 -0
- metadata +80 -15
- data/bin/gin +0 -3
- data/test/gin_test.rb +0 -8
data/lib/gin/response.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
class Gin::Response < Rack::Response
|
2
|
+
|
3
|
+
NO_HEADER_STATUSES = [100, 101, 204, 205, 304].freeze #:nodoc:
|
4
|
+
H_CTYPE = "Content-Type".freeze #:nodoc:
|
5
|
+
H_CLENGTH = "Content-Length".freeze #:nodoc:
|
6
|
+
|
7
|
+
attr_accessor :status
|
8
|
+
attr_reader :body
|
9
|
+
|
10
|
+
def body= value
|
11
|
+
value = value.body while Rack::Response === value
|
12
|
+
@body = value.respond_to?(:each) ? value : [value.to_s]
|
13
|
+
@body
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def finish
|
18
|
+
body_out = body
|
19
|
+
header[H_CTYPE] ||= 'text/html;charset=UTF-8'
|
20
|
+
|
21
|
+
if NO_HEADER_STATUSES.include?(status.to_i)
|
22
|
+
header.delete H_CTYPE
|
23
|
+
header.delete H_CLENGTH
|
24
|
+
|
25
|
+
if status.to_i > 200
|
26
|
+
close
|
27
|
+
body_out = []
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
update_content_length
|
32
|
+
|
33
|
+
[status.to_i, header, body_out]
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def update_content_length
|
40
|
+
if header[H_CTYPE] && !header[H_CLENGTH]
|
41
|
+
case body
|
42
|
+
when Array
|
43
|
+
header[H_CLENGTH] = body.inject(0) do |l, p|
|
44
|
+
l + Rack::Utils.bytesize(p)
|
45
|
+
end.to_s
|
46
|
+
when File
|
47
|
+
header[H_CLENGTH] = body.size.to_s
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/gin/router.rb
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
class Gin::Router
|
2
|
+
|
3
|
+
class PathArgumentError < Gin::Error; end
|
4
|
+
|
5
|
+
class Mount
|
6
|
+
DEFAULT_ACTION_MAP = {
|
7
|
+
:index => %w{get /},
|
8
|
+
:show => %w{get /:id},
|
9
|
+
:new => %w{get /new},
|
10
|
+
:create => %w{post /:id},
|
11
|
+
:edit => %w{get /:id/edit},
|
12
|
+
:update => %w{put /:id},
|
13
|
+
:destroy => %w{delete /:id}
|
14
|
+
}
|
15
|
+
|
16
|
+
VERBS = %w{get post put delete head options trace}
|
17
|
+
|
18
|
+
VERBS.each do |verb|
|
19
|
+
define_method(verb){|action, *args| add(verb, action, *args)}
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def initialize ctrl, base_path, sep="/", &block
|
24
|
+
@sep = sep
|
25
|
+
@ctrl = ctrl
|
26
|
+
@routes = []
|
27
|
+
@actions = []
|
28
|
+
@base_path = base_path.split(@sep)
|
29
|
+
|
30
|
+
instance_eval(&block) if block_given?
|
31
|
+
defaults unless block_given?
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Create restful routes if they aren't taken already.
|
36
|
+
def defaults restful_only=false
|
37
|
+
(@ctrl.actions - @actions).each do |action|
|
38
|
+
verb, path = DEFAULT_ACTION_MAP[action]
|
39
|
+
verb, path = ['get', "/#{action}"] if !restful_only && verb.nil?
|
40
|
+
|
41
|
+
add(verb, action, path) unless verb.nil? ||
|
42
|
+
@routes.any?{|(r,n,(c,a,p))| r == make_route(verb, path)[0] }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def any action, path=nil
|
48
|
+
VERBS.each{|verb| send verb, action, path}
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def make_route verb, path
|
53
|
+
param_keys = []
|
54
|
+
route = [verb].concat @base_path
|
55
|
+
route.concat path.split(@sep)
|
56
|
+
route.delete_if{|part| part.empty?}
|
57
|
+
|
58
|
+
route.map! do |part|
|
59
|
+
if part[0] == ":"
|
60
|
+
param_keys << part[1..-1]
|
61
|
+
"%s"
|
62
|
+
else
|
63
|
+
part
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
[route, param_keys]
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def add verb, action, *args
|
72
|
+
path = args.shift if String === args[0]
|
73
|
+
name = args.shift.to_sym if args[0]
|
74
|
+
|
75
|
+
path ||= action.to_s
|
76
|
+
name ||= :"#{action}_#{@ctrl.controller_name}"
|
77
|
+
|
78
|
+
route, param_keys = make_route(verb, path)
|
79
|
+
@routes << [route, name, [@ctrl, action, param_keys]]
|
80
|
+
@actions << action.to_sym
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def each_route &block
|
85
|
+
@routes.each{|(route, name, value)| block.call(route, name, value) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
class Node
|
91
|
+
attr_accessor :value
|
92
|
+
|
93
|
+
def initialize
|
94
|
+
@children = {}
|
95
|
+
end
|
96
|
+
|
97
|
+
def [] key
|
98
|
+
@children[key]
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_child key, val=nil
|
102
|
+
@children[key] ||= Node.new
|
103
|
+
@children[key].value = val unless val.nil?
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def initialize separator="/" # :nodoc:
|
109
|
+
@sep = separator
|
110
|
+
@routes_tree = Node.new
|
111
|
+
@routes_lookup = {}
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
##
|
116
|
+
# Add a Controller to the router with a base path.
|
117
|
+
|
118
|
+
def add ctrl, base_path=nil, &block
|
119
|
+
base_path ||= ctrl.controller_name
|
120
|
+
|
121
|
+
mount = Mount.new(ctrl, base_path, @sep, &block)
|
122
|
+
|
123
|
+
mount.each_route do |route_ary, name, val|
|
124
|
+
curr_node = @routes_tree
|
125
|
+
|
126
|
+
route_ary.each do |part|
|
127
|
+
curr_node.add_child part
|
128
|
+
curr_node = curr_node[part]
|
129
|
+
end
|
130
|
+
|
131
|
+
curr_node.value = val
|
132
|
+
route = [route_ary[0], "/" << route_ary[1..-1].join(@sep), val[2]]
|
133
|
+
|
134
|
+
@routes_lookup[name] = route if name
|
135
|
+
@routes_lookup[val[0..1]] = route
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
##
|
141
|
+
# Check if a Controller and action combo has a route.
|
142
|
+
|
143
|
+
def has_route? ctrl, action
|
144
|
+
!!@routes_lookup[[ctrl, action]]
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
##
|
149
|
+
# Yield every Controller, action, route combination.
|
150
|
+
|
151
|
+
def each_route &block
|
152
|
+
@routes_lookup.each do |key,route|
|
153
|
+
next unless Array === key
|
154
|
+
block.call route, key[0], key[1]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
##
|
160
|
+
# Get the path to the given Controller and action combo or route name,
|
161
|
+
# provided with the needed params. Routes with missing path params will raise
|
162
|
+
# MissingParamError. Returns a String starting with "/".
|
163
|
+
#
|
164
|
+
# path_to FooController, :show, :id => 123
|
165
|
+
# #=> "/foo/123"
|
166
|
+
#
|
167
|
+
# path_to :show_foo, :id => 123
|
168
|
+
# #=> "/foo/123"
|
169
|
+
|
170
|
+
def path_to *args
|
171
|
+
key = Class === args[0] ? args.slice!(0..1) : args.shift
|
172
|
+
verb, route, param_keys = @routes_lookup[key]
|
173
|
+
raise PathArgumentError, "No route for #{Array(key).join("#")}" unless route
|
174
|
+
|
175
|
+
params = (args.pop || {}).dup
|
176
|
+
|
177
|
+
route = route.dup
|
178
|
+
route = route % param_keys.map do |k|
|
179
|
+
params.delete(k) || params.delete(k.to_sym) ||
|
180
|
+
raise(PathArgumentError, "Missing param #{k}")
|
181
|
+
end unless param_keys.empty?
|
182
|
+
|
183
|
+
route << "?#{Gin.build_query(params)}" unless params.empty?
|
184
|
+
|
185
|
+
route
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
##
|
190
|
+
# Takes a path and returns an array of 3 items:
|
191
|
+
# [controller_class, action_symbol, path_params_hash]
|
192
|
+
# Returns nil if no match was found.
|
193
|
+
|
194
|
+
def resources_for http_verb, path
|
195
|
+
param_vals = []
|
196
|
+
curr_node = @routes_tree[http_verb.to_s.downcase]
|
197
|
+
|
198
|
+
path.scan(%r{/([^/]+|$)}) do |(key)|
|
199
|
+
next if key.empty?
|
200
|
+
|
201
|
+
if curr_node[key]
|
202
|
+
curr_node = curr_node[key]
|
203
|
+
|
204
|
+
elsif curr_node["%s"]
|
205
|
+
param_vals << key
|
206
|
+
curr_node = curr_node["%s"]
|
207
|
+
|
208
|
+
else
|
209
|
+
return
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
return unless curr_node.value
|
214
|
+
rsc = curr_node.value.dup
|
215
|
+
|
216
|
+
rsc[-1] = param_vals.empty? ?
|
217
|
+
Hash.new :
|
218
|
+
rsc[-1].inject({}){|h, name| h[name] = param_vals.shift; h}
|
219
|
+
|
220
|
+
rsc
|
221
|
+
end
|
222
|
+
end
|
data/lib/gin/stream.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
##
|
2
|
+
# Taken from Sinatra.
|
3
|
+
#
|
4
|
+
# Class of the response body in case you use #stream.
|
5
|
+
#
|
6
|
+
# Three things really matter: The front and back block (back being the
|
7
|
+
# block generating content, front the one sending it to the client) and
|
8
|
+
# the scheduler, integrating with whatever concurrency feature the Rack
|
9
|
+
# handler is using.
|
10
|
+
#
|
11
|
+
# Scheduler has to respond to defer and schedule.
|
12
|
+
|
13
|
+
class Gin::Stream
|
14
|
+
def self.schedule(*) yield end
|
15
|
+
def self.defer(*) yield end
|
16
|
+
|
17
|
+
|
18
|
+
def initialize(scheduler = self.class, keep_open = false, &back)
|
19
|
+
@back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
|
20
|
+
@callbacks, @closed = [], false
|
21
|
+
end
|
22
|
+
|
23
|
+
def close
|
24
|
+
return if @closed
|
25
|
+
@closed = true
|
26
|
+
@scheduler.schedule { @callbacks.each { |c| c.call }}
|
27
|
+
end
|
28
|
+
|
29
|
+
def each(&front)
|
30
|
+
@front = front
|
31
|
+
@scheduler.defer do
|
32
|
+
begin
|
33
|
+
@back.call(self)
|
34
|
+
rescue Exception => e
|
35
|
+
@scheduler.schedule { raise e }
|
36
|
+
end
|
37
|
+
close unless @keep_open
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def <<(data)
|
42
|
+
@scheduler.schedule { @front.call(data.to_s) }
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def callback(&block)
|
47
|
+
return yield if @closed
|
48
|
+
@callbacks << block
|
49
|
+
end
|
50
|
+
|
51
|
+
alias errback callback
|
52
|
+
|
53
|
+
def closed?
|
54
|
+
@closed
|
55
|
+
end
|
56
|
+
end
|
data/public/400.html
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Bad Request</title>
|
5
|
+
<link rel="stylesheet" type="text/css" href="/gin.css"/>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<div class="canvas error">
|
9
|
+
<h1>Bad Request</h1>
|
10
|
+
<p>The server could not process your request as formed.</p>
|
11
|
+
</div>
|
12
|
+
</body>
|
13
|
+
</html>
|
14
|
+
|
data/public/404.html
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Page Not Found</title>
|
5
|
+
<link rel="stylesheet" type="text/css" href="/gin.css"/>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<div class="canvas error">
|
9
|
+
<h1>Page Not Found</h1>
|
10
|
+
<p>The page you requested does not exist.</p>
|
11
|
+
</div>
|
12
|
+
</body>
|
13
|
+
</html>
|
data/public/500.html
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Server Error</title>
|
5
|
+
<link rel="stylesheet" type="text/css" href="/gin.css"/>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<div class="canvas error">
|
9
|
+
<h1>Server Error</h1>
|
10
|
+
<p>An unexpected error occurred. We have been notified, of this issue.<br/>
|
11
|
+
Please check again later.</p>
|
12
|
+
</div>
|
13
|
+
</body>
|
14
|
+
</html>
|
data/public/error.html
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>%s</title>
|
5
|
+
<link rel="stylesheet" type="text/css" href="/gin.css"/>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<div class="canvas">
|
9
|
+
<img src="/gin_sm.png" class="logo"/>
|
10
|
+
<h1>%s</h1>
|
11
|
+
<p>%s</p>
|
12
|
+
<div id="apptrace" class="trace">
|
13
|
+
<a href="#" onclick="show_fulltrace()">Show Full Trace</a>
|
14
|
+
%s
|
15
|
+
</div>
|
16
|
+
<div id="fulltrace" class="trace">
|
17
|
+
<a href="#" onclick="hide_fulltrace()">Hide Full Trace</a>
|
18
|
+
%s
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
<script>
|
22
|
+
var apptrace = document.getElementById('apptrace');
|
23
|
+
var fulltrace = document.getElementById('fulltrace');
|
24
|
+
|
25
|
+
function show_fulltrace(){
|
26
|
+
apptrace.style.display = "none";
|
27
|
+
fulltrace.style.display = "block";
|
28
|
+
return true;
|
29
|
+
}
|
30
|
+
|
31
|
+
function hide_fulltrace(){
|
32
|
+
apptrace.style.display = "block";
|
33
|
+
fulltrace.style.display = "none";
|
34
|
+
return true;
|
35
|
+
}
|
36
|
+
</script>
|
37
|
+
</body>
|
38
|
+
</html>
|
data/public/favicon.ico
ADDED
Binary file
|
data/public/gin.css
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
body {
|
3
|
+
font-family: Helvetica Neue, Helvetica, Sans-Serif;
|
4
|
+
background-color: #eee;
|
5
|
+
margin: 50px;
|
6
|
+
}
|
7
|
+
|
8
|
+
.canvas {
|
9
|
+
border: 1px solid #ccc;
|
10
|
+
padding: 12px;
|
11
|
+
padding-top: 10px;
|
12
|
+
border-radius: 15px;
|
13
|
+
box-shadow: 3px 3px 10px #aaa;
|
14
|
+
background-color: #fff;
|
15
|
+
margin: 0 auto;
|
16
|
+
margin-top: 50px;
|
17
|
+
max-width: 1200px;
|
18
|
+
}
|
19
|
+
|
20
|
+
.canvas h1 {
|
21
|
+
margin-top: 0;
|
22
|
+
border-bottom: 1px solid #ccc;
|
23
|
+
}
|
24
|
+
|
25
|
+
.canvas pre {
|
26
|
+
clear: both;
|
27
|
+
margin: 15px;
|
28
|
+
padding: 15px;
|
29
|
+
border: 1px solid #ccc;
|
30
|
+
overflow: scroll;
|
31
|
+
background-color: #efefef;
|
32
|
+
border-radius: 10px;
|
33
|
+
box-shadow: 3px 3px -10px #aaa;
|
34
|
+
}
|
35
|
+
|
36
|
+
.error {
|
37
|
+
width: 500px;
|
38
|
+
}
|
39
|
+
|
40
|
+
.trace a {
|
41
|
+
float: right;
|
42
|
+
color: #687c93;
|
43
|
+
font-size: 12px;
|
44
|
+
text-decoration: none;
|
45
|
+
position: relative;
|
46
|
+
top: -75px;
|
47
|
+
margin-right: 15px;
|
48
|
+
}
|
49
|
+
|
50
|
+
#fulltrace {
|
51
|
+
display: none;
|
52
|
+
}
|
53
|
+
|
54
|
+
img.logo {
|
55
|
+
float: left;
|
56
|
+
position: relative;
|
57
|
+
top: -35px;
|
58
|
+
left: -35px;
|
59
|
+
margin-bottom: -50px;
|
60
|
+
margin-right: -25px;
|
61
|
+
}
|