Skip to content

Commit

Permalink
Set it up to have simpler expansion to support SSL etc. Made the Halc…
Browse files Browse the repository at this point in the history
…yon::Client support having the Content-TType specified for the client and formatting the body appropriately. Also expanded specs. [#61 state:resolved]
  • Loading branch information
mtodd committed Sep 12, 2008
1 parent 6d6fa12 commit 238909f
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 33 deletions.
47 changes: 42 additions & 5 deletions lib/halcyon/client.rb
Expand Up @@ -44,7 +44,8 @@ class Client
USER_AGENT = "JSON/#{JSON::VERSION} Compatible (en-US) Halcyon::Client/#{Halcyon.version}".freeze
CONTENT_TYPE = "application/x-www-form-urlencoded".freeze
DEFAULT_OPTIONS = {
:raise_exceptions => false
:raise_exceptions => false,
:encode_post_body_as_json => false
}

attr_accessor :uri # The server URI
Expand Down Expand Up @@ -103,6 +104,7 @@ class Client
# except that it is not executed in a block.
#
# The differences are purely semantic and of personal taste.
#
def initialize(uri, headers = {})
self.uri = URI.parse(uri)
self.headers = headers
Expand All @@ -112,41 +114,58 @@ def initialize(uri, headers = {})
end
end

# Sets the option to raise exceptions when the response from the server is
# not a +200+ response.
#
def raise_exceptions!(setting = true)
self.options[:raise_exceptions] = setting
end

# Sets the option to encode the POST body as +application/json+ compatible.
#
def encode_post_body_as_json!(setting = true)
if self.options[:encode_post_body_as_json] = setting
set_content_type "application/json"
else
set_content_type "application/x-www-form-urlencoded"
end
end

#--
# Request Handling
#++

# Performs a GET request on the URI specified.
#
def get(uri, headers={})
req = Net::HTTP::Get.new(uri)
request(req, headers)
end

# Performs a POST request on the URI specified.
#
def post(uri, data = {}, headers={})
req = Net::HTTP::Post.new(uri)
req.body = format_body(data)
request(req, headers)
end

# Performs a DELETE request on the URI specified.
#
def delete(uri, headers={})
req = Net::HTTP::Delete.new(uri)
request(req, headers)
end

# Performs a PUT request on the URI specified.
#
def put(uri, data = {}, headers={})
req = Net::HTTP::Put.new(uri)
req.body = format_body(data)
request(req, headers)
end

private
private

# Performs an arbitrary HTTP request, receive the response, parse it with
# JSON, and return it to the caller. This is a private method because the
Expand All @@ -160,6 +179,7 @@ def put(uri, data = {}, headers={})
# (defined in Halcyon::Exceptions) which all inherit from
# +Halcyon::Exceptions+. It is up to the client to handle these
# exceptions specifically.
#
def request(req, headers={})
# set default headers
req["User-Agent"] = USER_AGENT
Expand All @@ -171,8 +191,10 @@ def request(req, headers={})
req[header] = value
end

# prepare and send HTTP request
res = Net::HTTP.start(self.uri.host, self.uri.port) {|http|http.request(req)}
# prepare and send HTTP/S request
serv = Net::HTTP.new(self.uri.host, self.uri.port)
prepare_server(serv) if private_methods.include?('prepare_server')
res = serv.start { |http| http.request(req) }

# parse response
# unescape just in case any problematic characters were POSTed through
Expand All @@ -192,9 +214,24 @@ def request(req, headers={})

# Formats the data of a POST or PUT request (the body) into an acceptable
# format according to Net::HTTP for sending through as a Hash.
#
def format_body(data)
data = {:body => data} unless data.is_a? Hash
Rack::Utils.build_query(data)
case CONTENT_TYPE
when "application/x-www-form-urlencoded"
Rack::Utils.build_query(data)
when "application/json"
data.to_json
else
raise ArgumentError.new("Unsupported Content-Type for POST body: #{CONTENT_TYPE}")
end
end

# Sets the +CONTENT_TYPE+ to the appropriate type.
#
def set_content_type(content_type)
self.class.send(:remove_const, :CONTENT_TYPE)
self.class.const_set(:CONTENT_TYPE, content_type.freeze)
end

end
Expand Down
30 changes: 3 additions & 27 deletions lib/halcyon/client/ssl.rb
Expand Up @@ -4,34 +4,10 @@ module Halcyon
class Client
private

def request(req, headers={})
# set default headers
req["Content-Type"] = CONTENT_TYPE
req["User-Agent"] = USER_AGENT

# apply provided headers
self.headers.merge(headers).each do |(header, value)|
req[header] = value
end

# prepare and send HTTPS request
serv = Net::HTTP.new(self.uri.host, self.uri.port)
# Sets the SSL-specific options for the Server.
#
def prepare_server(serv)
serv.use_ssl = true if self.uri.scheme == 'https'
res = serv.start {|http|http.request(req)}

# parse response
body = JSON.parse(res.body).to_mash

# handle non-successes
if self.options[:raise_exceptions] && !res.kind_of?(Net::HTTPSuccess)
raise self.class.const_get(Exceptions::HTTP_STATUS_CODES[body[:status]].tr(' ', '_').camel_case.gsub(/( |\-)/,'')).new
end

# return response
body
rescue Halcyon::Exceptions::Base => e
# log exception if logger is in place
raise
end

end
Expand Down
23 changes: 22 additions & 1 deletion spec/halcyon/client_spec.rb
Expand Up @@ -55,7 +55,7 @@
@client.get('/nonexistent/route')[:status].should == 404

# tell it to raise exceptions
@client.raise_exceptions! true
@client.raise_exceptions!
should.raise(Halcyon::Exceptions::NotFound) { @client.get('/nonexistent/route') }
@client.get('/time')[:status].should == 200
end
Expand All @@ -80,4 +80,25 @@
response[:body].should == {'controller' => 'application', 'action' => 'returner', 'key' => "%todd"}
end

it "should render the POST body with the correct content type, allowing application/json is set" do
body = {:key => "value"}

# default behavior is to set the POST body to application/x-www-form-urlencoded
@client.send(:format_body, body) == "key=value"
@client.post('/returner', body)[:body][:key].should == "value"

# tell it to send as application/json
@client.encode_post_body_as_json!
@client.send(:format_body, body).should == body.to_json
@client.post('/returner', body)[:body][:key].should == nil
# The server will not return the values from the POST body because it is
# not set to parse application/json values. Like your own apps, you must
# set it manually to accept this type of body encoding.

# set it back to ensure that the change is reversed
@client.encode_post_body_as_json! false
@client.send(:format_body, body) == "key=value"
@client.post('/returner', body)[:body][:key].should == "value"
end

end
5 changes: 5 additions & 0 deletions support/generators/halcyon/templates/runner.ru
Expand Up @@ -5,4 +5,9 @@ $:.unshift(Halcyon.root/'lib')
puts "(Starting in #{Halcyon.root})"

Thin::Logging.silent = true if defined? Thin

# Uncomment if you plan to allow clients to send requests with the POST body
# Content-Type as application/json.
# use Rack::PostBodyContentTypeParsers

run Halcyon::Runner.new
4 changes: 4 additions & 0 deletions support/generators/halcyon_flat/templates/runner.ru
Expand Up @@ -5,4 +5,8 @@ puts "(Starting in #{Halcyon.root})"

Thin::Logging.silent = true if defined? Thin

# Uncomment if you plan to allow clients to send requests with the POST body
# Content-Type as application/json.
# use Rack::PostBodyContentTypeParsers

run Halcyon::Runner.new

0 comments on commit 238909f

Please sign in to comment.