halcyon 0.5.3 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,7 +15,7 @@ $:.unshift File.dirname(__FILE__)
15
15
  #
16
16
  module Halcyon
17
17
 
18
- VERSION = [0,5,3] unless defined?(Halcyon::VERSION)
18
+ VERSION = [0,5,4] unless defined?(Halcyon::VERSION)
19
19
 
20
20
  autoload :Application, 'halcyon/application'
21
21
  autoload :Client, 'halcyon/client'
@@ -81,5 +81,6 @@ Object.send(:include, Halcyon::Logging::Helpers)
81
81
  module Rack
82
82
 
83
83
  autoload :JSONP, 'rack/jsonp'
84
+ autoload :PostBodyContentTypeParsers, 'rack/post_body_content_type_parsers'
84
85
 
85
86
  end
@@ -167,7 +167,7 @@ module Halcyon
167
167
  when String
168
168
  # pulled from URL, so camelize (from extlib) and symbolize first
169
169
  begin
170
- Object.const_get(route[:controller].camel_case.to_sym).new(env)
170
+ Object.full_const_get(route[:controller].to_const_string).new(env)
171
171
  rescue NameError => e
172
172
  raise NotFound.new
173
173
  end
@@ -43,8 +43,11 @@ module Halcyon
43
43
 
44
44
  USER_AGENT = "JSON/#{JSON::VERSION} Compatible (en-US) Halcyon::Client/#{Halcyon.version}".freeze
45
45
  CONTENT_TYPE = "application/x-www-form-urlencoded".freeze
46
+ ACCEPT = "application/json, */*".freeze
47
+
46
48
  DEFAULT_OPTIONS = {
47
- :raise_exceptions => false
49
+ :raise_exceptions => false,
50
+ :encode_post_body_as_json => false
48
51
  }
49
52
 
50
53
  attr_accessor :uri # The server URI
@@ -103,6 +106,7 @@ module Halcyon
103
106
  # except that it is not executed in a block.
104
107
  #
105
108
  # The differences are purely semantic and of personal taste.
109
+ #
106
110
  def initialize(uri, headers = {})
107
111
  self.uri = URI.parse(uri)
108
112
  self.headers = headers
@@ -112,21 +116,36 @@ module Halcyon
112
116
  end
113
117
  end
114
118
 
119
+ # Sets the option to raise exceptions when the response from the server is
120
+ # not a +200+ response.
121
+ #
115
122
  def raise_exceptions!(setting = true)
116
123
  self.options[:raise_exceptions] = setting
117
124
  end
118
125
 
126
+ # Sets the option to encode the POST body as +application/json+ compatible.
127
+ #
128
+ def encode_post_body_as_json!(setting = true)
129
+ if self.options[:encode_post_body_as_json] = setting
130
+ set_content_type "application/json"
131
+ else
132
+ set_content_type "application/x-www-form-urlencoded"
133
+ end
134
+ end
135
+
119
136
  #--
120
137
  # Request Handling
121
138
  #++
122
139
 
123
140
  # Performs a GET request on the URI specified.
141
+ #
124
142
  def get(uri, headers={})
125
143
  req = Net::HTTP::Get.new(uri)
126
144
  request(req, headers)
127
145
  end
128
146
 
129
147
  # Performs a POST request on the URI specified.
148
+ #
130
149
  def post(uri, data = {}, headers={})
131
150
  req = Net::HTTP::Post.new(uri)
132
151
  req.body = format_body(data)
@@ -134,19 +153,21 @@ module Halcyon
134
153
  end
135
154
 
136
155
  # Performs a DELETE request on the URI specified.
156
+ #
137
157
  def delete(uri, headers={})
138
158
  req = Net::HTTP::Delete.new(uri)
139
159
  request(req, headers)
140
160
  end
141
161
 
142
162
  # Performs a PUT request on the URI specified.
163
+ #
143
164
  def put(uri, data = {}, headers={})
144
165
  req = Net::HTTP::Put.new(uri)
145
166
  req.body = format_body(data)
146
167
  request(req, headers)
147
168
  end
148
169
 
149
- private
170
+ private
150
171
 
151
172
  # Performs an arbitrary HTTP request, receive the response, parse it with
152
173
  # JSON, and return it to the caller. This is a private method because the
@@ -160,9 +181,11 @@ module Halcyon
160
181
  # (defined in Halcyon::Exceptions) which all inherit from
161
182
  # +Halcyon::Exceptions+. It is up to the client to handle these
162
183
  # exceptions specifically.
184
+ #
163
185
  def request(req, headers={})
164
186
  # set default headers
165
187
  req["User-Agent"] = USER_AGENT
188
+ req["Accept"] = ACCEPT
166
189
  req["Content-Type"] = CONTENT_TYPE unless req.body.nil?
167
190
  req["Content-Length"] = req.body unless req.body.nil?
168
191
 
@@ -171,8 +194,10 @@ module Halcyon
171
194
  req[header] = value
172
195
  end
173
196
 
174
- # prepare and send HTTP request
175
- res = Net::HTTP.start(self.uri.host, self.uri.port) {|http|http.request(req)}
197
+ # prepare and send HTTP/S request
198
+ serv = Net::HTTP.new(self.uri.host, self.uri.port)
199
+ prepare_server(serv) if private_methods.include?('prepare_server')
200
+ res = serv.start { |http| http.request(req) }
176
201
 
177
202
  # parse response
178
203
  # unescape just in case any problematic characters were POSTed through
@@ -192,9 +217,24 @@ module Halcyon
192
217
 
193
218
  # Formats the data of a POST or PUT request (the body) into an acceptable
194
219
  # format according to Net::HTTP for sending through as a Hash.
220
+ #
195
221
  def format_body(data)
196
222
  data = {:body => data} unless data.is_a? Hash
197
- Rack::Utils.build_query(data)
223
+ case CONTENT_TYPE
224
+ when "application/x-www-form-urlencoded"
225
+ Rack::Utils.build_query(data)
226
+ when "application/json"
227
+ data.to_json
228
+ else
229
+ raise ArgumentError.new("Unsupported Content-Type for POST body: #{CONTENT_TYPE}")
230
+ end
231
+ end
232
+
233
+ # Sets the +CONTENT_TYPE+ to the appropriate type.
234
+ #
235
+ def set_content_type(content_type)
236
+ self.class.send(:remove_const, :CONTENT_TYPE)
237
+ self.class.const_set(:CONTENT_TYPE, content_type.freeze)
198
238
  end
199
239
 
200
240
  end
@@ -4,34 +4,10 @@ module Halcyon
4
4
  class Client
5
5
  private
6
6
 
7
- def request(req, headers={})
8
- # set default headers
9
- req["Content-Type"] = CONTENT_TYPE
10
- req["User-Agent"] = USER_AGENT
11
-
12
- # apply provided headers
13
- self.headers.merge(headers).each do |(header, value)|
14
- req[header] = value
15
- end
16
-
17
- # prepare and send HTTPS request
18
- serv = Net::HTTP.new(self.uri.host, self.uri.port)
7
+ # Sets the SSL-specific options for the Server.
8
+ #
9
+ def prepare_server(serv)
19
10
  serv.use_ssl = true if self.uri.scheme == 'https'
20
- res = serv.start {|http|http.request(req)}
21
-
22
- # parse response
23
- body = JSON.parse(res.body).to_mash
24
-
25
- # handle non-successes
26
- if self.options[:raise_exceptions] && !res.kind_of?(Net::HTTPSuccess)
27
- raise self.class.const_get(Exceptions::HTTP_STATUS_CODES[body[:status]].tr(' ', '_').camel_case.gsub(/( |\-)/,'')).new
28
- end
29
-
30
- # return response
31
- body
32
- rescue Halcyon::Exceptions::Base => e
33
- # log exception if logger is in place
34
- raise
35
11
  end
36
12
 
37
13
  end
@@ -31,7 +31,7 @@ module Rack
31
31
  #
32
32
  def pad(callback, response, body = "")
33
33
  response.each{ |s| body << s }
34
- "#{callback}(#{body})"
34
+ "#{callback}(#{body})"
35
35
  end
36
36
 
37
37
  end
@@ -0,0 +1,38 @@
1
+ begin
2
+ require 'json'
3
+ rescue LoadError => e
4
+ require 'json/pure'
5
+ end
6
+
7
+ module Rack
8
+
9
+ # A Rack middleware for parsing POST/PUT body data when Content-Type is
10
+ # not one of the standard supported types, like <tt>application/json</tt>.
11
+ #
12
+ class PostBodyContentTypeParsers
13
+
14
+ # Constants
15
+ #
16
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
17
+ POST_BODY = 'rack.input'.freeze
18
+ FORM_INPUT = 'rack.request.form_input'.freeze
19
+ FORM_HASH = 'rack.request.form_hash'.freeze
20
+
21
+ # Supported Content-Types
22
+ #
23
+ APPLICATION_JSON = 'application/json'.freeze
24
+
25
+ def initialize(app)
26
+ @app = app
27
+ end
28
+
29
+ def call(env)
30
+ case env[CONTENT_TYPE]
31
+ when APPLICATION_JSON
32
+ env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
33
+ end
34
+ @app.call(env)
35
+ end
36
+
37
+ end
38
+ end
@@ -61,4 +61,9 @@ describe "Halcyon::Application" do
61
61
  body['body'].should == "Internal Server Error"
62
62
  end
63
63
 
64
+ it "should dispatch to controllers inside of modules" do
65
+ response = Rack::MockRequest.new(@app).get("/nested/tests")
66
+ response.status.should == 200
67
+ end
68
+
64
69
  end
@@ -31,6 +31,21 @@ end
31
31
  # Tests
32
32
  #++
33
33
 
34
+ module Halcyon
35
+ class Client
36
+ private
37
+ def prepare_server(serv)
38
+ class << serv
39
+ alias :request_without_header_inspection :request
40
+ def request(req, body = nil, &block)
41
+ $accept = req["Accept"]
42
+ request_without_header_inspection(req, body, &block)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
34
49
  describe "Halcyon::Client" do
35
50
 
36
51
  before do
@@ -55,7 +70,7 @@ describe "Halcyon::Client" do
55
70
  @client.get('/nonexistent/route')[:status].should == 404
56
71
 
57
72
  # tell it to raise exceptions
58
- @client.raise_exceptions! true
73
+ @client.raise_exceptions!
59
74
  should.raise(Halcyon::Exceptions::NotFound) { @client.get('/nonexistent/route') }
60
75
  @client.get('/time')[:status].should == 200
61
76
  end
@@ -80,4 +95,30 @@ describe "Halcyon::Client" do
80
95
  response[:body].should == {'controller' => 'application', 'action' => 'returner', 'key' => "%todd"}
81
96
  end
82
97
 
98
+ it "should render the POST body with the correct content type, allowing application/json is set" do
99
+ body = {:key => "value"}
100
+
101
+ # default behavior is to set the POST body to application/x-www-form-urlencoded
102
+ @client.send(:format_body, body) == "key=value"
103
+ @client.post('/returner', body)[:body][:key].should == "value"
104
+
105
+ # tell it to send as application/json
106
+ @client.encode_post_body_as_json!
107
+ @client.send(:format_body, body).should == body.to_json
108
+ @client.post('/returner', body)[:body][:key].should == nil
109
+ # The server will not return the values from the POST body because it is
110
+ # not set to parse application/json values. Like your own apps, you must
111
+ # set it manually to accept this type of body encoding.
112
+
113
+ # set it back to ensure that the change is reversed
114
+ @client.encode_post_body_as_json! false
115
+ @client.send(:format_body, body) == "key=value"
116
+ @client.post('/returner', body)[:body][:key].should == "value"
117
+ end
118
+
119
+ it "should set the Accept header to the appropriate type" do
120
+ @client.get('/returner')[:status].should == 200
121
+ $accept.should == Halcyon::Client::ACCEPT
122
+ end
123
+
83
124
  end
@@ -0,0 +1,31 @@
1
+ app = lambda do |env|
2
+ [200, {'Content-Type' => 'text/plain'}, {'bar' => 'foo'}.to_json]
3
+ end
4
+
5
+ jsonp_app = Rack::Builder.new do
6
+ use Rack::JSONP
7
+ run app
8
+ end
9
+
10
+ describe "Rack::JSONP" do
11
+
12
+ before do
13
+ @log = ''
14
+ @logger = Logger.new(StringIO.new(@log))
15
+ Halcyon.config.use do |c|
16
+ c[:logger] = @logger
17
+ end
18
+ @app = Halcyon::Runner.new
19
+ end
20
+
21
+ it "should wrap the response body in the Javascript callback when provided" do
22
+ body = jsonp_app.call(Rack::MockRequest.env_for("/", :input => "foo=bar&callback=foo")).last
23
+ body.should == 'foo({"bar":"foo"})'
24
+ end
25
+
26
+ it "should not change anything if no :callback param is provided" do
27
+ body = app.call(Rack::MockRequest.env_for("/", :input => "foo=bar")).last
28
+ body.should == {'bar' => 'foo'}.to_json
29
+ end
30
+
31
+ end
@@ -0,0 +1,37 @@
1
+ app = lambda do |env|
2
+ request = Rack::Request.new(env)
3
+ [200, {'Content-Type' => 'text/plain'}, request.POST]
4
+ end
5
+
6
+ def env_for_post_with_headers(path, headers, body)
7
+ Rack::MockRequest.env_for(path, {:method => "POST", :input => body}.merge(headers))
8
+ end
9
+
10
+ describe "Rack::PostBodyContentTypeParsers" do
11
+
12
+ before do
13
+ @log = ''
14
+ @logger = Logger.new(StringIO.new(@log))
15
+ Halcyon.config.use do |c|
16
+ c[:logger] = @logger
17
+ end
18
+ @app = Halcyon::Runner.new
19
+ end
20
+
21
+ it "should handle requests with POST body Content-Type of application/json" do
22
+ parser = Rack::PostBodyContentTypeParsers.new(app)
23
+ env = env_for_post_with_headers('/', {'Content_Type'.upcase => 'application/json'}, {:body => "asdf", :status => "12"}.to_json)
24
+
25
+ response_body = parser.call(env).last
26
+
27
+ response_body['body'].should == "asdf"
28
+ response_body['status'].should == "12"
29
+ end
30
+
31
+ it "should change nothing when the POST body content type isn't application/json" do
32
+ response_body = app.call(Rack::MockRequest.env_for("/", :input => "body=asdf&status=12")).last
33
+ response_body['body'].should == "asdf"
34
+ response_body['status'].should == "12"
35
+ end
36
+
37
+ end
@@ -85,6 +85,17 @@ class Resources < Application
85
85
  end
86
86
  end
87
87
 
88
+ # Nested controller
89
+ module Nested
90
+ class Tests < Application
91
+
92
+ def index
93
+ ok
94
+ end
95
+
96
+ end
97
+ end
98
+
88
99
  # Models
89
100
 
90
101
  class Model
@@ -100,6 +111,7 @@ Halcyon.configurable_attr(:environment)
100
111
  Halcyon::Application.route do |r|
101
112
  r.resources :resources
102
113
 
114
+ r.match('/nested/tests').to(:controller => 'nested/tests', :action => 'index')
103
115
  r.match('/hello/:name').to(:controller => 'specs', :action => 'greeter')
104
116
  r.match('/:action').to(:controller => 'specs')
105
117
  r.match('/:controller/:action').to()
@@ -5,4 +5,9 @@ $:.unshift(Halcyon.root/'lib')
5
5
  puts "(Starting in #{Halcyon.root})"
6
6
 
7
7
  Thin::Logging.silent = true if defined? Thin
8
+
9
+ # Uncomment if you plan to allow clients to send requests with the POST body
10
+ # Content-Type as application/json.
11
+ # use Rack::PostBodyContentTypeParsers
12
+
8
13
  run Halcyon::Runner.new
@@ -5,4 +5,8 @@ puts "(Starting in #{Halcyon.root})"
5
5
 
6
6
  Thin::Logging.silent = true if defined? Thin
7
7
 
8
+ # Uncomment if you plan to allow clients to send requests with the POST body
9
+ # Content-Type as application/json.
10
+ # use Rack::PostBodyContentTypeParsers
11
+
8
12
  run Halcyon::Runner.new
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: halcyon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Todd
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-08-28 00:00:00 -04:00
12
+ date: 2008-09-19 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -85,6 +85,9 @@ files:
85
85
  - spec/halcyon/logging_spec.rb
86
86
  - spec/halcyon/router_spec.rb
87
87
  - spec/halcyon/runner_spec.rb
88
+ - spec/rack
89
+ - spec/rack/jsonp.rb
90
+ - spec/rack/post_body_content_type_parsers_spec.rb
88
91
  - spec/spec_helper.rb
89
92
  - lib/halcyon
90
93
  - lib/halcyon/application
@@ -117,6 +120,7 @@ files:
117
120
  - lib/halcyon.rb
118
121
  - lib/rack
119
122
  - lib/rack/jsonp.rb
123
+ - lib/rack/post_body_content_type_parsers.rb
120
124
  - support/generators
121
125
  - support/generators/halcyon
122
126
  - support/generators/halcyon/halcyon_generator.rb