halcyon 0.5.0 → 0.5.1

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