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.
@@ -95,8 +95,9 @@ module Halcyon
95
95
  status, body = http_status
96
96
  class_eval <<-"end;"
97
97
  class #{body.gsub(/( |\-)/,'')} < Halcyon::Exceptions::Base
98
- def initialize(s=#{status}, b='#{body}')
99
- super
98
+ def initialize(body=nil)
99
+ body = '#{body}' if body.nil?
100
+ super(#{status}, body)
100
101
  end
101
102
  end
102
103
  end;
@@ -4,6 +4,7 @@ module Halcyon
4
4
  # * setting up the logger
5
5
  # * loading initializers
6
6
  # * loading controllers
7
+ # All of which is done by the call to <tt>Halcyon::Application.boot</tt>.
7
8
  #
8
9
  # The Runner is a full-fledged Rack application, and accepts calls to #call.
9
10
  #
@@ -14,70 +15,14 @@ module Halcyon
14
15
  # Halcyon::Runner.run!(['start', '-p', '4647'])
15
16
  #
16
17
  # # load the config file and initialize the app
17
- # Halcyon::Runner.load_config Halcyon.root/'config'/'config.yml'
18
18
  # Halcyon::Runner.new
19
19
  class Runner
20
20
 
21
21
  autoload :Commands, 'halcyon/runner/commands'
22
22
 
23
- class << self
24
-
25
- # Runs commands from the CLI.
26
- # +argv+ the arguments to pass to the commands
27
- #
28
- # Returns nothing
29
- def run!(argv=ARGV)
30
- Commands.send(argv.shift, argv)
31
- end
32
-
33
- # Returns the path to the configuration file specified, defaulting
34
- # to the path for the <tt>config.yml</tt> file.
35
- # +file+ the name of the config file path (without the <tt>.yml</tt>
36
- # extension)
37
- def config_path(file = "config")
38
- Halcyon.paths[:config]/"#{file}.yml"
39
- end
40
-
41
- end
42
-
43
23
  # Initializes the application and application resources.
44
- def initialize
45
- Halcyon::Runner.load_paths if Halcyon.paths.nil?
46
-
47
- # Load the configuration if none is set already
48
- if Halcyon.config.nil?
49
- if File.exist?(Halcyon::Runner.config_path)
50
- Halcyon.config = Halcyon::Runner.load_config
51
- else
52
- Halcon.config = Halcyon::Application::DEFAULT_OPTIONS
53
- end
54
- end
55
-
56
- # Set application name
57
- Halcyon.app = Halcyon.config[:app] || Halcyon.root.split('/').last.camel_case
58
-
59
- # Setup logger
60
- if Halcyon.config[:logger]
61
- Halcyon.config[:logging] = (Halcyon.config[:logging] || Halcyon::Application::DEFAULT_OPTIONS[:logging]).merge({
62
- :type => Halcyon.config[:logger].class.to_s,
63
- :logger => Halcyon.config[:logger]
64
- })
65
- end
66
- Halcyon::Logging.set((Halcyon.config[:logging][:type] rescue nil))
67
- Halcyon.logger = Halcyon::Logger.setup(Halcyon.config[:logging])
68
-
69
- # Run initializers
70
- Dir.glob(Halcyon.paths[:init]/'{requires,hooks,routes,environment,*}.rb').each do |initializer|
71
- self.logger.debug "Init: #{File.basename(initializer).chomp('.rb').camel_case}" if
72
- require initializer.chomp('.rb')
73
- end
74
-
75
- # Setup autoloads for Controllers found in Halcyon.root/'app'
76
- Dir.glob(Halcyon.paths[:controller]/'{application,*}.rb').each do |controller|
77
- self.logger.debug "Load: #{File.basename(controller).chomp('.rb').camel_case} Controller" if
78
- require controller.chomp('.rb')
79
- end
80
-
24
+ def initialize(&block)
25
+ Halcyon::Application.boot(&block) unless Halcyon::Application.booted
81
26
  @app = Halcyon::Application.new
82
27
  end
83
28
 
@@ -91,48 +36,12 @@ module Halcyon
91
36
 
92
37
  class << self
93
38
 
94
- # Loads the configuration file specified into <tt>Halcyon.config</tt>.
95
- # +file+ the configuration file to load
96
- #
97
- # Examples
98
- # Halcyon::Runner.load_config Halcyon.root/'config'/'config.yml'
99
- # Halcyon.config #=> {:allow_from => :all, :logging => {...}, ...}.to_mash
100
- #
101
- # Returns {Symbol:key => String:value}.to_mash
102
- def load_config(file=Halcyon::Runner.config_path)
103
- if File.exist?(file)
104
- require 'yaml'
105
-
106
- # load the config file
107
- begin
108
- config = YAML.load_file(file).to_mash
109
- rescue Errno::EACCES
110
- raise LoadError.new("Can't access #{file}, try 'sudo #{$0}'")
111
- end
112
- else
113
- warn "#{file} not found, ensure the path to this file is correct. Ignoring."
114
- nil
115
- end
116
- end
117
-
118
- # Set the paths for resources to be located.
119
- #
120
- # Used internally for setting the load paths if not manually overridden
121
- # and needed to be set before normal application initialization.
122
- #
123
- # TODO: Move this to the planned <tt>Halcyon::Config</tt> object.
39
+ # Runs commands from the CLI.
40
+ # +argv+ the arguments to pass to the commands
124
41
  #
125
- # Returns nothing.
126
- def load_paths
127
- # Set the default application paths, not overwriting manually set paths
128
- Halcyon.paths = {
129
- :controller => Halcyon.root/'app',
130
- :model => Halcyon.root/'app'/'models',
131
- :lib => Halcyon.root/'lib',
132
- :config => Halcyon.root/'config',
133
- :init => Halcyon.root/'config'/'{init,initialize}',
134
- :log => Halcyon.root/'log'
135
- }.to_mash.merge(Halcyon.paths || {})
42
+ # Returns nothing
43
+ def run!(argv=ARGV)
44
+ Commands.send(argv.shift, argv)
136
45
  end
137
46
 
138
47
  end
@@ -63,10 +63,9 @@ module Halcyon
63
63
  # Set up the application
64
64
  Object.instance_eval do
65
65
  $log = ''
66
- Halcyon::Runner.load_paths if Halcyon.paths.nil?
67
- (Halcyon.config = Halcyon::Runner.load_config) || require(Halcyon.root/'app')
68
- Halcyon.config[:logger] = Logger.new(StringIO.new($log))
69
- $app = Halcyon::Runner.new
66
+ $app = Halcyon::Runner.new do |c|
67
+ c[:logger] = Logger.new(StringIO.new($log))
68
+ end
70
69
  $response = nil
71
70
  end
72
71
 
data/lib/rack/jsonp.rb ADDED
@@ -0,0 +1,38 @@
1
+ module Rack
2
+
3
+ # A Rack middleware for providing JSON-P support.
4
+ #
5
+ # Full credit to Flinn Mueller (http://actsasflinn.com/) for this contribution.
6
+ #
7
+ class JSONP
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ # Proxies the request to the application, stripping out the JSON-P callback
14
+ # method and padding the response with the appropriate callback format.
15
+ #
16
+ # Changes nothing if no <tt>callback</tt> param is specified.
17
+ #
18
+ def call(env)
19
+ status, headers, response = @app.call(env)
20
+ request = Rack::Request.new(env)
21
+ response = pad(request.params.delete('callback'), response) if request.params.include?('callback')
22
+ [status, headers, response]
23
+ end
24
+
25
+ # Pads the response with the appropriate callback format according to the
26
+ # JSON-P spec/requirements.
27
+ #
28
+ # The Rack response spec indicates that it should be enumerable. The method
29
+ # of combining all of the data into a sinle string makes sense since JSON
30
+ # is returned as a full string.
31
+ #
32
+ def pad(callback, response, body = "")
33
+ response.each{ |s| body << s }
34
+ "#{callback}(#{body})"
35
+ end
36
+
37
+ end
38
+ end
@@ -3,10 +3,9 @@ describe "Halcyon::Application" do
3
3
  before do
4
4
  @log = ''
5
5
  @logger = Logger.new(StringIO.new(@log))
6
- @config = $config.dup
7
- @config[:logger] = @logger
8
- @config[:app] = 'Specs'
9
- Halcyon.config = @config
6
+ Halcyon.config.use do |c|
7
+ c[:logger] = @logger
8
+ end
10
9
  @app = Halcyon::Runner.new
11
10
  end
12
11
 
@@ -17,7 +16,7 @@ describe "Halcyon::Application" do
17
16
 
18
17
  it "should dispatch methods according to their respective routes" do
19
18
  Rack::MockRequest.new(@app).get("/hello/Matt")
20
- @log.should =~ / INFO \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \(\d+\) Specs :: \[200\] \/hello\/Matt \(.+\)\n/
19
+ $hello.should == "Matt"
21
20
  end
22
21
 
23
22
  it "should handle requests and respond with JSON" do
@@ -26,13 +25,6 @@ describe "Halcyon::Application" do
26
25
  body['body'].should == "Found"
27
26
  end
28
27
 
29
- it "should handle requests with param values in the URL" do
30
- body = JSON.parse(Rack::MockRequest.new(@app).get("/hello/Matt?test=value").body)
31
- body['status'].should == 200
32
- body['body'].should == "Hello Matt"
33
- @log.split("\n").last.should =~ /"test"=>"value"/
34
- end
35
-
36
28
  it "should not dispatch private methods" do
37
29
  body = JSON.parse(Rack::MockRequest.new(@app).get("/specs/undispatchable_private_method").body)
38
30
  body['status'].should == 404
@@ -47,12 +39,14 @@ describe "Halcyon::Application" do
47
39
 
48
40
  it "should log activity" do
49
41
  Halcyon.logger.is_a?(Logger).should.be.true?
50
- Rack::MockRequest.new(@app).get("/lolcats/r/cute")
51
- @log.should =~ / INFO \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \(\d+\) Specs :: \[404\] \/lolcats\/r\/cute \(.+\)\n/
42
+ # Rack::MockRequest.new(@app).get("/lolcats/r/cute")
43
+ # @log.should =~ / INFO \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \(\d+\) Specs :: \[404\] \/lolcats\/r\/cute \(.+\)\n/
44
+ # Halcyon.logger.debug "Testing"
45
+ # @log.should =~ /Testing/
52
46
  end
53
47
 
54
48
  it "should allow all requests by default" do
55
- Halcyon.config[:allow_from].should == :all
49
+ Halcyon.config[:allow_from].to_sym.should == :all
56
50
  end
57
51
 
58
52
  it "should handle exceptions gracefully" do
@@ -0,0 +1,157 @@
1
+ describe "Halcyon::Config" do
2
+
3
+ before do
4
+ Halcyon.config = Halcyon::Config.new(:environment => :test)
5
+ # @log = ''
6
+ # @logger = Logger.new(StringIO.new(@log))
7
+ # @config = $config.dup
8
+ # @config[:logger] = @logger
9
+ # @config[:app] = 'Specs'
10
+ # Halcyon.config = @config
11
+ # @app = Halcyon::Runner.new
12
+ end
13
+
14
+ it "should provide the path of the application root directory" do
15
+ Halcyon.root.should == Dir.pwd
16
+ Halcyon.config[:root] = Dir.pwd
17
+ Halcyon.root.should == Halcyon.config[:root]
18
+ end
19
+
20
+ it "should provide numerous ways to retrieve configuration values" do
21
+ Halcyon.config[:foo] = :bar
22
+
23
+ Halcyon.config[:foo].should == :bar
24
+ Halcyon.config.foo.should == :bar
25
+ Halcyon.config.get(:foo).should == :bar
26
+ Halcyon.config.use {|c| c[:foo].should == :bar }
27
+ end
28
+
29
+ it "should provide numerous ways to set configuration values" do
30
+ Halcyon.config[:foo] = :a; Halcyon.config[:foo].should == :a
31
+ Halcyon.config.foo = :b; Halcyon.config[:foo].should == :b
32
+ Halcyon.config.put(:foo, :c); Halcyon.config[:foo].should == :c
33
+ Halcyon.config.put(:foo => :d); Halcyon.config[:foo].should == :d
34
+ Halcyon.config.use {|c| c[:foo] = :e }; Halcyon.config[:foo].should == :e
35
+ end
36
+
37
+ it "should access to major config values through Halcyon.<attr> accessors" do
38
+ Halcyon.config.use do |c|
39
+ c[:app] = "Specr"
40
+ c[:root] = Dir.pwd
41
+ c[:paths] = Halcyon::Config::Paths.new
42
+ c[:db] = Mash.new(:test => Mash.new)
43
+ c[:environment] = :test
44
+ end
45
+
46
+ Halcyon.app.should == Halcyon.config[:app]
47
+ Halcyon.root.should == (Halcyon.config[:root] || Dir.pwd)
48
+ Halcyon.paths.should == Halcyon.config[:paths]
49
+ Halcyon.db.should == Halcyon.config[:db]
50
+ Halcyon.environment.should == Halcyon.config[:environment]
51
+ end
52
+
53
+ it "should provide custom Halcyon.<attr> accessors interfacing config values" do
54
+ Halcyon.configurable_attr(:foo)
55
+ Halcyon.foo = true
56
+ Halcyon.foo.should == Halcyon.config[:foo]
57
+
58
+ Halcyon.configurable_reader(:bar) do
59
+ Halcyon.config[:bar].to_sym
60
+ end
61
+ Halcyon.config[:bar] = "foo"
62
+ Halcyon.bar.should == :foo
63
+
64
+ Halcyon.configurable_reader(:baz, "Halcyon.config[%s].to_sym")
65
+ Halcyon.config[:baz] = "bar"
66
+ Halcyon.baz.should == :bar
67
+
68
+ Halcyon.configurable_writer(:bing) do |value|
69
+ Halcyon.config[:bing] = value.to_sym
70
+ end
71
+ Halcyon.bing = "foo"
72
+ Halcyon.config[:bing].should == :foo
73
+
74
+ Halcyon.configurable_writer(:bong, "Halcyon.config[%s] = value.to_sym")
75
+ Halcyon.bong = "bar"
76
+ Halcyon.config[:bong].should == :bar
77
+ end
78
+
79
+ # it "should ..." do
80
+ # #
81
+ # end
82
+
83
+ end
84
+
85
+ describe "Halcyon::Config::Paths" do
86
+
87
+ before do
88
+ @paths = Halcyon::Config::Paths.new
89
+ end
90
+
91
+ it "should be able to look up paths by name" do
92
+ @paths.for(:config).should == Halcyon.root/'config'
93
+ end
94
+
95
+ it "should be able to define new paths" do
96
+ @paths.define(:foo => Halcyon.root/'bar', :bar => Halcyon.root/'foo')
97
+ @paths.for(:foo).should == Halcyon.root/'bar'
98
+ @paths.for(:bar).should == Halcyon.root/'foo'
99
+ end
100
+
101
+ it "should be able to define new paths by name" do
102
+ @paths.define(:foo, Halcyon.root/'baz')
103
+ @paths.for(:foo).should == Halcyon.root/'baz'
104
+ end
105
+
106
+ it "should provide a shortcut to look up paths by name" do
107
+ @paths[:config].should == @paths.for(:config)
108
+ end
109
+
110
+ it "should provide a shortcut to define new paths by name" do
111
+ @paths[:config] = Halcyon.root/'gifnoc'
112
+ @paths.for(:config).should == Halcyon.root/'gifnoc'
113
+ end
114
+
115
+ it "should raise an ArgumentError if a nonexistent path is queried for" do
116
+ should.raise(ArgumentError) { @paths.for(:nonexistent_path) }
117
+ end
118
+
119
+ end
120
+
121
+ require 'tmpdir'
122
+ require 'yaml'
123
+ describe "Halcyon::Config::File" do
124
+
125
+ before do
126
+ @config = {
127
+ :allow_from => "all",
128
+ :logging => {
129
+ :type => "Logger",
130
+ :level => "debug"
131
+ },
132
+ :root => Dir.pwd,
133
+ :app => "Specr",
134
+ :environment => "development"
135
+ }.to_mash
136
+ File.open(Dir.tmpdir/'config.yaml', 'w+'){|f| f << @config.to_yaml }
137
+ File.open(Dir.tmpdir/'config.json', 'w+'){|f| f << @config.to_json }
138
+ end
139
+
140
+ it "should load the configuration from the YAML config file" do
141
+ Halcyon::Config::File.new(Dir.tmpdir/'config.yaml').to_hash.should == @config
142
+ end
143
+
144
+ it "should load the configuration from the JSON config file" do
145
+ Halcyon::Config::File.new(Dir.tmpdir/'config.json').to_hash(:from_json).should == @config
146
+ end
147
+
148
+ it "should provide shortcuts for loading configuration files" do
149
+ Halcyon::Config::File.load(Dir.tmpdir/'config.yaml').should == @config
150
+ Halcyon::Config::File.load_from_json(Dir.tmpdir/'config.json')
151
+ end
152
+
153
+ it "should throw an ArgumentError when the config file doesn't exist" do
154
+ should.raise(ArgumentError) { Halcyon::Config::File.load('/path/to/nonexistent/file.yalm') }
155
+ end
156
+
157
+ end
@@ -3,22 +3,39 @@ describe "Halcyon::Controller" do
3
3
  before do
4
4
  @log = ''
5
5
  @logger = Logger.new(StringIO.new(@log))
6
- @config = $config.dup
7
- @config[:logger] = @logger
8
- @config[:app] = 'Specs'
9
- Halcyon.config = @config
6
+ Halcyon.config.use do |c|
7
+ c[:logger] = @logger
8
+ end
10
9
  @app = Halcyon::Runner.new
11
10
  end
12
11
 
13
12
  it "should provide various shorthand methods for simple responses but take custom response values" do
14
13
  controller = Specs.new(Rack::MockRequest.env_for('/'))
15
14
 
16
- response = {:status => 200, :body => 'OK'}
15
+ response = {:status => 200, :body => 'OK', :headers => {}}
17
16
  controller.ok.should == response
18
17
  controller.success.should == response
19
18
 
20
- controller.ok('').should == {:status => 200, :body => ''}
21
- controller.ok(['OK', 'Sure Thang', 'Correcto']).should == {:status => 200, :body => ['OK', 'Sure Thang', 'Correcto']}
19
+ controller.ok('').should == {:status => 200, :body => '', :headers => {}}
20
+ controller.ok(['OK', 'Sure Thang', 'Correcto']).should == {:status => 200, :body => ['OK', 'Sure Thang', 'Correcto'], :headers => {}}
21
+
22
+ headers = {'Date' => Time.now.strftime("%a, %d %h %Y %H:%I:%S %Z"), 'Test' => 'FooBar'}
23
+ controller.ok('OK', headers).should == {:status => 200, :body => 'OK', :headers => headers}
24
+ end
25
+
26
+ it "should provide extensive responders" do
27
+ controller = Specs.new(Rack::MockRequest.env_for('/'))
28
+
29
+ should.raise(Halcyon::Exceptions::UnprocessableEntity) { controller.status(:unprocessable_entity) }
30
+ response = Rack::MockRequest.new(@app).get("/specs/unprocessable_entity_test")
31
+ response.body.should =~ %r{Unprocessable Entity}
32
+
33
+ should.raise(Halcyon::Exceptions::UnprocessableEntity) { controller.status(:unprocessable_entity) }
34
+ end
35
+
36
+ it "should indicate service is unavailable if an status specified is not found" do
37
+ controller = Specs.new(Rack::MockRequest.env_for('/'))
38
+ should.raise(Halcyon::Exceptions::ServiceUnavailable) { controller.status(:this_state_does_not_exist) }
22
39
  end
23
40
 
24
41
  it "should provide a quick way to find out what method the request was performed using" do
@@ -52,17 +69,59 @@ describe "Halcyon::Controller" do
52
69
  controller = Specs.new(Rack::MockRequest.env_for(""))
53
70
  controller.uri.should == '/'
54
71
  end
55
-
56
- it 'should provide url accessor for resource index route' do
72
+
73
+ it "should provide url accessor for resource index route" do
57
74
  controller = Resources.new(Rack::MockRequest.env_for("/resources"))
58
75
  controller.uri.should == controller.url(:resources)
59
76
  end
60
-
61
- it 'should provide url accessor for resource show route' do
77
+
78
+ it "should provide url accessor for resource show route" do
62
79
  resource = Model.new
63
80
  resource.id = 1
64
81
  controller = Resources.new(Rack::MockRequest.env_for("/resources/1"))
65
82
  controller.uri.should == controller.url(:resource, resource)
66
83
  end
67
84
 
85
+ it "should accept response headers" do
86
+ controller = Specs.new(Rack::MockRequest.env_for(""))
87
+ headers = {'Date' => Time.now.strftime("%a, %d %h %Y %H:%I:%S %Z"), 'Foo' => 'Bar'}
88
+ controller.ok('OK', headers).should == {:status => 200, :body => 'OK', :headers => headers}
89
+ controller.ok('OK').should == {:status => 200, :body => 'OK', :headers => {}}
90
+
91
+ response = Rack::MockRequest.new(@app).get("/goob")
92
+ response['Date'].should == Time.now.strftime("%a, %d %h %Y %H:%I:%S %Z")
93
+ response['Content-Language'].should == 'en'
94
+ end
95
+
96
+ it "should handle return values from actions not from the response helpers" do
97
+ {
98
+ 'OK' => {'status' => 200, 'body' => 'OK'},
99
+ [200, {}, 'OK'] => {'status' => 200, 'body' => 'OK'},
100
+ [1, 2, 3] => {'status' => 200, 'body' => [1, 2, 3]},
101
+ {'foo' => 'bar'} => {'status' => 200, 'body' => {'foo' => 'bar'}}
102
+ }.each do |(value, expected)|
103
+ $return_value_for_gaff = value
104
+ response = JSON.parse(Rack::MockRequest.new(@app).get("/gaff").body).should == expected
105
+ end
106
+ end
107
+
108
+ it "should run filters before or after actions" do
109
+ response = Rack::MockRequest.new(@app).get("/index")
110
+ response.body.should =~ %r{Found}
111
+
112
+ # The Accepted exception is raised if +cause_exception_in_filter+ is set
113
+ response = Rack::MockRequest.new(@app).get("/index?cause_exception_in_filter=true")
114
+ response.body.should =~ %r{Accepted}
115
+
116
+ response = Rack::MockRequest.new(@app).get("/foobar")
117
+ response.body.should =~ %r{fubr}
118
+
119
+ # The Created exception is raised if +cause_exception_in_filter+ is set
120
+ response = Rack::MockRequest.new(@app).get("/foobar?cause_exception_in_filter=true")
121
+ response.body.should =~ %r{Created}
122
+
123
+ response = Rack::MockRequest.new(@app).get("/hello/Matt?cause_exception_in_filter_block=true")
124
+ response.body.should =~ %r{Not Found}
125
+ end
126
+
68
127
  end