canvas_sync 0.12.0 → 0.13.0

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