sessionm-resthome 0.8.1
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/.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
|
+
|