halcyon 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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}
|