activeresource 3.2.22.5 → 5.1.1

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 (33) hide show
  1. checksums.yaml +5 -5
  2. data/MIT-LICENSE +2 -2
  3. data/README.rdoc +147 -49
  4. data/lib/active_resource.rb +12 -12
  5. data/lib/active_resource/active_job_serializer.rb +26 -0
  6. data/lib/active_resource/associations.rb +175 -0
  7. data/lib/active_resource/associations/builder/association.rb +33 -0
  8. data/lib/active_resource/associations/builder/belongs_to.rb +16 -0
  9. data/lib/active_resource/associations/builder/has_many.rb +14 -0
  10. data/lib/active_resource/associations/builder/has_one.rb +14 -0
  11. data/lib/active_resource/base.rb +444 -231
  12. data/lib/active_resource/callbacks.rb +22 -0
  13. data/lib/active_resource/collection.rb +94 -0
  14. data/lib/active_resource/connection.rb +112 -105
  15. data/lib/active_resource/custom_methods.rb +24 -14
  16. data/lib/active_resource/exceptions.rb +5 -3
  17. data/lib/active_resource/formats.rb +5 -3
  18. data/lib/active_resource/formats/json_format.rb +4 -1
  19. data/lib/active_resource/formats/xml_format.rb +4 -2
  20. data/lib/active_resource/http_mock.rb +69 -31
  21. data/lib/active_resource/log_subscriber.rb +14 -3
  22. data/lib/active_resource/observing.rb +0 -29
  23. data/lib/active_resource/railtie.rb +14 -3
  24. data/lib/active_resource/reflection.rb +78 -0
  25. data/lib/active_resource/schema.rb +4 -4
  26. data/lib/active_resource/singleton.rb +113 -0
  27. data/lib/active_resource/threadsafe_attributes.rb +66 -0
  28. data/lib/active_resource/validations.rb +56 -14
  29. data/lib/active_resource/version.rb +7 -5
  30. data/lib/activeresource.rb +3 -0
  31. metadata +78 -16
  32. data/CHANGELOG.md +0 -437
  33. data/examples/performance.rb +0 -70
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/wrap"
4
+
5
+ module ActiveResource
6
+ module Callbacks
7
+ extend ActiveSupport::Concern
8
+
9
+ CALLBACKS = [
10
+ :before_validation, :after_validation, :before_save, :around_save, :after_save,
11
+ :before_create, :around_create, :after_create, :before_update, :around_update,
12
+ :after_update, :before_destroy, :around_destroy, :after_destroy
13
+ ]
14
+
15
+ included do
16
+ extend ActiveModel::Callbacks
17
+ include ActiveModel::Validations::Callbacks
18
+
19
+ define_model_callbacks :save, :create, :update, :destroy
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/delegation"
4
+ require "active_support/inflector"
5
+
6
+ module ActiveResource # :nodoc:
7
+ class Collection # :nodoc:
8
+ SELF_DEFINE_METHODS = [:to_a, :collect!, :map!]
9
+ include Enumerable
10
+ delegate :to_yaml, :all?, *(Array.instance_methods(false) - SELF_DEFINE_METHODS), to: :to_a
11
+
12
+ # The array of actual elements returned by index actions
13
+ attr_accessor :elements, :resource_class, :original_params
14
+
15
+ # ActiveResource::Collection is a wrapper to handle parsing index responses that
16
+ # do not directly map to Rails conventions.
17
+ #
18
+ # You can define a custom class that inherets from ActiveResource::Collection
19
+ # in order to to set the elements instance.
20
+ #
21
+ # GET /posts.json delivers following response body:
22
+ # {
23
+ # posts: [
24
+ # {
25
+ # title: "ActiveResource now has associations",
26
+ # body: "Lorem Ipsum"
27
+ # },
28
+ # {...}
29
+ # ],
30
+ # next_page: "/posts.json?page=2"
31
+ # }
32
+ #
33
+ # A Post class can be setup to handle it with:
34
+ #
35
+ # class Post < ActiveResource::Base
36
+ # self.site = "http://example.com"
37
+ # self.collection_parser = PostCollection
38
+ # end
39
+ #
40
+ # And the collection parser:
41
+ #
42
+ # class PostCollection < ActiveResource::Collection
43
+ # attr_accessor :next_page
44
+ # def initialize(parsed = {})
45
+ # @elements = parsed['posts']
46
+ # @next_page = parsed['next_page']
47
+ # end
48
+ # end
49
+ #
50
+ # The result from a find method that returns multiple entries will now be a
51
+ # PostParser instance. ActiveResource::Collection includes Enumerable and
52
+ # instances can be iterated over just like an array.
53
+ # @posts = Post.find(:all) # => PostCollection:xxx
54
+ # @posts.next_page # => "/posts.json?page=2"
55
+ # @posts.map(&:id) # =>[1, 3, 5 ...]
56
+ #
57
+ # The initialize method will receive the ActiveResource::Formats parsed result
58
+ # and should set @elements.
59
+ def initialize(elements = [])
60
+ @elements = elements
61
+ end
62
+
63
+ def to_a
64
+ elements
65
+ end
66
+
67
+ def collect!
68
+ return elements unless block_given?
69
+ set = []
70
+ each { |o| set << yield(o) }
71
+ @elements = set
72
+ self
73
+ end
74
+ alias map! collect!
75
+
76
+ def first_or_create(attributes = {})
77
+ first || resource_class.create(original_params.update(attributes))
78
+ rescue NoMethodError
79
+ raise "Cannot create resource from resource type: #{resource_class.inspect}"
80
+ end
81
+
82
+ def first_or_initialize(attributes = {})
83
+ first || resource_class.new(original_params.update(attributes))
84
+ rescue NoMethodError
85
+ raise "Cannot build resource from resource type: #{resource_class.inspect}"
86
+ end
87
+
88
+ def where(clauses = {})
89
+ raise ArgumentError, "expected a clauses Hash, got #{clauses.inspect}" unless clauses.is_a? Hash
90
+ new_clauses = original_params.merge(clauses)
91
+ resource_class.where(new_clauses)
92
+ end
93
+ end
94
+ end
@@ -1,26 +1,28 @@
1
- require 'active_support/core_ext/benchmark'
2
- require 'active_support/core_ext/uri'
3
- require 'active_support/core_ext/object/inclusion'
4
- require 'net/https'
5
- require 'date'
6
- require 'time'
7
- require 'uri'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/benchmark"
4
+ require "active_support/core_ext/uri"
5
+ require "active_support/core_ext/object/inclusion"
6
+ require "net/https"
7
+ require "date"
8
+ require "time"
9
+ require "uri"
8
10
 
9
11
  module ActiveResource
10
12
  # Class to handle connections to remote web services.
11
13
  # This class is used by ActiveResource::Base to interface with REST
12
14
  # services.
13
15
  class Connection
14
-
15
- HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept',
16
- :put => 'Content-Type',
17
- :post => 'Content-Type',
18
- :delete => 'Accept',
19
- :head => 'Accept'
16
+ HTTP_FORMAT_HEADER_NAMES = { get: "Accept",
17
+ put: "Content-Type",
18
+ post: "Content-Type",
19
+ patch: "Content-Type",
20
+ delete: "Accept",
21
+ head: "Accept"
20
22
  }
21
23
 
22
- attr_reader :site, :user, :password, :auth_type, :timeout, :proxy, :ssl_options
23
- attr_accessor :format
24
+ attr_reader :site, :user, :password, :bearer_token, :auth_type, :timeout, :open_timeout, :read_timeout, :proxy, :ssl_options
25
+ attr_accessor :format, :logger
24
26
 
25
27
  class << self
26
28
  def requests
@@ -30,34 +32,35 @@ module ActiveResource
30
32
 
31
33
  # The +site+ parameter is required and will set the +site+
32
34
  # attribute to the URI for the remote resource service.
33
- def initialize(site, format = ActiveResource::Formats::JsonFormat)
34
- raise ArgumentError, 'Missing site URI' unless site
35
- @user = @password = nil
35
+ def initialize(site, format = ActiveResource::Formats::JsonFormat, logger: nil)
36
+ raise ArgumentError, "Missing site URI" unless site
37
+ @proxy = @user = @password = @bearer_token = nil
36
38
  self.site = site
37
39
  self.format = format
40
+ self.logger = logger
38
41
  end
39
42
 
40
43
  # Set URI for remote service.
41
44
  def site=(site)
42
- @site = site.is_a?(URI) ? site : URI.parser.parse(site)
45
+ @site = site.is_a?(URI) ? site : URI.parse(site)
46
+ @ssl_options ||= {} if @site.is_a?(URI::HTTPS)
43
47
  @user = URI.parser.unescape(@site.user) if @site.user
44
48
  @password = URI.parser.unescape(@site.password) if @site.password
45
49
  end
46
50
 
47
51
  # Set the proxy for remote service.
48
52
  def proxy=(proxy)
49
- @proxy = proxy.is_a?(URI) ? proxy : URI.parser.parse(proxy)
53
+ @proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
50
54
  end
51
55
 
52
56
  # Sets the user for remote service.
53
- def user=(user)
54
- @user = user
55
- end
57
+ attr_writer :user
56
58
 
57
59
  # Sets the password for remote service.
58
- def password=(password)
59
- @password = password
60
- end
60
+ attr_writer :password
61
+
62
+ # Sets the bearer token for remote service.
63
+ attr_writer :bearer_token
61
64
 
62
65
  # Sets the auth type for remote service.
63
66
  def auth_type=(auth_type)
@@ -65,14 +68,16 @@ module ActiveResource
65
68
  end
66
69
 
67
70
  # Sets the number of seconds after which HTTP requests to the remote service should time out.
68
- def timeout=(timeout)
69
- @timeout = timeout
70
- end
71
+ attr_writer :timeout
72
+
73
+ # Sets the number of seconds after which HTTP connects to the remote service should time out.
74
+ attr_writer :open_timeout
75
+
76
+ # Sets the number of seconds after which HTTP read requests to the remote service should time out.
77
+ attr_writer :read_timeout
71
78
 
72
79
  # Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
73
- def ssl_options=(opts={})
74
- @ssl_options = opts
75
- end
80
+ attr_writer :ssl_options
76
81
 
77
82
  # Executes a GET request.
78
83
  # Used to get (find) resources.
@@ -86,15 +91,21 @@ module ActiveResource
86
91
  with_auth { request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path))) }
87
92
  end
88
93
 
94
+ # Executes a PATCH request (see HTTP protocol documentation if unfamiliar).
95
+ # Used to update resources.
96
+ def patch(path, body = "", headers = {})
97
+ with_auth { request(:patch, path, body.to_s, build_request_headers(headers, :patch, self.site.merge(path))) }
98
+ end
99
+
89
100
  # Executes a PUT request (see HTTP protocol documentation if unfamiliar).
90
101
  # Used to update resources.
91
- def put(path, body = '', headers = {})
102
+ def put(path, body = "", headers = {})
92
103
  with_auth { request(:put, path, body.to_s, build_request_headers(headers, :put, self.site.merge(path))) }
93
104
  end
94
105
 
95
106
  # Executes a POST request.
96
107
  # Used to create new resources.
97
- def post(path, body = '', headers = {})
108
+ def post(path, body = "", headers = {})
98
109
  with_auth { request(:post, path, body.to_s, build_request_headers(headers, :post, self.site.merge(path))) }
99
110
  end
100
111
 
@@ -122,32 +133,32 @@ module ActiveResource
122
133
  # Handles response and error codes from the remote service.
123
134
  def handle_response(response)
124
135
  case response.code.to_i
125
- when 301, 302, 303, 307
126
- raise(Redirection.new(response))
127
- when 200...400
128
- response
129
- when 400
130
- raise(BadRequest.new(response))
131
- when 401
132
- raise(UnauthorizedAccess.new(response))
133
- when 403
134
- raise(ForbiddenAccess.new(response))
135
- when 404
136
- raise(ResourceNotFound.new(response))
137
- when 405
138
- raise(MethodNotAllowed.new(response))
139
- when 409
140
- raise(ResourceConflict.new(response))
141
- when 410
142
- raise(ResourceGone.new(response))
143
- when 422
144
- raise(ResourceInvalid.new(response))
145
- when 401...500
146
- raise(ClientError.new(response))
147
- when 500...600
148
- raise(ServerError.new(response))
149
- else
150
- raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
136
+ when 301, 302, 303, 307
137
+ raise(Redirection.new(response))
138
+ when 200...400
139
+ response
140
+ when 400
141
+ raise(BadRequest.new(response))
142
+ when 401
143
+ raise(UnauthorizedAccess.new(response))
144
+ when 403
145
+ raise(ForbiddenAccess.new(response))
146
+ when 404
147
+ raise(ResourceNotFound.new(response))
148
+ when 405
149
+ raise(MethodNotAllowed.new(response))
150
+ when 409
151
+ raise(ResourceConflict.new(response))
152
+ when 410
153
+ raise(ResourceGone.new(response))
154
+ when 422
155
+ raise(ResourceInvalid.new(response))
156
+ when 401...500
157
+ raise(ClientError.new(response))
158
+ when 500...600
159
+ raise(ServerError.new(response))
160
+ else
161
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
151
162
  end
152
163
  end
153
164
 
@@ -159,45 +170,36 @@ module ActiveResource
159
170
 
160
171
  def new_http
161
172
  if @proxy
162
- Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
173
+ user = URI.parser.unescape(@proxy.user) if @proxy.user
174
+ password = URI.parser.unescape(@proxy.password) if @proxy.password
175
+ Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, user, password)
163
176
  else
164
177
  Net::HTTP.new(@site.host, @site.port)
165
178
  end
166
179
  end
167
180
 
168
181
  def configure_http(http)
169
- http = apply_ssl_options(http)
170
-
171
- # Net::HTTP timeouts default to 60 seconds.
172
- if @timeout
173
- http.open_timeout = @timeout
174
- http.read_timeout = @timeout
182
+ apply_ssl_options(http).tap do |https|
183
+ # Net::HTTP timeouts default to 60 seconds.
184
+ if defined? @timeout
185
+ https.open_timeout = @timeout
186
+ https.read_timeout = @timeout
187
+ end
188
+ https.open_timeout = @open_timeout if defined?(@open_timeout)
189
+ https.read_timeout = @read_timeout if defined?(@read_timeout)
175
190
  end
176
-
177
- http
178
191
  end
179
192
 
180
193
  def apply_ssl_options(http)
181
- return http unless @site.is_a?(URI::HTTPS)
182
-
183
- http.use_ssl = true
184
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
185
- return http unless defined?(@ssl_options)
194
+ http.tap do |https|
195
+ # Skip config if site is already a https:// URI.
196
+ if defined? @ssl_options
197
+ http.use_ssl = true
186
198
 
187
- http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path]
188
- http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file]
189
-
190
- http.cert = @ssl_options[:cert] if @ssl_options[:cert]
191
- http.key = @ssl_options[:key] if @ssl_options[:key]
192
-
193
- http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store]
194
- http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout]
195
-
196
- http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode]
197
- http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback]
198
- http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth]
199
-
200
- http
199
+ # All the SSL options have corresponding http settings.
200
+ @ssl_options.each { |key, value| http.send "#{key}=", value }
201
+ end
202
+ end
201
203
  end
202
204
 
203
205
  def default_header
@@ -218,7 +220,7 @@ module ActiveResource
218
220
  yield
219
221
  rescue UnauthorizedAccess => e
220
222
  raise if retried || auth_type != :digest
221
- @response_auth_header = e.response['WWW-Authenticate']
223
+ @response_auth_header = e.response["WWW-Authenticate"]
222
224
  retried = true
223
225
  retry
224
226
  end
@@ -226,10 +228,12 @@ module ActiveResource
226
228
  def authorization_header(http_method, uri)
227
229
  if @user || @password
228
230
  if auth_type == :digest
229
- { 'Authorization' => digest_auth_header(http_method, uri) }
231
+ { "Authorization" => digest_auth_header(http_method, uri) }
230
232
  else
231
- { 'Authorization' => 'Basic ' + ["#{@user}:#{@password}"].pack('m').delete("\r\n") }
233
+ { "Authorization" => "Basic " + ["#{@user}:#{@password}"].pack("m").delete("\r\n") }
232
234
  end
235
+ elsif @bearer_token
236
+ { "Authorization" => "Bearer #{@bearer_token}" }
233
237
  else
234
238
  {}
235
239
  end
@@ -244,8 +248,8 @@ module ActiveResource
244
248
  ha1 = Digest::MD5.hexdigest("#{@user}:#{params['realm']}:#{@password}")
245
249
  ha2 = Digest::MD5.hexdigest("#{http_method.to_s.upcase}:#{request_uri}")
246
250
 
247
- params.merge!('cnonce' => client_nonce)
248
- request_digest = Digest::MD5.hexdigest([ha1, params['nonce'], "0", params['cnonce'], params['qop'], ha2].join(":"))
251
+ params["cnonce"] = client_nonce
252
+ request_digest = Digest::MD5.hexdigest([ha1, params["nonce"], "0", params["cnonce"], params["qop"], ha2].join(":"))
249
253
  "Digest #{auth_attributes_for(uri, request_digest, params)}"
250
254
  end
251
255
 
@@ -262,26 +266,29 @@ module ActiveResource
262
266
  end
263
267
 
264
268
  def auth_attributes_for(uri, request_digest, params)
265
- [
266
- %Q(username="#{@user}"),
267
- %Q(realm="#{params['realm']}"),
268
- %Q(qop="#{params['qop']}"),
269
- %Q(uri="#{uri.path}"),
270
- %Q(nonce="#{params['nonce']}"),
271
- %Q(nc="0"),
272
- %Q(cnonce="#{params['cnonce']}"),
273
- %Q(opaque="#{params['opaque']}"),
274
- %Q(response="#{request_digest}")].join(", ")
269
+ auth_attrs =
270
+ [
271
+ %Q(username="#{@user}"),
272
+ %Q(realm="#{params['realm']}"),
273
+ %Q(qop="#{params['qop']}"),
274
+ %Q(uri="#{uri.path}"),
275
+ %Q(nonce="#{params['nonce']}"),
276
+ 'nc="0"',
277
+ %Q(cnonce="#{params['cnonce']}"),
278
+ %Q(response="#{request_digest}")]
279
+
280
+ auth_attrs << %Q(opaque="#{params['opaque']}") unless params["opaque"].blank?
281
+ auth_attrs.join(", ")
275
282
  end
276
283
 
277
284
  def http_format_header(http_method)
278
- {HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
285
+ { HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type }
279
286
  end
280
287
 
281
288
  def legitimize_auth_type(auth_type)
282
289
  return :basic if auth_type.nil?
283
290
  auth_type = auth_type.to_sym
284
- auth_type.in?([:basic, :digest]) ? auth_type : :basic
291
+ auth_type.in?([:basic, :digest, :bearer]) ? auth_type : :basic
285
292
  end
286
293
  end
287
294
  end