canvas_sync 0.12.0 → 0.13.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/canvas_sync.rb +16 -15
  4. data/lib/canvas_sync/api_syncable.rb +4 -162
  5. data/lib/canvas_sync/class_callback_executor.rb +35 -0
  6. data/lib/canvas_sync/concerns/account/ancestry.rb +60 -0
  7. data/lib/canvas_sync/concerns/api_syncable.rb +189 -0
  8. data/lib/canvas_sync/concerns/legacy_columns.rb +34 -0
  9. data/lib/canvas_sync/generators/templates/models/account.rb +7 -1
  10. data/lib/canvas_sync/generators/templates/models/admin.rb +2 -1
  11. data/lib/canvas_sync/generators/templates/models/assignment.rb +2 -1
  12. data/lib/canvas_sync/generators/templates/models/assignment_group.rb +2 -1
  13. data/lib/canvas_sync/generators/templates/models/context_module.rb +2 -1
  14. data/lib/canvas_sync/generators/templates/models/context_module_item.rb +2 -1
  15. data/lib/canvas_sync/generators/templates/models/course.rb +3 -2
  16. data/lib/canvas_sync/generators/templates/models/enrollment.rb +2 -1
  17. data/lib/canvas_sync/generators/templates/models/role.rb +2 -1
  18. data/lib/canvas_sync/generators/templates/models/section.rb +2 -1
  19. data/lib/canvas_sync/generators/templates/models/submission.rb +2 -1
  20. data/lib/canvas_sync/generators/templates/models/term.rb +2 -1
  21. data/lib/canvas_sync/generators/templates/models/user.rb +2 -1
  22. data/lib/canvas_sync/importers/bulk_importer.rb +7 -1
  23. data/lib/canvas_sync/importers/legacy_importer.rb +4 -2
  24. data/lib/canvas_sync/job.rb +3 -1
  25. data/lib/canvas_sync/job_chain.rb +57 -0
  26. data/lib/canvas_sync/jobs/sync_accounts_job.rb +31 -0
  27. data/lib/canvas_sync/record.rb +9 -0
  28. data/lib/canvas_sync/version.rb +1 -1
  29. data/spec/canvas_sync/canvas_sync_spec.rb +14 -14
  30. data/spec/dummy/app/models/account.rb +7 -1
  31. data/spec/dummy/app/models/admin.rb +2 -1
  32. data/spec/dummy/app/models/assignment.rb +2 -1
  33. data/spec/dummy/app/models/assignment_group.rb +2 -1
  34. data/spec/dummy/app/models/context_module.rb +2 -1
  35. data/spec/dummy/app/models/context_module_item.rb +2 -1
  36. data/spec/dummy/app/models/course.rb +3 -2
  37. data/spec/dummy/app/models/enrollment.rb +2 -1
  38. data/spec/dummy/app/models/role.rb +2 -1
  39. data/spec/dummy/app/models/section.rb +2 -1
  40. data/spec/dummy/app/models/submission.rb +2 -1
  41. data/spec/dummy/app/models/term.rb +2 -1
  42. data/spec/dummy/app/models/user.rb +2 -1
  43. data/spec/dummy/config/application.rb +12 -1
  44. data/spec/dummy/config/database.yml +11 -11
  45. data/spec/dummy/config/environments/development.rb +3 -3
  46. data/spec/dummy/config/initializers/assets.rb +1 -1
  47. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7795e4388557d88e9134d2c5ae6dee8770ba282f1dd72a10686aaa7974741c92
4
- data.tar.gz: db4251351aabda748429adb17bdcb3c3dbc383d0dc2a7a112c324d0e8e273067
3
+ metadata.gz: 5c8386afbd924c0148b5c2158d23ea7acd5b0b85349da51312103e1ea9368c54
4
+ data.tar.gz: 051471b40b5a464b66df8ca7ae482874f2791c55604e28f7fe9ce54cb07f875a
5
5
  SHA512:
6
- metadata.gz: 3babc49ea2a583a81560aed52f08dcf7c4a706b74123fb1128b04f77030c575c3764feda1f5aab33c0e009b5c7d959cd2ebd7cdd79f566d9fdbad1f7a57d204d
7
- data.tar.gz: '0048f0ebb889753af134a28a592a7b1ebee8f6b5c4d67c9ed816271fc4a14356a62ff8643b9bdfaf9a3e04046a95035768248790d56366b0db6f6a58701c2d4f'
6
+ metadata.gz: '00742595c4797ba758ef98ab8b034545eb2193a2cae92dfc585e720c7cd9adef663ba0aadcae2c7085e19497ff74461c6d5553d49114f42dac520c8fe48ca448'
7
+ data.tar.gz: d8c7eb61e630bb65ea9aa2e86b86c03c2c1f87c151e127314c938f7d1b2438c90dfc5f5ddcc4cb74fe0b525bc4ef9c03dfb140d09fc10045b2f7f56b41524e43
data/README.md CHANGED
@@ -223,7 +223,7 @@ users:
223
223
 
224
224
  ### API Sync
225
225
  Several models implement the `ApiSyncable` Concern. This is done in the Model Templates so as to be customizable and tweakable.
226
- Models that `include CanvasSync::ApiSyncable` should also call the `api_syncable` class method to configure the Synchronization.
226
+ Models that `include CanvasSync::Concerns::ApiSyncable` should also call the `api_syncable` class method to configure the Synchronization.
227
227
  `api_syncable` takes two arguments and an optional block callback:
228
228
  ```ruby
229
229
  class CanvasSyncModel < ApplicationRecord
@@ -1,28 +1,23 @@
1
1
  require "bearcat"
2
+
2
3
  require "canvas_sync/version"
3
4
  require "canvas_sync/engine"
5
+ require "canvas_sync/class_callback_executor"
4
6
  require "canvas_sync/job"
7
+ require "canvas_sync/job_chain"
5
8
  require "canvas_sync/sidekiq_job"
6
9
  require "canvas_sync/api_syncable"
10
+ require "canvas_sync/record"
7
11
  require "canvas_sync/jobs/report_starter"
8
12
  require "canvas_sync/jobs/report_checker"
9
- require "canvas_sync/jobs/fork_gather"
10
13
  require "canvas_sync/jobs/report_processor_job"
11
- require "canvas_sync/jobs/sync_provisioning_report_job"
12
- require "canvas_sync/jobs/sync_simple_table_job.rb"
13
- require "canvas_sync/jobs/sync_assignments_job"
14
- require "canvas_sync/jobs/sync_submissions_job"
15
- require "canvas_sync/jobs/sync_assignment_groups_job"
16
- require "canvas_sync/jobs/sync_context_modules_job"
17
- require "canvas_sync/jobs/sync_context_module_items_job"
18
- require "canvas_sync/jobs/sync_terms_job"
19
- require "canvas_sync/jobs/sync_users_job"
20
- require "canvas_sync/jobs/sync_roles_job"
21
- require "canvas_sync/jobs/sync_admins_job"
22
14
  require "canvas_sync/config"
15
+
16
+ Dir[File.dirname(__FILE__) + "/canvas_sync/jobs/*.rb"].each { |file| require file }
23
17
  Dir[File.dirname(__FILE__) + "/canvas_sync/processors/*.rb"].each { |file| require file }
24
18
  Dir[File.dirname(__FILE__) + "/canvas_sync/importers/*.rb"].each { |file| require file }
25
19
  Dir[File.dirname(__FILE__) + "/canvas_sync/generators/*.rb"].each { |file| require file }
20
+ Dir[File.dirname(__FILE__) + "/canvas_sync/concerns/**/*.rb"].each { |file| require file }
26
21
 
27
22
  module CanvasSync
28
23
  SUPPORTED_MODELS = %w[
@@ -111,6 +106,8 @@ module CanvasSync
111
106
  #
112
107
  # @param job_chain [Hash] A chain of jobs to execute
113
108
  def invoke_next(job_chain, extra_options: {})
109
+ job_chain = job_chain.chain_data if job_chain.is_a?(JobChain)
110
+
114
111
  return if job_chain[:jobs].empty?
115
112
 
116
113
  # Make sure all job classes are serialized as strings
@@ -126,6 +123,8 @@ module CanvasSync
126
123
  end
127
124
 
128
125
  def fork(job_log, job_chain, keys: [])
126
+ job_chain = job_chain.chain_data if job_chain.is_a?(JobChain)
127
+
129
128
  duped_job_chain = Marshal.load(Marshal.dump(job_chain))
130
129
  duped_job_chain[:global_options][:fork_path] ||= []
131
130
  duped_job_chain[:global_options][:fork_keys] ||= []
@@ -172,7 +171,7 @@ module CanvasSync
172
171
  global_options = {}
173
172
  global_options[:account_id] = account_id if account_id.present?
174
173
 
175
- { jobs: jobs, global_options: global_options }
174
+ JobChain.new(jobs: jobs, global_options: global_options)
176
175
  end
177
176
 
178
177
  # Syncs terms, users/roles/admins if necessary, then the rest of the specified models.
@@ -193,6 +192,7 @@ module CanvasSync
193
192
 
194
193
  model_job_map = {
195
194
  terms: CanvasSync::Jobs::SyncTermsJob,
195
+ accounts: CanvasSync::Jobs::SyncAccountsJob,
196
196
  users: CanvasSync::Jobs::SyncUsersJob,
197
197
  roles: CanvasSync::Jobs::SyncRolesJob,
198
198
  admins: CanvasSync::Jobs::SyncAdminsJob,
@@ -219,7 +219,8 @@ module CanvasSync
219
219
  models.unshift('terms') unless models.include?('terms')
220
220
  try_add_model_job.call('terms')
221
221
 
222
- # Users, roles, and admins are synced before provisioning because they cannot be scoped to term
222
+ # Accounts, users, roles, and admins are synced before provisioning because they cannot be scoped to term
223
+ try_add_model_job.call('accounts')
223
224
  try_add_model_job.call('users') if term_scope.present?
224
225
  try_add_model_job.call('roles')
225
226
  try_add_model_job.call('admins')
@@ -254,7 +255,7 @@ module CanvasSync
254
255
  global_options[:account_id] = account_id if account_id.present?
255
256
  global_options.merge!(options[:global]) if options[:global].present?
256
257
 
257
- { jobs: jobs, global_options: global_options }
258
+ JobChain.new(jobs: jobs, global_options: global_options)
258
259
  end
259
260
 
260
261
  # Calls the canvas_sync_client in your app. If you have specified an account
@@ -1,167 +1,9 @@
1
+ # DEPRECATED - See CHANGELOG for 0.13.0
2
+ # TODO: (0.14.0) Remove this module
1
3
  module CanvasSync::ApiSyncable
2
4
  extend ActiveSupport::Concern
3
- NON_EXISTANT_ERRORS = [Faraday::Error::ResourceNotFound, Footrest::HttpError::NotFound]
4
5
 
5
- class_methods do
6
- def find_or_fetch(canvas_id, save: false, retries: 1)
7
- inst = find_by(canvas_id: canvas_id)
8
- return inst if inst.present?
9
- inst = new(canvas_id: canvas_id)
10
- api_response = inst.request_from_api(retries: retries)
11
- inst.update_from_api_params(api_response)
12
- inst.save! if save
13
- inst
14
- rescue *NON_EXISTANT_ERRORS
15
- nil
16
- end
17
-
18
- def find_or_fetch!(*args)
19
- inst = find_or_fetch(*args)
20
- raise ActiveRecord::RecordNotFound unless inst.present?
21
- inst
22
- end
23
-
24
- def create_or_update_from_api_params(api_params)
25
- api_params = api_params.with_indifferent_access
26
- inst = find_or_initialize_by(canvas_id: api_params[:id])
27
- inst.update_from_api_params(api_params)
28
- inst.save! if inst.changed?
29
- inst
30
- end
31
-
32
- def api_sync_options=(opts)
33
- @api_sync_options = opts
34
- end
35
-
36
- def api_sync_options
37
- @api_sync_options || superclass.try(:api_sync_options)
38
- end
39
-
40
- # Define the model as being syncable via the Canvas API and configure sync options/parameters
41
- # @param [Hash] map A hash of local_field => (:api_response_key | ->(api_response){ value })
42
- # @param [->(bearcat?){ api_response }] fetch <description>
43
- # @param [Hash] options <description>
44
- # @option options [] :mark_deleted Hash to be merged | Symbol to invoke | ->(){ }
45
- # @yield [api_response, [mapped_data]] Callback to merge data into a Model instance
46
- def api_syncable(map, fetch, options={}, &blk)
47
- default_options = {
48
- mark_deleted: -> {
49
- %i[workflow_state= status=].each do |sym|
50
- next unless self.respond_to?(sym)
51
- self.send(sym, 'deleted')
52
- return
53
- end
54
- },
55
- field_map: map,
56
- fetch_from_api: fetch,
57
- process_response: blk,
58
- }
59
- default_options.merge!(options)
60
- self.api_sync_options = default_options.merge!(options)
61
- end
62
- end
63
-
64
- # Call the API and Syncs this model.
65
- # Calls the mark_deleted workflow if a 404 is received.
66
- # @param [Number] retries Number of retries
67
- # @return [Hash] Response Hash from API
68
- def sync_from_api(retries: 3)
69
- api_response = request_from_api(retries: retries)
70
- update_from_api_params!(api_response)
71
- api_response
72
- rescue *NON_EXISTANT_ERRORS
73
- api_mark_deleted
74
- save! if changed?
75
- nil
76
- end
77
-
78
- # Fetch this model from the API and return the response
79
- # @param [Number] retries Number of retries
80
- # @return [Hash] Response Hash from API
81
- def request_from_api(retries: 3)
82
- api_call_with_retry(retries || 3) {
83
- blk = api_sync_options[:fetch_from_api]
84
- case blk.arity
85
- when 1
86
- self.instance_exec(canvas_sync_client, &blk)
87
- else
88
- self.instance_exec(&blk)
89
- end
90
- }
91
- end
92
-
93
- # Apply a response Hash from the API to this model's attributes, but do not save
94
- # @param [Hash] api_params API-format Hash
95
- # @return [self] self
96
- def update_from_api_params(api_params)
97
- options = self.api_sync_options
98
- api_params = api_params.with_indifferent_access
99
-
100
- map = options[:field_map]
101
- mapped_params = {}
102
- if map.present?
103
- map.each do |local_name, remote_name|
104
- if remote_name.respond_to?(:call)
105
- mapped_params[local_name] = self.instance_exec(api_params, &remote_name)
106
- elsif api_params.include?(remote_name)
107
- mapped_params[local_name] = api_params[remote_name]
108
- if remote_name == :id
109
- current_value = send("#{local_name}")
110
- raise "Mismatched Canvas ID" if current_value.present? && current_value != api_params[remote_name]
111
- end
112
- end
113
- end
114
- end
115
-
116
- apply_block = options[:process_response]
117
- if apply_block.present?
118
- case apply_block.arity
119
- when 1
120
- self.instance_exec(api_params, &apply_block)
121
- when 2
122
- self.instance_exec(api_params, mapped_params, &apply_block)
123
- end
124
- else
125
- mapped_params.each do |local_name, val|
126
- send("#{local_name}=", val)
127
- end
128
- end
129
- self
130
- end
131
-
132
- # Apply a response Hash from the API to this model's attributes, and save if changed?
133
- # @param [Hash] api_params API-format Hash
134
- # @return [self] self
135
- def update_from_api_params!(api_params)
136
- update_from_api_params(api_params)
137
- save! if changed?
138
- self
139
- end
140
-
141
- def api_sync_options
142
- self.class.api_sync_options
143
- end
144
-
145
- private
146
-
147
- def api_call_with_retry(retries=3)
148
- tries ||= retries
149
- yield
150
- rescue Faraday::ConnectionFailed => e
151
- raise e if (tries -= 1).zero?
152
- sleep 5
153
- retry
154
- end
155
-
156
- def api_mark_deleted
157
- action = api_sync_options[:mark_deleted]
158
- case action
159
- when Hash
160
- assign_attributes(action)
161
- when Symbol
162
- send(action)
163
- when Proc
164
- self.instance_exec(&action)
165
- end
6
+ included do
7
+ raise "CanvasSync 0.13.0 includes breaking changes to ApiSyncable. See the CHANGELOG for upgrade steps."
166
8
  end
167
9
  end
@@ -0,0 +1,35 @@
1
+ module CanvasSync
2
+ # Helper/Hack class to allow calling ActiveSupport callbacks on a class instead of just on instances
3
+ class ClassCallbackExecutor
4
+ include ActiveSupport::Callbacks
5
+
6
+ attr_reader :callback_class
7
+ delegate :__callbacks, to: :callback_class
8
+ delegate_missing_to :callback_class
9
+
10
+ def initialize(cls, env)
11
+ @callback_class = cls
12
+ env.keys.each do |k|
13
+ define_singleton_method(k) do
14
+ env[k]
15
+ end
16
+ end
17
+ end
18
+
19
+ def clazz
20
+ callback_class
21
+ end
22
+
23
+ def self.run_callbacks(cls, callback, env={}, &blk)
24
+ new(cls, env).run_callbacks(callback, &blk)
25
+ end
26
+
27
+ def self.run_if_defined(cls, callback, *args, &blk)
28
+ if cls.respond_to?(:"_#{callback}_callbacks")
29
+ run_callbacks(cls, callback, *args, &blk)
30
+ else
31
+ blk.call
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,60 @@
1
+ module CanvasSync::Concerns
2
+ module Account
3
+ # Add support for the ancestry Gem to Accounts
4
+ #
5
+ # Requires `ancestry` to be added to the Gemfile and a migration to execute these steps:
6
+ # add_column :accounts, :ancestry, :string
7
+ # add_index :accounts, :ancestry
8
+ #
9
+ # Handles syncing any Ancestry changes after CanvasSync syncs Accounts.
10
+ module Ancestry
11
+ extend ActiveSupport::Concern
12
+ include CanvasSync::Record
13
+
14
+ included do
15
+ has_ancestry
16
+ before_save :relink_ancestry, if: :canvas_parent_account_id_changed?
17
+ after_sync_import :ancestry_after_sync
18
+ end
19
+
20
+ class_methods do
21
+ def ancestry_after_sync
22
+ trails = {}
23
+ includes(:canvas_parent).find_each do |account|
24
+ parent = account.canvas_parent
25
+ trail = trails[parent.canvas_id] if parent.present?
26
+
27
+ if trail.present?
28
+ account.ancestry = trail
29
+ new_trail = "#{trail}/#{account.id.to_s}"
30
+ elsif parent.present?
31
+ account.parent = parent
32
+ new_trail = "#{account.ancestry}/#{account.id.to_s}"
33
+ else
34
+ account.parent = parent
35
+ new_trail = account.id.to_s
36
+ end
37
+
38
+ trails[account.canvas_id] = new_trail
39
+ account.save! if account.changed?
40
+ end
41
+ end
42
+ end
43
+
44
+ def relink_ancestry
45
+ self.parent = canvas_parent
46
+ end
47
+
48
+ def ensure_ancestry
49
+ return unless canvas_parent_account_id.present?
50
+ return if canvas_parent.present?
51
+
52
+ self.canvas_parent = Account.find_or_fetch(canvas_parent_account_id)
53
+ canvas_parent.save!
54
+ canvas_parent.ensure_ancestry
55
+ relink_ancestry
56
+ save! if changed?
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,189 @@
1
+ module CanvasSync::Concerns
2
+ module ApiSyncable
3
+ extend ActiveSupport::Concern
4
+ NON_EXISTANT_ERRORS = [Faraday::Error::ResourceNotFound, Footrest::HttpError::NotFound]
5
+
6
+ class_methods do
7
+ def find_or_fetch(canvas_id, save: false, retries: 1, **kwargs)
8
+ inst = find_or_initialize_by(canvas_id: canvas_id)
9
+ return inst if inst.persisted?
10
+
11
+ api_response = inst.request_from_api(retries: retries, **kwargs)
12
+ api_sync_race_create!(inst, save: save) do |inst2|
13
+ inst2.assign_from_api_params(api_response, **kwargs)
14
+ end
15
+ rescue *NON_EXISTANT_ERRORS
16
+ nil
17
+ end
18
+
19
+ def find_or_fetch!(*args)
20
+ inst = find_or_fetch(*args)
21
+ raise ActiveRecord::RecordNotFound unless inst.present?
22
+ inst
23
+ end
24
+
25
+ def create_or_update_from_api_params(api_params)
26
+ api_params = api_params.with_indifferent_access
27
+ api_sync_race_create!(api_params[:id]) do |inst|
28
+ inst.assign_from_api_params(api_params)
29
+ end
30
+ end
31
+
32
+ def api_sync_options=(opts)
33
+ @api_sync_options = opts
34
+ end
35
+
36
+ def api_sync_options
37
+ @api_sync_options || superclass.try(:api_sync_options)
38
+ end
39
+
40
+ # Define the model as being syncable via the Canvas API and configure sync options/parameters
41
+ # @param [Hash] map A hash of local_field => (:api_response_key | ->(api_response){ value })
42
+ # @param [->(bearcat?){ api_response }] fetch <description>
43
+ # @param [Hash] options <description>
44
+ # @option options [] :mark_deleted Hash to be merged | Symbol to invoke | ->(){ }
45
+ # @yield [api_response, [mapped_data]] Callback to merge data into a Model instance
46
+ def api_syncable(map, fetch, options={}, &blk)
47
+ default_options = {
48
+ mark_deleted: -> {
49
+ %i[workflow_state= status=].each do |sym|
50
+ next unless self.respond_to?(sym)
51
+ self.send(sym, 'deleted')
52
+ return
53
+ end
54
+ },
55
+ field_map: map,
56
+ fetch_from_api: fetch,
57
+ process_response: blk,
58
+ }
59
+ default_options.merge!(options)
60
+ self.api_sync_options = default_options.merge!(options)
61
+ end
62
+
63
+ private
64
+
65
+ def api_sync_race_create!(inst, save: true)
66
+ inst = find_or_initialize_by(canvas_id: inst) unless inst.is_a?(self)
67
+ yield inst
68
+ inst.save! if save && inst.changed?
69
+ inst
70
+ rescue ActiveRecord::RecordNotUnique
71
+ inst = find_by(canvas_id: inst.canvas_id)
72
+ yield inst
73
+ inst.save! if save && inst.changed?
74
+ inst
75
+ end
76
+ end
77
+
78
+ # Call the API and Syncs this model.
79
+ # Calls the mark_deleted workflow if a 404 is received.
80
+ # @param [Number] retries Number of retries
81
+ # @return [Hash] Response Hash from API
82
+ def sync_from_api(retries: 3, **kwargs)
83
+ api_response = request_from_api(retries: retries, **kwargs)
84
+ update_from_api_params!(api_response, **kwargs)
85
+ api_response
86
+ rescue *NON_EXISTANT_ERRORS
87
+ api_mark_deleted
88
+ save! if changed?
89
+ nil
90
+ end
91
+
92
+ # Fetch this model from the API and return the response
93
+ # @param [Number] retries Number of retries
94
+ # @return [Hash] Response Hash from API
95
+ def request_from_api(retries: 3, **kwargs)
96
+ api_call_with_retry(retries || 3) {
97
+ blk = api_sync_options[:fetch_from_api]
98
+ case blk.arity
99
+ when 1
100
+ self.instance_exec(canvas_sync_client, &blk)
101
+ else
102
+ self.instance_exec(&blk)
103
+ end
104
+ }
105
+ end
106
+
107
+ # Apply a response Hash from the API to this model's attributes, but do not save
108
+ # @param [Hash] api_params API-format Hash
109
+ # @return [self] self
110
+ def assign_from_api_params(api_params, **kwargs)
111
+ options = self.api_sync_options
112
+ api_params = api_params.with_indifferent_access
113
+
114
+ map = options[:field_map]
115
+ mapped_params = {}
116
+ if map.present?
117
+ map.each do |local_name, remote_name|
118
+ if remote_name.respond_to?(:call)
119
+ mapped_params[local_name] = self.instance_exec(api_params, &remote_name)
120
+ elsif api_params.include?(remote_name)
121
+ mapped_params[local_name] = api_params[remote_name]
122
+ if remote_name == :id
123
+ current_value = send("#{local_name}")
124
+ raise "Mismatched Canvas ID" if current_value.present? && current_value != api_params[remote_name]
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ apply_block = options[:process_response]
131
+ if apply_block.present?
132
+ case apply_block.arity
133
+ when 1
134
+ self.instance_exec(api_params, &apply_block)
135
+ when 2
136
+ self.instance_exec(api_params, mapped_params, &apply_block)
137
+ end
138
+ else
139
+ mapped_params.each do |local_name, val|
140
+ send("#{local_name}=", val)
141
+ end
142
+ end
143
+ self
144
+ end
145
+
146
+ # Apply a response Hash from the API to this model's attributes and save if changed?
147
+ # @param [Hash] api_params API-format Hash
148
+ # @return [self] self
149
+ def update_from_api_params(api_params, **kwargs)
150
+ assign_from_api_params(*args)
151
+ save if changed?
152
+ end
153
+
154
+ # Apply a response Hash from the API to this model's attributes, and save! if changed?
155
+ # @param [Hash] api_params API-format Hash
156
+ # @return [self] self
157
+ def update_from_api_params!(*args)
158
+ assign_from_api_params(*args)
159
+ save! if changed?
160
+ end
161
+
162
+ def api_sync_options
163
+ self.class.api_sync_options
164
+ end
165
+
166
+ private
167
+
168
+ def api_call_with_retry(retries=3)
169
+ tries ||= retries
170
+ yield
171
+ rescue Faraday::ConnectionFailed => e
172
+ raise e if (tries -= 1).zero?
173
+ sleep 5
174
+ retry
175
+ end
176
+
177
+ def api_mark_deleted
178
+ action = api_sync_options[:mark_deleted]
179
+ case action
180
+ when Hash
181
+ assign_attributes(action)
182
+ when Symbol
183
+ send(action)
184
+ when Proc
185
+ self.instance_exec(&action)
186
+ end
187
+ end
188
+ end
189
+ end