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,25 @@
1
+ require 'active_support/json'
2
+
3
+ module OldApiResource
4
+ module Formats
5
+ module JsonFormat
6
+ extend self
7
+
8
+ def extension
9
+ "json"
10
+ end
11
+
12
+ def mime_type
13
+ "application/json"
14
+ end
15
+
16
+ def encode(hash, options = nil)
17
+ ActiveSupport::JSON.encode(hash, options)
18
+ end
19
+
20
+ def decode(json)
21
+ ActiveSupport::JSON.decode(json)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ require 'active_support/core_ext/hash/conversions'
2
+
3
+ module OldApiResource
4
+ module Formats
5
+ module XmlFormat
6
+ extend self
7
+
8
+ def extension
9
+ "xml"
10
+ end
11
+
12
+ def mime_type
13
+ "application/xml"
14
+ end
15
+
16
+ def encode(hash, options={})
17
+ hash.to_xml(options)
18
+ end
19
+
20
+ def decode(xml)
21
+ from_xml_data(Hash.from_xml(xml))
22
+ end
23
+
24
+ private
25
+ # Manipulate from_xml Hash, because xml_simple is not exactly what we
26
+ # want for Active Resource.
27
+ def from_xml_data(data)
28
+ if data.is_a?(Hash) && data.keys.size == 1
29
+ data.values.first
30
+ else
31
+ data
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ module OldApiResource
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def request(event)
4
+ result = event[:payload]
5
+ info "#{event.payload[:method].to_s.upcase} #{even.payload[:request_uri]}"
6
+ info "--> %d %s %d (%.1fms)" % [result.code, result.message, result.body.to_s.length, event.duration]
7
+ end
8
+
9
+ def logger
10
+ Rails.logger
11
+ end
12
+ end
13
+ end
14
+
15
+ OldApiResource::LogSubscriber.attach_to :old_api_resource
@@ -0,0 +1,260 @@
1
+ require 'old_api_resource'
2
+
3
+ module OldApiResource
4
+
5
+ module Mocks
6
+
7
+ @@endpoints = {}
8
+ @@path = nil
9
+
10
+ # A simple interface class to change the new connection to look like the
11
+ # old activeresource connection
12
+ class Interface
13
+
14
+ def initialize(path)
15
+ @path = path
16
+ end
17
+
18
+ def get(*args, &block)
19
+ Connection.send(:get, @path, *args, &block)
20
+ end
21
+ def post(*args, &block)
22
+ Connection.send(:post, @path, *args, &block)
23
+ end
24
+ def put(*args, &block)
25
+ Connection.send(:put, @path, *args, &block)
26
+ end
27
+ def delete(*args, &block)
28
+ Connection.send(:delete, @path, *args, &block)
29
+ end
30
+ def head(*args, &block)
31
+ Connection.send(:head, @path, *args, &block)
32
+ end
33
+ end
34
+
35
+ # set OldApiResource's http
36
+ def self.init
37
+ ::OldApiResource::Connection.class_eval do
38
+ private
39
+ def http(path)
40
+ Interface.new(path)
41
+ end
42
+ end
43
+ end
44
+
45
+ # clear out the defined mocks
46
+ def self.clear_endpoints
47
+ ret = @@endpoints
48
+ @@endpoints = {}
49
+ ret
50
+ end
51
+ # re-set the endpoints
52
+ def self.set_endpoints(new_endpoints)
53
+ @@endpoints = new_endpoints
54
+ end
55
+ # return the defined endpoints
56
+ def self.endpoints
57
+ @@endpoints
58
+ end
59
+ def self.define(&block)
60
+ instance_eval(&block) if block_given?
61
+ end
62
+ # define an endpoint for the mock
63
+ def self.endpoint(path, &block)
64
+ path, format = path.split(".")
65
+ @@endpoints[path] ||= []
66
+ with_path_and_format(path, format) do
67
+ instance_eval(&block) if block_given?
68
+ end
69
+ end
70
+ # find a matching response
71
+ def self.find_response(request)
72
+ path = request.path.split("?").first
73
+ path = path.split(/\./).first
74
+ # these are stored as [[Request, Response], [Request, Response]]
75
+ responses_and_params = self.matching(path)
76
+ ret = (responses_and_params[:responses] || []).select{|pair| pair.first.match?(request)}
77
+ raise Exception.new("More than one response matches #{request}") if ret.length > 1
78
+ return ret.first ? {:response => ret.first[1], :params => responses_and_params[:params]} : nil
79
+ end
80
+
81
+ # returns a hash {:responses => [[Request, Response],[Request,Response]], :params => {...}}
82
+ # if there is no match returns nil
83
+ def self.matching(path)
84
+ # The obvious case
85
+ if @@endpoints[path]
86
+ return {:responses => @@endpoints[path], :params => {}}
87
+ end
88
+ # parameter names prefixed with colons should match parts
89
+ # of the path and push those parameters into the response
90
+ @@endpoints.keys.each do |possible_path|
91
+ if self.paths_match?(possible_path, path)
92
+ return {:responses => @@endpoints[possible_path], :params => self.extract_params(possible_path, path)}
93
+ end
94
+ end
95
+
96
+ return {:responses => nil, :params => nil}
97
+ end
98
+
99
+ def self.paths_match?(known_path, entered_path)
100
+ PathString.paths_match?(known_path, entered_path)
101
+ end
102
+
103
+ # This method assumes that the two are matching paths
104
+ # if they aren't the behavior is undefined
105
+ def self.extract_params(known_path, entered_path)
106
+ PathString.extract_params(known_path, entered_path)
107
+ end
108
+
109
+ private
110
+
111
+ def self.with_path_and_format(path, format, &block)
112
+ @@path, @@format = path, format
113
+ ret = yield
114
+ @@path, @@format = nil, nil
115
+ ret
116
+ end
117
+ # define the
118
+ [:post, :put, :get, :delete, :head].each do |verb|
119
+ instance_eval <<-EOE, __FILE__, __LINE__ + 1
120
+ def #{verb}(response_body, opts = {}, &block)
121
+
122
+ raise Exception.new("Must be called from within an endpoint block") unless @@path
123
+ opts = opts.reverse_merge({:status_code => 200, :response_headers => {}, :params => {}})
124
+
125
+ @@endpoints[@@path] << [MockRequest.new(:#{verb}, @@path, :params => opts[:params], :format => @@format), MockResponse.new(response_body, :status_code => opts[:status_code], :headers => opts[:response_headers], :format => @@format, &block)]
126
+ end
127
+ EOE
128
+ end
129
+
130
+ class MockResponse
131
+ attr_reader :body, :headers, :code, :format, :block
132
+ def initialize(body, opts = {}, &block)
133
+ opts = opts.reverse_merge({:headers => {}, :status_code => 200})
134
+ @body = body
135
+ @headers = opts[:headers]
136
+ @code = opts[:status_code]
137
+ @format = (opts[:format] || :json)
138
+ @block = block if block_given?
139
+ end
140
+ def []=(key, val)
141
+ @headers[key] = val
142
+ end
143
+ def [](key)
144
+ @headers[key]
145
+ end
146
+
147
+ def body
148
+ raise Exception.new("Body must respond to to_#{self.format}") unless @body.respond_to?("to_#{self.format}")
149
+ @body.send("to_#{self.format}")
150
+ end
151
+
152
+ def body_as_object
153
+ return @body
154
+ end
155
+
156
+ def generate_response(params)
157
+ @body = @body.instance_exec(params, &self.block) if self.block
158
+ end
159
+ end
160
+
161
+ class MockRequest
162
+ attr_reader :method, :path, :body, :headers, :params, :format, :query
163
+
164
+ def initialize(method, path, opts = {})
165
+ @method = method.to_sym
166
+
167
+ # set the normalized path, format and query string
168
+ @path, @query = path.split("?")
169
+ @path, @format = @path.split(".")
170
+
171
+ # if we have params, it is a MockRequest definition
172
+ if opts[:params]
173
+ @params = JSON.parse(JSON.unparse(opts[:params]))
174
+ # otherwise, we need to check either the query string or the body
175
+ # depending on the http verb
176
+ else
177
+ case @method
178
+ when :post, :put
179
+ @params = JSON.parse(opts[:body] || "").sort
180
+ when :get, :delete, :head
181
+ @params = sorted_params(@query || "")
182
+ end
183
+ end
184
+ @body = opts[:body]
185
+ @headers = opts[:headers] || {}
186
+ @headers["Content-Length"] = @body.blank? ? "0" : @body.size.to_s
187
+ end
188
+
189
+ #
190
+ def sorted_params(data)
191
+ ret = {}
192
+ data.split("&").each do |val|
193
+ val = val.split("=")
194
+ if val.last =~ /^\d+$/
195
+ ret[val.first] = val.last.to_i
196
+ elsif val.last =~ /^[\d\.]+$/
197
+ ret[val.first] = val.last.to_f
198
+ elsif val.last == "true"
199
+ ret[val.first] = true
200
+ elsif val.last == "false"
201
+ ret[val.first] = false
202
+ else
203
+ ret[val.first] = val.last
204
+ end
205
+ end
206
+ ret.sort
207
+ end
208
+
209
+ # because of the context these come from, we can assume that the path already matches
210
+ def match?(request)
211
+ return false unless self.method == request.method
212
+ return false unless self.format == request.format || request.format.nil? || self.format.nil?
213
+ PathString.as_sorted_json(self.params) == PathString.as_sorted_json(request.params)
214
+ end
215
+ # string representation
216
+ def to_s
217
+ "#{self.method.upcase} #{self.format} #{self.path} #{self.params}"
218
+ end
219
+ end
220
+ class Connection
221
+
222
+ cattr_accessor :requests
223
+ self.requests = []
224
+
225
+ # body? methods
226
+ { true => %w(post put),
227
+ false => %w(get delete head) }.each do |has_body, methods|
228
+ methods.each do |method|
229
+ # def post(path, body, headers)
230
+ # request = OldApiResource::Request.new(:post, path, body, headers)
231
+ # self.class.requests << request
232
+ # if response = LifebookerClient::Mocks.find_response(request)
233
+ # response
234
+ # else
235
+ # raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}")
236
+ # end
237
+ # end
238
+ instance_eval <<-EOE, __FILE__, __LINE__ + 1
239
+ def #{method}(path, #{'body, ' if has_body}headers)
240
+ opts = {:headers => headers}
241
+ #{"opts[:body] = body" if has_body}
242
+ request = MockRequest.new(:#{method}, path, opts)
243
+ self.requests << request
244
+ if response = Mocks.find_response(request)
245
+ response[:response].tap{|resp| resp.generate_response(response[:params])}
246
+ else
247
+ raise OldApiResource::ResourceNotFound.new(
248
+ MockResponse.new("", {:headers => {"Content-type" => "application/json"}, :status_code => 404}),
249
+ "Could not find a response recorded for \#{request.pretty_inspect}
250
+ Valid Responses Are:
251
+ \#{Mocks.endpoints.collect{|url, reqs| [url, reqs.collect(&:first).pretty_inspect] }.pretty_inspect}"
252
+ )
253
+ end
254
+ end
255
+ EOE
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,86 @@
1
+ module OldApiResource
2
+
3
+ class Errors < ::ActiveModel::Errors
4
+
5
+ def from_array(messages, save_cache = false)
6
+ clear unless save_cache
7
+ humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
8
+ messages.each do |message|
9
+ attr_message = humanized_attributes.keys.detect do |attr_name|
10
+ if message[0,attr_name.size + 1] == "#{attr_name} "
11
+ add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ def from_hash(messages, save_cache = false)
18
+ clear unless save_cache
19
+ messages.each do |attr, message_array|
20
+ message_array.each do |message|
21
+ add attr, message
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ module ModelErrors
29
+
30
+ extend ActiveSupport::Concern
31
+ include ActiveModel::Validations
32
+
33
+ included do
34
+ # this is required here because this module will be above Base in the inheritance chain
35
+ alias_method_chain :save, :validations
36
+ end
37
+
38
+ module InstanceMethods
39
+
40
+ def save_with_validations(*args)
41
+ # we want to leave the original intact
42
+ options = args.clone.extract_options!
43
+
44
+ perform_validation = options.blank? ? true : options[:validate]
45
+
46
+ @remote_errors = nil
47
+ if perform_validation && valid? || !perform_validation
48
+ save_without_validations(*args)
49
+ true
50
+ else
51
+ false
52
+ end
53
+ rescue OldApiResource::UnprocessableEntity => error
54
+ @remote_errors = error
55
+ load_remote_errors(@remote_errors, true)
56
+ false
57
+ end
58
+
59
+ def load_remote_errors(remote_errors, save_cache = false)
60
+ error_data = self.class.format.decode(remote_errors.response.body)['errors'] || {}
61
+ if error_data.is_a?(Hash)
62
+ self.errors.from_hash(error_data)
63
+ elsif error_data.is_a?(Array)
64
+ self.errors.from_array(error_data)
65
+ else
66
+ raise Exception.new
67
+ end
68
+ rescue Exception
69
+ raise "Invalid response for invalid object: expected an array or hash got #{remote_errors}"
70
+ end
71
+
72
+ # This method runs any local validations but not remote ones
73
+ def valid?
74
+ super
75
+ errors.empty?
76
+ end
77
+
78
+ def errors
79
+ @errors ||= OldApiResource::Errors.new(self)
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,29 @@
1
+ module OldApiResource
2
+
3
+ module Observing
4
+
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::Observing
7
+
8
+ # Redefine these methods to
9
+ included do
10
+ %w( create save update destroy ).each do |method|
11
+ alias_method_chain method, :observers
12
+ end
13
+
14
+ module InstanceMethods
15
+ %w( create save update destroy ).each do |method|
16
+ module_eval <<-EOE, __FILE__, __LINE__ + 1
17
+ def #{method}_with_observers(*args)
18
+ notify_observers(:before_#method)
19
+ if result = #{method}_without_observers(*args)
20
+ notify_observers(:after_#{method})
21
+ end
22
+ return result
23
+ end
24
+ EOE
25
+ end
26
+ end
27
+
28
+ end
29
+ end