old_api_resource 0.3.0

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.
Files changed (48) hide show
  1. data/.document +5 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +26 -0
  4. data/Guardfile +22 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +49 -0
  8. data/VERSION +1 -0
  9. data/lib/old_api_resource.rb +70 -0
  10. data/lib/old_api_resource/associations.rb +192 -0
  11. data/lib/old_api_resource/associations/association_proxy.rb +92 -0
  12. data/lib/old_api_resource/associations/multi_object_proxy.rb +74 -0
  13. data/lib/old_api_resource/associations/related_object_hash.rb +12 -0
  14. data/lib/old_api_resource/associations/relation_scope.rb +24 -0
  15. data/lib/old_api_resource/associations/resource_scope.rb +25 -0
  16. data/lib/old_api_resource/associations/scope.rb +88 -0
  17. data/lib/old_api_resource/associations/single_object_proxy.rb +64 -0
  18. data/lib/old_api_resource/attributes.rb +162 -0
  19. data/lib/old_api_resource/base.rb +548 -0
  20. data/lib/old_api_resource/callbacks.rb +49 -0
  21. data/lib/old_api_resource/connection.rb +167 -0
  22. data/lib/old_api_resource/core_extensions.rb +7 -0
  23. data/lib/old_api_resource/custom_methods.rb +119 -0
  24. data/lib/old_api_resource/exceptions.rb +85 -0
  25. data/lib/old_api_resource/formats.rb +14 -0
  26. data/lib/old_api_resource/formats/json_format.rb +25 -0
  27. data/lib/old_api_resource/formats/xml_format.rb +36 -0
  28. data/lib/old_api_resource/log_subscriber.rb +15 -0
  29. data/lib/old_api_resource/mocks.rb +260 -0
  30. data/lib/old_api_resource/model_errors.rb +86 -0
  31. data/lib/old_api_resource/observing.rb +29 -0
  32. data/lib/old_api_resource/railtie.rb +18 -0
  33. data/old_api_resource.gemspec +134 -0
  34. data/spec/lib/associations_spec.rb +519 -0
  35. data/spec/lib/attributes_spec.rb +121 -0
  36. data/spec/lib/base_spec.rb +499 -0
  37. data/spec/lib/callbacks_spec.rb +68 -0
  38. data/spec/lib/mocks_spec.rb +28 -0
  39. data/spec/lib/model_errors_spec.rb +29 -0
  40. data/spec/spec_helper.rb +36 -0
  41. data/spec/support/mocks/association_mocks.rb +46 -0
  42. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  43. data/spec/support/mocks/test_resource_mocks.rb +43 -0
  44. data/spec/support/requests/association_requests.rb +14 -0
  45. data/spec/support/requests/error_resource_requests.rb +25 -0
  46. data/spec/support/requests/test_resource_requests.rb +31 -0
  47. data/spec/support/test_resource.rb +50 -0
  48. metadata +286 -0
@@ -0,0 +1,49 @@
1
+ module OldApiResource
2
+
3
+ module Callbacks
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+
9
+ extend ActiveModel::Callbacks
10
+
11
+ define_model_callbacks :save, :create, :update, :destroy
12
+
13
+ [:save, :create, :update, :destroy].each do |action|
14
+ alias_method_chain action, :callbacks
15
+ end
16
+
17
+ end
18
+
19
+ module InstanceMethods
20
+
21
+ def save_with_callbacks(*args)
22
+ _run_save_callbacks do
23
+ save_without_callbacks(*args)
24
+ end
25
+ end
26
+
27
+ def create_with_callbacks(*args)
28
+ _run_create_callbacks do
29
+ create_without_callbacks(*args)
30
+ end
31
+ end
32
+
33
+ def update_with_callbacks(*args)
34
+ _run_update_callbacks do
35
+ update_without_callbacks(*args)
36
+ end
37
+ end
38
+
39
+ def destroy_with_callbacks(*args)
40
+ _run_destroy_callbacks do
41
+ destroy_without_callbacks(*args)
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,167 @@
1
+ require 'active_support/core_ext/benchmark'
2
+ require 'rest_client'
3
+ require 'net/https'
4
+ require 'date'
5
+ require 'time'
6
+ require 'uri'
7
+
8
+ module OldApiResource
9
+ # Class to handle connections to remote web services.
10
+ # This class is used by ActiveResource::Base to interface with REST
11
+ # services.
12
+ class Connection
13
+
14
+ HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept',
15
+ :put => 'Content-Type',
16
+ :post => 'Content-Type',
17
+ :delete => 'Accept',
18
+ :head => 'Accept'
19
+ }
20
+
21
+ attr_reader :site, :user, :password, :auth_type, :timeout, :proxy, :ssl_options
22
+ attr_accessor :format
23
+
24
+ class << self
25
+ def requests
26
+ @@requests ||= []
27
+ end
28
+ end
29
+
30
+ # The +site+ parameter is required and will set the +site+
31
+ # attribute to the URI for the remote resource service.
32
+ def initialize(site, format = OldApiResource::Formats::JsonFormat)
33
+ raise ArgumentError, 'Missing site URI' unless site
34
+ @user = @password = nil
35
+ @uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
36
+ self.site = site
37
+ self.format = format
38
+ end
39
+
40
+ # Set URI for remote service.
41
+ def site=(site)
42
+ @site = site.is_a?(URI) ? site : @uri_parser.parse(site)
43
+ @user = @uri_parser.unescape(@site.user) if @site.user
44
+ @password = @uri_parser.unescape(@site.password) if @site.password
45
+ end
46
+
47
+ # Sets the number of seconds after which HTTP requests to the remote service should time out.
48
+ def timeout=(timeout)
49
+ @timeout = timeout
50
+ end
51
+
52
+ def get(path, headers = {})
53
+ format.decode(request(:get, path, build_request_headers(headers, :get, self.site.merge(path))))
54
+ end
55
+
56
+ def delete(path, headers = {})
57
+ request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path)))
58
+ return true
59
+ end
60
+
61
+ def head(path, headers = {})
62
+ request(:head, path, build_request_headers(headers, :head, self.site.merge(path)))
63
+ end
64
+
65
+
66
+ def put(path, body = {}, headers = {})
67
+ # If there's a file to send then we can't use JSON or XML
68
+ if !body.is_a?(String) && RestClient::Payload.has_file?(body)
69
+ format.decode(request(:put, path, body, build_request_headers(headers, :put, self.site.merge(path))))
70
+ else
71
+ format.decode(request(:put, path, body, build_request_headers(headers, :put, self.site.merge(path))))
72
+ end
73
+ end
74
+
75
+ def post(path, body = {}, headers = {})
76
+ if !body.is_a?(String) && RestClient::Payload.has_file?(body)
77
+ format.decode(request(:post, path, body, build_request_headers(headers, :post, self.site.merge(path))))
78
+ else
79
+ format.decode(request(:post, path, body, build_request_headers(headers, :post, self.site.merge(path))))
80
+ end
81
+ end
82
+
83
+
84
+ private
85
+ # Makes a request to the remote service.
86
+ def request(method, path, *arguments)
87
+ handle_response do
88
+ ActiveSupport::Notifications.instrument("request.old_old_api_resource") do |payload|
89
+
90
+ # debug logging
91
+ OldApiResource.logger.debug("#{method.to_s.upcase} #{path}")
92
+
93
+ payload[:method] = method
94
+ payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
95
+ payload[:result] = http(path).send(method, *arguments)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Handles response and error codes from the remote service.
101
+ def handle_response(&block)
102
+ begin
103
+ result = yield
104
+ rescue RestClient::RequestTimeout
105
+ raise OldApiResource::RequestTimeout.new("Request Time Out")
106
+ rescue Exception => error
107
+ if error.respond_to?(:http_code)
108
+ OldApiResource.logger.error(error.message)
109
+ result = error.response
110
+ else
111
+ raise OldApiResource::ConnectionError.new(nil, "Unknown error #{error}")
112
+ end
113
+ end
114
+ return propogate_response_or_error(result, result.code)
115
+ end
116
+
117
+ def propogate_response_or_error(response, code)
118
+ case code.to_i
119
+ when 301,302
120
+ raise OldApiResource::Redirection.new(response)
121
+ when 200..400
122
+ response.body
123
+ when 400
124
+ raise OldApiResource::BadRequest.new(response)
125
+ when 401
126
+ raise OldApiResource::UnauthorizedAccess.new(response)
127
+ when 403
128
+ raise OldApiResource::ForbiddenAccess.new(response)
129
+ when 404
130
+ raise OldApiResource::ResourceNotFound.new(response)
131
+ when 405
132
+ raise OldApiResource::MethodNotAllowed.new(response)
133
+ when 406
134
+ raise OldApiResource::NotAccepatable.new(response)
135
+ when 409
136
+ raise OldApiResource::ResourceNotFound.new(response)
137
+ when 410
138
+ raise OldApiResource::ResourceGone.new(response)
139
+ when 422
140
+ raise OldApiResource::UnprocessableEntity.new(response)
141
+ when 401..500
142
+ raise OldApiResource::ClientError.new(response)
143
+ when 500..600
144
+ raise OldApiResource::ServerError.new(response)
145
+ else
146
+ raise OldApiResource::ConnectionError.new(response, "Unknown response code: #{code}")
147
+ end
148
+ end
149
+
150
+ # Creates new Net::HTTP instance for communication with the
151
+ # remote service and resources.
152
+ def http(path)
153
+ unless path =~ /\./
154
+ path += ".#{self.format.extension}"
155
+ end
156
+ RestClient::Resource.new "#{site.scheme}://#{site.host}:#{site.port}#{path}", :timeout => @timeout ? @timeout : 10, :open_timeout => @timeout ? @timeout : 10
157
+ end
158
+
159
+ def build_request_headers(headers, verb, uri)
160
+ http_format_header(verb).update(headers)
161
+ end
162
+
163
+ def http_format_header(verb)
164
+ {HTTP_FORMAT_HEADER_NAMES[verb] => format.mime_type}
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,7 @@
1
+ class Array
2
+
3
+ def symbolize_array
4
+ self.collect{|item| item.to_s.to_sym}
5
+ end
6
+
7
+ end
@@ -0,0 +1,119 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ module OldApiResource
4
+ # A module to support custom REST methods and sub-resources, allowing you to break out
5
+ # of the "default" REST methods with your own custom resource requests. For example,
6
+ # say you use Rails to expose a REST service and configure your routes with:
7
+ #
8
+ # map.resources :people, :new => { :register => :post },
9
+ # :member => { :promote => :put, :deactivate => :delete }
10
+ # :collection => { :active => :get }
11
+ #
12
+ # This route set creates routes for the following HTTP requests:
13
+ #
14
+ # POST /people/new/register.xml # PeopleController.register
15
+ # PUT /people/1/promote.xml # PeopleController.promote with :id => 1
16
+ # DELETE /people/1/deactivate.xml # PeopleController.deactivate with :id => 1
17
+ # GET /people/active.xml # PeopleController.active
18
+ #
19
+ # Using this module, Active Resource can use these custom REST methods just like the
20
+ # standard methods.
21
+ #
22
+ # class Person < ActiveResource::Base
23
+ # self.site = "http://37s.sunrise.i:3000"
24
+ # end
25
+ #
26
+ # Person.new(:name => 'Ryan).post(:register) # POST /people/new/register.xml
27
+ # # => { :id => 1, :name => 'Ryan' }
28
+ #
29
+ # Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.xml
30
+ # Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.xml
31
+ #
32
+ # Person.get(:active) # GET /people/active.xml
33
+ # # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
34
+ #
35
+ module CustomMethods
36
+ extend ActiveSupport::Concern
37
+
38
+ included do
39
+ class << self
40
+ alias :orig_delete :delete
41
+
42
+ # Invokes a GET to a given custom REST method. For example:
43
+ #
44
+ # Person.get(:active) # GET /people/active.xml
45
+ # # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
46
+ #
47
+ # Person.get(:active, :awesome => true) # GET /people/active.xml?awesome=true
48
+ # # => [{:id => 1, :name => 'Ryan'}]
49
+ #
50
+ # Note: the objects returned from this method are not automatically converted
51
+ # into ActiveResource::Base instances - they are ordinary Hashes. If you are expecting
52
+ # ActiveResource::Base instances, use the <tt>find</tt> class method with the
53
+ # <tt>:from</tt> option. For example:
54
+ #
55
+ # Person.find(:all, :from => :active)
56
+ def get(custom_method_name, options = {})
57
+ connection.get(custom_method_collection_url(custom_method_name, options), headers)
58
+ end
59
+
60
+ def post(custom_method_name, options = {}, body = '')
61
+ connection.post(custom_method_collection_url(custom_method_name, options), body, headers)
62
+ end
63
+
64
+ def put(custom_method_name, options = {}, body = '')
65
+ connection.put(custom_method_collection_url(custom_method_name, options), body, headers)
66
+ end
67
+
68
+ def delete(custom_method_name, options = {})
69
+ # Need to jump through some hoops to retain the original class 'delete' method
70
+ if custom_method_name.is_a?(Symbol)
71
+ connection.delete(custom_method_collection_url(custom_method_name, options), headers)
72
+ else
73
+ orig_delete(custom_method_name, options)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ module ClassMethods
80
+ def custom_method_collection_url(method_name, options = {})
81
+ prefix_options, query_options = split_options(options)
82
+ "#{prefix(prefix_options)}#{collection_name}/#{method_name}.#{format.extension}#{query_string(query_options)}"
83
+ end
84
+ end
85
+
86
+ module InstanceMethods
87
+ def get(method_name, options = {})
88
+ connection.get(custom_method_element_url(method_name, options), self.class.headers)
89
+ end
90
+
91
+ def post(method_name, options = {}, body = nil)
92
+ request_body = body.blank? ? encode : body
93
+ if new?
94
+ connection.post(custom_method_new_element_url(method_name, options), request_body, self.class.headers)
95
+ else
96
+ connection.post(custom_method_element_url(method_name, options), request_body, self.class.headers)
97
+ end
98
+ end
99
+
100
+ def put(method_name, options = {}, body = '')
101
+ connection.put(custom_method_element_url(method_name, options), body, self.class.headers)
102
+ end
103
+
104
+ def delete(method_name, options = {})
105
+ connection.delete(custom_method_element_url(method_name, options), self.class.headers)
106
+ end
107
+
108
+
109
+ private
110
+ def custom_method_element_url(method_name, options = {})
111
+ "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
112
+ end
113
+
114
+ def custom_method_new_element_url(method_name, options = {})
115
+ "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,85 @@
1
+ module OldApiResource
2
+ class ConnectionError < StandardError # :nodoc:
3
+
4
+ cattr_accessor :http_code
5
+
6
+ attr_reader :response
7
+
8
+ def initialize(response, message = nil)
9
+ @response = response
10
+ @message = message
11
+ end
12
+
13
+ def to_s
14
+ message = "Failed."
15
+ message << " Response code = #{response.code}." if response.respond_to?(:code)
16
+ message << " Response message = #{response.message}." if response.respond_to?(:message)
17
+ message << "\n#{@message}"
18
+ end
19
+
20
+ def http_code
21
+ self.class.http_code
22
+ end
23
+
24
+ end
25
+
26
+ # Raised when a Timeout::Error occurs.
27
+ class RequestTimeout < ConnectionError
28
+ def initialize(message)
29
+ @message = message
30
+ end
31
+ def to_s; @message ;end
32
+ end
33
+
34
+ # Raised when a OpenSSL::SSL::SSLError occurs.
35
+ class SSLError < ConnectionError
36
+ def initialize(message)
37
+ @message = message
38
+ end
39
+ def to_s; @message ;end
40
+ end
41
+
42
+ # 3xx Redirection
43
+ class Redirection < ConnectionError # :nodoc:
44
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
45
+ end
46
+
47
+ # 4xx Client Error
48
+ class ClientError < ConnectionError; end # :nodoc:
49
+
50
+ # 400 Bad Request
51
+ class BadRequest < ClientError; self.http_code = 400; end # :nodoc
52
+
53
+ # 401 Unauthorized
54
+ class UnauthorizedAccess < ClientError; self.http_code = 401; end # :nodoc
55
+
56
+ # 403 Forbidden
57
+ class ForbiddenAccess < ClientError; self.http_code = 403; end # :nodoc
58
+
59
+ # 404 Not Found
60
+ class ResourceNotFound < ClientError; self.http_code = 404; end # :nodoc:
61
+
62
+ # 406 Not Acceptable
63
+ class NotAcceptable < ClientError; self.http_code = 406; end
64
+
65
+ # 409 Conflict
66
+ class ResourceConflict < ClientError; self.http_code = 409; end # :nodoc:
67
+
68
+ # 410 Gone
69
+ class ResourceGone < ClientError; self.http_code = 410; end # :nodoc:
70
+
71
+ class UnprocessableEntity < ClientError; self.http_code = 422; end
72
+
73
+ # 5xx Server Error
74
+ class ServerError < ConnectionError; self.http_code = 400; end # :nodoc:
75
+
76
+ # 405 Method Not Allowed
77
+ class MethodNotAllowed < ClientError # :nodoc:
78
+
79
+ self.http_code = 405
80
+
81
+ def allowed_methods
82
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,14 @@
1
+ module OldApiResource
2
+ module Formats
3
+ autoload :XmlFormat, 'old_api_resource/formats/xml_format'
4
+ autoload :JsonFormat, 'old_api_resource/formats/json_format'
5
+
6
+ # Lookup the format class from a mime type reference symbol. Example:
7
+ #
8
+ # ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
9
+ # ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
10
+ def self.[](mime_type_reference)
11
+ OldApiResource::Formats.const_get(ActiveSupport::Inflector.camelize(mime_type_reference.to_s) + "Format")
12
+ end
13
+ end
14
+ end