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/.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
|