sessionm-resthome 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +28 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +140 -0
- data/Rakefile +43 -0
- data/VERSION +1 -0
- data/examples/amazon_product_web_service.rb +84 -0
- data/examples/amazon_ses_service.rb +74 -0
- data/examples/chargify_web_service.rb +55 -0
- data/examples/last_fm_web_service.rb +33 -0
- data/examples/twilio_web_service.rb +31 -0
- data/examples/wordpress_web_service.rb +89 -0
- data/lib/resthome.rb +370 -0
- data/resthome.gemspec +86 -0
- data/spec/helper.rb +19 -0
- data/spec/lib/resthome_spec.rb +546 -0
- data/tasks/spec.rake +9 -0
- metadata +150 -0
data/lib/resthome.rb
ADDED
@@ -0,0 +1,370 @@
|
|
1
|
+
# Copyright (C) Doug Youch
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
class RESTHome
|
6
|
+
class Error < Exception; end
|
7
|
+
class InvalidResponse < Error; end
|
8
|
+
class MethodMissing < Error; end
|
9
|
+
|
10
|
+
include HTTParty
|
11
|
+
|
12
|
+
attr_accessor :base_uri, :basic_auth, :cookies
|
13
|
+
attr_reader :response, :request_url, :request_options, :request_method
|
14
|
+
|
15
|
+
# Defines a web service route
|
16
|
+
#
|
17
|
+
# === Arguments
|
18
|
+
# *name* of the method to create
|
19
|
+
# name has special meaning.
|
20
|
+
# * If starts with create or add the method will be set to POST.
|
21
|
+
# * If starts with edit or update the method will be set to PUT.
|
22
|
+
# * If starts with delete the method will be set to DELETE.
|
23
|
+
# * Else by default the method is GET.
|
24
|
+
#
|
25
|
+
# *path* is the path to the web service
|
26
|
+
#
|
27
|
+
# === Options
|
28
|
+
#
|
29
|
+
# [:method]
|
30
|
+
# The request method get/post/put/delete. Default is get.
|
31
|
+
# [:expected_status]
|
32
|
+
# Expected status code of the response, will raise InvalidResponse. Can be an array of codes.
|
33
|
+
# [:return]
|
34
|
+
# The method to call, the class to create or a Proc to call before method returns.
|
35
|
+
# [:resource]
|
36
|
+
# The name of the element to return from the response.
|
37
|
+
# [:no_body]
|
38
|
+
# Removes the body argument from a post/put route
|
39
|
+
# [:query]
|
40
|
+
# Default set of query arguments
|
41
|
+
# [:body]
|
42
|
+
# Default set of body arguments
|
43
|
+
def self.route(name, path, options={}, &block)
|
44
|
+
args = path.scan /:[a-z_]+/
|
45
|
+
path = "#{@path_prefix.join if @path_prefix}#{path}"
|
46
|
+
function_args = args.collect{ |arg| arg[1..-1] }
|
47
|
+
|
48
|
+
body_args = get_function_args options, :body
|
49
|
+
function_args += body_args.map { |a| a.downcase.gsub(/[^a-z0-9_]/, '_').sub(/^\d+/, '') }
|
50
|
+
|
51
|
+
query_args = get_function_args options, :query
|
52
|
+
function_args += query_args.map { |a| a.downcase.gsub(/[^a-z0-9_]/, '_').sub(/^\d+/, '') }
|
53
|
+
|
54
|
+
method = options[:method]
|
55
|
+
if method.nil?
|
56
|
+
if name.to_s =~ /^(create|add|edit|update|delete)_/
|
57
|
+
case $1
|
58
|
+
when 'create'
|
59
|
+
method = 'post'
|
60
|
+
when 'add'
|
61
|
+
method = 'post'
|
62
|
+
when 'edit'
|
63
|
+
method = 'put'
|
64
|
+
when 'update'
|
65
|
+
method = 'put'
|
66
|
+
when 'delete'
|
67
|
+
method = 'delete'
|
68
|
+
end
|
69
|
+
else
|
70
|
+
method = 'get'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
method = method.to_s
|
74
|
+
|
75
|
+
expected_status = options[:expected_status]
|
76
|
+
if expected_status.nil?
|
77
|
+
case method
|
78
|
+
when 'post'
|
79
|
+
expected_status ||= [200, 201]
|
80
|
+
when 'delete'
|
81
|
+
expected_status ||= [200, 204]
|
82
|
+
else
|
83
|
+
expected_status ||= 200
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
function_args << 'body' if (method == 'post' || method == 'put') && options[:no_body].nil?
|
88
|
+
function_args << 'options={}, &block'
|
89
|
+
|
90
|
+
method_src = <<-METHOD
|
91
|
+
def #{name}(#{function_args.join(',')})
|
92
|
+
path = "#{path}"
|
93
|
+
METHOD
|
94
|
+
|
95
|
+
args.each_with_index do |arg, idx|
|
96
|
+
method_src << "path.sub! '#{arg}', URI.escape(#{function_args[idx]}.to_s)\n"
|
97
|
+
end
|
98
|
+
|
99
|
+
if options[:no_body]
|
100
|
+
if options[:body]
|
101
|
+
method_src << "options[:body] = #{options[:body].inspect}.merge(options[:body] || {})\n"
|
102
|
+
elsif body_args.size > 0
|
103
|
+
method_src << "options[:body] ||= {}\n"
|
104
|
+
end
|
105
|
+
else
|
106
|
+
if method == 'post' || method == 'put'
|
107
|
+
if options[:resource]
|
108
|
+
method_src << "options[:body] = {'#{options[:resource].to_s}' => body}\n"
|
109
|
+
elsif options[:body]
|
110
|
+
method_src << "options[:body] = #{options[:body].inspect}.merge(body || {})\n"
|
111
|
+
else
|
112
|
+
method_src << "options[:body] = body\n"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
if options[:query]
|
118
|
+
method_src << "options[:query] = #{options[:query].inspect}.merge(options[:query] || {})\n"
|
119
|
+
elsif query_args.size > 0
|
120
|
+
method_src << "options[:query] ||= {}\n"
|
121
|
+
end
|
122
|
+
|
123
|
+
body_args.each_with_index do |arg, idx|
|
124
|
+
idx += args.size
|
125
|
+
method_src << "options[:body]['#{arg}'] = #{function_args[idx]}\n"
|
126
|
+
end
|
127
|
+
|
128
|
+
query_args.each_with_index do |arg, idx|
|
129
|
+
idx += body_args.size + args.size
|
130
|
+
method_src << "options[:query]['#{arg}'] = #{function_args[idx]}\n"
|
131
|
+
end
|
132
|
+
|
133
|
+
method_src << "request :#{method}, path, options\n"
|
134
|
+
|
135
|
+
if expected_status
|
136
|
+
if expected_status.is_a?(Array)
|
137
|
+
method_src << 'raise InvalidResponse.new "Invalid response code #{response.code}" if ! [' + expected_status.join(',') + "].include?(response.code)\n"
|
138
|
+
else
|
139
|
+
method_src << 'raise InvalidResponse.new "Invalid response code #{response.code}" if response.code != ' + expected_status.to_s + "\n"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
return_method = 'nil'
|
144
|
+
if options[:return].nil? || options[:return].is_a?(Proc)
|
145
|
+
block ||= options[:return]
|
146
|
+
if block
|
147
|
+
register_route_block name, block
|
148
|
+
return_method = "self.class.route_blocks['#{name}']"
|
149
|
+
end
|
150
|
+
elsif options[:return].is_a?(Class)
|
151
|
+
return_method = options[:return].to_s
|
152
|
+
else
|
153
|
+
return_method = ":#{options[:return]}"
|
154
|
+
end
|
155
|
+
|
156
|
+
resource = options[:resource] ? "'#{options[:resource]}'" : 'nil'
|
157
|
+
|
158
|
+
method_src << "parse_response!\n"
|
159
|
+
|
160
|
+
method_src << "_handle_response response, :resource => #{resource}, :return => #{return_method}, &block\n"
|
161
|
+
|
162
|
+
method_src << "end\n"
|
163
|
+
|
164
|
+
if options[:instance]
|
165
|
+
options[:instance].instance_eval method_src, __FILE__, __LINE__
|
166
|
+
elsif options[:class]
|
167
|
+
options[:class].class_eval method_src, __FILE__, __LINE__
|
168
|
+
else
|
169
|
+
self.class_eval method_src, __FILE__, __LINE__
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Adds a route to the current object
|
174
|
+
def route(name, path, options={})
|
175
|
+
self.class.route name, path, options.merge(:instance => self)
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.namespace(path_prefix)
|
179
|
+
@path_prefix ||= []
|
180
|
+
@path_prefix.push path_prefix
|
181
|
+
yield
|
182
|
+
@path_prefix.pop
|
183
|
+
end
|
184
|
+
|
185
|
+
# Creates routes for a RESTful API
|
186
|
+
#
|
187
|
+
# *resource_name* is the name of the items returned by the API,
|
188
|
+
# *collection_name* is the plural name of the items,
|
189
|
+
# *base_path* is the path to the collection
|
190
|
+
#
|
191
|
+
# Sets up 5 most common RESTful routes
|
192
|
+
#
|
193
|
+
# Example
|
194
|
+
# /customers.json GET list of customers, POST to create a customer
|
195
|
+
# /customers/1.json GET a customers, PUT to edit a customer, DELETE to delete a customer
|
196
|
+
# JSON response returns {'customer': {'id':1, 'name':'Joe', ...}}
|
197
|
+
#
|
198
|
+
# Setup the RESTful routes
|
199
|
+
# rest :customer, :customers, '/customers.json'
|
200
|
+
# # same as
|
201
|
+
# route :customers, '/customers.json', :resource => :customer
|
202
|
+
# route :create_customer, '/customers.json', :resource => :customer
|
203
|
+
# route :customer, '/customers/:customer_id.json', :resource => :customer
|
204
|
+
# route :edit_customer, '/customers/:customer_id.json', :resource => :customer
|
205
|
+
# route :delete_customer, '/customers/:customer_id.json', :resource => :customer
|
206
|
+
#
|
207
|
+
# Following methods are created
|
208
|
+
# customers # return an array of customers
|
209
|
+
# create_customer :name => 'Smith' # returns {'id' => 2, 'name' => 'Smith'}
|
210
|
+
# customer 1 # return data for customer 1
|
211
|
+
# edit_customer 1, :name => 'Joesph'
|
212
|
+
# delete_customer 1
|
213
|
+
def self.rest(resource_name, collection_name, base_path, options={})
|
214
|
+
options[:resource] ||= resource_name
|
215
|
+
self.route collection_name, base_path, options
|
216
|
+
self.route resource_name, base_path.sub(/(\.[a-zA-Z0-9]+)$/, "/:#{resource_name}_id\\1"), options
|
217
|
+
self.route "edit_#{resource_name}", base_path.sub(/(\.[a-zA-Z0-9]+)$/, "/:#{resource_name}_id\\1"), options
|
218
|
+
self.route "create_#{resource_name}", base_path, options
|
219
|
+
self.route "delete_#{resource_name}", base_path.sub(/(\.[a-zA-Z0-9]+)$/, "/:#{resource_name}_id\\1"), options
|
220
|
+
end
|
221
|
+
|
222
|
+
# Creates the url
|
223
|
+
def build_url(path)
|
224
|
+
"#{self.base_uri || self.class.base_uri}#{path}"
|
225
|
+
end
|
226
|
+
|
227
|
+
# Adds the basic_auth and cookie options
|
228
|
+
# This method should be overwritten as needed.
|
229
|
+
def build_options!(options)
|
230
|
+
options[:basic_auth] = self.basic_auth if self.basic_auth
|
231
|
+
if @cookies
|
232
|
+
options[:headers] ||= {}
|
233
|
+
options[:headers]['cookie'] = @cookies.to_a.collect{|c| "#{c[0]}=#{c[1]}"}.join('; ') + ';'
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Makes the request using HTTParty. Saves the method, path and options used.
|
238
|
+
def request(method, path, options)
|
239
|
+
build_options! options
|
240
|
+
url = build_url path
|
241
|
+
@request_method = method
|
242
|
+
@request_url = url
|
243
|
+
@request_options = options
|
244
|
+
|
245
|
+
@response = self.class.send(method, url, options)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Will either call edit_<name> or add_<name> based on wether or not the body[:id] exists.
|
249
|
+
def save(name, body, options={})
|
250
|
+
id = body[:id] || body['id']
|
251
|
+
if id
|
252
|
+
if self.class.method_defined?("edit_#{name}")
|
253
|
+
self.send("edit_#{name}", id, body, options)
|
254
|
+
elsif self.class.method_defined?("update_#{name}")
|
255
|
+
self.send("update_#{name}", id, body, options)
|
256
|
+
else
|
257
|
+
raise MethodMissing.new "No edit/update method found for #{name}"
|
258
|
+
end
|
259
|
+
else
|
260
|
+
if self.class.method_defined?("add_#{name}")
|
261
|
+
self.send("add_#{name}", body, options)
|
262
|
+
elsif self.class.method_defined?("create_#{name}")
|
263
|
+
self.send("create_#{name}", body, options)
|
264
|
+
else
|
265
|
+
raise MethodMissing.new "No add/create method found for #{name}"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def method_missing(method, *args, &block) #:nodoc:
|
271
|
+
if method.to_s =~ /^find_(.*?)_by_(.*)$/
|
272
|
+
find_method = "find_#{$1}"
|
273
|
+
find_args = $2.split '_and_'
|
274
|
+
raise MethodMissing.new "Missing method #{find_method}" unless self.class.method_defined?(find_method)
|
275
|
+
start = (self.method(find_method).arity + 1).abs
|
276
|
+
options = args[-1].is_a?(Hash) ? args[-1] : {}
|
277
|
+
options[:query] ||= {}
|
278
|
+
find_args.each_with_index do |find_arg, idx|
|
279
|
+
options[:query][find_arg] = args[start+idx]
|
280
|
+
end
|
281
|
+
|
282
|
+
if start > 0
|
283
|
+
send_args = args[0..(start-1)]
|
284
|
+
send_args << options
|
285
|
+
return self.send(find_method, *send_args, &block)
|
286
|
+
else
|
287
|
+
return self.send(find_method, options, &block)
|
288
|
+
end
|
289
|
+
else
|
290
|
+
super
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Convenience method for saving all cookies by default called from parse_response!.
|
295
|
+
def save_cookies!
|
296
|
+
return unless @response.headers.to_hash['set-cookie']
|
297
|
+
save_cookies @response.headers.to_hash['set-cookie']
|
298
|
+
end
|
299
|
+
|
300
|
+
# Parse an array of Set-cookie headers
|
301
|
+
def save_cookies(data)
|
302
|
+
@cookies ||= {}
|
303
|
+
data.delete_if{ |c| c.blank? }.collect { |cookie| parts = cookie.split("\; "); parts[0] ? parts[0].split('=') : nil }.each do |c|
|
304
|
+
@cookies[c[0].strip] = c[1].strip if c && c[0] && c[1]
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Called after every valid request. Useful for parsing response headers.
|
309
|
+
# This method should be overwritten as needed.
|
310
|
+
def parse_response!
|
311
|
+
save_cookies!
|
312
|
+
end
|
313
|
+
|
314
|
+
def self.route_blocks #:nodoc:
|
315
|
+
{}
|
316
|
+
end
|
317
|
+
|
318
|
+
def self.register_route_block(route, proc) #:nodoc:
|
319
|
+
blocks = self.route_blocks
|
320
|
+
blocks[route.to_s] = proc
|
321
|
+
|
322
|
+
sing = class << self; self; end
|
323
|
+
sing.send :define_method, :route_blocks do
|
324
|
+
blocks
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
protected
|
329
|
+
|
330
|
+
def _handle_response(response, opts={}, &block) #:nodoc:
|
331
|
+
if response.is_a?(Array)
|
332
|
+
response.to_a.collect do |obj|
|
333
|
+
_handle_response_object obj, opts, &block
|
334
|
+
end
|
335
|
+
else
|
336
|
+
_handle_response_object response, opts, &block
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def _handle_response_object(obj, opts={}) #:nodoc:
|
341
|
+
obj = obj[opts[:resource]] unless opts[:resource].blank?
|
342
|
+
if opts[:return]
|
343
|
+
if opts[:return].is_a?(Class)
|
344
|
+
obj = opts[:return].new obj
|
345
|
+
elsif opts[:return].is_a?(Proc)
|
346
|
+
obj = opts[:return].call obj
|
347
|
+
else
|
348
|
+
obj = send opts[:return], obj
|
349
|
+
end
|
350
|
+
end
|
351
|
+
obj = yield(obj) if block_given?
|
352
|
+
obj
|
353
|
+
end
|
354
|
+
|
355
|
+
private
|
356
|
+
|
357
|
+
def self.get_function_args(options, fld)
|
358
|
+
args = []
|
359
|
+
if options[fld]
|
360
|
+
options[fld].each do |n, v|
|
361
|
+
next unless v.is_a?(Symbol)
|
362
|
+
idx = v.to_s.gsub(/[^\d]/, '').to_i
|
363
|
+
args[idx] = n.to_s
|
364
|
+
options[fld].delete n
|
365
|
+
end
|
366
|
+
args.compact!
|
367
|
+
end
|
368
|
+
args
|
369
|
+
end
|
370
|
+
end
|
data/resthome.gemspec
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{sessionm-resthome}
|
8
|
+
s.version = "0.8.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Doug Youch"]
|
12
|
+
s.date = %q{2011-03-09}
|
13
|
+
s.description = %q{Simple wrapper class generator for consuming RESTful web services}
|
14
|
+
s.email = %q{doug@sessionm.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
"Gemfile",
|
22
|
+
"Gemfile.lock",
|
23
|
+
"LICENSE.txt",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"examples/amazon_product_web_service.rb",
|
28
|
+
"examples/amazon_ses_service.rb",
|
29
|
+
"examples/chargify_web_service.rb",
|
30
|
+
"examples/last_fm_web_service.rb",
|
31
|
+
"examples/twilio_web_service.rb",
|
32
|
+
"examples/wordpress_web_service.rb",
|
33
|
+
"lib/resthome.rb",
|
34
|
+
"resthome.gemspec",
|
35
|
+
"spec/helper.rb",
|
36
|
+
"spec/lib/resthome_spec.rb",
|
37
|
+
"tasks/spec.rake"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/sessionm/resthome}
|
40
|
+
s.licenses = ["MIT"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.rubygems_version = %q{1.3.7}
|
43
|
+
s.summary = %q{RESTful web services consumer}
|
44
|
+
s.test_files = [
|
45
|
+
"examples/amazon_product_web_service.rb",
|
46
|
+
"examples/amazon_ses_service.rb",
|
47
|
+
"examples/chargify_web_service.rb",
|
48
|
+
"examples/last_fm_web_service.rb",
|
49
|
+
"examples/twilio_web_service.rb",
|
50
|
+
"examples/wordpress_web_service.rb",
|
51
|
+
"spec/helper.rb",
|
52
|
+
"spec/lib/resthome_spec.rb"
|
53
|
+
]
|
54
|
+
|
55
|
+
if s.respond_to? :specification_version then
|
56
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
57
|
+
s.specification_version = 3
|
58
|
+
|
59
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
60
|
+
s.add_runtime_dependency(%q<httparty>, [">= 0"])
|
61
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
62
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
|
63
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
64
|
+
s.add_development_dependency(%q<fakeweb>, [">= 0"])
|
65
|
+
s.add_development_dependency(%q<json>, [">= 0"])
|
66
|
+
s.add_development_dependency(%q<rspec>, [">= 0"])
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<httparty>, [">= 0"])
|
69
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
70
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
71
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
72
|
+
s.add_dependency(%q<fakeweb>, [">= 0"])
|
73
|
+
s.add_dependency(%q<json>, [">= 0"])
|
74
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
75
|
+
end
|
76
|
+
else
|
77
|
+
s.add_dependency(%q<httparty>, [">= 0"])
|
78
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
79
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
80
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
81
|
+
s.add_dependency(%q<fakeweb>, [">= 0"])
|
82
|
+
s.add_dependency(%q<json>, [">= 0"])
|
83
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
begin
|
5
|
+
Bundler.setup(:default, :development)
|
6
|
+
rescue Bundler::BundlerError => e
|
7
|
+
$stderr.puts e.message
|
8
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
9
|
+
exit e.status_code
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'spec/autorun'
|
13
|
+
require 'fakeweb'
|
14
|
+
require 'json'
|
15
|
+
|
16
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
17
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
18
|
+
require 'resthome'
|
19
|
+
|