resource 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -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/api_resource.rb +51 -0
  10. data/lib/api_resource/associations.rb +472 -0
  11. data/lib/api_resource/attributes.rb +154 -0
  12. data/lib/api_resource/base.rb +517 -0
  13. data/lib/api_resource/callbacks.rb +49 -0
  14. data/lib/api_resource/connection.rb +162 -0
  15. data/lib/api_resource/core_extensions.rb +7 -0
  16. data/lib/api_resource/custom_methods.rb +119 -0
  17. data/lib/api_resource/exceptions.rb +74 -0
  18. data/lib/api_resource/formats.rb +14 -0
  19. data/lib/api_resource/formats/json_format.rb +25 -0
  20. data/lib/api_resource/formats/xml_format.rb +36 -0
  21. data/lib/api_resource/log_subscriber.rb +15 -0
  22. data/lib/api_resource/mocks.rb +249 -0
  23. data/lib/api_resource/model_errors.rb +86 -0
  24. data/lib/api_resource/observing.rb +29 -0
  25. data/resource.gemspec +125 -0
  26. data/spec/lib/associations_spec.rb +412 -0
  27. data/spec/lib/attributes_spec.rb +109 -0
  28. data/spec/lib/base_spec.rb +454 -0
  29. data/spec/lib/callbacks_spec.rb +68 -0
  30. data/spec/lib/model_errors_spec.rb +29 -0
  31. data/spec/spec_helper.rb +32 -0
  32. data/spec/support/mocks/association_mocks.rb +18 -0
  33. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  34. data/spec/support/mocks/test_resource_mocks.rb +23 -0
  35. data/spec/support/requests/association_requests.rb +14 -0
  36. data/spec/support/requests/error_resource_requests.rb +25 -0
  37. data/spec/support/requests/test_resource_requests.rb +31 -0
  38. data/spec/support/test_resource.rb +19 -0
  39. metadata +277 -0
@@ -0,0 +1,36 @@
1
+ require 'active_support/core_ext/hash/conversions'
2
+
3
+ module ApiResource
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 ApiResource
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
+ ApiResource::LogSubscriber.attach_to :api_resource
@@ -0,0 +1,249 @@
1
+ require 'api_resource'
2
+
3
+ module ApiResource
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 ApiResource's http
36
+ def self.init
37
+ ::ApiResource::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 = sorted_params(URI.decode(opts[:params].to_query))
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 = sorted_params(JSON.parse((opts[:body] || "")).to_query)
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
+ ret[val.first] = val.last
195
+ end
196
+ ret.sort
197
+ end
198
+
199
+ # because of the context these come from, we can assume that the path already matches
200
+ def match?(request)
201
+ return false unless self.method == request.method
202
+ return false unless self.format == request.format || request.format.nil? || self.format.nil?
203
+ return PathString.as_sorted_json(self.params) == PathString.as_sorted_json(request.params)
204
+ end
205
+ # string representation
206
+ def to_s
207
+ "#{self.method.upcase} #{self.format} #{self.path} #{self.params}"
208
+ end
209
+ end
210
+ class Connection
211
+
212
+ cattr_accessor :requests
213
+ self.requests = []
214
+
215
+ # body? methods
216
+ { true => %w(post put),
217
+ false => %w(get delete head) }.each do |has_body, methods|
218
+ methods.each do |method|
219
+ # def post(path, body, headers)
220
+ # request = ApiResource::Request.new(:post, path, body, headers)
221
+ # self.class.requests << request
222
+ # if response = LifebookerClient::Mocks.find_response(request)
223
+ # response
224
+ # else
225
+ # raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}")
226
+ # end
227
+ # end
228
+ instance_eval <<-EOE, __FILE__, __LINE__ + 1
229
+ def #{method}(path, #{'body, ' if has_body}headers)
230
+ opts = {:headers => headers}
231
+ #{"opts[:body] = body" if has_body}
232
+ request = MockRequest.new(:#{method}, path, opts)
233
+ self.requests << request
234
+ if response = Mocks.find_response(request)
235
+ response[:response].tap{|resp| resp.generate_response(response[:params])}
236
+ else
237
+ raise Exception.new("
238
+ Could not find a response recorded for \#{request.to_s}
239
+ Valid Responses Are:
240
+ \#{Mocks.endpoints.collect{|url, reqs| [url, reqs.collect(&:first)].to_s }.join("\n")}
241
+ ")
242
+ end
243
+ end
244
+ EOE
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,86 @@
1
+ module ApiResource
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
49
+ true
50
+ else
51
+ false
52
+ end
53
+ rescue ApiResource::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 ||= ApiResource::Errors.new(self)
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,29 @@
1
+ module ApiResource
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
@@ -0,0 +1,125 @@
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{resource}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{Ethan Langevin}]
12
+ s.date = %q{2011-08-26}
13
+ s.description = %q{A replacement for ActiveResource for RESTful APIs that handles associated object and multiple data sources}
14
+ s.email = %q{ejl6266@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "Guardfile",
24
+ "LICENSE.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "lib/api_resource.rb",
29
+ "lib/api_resource/associations.rb",
30
+ "lib/api_resource/attributes.rb",
31
+ "lib/api_resource/base.rb",
32
+ "lib/api_resource/callbacks.rb",
33
+ "lib/api_resource/connection.rb",
34
+ "lib/api_resource/core_extensions.rb",
35
+ "lib/api_resource/custom_methods.rb",
36
+ "lib/api_resource/exceptions.rb",
37
+ "lib/api_resource/formats.rb",
38
+ "lib/api_resource/formats/json_format.rb",
39
+ "lib/api_resource/formats/xml_format.rb",
40
+ "lib/api_resource/log_subscriber.rb",
41
+ "lib/api_resource/mocks.rb",
42
+ "lib/api_resource/model_errors.rb",
43
+ "lib/api_resource/observing.rb",
44
+ "resource.gemspec",
45
+ "spec/lib/associations_spec.rb",
46
+ "spec/lib/attributes_spec.rb",
47
+ "spec/lib/base_spec.rb",
48
+ "spec/lib/callbacks_spec.rb",
49
+ "spec/lib/model_errors_spec.rb",
50
+ "spec/spec_helper.rb",
51
+ "spec/support/mocks/association_mocks.rb",
52
+ "spec/support/mocks/error_resource_mocks.rb",
53
+ "spec/support/mocks/test_resource_mocks.rb",
54
+ "spec/support/requests/association_requests.rb",
55
+ "spec/support/requests/error_resource_requests.rb",
56
+ "spec/support/requests/test_resource_requests.rb",
57
+ "spec/support/test_resource.rb"
58
+ ]
59
+ s.homepage = %q{http://github.com/ejlangev/resource}
60
+ s.licenses = [%q{MIT}]
61
+ s.require_paths = [%q{lib}]
62
+ s.rubygems_version = %q{1.8.8}
63
+ s.summary = %q{A replacement for ActiveResource for RESTful APIs that handles associated object and multiple data sources}
64
+
65
+ if s.respond_to? :specification_version then
66
+ s.specification_version = 3
67
+
68
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
69
+ s.add_runtime_dependency(%q<rails>, ["= 3.0.9"])
70
+ s.add_runtime_dependency(%q<activeresource>, ["= 3.0.9"])
71
+ s.add_runtime_dependency(%q<hash_dealer>, [">= 0"])
72
+ s.add_runtime_dependency(%q<rest-client>, [">= 0"])
73
+ s.add_development_dependency(%q<rspec>, [">= 0"])
74
+ s.add_development_dependency(%q<ruby-debug19>, [">= 0"])
75
+ s.add_development_dependency(%q<growl>, [">= 0"])
76
+ s.add_development_dependency(%q<rspec-rails>, [">= 0"])
77
+ s.add_development_dependency(%q<factory_girl>, [">= 0"])
78
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
79
+ s.add_development_dependency(%q<faker>, [">= 0"])
80
+ s.add_development_dependency(%q<guard>, [">= 0"])
81
+ s.add_development_dependency(%q<guard-rspec>, [">= 0"])
82
+ s.add_development_dependency(%q<mocha>, [">= 0"])
83
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
84
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
85
+ s.add_development_dependency(%q<rcov>, [">= 0"])
86
+ else
87
+ s.add_dependency(%q<rails>, ["= 3.0.9"])
88
+ s.add_dependency(%q<activeresource>, ["= 3.0.9"])
89
+ s.add_dependency(%q<hash_dealer>, [">= 0"])
90
+ s.add_dependency(%q<rest-client>, [">= 0"])
91
+ s.add_dependency(%q<rspec>, [">= 0"])
92
+ s.add_dependency(%q<ruby-debug19>, [">= 0"])
93
+ s.add_dependency(%q<growl>, [">= 0"])
94
+ s.add_dependency(%q<rspec-rails>, [">= 0"])
95
+ s.add_dependency(%q<factory_girl>, [">= 0"])
96
+ s.add_dependency(%q<simplecov>, [">= 0"])
97
+ s.add_dependency(%q<faker>, [">= 0"])
98
+ s.add_dependency(%q<guard>, [">= 0"])
99
+ s.add_dependency(%q<guard-rspec>, [">= 0"])
100
+ s.add_dependency(%q<mocha>, [">= 0"])
101
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
102
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
103
+ s.add_dependency(%q<rcov>, [">= 0"])
104
+ end
105
+ else
106
+ s.add_dependency(%q<rails>, ["= 3.0.9"])
107
+ s.add_dependency(%q<activeresource>, ["= 3.0.9"])
108
+ s.add_dependency(%q<hash_dealer>, [">= 0"])
109
+ s.add_dependency(%q<rest-client>, [">= 0"])
110
+ s.add_dependency(%q<rspec>, [">= 0"])
111
+ s.add_dependency(%q<ruby-debug19>, [">= 0"])
112
+ s.add_dependency(%q<growl>, [">= 0"])
113
+ s.add_dependency(%q<rspec-rails>, [">= 0"])
114
+ s.add_dependency(%q<factory_girl>, [">= 0"])
115
+ s.add_dependency(%q<simplecov>, [">= 0"])
116
+ s.add_dependency(%q<faker>, [">= 0"])
117
+ s.add_dependency(%q<guard>, [">= 0"])
118
+ s.add_dependency(%q<guard-rspec>, [">= 0"])
119
+ s.add_dependency(%q<mocha>, [">= 0"])
120
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
121
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
122
+ s.add_dependency(%q<rcov>, [">= 0"])
123
+ end
124
+ end
125
+