synced 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b945df81754cad42074724a1571ee354c1933224
4
- data.tar.gz: 751cd8bbe6f45e57291d803b84a732ce85f8b94f
3
+ metadata.gz: d0c560c40b86f1e58dc3d88f4c8784124f018999
4
+ data.tar.gz: b9c289d5ca8cda22e6d9dc7551f7bbae965d2cde
5
5
  SHA512:
6
- metadata.gz: 8ddb414c76f87761d53d9cef876709c216da97bff6ad3deed9e972919559d41ec71d1deef1e6c6bb3e7a584f3c8e2dae4d9ad590d5ac9e873daa1667b737e4d5
7
- data.tar.gz: ac416d727ee33daf592af9b7d4d0717d151212d0d3c39070abcb65e3492ffc28b6e5e04acded3bc97dcf783f3e9a1d9789250cd704536653d6d8bfe6249a5b59
6
+ metadata.gz: 3aa68754bac3e77f985740aa5e65b16649075d58ded301f3d660dd9627094dd346a93a10b46c2dfec2cfb28e43786b06419f921426fb2b350005f9b96268be0e
7
+ data.tar.gz: 465449c7d9c65f0355fc783957149fc621cb4a0f6f99c20737fc866251da48c4f60654a97ad635cf301d35eaa887009c7c83447c5ddcb3c159cab0c918835e41
data/README.md CHANGED
@@ -76,6 +76,43 @@ rental = account.rentals.first
76
76
  rental.synced_data.bedrooms # => 4
77
77
  rental.synced_data.rental_type # => "villa"
78
78
  ```
79
+ ## Fetching data
80
+
81
+ You can choose between two ways of [fetching remote data](https://github.com/BookingSync/bookingsync-api#pagination):
82
+ 1. `auto_paginate` - default strategy, which fetches and persists all data at once. This strategy ensures that resources are fetched as quickly as possible, therefore minimizing risk of data changes during request. However, this way may become cumbersome when working with large data-sets (high memory usage).
83
+ 2. `pagination with block` - fetches and persists records in batches. Especially helpful, when dealing with large data-sets. Increases overall syncing time, but significantly reduces memory usage. Number of entries per page can be customized by `:per_page` attribute inside `:query_params`
84
+
85
+ In order to switch to `pagination with block`, you just need to use `auto_paginate: false`:
86
+ ```ruby
87
+ class Rental
88
+ synced auto_paginate: false, query_params: { per_page: 50 }
89
+ end
90
+ ```
91
+ ### Persisted objects
92
+
93
+ There is another major difference between `auto_paginate` and `pagination with block` - when using `auto_paginate`, `.synchronize` returns collection of all **persisted** records. On the other hand, `pagination with block` returns last batch of **fetched** resources.
94
+ If you need to process persisted data we encourage you to use `handle_processed_objects_proc`. This proc takes one argument (persisted records) and is called after persisting each batch of remote objects. So when using `auto_paginate`, this:
95
+
96
+ ```ruby
97
+ class Rental
98
+ synced handle_processed_objects_proc: Proc.new { |persisted_rentals|
99
+ persisted_rentals.each { |rental| rental.do_stuff }
100
+ }
101
+ end
102
+ ```
103
+ would be an equivalent of overriding `.synchronize`:
104
+ ```ruby
105
+ class Rental
106
+ synced
107
+
108
+ def self.synchronize(options)
109
+ super.tap do |persisted_rentals|
110
+ persisted_rentals.each { |rental| rental.do_stuff }
111
+ end
112
+ end
113
+ end
114
+ ```
115
+ **When using `pagination with block` only the former will work.**
79
116
 
80
117
  ## Synced strategies
81
118
 
@@ -346,6 +383,7 @@ Location.synchronize(remote: remote_locations)
346
383
  ```
347
384
 
348
385
  NOTE: Partial updates are disabled when providing remote objects.
386
+ **WARNING:** When using `remove: true` with `remote`, remember that synced will remove **ALL** records that are not passed to `remote`.
349
387
 
350
388
  ## Removing local objects
351
389
 
@@ -418,6 +456,7 @@ If you want to access synced attribute with different name, you can pass a Hash:
418
456
  class Photo < ActiveRecord::Base
419
457
  synced delegate_attributes: {title: :name}
420
458
  end
459
+ ```
421
460
 
422
461
  keys are delegated attributes' names and values are keys on synced data Hash. This is a simpler
423
462
  version of `delegate :name, to: :synced_data` which works with Hash reserved attributes names, like
@@ -436,7 +475,9 @@ Option name | Default value | Description
436
475
  `:include` | `[]` | [An array of associations to be fetched](#including-associations-in-synced_data) | YES | YES |
437
476
  `:fields` | `[]` | [An array of fields to be fetched](#selecting-fields-to-be-synchronized) | YES | YES |
438
477
  `:remote` | `nil` | [Remote objects to be synchronized with local ones](#synchronization-of-given-remote-objects) | NO | YES |
439
- `:delegate_attributes`| `[]` | [Define delegators to synced data Hash attributes](#delegate-attributes) | YES | NO |
478
+ `:delegate_attributes`| `[]` | [Define delegators to synced data Hash attributes](#delegate-attributes) | YES | NO |
479
+ `:auto_paginate` | `true` | [Whether data should be fetched in batches or as one response](#fetching-methods) | YES | YES |
480
+ `:handle_processed_objects_proc` | `nil` | [Custom proc taking persisted remote objects, called after persisting batch of data](#persisted-objects) | YES | NO |
440
481
 
441
482
  ## Documentation
442
483
 
data/lib/synced/model.rb CHANGED
@@ -35,16 +35,21 @@ module Synced
35
35
  # on synchronized object and delegated to synced_data Hash
36
36
  # @option options [Hash] query_params: Given attributes and their values
37
37
  # which will be passed to api client to perform search
38
+ # @option options [Boolean] auto_paginate: If true (default) will fetch and save all
39
+ # records at once. If false will fetch and save records in batches.
40
+ # @option options [Proc] handle_processed_objects_proc: Proc taking one argument (persisted remote objects).
41
+ # Called after persisting remote objects (once in case of auto_paginate, after each batch
42
+ # when paginating with block).
38
43
  def synced(strategy: :updated_since, **options)
39
44
  options.assert_valid_keys(:associations, :data_key, :fields,
40
45
  :globalized_attributes, :id_key, :include, :initial_sync_since,
41
- :local_attributes, :mapper, :only_updated, :remove,
42
- :delegate_attributes, :query_params, :timestamp_strategy)
46
+ :local_attributes, :mapper, :only_updated, :remove, :auto_paginate,
47
+ :delegate_attributes, :query_params, :timestamp_strategy, :handle_processed_objects_proc)
43
48
  class_attribute :synced_id_key, :synced_data_key,
44
49
  :synced_local_attributes, :synced_associations, :synced_only_updated,
45
- :synced_mapper, :synced_remove, :synced_include, :synced_fields,
50
+ :synced_mapper, :synced_remove, :synced_include, :synced_fields, :synced_auto_paginate,
46
51
  :synced_globalized_attributes, :synced_initial_sync_since, :synced_delegate_attributes,
47
- :synced_query_params, :synced_timestamp_strategy, :synced_strategy
52
+ :synced_query_params, :synced_timestamp_strategy, :synced_strategy, :synced_handle_processed_objects_proc
48
53
  self.synced_strategy = strategy
49
54
  self.synced_id_key = options.fetch(:id_key, :synced_id)
50
55
  self.synced_data_key = options.fetch(:data_key,
@@ -63,6 +68,8 @@ module Synced
63
68
  self.synced_delegate_attributes = options.fetch(:delegate_attributes, [])
64
69
  self.synced_query_params = options.fetch(:query_params, {})
65
70
  self.synced_timestamp_strategy = options.fetch(:timestamp_strategy, nil)
71
+ self.synced_auto_paginate = options.fetch(:auto_paginate, true)
72
+ self.synced_handle_processed_objects_proc = options.fetch(:handle_processed_objects_proc, nil)
66
73
  include Synced::DelegateAttributes
67
74
  include Synced::HasSyncedData
68
75
  end
@@ -84,6 +91,8 @@ module Synced
84
91
  # You can also force method to remove local objects by passing it
85
92
  # to remove: :mark_as_missing. This option can be defined in the model
86
93
  # and then overwritten in the synchronize method.
94
+ # @param auto_paginate [Boolean] - If true (default) will fetch and save all
95
+ # records at once. If false will fetch and save records in batches.
87
96
  # @param api [BookingSync::API::Client] - API client to be used for fetching
88
97
  # remote objects
89
98
  # @example Synchronizing amenities
@@ -97,11 +106,12 @@ module Synced
97
106
  # website.rentals.synchronize(remote: remote_rentals)
98
107
  #
99
108
  def synchronize(scope: scope_from_relation, strategy: synced_strategy, **options)
100
- options.assert_valid_keys(:api, :fields, :include, :remote, :remove, :query_params, :association_sync)
109
+ options.assert_valid_keys(:api, :fields, :include, :remote, :remove, :query_params, :association_sync, :auto_paginate)
101
110
  options[:remove] = synced_remove unless options.has_key?(:remove)
102
111
  options[:include] = Array.wrap(synced_include) unless options.has_key?(:include)
103
112
  options[:fields] = Array.wrap(synced_fields) unless options.has_key?(:fields)
104
113
  options[:query_params] = synced_query_params unless options.has_key?(:query_params)
114
+ options[:auto_paginate] = synced_auto_paginate unless options.has_key?(:auto_paginate)
105
115
  options.merge!({
106
116
  scope: scope,
107
117
  strategy: strategy,
@@ -114,7 +124,8 @@ module Synced
114
124
  mapper: synced_mapper,
115
125
  globalized_attributes: synced_globalized_attributes,
116
126
  initial_sync_since: synced_initial_sync_since,
117
- timestamp_strategy: synced_timestamp_strategy
127
+ timestamp_strategy: synced_timestamp_strategy,
128
+ handle_processed_objects_proc: synced_handle_processed_objects_proc
118
129
  })
119
130
  Synced::Synchronizer.new(self, options).perform
120
131
  end
@@ -19,24 +19,32 @@ module Synced
19
19
  # ActiveRecord::Model #changes hash is returned - changed objects
20
20
  # @return [Synced::Strategies::Check::Result] Integrity check result
21
21
  def perform
22
+ process_remote_objects(remote_objects_tester)
22
23
  result.additional = remove_relation.to_a
23
- remote_objects.map do |remote|
24
- if local_object = local_object_by_remote_id(remote.id)
25
- remote.extend(@mapper) if @mapper
26
- local_object.attributes = default_attributes_mapping(remote)
27
- local_object.attributes = local_attributes_mapping(remote)
28
- if @globalized_attributes.present?
29
- local_object.attributes = globalized_attributes_mapping(remote,
30
- local_object.translations.translated_locales)
31
- end
32
- if local_object.changed?
33
- result.changed << [{ id: local_object.id }, local_object.changes]
24
+ result
25
+ end
26
+
27
+ def remote_objects_tester
28
+ lambda do |remote_objects|
29
+ @remote_objects_ids.concat(remote_objects.map(&:id))
30
+
31
+ remote_objects.map do |remote|
32
+ if local_object = local_object_by_remote_id(remote.id)
33
+ remote.extend(@mapper) if @mapper
34
+ local_object.attributes = default_attributes_mapping(remote)
35
+ local_object.attributes = local_attributes_mapping(remote)
36
+ if @globalized_attributes.present?
37
+ local_object.attributes = globalized_attributes_mapping(remote,
38
+ local_object.translations.translated_locales)
39
+ end
40
+ if local_object.changed?
41
+ result.changed << [{ id: local_object.id }, local_object.changes]
42
+ end
43
+ else
44
+ result.missing << remote
34
45
  end
35
- else
36
- result.missing << remote
37
46
  end
38
47
  end
39
- result
40
48
  end
41
49
 
42
50
  # If we check model which uses cancel instead of destroy, we skip canceled
@@ -42,6 +42,8 @@ module Synced
42
42
  # mapping remote objects attributes into local object attributes
43
43
  # @option options [Array|Hash] globalized_attributes: A list of attributes
44
44
  # which will be mapped with their translations.
45
+ # @option options [Boolean] auto_paginate: If true (default) will fetch and save all
46
+ # records at once. If false will fetch and save records in batches.
45
47
  def initialize(model_class, options = {})
46
48
  @model_class = model_class
47
49
  @scope = options[:scope]
@@ -61,30 +63,21 @@ module Synced
61
63
  @remote_objects = Array.wrap(options[:remote]) unless @perform_request
62
64
  @globalized_attributes = synced_attributes_as_hash(options[:globalized_attributes])
63
65
  @query_params = options[:query_params]
66
+ @auto_paginate = options[:auto_paginate]
67
+ @handle_processed_objects_proc = options[:handle_processed_objects_proc]
68
+ @remote_objects_ids = []
64
69
  end
65
70
 
66
71
  def perform
67
72
  instrument("perform.synced", model: @model_class) do
68
73
  relation_scope.transaction do
74
+ processed_objects = instrument("sync_perform.synced", model: @model_class) do
75
+ process_remote_objects(remote_objects_persistor)
76
+ end
69
77
  instrument("remove_perform.synced", model: @model_class) do
70
78
  remove_relation.send(remove_strategy) if @remove
71
79
  end
72
- instrument("sync_perform.synced", model: @model_class) do
73
- remote_objects.map do |remote|
74
- remote.extend(@mapper) if @mapper
75
- local_object = local_object_by_remote_id(remote.id) || relation_scope.new
76
- local_object.attributes = default_attributes_mapping(remote)
77
- local_object.attributes = local_attributes_mapping(remote)
78
- if @globalized_attributes.present?
79
- local_object.attributes = globalized_attributes_mapping(remote,
80
- local_object.translations.translated_locales)
81
- end
82
- local_object.save! if local_object.changed?
83
- local_object.tap do |local_object|
84
- synchronize_associations(remote, local_object)
85
- end
86
- end
87
- end
80
+ processed_objects
88
81
  end
89
82
  end
90
83
  end
@@ -95,6 +88,32 @@ module Synced
95
88
 
96
89
  private
97
90
 
91
+ def remote_objects_persistor
92
+ lambda do |remote_objects|
93
+ additional_errors_check
94
+ @remote_objects_ids.concat(remote_objects.map(&:id))
95
+
96
+ processed_objects =
97
+ remote_objects.map do |remote|
98
+ remote.extend(@mapper) if @mapper
99
+ local_object = local_object_by_remote_id(remote.id) || relation_scope.new
100
+ local_object.attributes = default_attributes_mapping(remote)
101
+ local_object.attributes = local_attributes_mapping(remote)
102
+ if @globalized_attributes.present?
103
+ local_object.attributes = globalized_attributes_mapping(remote,
104
+ local_object.translations.translated_locales)
105
+ end
106
+ local_object.save! if local_object.changed?
107
+ local_object.tap do |local_object|
108
+ synchronize_associations(remote, local_object)
109
+ end
110
+ end
111
+
112
+ @handle_processed_objects_proc.call(processed_objects) if @handle_processed_objects_proc.respond_to?(:call)
113
+ processed_objects
114
+ end
115
+ end
116
+
98
117
  def synchronize_associations(remote, local_object)
99
118
  @associations.each do |association|
100
119
  klass = association.to_s.classify.constantize
@@ -158,20 +177,32 @@ module Synced
158
177
  end
159
178
 
160
179
  def local_objects
161
- @local_objects ||= relation_scope.where(@id_key => remote_objects_ids).to_a
180
+ relation_scope.where(@id_key => remote_objects_ids).to_a
162
181
  end
163
182
 
164
183
  def remote_objects_ids
165
- @remote_objects_ids ||= remote_objects.map(&:id)
184
+ @remote_objects_ids
166
185
  end
167
186
 
168
- def remote_objects
169
- @remote_objects ||= @perform_request ? fetch_remote_objects : nil
187
+ def process_remote_objects(processor)
188
+ if @remote_objects
189
+ processor.call(@remote_objects)
190
+ elsif @perform_request
191
+ fetch_and_save_remote_objects(processor)
192
+ else
193
+ nil
194
+ end
170
195
  end
171
196
 
172
- def fetch_remote_objects
197
+ def fetch_and_save_remote_objects(processor)
173
198
  instrument("fetch_remote_objects.synced", model: @model_class) do
174
- api.paginate(resource_name, api_request_options)
199
+ if @auto_paginate
200
+ processor.call(api.paginate(resource_name, api_request_options))
201
+ else
202
+ api.paginate(resource_name, api_request_options) do |batch|
203
+ processor.call(batch)
204
+ end
205
+ end
175
206
  end
176
207
  end
177
208
 
@@ -183,7 +214,7 @@ module Synced
183
214
  options[:include] += @include
184
215
  end
185
216
  options[:fields] = @fields if @fields.present?
186
- options[:auto_paginate] = true
217
+ options[:auto_paginate] = @auto_paginate
187
218
  end.merge(query_params)
188
219
  end
189
220
 
@@ -223,6 +254,9 @@ module Synced
223
254
  Synced.instrumenter.instrument(*args, &block)
224
255
  end
225
256
 
257
+ def additional_errors_check
258
+ end
259
+
226
260
  class MissingAPIClient < StandardError
227
261
  def initialize(scope, model_class)
228
262
  @scope = scope
@@ -16,11 +16,8 @@ module Synced
16
16
  end
17
17
 
18
18
  def perform
19
- raise MissingTimestampError.new unless first_request_timestamp
20
19
  super.tap do |local_objects|
21
20
  instrument("update_synced_timestamp_perform.synced", model: @model_class) do
22
- # TODO: it can't be Time.now. this value has to be fetched from the API as well
23
- # https://github.com/BookingSync/synced/issues/29
24
21
  @timestamp_strategy.update(first_request_timestamp)
25
22
  end
26
23
  end
@@ -62,13 +59,13 @@ module Synced
62
59
  end
63
60
 
64
61
  def meta
65
- remote_objects
66
- @meta ||= api.last_response.meta
62
+ @meta ||=
63
+ (api.last_response && api.last_response.meta) || {}
67
64
  end
68
65
 
69
66
  def first_response_headers
70
- remote_objects
71
- @first_response_headers ||= api.pagination_first_response.headers
67
+ @first_response_headers ||=
68
+ (api.pagination_first_response && api.pagination_first_response.headers) || {}
72
69
  end
73
70
 
74
71
  # Remove all objects with ids from deleted_ids field in the meta key
@@ -76,6 +73,10 @@ module Synced
76
73
  relation_scope.where(@id_key => deleted_remote_objects_ids)
77
74
  end
78
75
 
76
+ def additional_errors_check
77
+ raise MissingTimestampError.new unless first_request_timestamp
78
+ end
79
+
79
80
  class CannotDeleteDueToNoDeletedIdsError < StandardError
80
81
  def initialize(model_class)
81
82
  @model_class = model_class
@@ -1,3 +1,3 @@
1
1
  module Synced
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: synced
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastien Grosjean
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-12-01 00:00:00.000000000 Z
12
+ date: 2017-02-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -31,14 +31,14 @@ dependencies:
31
31
  requirements:
32
32
  - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: 0.1.3
34
+ version: 0.1.4
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
- version: 0.1.3
41
+ version: 0.1.4
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: hashie
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -53,6 +53,20 @@ dependencies:
53
53
  - - ">="
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: appraisal
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
56
70
  - !ruby/object:Gem::Dependency
57
71
  name: sqlite3
58
72
  requirement: !ruby/object:Gem::Requirement
@@ -155,14 +169,14 @@ dependencies:
155
169
  name: globalize
156
170
  requirement: !ruby/object:Gem::Requirement
157
171
  requirements:
158
- - - "~>"
172
+ - - ">="
159
173
  - !ruby/object:Gem::Version
160
174
  version: 4.0.2
161
175
  type: :development
162
176
  prerelease: false
163
177
  version_requirements: !ruby/object:Gem::Requirement
164
178
  requirements:
165
- - - "~>"
179
+ - - ">="
166
180
  - !ruby/object:Gem::Version
167
181
  version: 4.0.2
168
182
  - !ruby/object:Gem::Dependency
@@ -224,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
224
238
  version: '0'
225
239
  requirements: []
226
240
  rubyforge_project:
227
- rubygems_version: 2.5.1
241
+ rubygems_version: 2.6.10
228
242
  signing_key:
229
243
  specification_version: 4
230
244
  summary: Keep your BookingSync Application synced with BookingSync.