halcyon 0.5.3 → 0.5.4

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.
@@ -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