rpush 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +11 -7
  4. data/lib/generators/templates/rpush.rb +8 -2
  5. data/lib/generators/templates/rpush_2_0_0_updates.rb +1 -1
  6. data/lib/rpush/cli.rb +61 -27
  7. data/lib/rpush/client/active_model.rb +3 -0
  8. data/lib/rpush/client/active_model/apns/notification.rb +1 -1
  9. data/lib/rpush/client/active_model/wns/app.rb +23 -0
  10. data/lib/rpush/client/active_model/wns/notification.rb +28 -0
  11. data/lib/rpush/client/active_model/wpns/notification.rb +11 -6
  12. data/lib/rpush/client/active_record.rb +3 -0
  13. data/lib/rpush/client/active_record/wns/app.rb +11 -0
  14. data/lib/rpush/client/active_record/wns/notification.rb +11 -0
  15. data/lib/rpush/client/mongoid.rb +3 -0
  16. data/lib/rpush/client/mongoid/apns/feedback.rb +3 -0
  17. data/lib/rpush/client/mongoid/notification.rb +6 -0
  18. data/lib/rpush/client/mongoid/wns/app.rb +14 -0
  19. data/lib/rpush/client/mongoid/wns/notification.rb +11 -0
  20. data/lib/rpush/client/redis.rb +3 -0
  21. data/lib/rpush/client/redis/wns/app.rb +14 -0
  22. data/lib/rpush/client/redis/wns/notification.rb +11 -0
  23. data/lib/rpush/configuration.rb +3 -7
  24. data/lib/rpush/daemon.rb +9 -0
  25. data/lib/rpush/daemon/apns/feedback_receiver.rb +5 -0
  26. data/lib/rpush/daemon/app_runner.rb +4 -5
  27. data/lib/rpush/daemon/dispatcher/apns_tcp.rb +47 -12
  28. data/lib/rpush/daemon/dispatcher_loop.rb +5 -0
  29. data/lib/rpush/daemon/feeder.rb +11 -0
  30. data/lib/rpush/daemon/interruptible_sleep.rb +8 -3
  31. data/lib/rpush/daemon/loggable.rb +4 -0
  32. data/lib/rpush/daemon/rpc.rb +9 -0
  33. data/lib/rpush/daemon/rpc/client.rb +27 -0
  34. data/lib/rpush/daemon/rpc/server.rb +82 -0
  35. data/lib/rpush/daemon/signal_handler.rb +7 -0
  36. data/lib/rpush/daemon/store/active_record.rb +17 -3
  37. data/lib/rpush/daemon/store/mongoid.rb +2 -2
  38. data/lib/rpush/daemon/store/redis.rb +2 -2
  39. data/lib/rpush/daemon/tcp_connection.rb +2 -2
  40. data/lib/rpush/daemon/wns.rb +9 -0
  41. data/lib/rpush/daemon/wns/delivery.rb +206 -0
  42. data/lib/rpush/embed.rb +15 -13
  43. data/lib/rpush/logger.rb +4 -0
  44. data/lib/rpush/plugin.rb +1 -1
  45. data/lib/rpush/push.rb +2 -11
  46. data/lib/rpush/reflection_collection.rb +15 -17
  47. data/lib/rpush/reflection_public_methods.rb +6 -4
  48. data/lib/rpush/version.rb +1 -1
  49. data/spec/functional/apns_spec.rb +1 -11
  50. data/spec/functional/cli_spec.rb +35 -0
  51. data/spec/functional_spec_helper.rb +11 -1
  52. data/spec/spec_helper.rb +4 -3
  53. data/spec/support/active_record_setup.rb +1 -1
  54. data/spec/unit/client/active_record/apns/notification_spec.rb +1 -1
  55. data/spec/unit/configuration_spec.rb +0 -7
  56. data/spec/unit/daemon/adm/delivery_spec.rb +2 -2
  57. data/spec/unit/daemon/app_runner_spec.rb +2 -3
  58. data/spec/unit/daemon/gcm/delivery_spec.rb +1 -1
  59. data/spec/unit/daemon/tcp_connection_spec.rb +1 -1
  60. data/spec/unit/daemon/wns/delivery_spec.rb +171 -0
  61. data/spec/unit/daemon/wpns/delivery_spec.rb +1 -1
  62. data/spec/unit/daemon_spec.rb +2 -0
  63. data/spec/unit/embed_spec.rb +4 -11
  64. data/spec/unit/logger_spec.rb +2 -2
  65. data/spec/unit/push_spec.rb +0 -7
  66. data/spec/unit_spec_helper.rb +1 -1
  67. metadata +20 -3
@@ -1,6 +1,8 @@
1
1
  module Rpush
2
2
  module Daemon
3
3
  class SignalHandler
4
+ extend Loggable
5
+
4
6
  class << self
5
7
  attr_reader :thread
6
8
  end
@@ -18,6 +20,11 @@ module Rpush
18
20
  def self.stop
19
21
  @write_io.puts('break') if @write_io
20
22
  @thread.join if @thread
23
+ rescue StandardError => e
24
+ log_error(e)
25
+ reflect(:error, e)
26
+ ensure
27
+ @thread = nil
21
28
  end
22
29
 
23
30
  def self.start_handler(read_io)
@@ -11,7 +11,8 @@ module Rpush
11
11
  DEFAULT_MARK_OPTIONS = { persist: true }
12
12
 
13
13
  def initialize
14
- reopen_log
14
+ @using_oracle = adapter_name =~ /oracle/
15
+ reopen_log unless Rpush.config.embedded
15
16
  end
16
17
 
17
18
  def reopen_log
@@ -31,7 +32,7 @@ module Rpush
31
32
  Rpush::Client::ActiveRecord::Notification.transaction do
32
33
  relation = ready_for_delivery
33
34
  relation = relation.limit(limit)
34
- notifications = relation.lock(true).to_a
35
+ notifications = claim(relation)
35
36
  mark_processing(notifications)
36
37
  notifications
37
38
  end
@@ -188,7 +189,8 @@ module Rpush
188
189
  end
189
190
 
190
191
  def ready_for_delivery
191
- Rpush::Client::ActiveRecord::Notification.where('processing = ? AND delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', false, false, false, Time.now).order('created_at ASC')
192
+ relation = Rpush::Client::ActiveRecord::Notification.where('processing = ? AND delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', false, false, false, Time.now)
193
+ @using_oracle ? relation : relation.order('created_at ASC')
192
194
  end
193
195
 
194
196
  def mark_processing(notifications)
@@ -201,6 +203,18 @@ module Rpush
201
203
  end
202
204
  Rpush::Client::ActiveRecord::Notification.where(id: ids).update_all(['processing = ?', true])
203
205
  end
206
+
207
+ def claim(relation)
208
+ notifications = relation.lock(true).to_a
209
+ @using_oracle ? notification.sort_by(&:created_at) : notifications
210
+ end
211
+
212
+ def adapter_name
213
+ env = (defined?(Rails) && Rails.env) ? Rails.env : 'development'
214
+ config = ::ActiveRecord::Base.configurations[env]
215
+ return '' unless config
216
+ Hash[config.map { |k, v| [k.to_sym, v] }][:adapter]
217
+ end
204
218
  end
205
219
  end
206
220
  end
@@ -96,12 +96,12 @@ module Rpush
96
96
  Rpush::Client::Mongoid::Apns::Feedback.create!(failed_at: failed_at, device_token: device_token, app: app)
97
97
  end
98
98
 
99
- def create_gcm_notification(attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
99
+ def create_gcm_notification(attrs, data, registration_ids, deliver_after, app)
100
100
  notification = Rpush::Client::Mongoid::Gcm::Notification.new
101
101
  create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
102
102
  end
103
103
 
104
- def create_adm_notification(attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
104
+ def create_adm_notification(attrs, data, registration_ids, deliver_after, app)
105
105
  notification = Rpush::Client::Mongoid::Adm::Notification.new
106
106
  create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
107
107
  end
@@ -82,12 +82,12 @@ module Rpush
82
82
  Rpush::Client::Redis::Apns::Feedback.create!(failed_at: failed_at, device_token: device_token, app_id: app.id)
83
83
  end
84
84
 
85
- def create_gcm_notification(attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
85
+ def create_gcm_notification(attrs, data, registration_ids, deliver_after, app)
86
86
  notification = Rpush::Client::Redis::Gcm::Notification.new
87
87
  create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
88
88
  end
89
89
 
90
- def create_adm_notification(attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
90
+ def create_adm_notification(attrs, data, registration_ids, deliver_after, app)
91
91
  notification = Rpush::Client::Redis::Adm::Notification.new
92
92
  create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
93
93
  end
@@ -156,7 +156,7 @@ module Rpush
156
156
  [tcp_socket, ssl_socket]
157
157
  rescue *TCP_ERRORS => error
158
158
  if error.message =~ /certificate revoked/i
159
- log_warn('Certificate has been revoked.')
159
+ log_error('Certificate has been revoked.')
160
160
  reflect(:ssl_certificate_revoked, @app, error)
161
161
  end
162
162
  raise TcpConnectionError, "#{error.class.name}, #{error.message}"
@@ -166,7 +166,7 @@ module Rpush
166
166
  cert = @ssl_context.cert
167
167
  if certificate_expired?
168
168
  log_error(certificate_msg('expired'))
169
- fail Rpush::CertificateExpiredError.new(@app, cert.not_after)
169
+ raise Rpush::CertificateExpiredError.new(@app, cert.not_after)
170
170
  elsif certificate_expires_soon?
171
171
  log_warn(certificate_msg('will expire'))
172
172
  reflect(:ssl_certificate_will_expire, @app, cert.not_after)
@@ -0,0 +1,9 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Wns
4
+ extend ServiceConfigMethods
5
+
6
+ dispatcher :http
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,206 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Wns
4
+ # https://msdn.microsoft.com/en-us/library/windows/apps/hh465435.aspx
5
+ class Delivery < Rpush::Daemon::Delivery
6
+ # Oauth2.0 token endpoint. This endpoint is used to request authorization tokens.
7
+ WPN_TOKEN_URI = URI.parse('https://login.live.com/accesstoken.srf')
8
+
9
+ # Data used to request authorization tokens.
10
+ ACCESS_TOKEN_REQUEST_DATA = { "grant_type" => "client_credentials", "scope" => "notify.windows.com" }
11
+
12
+ MAX_RETRIES = 14
13
+
14
+ FAILURE_MESSAGES = {
15
+ 400 => 'One or more headers were specified incorrectly or conflict with another header.',
16
+ 401 => 'The cloud service did not present a valid authentication ticket. The OAuth ticket may be invalid.',
17
+ 403 => 'The cloud service is not authorized to send a notification to this URI even though they are authenticated.',
18
+ 404 => 'The channel URI is not valid or is not recognized by WNS.',
19
+ 405 => 'Invalid method (GET, CREATE); only POST (Windows or Windows Phone) or DELETE (Windows Phone only) is allowed.',
20
+ 406 => 'The cloud service exceeded its throttle limit.',
21
+ 410 => 'The channel expired.',
22
+ 413 => 'The notification payload exceeds the 5000 byte size limit.',
23
+ 500 => 'An internal failure caused notification delivery to fail.',
24
+ 503 => 'The server is currently unavailable.'
25
+ }
26
+
27
+ def initialize(app, http, notification, batch)
28
+ @app = app
29
+ @http = http
30
+ @notification = notification
31
+ @batch = batch
32
+ end
33
+
34
+ def perform
35
+ handle_response(do_post)
36
+ rescue SocketError => error
37
+ mark_retryable(@notification, Time.now + 10.seconds, error)
38
+ raise
39
+ rescue StandardError => error
40
+ mark_failed(error)
41
+ raise
42
+ ensure
43
+ @batch.notification_processed
44
+ end
45
+
46
+ private
47
+
48
+ def handle_response(response)
49
+ code = response.code.to_i
50
+ case code
51
+ when 200
52
+ ok(response)
53
+ when 401
54
+ unauthorized
55
+ when 404
56
+ invalid_channel(code)
57
+ when 406
58
+ not_acceptable
59
+ when 410
60
+ invalid_channel(code)
61
+ when 412
62
+ precondition_failed
63
+ when 503
64
+ service_unavailable
65
+ else
66
+ handle_failure(code)
67
+ end
68
+ end
69
+
70
+ def handle_failure(code, msg = nil)
71
+ unless msg
72
+ msg = FAILURE_MESSAGES.key?(code) ? FAILURE_MESSAGES[code] : Rpush::Daemon::HTTP_STATUS_CODES[code]
73
+ end
74
+ fail Rpush::DeliveryError.new(code, @notification.id, msg)
75
+ end
76
+
77
+ def ok(response)
78
+ status = status_from_response(response)
79
+ case status[:notification]
80
+ when ["received"]
81
+ mark_delivered
82
+ log_info("#{@notification.id} sent successfully")
83
+ when ["channelthrottled"]
84
+ mark_retryable(@notification, Time.now + (60 * 10))
85
+ log_warn("#{@notification.id} cannot be sent. The Queue is full.")
86
+ when ["dropped"]
87
+ log_error("#{@notification.id} was dropped. Headers: #{status}")
88
+ handle_failure(200, "Notification was received but suppressed by the service (#{status[:error_description]}).")
89
+ end
90
+ end
91
+
92
+ def unauthorized
93
+ @notification.app.access_token = nil
94
+ Rpush::Daemon.store.update_app(@notification.app)
95
+ if @notification.retries < MAX_RETRIES
96
+ retry_notification("Token invalid.")
97
+ else
98
+ msg = "Notification failed to be delivered in #{MAX_RETRIES} retries."
99
+ mark_failed(Rpush::DeliveryError.new(nil, @notification.id, msg))
100
+ end
101
+ end
102
+
103
+ def invalid_channel(code, msg = nil)
104
+ unless msg
105
+ msg = FAILURE_MESSAGES.key?(code) ? FAILURE_MESSAGES[code] : Rpush::Daemon::HTTP_STATUS_CODES[code]
106
+ end
107
+ reflect(:wns_invalid_channel, @notification, @notification.uri, "#{code}. #{msg}")
108
+ handle_failure(code, msg)
109
+ end
110
+
111
+ def not_acceptable
112
+ retry_notification("Per-day throttling limit reached.")
113
+ end
114
+
115
+ def precondition_failed
116
+ retry_notification("Device unreachable.")
117
+ end
118
+
119
+ def service_unavailable
120
+ mark_retryable_exponential(@notification)
121
+ log_warn("Service Unavailable. " + retry_message)
122
+ end
123
+
124
+ def retry_message
125
+ "Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime('%Y-%m-%d %H:%M:%S')} (retry #{@notification.retries})."
126
+ end
127
+
128
+ def retry_notification(reason)
129
+ deliver_after = Time.now + (60 * 60)
130
+ mark_retryable(@notification, deliver_after)
131
+ log_warn("#{reason} " + retry_message)
132
+ end
133
+
134
+ def do_post
135
+ body = notification_to_xml
136
+ uri = URI.parse(@notification.uri)
137
+ post = Net::HTTP::Post.new(uri.request_uri,
138
+ "Content-Length" => body.length.to_s,
139
+ "Content-Type" => "text/xml",
140
+ "X-WNS-Type" => "wns/toast",
141
+ "X-WNS-RequestForStatus" => "true",
142
+ "Authorization" => "Bearer #{access_token}")
143
+ post.body = body
144
+ @http.request(URI.parse(@notification.uri), post)
145
+ end
146
+
147
+ def status_from_response(response)
148
+ headers = response.to_hash
149
+ {
150
+ notification: headers["X-WNS-Status"],
151
+ device_connection: headers["X-WNS-DeviceConnectionStatus"],
152
+ msg_id: headers["X-WNS-Msg-ID"],
153
+ error_description: headers["X-WNS-Error-Description"],
154
+ debug_trace: headers["X-WNS-Debug-Trace"]
155
+ }
156
+ end
157
+
158
+ def notification_to_xml
159
+ title = clean_param_string(@notification.data['title']) if @notification.data['title'].present?
160
+ body = clean_param_string(@notification.data['body']) if @notification.data['body'].present?
161
+ param = clean_param_string(@notification.data['param']) if @notification.data['param'].present?
162
+ "<toast>
163
+ <visual version='1' lang='en-US'>
164
+ <binding template='ToastText02'>
165
+ <text id='1'>#{title}</text>
166
+ <text id='2'>#{body}</text>
167
+ <param>#{param}</param>
168
+ </binding>
169
+ </visual>
170
+ </toast>"
171
+ end
172
+
173
+ def clean_param_string(string)
174
+ string.gsub(/&/, "&amp;").gsub(/</, "&lt;") \
175
+ .gsub(/>/, "&gt;").gsub(/'/, "&apos;").gsub(/"/, "&quot;")
176
+ end
177
+
178
+ def access_token
179
+ if @notification.app.access_token.nil? || @notification.app.access_token_expired?
180
+ post = Net::HTTP::Post.new(WPN_TOKEN_URI.path, 'Content-Type' => 'application/x-www-form-urlencoded')
181
+ post.set_form_data(ACCESS_TOKEN_REQUEST_DATA.merge('client_id' => @notification.app.client_id, 'client_secret' => @notification.app.client_secret))
182
+
183
+ handle_access_token(@http.request(WPN_TOKEN_URI, post))
184
+ end
185
+
186
+ @notification.app.access_token
187
+ end
188
+
189
+ def handle_access_token(response)
190
+ if response.code.to_i == 200
191
+ update_access_token(JSON.parse(response.body))
192
+ Rpush::Daemon.store.update_app(@notification.app)
193
+ log_info("WNS access token updated: token = #{@notification.app.access_token}, expires = #{@notification.app.access_token_expiration}")
194
+ else
195
+ log_warn("Could not retrieve access token from WNS: #{response.body}")
196
+ end
197
+ end
198
+
199
+ def update_access_token(data)
200
+ @notification.app.access_token = data['access_token']
201
+ @notification.app.access_token_expiration = Time.now + data['expires_in'].to_i
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
data/lib/rpush/embed.rb CHANGED
@@ -1,21 +1,13 @@
1
1
  module Rpush
2
- def self.embed(options = {})
2
+ def self.embed
3
3
  require 'rpush/daemon'
4
4
 
5
- unless options.empty?
6
- warning = "Passing configuration options directly to Rpush.embed is deprecated and will be removed from Rpush 2.5.0. Please setup configuration using Rpush.configure { |config| ... } before calling embed."
7
- Rpush::Deprecation.warn_with_backtrace(warning)
8
- end
9
-
10
5
  if @embed_thread
11
6
  STDERR.puts 'Rpush.embed can only be run once inside this process.'
12
7
  end
13
8
 
14
- config = Rpush::ConfigurationWithoutDefaults.new
15
- options.each { |k, v| config.send("#{k}=", v) }
16
- config.embedded = true
17
- config.foreground = true
18
- Rpush.config.update(config)
9
+ Rpush.config.embedded = true
10
+ Rpush.config.foreground = true
19
11
  Kernel.at_exit { shutdown }
20
12
  @embed_thread = Thread.new { Rpush::Daemon.start }
21
13
  end
@@ -24,6 +16,10 @@ module Rpush
24
16
  return unless Rpush.config.embedded
25
17
  Rpush::Daemon.shutdown
26
18
  @embed_thread.join if @embed_thread
19
+ rescue StandardError => e
20
+ STDERR.puts(e.message)
21
+ STDERR.puts(e.backtrace.join("\n"))
22
+ ensure
27
23
  @embed_thread = nil
28
24
  end
29
25
 
@@ -32,8 +28,14 @@ module Rpush
32
28
  Rpush::Daemon::Synchronizer.sync
33
29
  end
34
30
 
35
- def self.debug
31
+ def self.status
36
32
  return unless Rpush.config.embedded
37
- Rpush::Daemon::AppRunner.debug
33
+ status = Rpush::Daemon::AppRunner.status
34
+ Rpush.logger.info(JSON.pretty_generate(status))
35
+ status
36
+ end
37
+
38
+ def self.debug
39
+ status
38
40
  end
39
41
  end
data/lib/rpush/logger.rb CHANGED
@@ -11,6 +11,10 @@ module Rpush
11
11
  error('Logging disabled.')
12
12
  end
13
13
 
14
+ def debug(msg, inline = false)
15
+ log(:debug, msg, inline)
16
+ end
17
+
14
18
  def info(msg, inline = false)
15
19
  log(:info, msg, inline)
16
20
  end
data/lib/rpush/plugin.rb CHANGED
@@ -32,7 +32,7 @@ module Rpush
32
32
  Rpush.config.plugin.send("#{@name}=", @config)
33
33
  end
34
34
 
35
- def init(&block) # rubocop:disable Style/TrivialAccessors
35
+ def init(&block)
36
36
  @init_block = block
37
37
  end
38
38
 
data/lib/rpush/push.rb CHANGED
@@ -1,17 +1,8 @@
1
1
  module Rpush
2
- def self.push(options = {})
2
+ def self.push
3
3
  require 'rpush/daemon'
4
4
 
5
- unless options.empty?
6
- warning = "Passing configuration options directly to Rpush.push is deprecated and will be removed from Rpush 2.5.0. Please setup configuration using Rpush.configure { |config| ... } before calling push."
7
- Rpush::Deprecation.warn_with_backtrace(warning)
8
- end
9
-
10
- config = Rpush::ConfigurationWithoutDefaults.new
11
- options.each { |k, v| config.send("#{k}=", v) }
12
- config.push = true
13
- Rpush.config.update(config)
14
-
5
+ Rpush.config.push = true
15
6
  Rpush::Daemon.common_init
16
7
  Rpush::Daemon::Synchronizer.sync
17
8
  Rpush::Daemon::Feeder.start(true) # non-blocking
@@ -6,7 +6,7 @@ module Rpush
6
6
  :apns_feedback, :notification_enqueued, :notification_delivered,
7
7
  :notification_failed, :notification_will_retry, :gcm_delivered_to_recipient,
8
8
  :gcm_failed_to_recipient, :gcm_canonical_id, :gcm_invalid_registration_id,
9
- :error, :adm_canonical_id, :adm_failed_to_recipient,
9
+ :error, :adm_canonical_id, :adm_failed_to_recipient, :wns_invalid_channel,
10
10
  :tcp_connection_lost, :ssl_certificate_will_expire, :ssl_certificate_revoked,
11
11
  :notification_id_will_retry, :notification_id_failed
12
12
  ]
@@ -17,30 +17,28 @@ module Rpush
17
17
  class_eval(<<-RUBY, __FILE__, __LINE__)
18
18
  def #{reflection}(*args, &blk)
19
19
  raise "block required" unless block_given?
20
- reflections[:#{reflection}] = blk
20
+ @reflections[:#{reflection}] = blk
21
21
  end
22
22
  RUBY
23
23
  end
24
24
 
25
+ def initialize
26
+ @reflections = {}
27
+ end
28
+
25
29
  def __dispatch(reflection, *args)
26
- reflection = reflection.to_sym
30
+ blk = @reflections[reflection]
27
31
 
28
- unless REFLECTIONS.include?(reflection)
29
- fail NoSuchReflectionError, reflection
30
- end
32
+ if blk
33
+ blk.call(*args)
31
34
 
32
- if DEPRECATIONS.key?(reflection)
33
- replacement, removal_version = DEPRECATIONS[reflection]
34
- Rpush::Deprecation.warn("#{reflection} is deprecated and will be removed in version #{removal_version}. Use #{replacement} instead.")
35
+ if DEPRECATIONS.key?(reflection)
36
+ replacement, removal_version = DEPRECATIONS[reflection]
37
+ Rpush::Deprecation.warn("#{reflection} is deprecated and will be removed in version #{removal_version}. Use #{replacement} instead.")
38
+ end
39
+ elsif !REFLECTIONS.include?(reflection)
40
+ raise NoSuchReflectionError, reflection
35
41
  end
36
-
37
- reflections[reflection].call(*args) if reflections[reflection]
38
- end
39
-
40
- private
41
-
42
- def reflections
43
- @reflections ||= {}
44
42
  end
45
43
  end
46
44
  end