old_api_resource 0.3.0

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