gin 1.0.4 → 1.1.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/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