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 +4 -4
- data/README.md +42 -1
- data/lib/synced/model.rb +17 -6
- data/lib/synced/strategies/check.rb +22 -14
- data/lib/synced/strategies/full.rb +57 -23
- data/lib/synced/strategies/updated_since.rb +8 -7
- data/lib/synced/version.rb +1 -1
- metadata +21 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0c560c40b86f1e58dc3d88f4c8784124f018999
|
4
|
+
data.tar.gz: b9c289d5ca8cda22e6d9dc7551f7bbae965d2cde
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
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
|
-
|
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
|
184
|
+
@remote_objects_ids
|
166
185
|
end
|
167
186
|
|
168
|
-
def
|
169
|
-
@remote_objects
|
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
|
197
|
+
def fetch_and_save_remote_objects(processor)
|
173
198
|
instrument("fetch_remote_objects.synced", model: @model_class) do
|
174
|
-
|
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] =
|
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
|
-
|
66
|
-
|
62
|
+
@meta ||=
|
63
|
+
(api.last_response && api.last_response.meta) || {}
|
67
64
|
end
|
68
65
|
|
69
66
|
def first_response_headers
|
70
|
-
|
71
|
-
|
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
|
data/lib/synced/version.rb
CHANGED
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
|
+
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:
|
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.
|
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.
|
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.
|
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.
|