halcyon 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -92,6 +92,11 @@ namespace 'spec' do
92
92
  task :verb do
93
93
  sh "bacon -r~/lib/bacon/output -rlib/halcyon -rspec/spec_helper spec/**/* -o CSpecDox"
94
94
  end
95
+
96
+ desc "run single rspec verbosely (specify SPEC)"
97
+ task :select do
98
+ sh "bacon -r~/lib/bacon/output -rlib/halcyon -rspec/spec_helper spec/**/#{ENV['SPEC']}_spec.rb -o CSpecDox"
99
+ end
95
100
  end
96
101
 
97
102
  desc "Do predistribution stuff"
data/lib/halcyon.rb CHANGED
@@ -11,57 +11,75 @@ $:.unshift File.dirname(__FILE__)
11
11
  # Halcyon.config #=> {:allow_from => :all, :logging => {...}, ...}
12
12
  # Halcyon.paths #=> {:config => Halcyon.root/'config', ...}
13
13
  # Halcyon.logger #=> #<Logger>
14
- # Halcyon.version #=> "0.5.0"
14
+ # Halcyon.version #=> "0.5.1"
15
+ #
15
16
  module Halcyon
16
17
 
17
- VERSION = [0,5,0] unless defined?(Halcyon::VERSION)
18
+ VERSION = [0,5,1] unless defined?(Halcyon::VERSION)
18
19
 
19
20
  autoload :Application, 'halcyon/application'
20
21
  autoload :Client, 'halcyon/client'
22
+ autoload :Config, 'halcyon/config'
21
23
  autoload :Controller, 'halcyon/controller'
22
24
  autoload :Exceptions, 'halcyon/exceptions'
23
25
  autoload :Logging, 'halcyon/logging'
24
26
  autoload :Runner, 'halcyon/runner'
25
27
 
28
+ include Config::Helpers
29
+
26
30
  class << self
27
31
 
28
- attr_accessor :app
29
32
  attr_accessor :logger
30
- attr_accessor :config
31
- attr_accessor :paths
33
+ attr_writer :config
32
34
 
33
35
  def version
34
36
  VERSION.join('.')
35
37
  end
36
38
 
37
- # The root directory of the current application.
39
+ # The default <tt>root</tt> setting, overwritten by a configuration helper.
40
+ # This is so that the paths can be loaded when first booting the app.
41
+ # This won't be necessary for certain cases where the paths the root is
42
+ # manually configured, but for all other cases it can cause problems.
38
43
  #
39
- # Returns String:root_directory
40
44
  def root
41
- self.config[:root] || Dir.pwd rescue Dir.pwd
45
+ Dir.pwd
42
46
  end
43
47
 
44
- def configurable(attribute)
45
- eval <<-"end;"
46
- def #{attribute.to_s}
47
- Halcyon.config[:#{attribute.to_s}]
48
- end
49
- def #{attribute.to_s}=(value)
50
- value = value.to_mash if value.is_a?(Hash)
51
- Halcyon.config[:#{attribute.to_s}] = value
52
- end
53
- end;
48
+ # Configuration accessor which creates a configuration object when
49
+ # necessary.
50
+ #
51
+ def config
52
+ @config ||= Halcyon::Config.new
53
+ end
54
+
55
+ # Tests for Windows platform (to compensate for numerous Windows-specific
56
+ # bugs and oddities.)
57
+ #
58
+ # Returns Boolean:is_windows
59
+ #
60
+ def windows?
61
+ RUBY_PLATFORM =~ /mswin/
62
+ end
63
+
64
+ # Tests for Linux platform.
65
+ #
66
+ # Returns Boolean:is_linux
67
+ #
68
+ def linux?
69
+ RUBY_PLATFORM =~ /linux/
54
70
  end
55
- alias_method :configurable_attr, :configurable
56
71
 
57
72
  end
58
73
 
59
- # Creates <tt>Halcyon.db</tt> to alias <tt>Halcyon.config[:db]</tt>.
60
- # Also creates the complementary assignment method, <tt>Halcyon.db=</tt>
61
- # that aliases <tt>Halcyon.config[:db]=</tt>.
62
- configurable_attr :db
63
-
64
74
  end
65
75
 
66
76
  # Include the klass#logger and klass.logger accessor methods into Object.
67
77
  Object.send(:include, Halcyon::Logging::Helpers)
78
+
79
+ # The server-interface framework.
80
+ #
81
+ module Rack
82
+
83
+ autoload :JSONP, 'rack/jsonp'
84
+
85
+ end
@@ -7,21 +7,13 @@ module Halcyon
7
7
  # Manages shutting down and starting up hooks, routing, dispatching, etc.
8
8
  # Also restricts the requests to acceptable clients, defaulting to all.
9
9
  #
10
- class Application
11
- include Exceptions
10
+ class Application
12
11
 
12
+ autoload :Hooks, 'halcyon/application/hooks'
13
13
  autoload :Router, 'halcyon/application/router'
14
14
 
15
- attr_accessor :session
16
-
17
- DEFAULT_OPTIONS = {
18
- :root => Dir.pwd,
19
- :logging => {
20
- :type => 'Logger',
21
- :level => 'info'
22
- },
23
- :allow_from => :all
24
- }.to_mash
15
+ include Exceptions
16
+ include Hooks
25
17
 
26
18
  # Initializes the app:
27
19
  # * runs startup hooks
@@ -30,7 +22,7 @@ module Halcyon
30
22
  def initialize
31
23
  self.logger.info "Starting up..."
32
24
 
33
- self.hooks[:startup].call(Halcyon.config) if self.hooks[:startup]
25
+ Halcyon.hooks[:startup].each {|hook| hook.call(Halcyon.config) }
34
26
 
35
27
  # clean after ourselves and get prepared to start serving things
36
28
  self.logger.debug "Starting GC."
@@ -40,7 +32,7 @@ module Halcyon
40
32
 
41
33
  at_exit do
42
34
  self.logger.info "Shutting down #{$$}."
43
- self.hooks[:shutdown].call(Halcyon.config) if self.hooks[:shutdown]
35
+ Halcyon.hooks[:shutdown].each {|hook| hook.call(Halcyon.config) }
44
36
  self.logger.info "Done."
45
37
  end
46
38
  end
@@ -92,11 +84,51 @@ module Halcyon
92
84
  self.logger.error "#{e.message}\n\t" << e.backtrace.join("\n\t")
93
85
  end
94
86
 
87
+ # This handles various sorts of response formats.
88
+ #
89
+ # The primary format is <tt>{:status => 200, :body => ...}</tt> which
90
+ # also supports a <tt>:headers</tt> entity (also a Hash).
91
+ #
92
+ # If this format is not followed, we assume they've just returned data so
93
+ # we package it up like normal and respond (for the user).
94
+ #
95
+ # The one exception to the previous rule is if the data is in the
96
+ # standard response structure [200, {}, 'OK'] or some such, we construct
97
+ # the reply accordingly.
98
+ #
99
+ response.status = 200 # we assume 200, but when specified get updated appropriately
100
+ result = case result
101
+ when Hash
102
+ if result[:status] and result[:body]
103
+ # {:status => 200, :body => 'OK'}
104
+ # no coercion necessary
105
+ result
106
+ else
107
+ # {*}
108
+ {:status => 200, :body => result}
109
+ end
110
+ when Array
111
+ if result[0].is_a?(Integer) and result[1].is_a?(Hash) and result[2]
112
+ # [200, {}, 'OK'] format followed
113
+ {:status => result[0], :headers => result[1], :body => result[2]}
114
+ else
115
+ # [*]
116
+ {:status => 200, :body => result}
117
+ end
118
+ else
119
+ # *
120
+ {:status => 200, :body => result}
121
+ end
122
+ # set response data
123
+ headers = result.delete(:headers) || {}
95
124
  response.status = result[:status]
125
+ headers.each {|(header,val)| response[header] = val }
96
126
  response.write result.to_json
97
127
 
98
128
  timing[:finished] = Time.now
99
- timing[:total] = (((timing[:finished] - timing[:started])*1e4).round.to_f/1e4)
129
+ # the rescue is necessary if for some reason the timing is faster than
130
+ # system timing usec. this actually happens on Windows
131
+ timing[:total] = ((((timing[:finished] - timing[:started])*1e4).round.to_f/1e4) rescue 0.01)
100
132
  timing[:per_sec] = (((1.0/(timing[:total]))*1e2).round.to_f/1e2)
101
133
 
102
134
  self.logger.info "[#{response.status}] #{URI.parse(env['REQUEST_URI'] || env['PATH_INFO']).path} (#{timing[:total]}s;#{timing[:per_sec]}req/s)"
@@ -151,9 +183,34 @@ module Halcyon
151
183
  # if no errors have occured up to this point, the route should be fully
152
184
  # valid and all exceptions raised should be treated as
153
185
  # <tt>500 Internal Server Error</tt>s, which is handled by <tt>call</tt>.
154
- controller.send(action)
186
+ controller._dispatch(action)
155
187
  end
156
188
 
189
+ # def apply_filters(where, controller, action)
190
+ # self.logger.debug "Applying #{where.to_s} filters to #{controller.class.to_s}##{action.to_s}"
191
+ # if controller.filters[:all].include?(action)
192
+ # controller.filters[:all][action].select {|filter| filter[:apply] == where}.each do |filter|
193
+ # if filter[:filter_or_block].is_a?(Proc)
194
+ # filter[:filter_or_block].call
195
+ # else
196
+ # controller.send(filter[:filter_or_block])
197
+ # end
198
+ # end
199
+ # end
200
+ # if controller.filters[:only].include?(action)
201
+ # controller.filters[:only][action].select {|filter| filter[:apply] == where && filter}.each do |filter|
202
+ # controller.send(filter[:filter_or_block])
203
+ # end
204
+ # end
205
+ # controller.filters[:except].each do |(filters_not_for_action, filters)|
206
+ # unless filters_not_for_action == action
207
+ # filters.each do |filter|
208
+ # controller.send(filter[:filter_or_block])
209
+ # end
210
+ # end
211
+ # end
212
+ # end
213
+
157
214
  # Filters unacceptable requests depending on the configuration of the
158
215
  # <tt>:allow_from</tt> option.
159
216
  #
@@ -193,22 +250,8 @@ module Halcyon
193
250
  request.params.merge(env['halcyon.route'])
194
251
  end
195
252
 
196
- # See the documentation for generated apps in <tt>config/initialze/hooks.rb</tt>
197
- #
198
- def hooks
199
- self.class.hooks
200
- end
201
-
202
253
  class << self
203
254
 
204
- attr_accessor :hooks
205
-
206
- # See the documentation for generated apps in <tt>config/initialze/hooks.rb</tt>
207
- #
208
- def hooks
209
- @hooks ||= {}
210
- end
211
-
212
255
  # Defines routes for the application.
213
256
  #
214
257
  # Refer to Halcyon::Application::Router for documentation and resources.
@@ -221,23 +264,69 @@ module Halcyon
221
264
  end
222
265
  end
223
266
 
224
- # Sets the startup hook to the proc.
225
- #
226
- # Use this to initialize application-wide resources, such as database
227
- # connections.
228
- #
229
- # Use initializers where possible.
230
- #
231
- def startup &hook
232
- self.hooks[:startup] = hook
233
- end
267
+ #--
268
+ # Boot Process
269
+ #++
234
270
 
235
- # Sets the shutdown hook to the proc.
236
- #
237
- # Close any resources opened in the +startup+ hook.
271
+ # Used to keep track of whether the boot process has been run yet.
272
+ attr_accessor :booted
273
+
274
+ # Runs through the bootup process. This involves:
275
+ # * establishing configuration directives
276
+ # * loading required libraries
238
277
  #
239
- def shutdown &hook
240
- self.hooks[:shutdown] = hook
278
+ def boot(&block)
279
+ Halcyon.config ||= Halcyon::Config.new
280
+
281
+ # Set application name
282
+ Halcyon.app = Halcyon.config[:app] || Halcyon.root.split('/').last.camel_case
283
+
284
+ # Load configuration files (when available)
285
+ Dir.glob(%w(config app).map{|conf|Halcyon.paths[:config]/conf+'.{yml,yaml}'}).each do |config_file|
286
+ Halcyon.config.load_from(config_file) if File.exist?(config_file)
287
+ end
288
+
289
+ # Run configuration files (when available)
290
+ # These are unique in that they are Ruby files that we require so we
291
+ # can get rid of YAML config files and use Ruby configuration files.
292
+ Dir.glob(Halcyon.paths[:config]/'*.rb').each do |config_file|
293
+ require config_file
294
+ end
295
+
296
+ # Yield to the block to handle boot configuration (and other tasks).
297
+ Halcyon.config.use(&block) if block_given?
298
+
299
+ # Setup logger
300
+ if Halcyon.config[:logger]
301
+ Halcyon.config[:logging] = (Halcyon.config[:logging] || Halcyon::Config.defaults[:logging]).merge({
302
+ :type => Halcyon.config[:logger].class.to_s,
303
+ :logger => Halcyon.config[:logger]
304
+ })
305
+ end
306
+ Halcyon::Logging.set(Halcyon.config[:logging][:type])
307
+ Halcyon.logger = Halcyon::Logger.setup(Halcyon.config[:logging])
308
+
309
+ # Run initializers
310
+ Dir.glob(%w(requires hooks routes *).map{|init|Halcyon.paths[:init]/init+'.rb'}).each do |initializer|
311
+ self.logger.debug "Init: #{File.basename(initializer).chomp('.rb').camel_case}" if
312
+ require initializer.chomp('.rb')
313
+ end
314
+
315
+ # Setup autoloads for Controllers found in Halcyon.root/'app' (by default)
316
+ Dir.glob([Halcyon.paths[:controller]/'application.rb', Halcyon.paths[:controller]/'*.rb']).each do |controller|
317
+ self.logger.debug "Load: #{File.basename(controller).chomp('.rb').camel_case} Controller" if
318
+ require controller.chomp('.rb')
319
+ end
320
+
321
+ # Setup autoloads for Models found in Halcyon.root/'app'/'models' (by default)
322
+ Dir.glob(Halcyon.paths[:model]/'*.rb').each do |model|
323
+ self.logger.debug "Load: #{File.basename(model).chomp('.rb').camel_case} Model" if
324
+ require model.chomp('.rb')
325
+ end
326
+
327
+ # Set to loaded so additional calls to boot are ignored (unless
328
+ # forcefully loaded by ignoring this value).
329
+ self.booted = true
241
330
  end
242
331
 
243
332
  end
@@ -0,0 +1,38 @@
1
+ module Halcyon
2
+ class Application
3
+
4
+ # Helper that provides access to setting and running hooks.
5
+ #
6
+ module Hooks
7
+
8
+ # Extends the target with the class methods necessary.
9
+ def self.included(target)
10
+ target.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ # Sets the startup hook to the proc.
16
+ #
17
+ # Use this to initialize application-wide resources, such as database
18
+ # connections.
19
+ #
20
+ # Use initializers where possible.
21
+ #
22
+ def startup &hook
23
+ Halcyon.hooks[:startup] << hook
24
+ end
25
+
26
+ # Sets the shutdown hook to the proc.
27
+ #
28
+ # Close any resources opened in the +startup+ hook.
29
+ #
30
+ def shutdown &hook
31
+ Halcyon.hooks[:shutdown] << hook
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,252 @@
1
+ module Halcyon
2
+
3
+ # Application configuration map.
4
+ #
5
+ class Config
6
+
7
+ attr_accessor :config
8
+
9
+ autoload :Helpers, 'halcyon/config/helpers'
10
+ autoload :Paths, 'halcyon/config/paths'
11
+ autoload :File, 'halcyon/config/file'
12
+
13
+ # Creates an empty configuration hash (Mash) and sets up the configuration
14
+ # to whatever the settings are provided, merging over the defaults.
15
+ #
16
+ # Examples:
17
+ #
18
+ # Halcyon::Config.new(:environment => :development)
19
+ #
20
+ # OR
21
+ #
22
+ # Halcyon::Config.new(:allow_from => :all)
23
+ #
24
+ # OR
25
+ #
26
+ # Halcyon::Config.new
27
+ #
28
+ # OR
29
+ #
30
+ # Halcyon::Config.new do |c|
31
+ # c[:foo] = true
32
+ # end
33
+ #
34
+ def initialize(config={}, &block)
35
+ env = config.delete(:environment)
36
+ self.config = Mash.new
37
+ self.setup(self.defaults(env).merge(config))
38
+ self.use(&block) if block_given?
39
+ end
40
+
41
+ # Sets the configuration up with the values given.
42
+ #
43
+ def configure(config={})
44
+ config.each do |(key, val)|
45
+ self.config[key] = val
46
+ end
47
+ end
48
+
49
+ # Sets up the configuration by storing the settings provided (via param or
50
+ # via block).
51
+ #
52
+ # Usage:
53
+ #
54
+ # Halcyon.config.setup do |c|
55
+ # c[:foo] = true
56
+ # end
57
+ #
58
+ # or
59
+ #
60
+ # Halcyon.config.setup(:foo => true)
61
+ #
62
+ def setup(config={})
63
+ if block_given?
64
+ yield(self.config.dup)
65
+ end
66
+ # merge new settings
67
+ self.configure(config)
68
+ end
69
+
70
+ # Yields and returns the configuration.
71
+ #
72
+ # Examples:
73
+ #
74
+ # Halcyon.config.use do |c|
75
+ # c[:foo] = true
76
+ # end
77
+ #
78
+ def use
79
+ if block_given?
80
+ yield self.config
81
+ end
82
+ self.config
83
+ end
84
+
85
+ # Allows retrieval of single key config values and setting single config
86
+ # values.
87
+ #
88
+ # Examples:
89
+ #
90
+ # Halcyon.config.app #=> 'AppName'
91
+ # Halcyon.config[:app] #=> 'AppName'
92
+ #
93
+ def method_missing(method, *args)
94
+ if method.to_s[-1,1] == '='
95
+ self.put(method.to_s.tr('=',''), *args)
96
+ else
97
+ self.get(method)
98
+ end
99
+ end
100
+
101
+ # Get the configuration value associated with the key.
102
+ #
103
+ # Examples:
104
+ #
105
+ # Halcyon.config.get(:app) #=> 'AppName'
106
+ #
107
+ def get(key)
108
+ self.config[key]
109
+ end
110
+
111
+ # Put the configuration value associated with the key or setup with a hash.
112
+ #
113
+ # Examples:
114
+ #
115
+ # Halcyon.config.put(:app, 'AppName')
116
+ #
117
+ # OR
118
+ #
119
+ # Halcyon.config.put(:app => 'AppName')
120
+ #
121
+ def put(key_or_config_hash, value = nil)
122
+ if value.nil? and key_or_config_hash.is_a?(Hash)
123
+ self.configure(key_or_config_hash)
124
+ else
125
+ self.config[key_or_config_hash] = value
126
+ end
127
+ end
128
+
129
+ # Removes the configuration value from the hash.
130
+ #
131
+ # Examples:
132
+ #
133
+ # Halcyon.config.delete(:app) #=> 'AppName'
134
+ #
135
+ def delete(key)
136
+ self.config.delete(key)
137
+ end
138
+
139
+ # Alias for the <tt>get</tt> method.
140
+ #
141
+ # Examples:
142
+ #
143
+ # Halcyon.config[:foo] #=> true
144
+ #
145
+ def [](key)
146
+ self.get(key)
147
+ end
148
+
149
+ # Alias for the <tt>put</tt> method. (Restricted to the key/value pair.)
150
+ #
151
+ # Examples:
152
+ #
153
+ # Halcyon.config[:foo] = true
154
+ #
155
+ def []=(key, value)
156
+ self.put(key, value)
157
+ end
158
+
159
+ # Returns the configuration rendered as YAML.
160
+ #
161
+ def to_yaml
162
+ require 'yaml'
163
+ self.config.to_hash.to_yaml
164
+ end
165
+
166
+ # Returns the configuration as a hash.
167
+ #
168
+ def to_hash
169
+ self.config.to_hash
170
+ end
171
+
172
+ # Shortcut for Halcyon::Config.defaults.
173
+ #
174
+ def defaults(env = nil)
175
+ Halcyon::Config.defaults(env)
176
+ end
177
+
178
+ # Loads the contents of a configuration file found at <tt>path</tt> and
179
+ # merges it with the current configuration.
180
+ #
181
+ def load_from(path)
182
+ self.configure(Halcyon::Config::File.load(path))
183
+ self
184
+ end
185
+
186
+ def inspect
187
+ attrs = ""
188
+ self.config.keys.each {|key| attrs << " #{key}=#{self.config[key].inspect}"}
189
+ "#<Halcyon::Config#{attrs}>"
190
+ end
191
+
192
+ class << self
193
+
194
+ # Default configuration values.
195
+ #
196
+ # Defaults to the configuration for <tt>:development</tt>.
197
+ #
198
+ def defaults(env = nil)
199
+ base = {
200
+ :app => nil,
201
+ :root => Dir.pwd,
202
+ :environment => :development,
203
+ :allow_from => 'all',
204
+ :logging => {
205
+ :type => 'Logger',
206
+ :level => 'debug'
207
+ },
208
+ :paths => Paths.new,
209
+ :hooks => Hash.new([])
210
+ }
211
+ case (env || :development)
212
+ when :development
213
+ base.merge({
214
+ :environment => :development
215
+ })
216
+ when :test
217
+ base.merge({
218
+ :app => 'Specs',
219
+ :environment => :test,
220
+ :logging => {
221
+ :type => 'Logger',
222
+ :level => 'warn',
223
+ :file => 'log/test.log'
224
+ }
225
+ })
226
+ when :console
227
+ base.merge({
228
+ :environment => :console
229
+ })
230
+ when :production
231
+ base.merge({
232
+ :environment => :production,
233
+ :logging => {
234
+ :type => 'Logger',
235
+ :level => 'warn',
236
+ :file => 'log/production.log'
237
+ }
238
+ })
239
+ end
240
+ end
241
+
242
+ # Loads the contents of a configuration file found at <tt>path</tt>.
243
+ #
244
+ def load_from(path)
245
+ Halcyon::Config::File.load(path)
246
+ end
247
+
248
+ end
249
+
250
+ end
251
+
252
+ end