sessionm-resthome 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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
+