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/.autotest
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'autotest/restart'
|
4
4
|
|
5
|
-
|
5
|
+
Autotest.add_hook :initialize do |at|
|
6
6
|
# at.extra_files << "../some/external/dependency.rb"
|
7
7
|
#
|
8
8
|
# at.libs << ":../some/external"
|
9
9
|
#
|
10
|
-
|
10
|
+
at.add_exception 'test/app'
|
11
11
|
#
|
12
12
|
# at.add_mapping(/dependency.rb/) do |f, _|
|
13
13
|
# at.files_matching(/test_.*rb$/)
|
@@ -16,7 +16,7 @@ require 'autotest/restart'
|
|
16
16
|
# %w(TestA TestB).each do |klass|
|
17
17
|
# at.extra_class_map[klass] = "test/test_misc.rb"
|
18
18
|
# end
|
19
|
-
|
19
|
+
end
|
20
20
|
|
21
21
|
# Autotest.add_hook :run_command do |at|
|
22
22
|
# system "rake build"
|
data/.gitignore
ADDED
data/History.rdoc
CHANGED
data/Manifest.txt
CHANGED
@@ -1,8 +1,42 @@
|
|
1
1
|
.autotest
|
2
|
+
.gitignore
|
2
3
|
History.rdoc
|
3
4
|
Manifest.txt
|
4
5
|
README.rdoc
|
5
6
|
Rakefile
|
6
|
-
bin/gin
|
7
7
|
lib/gin.rb
|
8
|
-
|
8
|
+
lib/gin/app.rb
|
9
|
+
lib/gin/config.rb
|
10
|
+
lib/gin/controller.rb
|
11
|
+
lib/gin/core_ext/cgi.rb
|
12
|
+
lib/gin/core_ext/gin_class.rb
|
13
|
+
lib/gin/errorable.rb
|
14
|
+
lib/gin/filterable.rb
|
15
|
+
lib/gin/reloadable.rb
|
16
|
+
lib/gin/request.rb
|
17
|
+
lib/gin/response.rb
|
18
|
+
lib/gin/router.rb
|
19
|
+
lib/gin/stream.rb
|
20
|
+
public/400.html
|
21
|
+
public/404.html
|
22
|
+
public/500.html
|
23
|
+
public/error.html
|
24
|
+
public/favicon.ico
|
25
|
+
public/gin.css
|
26
|
+
public/gin_sm.png
|
27
|
+
test/app/app_foo.rb
|
28
|
+
test/app/controllers/app_controller.rb
|
29
|
+
test/app/controllers/foo_controller.rb
|
30
|
+
test/mock_config/backend.yml
|
31
|
+
test/mock_config/memcache.yml
|
32
|
+
test/mock_config/not_a_config.txt
|
33
|
+
test/test_app.rb
|
34
|
+
test/test_config.rb
|
35
|
+
test/test_controller.rb
|
36
|
+
test/test_errorable.rb
|
37
|
+
test/test_filterable.rb
|
38
|
+
test/test_gin.rb
|
39
|
+
test/test_helper.rb
|
40
|
+
test/test_request.rb
|
41
|
+
test/test_response.rb
|
42
|
+
test/test_router.rb
|
data/README.rdoc
CHANGED
@@ -1,29 +1,39 @@
|
|
1
|
-
=
|
1
|
+
= Gin
|
2
2
|
|
3
|
-
*
|
3
|
+
* http://yaks.me/gin
|
4
4
|
|
5
|
-
==
|
5
|
+
== Description
|
6
6
|
|
7
|
-
|
7
|
+
Gin is a small web framework built from the redistillation of
|
8
|
+
Sinatra and Rails idioms. Specifically, it uses much of Sinatra's
|
9
|
+
request flow and HTTP helper methods with dedicated controller classes
|
10
|
+
that support Rails-like filters.
|
8
11
|
|
9
|
-
==
|
12
|
+
== Hello World
|
10
13
|
|
11
|
-
|
14
|
+
# config.ru
|
15
|
+
require 'gin'
|
12
16
|
|
13
|
-
|
17
|
+
class HelloWorldCtrl < Gin::Controller
|
18
|
+
def index; "Hello World!"; end
|
19
|
+
end
|
14
20
|
|
15
|
-
|
21
|
+
class MyApp < Gin::App
|
22
|
+
mount HelloWorldCtrl, "/"
|
23
|
+
end
|
16
24
|
|
17
|
-
|
25
|
+
run MyApp.new
|
18
26
|
|
19
|
-
|
27
|
+
== Requirements
|
20
28
|
|
21
|
-
|
29
|
+
* rack
|
30
|
+
* rack-protection
|
22
31
|
|
23
|
-
|
24
|
-
|
32
|
+
== Install
|
33
|
+
|
34
|
+
* gem install gin
|
25
35
|
|
26
|
-
==
|
36
|
+
== License
|
27
37
|
|
28
38
|
(The MIT License)
|
29
39
|
|
data/Rakefile
CHANGED
@@ -3,21 +3,14 @@
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'hoe'
|
5
5
|
|
6
|
-
# Hoe.plugin :compiler
|
7
|
-
# Hoe.plugin :gem_prelude_sucks
|
8
|
-
# Hoe.plugin :inline
|
9
|
-
# Hoe.plugin :isolate
|
10
|
-
# Hoe.plugin :racc
|
11
|
-
# Hoe.plugin :rcov
|
12
|
-
# Hoe.plugin :rubyforge
|
13
|
-
|
14
6
|
Hoe.spec 'gin' do
|
15
7
|
developer('Jeremie Castagna', 'yaksnrainbows@gmail.com')
|
16
8
|
self.readme_file = "README.rdoc"
|
17
9
|
self.history_file = "History.rdoc"
|
18
10
|
self.extra_rdoc_files = FileList['*.rdoc']
|
19
11
|
|
20
|
-
self.extra_deps << ['rack',
|
12
|
+
self.extra_deps << ['rack', '~>1.1']
|
13
|
+
self.extra_deps << ['rack-protection', '~>1.0']
|
21
14
|
end
|
22
15
|
|
23
16
|
# vim: syntax=ruby
|
data/lib/gin.rb
CHANGED
@@ -1,3 +1,124 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'rack-protection'
|
5
|
+
|
6
|
+
|
1
7
|
class Gin
|
2
|
-
VERSION = '
|
8
|
+
VERSION = '1.0.0'
|
9
|
+
|
10
|
+
LIB_DIR = File.expand_path("..", __FILE__) #:nodoc:
|
11
|
+
PUBLIC_DIR = File.expand_path("../../public/", __FILE__) #:nodoc:
|
12
|
+
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
class BadRequest < ArgumentError
|
16
|
+
def http_status; 400; end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NotFound < NameError
|
20
|
+
def http_status; 404; end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
##
|
25
|
+
# Change string to underscored version.
|
26
|
+
|
27
|
+
def self.underscore str
|
28
|
+
str = str.dup
|
29
|
+
str.gsub!('::', '/')
|
30
|
+
str.gsub!(/([A-Z]+?)([A-Z][a-z])/, '\1_\2')
|
31
|
+
str.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
32
|
+
str.downcase
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
##
|
37
|
+
# Create a URI query from a Hash.
|
38
|
+
|
39
|
+
def self.build_query value, prefix=nil
|
40
|
+
case value
|
41
|
+
when Array
|
42
|
+
raise ArgumentError, "no prefix given" if prefix.nil?
|
43
|
+
value.map { |v|
|
44
|
+
build_query(v, "#{prefix}[]")
|
45
|
+
}.join("&")
|
46
|
+
|
47
|
+
when Hash
|
48
|
+
value.map { |k, v|
|
49
|
+
build_query(v, prefix ?
|
50
|
+
"#{prefix}[#{CGI.escape(k.to_s)}]" : CGI.escape(k.to_s))
|
51
|
+
}.join("&")
|
52
|
+
|
53
|
+
when String, Integer, Float, TrueClass, FalseClass
|
54
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
55
|
+
"#{prefix}=#{CGI.escape(value.to_s)}"
|
56
|
+
|
57
|
+
else
|
58
|
+
prefix
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
##
|
64
|
+
# Returns the full path to the file given based on the load paths.
|
65
|
+
|
66
|
+
def self.find_loadpath file
|
67
|
+
name = file.dup
|
68
|
+
name << ".rb" unless name[-3..-1] == ".rb"
|
69
|
+
|
70
|
+
return name if name[0] == ?/ && File.file?(name)
|
71
|
+
|
72
|
+
filepath = nil
|
73
|
+
|
74
|
+
dir = $:.find do |path|
|
75
|
+
filepath = File.expand_path(name, path)
|
76
|
+
File.file? filepath
|
77
|
+
end
|
78
|
+
|
79
|
+
dir && filepath
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
##
|
84
|
+
# Get a namespaced constant.
|
85
|
+
|
86
|
+
def self.const_find str_or_ary, parent=Object
|
87
|
+
const = nil
|
88
|
+
names = Array === str_or_ary ? str_or_ary : str_or_ary.split("::")
|
89
|
+
names.each do |name|
|
90
|
+
const = parent.const_get(name)
|
91
|
+
parent = const
|
92
|
+
end
|
93
|
+
|
94
|
+
const
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
APP_START_MATCH = %r{/gin/app\.rb:\d+:in `dispatch'} #:nodoc:
|
99
|
+
|
100
|
+
##
|
101
|
+
# Get the application backtrace only.
|
102
|
+
# Removes gem and Gin paths from the trace.
|
103
|
+
|
104
|
+
def self.app_trace trace
|
105
|
+
trace = trace.dup
|
106
|
+
trace.pop until trace.last.nil? || trace.last =~ APP_START_MATCH
|
107
|
+
trace.pop while trace.last && trace.last.start_with?(LIB_DIR)
|
108
|
+
trace
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
require 'gin/core_ext/cgi'
|
113
|
+
require 'gin/core_ext/gin_class'
|
114
|
+
|
115
|
+
require 'gin/app'
|
116
|
+
require 'gin/router'
|
117
|
+
require 'gin/config'
|
118
|
+
require 'gin/request'
|
119
|
+
require 'gin/response'
|
120
|
+
require 'gin/stream'
|
121
|
+
require 'gin/errorable'
|
122
|
+
require 'gin/filterable'
|
123
|
+
require 'gin/controller'
|
3
124
|
end
|
data/lib/gin/app.rb
ADDED
@@ -0,0 +1,595 @@
|
|
1
|
+
##
|
2
|
+
# The Gin::App is the entry point for Rack, for all Gin Applications.
|
3
|
+
# This class MUST be subclassed and initialized.
|
4
|
+
# # my_app.rb
|
5
|
+
# class MyApp < Gin::App
|
6
|
+
# require 'my_controller'
|
7
|
+
# mount MyController, "/"
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# # config.ru
|
11
|
+
# require 'my_app'
|
12
|
+
# run MyApp.new
|
13
|
+
|
14
|
+
class Gin::App
|
15
|
+
extend GinClass
|
16
|
+
|
17
|
+
class RouterError < Gin::Error; end
|
18
|
+
|
19
|
+
RACK_KEYS = { #:nodoc:
|
20
|
+
:stack => 'gin.stack'.freeze,
|
21
|
+
:path_params => 'gin.path_query_hash'.freeze,
|
22
|
+
:reloaded => 'gin.reloaded'.freeze,
|
23
|
+
:errors => 'gin.errors'.freeze
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
|
27
|
+
CALLERS_TO_IGNORE = [ # :nodoc:
|
28
|
+
/\/gin(\/(.*?))?\.rb$/, # all gin code
|
29
|
+
/lib\/tilt.*\.rb$/, # all tilt code
|
30
|
+
/^\(.*\)$/, # generated code
|
31
|
+
/rubygems\/custom_require\.rb$/, # rubygems require hacks
|
32
|
+
/active_support/, # active_support require hacks
|
33
|
+
/bundler(\/runtime)?\.rb/, # bundler require hacks
|
34
|
+
/<internal:/, # internal in ruby >= 1.9.2
|
35
|
+
/src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
|
36
|
+
]
|
37
|
+
|
38
|
+
|
39
|
+
def self.inherited subclass #:nodoc:
|
40
|
+
caller_line = caller.find{|line| !CALLERS_TO_IGNORE.any?{|m| line =~ m} }
|
41
|
+
filepath = File.expand_path(caller_line.split(/:\d+:in `/).first)
|
42
|
+
dir = File.dirname(filepath)
|
43
|
+
subclass.root_dir dir
|
44
|
+
subclass.instance_variable_set("@source_file", filepath)
|
45
|
+
subclass.instance_variable_set("@source_class", subclass.to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
##
|
50
|
+
# Create a new intance of the app and call it.
|
51
|
+
|
52
|
+
def self.call env
|
53
|
+
@instance ||= self.new
|
54
|
+
@instance.call env
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
##
|
59
|
+
# Enable or disable auto-app reloading.
|
60
|
+
# On by default in development mode.
|
61
|
+
#
|
62
|
+
# In order for an app to be reloadable, the libs and controllers must be
|
63
|
+
# required from the Gin::App class context, or use MyApp.require("lib").
|
64
|
+
#
|
65
|
+
# Reloading is not supported for applications defined in the config.ru file.
|
66
|
+
|
67
|
+
def self.autoreload val=nil
|
68
|
+
@autoreload = val unless val.nil?
|
69
|
+
|
70
|
+
if @autoreload.nil?
|
71
|
+
@autoreload = File.extname(source_file) != ".ru" && development?
|
72
|
+
end
|
73
|
+
|
74
|
+
if @autoreload && (!defined?(Gin::Reloadable) || !include?(Gin::Reloadable))
|
75
|
+
require 'gin/reloadable'
|
76
|
+
include Gin::Reloadable
|
77
|
+
end
|
78
|
+
|
79
|
+
@autoreload
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
##
|
84
|
+
# Mount a Gin::Controller into the App and specify a base path. If controller
|
85
|
+
# mounts at root, use "/" as the base path.
|
86
|
+
# mount UserController, "/user" do
|
87
|
+
# get :index, "/"
|
88
|
+
# get :show, "/:id"
|
89
|
+
# post :create, "/"
|
90
|
+
# get :stats # mounts to "/stats" by default
|
91
|
+
# any :logged_in # any HTTP verb will trigger this action
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# Controllers with non-mounted actions will throw a warning at boot time.
|
95
|
+
#
|
96
|
+
# Restful routes are automatically mounted when no block is given:
|
97
|
+
#
|
98
|
+
# mount UserController
|
99
|
+
# # restfully mounted to /user
|
100
|
+
# # non-canonical actions are mounted to /user/<action_name>
|
101
|
+
#
|
102
|
+
# Mount blocks also support routing whatever actions are left to their restful
|
103
|
+
# defaults:
|
104
|
+
#
|
105
|
+
# mount UserController do
|
106
|
+
# get :foo, "/"
|
107
|
+
# defaults
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# All Gin::Controller methods are considered actions and will be mounted in
|
111
|
+
# restful mode. For helper methods, include a module into your controller.
|
112
|
+
|
113
|
+
def self.mount ctrl, base_path=nil, &block
|
114
|
+
router.add ctrl, base_path, &block
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
##
|
119
|
+
# Returns the source file of the current app.
|
120
|
+
|
121
|
+
def self.source_file
|
122
|
+
@source_file
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def self.namespace #:nodoc:
|
127
|
+
# Parent namespace of the App class. Used for reloading purposes.
|
128
|
+
Gin.const_find(@source_class.split("::")[0..-2]) if @source_class
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
def self.source_class #:nodoc:
|
133
|
+
# Lookup the class from its name. Used for reloading purposes.
|
134
|
+
Gin.const_find(@source_class) if @source_class
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
##
|
139
|
+
# Get or set the root directory of the application.
|
140
|
+
# Defaults to the app file's directory.
|
141
|
+
|
142
|
+
def self.root_dir dir=nil
|
143
|
+
@root_dir = dir if dir
|
144
|
+
@root_dir
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
##
|
149
|
+
# Get or set the path to the config directory.
|
150
|
+
# Defaults to root_dir + "config"
|
151
|
+
#
|
152
|
+
# Configs are expected to be YAML files following this pattern:
|
153
|
+
# default: &default
|
154
|
+
# key: value
|
155
|
+
#
|
156
|
+
# development: *default
|
157
|
+
# other_key: value
|
158
|
+
#
|
159
|
+
# production: *default
|
160
|
+
# ...
|
161
|
+
#
|
162
|
+
# Configs will be named according to the filename, and only the config for
|
163
|
+
# the current environment will be accessible.
|
164
|
+
|
165
|
+
def self.config_dir dir=nil
|
166
|
+
@config_dir = dir if dir
|
167
|
+
@config_dir ||= File.join(root_dir, "config")
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
##
|
172
|
+
# Access the config for your application, loaded from the config_dir.
|
173
|
+
# # config/memcache.yml
|
174
|
+
# default: &default
|
175
|
+
# host: example.com
|
176
|
+
# connections: 1
|
177
|
+
# development: *default
|
178
|
+
# host: localhost
|
179
|
+
#
|
180
|
+
# # access from App class or instance
|
181
|
+
# config.memcache['host']
|
182
|
+
|
183
|
+
def self.config
|
184
|
+
@config ||= Gin::Config.new environment, config_dir
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
##
|
189
|
+
# Loads all configs from the config_dir.
|
190
|
+
|
191
|
+
def self.load_config
|
192
|
+
return unless File.directory?(config_dir)
|
193
|
+
config.dir = config_dir
|
194
|
+
config.load!
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
##
|
199
|
+
# Get or set the path to the public directory.
|
200
|
+
# Defaults to root_dir + "public"
|
201
|
+
|
202
|
+
def self.public_dir dir=nil
|
203
|
+
@public_dir = dir if dir
|
204
|
+
@public_dir ||= File.join(root_dir, "public")
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
##
|
209
|
+
# Get or set the CDN asset host (and path).
|
210
|
+
# If block is given, evaluates the block on every read.
|
211
|
+
|
212
|
+
def self.asset_host host=nil, &block
|
213
|
+
@asset_host = host if host
|
214
|
+
@asset_host = block if block_given?
|
215
|
+
host = @asset_host.respond_to?(:call) ? @asset_host.call : @asset_host
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
##
|
220
|
+
# Returns the asset host for a given asset name. This is useful when assigning
|
221
|
+
# a block for the asset_host. The asset_name argument is passed to the block.
|
222
|
+
|
223
|
+
def self.asset_host_for asset_name
|
224
|
+
@asset_host.respond_to?(:call) ? @asset_host.call(asset_name) : @asset_host
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
##
|
229
|
+
# Returns the first 8 bytes of the asset file's md5.
|
230
|
+
# File path is assumed relative to the public_dir.
|
231
|
+
|
232
|
+
def self.asset_version path
|
233
|
+
path = File.expand_path(File.join(public_dir, path))
|
234
|
+
return unless File.file?(path)
|
235
|
+
|
236
|
+
@asset_versions ||= {}
|
237
|
+
@asset_versions[path] ||= md5(path)
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
MD5 = RUBY_PLATFORM =~ /darwin/ ? 'md5 -q' : 'md5sum' #:nodoc:
|
242
|
+
|
243
|
+
def self.md5 path #:nodoc:
|
244
|
+
`#{MD5} #{path}`[0...8]
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
##
|
249
|
+
# Define a Gin::Controller as a catch-all error rendering controller.
|
250
|
+
# This can be a dedicated controller, or a parent controller
|
251
|
+
# such as AppController. Defaults to Gin::Controller.
|
252
|
+
#
|
253
|
+
# The error delegate should handle the following errors
|
254
|
+
# for creating custom pages for Gin errors:
|
255
|
+
# Gin::NotFound, Gin::BadRequest, ::Exception
|
256
|
+
|
257
|
+
def self.error_delegate ctrl=nil
|
258
|
+
@error_delegate = ctrl if ctrl
|
259
|
+
@error_delegate ||= Gin::Controller
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
##
|
264
|
+
# Router instance that handles mapping Rack-env -> Controller#action.
|
265
|
+
|
266
|
+
def self.router
|
267
|
+
@router ||= Gin::Router.new
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
##
|
272
|
+
# Lookup or register a mime type in Rack's mime registry.
|
273
|
+
|
274
|
+
def self.mime_type type, value=nil
|
275
|
+
return type if type.nil? || type.to_s.include?('/')
|
276
|
+
type = ".#{type}" unless type.to_s[0] == ?.
|
277
|
+
return Rack::Mime.mime_type(type, nil) unless value
|
278
|
+
Rack::Mime::MIME_TYPES[type] = value
|
279
|
+
end
|
280
|
+
|
281
|
+
|
282
|
+
##
|
283
|
+
# Provides all mime types matching type, including deprecated types:
|
284
|
+
# mime_types :html # => ['text/html']
|
285
|
+
# mime_types :js # => ['application/javascript', 'text/javascript']
|
286
|
+
|
287
|
+
def self.mime_types type
|
288
|
+
type = mime_type type
|
289
|
+
type =~ /^application\/(xml|javascript)$/ ? [type, "text/#$1"] : [type]
|
290
|
+
end
|
291
|
+
|
292
|
+
|
293
|
+
##
|
294
|
+
# Add middleware internally to the app.
|
295
|
+
# Middleware statuses and Exceptions will NOT be
|
296
|
+
# handled by the error_delegate.
|
297
|
+
|
298
|
+
def self.use middleware, *args, &block
|
299
|
+
ary = [middleware, *args]
|
300
|
+
ary << block if block_given?
|
301
|
+
self.middleware << ary
|
302
|
+
end
|
303
|
+
|
304
|
+
|
305
|
+
##
|
306
|
+
# List of internal app middleware.
|
307
|
+
|
308
|
+
def self.middleware
|
309
|
+
@middleware ||= []
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
##
|
314
|
+
# Use rack sessions or not. Supports assigning
|
315
|
+
# hash for options. Defaults to true.
|
316
|
+
|
317
|
+
def self.sessions opts=nil
|
318
|
+
@session = opts unless opts.nil?
|
319
|
+
@session = true if @session.nil?
|
320
|
+
@session
|
321
|
+
end
|
322
|
+
|
323
|
+
|
324
|
+
##
|
325
|
+
# Get or set the session secret String.
|
326
|
+
# Defaults to a new random value on boot.
|
327
|
+
|
328
|
+
def self.session_secret val=nil
|
329
|
+
@session_secret = val if val
|
330
|
+
@session_secret ||= "%064x" % Kernel.rand(2**256-1)
|
331
|
+
end
|
332
|
+
|
333
|
+
|
334
|
+
##
|
335
|
+
# Use rack-protection or not. Supports assigning
|
336
|
+
# hash for options. Defaults to true.
|
337
|
+
|
338
|
+
def self.protection opts=nil
|
339
|
+
@protection = opts unless opts.nil?
|
340
|
+
@protection = true if @protection.nil?
|
341
|
+
@protection
|
342
|
+
end
|
343
|
+
|
344
|
+
|
345
|
+
##
|
346
|
+
# Get or set the current environment name,
|
347
|
+
# by default ENV ['RACK_ENV'], or "development".
|
348
|
+
|
349
|
+
def self.environment env=nil
|
350
|
+
@environment = env if env
|
351
|
+
@environment ||= ENV['RACK_ENV'] || "development"
|
352
|
+
end
|
353
|
+
|
354
|
+
|
355
|
+
##
|
356
|
+
# Check if running in development mode.
|
357
|
+
|
358
|
+
def self.development?
|
359
|
+
self.environment == "development"
|
360
|
+
end
|
361
|
+
|
362
|
+
|
363
|
+
##
|
364
|
+
# Check if running in test mode.
|
365
|
+
|
366
|
+
def self.test?
|
367
|
+
self.environment == "test"
|
368
|
+
end
|
369
|
+
|
370
|
+
|
371
|
+
##
|
372
|
+
# Check if running in staging mode.
|
373
|
+
|
374
|
+
def self.staging?
|
375
|
+
self.environment == "staging"
|
376
|
+
end
|
377
|
+
|
378
|
+
|
379
|
+
##
|
380
|
+
# Check if running in production mode.
|
381
|
+
|
382
|
+
def self.production?
|
383
|
+
self.environment == "production"
|
384
|
+
end
|
385
|
+
|
386
|
+
|
387
|
+
class_proxy :protection, :sessions, :session_secret, :middleware, :autoreload
|
388
|
+
class_proxy :error_delegate, :router
|
389
|
+
class_proxy :root_dir, :public_dir
|
390
|
+
class_proxy :mime_type, :asset_host_for, :asset_host, :asset_version
|
391
|
+
class_proxy :environment, :development?, :test?, :staging?, :production?
|
392
|
+
class_proxy :load_config, :config, :config_dir
|
393
|
+
|
394
|
+
# Application logger. Defaults to log to $stdout.
|
395
|
+
attr_accessor :logger
|
396
|
+
|
397
|
+
# App to fallback on if Gin::App is used as middleware and no route is found.
|
398
|
+
attr_reader :rack_app
|
399
|
+
|
400
|
+
# Internal Rack stack.
|
401
|
+
attr_reader :stack
|
402
|
+
|
403
|
+
|
404
|
+
##
|
405
|
+
# Create a new Rack-mountable Gin::App instance, with an optional
|
406
|
+
# rack_app and logger.
|
407
|
+
|
408
|
+
def initialize rack_app=nil, logger=nil
|
409
|
+
load_config
|
410
|
+
|
411
|
+
if !rack_app.respond_to?(:call) && rack_app.respond_to?(:log) && logger.nil?
|
412
|
+
@rack_app = nil
|
413
|
+
@logger = rack_app
|
414
|
+
else
|
415
|
+
@rack_app = rack_app
|
416
|
+
@logger = Logger.new $stdout
|
417
|
+
end
|
418
|
+
|
419
|
+
validate_all_controllers!
|
420
|
+
|
421
|
+
@app = self
|
422
|
+
@stack = build_app Rack::Builder.new
|
423
|
+
end
|
424
|
+
|
425
|
+
|
426
|
+
##
|
427
|
+
# Used for auto reloading the whole app in development mode.
|
428
|
+
# Will only reload if Gin::App.autoreload is set to true.
|
429
|
+
#
|
430
|
+
# If you use this in production, you're gonna have a bad time.
|
431
|
+
|
432
|
+
def reload!
|
433
|
+
return unless autoreload
|
434
|
+
self.class.erase! [self.class.source_file],
|
435
|
+
[self.class.name.split("::").last],
|
436
|
+
self.class.namespace
|
437
|
+
|
438
|
+
self.class.erase_dependencies!
|
439
|
+
Object.send(:require, self.class.source_file)
|
440
|
+
@app = self.class.source_class.new @rack_app, @logger
|
441
|
+
end
|
442
|
+
|
443
|
+
|
444
|
+
##
|
445
|
+
# Default Rack call method.
|
446
|
+
|
447
|
+
def call env
|
448
|
+
if filename = static?(env)
|
449
|
+
return error_delegate.exec(self, env){ send_file filename }
|
450
|
+
end
|
451
|
+
|
452
|
+
if autoreload && !env[RACK_KEYS[:reloaded]]
|
453
|
+
env[RACK_KEYS[:reloaded]] = true
|
454
|
+
reload!
|
455
|
+
@app.call env
|
456
|
+
|
457
|
+
elsif env[RACK_KEYS[:stack]]
|
458
|
+
env.delete RACK_KEYS[:stack]
|
459
|
+
@app.call! env
|
460
|
+
|
461
|
+
else
|
462
|
+
env[RACK_KEYS[:stack]] = true
|
463
|
+
@stack.call env
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
##
|
469
|
+
# Call App instance without internal middleware or reloading.
|
470
|
+
|
471
|
+
def call! env
|
472
|
+
ctrl, action, env[RACK_KEYS[:path_params]] =
|
473
|
+
router.resources_for env['REQUEST_METHOD'], env['PATH_INFO']
|
474
|
+
|
475
|
+
dispatch env, ctrl, action
|
476
|
+
end
|
477
|
+
|
478
|
+
|
479
|
+
STATIC_PATH_CLEANER = %r{\.+/|/\.+} #:nodoc:
|
480
|
+
|
481
|
+
##
|
482
|
+
# Check if the request is for a static file.
|
483
|
+
|
484
|
+
def static? env
|
485
|
+
%w{GET HEAD}.include?(env['REQUEST_METHOD']) && asset(env['PATH_INFO'])
|
486
|
+
end
|
487
|
+
|
488
|
+
|
489
|
+
##
|
490
|
+
# Check if an asset exists.
|
491
|
+
# Returns the full path to the asset if found, otherwise nil.
|
492
|
+
|
493
|
+
def asset path
|
494
|
+
path = path.gsub STATIC_PATH_CLEANER, ""
|
495
|
+
|
496
|
+
filepath = File.expand_path(File.join(public_dir, path))
|
497
|
+
return filepath if File.file? filepath
|
498
|
+
|
499
|
+
filepath = File.expand_path(File.join(Gin::PUBLIC_DIR, path))
|
500
|
+
return filepath if File.file? filepath
|
501
|
+
end
|
502
|
+
|
503
|
+
|
504
|
+
##
|
505
|
+
# Dispatch the Rack env to the given controller and action.
|
506
|
+
|
507
|
+
def dispatch env, ctrl, action
|
508
|
+
raise Gin::NotFound,
|
509
|
+
"No route exists for: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}" unless
|
510
|
+
ctrl && action
|
511
|
+
|
512
|
+
ctrl.new(self, env).call_action action
|
513
|
+
|
514
|
+
rescue Gin::NotFound => err
|
515
|
+
@rack_app ? @rack_app.call(env) : handle_error(err, env)
|
516
|
+
|
517
|
+
rescue ::Exception => err
|
518
|
+
handle_error(err, env)
|
519
|
+
end
|
520
|
+
|
521
|
+
|
522
|
+
##
|
523
|
+
# Handle error with error_delegate if available, otherwise re-raise.
|
524
|
+
|
525
|
+
def handle_error err, env
|
526
|
+
delegate = error_delegate
|
527
|
+
|
528
|
+
begin
|
529
|
+
trace = Gin.app_trace(Array(err.backtrace)).join("\n")
|
530
|
+
logger.error("#{err.class.name}: #{err.message}\n#{trace}")
|
531
|
+
delegate.exec(self, env){ handle_error(err) }
|
532
|
+
|
533
|
+
rescue ::Exception => err
|
534
|
+
delegate = Gin::Controller and retry unless delegate == Gin::Controller
|
535
|
+
raise
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
|
540
|
+
private
|
541
|
+
|
542
|
+
|
543
|
+
def build_app builder
|
544
|
+
setup_sessions builder
|
545
|
+
setup_protection builder
|
546
|
+
middleware.each do |args|
|
547
|
+
block = args.pop if Proc === args.last
|
548
|
+
builder.use(*args, &block)
|
549
|
+
end
|
550
|
+
|
551
|
+
builder.run self
|
552
|
+
builder.to_app
|
553
|
+
end
|
554
|
+
|
555
|
+
|
556
|
+
def setup_sessions builder
|
557
|
+
return unless sessions
|
558
|
+
options = {}
|
559
|
+
options[:secret] = session_secret if session_secret
|
560
|
+
options.merge! sessions.to_hash if sessions.respond_to? :to_hash
|
561
|
+
builder.use Rack::Session::Cookie, options
|
562
|
+
end
|
563
|
+
|
564
|
+
|
565
|
+
def setup_protection builder
|
566
|
+
return unless protection
|
567
|
+
options = Hash === protection ? protection.dup : {}
|
568
|
+
options[:except] = Array options[:except]
|
569
|
+
options[:except] += [:session_hijacking, :remote_token] unless sessions
|
570
|
+
options[:reaction] ||= :drop_session
|
571
|
+
builder.use Rack::Protection, options
|
572
|
+
end
|
573
|
+
|
574
|
+
|
575
|
+
##
|
576
|
+
# Make sure all controller actions have a route, or raise a RouterError.
|
577
|
+
|
578
|
+
def validate_all_controllers!
|
579
|
+
actions = {}
|
580
|
+
|
581
|
+
router.each_route do |route, ctrl, action|
|
582
|
+
(actions[ctrl] ||= []) << action
|
583
|
+
end
|
584
|
+
|
585
|
+
actions.each do |ctrl, actions|
|
586
|
+
not_mounted = ctrl.actions - actions
|
587
|
+
raise RouterError, "#{ctrl}##{not_mounted[0]} has no route." unless
|
588
|
+
not_mounted.empty?
|
589
|
+
|
590
|
+
extra_mounted = actions - ctrl.actions
|
591
|
+
raise RouterError, "#{ctrl}##{extra_mounted[0]} is not a method" unless
|
592
|
+
extra_mounted.empty?
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|