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/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
+