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