gin 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.rdoc CHANGED
@@ -1,3 +1,19 @@
1
+ === 1.1.0 / 2013-07-10
2
+
3
+ * Major Enhancements
4
+ * Test helper methods and assertions
5
+ * Template/View support with tilt
6
+ * New config implementation with auto-(re)loading
7
+ * App instances are individually configurable
8
+ * Gin command for easy new app generation
9
+
10
+ * Minor Enhancements
11
+ * Routes now support multiple embedded params between slashes
12
+
13
+ * Bugfixes
14
+ * Lots of thread-safety fixes
15
+ * Fixes to rare reloader path issue
16
+
1
17
  === 1.0.4 / 2013-03-19
2
18
 
3
19
  * Minor Enhancements
data/Manifest.txt CHANGED
@@ -1,11 +1,11 @@
1
- .autotest
2
- .gitignore
3
1
  History.rdoc
4
2
  Manifest.txt
5
3
  README.rdoc
6
4
  Rakefile
5
+ bin/gin
7
6
  lib/gin.rb
8
7
  lib/gin/app.rb
8
+ lib/gin/cache.rb
9
9
  lib/gin/config.rb
10
10
  lib/gin/constants.rb
11
11
  lib/gin/controller.rb
@@ -18,7 +18,9 @@ lib/gin/reloadable.rb
18
18
  lib/gin/request.rb
19
19
  lib/gin/response.rb
20
20
  lib/gin/router.rb
21
+ lib/gin/rw_lock.rb
21
22
  lib/gin/stream.rb
23
+ lib/gin/test.rb
22
24
  public/400.html
23
25
  public/404.html
24
26
  public/500.html
@@ -29,10 +31,16 @@ public/gin_sm.png
29
31
  test/app/app_foo.rb
30
32
  test/app/controllers/app_controller.rb
31
33
  test/app/controllers/foo_controller.rb
34
+ test/app/layouts/bar.erb
35
+ test/app/layouts/foo.erb
36
+ test/app/views/bar.erb
32
37
  test/mock_config/backend.yml
38
+ test/mock_config/invalid.yml
33
39
  test/mock_config/memcache.yml
34
40
  test/mock_config/not_a_config.txt
41
+ test/mock_app.rb
35
42
  test/test_app.rb
43
+ test/test_cache.rb
36
44
  test/test_config.rb
37
45
  test/test_controller.rb
38
46
  test/test_errorable.rb
@@ -42,3 +50,5 @@ test/test_helper.rb
42
50
  test/test_request.rb
43
51
  test/test_response.rb
44
52
  test/test_router.rb
53
+ test/test_rw_lock.rb
54
+ test/test_test.rb
data/README.rdoc CHANGED
@@ -7,8 +7,15 @@
7
7
  Gin is a small Ruby web framework, built on Rack, which borrows from
8
8
  Sinatra expressiveness, and targets larger applications.
9
9
 
10
+ == Getting Started
11
+
12
+ Gin provides a simple command for setting up a new application.
13
+ $ gin path/to/appname
14
+
10
15
  == Hello World
11
16
 
17
+ A Hello World application requires at minimum a Gin::App instance and a Gin::Controller.
18
+
12
19
  # config.ru
13
20
  require 'gin'
14
21
 
@@ -22,10 +29,36 @@ Sinatra expressiveness, and targets larger applications.
22
29
 
23
30
  run MyApp.new
24
31
 
32
+ == Why Gin?
33
+
34
+ Gin tries to fill a need for a framework that's simple and lightweight, yet scalable for large applications. Gin is comparable to Sinatra in performance and memory consumption, but supports multiple controllers and filters.
35
+
36
+ === Features
37
+
38
+ * Easily mount multiple apps as Rack middleware
39
+ * Simple and fast RESTful routing
40
+ * Controller classes and action arguments
41
+ * Inheritable Before/After filter chains
42
+ * Response streaming
43
+ * Asset urls with cache-busting and CDN support
44
+ * Configs for multiple environments
45
+ * Periodic config reloading
46
+ * Inheritable error handling
47
+ * In-app middleware
48
+ * Rack-Session and Rack-Protection
49
+ * Dev-mode auto-reloading
50
+ * Templates (layouts/views) with Tilt
51
+
52
+ === What's Not Included
53
+
54
+ * Helpers - those are called Modules in Ruby
55
+ * ORM - choose your own library
56
+
25
57
  == Requirements
26
58
 
27
59
  * rack
28
60
  * rack-protection
61
+ * tilt
29
62
 
30
63
  == Install
31
64
 
data/Rakefile CHANGED
@@ -11,6 +11,11 @@ Hoe.spec 'gin' do
11
11
 
12
12
  self.extra_deps << ['rack', '~>1.1']
13
13
  self.extra_deps << ['rack-protection', '~>1.0']
14
+ self.extra_deps << ['tilt', '~>1.4']
15
+
16
+ self.extra_dev_deps << ['nokogiri', '~>1.5.9']
17
+ self.extra_dev_deps << ['plist', '~>3.1.0']
18
+ self.extra_dev_deps << ['bson', '~>1.9.0']
14
19
  end
15
20
 
16
21
  # vim: syntax=ruby
data/TODO.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ - Extend router functionality to allow multiple %s per node.
2
+
3
+ = Maybe
4
+ - Custom environment names for dev, test, stage, and prod
5
+ - Find a better default non-locking logger
6
+ - Bundler-runtime-free
7
+ - script/server equivalent
data/bin/gin ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), "../lib") if $0 == "bin/gin"
4
+ require 'gin'
5
+
6
+ class Gin::Cmd
7
+ def self.run argv=ARGV
8
+ show_help if argv.empty? || argv.delete("-h") || argv.delete("--help")
9
+
10
+ bare = !!argv.delete("--bare")
11
+
12
+ path = argv.pop
13
+ error("Missing app_name argument. Use gin -h for help.") if !path
14
+
15
+ error("Invalid options #{argv.join(", ")}.") unless argv.empty?
16
+
17
+ name = File.basename(path)
18
+ dir = File.expand_path(path)
19
+ parent_ru_path = File.expand_path(File.join(dir, "..", "*.ru"))
20
+ standalone = Dir[parent_ru_path].empty?
21
+
22
+ make_dirs(dir, bare)
23
+
24
+ app_class_name = Gin.camelize(name)
25
+ make_config_ru(app_class_name, name, dir) if standalone
26
+ make_console(name, dir) if standalone
27
+ make_app_rb(app_class_name, name, dir, standalone)
28
+ make_home_ctrl_rb(app_class_name, name, dir)
29
+
30
+ puts "You're all set! Your new app is waiting at #{dir}.\n\n"
31
+ exit 0
32
+ end
33
+
34
+
35
+ def self.error msg
36
+ $stderr.puts "#{msg}\n\n"
37
+ exit 1
38
+ end
39
+
40
+
41
+ def self.show_help
42
+ puts <<-STR
43
+
44
+ gin #{Gin::VERSION}
45
+
46
+ Create a new Gin application.
47
+
48
+ gin <path/to/app_name>
49
+
50
+ Examples:
51
+
52
+ $ gin my_website
53
+ $ gin my_api --bare
54
+
55
+ Options:
56
+ --bare Don't create view-related files/dirs
57
+ -h --help Show this screen
58
+
59
+ Gin applications created in a directory containing
60
+ a *.ru file will not generate its own config.ru.
61
+
62
+ STR
63
+ true
64
+ end
65
+
66
+
67
+ def self.make_dirs dir, bare=false
68
+ error("Can't create directory: #{dir} already exists") if File.directory?(dir)
69
+
70
+ Dir.mkdir(dir)
71
+
72
+ unless bare
73
+ Dir.mkdir(File.join(dir, "views"))
74
+ Dir.mkdir(File.join(dir, "layouts"))
75
+ Dir.mkdir(File.join(dir, "public"))
76
+ Dir.mkdir(File.join(dir, "public", "js"))
77
+ Dir.mkdir(File.join(dir, "public", "css"))
78
+ Dir.mkdir(File.join(dir, "public", "img"))
79
+ end
80
+
81
+ Dir.mkdir(File.join(dir, "lib"))
82
+ Dir.mkdir(File.join(dir, "config"))
83
+ Dir.mkdir(File.join(dir, "controllers"))
84
+ end
85
+
86
+
87
+ def self.make_home_ctrl_rb app_class_name, name, dir
88
+ contents = <<-STR
89
+ class #{app_class_name}::HomeController < Gin::Controller
90
+ def index
91
+ <<-HTML
92
+ <!DOCTYPE html>
93
+ <html>
94
+ <head>
95
+ <title>Welcome to Gin</title>
96
+ <link rel="stylesheet" type="text/css" href="/gin.css"/>
97
+ </head>
98
+ <body style="">
99
+ <div class="canvas">
100
+ <img src="/gin_sm.png" class="logo"/>
101
+ <h1>Welcome to Gin</h1>
102
+ <p>Gin is a lightweight framework geared towards API and Website development.</br>
103
+ Start building your app by editing your app's root <strong>.rb</strong> file and
104
+ your <strong>HomeController</strong> at <strong>controllers/home_controller.rb</strong>.</p>
105
+ <p><center>The <a href="http://yaks.me/gin/wiki" target="_blank">WIKI</a> is available for help and documentation.
106
+ </center></p>
107
+ </div>
108
+ </body>
109
+ </html>
110
+ HTML
111
+ end
112
+ end
113
+ STR
114
+
115
+ File.write(File.join(dir, "controllers/home_controller.rb"), contents)
116
+ end
117
+
118
+
119
+ def self.make_app_rb app_class_name, name, dir, standalone
120
+ contents = <<-STR
121
+ require 'gin'
122
+
123
+ $:.unshift( File.expand_path('../lib', __FILE__) )
124
+ #{"$:.unshift( File.expand_path('../controllers', __FILE__) )\n" if standalone}
125
+
126
+ class #{app_class_name} < Gin::App
127
+ require '#{"#{name}/controllers/" if !standalone}home_controller'
128
+ mount #{app_class_name}::HomeController, "/"
129
+ end
130
+ STR
131
+
132
+ File.write(File.join(dir, "#{name}.rb"), contents)
133
+ end
134
+
135
+
136
+ def self.make_config_ru app_class_name, name, dir
137
+ contents = <<-STR
138
+ $:.unshift File.expand_path("..", __FILE__)
139
+ require '#{name}'
140
+ run #{app_class_name}.new
141
+ STR
142
+
143
+ filepath = File.join(dir, 'config.ru')
144
+ File.write(filepath, contents)
145
+ end
146
+
147
+
148
+ def self.make_console name, dir
149
+ filepath = File.join(dir, 'console')
150
+ bash = "irb -I \"$( dirname \"${BASH_SOURCE[0]}\" )\" -r #{name}\n"
151
+ File.write(filepath, bash)
152
+ require 'fileutils'
153
+ FileUtils.chmod "u=wrx,go=rx", filepath
154
+ end
155
+ end
156
+
157
+
158
+ Gin::Cmd.run
data/lib/gin.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  require 'rack'
2
+ require 'tilt'
2
3
 
3
4
  class Gin
4
- VERSION = '1.0.4'
5
+ VERSION = '1.1.0'
5
6
 
6
7
  LIB_DIR = File.expand_path("..", __FILE__) #:nodoc:
7
8
  PUBLIC_DIR = File.expand_path("../../public/", __FILE__) #:nodoc:
@@ -16,6 +17,10 @@ class Gin
16
17
  def http_status; 404; end
17
18
  end
18
19
 
20
+ class MissingConfig < Error; end
21
+
22
+ class TemplateMissing < Error; end
23
+
19
24
 
20
25
  ##
21
26
  # Change string to underscored version.
@@ -29,6 +34,16 @@ class Gin
29
34
  end
30
35
 
31
36
 
37
+ ##
38
+ # Change string to camel-case version.
39
+
40
+ def self.camelize str
41
+ str = str.dup
42
+ str.gsub!(/\/([^_:])/){|m| "::#{$1.upcase}" }
43
+ str.gsub!(/(^|_+)([^_:])/){|m| $2.upcase }
44
+ end
45
+
46
+
32
47
  ##
33
48
  # Create a URI query from a Hash.
34
49
 
@@ -110,13 +125,15 @@ class Gin
110
125
  require 'gin/core_ext/rack_commonlogger'
111
126
 
112
127
  require 'gin/constants'
113
- require 'gin/app'
128
+ require 'gin/cache'
114
129
  require 'gin/router'
115
130
  require 'gin/config'
116
131
  require 'gin/request'
117
132
  require 'gin/response'
133
+ require 'gin/rw_lock'
118
134
  require 'gin/stream'
119
135
  require 'gin/errorable'
120
136
  require 'gin/filterable'
121
137
  require 'gin/controller'
138
+ require 'gin/app'
122
139
  end
data/lib/gin/app.rb CHANGED
@@ -18,10 +18,10 @@ class Gin::App
18
18
  class RouterError < Gin::Error; end
19
19
 
20
20
  CALLERS_TO_IGNORE = [ # :nodoc:
21
- /\/gin(\/(.*?))?\.rb$/, # all gin code
22
- /lib\/tilt.*\.rb$/, # all tilt code
21
+ /lib\/gin(\/(.*?))?\.rb/, # all gin code
22
+ /lib\/tilt.*\.rb/, # all tilt code
23
23
  /^\(.*\)$/, # generated code
24
- /rubygems\/custom_require\.rb$/, # rubygems require hacks
24
+ /rubygems\/custom_require\.rb/, # rubygems require hacks
25
25
  /active_support/, # active_support require hacks
26
26
  /bundler(\/runtime)?\.rb/, # bundler require hacks
27
27
  /<internal:/, # internal in ruby >= 1.9.2
@@ -30,17 +30,41 @@ class Gin::App
30
30
 
31
31
 
32
32
  def self.inherited subclass #:nodoc:
33
+ subclass.setup
34
+ end
35
+
36
+
37
+ def self.setup # :nodoc:
33
38
  caller_line = caller.find{|line| !CALLERS_TO_IGNORE.any?{|m| line =~ m} }
34
39
  filepath = File.expand_path(caller_line.split(/:\d+:in `/).first)
35
- dir = File.dirname(filepath)
36
- subclass.root_dir dir
37
- subclass.instance_variable_set("@source_file", filepath)
38
- subclass.instance_variable_set("@source_class", subclass.to_s)
40
+ dir = File.dirname(filepath)
41
+
42
+ @source_file = filepath
43
+ @source_class = self.to_s
44
+ @templates = Gin::Cache.new
45
+ @md5s = Gin::Cache.new
46
+ @reload_mutex = Mutex.new
47
+ @autoreload = nil
48
+ @instance = nil
49
+
50
+ @options = {}
51
+ @options[:root_dir] = dir
52
+ @options[:environment] = ENV['RACK_ENV'] || ENV_DEV
53
+ @options[:error_delegate] = Gin::Controller
54
+ @options[:middleware] = []
55
+ @options[:logger] = $stdout
56
+ @options[:router] = Gin::Router.new
57
+ @options[:session_secret] = SESSION_SECRET
58
+ @options[:protection] = false
59
+ @options[:sessions] = false
60
+ @options[:config_reload] = false
61
+ @options[:layout] = :layout
62
+ @options[:template_engines] = Tilt.mappings.merge(nil => [Tilt::ERBTemplate])
39
63
  end
40
64
 
41
65
 
42
66
  ##
43
- # Create a new intance of the app and call it.
67
+ # Create a new instance of the app and call it.
44
68
 
45
69
  def self.call env
46
70
  @instance ||= self.new
@@ -48,6 +72,35 @@ class Gin::App
48
72
  end
49
73
 
50
74
 
75
+ ##
76
+ # Hash of the full Gin::App configuration.
77
+ # Result of using class-level setter methods such as Gin::App.environment.
78
+
79
+ def self.options
80
+ @options
81
+ end
82
+
83
+
84
+ ##
85
+ # Returns the source file of the current app.
86
+
87
+ def self.source_file
88
+ @source_file
89
+ end
90
+
91
+
92
+ def self.namespace #:nodoc:
93
+ # Parent namespace of the App class. Used for reloading purposes.
94
+ Gin.const_find(@source_class.split("::")[0..-2]) if @source_class
95
+ end
96
+
97
+
98
+ def self.source_class #:nodoc:
99
+ # Lookup the class from its name. Used for reloading purposes.
100
+ Gin.const_find(@source_class) if @source_class
101
+ end
102
+
103
+
51
104
  ##
52
105
  # Enable or disable auto-app reloading.
53
106
  # On by default in development mode.
@@ -55,21 +108,36 @@ class Gin::App
55
108
  # In order for an app to be reloadable, the libs and controllers must be
56
109
  # required from the Gin::App class context, or use MyApp.require("lib").
57
110
  #
58
- # Reloading is not supported for applications defined in the config.ru file.
111
+ # Gin::App class reloading is not supported for applications defined in
112
+ # the config.ru file. Dependencies will, however, still be reloaded.
113
+ #
114
+ # Autoreload must be enabled before any calls to Gin::App.require for
115
+ # those files to be reloaded.
116
+ #
117
+ # class MyApp < Gin::App
118
+ #
119
+ # # Only reloaded in development mode
120
+ # require 'nokogiri'
121
+ #
122
+ # autoreload false
123
+ # # Never reloaded
124
+ # require 'static_thing'
125
+ #
126
+ # autoreload true
127
+ # # Reloaded every request
128
+ # require 'my_app/home_controller'
129
+ # end
59
130
 
60
131
  def self.autoreload val=nil
61
132
  @autoreload = val unless val.nil?
133
+ reload = @autoreload.nil? ? self.environment == ENV_DEV : @autoreload
62
134
 
63
- if @autoreload.nil?
64
- @autoreload = File.extname(source_file) != ".ru" && development?
135
+ if reload
136
+ Object.send :require, 'gin/reloadable' unless defined?(Gin::Reloadable)
137
+ include Gin::Reloadable unless self < Gin::Reloadable
65
138
  end
66
139
 
67
- if @autoreload && (!defined?(Gin::Reloadable) || !include?(Gin::Reloadable))
68
- Object.send :require, 'gin/reloadable'
69
- include Gin::Reloadable
70
- end
71
-
72
- @autoreload
140
+ reload
73
141
  end
74
142
 
75
143
 
@@ -121,38 +189,47 @@ class Gin::App
121
189
 
122
190
 
123
191
  ##
124
- # Returns the source file of the current app.
125
-
126
- def self.source_file
127
- @source_file
128
- end
129
-
192
+ # Get or set the CDN asset host (and path).
193
+ # If block is given, evaluates the block on every read.
130
194
 
131
- def self.namespace #:nodoc:
132
- # Parent namespace of the App class. Used for reloading purposes.
133
- Gin.const_find(@source_class.split("::")[0..-2]) if @source_class
195
+ def self.asset_host host=nil, &block
196
+ @options[:asset_host] = host if host
197
+ @options[:asset_host] = block if block_given?
198
+ @options[:asset_host]
134
199
  end
135
200
 
136
201
 
137
- def self.source_class #:nodoc:
138
- # Lookup the class from its name. Used for reloading purposes.
139
- Gin.const_find(@source_class) if @source_class
202
+ def self.make_config opts={} # :nodoc:
203
+ Gin::Config.new opts[:environment] || self.environment,
204
+ dir: opts[:config_dir] || self.config_dir,
205
+ logger: opts[:logger] || self.logger,
206
+ ttl: opts[:config_reload] || self.config_reload
140
207
  end
141
208
 
142
209
 
143
210
  ##
144
- # Get or set the root directory of the application.
145
- # Defaults to the app file's directory.
211
+ # Access the config for your application, loaded from the config_dir.
212
+ # # config/memcache.yml
213
+ # default: &default
214
+ # host: example.com
215
+ # connections: 1
216
+ # development: *default
217
+ # host: localhost
218
+ #
219
+ # # access from App class
220
+ # CACHE = Memcache.new( MyApp.config['memcache.host'] )
221
+ #
222
+ # The config object is shared across all instances of the App and has
223
+ # thread-safety built-in.
146
224
 
147
- def self.root_dir dir=nil
148
- @root_dir = dir if dir
149
- @root_dir
225
+ def self.config
226
+ @options[:config] ||= make_config
150
227
  end
151
228
 
152
229
 
153
230
  ##
154
231
  # Get or set the path to the config directory.
155
- # Defaults to root_dir + "config"
232
+ # Defaults to "<root_dir>/config"
156
233
  #
157
234
  # Configs are expected to be YAML files following this pattern:
158
235
  # default: &default
@@ -168,108 +245,129 @@ class Gin::App
168
245
  # the current environment will be accessible.
169
246
 
170
247
  def self.config_dir dir=nil
171
- @config_dir = dir if dir
172
- @config_dir ||= File.join(root_dir, "config")
248
+ if String === dir
249
+ @options[:config_dir] = dir
250
+ @options[:config].dir = dir if @options[:config]
251
+ end
252
+
253
+ @options[:config_dir] || File.join(self.root_dir, "config")
173
254
  end
174
255
 
175
256
 
176
257
  ##
177
- # Access the config for your application, loaded from the config_dir.
178
- # # config/memcache.yml
179
- # default: &default
180
- # host: example.com
181
- # connections: 1
182
- # development: *default
183
- # host: localhost
258
+ # Get or set the config max age for auto-reloading in seconds.
259
+ # Turns config reloading off if set to false. Defaults to false.
260
+ # Config only gets reloaded on demand.
261
+ #
262
+ # # Set to 10 minutes
263
+ # config_reload 600
184
264
  #
185
- # # access from App class or instance
186
- # config.memcache['host']
265
+ # # Set to never reload
266
+ # config_reload false
187
267
 
188
- def self.config
189
- @config ||= Gin::Config.new environment, config_dir
268
+ def self.config_reload ttl=nil
269
+ unless ttl.nil?
270
+ @options[:config_reload] = ttl
271
+ @options[:config].ttl = ttl if @options[:config]
272
+ end
273
+ @options[:config_reload]
190
274
  end
191
275
 
192
276
 
193
277
  ##
194
- # Loads all configs from the config_dir.
278
+ # Set the default templating engine to use for various
279
+ # file extensions, or by default:
280
+ # # Default for .markdown and .md files
281
+ # default_template Tilt::MarukuTemplate, 'markdown', 'md'
282
+ #
283
+ # # Default for files without preset default
284
+ # default_template Tilt::BlueClothTemplate
195
285
 
196
- def self.load_config
197
- return unless File.directory?(config_dir)
198
- config.dir = config_dir
199
- config.load!
286
+ def self.default_template klass, *extensions
287
+ extensions = [nil] if extensions.empty?
288
+ extensions.each{|ext|
289
+ (@options[:template_engines][ext] ||= []).unshift klass }
200
290
  end
201
291
 
202
292
 
203
293
  ##
204
- # Get or set the path to the public directory.
205
- # Defaults to root_dir + "public"
294
+ # Get or set the current environment name,
295
+ # by default ENV ['RACK_ENV'], or "development".
206
296
 
207
- def self.public_dir dir=nil
208
- @public_dir = dir if dir
209
- @public_dir ||= File.join(root_dir, "public")
297
+ def self.environment env=nil
298
+ if env
299
+ @options[:environment] = env
300
+ @options[:config].environment = env if @options[:config]
301
+ end
302
+ @options[:environment]
210
303
  end
211
304
 
212
305
 
213
306
  ##
214
- # Get or set the CDN asset host (and path).
215
- # If block is given, evaluates the block on every read.
307
+ # Define a Gin::Controller as a catch-all error rendering controller.
308
+ # This can be a dedicated controller, or a parent controller
309
+ # such as AppController. Defaults to Gin::Controller.
310
+ #
311
+ # The error delegate should handle the following errors
312
+ # for creating custom pages for Gin errors:
313
+ # Gin::NotFound, Gin::BadRequest, ::Exception
216
314
 
217
- def self.asset_host host=nil, &block
218
- @asset_host = host if host
219
- @asset_host = block if block_given?
220
- host = @asset_host.respond_to?(:call) ? @asset_host.call : @asset_host
315
+ def self.error_delegate ctrl=nil
316
+ @options[:error_delegate] = ctrl if ctrl
317
+ @options[:error_delegate]
221
318
  end
222
319
 
223
320
 
224
321
  ##
225
- # Returns the asset host for a given asset name. This is useful when assigning
226
- # a block for the asset_host. The asset_name argument is passed to the block.
322
+ # Get or set the layout name. Layout file location is assumed to be in
323
+ # the views_dir. If the views dir has a controller wildcard '*', the layout
324
+ # is assumed to be one level above the controller-specific directory.
325
+ #
326
+ # Defaults to :layout.
227
327
 
228
- def self.asset_host_for asset_name
229
- @asset_host.respond_to?(:call) ? @asset_host.call(asset_name) : @asset_host
328
+ def self.layout name=nil
329
+ @options[:layout] = name if name
330
+ @options[:layout]
230
331
  end
231
332
 
232
333
 
233
334
  ##
234
- # Returns the first 8 bytes of the asset file's md5.
235
- # File path is assumed relative to the public_dir.
236
-
237
- def self.asset_version path
238
- path = File.expand_path(File.join(public_dir, path))
239
- return unless File.file?(path)
335
+ # Get or set the directory for view layouts.
336
+ # Defaults to the "<root_dir>/layouts".
240
337
 
241
- @asset_versions ||= {}
242
- @asset_versions[path] ||= md5(path)
338
+ def self.layouts_dir dir=nil
339
+ @options[:layouts_dir] = dir if dir
340
+ @options[:layouts_dir] || File.join(root_dir, 'layouts')
243
341
  end
244
342
 
245
343
 
246
- MD5 = RUBY_PLATFORM =~ /darwin/ ? 'md5 -q' : 'md5sum' #:nodoc:
344
+ ##
345
+ # Get or set the logger for your application. Loggers must respond
346
+ # to the << method.
247
347
 
248
- def self.md5 path #:nodoc:
249
- `#{MD5} #{path}`[0...8]
348
+ def self.logger new_logger=nil
349
+ if new_logger
350
+ @options[:logger] = new_logger
351
+ @options[:config].logger = new_logger if @options[:config]
352
+ end
353
+ @options[:logger]
250
354
  end
251
355
 
252
356
 
253
357
  ##
254
- # Define a Gin::Controller as a catch-all error rendering controller.
255
- # This can be a dedicated controller, or a parent controller
256
- # such as AppController. Defaults to Gin::Controller.
257
- #
258
- # The error delegate should handle the following errors
259
- # for creating custom pages for Gin errors:
260
- # Gin::NotFound, Gin::BadRequest, ::Exception
358
+ # Cache of file md5s shared across all instances of an App class.
359
+ # Used for static asset versioning.
261
360
 
262
- def self.error_delegate ctrl=nil
263
- @error_delegate = ctrl if ctrl
264
- @error_delegate ||= Gin::Controller
361
+ def self.md5s
362
+ @md5s
265
363
  end
266
364
 
267
365
 
268
366
  ##
269
- # Router instance that handles mapping Rack-env -> Controller#action.
367
+ # List of internal app middleware.
270
368
 
271
- def self.router
272
- @router ||= Gin::Router.new
369
+ def self.middleware
370
+ @options[:middleware]
273
371
  end
274
372
 
275
373
 
@@ -296,22 +394,45 @@ class Gin::App
296
394
 
297
395
 
298
396
  ##
299
- # Add middleware internally to the app.
300
- # Middleware statuses and Exceptions will NOT be
301
- # handled by the error_delegate.
397
+ # Use rack-protection or not. Supports assigning
398
+ # hash for options. Defaults to false.
302
399
 
303
- def self.use middleware, *args, &block
304
- ary = [middleware, *args]
305
- ary << block if block_given?
306
- self.middleware << ary
400
+ def self.protection opts=nil
401
+ @options[:protection] = opts unless opts.nil?
402
+ @options[:protection]
307
403
  end
308
404
 
309
405
 
310
406
  ##
311
- # List of internal app middleware.
407
+ # Get or set the path to the public directory.
408
+ # Defaults to "<root_dir>/public"
312
409
 
313
- def self.middleware
314
- @middleware ||= []
410
+ def self.public_dir dir=nil
411
+ @options[:public_dir] = dir if dir
412
+ @options[:public_dir] || File.join(root_dir, "public")
413
+ end
414
+
415
+
416
+ def self.reload_mutex # :nodoc:
417
+ @reload_mutex
418
+ end
419
+
420
+
421
+ ##
422
+ # Get or set the root directory of the application.
423
+ # Defaults to the app file's directory.
424
+
425
+ def self.root_dir dir=nil
426
+ @options[:root_dir] = File.expand_path(dir) if dir
427
+ @options[:root_dir]
428
+ end
429
+
430
+
431
+ ##
432
+ # Router instance that handles mapping Rack-env -> Controller#action.
433
+
434
+ def self.router
435
+ @options[:router]
315
436
  end
316
437
 
317
438
 
@@ -320,9 +441,8 @@ class Gin::App
320
441
  # hash for options. Defaults to false.
321
442
 
322
443
  def self.sessions opts=nil
323
- @session = opts unless opts.nil?
324
- @session = false if @session.nil?
325
- @session
444
+ @options[:sessions] = opts unless opts.nil?
445
+ @options[:sessions]
326
446
  end
327
447
 
328
448
 
@@ -331,36 +451,117 @@ class Gin::App
331
451
  # Defaults to a new random value on boot.
332
452
 
333
453
  def self.session_secret val=nil
334
- @session_secret = val if val
335
- @session_secret ||= "%064x" % Kernel.rand(2**256-1)
454
+ @options[:session_secret] = val if val
455
+ @options[:session_secret]
336
456
  end
337
457
 
338
458
 
339
459
  ##
340
- # Use rack-protection or not. Supports assigning
341
- # hash for options. Defaults to false.
460
+ # Cache of precompiled templates, shared across all instances of a
461
+ # given App class.
342
462
 
343
- def self.protection opts=nil
344
- @protection = opts unless opts.nil?
345
- @protection = false if @protection.nil?
346
- @protection
463
+ def self.templates
464
+ @templates
347
465
  end
348
466
 
349
467
 
350
468
  ##
351
- # Get or set the current environment name,
352
- # by default ENV ['RACK_ENV'], or "development".
469
+ # Add middleware internally to the app.
470
+ # Middleware statuses and Exceptions will NOT be
471
+ # handled by the error_delegate.
353
472
 
354
- def self.environment env=nil
355
- @environment = env if env
356
- @environment ||= ENV['RACK_ENV'] || ENV_DEV
473
+ def self.use middleware, *args, &block
474
+ ary = [middleware, *args]
475
+ ary << block if block_given?
476
+ self.middleware << ary
477
+ end
478
+
479
+
480
+ ##
481
+ # Get or set the path to the views directory.
482
+ # The wildcard '*' will be replaced by the controller name.
483
+ #
484
+ # Defaults to "<root_dir>/views"
485
+
486
+ def self.views_dir dir=nil
487
+ @options[:views_dir] = dir if dir
488
+ @options[:views_dir] || File.join(root_dir, 'views')
489
+ end
490
+
491
+
492
+ opt_reader :protection, :sessions, :session_secret, :middleware
493
+ opt_reader :error_delegate, :router, :logger
494
+ opt_reader :layout, :layouts_dir, :views_dir, :template_engines
495
+ opt_reader :root_dir, :public_dir, :environment
496
+
497
+ class_proxy :mime_type, :md5s, :templates, :reload_mutex, :autoreload
498
+
499
+ # App to fallback on if Gin::App is used as middleware and no route is found.
500
+ attr_reader :rack_app
501
+
502
+ # Options applied to the Gin::App instance. Typically a result of
503
+ # class-level configuration methods, such as Gin::App.environment.
504
+ attr_reader :options
505
+
506
+ # Internal Rack stack.
507
+ attr_reader :stack
508
+
509
+ ##
510
+ # Create a new Rack-mountable Gin::App instance, with an optional
511
+ # rack_app and options.
512
+
513
+ def initialize rack_app=nil, options={}
514
+ if Hash === rack_app
515
+ options = rack_app
516
+ @rack_app = nil
517
+ else
518
+ @rack_app = rack_app
519
+ end
520
+
521
+ @options = {
522
+ config_dir: self.class.config_dir,
523
+ public_dir: self.class.public_dir,
524
+ layouts_dir: self.class.layouts_dir,
525
+ views_dir: self.class.views_dir,
526
+ config: self.class.config
527
+ }.merge(self.class.options).merge(options)
528
+
529
+ @options[:config] = self.class.make_config(@options) if
530
+ @options[:environment] != @options[:config].environment ||
531
+ @options[:config_dir] != @options[:config].dir ||
532
+ @options[:config_reload] != @options[:config].ttl
533
+
534
+ validate_all_controllers!
535
+
536
+ @app = self
537
+ @stack = build_app Rack::Builder.new
538
+ end
539
+
540
+
541
+ ##
542
+ # Access the config for your application, loaded from the config_dir.
543
+ # # config/memcache.yml
544
+ # default: &default
545
+ # host: example.com
546
+ # connections: 1
547
+ # development: *default
548
+ # host: localhost
549
+ #
550
+ # # access from App instance
551
+ # @app.config['memcache.host']
552
+ #
553
+ # The config object is shared across all instances of the App and has
554
+ # thread-safety built-in.
555
+
556
+ def config
557
+ @options[:config]
357
558
  end
358
559
 
359
560
 
360
561
  ##
361
562
  # Check if running in development mode.
362
563
 
363
- def self.development?
564
+ def development?
364
565
  self.environment == ENV_DEV
365
566
  end
366
567
 
@@ -368,7 +569,7 @@ class Gin::App
368
569
  ##
369
570
  # Check if running in test mode.
370
571
 
371
- def self.test?
572
+ def test?
372
573
  self.environment == ENV_TEST
373
574
  end
374
575
 
@@ -376,7 +577,7 @@ class Gin::App
376
577
  ##
377
578
  # Check if running in staging mode.
378
579
 
379
- def self.staging?
580
+ def staging?
380
581
  self.environment == ENV_STAGE
381
582
  end
382
583
 
@@ -384,47 +585,84 @@ class Gin::App
384
585
  ##
385
586
  # Check if running in production mode.
386
587
 
387
- def self.production?
588
+ def production?
388
589
  self.environment == ENV_PROD
389
590
  end
390
591
 
391
592
 
392
- class_proxy :protection, :sessions, :session_secret, :middleware, :autoreload
393
- class_proxy :error_delegate, :router
394
- class_proxy :root_dir, :public_dir
395
- class_proxy :mime_type, :asset_host_for, :asset_host, :asset_version
396
- class_proxy :environment, :development?, :test?, :staging?, :production?
397
- class_proxy :load_config, :config, :config_dir
593
+ ##
594
+ # Returns the asset host for a given asset name. This is useful when assigning
595
+ # a block for the asset_host. The asset_name argument is passed to the block.
596
+
597
+ def asset_host_for asset_name
598
+ @options[:asset_host].respond_to?(:call) ?
599
+ @options[:asset_host].call(asset_name) : @options[:asset_host]
600
+ end
398
601
 
399
- # Application logger. Defaults to log to $stdout.
400
- attr_accessor :logger
401
602
 
402
- # App to fallback on if Gin::App is used as middleware and no route is found.
403
- attr_reader :rack_app
603
+ ##
604
+ # Returns the default asset host.
404
605
 
405
- # Internal Rack stack.
406
- attr_reader :stack
606
+ def asset_host
607
+ asset_host_for(nil)
608
+ end
407
609
 
408
610
 
409
611
  ##
410
- # Create a new Rack-mountable Gin::App instance, with an optional
411
- # rack_app and logger.
612
+ # Returns the first 8 bytes of the asset file's md5.
613
+ # File path is assumed relative to the public_dir.
614
+
615
+ def asset_version path
616
+ path = File.expand_path(File.join(public_dir, path))
617
+ md5(path)
618
+ end
412
619
 
413
- def initialize rack_app=nil, logger=nil
414
- load_config
415
620
 
416
- if !rack_app.respond_to?(:call) && rack_app.respond_to?(:<<) && logger.nil?
417
- @rack_app = nil
418
- @logger = rack_app
419
- else
420
- @rack_app = rack_app
421
- @logger = $stdout
621
+ MD5 = RUBY_PLATFORM =~ /darwin/ ? 'md5 -q' : 'md5sum' #:nodoc:
622
+
623
+ ##
624
+ # Returns the first 8 characters of a file's MD5 hash.
625
+ # Values are cached for future reference.
626
+
627
+ def md5 path
628
+ return unless File.file?(path)
629
+ self.md5s[path] ||= `#{MD5} #{path}`[0...8]
630
+ end
631
+
632
+
633
+ ##
634
+ # Returns the tilt template for the given template name.
635
+ # Returns nil if no template file is found.
636
+ # template_for 'user/show'
637
+ # #=> <Tilt::ERBTemplate @file="views/user/show.erb" ...>
638
+ #
639
+ # template_for 'user/show.haml'
640
+ # #=> <Tilt::HamlTemplate @file="views/user/show.haml" ...>
641
+ #
642
+ # template_for 'non-existant'
643
+ # #=> nil
644
+
645
+ def template_for path, engine=nil
646
+ templates.cache([path, engine]) do
647
+ if file = template_files(path).first
648
+ ext = File.extname(file)
649
+ ext = ext.empty? ? nil : ext[1..-1]
650
+ engine ||= template_engines[ext].first
651
+ engine.new(file) if engine
652
+ end
422
653
  end
654
+ end
423
655
 
424
- validate_all_controllers!
425
656
 
426
- @app = self
427
- @stack = build_app Rack::Builder.new
657
+ ##
658
+ # Returns an Array of file paths that match a valid path and maps
659
+ # to a known template engine.
660
+ # app.template_files 'views/foo'
661
+ # #=> ['views/foo.erb', 'views/foo.md']
662
+
663
+ def template_files path
664
+ exts = template_engines.keys.map{|e| "." << e if e }.join(",")
665
+ Dir["#{path}{#{exts}}"]
428
666
  end
429
667
 
430
668
 
@@ -435,16 +673,17 @@ class Gin::App
435
673
  # If you use this in production, you're gonna have a bad time.
436
674
 
437
675
  def reload!
438
- @mutex ||= Mutex.new
676
+ reload_mutex.synchronize do
677
+ self.class.erase_dependencies!
439
678
 
440
- @mutex.synchronize do
441
- self.class.erase! [self.class.source_file],
442
- [self.class.name.split("::").last],
443
- self.class.namespace
679
+ if File.extname(self.class.source_file) != ".ru"
680
+ self.class.erase! [self.class.source_file],
681
+ [self.class.name.split("::").last],
682
+ self.class.namespace
683
+ require self.class.source_file
684
+ end
444
685
 
445
- self.class.erase_dependencies!
446
- require self.class.source_file
447
- @app = self.class.source_class.new @rack_app, @logger
686
+ @app = self.class.source_class.new @rack_app, @options
448
687
  end
449
688
  end
450
689
 
@@ -497,8 +736,8 @@ class Gin::App
497
736
 
498
737
 
499
738
  ##
500
- # Returns a static file Rack response Array from the given gin.static
501
- # env filename.
739
+ # Returns a static file Rack response Array from the filename set
740
+ # in env['gin.static'].
502
741
 
503
742
  def call_static env
504
743
  with_log_request(env) do
@@ -509,7 +748,8 @@ class Gin::App
509
748
 
510
749
  ##
511
750
  # Check if the request is for a static file and set the gin.static env
512
- # variable to the filepath.
751
+ # variable to the filepath. Returns true if request is to a static asset,
752
+ # otherwise false.
513
753
 
514
754
  def static! env
515
755
  filepath = %w{GET HEAD}.include?(env[REQ_METHOD]) &&
@@ -526,6 +766,7 @@ class Gin::App
526
766
  # Check if the request routes to a controller and action and set
527
767
  # gin.controller, gin.action, gin.path_query_hash,
528
768
  # and gin.http_route env variables.
769
+ # Returns true if a route is found, otherwise false.
529
770
 
530
771
  def route! env
531
772
  http_route = "#{env[REQ_METHOD]} #{env[PATH_INFO]}"
@@ -566,7 +807,8 @@ class Gin::App
566
807
  "No route exists for: #{env[REQ_METHOD]} #{env[PATH_INFO]}" unless
567
808
  ctrl && action
568
809
 
569
- ctrl.new(self, env).call_action action
810
+ env[GIN_CTRL] = ctrl.new(self, env)
811
+ env[GIN_CTRL].call_action action
570
812
 
571
813
  rescue ::Exception => err
572
814
  handle_error(err, env)
@@ -599,10 +841,12 @@ class Gin::App
599
841
  now = Time.now
600
842
  time = now - env[GIN_TIMESTAMP] if env[GIN_TIMESTAMP]
601
843
 
602
- ctrl, action = env[GIN_CTRL], env[GIN_ACTION]
844
+ ctrl, action = env[GIN_CTRL].class, env[GIN_ACTION]
603
845
  target = "#{ctrl}##{action}" if ctrl && action
846
+ target = resp[2].path if !target && resp[2].respond_to?(:path)
847
+ target = "<stream>" if !target && (!(Array === resp[2]) || !resp[2].empty?)
604
848
 
605
- @logger << ( LOG_FORMAT % [
849
+ logger << ( LOG_FORMAT % [
606
850
  env[FWD_FOR] || env[REMOTE_ADDR] || "-",
607
851
  env[REMOTE_USER] || "-",
608
852
  now.strftime(TIME_FORMAT),
@@ -640,10 +884,10 @@ class Gin::App
640
884
 
641
885
  def setup_sessions builder
642
886
  return unless sessions
643
- options = {}
644
- options[:secret] = session_secret if session_secret
645
- options.merge! sessions.to_hash if sessions.respond_to? :to_hash
646
- builder.use Rack::Session::Cookie, options
887
+ opts = {}
888
+ opts[:secret] = session_secret if session_secret
889
+ opts.merge! sessions.to_hash if sessions.respond_to? :to_hash
890
+ builder.use Rack::Session::Cookie, opts
647
891
  end
648
892
 
649
893
 
@@ -651,11 +895,11 @@ class Gin::App
651
895
  return unless protection
652
896
  require 'rack-protection' unless defined?(Rack::Protection)
653
897
 
654
- options = Hash === protection ? protection.dup : {}
655
- options[:except] = Array options[:except]
656
- options[:except] += [:session_hijacking, :remote_token] unless sessions
657
- options[:reaction] ||= :drop_session
658
- builder.use Rack::Protection, options
898
+ opts = Hash === protection ? protection.dup : {}
899
+ opts[:except] = Array opts[:except]
900
+ opts[:except] += [:session_hijacking, :remote_token] unless sessions
901
+ opts[:reaction] ||= :drop_session
902
+ builder.use Rack::Protection, opts
659
903
  end
660
904
 
661
905
 
@@ -663,13 +907,13 @@ class Gin::App
663
907
  # Make sure all controller actions have a route, or raise a RouterError.
664
908
 
665
909
  def validate_all_controllers!
666
- actions = {}
910
+ actions_map = {}
667
911
 
668
912
  router.each_route do |route, ctrl, action|
669
- (actions[ctrl] ||= []) << action
913
+ (actions_map[ctrl] ||= []) << action
670
914
  end
671
915
 
672
- actions.each do |ctrl, actions|
916
+ actions_map.each do |ctrl, actions|
673
917
  not_mounted = ctrl.actions - actions
674
918
  raise RouterError, "#{ctrl}##{not_mounted[0]} has no route." unless
675
919
  not_mounted.empty?
@@ -679,4 +923,7 @@ class Gin::App
679
923
  extra_mounted.empty?
680
924
  end
681
925
  end
926
+
927
+
928
+ setup
682
929
  end