synced 0.0.3 → 0.0.4
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.
- checksums.yaml +4 -4
- data/lib/synced/has_synced_data.rb +27 -25
- data/lib/synced/model.rb +72 -70
- data/lib/synced/rails.rb +4 -1
- data/lib/synced/synchronizer.rb +160 -158
- data/lib/synced/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9269d91cf4e1c50718fb11cc8c3a6860ede34a3a
|
4
|
+
data.tar.gz: 07a7b737a0d403e3e78011a2dd9b49385e65bfa9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e787d2b0ccca79dd8cd4dbe21814bbef16664e88290a82185253a88e4e4088cac2c30b8c925b8e01e2048570d552b0696151f06e1d9dde0ddcf8b5edca767613
|
7
|
+
data.tar.gz: 5236ccc2bebfbd74810f21d29630075322426a6c5ffc9e60fed447b912637b05875a96f74536240c9212764217c600a907633bf87355364159400b25b522cec1
|
@@ -3,39 +3,41 @@ require 'hashie'
|
|
3
3
|
# Provide a serialized attribute for models. This attribute is `synced_data_key`
|
4
4
|
# which by default is `:synced_data`. This is a friendlier alternative to
|
5
5
|
# `serialize` with respect to dirty attributes.
|
6
|
-
module Synced
|
7
|
-
|
8
|
-
|
6
|
+
module Synced
|
7
|
+
module HasSyncedData
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
class SyncedData < Hashie::Mash; end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
included do
|
12
|
+
if synced_data_key
|
13
|
+
define_method "#{synced_data_key}=" do |object|
|
14
|
+
write_attribute synced_data_key, dump(object)
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
define_method synced_data_key do
|
18
|
+
instance_variable_get("@#{synced_data_key}") ||
|
19
|
+
instance_variable_set("@#{synced_data_key}",
|
20
|
+
SyncedData.new(loaded_synced_data))
|
21
|
+
end
|
20
22
|
end
|
21
23
|
end
|
22
|
-
end
|
23
24
|
|
24
|
-
|
25
|
+
private
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
def loaded_synced_data
|
28
|
+
if data = read_attribute(synced_data_key)
|
29
|
+
load data
|
30
|
+
else
|
31
|
+
{}
|
32
|
+
end
|
31
33
|
end
|
32
|
-
end
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
def dump(object)
|
36
|
+
JSON.dump object
|
37
|
+
end
|
37
38
|
|
38
|
-
|
39
|
-
|
39
|
+
def load(source)
|
40
|
+
JSON.load source
|
41
|
+
end
|
40
42
|
end
|
41
43
|
end
|
data/lib/synced/model.rb
CHANGED
@@ -1,75 +1,77 @@
|
|
1
1
|
require "synced/synchronizer"
|
2
2
|
|
3
|
-
module Synced
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
:
|
23
|
-
|
24
|
-
|
25
|
-
:
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
3
|
+
module Synced
|
4
|
+
module Model
|
5
|
+
# Enables synced for ActiveRecord model.
|
6
|
+
#
|
7
|
+
# @param options [Hash] Configuration options for synced. They are inherited
|
8
|
+
# by subclasses, but can be overwritten in the subclass.
|
9
|
+
# @option options [Symbol] id_key: attribute name under which
|
10
|
+
# remote object's ID is stored, default is :synced_id.
|
11
|
+
# @option options [Symbol] synced_all_at_key: attribute name under which
|
12
|
+
# last synchronization time is stored, default is :synced_all_at. It's only
|
13
|
+
# used when only_updated option is enabled.
|
14
|
+
# @option options [Boolean] only_updated: If true requests to API will take
|
15
|
+
# advantage of updated_since param and fetch only created/changed/deleted
|
16
|
+
# remote objects
|
17
|
+
# @option options [Symbol] data_key: attribute name under which remote
|
18
|
+
# object's data is stored.
|
19
|
+
# @option options [Array] local_attributes: Array of attributes in the remote
|
20
|
+
# object which will be mapped to local object attributes.
|
21
|
+
def synced(options = {})
|
22
|
+
class_attribute :synced_id_key, :synced_all_at_key, :synced_data_key,
|
23
|
+
:synced_local_attributes, :synced_associations, :synced_only_updated
|
24
|
+
self.synced_id_key = options.fetch(:id_key, :synced_id)
|
25
|
+
self.synced_all_at_key = options.fetch(:synced_all_at_key,
|
26
|
+
:synced_all_at)
|
27
|
+
self.synced_data_key = options.fetch(:data_key, :synced_data)
|
28
|
+
self.synced_local_attributes = options.fetch(:local_attributes, [])
|
29
|
+
self.synced_associations = options.fetch(:associations, [])
|
30
|
+
self.synced_only_updated = options.fetch(:only_updated, false)
|
31
|
+
include Synced::HasSyncedData
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
34
|
+
# Performs synchronization of given remote objects to local database.
|
35
|
+
#
|
36
|
+
# @param remote [Array] - Remote objects to be synchronized with local db. If
|
37
|
+
# it's nil then synchronizer will make request on it's own.
|
38
|
+
# @param model_class [Class] - ActiveRecord model class to which remote objects
|
39
|
+
# will be synchronized.
|
40
|
+
# @param scope [ActiveRecord::Base] - Within this object scope local objects
|
41
|
+
# will be synchronized. By default it's model_class.
|
42
|
+
# @param remove [Boolean] - If it's true all local objects within
|
43
|
+
# current scope which are not present in the remote array will be destroyed.
|
44
|
+
# If only_updated is enabled, ids of objects to be deleted will be taken
|
45
|
+
# from the meta part. By default if cancel_at column is present, all
|
46
|
+
# missing local objects will be canceled with cancel_all,
|
47
|
+
# if it's missing, all will be destroyed with destroy_all.
|
48
|
+
# You can also force method to remove local objects by passing it
|
49
|
+
# to remove: :mark_as_missing.
|
50
|
+
# @example Synchronizing amenities
|
51
|
+
#
|
52
|
+
# Amenity.synchronize(remote: [remote_amenity1, remote_amenity2])
|
53
|
+
#
|
54
|
+
# @example Synchronizing rentals within given website. This will
|
55
|
+
# create/remove/update rentals only within website.
|
56
|
+
# It requires relation website.rentals to exist.
|
57
|
+
#
|
58
|
+
# Rental.synchronize(remote: remote_rentals, scope: website)
|
59
|
+
#
|
60
|
+
def synchronize(remote: nil, model_class: self, scope: nil, remove: false,
|
61
|
+
include: nil)
|
62
|
+
options = {
|
63
|
+
scope: scope,
|
64
|
+
id_key: synced_id_key,
|
65
|
+
synced_all_at_key: synced_all_at_key,
|
66
|
+
data_key: synced_data_key,
|
67
|
+
remove: remove,
|
68
|
+
local_attributes: synced_local_attributes,
|
69
|
+
associations: synced_associations,
|
70
|
+
only_updated: synced_only_updated,
|
71
|
+
include: include
|
72
|
+
}
|
73
|
+
synchronizer = Synced::Synchronizer.new(remote, model_class, options)
|
74
|
+
synchronizer.perform
|
75
|
+
end
|
74
76
|
end
|
75
77
|
end
|
data/lib/synced/rails.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require "synced/has_synced_data"
|
2
1
|
require "synced/model"
|
3
2
|
|
4
3
|
module Synced
|
@@ -9,6 +8,10 @@ module Synced
|
|
9
8
|
g.test_framework :rspec
|
10
9
|
end
|
11
10
|
|
11
|
+
config.to_prepare do
|
12
|
+
require "synced/has_synced_data"
|
13
|
+
end
|
14
|
+
|
12
15
|
ActiveSupport.on_load :active_record do
|
13
16
|
extend Synced::Model
|
14
17
|
end
|
data/lib/synced/synchronizer.rb
CHANGED
@@ -1,194 +1,196 @@
|
|
1
1
|
# Synchronizer class which performs actual synchronization between
|
2
2
|
# local database and given array of remote objects
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
3
|
+
module Synced
|
4
|
+
class Synchronizer
|
5
|
+
attr_reader :id_key
|
6
|
+
|
7
|
+
# Initializes a new Synchronizer
|
8
|
+
#
|
9
|
+
# @param remote_objects [Array|NilClass] Array of objects to be synchronized
|
10
|
+
# with local database. Objects need to respond to at least :id message.
|
11
|
+
# If it's nil, then synchronizer will fetch the remote objects on it's own.
|
12
|
+
# @param model_class [Class] ActiveRecord model class from which local objects
|
13
|
+
# will be created.
|
14
|
+
# @param options [Hash]
|
15
|
+
# @option options [Symbol] scope: Within this object scope local objects
|
16
|
+
# will be synchronized. By default it's model_class.
|
17
|
+
# @option options [Symbol] id_key: attribute name under which
|
18
|
+
# remote object's ID is stored, default is :synced_id.
|
19
|
+
# @option options [Symbol] synced_all_at_key: attribute name under which
|
20
|
+
# remote object's sync time is stored, default is :synced_all_at
|
21
|
+
# @option options [Symbol] data_key: attribute name under which remote
|
22
|
+
# object's data is stored.
|
23
|
+
# @option options [Array] local_attributes: Array of attributes in the remote
|
24
|
+
# object which will be mapped to local object attributes.
|
25
|
+
# @option options [Boolean] remove: If it's true all local objects within
|
26
|
+
# current scope which are not present in the remote array will be destroyed.
|
27
|
+
# If only_updated is enabled, ids of objects to be deleted will be taken
|
28
|
+
# from the meta part. By default if cancel_at column is present, all
|
29
|
+
# missing local objects will be canceled with cancel_all,
|
30
|
+
# if it's missing, all will be destroyed with destroy_all.
|
31
|
+
# You can also force method to remove local objects by passing it
|
32
|
+
# to remove: :mark_as_missing.
|
33
|
+
# @option options [Boolean] only_updated: If true requests to API will take
|
34
|
+
# advantage of updated_since param and fetch only created/changed/deleted
|
35
|
+
# remote objects
|
36
|
+
def initialize(remote_objects, model_class, options = {})
|
37
|
+
@model_class = model_class
|
38
|
+
@scope = options[:scope]
|
39
|
+
@id_key = options[:id_key]
|
40
|
+
@synced_all_at_key = options[:synced_all_at_key]
|
41
|
+
@data_key = options[:data_key]
|
42
|
+
@remove = options[:remove]
|
43
|
+
@only_updated = options[:only_updated]
|
44
|
+
@include = options[:include]
|
45
|
+
@local_attributes = Array(options[:local_attributes])
|
46
|
+
@associations = Array(options[:associations])
|
47
|
+
@remote_objects = Array(remote_objects) if remote_objects
|
48
|
+
@request_performed = false
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
51
|
+
def perform
|
52
|
+
relation_scope.transaction do
|
53
|
+
remove_relation.send(remove_strategy) if @remove
|
54
|
+
|
55
|
+
remote_objects.map do |remote|
|
56
|
+
local_object = local_object_by_remote_id(remote.id) || relation_scope.new
|
57
|
+
local_object.attributes = default_attributes_mapping(remote)
|
58
|
+
local_object.attributes = local_attributes_mapping(remote)
|
59
|
+
local_object.save! if local_object.changed?
|
60
|
+
local_object.tap do |local_object|
|
61
|
+
@associations.each do |association|
|
62
|
+
klass = association.to_s.classify.constantize
|
63
|
+
klass.synchronize(remote: remote[association], scope: local_object,
|
64
|
+
remove: @remove)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end.tap do |local_objects|
|
68
|
+
if updated_since_enabled? && @request_performed
|
69
|
+
relation_scope.update_all(@synced_all_at_key => Time.now)
|
64
70
|
end
|
65
|
-
end
|
66
|
-
end.tap do |local_objects|
|
67
|
-
if updated_since_enabled? && @request_performed
|
68
|
-
relation_scope.update_all(@synced_all_at_key => Time.now)
|
69
71
|
end
|
70
72
|
end
|
71
73
|
end
|
72
|
-
end
|
73
|
-
|
74
|
-
private
|
75
74
|
|
76
|
-
|
77
|
-
Hash[@local_attributes.map { |k| [k, remote[k]] }]
|
78
|
-
end
|
75
|
+
private
|
79
76
|
|
80
|
-
|
81
|
-
|
82
|
-
attributes[@id_key] = remote.id
|
83
|
-
attributes[@data_key] = remote if @data_key
|
77
|
+
def local_attributes_mapping(remote)
|
78
|
+
Hash[@local_attributes.map { |k| [k, remote[k]] }]
|
84
79
|
end
|
85
|
-
end
|
86
80
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# @return [ActiveRecord::Relation|Class]
|
94
|
-
def relation_scope
|
95
|
-
@scope ? @scope.send(resource_name) : @model_class
|
96
|
-
end
|
81
|
+
def default_attributes_mapping(remote)
|
82
|
+
{}.tap do |attributes|
|
83
|
+
attributes[@id_key] = remote.id
|
84
|
+
attributes[@data_key] = remote if @data_key
|
85
|
+
end
|
86
|
+
end
|
97
87
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
88
|
+
# Returns relation within which local objects are created/edited and removed
|
89
|
+
# If no scope is provided, the relation_scope will be class on which
|
90
|
+
# .synchronize method is called.
|
91
|
+
# If scope is provided, like: account, then relation_scope will be a relation
|
92
|
+
# account.rentals (given we run .synchronize on Rental class)
|
93
|
+
#
|
94
|
+
# @return [ActiveRecord::Relation|Class]
|
95
|
+
def relation_scope
|
96
|
+
@scope ? @scope.send(resource_name) : @model_class
|
97
|
+
end
|
108
98
|
|
109
|
-
|
110
|
-
|
111
|
-
|
99
|
+
# Returns api client from the closest possible source.
|
100
|
+
#
|
101
|
+
# @raise [BookingSync::API::Unauthorized] - On unauthorized user
|
102
|
+
# @return [BookingSync::API::Client] BookingSync API client
|
103
|
+
def api
|
104
|
+
closest = [@scope, @scope.class, @model_class].detect do |o|
|
105
|
+
o.respond_to?(:api)
|
106
|
+
end
|
107
|
+
closest && closest.api || raise(MissingAPIClient.new(@scope, @model_class))
|
108
|
+
end
|
112
109
|
|
113
|
-
|
114
|
-
|
115
|
-
|
110
|
+
def local_object_by_remote_id(remote_id)
|
111
|
+
local_objects.find { |l| l.attributes[id_key.to_s] == remote_id }
|
112
|
+
end
|
116
113
|
|
117
|
-
|
118
|
-
|
119
|
-
|
114
|
+
def local_objects
|
115
|
+
@local_objects ||= relation_scope.where(id_key => remote_objects_ids).to_a
|
116
|
+
end
|
120
117
|
|
121
|
-
|
122
|
-
|
123
|
-
|
118
|
+
def remote_objects_ids
|
119
|
+
@remote_objects_ids ||= remote_objects.map(&:id)
|
120
|
+
end
|
124
121
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
end
|
122
|
+
def remote_objects
|
123
|
+
@remote_objects ||= fetch_remote_objects
|
124
|
+
end
|
129
125
|
|
130
|
-
|
131
|
-
|
132
|
-
|
126
|
+
def deleted_remote_objects_ids
|
127
|
+
remote_objects unless @request_performed
|
128
|
+
api.last_response.meta[:deleted_ids]
|
133
129
|
end
|
134
|
-
end
|
135
130
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
if @include.present?
|
140
|
-
options[:include] ||= []
|
141
|
-
options[:include] += @include
|
131
|
+
def fetch_remote_objects
|
132
|
+
api.paginate(resource_name, api_request_options).tap do
|
133
|
+
@request_performed = true
|
142
134
|
end
|
143
|
-
options[:updated_since] = minimum_updated_at if updated_since_enabled?
|
144
|
-
options[:auto_paginate] = true
|
145
135
|
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def minimum_updated_at
|
149
|
-
relation_scope.minimum(@synced_all_at_key)
|
150
|
-
end
|
151
136
|
|
152
|
-
|
153
|
-
|
154
|
-
|
137
|
+
def api_request_options
|
138
|
+
{}.tap do |options|
|
139
|
+
options[:include] = @associations if @associations.present?
|
140
|
+
if @include.present?
|
141
|
+
options[:include] ||= []
|
142
|
+
options[:include] += @include
|
143
|
+
end
|
144
|
+
options[:updated_since] = minimum_updated_at if updated_since_enabled?
|
145
|
+
options[:auto_paginate] = true
|
146
|
+
end
|
147
|
+
end
|
155
148
|
|
156
|
-
|
157
|
-
|
158
|
-
|
149
|
+
def minimum_updated_at
|
150
|
+
relation_scope.minimum(@synced_all_at_key)
|
151
|
+
end
|
159
152
|
|
160
|
-
|
161
|
-
|
162
|
-
|
153
|
+
def updated_since_enabled?
|
154
|
+
@only_updated && @synced_all_at_key
|
155
|
+
end
|
163
156
|
|
164
|
-
|
165
|
-
|
166
|
-
:cancel_all
|
167
|
-
else
|
168
|
-
:destroy_all
|
157
|
+
def resource_name
|
158
|
+
@model_class.to_s.tableize
|
169
159
|
end
|
170
|
-
end
|
171
160
|
|
172
|
-
|
173
|
-
|
174
|
-
relation_scope.where(id_key => deleted_remote_objects_ids)
|
175
|
-
else
|
176
|
-
relation_scope.where.not(id_key => remote_objects_ids)
|
161
|
+
def remove_strategy
|
162
|
+
@remove == true ? default_remove_strategy : @remove
|
177
163
|
end
|
178
|
-
end
|
179
164
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
165
|
+
def default_remove_strategy
|
166
|
+
if @model_class.column_names.include?("canceled_at")
|
167
|
+
:cancel_all
|
168
|
+
else
|
169
|
+
:destroy_all
|
170
|
+
end
|
184
171
|
end
|
185
172
|
|
186
|
-
def
|
187
|
-
if @
|
188
|
-
|
189
|
-
#{@scope.class} class}
|
173
|
+
def remove_relation
|
174
|
+
if @only_updated
|
175
|
+
relation_scope.where(id_key => deleted_remote_objects_ids)
|
190
176
|
else
|
191
|
-
|
177
|
+
relation_scope.where.not(id_key => remote_objects_ids)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class MissingAPIClient < StandardError
|
182
|
+
def initialize(scope, model_class)
|
183
|
+
@scope = scope
|
184
|
+
@model_class = model_class
|
185
|
+
end
|
186
|
+
|
187
|
+
def message
|
188
|
+
if @scope
|
189
|
+
%Q{Missing BookingSync API client in #{@scope} object or
|
190
|
+
#{@scope.class} class}
|
191
|
+
else
|
192
|
+
%Q{Missing BookingSync API client in #{@model_class} class}
|
193
|
+
end
|
192
194
|
end
|
193
195
|
end
|
194
196
|
end
|
data/lib/synced/version.rb
CHANGED