synced 0.0.1 → 0.0.2

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: 7216624feb81567456dd5c0d2468da9d36232efe
4
- data.tar.gz: b2ac5bb712039e8ddedb5713960a5a68785545a4
3
+ metadata.gz: 129cd901e2a89c56c4854ebc844b1f37f57833e3
4
+ data.tar.gz: c52f215a8f82e22919adfc6925db0e9fbde894f4
5
5
  SHA512:
6
- metadata.gz: bec0c589e63b7ff64d901a3d3e9172a032c7b1ba8bd9466122011d0dc9d127510294daa0d8597a95fbe60214c99c8feef96cfe9d598b63beb511cf7a6884832b
7
- data.tar.gz: 955521df6291cc240e6ddd38e6643a0ebf79685fadd4a6285d585bdcee1b2445080635200e585b9cfd6daed47b2a295950a75120e53e2a0e116c8ae9a26573c5
6
+ metadata.gz: 59b45c2d36ed714e375e2d34eabe2d57ba62453aad252a4a8c6cec61f134fc692c8692476bfa5c530334ad536a48199edbde2cf372f2ca3b746aa4ffbf1b75eb
7
+ data.tar.gz: 77a485f2007ce67c0f01be365622a770b41c01f96b240212fa87985e495cb0e2fbba594ca146c38cf0199d6e7ccc86a681ee58307684064512578f3e3cb30398
data/README.md CHANGED
@@ -1,12 +1,17 @@
1
1
  # Synced
2
2
 
3
- Synced is a Rails Engine that helps you keep local models synchronized with their BookingSync representation.
3
+ Synced is a Rails Engine that helps you keep local models synchronized with
4
+ their BookingSync representation.
4
5
 
5
- Note: This synchronization is in one way only, from BookingSync to your application. If you want to do a 2 way synchronization, you will need to implement it yourself using [BookingSync-API](https://github.com/BookingSync/bookingsync-api)
6
+ Note: This synchronization is in one way only, from BookingSync to your
7
+ application. If you want to do a 2 way synchronization, you will need to
8
+ implement it yourself using
9
+ [BookingSync-API](https://github.com/BookingSync/bookingsync-api)
6
10
 
7
11
  ## Requirements
8
12
 
9
- This engine requires BookingSync API `>= 0.0.17`, Rails `>= 4.0.0` and Ruby `>= 2.0.0`.
13
+ This engine requires BookingSync API `>= 0.0.17`, Rails `>= 4.0.0` and
14
+ Ruby `>= 2.0.0`.
10
15
 
11
16
  ## Documentation
12
17
 
@@ -14,24 +19,19 @@ This engine requires BookingSync API `>= 0.0.17`, Rails `>= 4.0.0` and Ruby `>=
14
19
 
15
20
  ## Installation
16
21
 
17
- Synced works with BookingSync API 0.0.17 onwards, Rails 4.0 onwards and Ruby 2.0 onwards. To get started, add it to your Gemfile with:
22
+ Synced works with BookingSync API 0.0.17 onwards, Rails 4.0 onwards and Ruby
23
+ 2.0 onwards. To get started, add it to your Gemfile with:
18
24
 
19
25
  ```ruby
20
26
  gem 'synced'
21
27
  ```
22
28
 
23
- Then run the installer to copy the migrations,
24
-
25
- ```console
26
- rake synced:install:migrations
27
- ```
28
-
29
- Then, generate a migration to add Synced fields for the model you want to synchronize:
29
+ Generate a migration to add Synced fields for the model you want to synchronize:
30
30
 
31
31
  Example:
32
32
  ```console
33
- rails g migration AddSyncedFieldsToRentals synced_id:integer:index synced_data:text \
34
- synced_updated_at:datetime
33
+ rails g migration AddSyncedFieldsToRentals synced_id:integer:index \
34
+ synced_data:text synced_updated_at:datetime
35
35
  ```
36
36
 
37
37
  and migrate:
@@ -40,11 +40,103 @@ and migrate:
40
40
  rake db:migrate
41
41
  ```
42
42
 
43
- And include `Synced::HasSyncedData` in the model you want to keep in sync:
43
+ And `synced` statement to the model you want to keep in sync.
44
+
45
+ Example:
46
+
47
+ ```ruby
48
+ class Rental < ActiveRecord::Base
49
+ synced
50
+ end
51
+ ```
52
+
53
+ Run synchronization with given remote rentals
54
+
55
+ Example:
56
+
57
+ ```ruby
58
+ Rental.synchronize(remote: remote_rentals)
59
+ ```
60
+
61
+ Run rentals synchronization in website scope
44
62
 
45
63
  Example:
64
+
46
65
  ```ruby
66
+ Rental.synchronize(remote: remote_rentals, scope: website)
67
+ ```
68
+
69
+ ## Custom fields for storing remote object data.
70
+
71
+ By default synced stores remote object in the following db columns.
72
+
73
+ `synced_id` - ID of the remote object
74
+ `synced_data` - Whole remote object is serialized into this attribute
75
+ `synced_all_at` - Synchronization time of the local object when using
76
+ updated_since param
77
+
78
+ You can configure your own fields in `synced` declaration in your model.
79
+
80
+ ```
47
81
  class Rental < ActiveRecord::Base
48
- include Synced::HasSyncedData
82
+ synced id_key: :remote_id, data_key: :remote_data, synced_all_at_key: :remote_all_synced_at
49
83
  end
50
84
  ```
85
+
86
+ ## Local attributes
87
+
88
+ All remote data is stored in `synced_data`, however sometimes it's useful to have some attributes directly in your model. You can use `local_attributes` for that.
89
+
90
+ ```
91
+ class Rental < ActiveRecord::Base
92
+ synced local_attributes: [:name, :size]
93
+ end
94
+ ```
95
+
96
+ This assumes that model has name and size attributes. On every sychronization these two attributes will be assigned with value of `remote_object.name` and `remote_object.size` appropriately.
97
+
98
+ ## Disabling synchronization for selected fields.
99
+
100
+ In some cases you only need one attribute to be synchronized and nothing more.
101
+ By default even when using local_attributes, whole remote object will be
102
+ saved in the `synced_data` and its updated_at in the `synced_all_at`.
103
+ This may take additonal space in the database.
104
+ In order to disable synchronizing these fields, set their names in the `synced` declaration to nil, as in the below example:
105
+
106
+ ```
107
+ class Rental < ActiveRecord::Base
108
+ synced data_key: nil, synced_all_at_key: nil
109
+ end
110
+ ```
111
+
112
+ You cannot disable synchronizing `synced_id` as it's required to match local
113
+ objects with the remote ones.
114
+
115
+ ## Associations
116
+
117
+ It's possible to synchronize objects together with it's associations. For that
118
+ you need to
119
+
120
+ 1. Specify associations you want to synchronize within `synced`
121
+ declaration of the parent model
122
+ 2. Add `synced` declaration to the associated model
123
+
124
+ ```ruby
125
+ class Location < ActiveRecord::Base
126
+ synced associations: :photos
127
+ has_many :photos
128
+ end
129
+
130
+ class Photo < ActiveRecord::Base
131
+ synced
132
+ belongs_to :location
133
+ end
134
+ ```
135
+
136
+ Then run synchronization of the parent objects. Every of the remote_locations
137
+ objects needs to respond to `remote_location[:photos]` from where data for
138
+ photos association will be taken.
139
+
140
+ ```ruby
141
+ Location.synchronize(remote: remote_locations)
142
+ ```
@@ -7,7 +7,13 @@ module Synced
7
7
  end
8
8
 
9
9
  config.to_prepare do
10
- require "synced/engine/lib/has_synced_data"
10
+ require "synced/engine/has_synced_data"
11
+ end
12
+
13
+ ActiveSupport.on_load :active_record do
14
+ extend Synced::Engine::Model
11
15
  end
12
16
  end
13
17
  end
18
+
19
+ require "synced/engine/model"
@@ -0,0 +1,41 @@
1
+ require 'hashie'
2
+
3
+ # Provide a serialized attribute for models. This attribute is `synced_data_key`
4
+ # which by default is `:synced_data`. This is a friendlier alternative to
5
+ # `serialize` with respect to dirty attributes.
6
+ module Synced::Engine::HasSyncedData
7
+ extend ActiveSupport::Concern
8
+ class SyncedData < Hashie::Mash; end
9
+
10
+ included do
11
+ if synced_data_key
12
+ define_method "#{synced_data_key}=" do |object|
13
+ write_attribute synced_data_key, dump(object)
14
+ end
15
+
16
+ define_method synced_data_key do
17
+ instance_variable_get("@#{synced_data_key}") ||
18
+ instance_variable_set("@#{synced_data_key}",
19
+ SyncedData.new(loaded_synced_data))
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def loaded_synced_data
27
+ if data = read_attribute(synced_data_key)
28
+ load data
29
+ else
30
+ {}
31
+ end
32
+ end
33
+
34
+ def dump(object)
35
+ JSON.dump object
36
+ end
37
+
38
+ def load(source)
39
+ JSON.load source
40
+ end
41
+ end
@@ -0,0 +1,74 @@
1
+ require "synced/engine/synchronizer"
2
+
3
+ module Synced::Engine::Model
4
+ # Enables synced for ActiveRecord model.
5
+ #
6
+ # @param options [Hash] Configuration options for synced. They are inherited
7
+ # by subclasses, but can be overwritten in the subclass.
8
+ # @option options [Symbol] id_key: attribute name under which
9
+ # remote object's ID is stored, default is :synced_id.
10
+ # @option options [Symbol] synced_all_at_key: attribute name under which
11
+ # last synchronization time is stored, default is :synced_all_at. It's only
12
+ # used when only_updated option is enabled.
13
+ # @option options [Boolean] only_updated: If true requests to API will take
14
+ # advantage of updated_since param and fetch only created/changed/deleted
15
+ # remote objects
16
+ # @option options [Symbol] data_key: attribute name under which remote
17
+ # object's data is stored.
18
+ # @option options [Array] local_attributes: Array of attributes in the remote
19
+ # object which will be mapped to local object attributes.
20
+ def synced(options = {})
21
+ class_attribute :synced_id_key, :synced_all_at_key, :synced_data_key,
22
+ :synced_local_attributes, :synced_associations, :synced_only_updated
23
+ self.synced_id_key = options.fetch(:id_key, :synced_id)
24
+ self.synced_all_at_key = options.fetch(:synced_all_at_key,
25
+ :synced_all_at)
26
+ self.synced_data_key = options.fetch(:data_key, :synced_data)
27
+ self.synced_local_attributes = options.fetch(:local_attributes, [])
28
+ self.synced_associations = options.fetch(:associations, [])
29
+ self.synced_only_updated = options.fetch(:only_updated, false)
30
+ include Synced::Engine::HasSyncedData
31
+ end
32
+
33
+ # Performs synchronization of given remote objects to local database.
34
+ #
35
+ # @param remote [Array] - Remote objects to be synchronized with local db. If
36
+ # it's nil then synchronizer will make request on it's own.
37
+ # @param model_class [Class] - ActiveRecord model class to which remote objects
38
+ # will be synchronized.
39
+ # @param scope [ActiveRecord::Base] - Within this object scope local objects
40
+ # will be synchronized. By default it's model_class.
41
+ # @param remove [Boolean] - If it's true all local objects within
42
+ # current scope which are not present in the remote array will be destroyed.
43
+ # If only_updated is enabled, ids of objects to be deleted will be taken
44
+ # from the meta part. By default if cancel_at column is present, all
45
+ # missing local objects will be canceled with cancel_all,
46
+ # if it's missing, all will be destroyed with destroy_all.
47
+ # You can also force method to remove local objects by passing it
48
+ # to remove: :mark_as_missing.
49
+ # @example Synchronizing amenities
50
+ #
51
+ # Amenity.synchronize(remote: [remote_amenity1, remote_amenity2])
52
+ #
53
+ # @example Synchronizing rentals within given website. This will
54
+ # create/remove/update rentals only within website.
55
+ # It requires relation website.rentals to exist.
56
+ #
57
+ # Rental.synchronize(remote: remote_rentals, scope: website)
58
+ #
59
+ def synchronize(remote: nil, model_class: self, scope: nil, remove: false)
60
+ options = {
61
+ scope: scope,
62
+ id_key: synced_id_key,
63
+ synced_all_at_key: synced_all_at_key,
64
+ data_key: synced_data_key,
65
+ remove: remove,
66
+ local_attributes: synced_local_attributes,
67
+ associations: synced_associations,
68
+ only_updated: synced_only_updated
69
+ }
70
+ synchronizer = Synced::Engine::Synchronizer.new(remote, model_class,
71
+ options)
72
+ synchronizer.perform
73
+ end
74
+ end
@@ -0,0 +1,188 @@
1
+ # Synchronizer class which performs actual synchronization between
2
+ # local database and given array of remote objects
3
+ class Synced::Engine::Synchronizer
4
+ attr_reader :id_key
5
+
6
+ # Initializes a new Synchronizer
7
+ #
8
+ # @param remote_objects [Array|NilClass] Array of objects to be synchronized
9
+ # with local database. Objects need to respond to at least :id message.
10
+ # If it's nil, then synchronizer will fetch the remote objects on it's own.
11
+ # @param model_class [Class] ActiveRecord model class from which local objects
12
+ # will be created.
13
+ # @param options [Hash]
14
+ # @option options [Symbol] scope: Within this object scope local objects
15
+ # will be synchronized. By default it's model_class.
16
+ # @option options [Symbol] id_key: attribute name under which
17
+ # remote object's ID is stored, default is :synced_id.
18
+ # @option options [Symbol] synced_all_at_key: attribute name under which
19
+ # remote object's sync time is stored, default is :synced_all_at
20
+ # @option options [Symbol] data_key: attribute name under which remote
21
+ # object's data is stored.
22
+ # @option options [Array] local_attributes: Array of attributes in the remote
23
+ # object which will be mapped to local object attributes.
24
+ # @option options [Boolean] remove: If it's true all local objects within
25
+ # current scope which are not present in the remote array will be destroyed.
26
+ # If only_updated is enabled, ids of objects to be deleted will be taken
27
+ # from the meta part. By default if cancel_at column is present, all
28
+ # missing local objects will be canceled with cancel_all,
29
+ # if it's missing, all will be destroyed with destroy_all.
30
+ # You can also force method to remove local objects by passing it
31
+ # to remove: :mark_as_missing.
32
+ # @option options [Boolean] only_updated: If true requests to API will take
33
+ # advantage of updated_since param and fetch only created/changed/deleted
34
+ # remote objects
35
+ def initialize(remote_objects, model_class, options = {})
36
+ @model_class = model_class
37
+ @scope = options[:scope]
38
+ @id_key = options[:id_key]
39
+ @synced_all_at_key = options[:synced_all_at_key]
40
+ @data_key = options[:data_key]
41
+ @remove = options[:remove]
42
+ @only_updated = options[:only_updated]
43
+ @local_attributes = Array(options[:local_attributes])
44
+ @associations = Array(options[:associations])
45
+ @remote_objects = Array(remote_objects) if remote_objects
46
+ @request_performed = false
47
+ end
48
+
49
+ def perform
50
+ relation_scope.transaction do
51
+ remove_relation.send(remove_strategy) if @remove
52
+
53
+ remote_objects.map do |remote|
54
+ local_object = local_object_by_remote_id(remote.id) || relation_scope.new
55
+ local_object.attributes = default_attributes_mapping(remote)
56
+ local_object.attributes = local_attributes_mapping(remote)
57
+ local_object.save! if local_object.changed?
58
+ local_object.tap do |local_object|
59
+ @associations.each do |association|
60
+ klass = association.to_s.classify.constantize
61
+ klass.synchronize(remote: remote[association], scope: local_object,
62
+ remove: @remove)
63
+ end
64
+ end
65
+ end.tap do |local_objects|
66
+ if updated_since_enabled? && @request_performed
67
+ relation_scope.update_all(@synced_all_at_key => Time.now)
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def local_attributes_mapping(remote)
76
+ Hash[@local_attributes.map { |k| [k, remote[k]] }]
77
+ end
78
+
79
+ def default_attributes_mapping(remote)
80
+ {}.tap do |attributes|
81
+ attributes[@id_key] = remote.id
82
+ attributes[@data_key] = remote if @data_key
83
+ end
84
+ end
85
+
86
+ # Returns relation within which local objects are created/edited and removed
87
+ # If no scope is provided, the relation_scope will be class on which
88
+ # .synchronize method is called.
89
+ # If scope is provided, like: account, then relation_scope will be a relation
90
+ # account.rentals (given we run .synchronize on Rental class)
91
+ #
92
+ # @return [ActiveRecord::Relation|Class]
93
+ def relation_scope
94
+ @scope ? @scope.send(resource_name) : @model_class
95
+ end
96
+
97
+ # Returns api client from the closest possible source.
98
+ #
99
+ # @raise [BookingSync::API::Unauthorized] - On unauthorized user
100
+ # @return [BookingSync::API::Client] BookingSync API client
101
+ def api
102
+ closest = [@scope, @scope.class, @model_class].detect do |o|
103
+ o.respond_to?(:api)
104
+ end
105
+ closest && closest.api || raise(MissingAPIClient.new(@scope, @model_class))
106
+ end
107
+
108
+ def local_object_by_remote_id(remote_id)
109
+ local_objects.find { |l| l.attributes[id_key.to_s] == remote_id }
110
+ end
111
+
112
+ def local_objects
113
+ @local_objects ||= relation_scope.where(id_key => remote_objects_ids).to_a
114
+ end
115
+
116
+ def remote_objects_ids
117
+ @remote_objects_ids ||= remote_objects.map(&:id)
118
+ end
119
+
120
+ def remote_objects
121
+ @remote_objects ||= fetch_remote_objects
122
+ end
123
+
124
+ def deleted_remote_objects_ids
125
+ api.last_response.meta[:deleted_ids]
126
+ end
127
+
128
+ def fetch_remote_objects
129
+ api.get("/#{resource_name}", api_request_options).tap do
130
+ @request_performed = true
131
+ end
132
+ end
133
+
134
+ def api_request_options
135
+ {}.tap do |options|
136
+ options[:include] = @associations if @associations.present?
137
+ options[:updated_since] = minimum_updated_at if updated_since_enabled?
138
+ end
139
+ end
140
+
141
+ def minimum_updated_at
142
+ relation_scope.minimum(@synced_all_at_key)
143
+ end
144
+
145
+ def updated_since_enabled?
146
+ @only_updated && @synced_all_at_key
147
+ end
148
+
149
+ def resource_name
150
+ @model_class.to_s.tableize
151
+ end
152
+
153
+ def remove_strategy
154
+ @remove == true ? default_remove_strategy : @remove
155
+ end
156
+
157
+ def default_remove_strategy
158
+ if @model_class.column_names.include?("canceled_at")
159
+ :cancel_all
160
+ else
161
+ :destroy_all
162
+ end
163
+ end
164
+
165
+ def remove_relation
166
+ if @only_updated
167
+ relation_scope.where(id_key => deleted_remote_objects_ids)
168
+ else
169
+ relation_scope.where.not(id_key => remote_objects_ids)
170
+ end
171
+ end
172
+
173
+ class MissingAPIClient < StandardError
174
+ def initialize(scope, model_class)
175
+ @scope = scope
176
+ @model_class = model_class
177
+ end
178
+
179
+ def message
180
+ if @scope
181
+ %Q{Missing BookingSync API client in #{@scope} object or
182
+ #{@scope.class} class}
183
+ else
184
+ %Q{Missing BookingSync API client in #{@model_class} class}
185
+ end
186
+ end
187
+ end
188
+ end
@@ -1,3 +1,3 @@
1
1
  module Synced
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: synced
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastien Grosjean
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-21 00:00:00.000000000 Z
11
+ date: 2014-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: timecop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: Keep your BookingSync Application synced with BookingSync.
98
112
  email:
99
113
  - dev@bookingsync.com
@@ -104,12 +118,12 @@ files:
104
118
  - MIT-LICENSE
105
119
  - README.md
106
120
  - Rakefile
107
- - app/models/synced/synchronization.rb
108
121
  - config/routes.rb
109
- - db/migrate/20140621143737_create_synced_synchronizations.rb
110
122
  - lib/synced.rb
111
123
  - lib/synced/engine.rb
112
- - lib/synced/engine/lib/has_synced_data.rb
124
+ - lib/synced/engine/has_synced_data.rb
125
+ - lib/synced/engine/model.rb
126
+ - lib/synced/engine/synchronizer.rb
113
127
  - lib/synced/version.rb
114
128
  homepage: https://github.com/BookingSync/synced
115
129
  licenses:
@@ -1,4 +0,0 @@
1
- module Synced
2
- class Synchronization < ActiveRecord::Base
3
- end
4
- end
@@ -1,10 +0,0 @@
1
- class CreateSyncedSynchronizations < ActiveRecord::Migration
2
- def change
3
- create_table :synced_synchronizations do |t|
4
- t.string :model
5
- t.datetime :synchronized_at
6
-
7
- t.timestamps
8
- end
9
- end
10
- end
@@ -1,37 +0,0 @@
1
- require 'hashie'
2
-
3
- module Synced
4
- # Provide a serialized `bs_data` attribute for models. This is a friendlier
5
- # alternative to `serialize` with respect to dirty attributes.
6
- module HasSyncedData
7
- class SyncedData < Hashie::Mash; end
8
-
9
- # Serialize and set remote data from `object`.
10
- def synced_data=(object)
11
- write_attribute :synced_data, dump(object)
12
- ensure
13
- @synced_data = nil
14
- end
15
-
16
- # Return remote data as a cached instance.
17
- def synced_data
18
- @synced_data ||= SyncedData.new loaded_synced_data
19
- end
20
-
21
- private
22
-
23
- def loaded_synced_data
24
- load read_attribute(:synced_data)
25
- rescue
26
- {}
27
- end
28
-
29
- def dump(object)
30
- JSON.dump object
31
- end
32
-
33
- def load(source)
34
- JSON.load source
35
- end
36
- end
37
- end