synced 0.0.1 → 0.0.2

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 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