gin 0.0.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|