halcyon 0.4.0 → 0.5.0
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/AUTHORS +1 -0
- data/LICENSE +20 -0
- data/README +107 -0
- data/Rakefile +8 -6
- data/bin/halcyon +3 -204
- data/lib/halcyon.rb +55 -42
- data/lib/halcyon/application.rb +247 -0
- data/lib/halcyon/application/router.rb +86 -0
- data/lib/halcyon/client.rb +187 -35
- data/lib/halcyon/client/ssl.rb +38 -0
- data/lib/halcyon/controller.rb +154 -0
- data/lib/halcyon/exceptions.rb +67 -59
- data/lib/halcyon/logging.rb +31 -0
- data/lib/halcyon/logging/analogger.rb +31 -0
- data/lib/halcyon/logging/helpers.rb +37 -0
- data/lib/halcyon/logging/log4r.rb +25 -0
- data/lib/halcyon/logging/logger.rb +20 -0
- data/lib/halcyon/logging/logging.rb +19 -0
- data/lib/halcyon/runner.rb +141 -0
- data/lib/halcyon/runner/commands.rb +141 -0
- data/lib/halcyon/runner/helpers.rb +9 -0
- data/lib/halcyon/runner/helpers/command_helper.rb +71 -0
- data/spec/halcyon/application_spec.rb +70 -0
- data/spec/halcyon/client_spec.rb +63 -0
- data/spec/halcyon/controller_spec.rb +68 -0
- data/spec/halcyon/halcyon_spec.rb +63 -0
- data/spec/halcyon/logging_spec.rb +31 -0
- data/spec/halcyon/router_spec.rb +37 -12
- data/spec/halcyon/runner_spec.rb +54 -0
- data/spec/spec_helper.rb +75 -9
- data/support/generators/halcyon/USAGE +0 -0
- data/support/generators/halcyon/halcyon_generator.rb +52 -0
- data/support/generators/halcyon/templates/README +26 -0
- data/support/generators/halcyon/templates/Rakefile +32 -0
- data/support/generators/halcyon/templates/app/application.rb +43 -0
- data/support/generators/halcyon/templates/config/config.yml +36 -0
- data/support/generators/halcyon/templates/config/init/environment.rb +11 -0
- data/support/generators/halcyon/templates/config/init/hooks.rb +39 -0
- data/support/generators/halcyon/templates/config/init/requires.rb +10 -0
- data/support/generators/halcyon/templates/config/init/routes.rb +50 -0
- data/support/generators/halcyon/templates/lib/client.rb +77 -0
- data/support/generators/halcyon/templates/runner.ru +8 -0
- data/support/generators/halcyon_flat/USAGE +0 -0
- data/support/generators/halcyon_flat/halcyon_flat_generator.rb +52 -0
- data/support/generators/halcyon_flat/templates/README +26 -0
- data/support/generators/halcyon_flat/templates/Rakefile +32 -0
- data/support/generators/halcyon_flat/templates/app.rb +49 -0
- data/support/generators/halcyon_flat/templates/lib/client.rb +17 -0
- data/support/generators/halcyon_flat/templates/runner.ru +8 -0
- metadata +73 -20
- data/lib/halcyon/client/base.rb +0 -261
- data/lib/halcyon/client/exceptions.rb +0 -41
- data/lib/halcyon/client/router.rb +0 -106
- data/lib/halcyon/server.rb +0 -62
- data/lib/halcyon/server/auth/basic.rb +0 -107
- data/lib/halcyon/server/base.rb +0 -774
- data/lib/halcyon/server/exceptions.rb +0 -41
- data/lib/halcyon/server/router.rb +0 -103
- data/spec/halcyon/error_spec.rb +0 -55
- data/spec/halcyon/server_spec.rb +0 -105
@@ -0,0 +1,247 @@
|
|
1
|
+
module Halcyon
|
2
|
+
|
3
|
+
# The core of Halcyon on the server side is the Halcyon::Application class
|
4
|
+
# which handles dispatching requests and responding with appropriate messages
|
5
|
+
# to the client (which can be specified).
|
6
|
+
#
|
7
|
+
# Manages shutting down and starting up hooks, routing, dispatching, etc.
|
8
|
+
# Also restricts the requests to acceptable clients, defaulting to all.
|
9
|
+
#
|
10
|
+
class Application
|
11
|
+
include Exceptions
|
12
|
+
|
13
|
+
autoload :Router, 'halcyon/application/router'
|
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
|
25
|
+
|
26
|
+
# Initializes the app:
|
27
|
+
# * runs startup hooks
|
28
|
+
# * registers shutdown hooks
|
29
|
+
#
|
30
|
+
def initialize
|
31
|
+
self.logger.info "Starting up..."
|
32
|
+
|
33
|
+
self.hooks[:startup].call(Halcyon.config) if self.hooks[:startup]
|
34
|
+
|
35
|
+
# clean after ourselves and get prepared to start serving things
|
36
|
+
self.logger.debug "Starting GC."
|
37
|
+
GC.start
|
38
|
+
|
39
|
+
self.logger.info "Started. PID is #{$$}"
|
40
|
+
|
41
|
+
at_exit do
|
42
|
+
self.logger.info "Shutting down #{$$}."
|
43
|
+
self.hooks[:shutdown].call(Halcyon.config) if self.hooks[:shutdown]
|
44
|
+
self.logger.info "Done."
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets up the request and response objects for use in the controllers and
|
49
|
+
# dispatches requests. Renders response data as JSON for response.
|
50
|
+
# +env+ the request environment details
|
51
|
+
#
|
52
|
+
# The internal router (which inherits from the Merb router) is sent the
|
53
|
+
# request to pass back the route for the dispatcher to call. This route is
|
54
|
+
# stored in <tt>env['halcyon.route']</tt> (as per the Rack spec).
|
55
|
+
#
|
56
|
+
# Configs
|
57
|
+
# <tt>Halcyon.config[:allow_from]</tt> #=> (default) <tt>:all</tt>
|
58
|
+
# :all => does not restrict requests from any User-Agent
|
59
|
+
# :local => restricts requests to only local requests (from
|
60
|
+
# localhost, 0.0.0.0, 127.0.0.1)
|
61
|
+
# :halcyon_clients => restricts to only Halcyon clients (identified by
|
62
|
+
# User-Agent)
|
63
|
+
#
|
64
|
+
# Exceptions
|
65
|
+
# If a request raises an exception that inherits from
|
66
|
+
# <tt>Halcyon::Exceptions::Base</tt> (<tt>NotFound</tt>, etc), then the
|
67
|
+
# response is sent with this information.
|
68
|
+
# If a request raises any other kind of <tt>Exception</tt>, it is logged
|
69
|
+
# as an error and a <tt>500 Internal Server Error</tt> is returned.
|
70
|
+
#
|
71
|
+
# Returns [Fixnum:status, {String:header => String:value}, [String:body].to_json]
|
72
|
+
#
|
73
|
+
def call(env)
|
74
|
+
timing = {:started => Time.now}
|
75
|
+
|
76
|
+
request = Rack::Request.new(env)
|
77
|
+
response = Rack::Response.new
|
78
|
+
|
79
|
+
response['Content-Type'] = "application/json"
|
80
|
+
response['User-Agent'] = "JSON/#{JSON::VERSION} Compatible (en-US) Halcyon::Application/#{Halcyon.version}"
|
81
|
+
|
82
|
+
begin
|
83
|
+
acceptable_request!(env)
|
84
|
+
|
85
|
+
env['halcyon.route'] = Router.route(request)
|
86
|
+
result = dispatch(env)
|
87
|
+
rescue Exceptions::Base => e
|
88
|
+
result = {:status => e.status, :body => e.body}
|
89
|
+
self.logger.info e.message
|
90
|
+
rescue Exception => e
|
91
|
+
result = {:status => 500, :body => 'Internal Server Error'}
|
92
|
+
self.logger.error "#{e.message}\n\t" << e.backtrace.join("\n\t")
|
93
|
+
end
|
94
|
+
|
95
|
+
response.status = result[:status]
|
96
|
+
response.write result.to_json
|
97
|
+
|
98
|
+
timing[:finished] = Time.now
|
99
|
+
timing[:total] = (((timing[:finished] - timing[:started])*1e4).round.to_f/1e4)
|
100
|
+
timing[:per_sec] = (((1.0/(timing[:total]))*1e2).round.to_f/1e2)
|
101
|
+
|
102
|
+
self.logger.info "[#{response.status}] #{URI.parse(env['REQUEST_URI'] || env['PATH_INFO']).path} (#{timing[:total]}s;#{timing[:per_sec]}req/s)"
|
103
|
+
# self.logger << "Session ID: #{self.session.id}\n" # TODO: Implement session
|
104
|
+
self.logger << "Params: #{filter_params_for_log(request, env).inspect}\n\n"
|
105
|
+
|
106
|
+
response.finish
|
107
|
+
end
|
108
|
+
|
109
|
+
# Dispatches the controller and action according the routed request.
|
110
|
+
# +env+ the request environment details, including "halcyon.route"
|
111
|
+
#
|
112
|
+
# If no <tt>:controller</tt> is specified, the default <tt>Application</tt>
|
113
|
+
# controller is dispatched to.
|
114
|
+
#
|
115
|
+
# Once the controller is selected and instantiated, the action is called,
|
116
|
+
# defaulting to <tt>:default</tt> if no action is provided.
|
117
|
+
#
|
118
|
+
# If the action called is not defined, a <tt>404 Not Found</tt> exception
|
119
|
+
# will be raised. This will be sent to the client as such, or handled by
|
120
|
+
# the Rack application container, such as the Rack Cascade middleware to
|
121
|
+
# failover to another application (such as Merb or Rails).
|
122
|
+
#
|
123
|
+
# Refer to Halcyon::Application::Router for more details on defining routes
|
124
|
+
# and for where to get further documentation.
|
125
|
+
#
|
126
|
+
# Returns (String|Array|Hash):body
|
127
|
+
#
|
128
|
+
def dispatch(env)
|
129
|
+
route = env['halcyon.route']
|
130
|
+
# make sure that the right controller/action is called based on the route
|
131
|
+
controller = case route[:controller]
|
132
|
+
when NilClass
|
133
|
+
# default to the Application controller
|
134
|
+
::Application.new(env)
|
135
|
+
when String
|
136
|
+
# pulled from URL, so camelize (from merb/core_ext) and symbolize first
|
137
|
+
begin
|
138
|
+
Object.const_get(route[:controller].camel_case.to_sym).new(env)
|
139
|
+
rescue NameError => e
|
140
|
+
raise NotFound.new
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Establish the selected action, defaulting to +default+.
|
145
|
+
action = (route[:action] || 'default').to_sym
|
146
|
+
|
147
|
+
# Respond correctly that a non-existent action was specified if the
|
148
|
+
# method does not exist.
|
149
|
+
raise NotFound.new unless controller.methods.include?(action.to_s)
|
150
|
+
|
151
|
+
# if no errors have occured up to this point, the route should be fully
|
152
|
+
# valid and all exceptions raised should be treated as
|
153
|
+
# <tt>500 Internal Server Error</tt>s, which is handled by <tt>call</tt>.
|
154
|
+
controller.send(action)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Filters unacceptable requests depending on the configuration of the
|
158
|
+
# <tt>:allow_from</tt> option.
|
159
|
+
#
|
160
|
+
# This method is not directly called by the user, instead being called
|
161
|
+
# in the #call method.
|
162
|
+
#
|
163
|
+
# Acceptable values include:
|
164
|
+
# <tt>:all</tt>:: allow every request to go through
|
165
|
+
# <tt>:halcyon_clients</tt>:: only allow Halcyon clients
|
166
|
+
# <tt>:local</tt>:: do not allow for requests from an outside host
|
167
|
+
#
|
168
|
+
# Raises Forbidden
|
169
|
+
#
|
170
|
+
def acceptable_request!(env)
|
171
|
+
case Halcyon.config[:allow_from].to_sym
|
172
|
+
when :all
|
173
|
+
# allow every request to go through
|
174
|
+
when :halcyon_clients
|
175
|
+
# only allow Halcyon clients
|
176
|
+
raise Forbidden.new unless env['USER_AGENT'] =~ /JSON\/1\.1\.\d+ Compatible \(en-US\) Halcyon::Client\(\d+\.\d+\.\d+\)/
|
177
|
+
when :local
|
178
|
+
# do not allow for requests from an outside host
|
179
|
+
raise Forbidden.new unless ['localhost', '127.0.0.1', '0.0.0.0'].member? env["REMOTE_ADDR"]
|
180
|
+
else
|
181
|
+
logger.warn "Unrecognized allow_from configuration value (#{Halcyon.config[:allow_from].to_s}); use all, halcyon_clients, or local. Allowing all requests."
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Assemble params for logging.
|
186
|
+
#
|
187
|
+
# This method exists to be overridden or method-chained to filter out params
|
188
|
+
# from being logged for applications with sensitive data like passwords.
|
189
|
+
#
|
190
|
+
# Returns Hash:params_to_log
|
191
|
+
#
|
192
|
+
def filter_params_for_log(request, env)
|
193
|
+
request.params.merge(env['halcyon.route'])
|
194
|
+
end
|
195
|
+
|
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
|
+
class << self
|
203
|
+
|
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
|
+
# Defines routes for the application.
|
213
|
+
#
|
214
|
+
# Refer to Halcyon::Application::Router for documentation and resources.
|
215
|
+
#
|
216
|
+
def route
|
217
|
+
if block_given?
|
218
|
+
Router.prepare do |router|
|
219
|
+
Router.default_to yield(router) || {:controller => 'application', :action => 'not_found'}
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
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
|
234
|
+
|
235
|
+
# Sets the shutdown hook to the proc.
|
236
|
+
#
|
237
|
+
# Close any resources opened in the +startup+ hook.
|
238
|
+
#
|
239
|
+
def shutdown &hook
|
240
|
+
self.hooks[:shutdown] = hook
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
%w(rubygems merb-core/core_ext merb-core/dispatch/router uri).each {|dep|require dep}
|
2
|
+
|
3
|
+
module Halcyon
|
4
|
+
class Application
|
5
|
+
|
6
|
+
# = Routing
|
7
|
+
#
|
8
|
+
# Handles routing.
|
9
|
+
#
|
10
|
+
# == Usage
|
11
|
+
#
|
12
|
+
# class Halcyon::Application
|
13
|
+
# route do |r|
|
14
|
+
# r.match('/path/to/match').to(:controller => 'a', :action => 'b')
|
15
|
+
# {:action => 'not_found'} # the default route
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# == Default Routes
|
20
|
+
#
|
21
|
+
# Supplying a default route if none of the others match is good practice,
|
22
|
+
# but is unnecessary as the predefined route is always, automatically,
|
23
|
+
# going to contain a redirection to the +not_found+ method which already
|
24
|
+
# exists in Halcyon::Controller. This method is freely overwritable, and
|
25
|
+
# is recommended for those that wish to handle unroutable requests
|
26
|
+
# themselves.
|
27
|
+
#
|
28
|
+
# In order to set a different default route, simply end the call to +route+
|
29
|
+
# with a hash containing the action (and optionally the module) to run.
|
30
|
+
#
|
31
|
+
# == The Hard Work
|
32
|
+
#
|
33
|
+
# The mechanics of the router are solely from the efforts of the Merb
|
34
|
+
# community. This functionality is completely ripped right out of Merb
|
35
|
+
# and makes it functional. All credit to them, and be sure to check out
|
36
|
+
# their great framework: if Halcyon isn't quite what you need, maybe Merb
|
37
|
+
# is.
|
38
|
+
#
|
39
|
+
# http://merbivore.com/
|
40
|
+
class Router < Merb::Router
|
41
|
+
|
42
|
+
class << self
|
43
|
+
|
44
|
+
# Retrieves the last value from the +route+ call in Halcyon::Controller
|
45
|
+
# and, if it's a Hash, sets it to +@@default_route+ to designate the
|
46
|
+
# failover route. If +route+ is not a Hash, though, the internal default
|
47
|
+
# should be used instead (as the last returned value is probably a Route
|
48
|
+
# object returned by the +r.match().to()+ call).
|
49
|
+
# +route+ the default route, or nothing to use <tt>not_found</tt>
|
50
|
+
#
|
51
|
+
# Used exclusively internally.
|
52
|
+
#
|
53
|
+
# Returns nothing
|
54
|
+
def default_to(route)
|
55
|
+
@@default_route = route.is_a?(Hash) ? route : {:action => 'not_found'}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Called internally by the Halcyon::Controller#call method to match
|
59
|
+
# the current request against the currently defined routes. Returns the
|
60
|
+
# params list defined in the +to+ routing definition, opting for the
|
61
|
+
# default route if no match is made.
|
62
|
+
# +request+ the request object to route against
|
63
|
+
#
|
64
|
+
# Returns Hash:{:controller=>..., :action=>..., ...}
|
65
|
+
def route(request)
|
66
|
+
req = Struct.new(:path, :method, :params).new(request.path_info, request.request_method.downcase.to_sym, request.params)
|
67
|
+
|
68
|
+
# perform match
|
69
|
+
route = self.match(req)
|
70
|
+
|
71
|
+
# make sure a route is returned even if no match is found
|
72
|
+
if route[0].nil?
|
73
|
+
#return default route
|
74
|
+
self.logger.debug "No route found. Using default."
|
75
|
+
@@default_route
|
76
|
+
else
|
77
|
+
# params (including action and module if set) for the matching route
|
78
|
+
route[1]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/halcyon/client.rb
CHANGED
@@ -1,49 +1,201 @@
|
|
1
|
-
|
2
|
-
# Created by Matt Todd on 2007-12-14.
|
3
|
-
# Copyright (c) 2007. All rights reserved.
|
4
|
-
#++
|
5
|
-
|
6
|
-
$:.unshift File.dirname(File.join('..', __FILE__))
|
7
|
-
$:.unshift File.dirname(__FILE__)
|
8
|
-
|
9
|
-
#--
|
10
|
-
# dependencies
|
11
|
-
#++
|
12
|
-
|
13
|
-
%w(rubygems halcyon).each {|dep|require dep}
|
14
|
-
begin
|
15
|
-
require 'json/ext'
|
16
|
-
rescue LoadError => e
|
17
|
-
warn 'Using the Pure Ruby JSON... install the json gem to get faster JSON parsing.'
|
18
|
-
require 'json/pure'
|
19
|
-
end
|
20
|
-
|
21
|
-
#--
|
22
|
-
# module
|
23
|
-
#++
|
1
|
+
%w(net/http uri json).each {|dep|require dep}
|
24
2
|
|
25
3
|
module Halcyon
|
26
4
|
|
27
|
-
#
|
28
|
-
#
|
5
|
+
# = Building Custom Clients
|
6
|
+
#
|
7
|
+
# Once your Halcyon JSON Server App starts to take shape, it may be useful
|
8
|
+
# to begin to write tests on expected functionality, and then to implement
|
9
|
+
# API calls with a designated Client lib for your Ruby or Rails apps, etc.
|
10
|
+
# The Base class provides a standard implementation and several options for
|
11
|
+
# wrapping up functionality of your app from the server side into the
|
12
|
+
# client side so that you may begin to use response data.
|
13
|
+
#
|
14
|
+
# == Creating Your Client
|
15
|
+
#
|
16
|
+
# Creating a simple client can be as simple as this:
|
29
17
|
#
|
30
|
-
#
|
18
|
+
# class Simple < Halcyon::Client
|
19
|
+
# def greet(name)
|
20
|
+
# get("/hello/#{name}")
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# The only thing simply may be actually using the Simple client you just
|
25
|
+
# created.
|
26
|
+
#
|
27
|
+
# But to actually get in and use the library, one has to take full
|
28
|
+
# advantage of the HTTP request methods, +get+, +post+, +put+, and
|
29
|
+
# +delete+. These methods simply return the JSON-parsed data from the
|
30
|
+
# server, effectively returning a hash with two key values, +status+ which
|
31
|
+
# contains the HTTP status code, and +body+ which contains the body of the
|
32
|
+
# content returned which can be any number of objects, including, but not
|
33
|
+
# limited to Hash, Array, Numeric, Nil, Boolean, String, etc.
|
34
|
+
#
|
35
|
+
# You are not limited to what your methods can call: they are arbitrarily
|
36
|
+
# and solely up to your whims and needs. It is simply a matter of good
|
37
|
+
# design and performance when it comes to structuring and implementing
|
38
|
+
# client actions which can be complex or simple series of requests to the
|
39
|
+
# server.
|
31
40
|
#
|
32
|
-
# For documentation on using Halcyon, check out the Halcyon::Server::Base and
|
33
|
-
# Halcyon::Client::Base classes which contain much more usage documentation.
|
34
41
|
class Client
|
35
|
-
|
36
|
-
|
42
|
+
include Exceptions
|
43
|
+
|
44
|
+
USER_AGENT = "JSON/#{JSON::VERSION} Compatible (en-US) Halcyon::Client/#{Halcyon.version}"
|
45
|
+
CONTENT_TYPE = 'application/json'
|
46
|
+
DEFAULT_OPTIONS = {
|
47
|
+
:raise_exceptions => false
|
48
|
+
}
|
49
|
+
|
50
|
+
attr_accessor :uri # The server URI
|
51
|
+
attr_accessor :headers # Instance-wide headers
|
52
|
+
attr_accessor :options # Options
|
53
|
+
|
54
|
+
#--
|
55
|
+
# Initialization and setup
|
56
|
+
#++
|
57
|
+
|
58
|
+
# = Connecting to the Server
|
59
|
+
#
|
60
|
+
# Creates a new Client object to allow for requests and responses from
|
61
|
+
# the specified server.
|
62
|
+
#
|
63
|
+
# The +uri+ param contains the URL to the actual server, and should be in
|
64
|
+
# the format: "http://localhost:3801" or "http://app.domain.com:3401/"
|
65
|
+
#
|
66
|
+
# == Server Connections
|
67
|
+
#
|
68
|
+
# Connecting only occurs at the actual event that a request is performed,
|
69
|
+
# so there is no need to worry about closing connections or managing
|
70
|
+
# connections in general other than good object housecleaning. (Be nice
|
71
|
+
# to your Garbage Collector.)
|
72
|
+
#
|
73
|
+
# == Usage
|
74
|
+
#
|
75
|
+
# You can either provide a block to perform all of your requests and
|
76
|
+
# processing inside of or you can simply accept the object in response
|
77
|
+
# and call your request methods off of the returned object.
|
78
|
+
#
|
79
|
+
# Alternatively, you could do both.
|
80
|
+
#
|
81
|
+
# An example of creating and using a Simple client:
|
82
|
+
#
|
83
|
+
# class Simple < Halcyon::Client
|
84
|
+
# def greet(name)
|
85
|
+
# get("/hello/#{name}")
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
# Simple.new('http://localhost:3801') do |s|
|
89
|
+
# puts s.greet("Johnny").inspect
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# This should effectively call +inspect+ on a response hash similar to
|
93
|
+
# this:
|
94
|
+
#
|
95
|
+
# {:status => 200, :body => 'Hello Johnny'}
|
96
|
+
#
|
97
|
+
# Alternatively, you could perform the same with the following:
|
98
|
+
#
|
99
|
+
# s = Simple.new('http://localhost:3801')
|
100
|
+
# puts s.greet("Johnny").inspect
|
101
|
+
#
|
102
|
+
# This should generate the exact same outcome as the previous example,
|
103
|
+
# except that it is not executed in a block.
|
104
|
+
#
|
105
|
+
# The differences are purely semantic and of personal taste.
|
106
|
+
def initialize(uri, headers = {})
|
107
|
+
self.uri = URI.parse(uri)
|
108
|
+
self.headers = headers
|
109
|
+
self.options = DEFAULT_OPTIONS
|
110
|
+
if block_given?
|
111
|
+
yield self
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def raise_exceptions!(setting = true)
|
116
|
+
self.options[:raise_exceptions] = setting
|
37
117
|
end
|
38
118
|
|
39
119
|
#--
|
40
|
-
#
|
120
|
+
# Request Handling
|
41
121
|
#++
|
42
122
|
|
43
|
-
|
44
|
-
|
123
|
+
# Performs a GET request on the URI specified.
|
124
|
+
def get(uri, headers={})
|
125
|
+
req = Net::HTTP::Get.new(uri)
|
126
|
+
request(req, headers)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Performs a POST request on the URI specified.
|
130
|
+
def post(uri, data = {}, headers={})
|
131
|
+
req = Net::HTTP::Post.new(uri)
|
132
|
+
req.body = format_body(data)
|
133
|
+
request(req, headers)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Performs a DELETE request on the URI specified.
|
137
|
+
def delete(uri, headers={})
|
138
|
+
req = Net::HTTP::Delete.new(uri)
|
139
|
+
request(req, headers)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Performs a PUT request on the URI specified.
|
143
|
+
def put(uri, data = {}, headers={})
|
144
|
+
req = Net::HTTP::Put.new(uri)
|
145
|
+
req.body = format_body(data)
|
146
|
+
request(req, headers)
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# Performs an arbitrary HTTP request, receive the response, parse it with
|
152
|
+
# JSON, and return it to the caller. This is a private method because the
|
153
|
+
# user/developer should be quite satisfied with the +get+, +post+, +put+,
|
154
|
+
# and +delete+ methods.
|
155
|
+
#
|
156
|
+
# == Request Failures
|
157
|
+
#
|
158
|
+
# If the server responds with any kind of failure (anything with a status
|
159
|
+
# that isn't 200), Halcyon will in turn raise the respective exception
|
160
|
+
# (defined in Halcyon::Exceptions) which all inherit from
|
161
|
+
# +Halcyon::Exceptions+. It is up to the client to handle these
|
162
|
+
# exceptions specifically.
|
163
|
+
def request(req, headers={})
|
164
|
+
# set default headers
|
165
|
+
req["Content-Type"] = CONTENT_TYPE
|
166
|
+
req["User-Agent"] = USER_AGENT
|
167
|
+
|
168
|
+
# apply provided headers
|
169
|
+
self.headers.merge(headers).each do |(header, value)|
|
170
|
+
req[header] = value
|
171
|
+
end
|
172
|
+
|
173
|
+
# prepare and send HTTP request
|
174
|
+
res = Net::HTTP.start(self.uri.host, self.uri.port) {|http|http.request(req)}
|
175
|
+
|
176
|
+
# parse response
|
177
|
+
body = JSON.parse(res.body).to_mash
|
178
|
+
|
179
|
+
# handle non-successes
|
180
|
+
if self.options[:raise_exceptions] && !res.kind_of?(Net::HTTPSuccess)
|
181
|
+
raise self.class.const_get(Exceptions::HTTP_STATUS_CODES[body[:status]].tr(' ', '_').camel_case.gsub(/( |\-)/,'')).new
|
182
|
+
end
|
183
|
+
|
184
|
+
# return response
|
185
|
+
body
|
186
|
+
rescue Halcyon::Exceptions::Base => e
|
187
|
+
# log exception if logger is in place
|
188
|
+
raise
|
189
|
+
end
|
190
|
+
|
191
|
+
# Formats the data of a POST or PUT request (the body) into an acceptable
|
192
|
+
# format according to Net::HTTP for sending through as a Hash.
|
193
|
+
def format_body(data)
|
194
|
+
data = {:body => data} unless data.is_a? Hash
|
195
|
+
data.to_mash
|
196
|
+
# uses the Merb Hash#to_params method defined in merb/core_ext.
|
197
|
+
data.to_params
|
198
|
+
end
|
45
199
|
|
46
200
|
end
|
47
201
|
end
|
48
|
-
|
49
|
-
%w(halcyon/client/exceptions).each {|dep|require dep}
|