synchronisable 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/README.md +110 -4
- data/TODO.md +23 -15
- data/lib/generators/synchronisable/templates/initializer.rb +4 -0
- data/lib/synchronisable/attribute_mapper.rb +39 -0
- data/lib/synchronisable/context.rb +1 -1
- data/lib/synchronisable/data_builder.rb +58 -0
- data/lib/synchronisable/dsl/associations/association.rb +5 -2
- data/lib/synchronisable/input_descriptor.rb +42 -0
- data/lib/synchronisable/locale/en.yml +3 -3
- data/lib/synchronisable/model.rb +1 -1
- data/lib/synchronisable/source.rb +30 -10
- data/lib/synchronisable/synchronizer.rb +12 -32
- data/lib/synchronisable/version.rb +1 -1
- data/lib/synchronisable/worker.rb +26 -49
- data/lib/synchronisable.rb +7 -1
- data/spec/dummy/app/synchronizers/break_convention_team_synchronizer.rb +2 -0
- data/spec/dummy/app/synchronizers/match_synchronizer.rb +0 -2
- data/spec/dummy/config/application.rb +0 -1
- data/spec/dummy/config/initializers/{synchronizable.rb → synchronisable.rb} +0 -0
- data/spec/factories/import.rb +1 -1
- data/spec/models/team_spec.rb +86 -1
- data/spec/spec_helper.rb +2 -2
- data/spec/synchronisable/support/shared/contexts.rb +33 -0
- data/spec/synchronisable/support/shared/examples.rb +0 -0
- data/synchronisable.gemspec +1 -1
- metadata +68 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 373108e0dab22db4d8c9ff814efc6df6c875a012
|
4
|
+
data.tar.gz: 01d16982d0a8085ed46b69a117b4189ef8d39be1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ecefc4f286f2227d86193e6aa5c16861ca1d2feb668ba874e28dbc0815cae88ba080c41978a57352ac4935f0c08db7bbe7b54b5bc943de60ddaf45263dde2465
|
7
|
+
data.tar.gz: 1a67ad8603495c3b6e41d0a6a95f512ad31f8b65213711f784a671504ffffc242bda2befed97e8ac2b0855f5af7e9dc3842ecd79f9aa1a71411b6c5904400c13
|
data/README.md
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
[](https://travis-ci.org/vyorkin/synchronisable)
|
2
2
|
[](https://codeclimate.com/github/vyorkin/synchronisable)
|
3
|
+
[](http://inch-pages.github.io/github/vyorkin/synchronisable)
|
3
4
|
[](https://coveralls.io/r/vyorkin/synchronisable)
|
4
|
-
[](http://inch-pages.github.io/github/vyorkin/synchronisable)
|
5
5
|
[](http://stillmaintained.com/vyorkin/synchronisable)
|
6
6
|
[](https://gemnasium.com/vyorkin/synchronisable)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
8
|
# Synchronisable
|
11
9
|
|
12
|
-
Provides base fuctionality (models, DSL) for AR synchronization
|
10
|
+
Provides base fuctionality (models, DSL) for AR synchronization
|
11
|
+
with external resources (apis, services etc).
|
12
|
+
|
13
|
+
## Overview
|
13
14
|
|
14
15
|
## Installation
|
15
16
|
|
@@ -27,3 +28,108 @@ Or install it yourself as:
|
|
27
28
|
|
28
29
|
## Usage
|
29
30
|
|
31
|
+
For examples we'll be using a well-known domain with posts & comments
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Post < ActiveRecord::Base
|
35
|
+
has_many :comments
|
36
|
+
|
37
|
+
synchronisable
|
38
|
+
end
|
39
|
+
|
40
|
+
class Comment < ActiveRecord::Base
|
41
|
+
belongs_to :post
|
42
|
+
|
43
|
+
synchronisable MyCommentSynchronizer
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
As you can see above the first step is to declare your models to be
|
48
|
+
synchronisable. You can do so by using corresponding dsl instruction,
|
49
|
+
that optionally takes a synchonizer class to be used. Actually,
|
50
|
+
the only reason to specify it its when it has a name, that can't be figured out
|
51
|
+
by the following convention: `ModelSynchronizer`.
|
52
|
+
|
53
|
+
After that you should define your model synchronizers
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class PostSynchronizer < Synchronisable::Synchronizer
|
57
|
+
remote_id :p_id
|
58
|
+
|
59
|
+
mappings(
|
60
|
+
:t => :title,
|
61
|
+
:c => :content
|
62
|
+
)
|
63
|
+
|
64
|
+
except :ignored_attr1, :ignored_attr42
|
65
|
+
|
66
|
+
has_many :comments
|
67
|
+
|
68
|
+
fetch do
|
69
|
+
# return array of hashes with
|
70
|
+
# remote entity attributes
|
71
|
+
end
|
72
|
+
|
73
|
+
find do |id|
|
74
|
+
# return a hash with
|
75
|
+
# with remote entity attributes
|
76
|
+
end
|
77
|
+
|
78
|
+
# Hooks/callbacks
|
79
|
+
|
80
|
+
before_record_sync do |source|
|
81
|
+
# ...
|
82
|
+
end
|
83
|
+
|
84
|
+
after_record_sync do |source|
|
85
|
+
# ...
|
86
|
+
end
|
87
|
+
|
88
|
+
before_association_sync do |source, remote_id, association|
|
89
|
+
# ...
|
90
|
+
end
|
91
|
+
|
92
|
+
after_association_sync do |source, remote_id, association|
|
93
|
+
# ...
|
94
|
+
end
|
95
|
+
|
96
|
+
before_sync do |source|
|
97
|
+
# ...
|
98
|
+
end
|
99
|
+
|
100
|
+
after_sync do |source|
|
101
|
+
# ...
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class MyCommentSynchronizer < Synchronisable::Synchronizer
|
106
|
+
remote_id :c_id
|
107
|
+
|
108
|
+
mappings(
|
109
|
+
:a => :author,
|
110
|
+
:t => :body
|
111
|
+
)
|
112
|
+
|
113
|
+
only :author, :body
|
114
|
+
|
115
|
+
fetch do
|
116
|
+
# ...
|
117
|
+
end
|
118
|
+
|
119
|
+
find do |id|
|
120
|
+
# ...
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
To start synchronization
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
Post.sync
|
130
|
+
```
|
131
|
+
P.S.: Better readme & wiki is coming! ^__^
|
132
|
+
|
133
|
+
## Support
|
134
|
+
|
135
|
+
<a href='https://www.codersclan.net/task/yorkinv' target='_blank'><img src='https://www.codersclan.net/button/yorkinv' alt='expert-button' width='205' height='64' style='width: 205px; height: 64px;'></a>
|
data/TODO.md
CHANGED
@@ -1,20 +1,28 @@
|
|
1
1
|
Primary objectives
|
2
2
|
======================================
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
3
|
+
- [x] general tests
|
4
|
+
- [x] except/only
|
5
|
+
- [x] sync method
|
6
|
+
- [x] dependent syncronization & mapping
|
7
|
+
- [x] tests for Synchronisable.sync
|
8
|
+
- [ ] fix a mess with Context
|
9
|
+
- [ ] destroy_missed
|
10
|
+
- [ ] worker.rb refactoring
|
11
|
+
- [x] integrate with travis, stillmaintained, gemnasium,
|
12
|
+
codeclimate, coveralls, inch-pages, codersclan
|
13
|
+
- [ ] write a good README
|
14
|
+
- [ ] extended interface
|
15
|
+
- [ ] sync with include
|
16
|
+
- [x] sync with ids array
|
17
|
+
- [ ] handle case when association type is a :hash
|
18
|
+
- [ ] sync method for collection proxy (Model.where(condition).sync)
|
19
|
+
|
20
|
+
Think about
|
21
|
+
======================================
|
22
|
+
- [ ] has_many :bars, :through => FooModel
|
23
|
+
- [ ] polymorphic associations
|
16
24
|
|
17
25
|
Secondary objectives
|
18
26
|
======================================
|
19
|
-
|
20
|
-
|
27
|
+
- [x] option for verbose logging
|
28
|
+
- [x] colorized STDOUT
|
@@ -1,7 +1,11 @@
|
|
1
1
|
Synchronisable.configure do |config|
|
2
2
|
# Logging configuration
|
3
3
|
#
|
4
|
+
# Default logger fallbacks to `Rails.logger` if available, otherwise
|
5
|
+
# `STDOUT` will be used for output.
|
6
|
+
#
|
4
7
|
# config.logging = {
|
8
|
+
# :logger => defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
5
9
|
# :verbose => true,
|
6
10
|
# :colorize => true
|
7
11
|
# }
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Synchronisable
|
2
|
+
class AttributeMapper
|
3
|
+
class << self
|
4
|
+
def map(source, mappings, options = {})
|
5
|
+
new(mappings, options).map(source)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(mappings, options = {})
|
10
|
+
@mappings = mappings
|
11
|
+
@keep = options[:keep] || []
|
12
|
+
@only, @except = options[:only], options[:except]
|
13
|
+
end
|
14
|
+
|
15
|
+
def map(source)
|
16
|
+
result = source.dup
|
17
|
+
|
18
|
+
apply_mappings(result) if @mappings.present?
|
19
|
+
apply_only(result) if @only.present?
|
20
|
+
apply_except(result) if @except.present?
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def apply_mappings(source)
|
28
|
+
source.transform_keys! { |key| @mappings[key] || key }
|
29
|
+
end
|
30
|
+
|
31
|
+
def apply_only(source)
|
32
|
+
source.keep_if { |key| @only.include?(key) || @keep.include?(key) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def apply_except(source)
|
36
|
+
source.delete_if { |key| key.nil? || @except.include?(key) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'synchronisable/input_descriptor'
|
2
|
+
|
3
|
+
module Synchronisable
|
4
|
+
class DataBuilder
|
5
|
+
class << self
|
6
|
+
def build(model, synchronizer, data)
|
7
|
+
new(model, synchronizer).build(data)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(model, synchronizer)
|
12
|
+
@model = model
|
13
|
+
@synchronizer = synchronizer
|
14
|
+
end
|
15
|
+
|
16
|
+
def build(data)
|
17
|
+
input = InputDescriptor.new(data)
|
18
|
+
|
19
|
+
result = case input
|
20
|
+
when ->(i) { i.empty? }
|
21
|
+
@synchronizer.fetch.()
|
22
|
+
when ->(i) { i.remote_id? }
|
23
|
+
@synchronizer.find.(data)
|
24
|
+
when ->(i) { i.local_id? }
|
25
|
+
find_by_local_id(data)
|
26
|
+
when ->(i) { i.array_of_ids? }
|
27
|
+
find_by_array_of_ids(input)
|
28
|
+
else
|
29
|
+
result = data.dup
|
30
|
+
end
|
31
|
+
|
32
|
+
[result].flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def find_by_array_of_ids(input)
|
38
|
+
records = find_imports(input.element_class.name, input.data)
|
39
|
+
records.map { |r| @synchronizer.find.(r.remote_id) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_by_local_id(id)
|
43
|
+
import = @model.find_by(id: id).try(:import)
|
44
|
+
import ? @synchronizer.find.(import.remote_id) : nil
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def find_imports(class_name, ids)
|
50
|
+
case class_name
|
51
|
+
when 'Fixnum'
|
52
|
+
ids.map { |id| @model.find_by(id: id).try(&:import) }
|
53
|
+
when 'String'
|
54
|
+
ids.map { |id| Import.find_by(id: id) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -17,9 +17,9 @@ module Synchronisable
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
self.valid_options = %i(key class_name required)
|
20
|
+
self.valid_options = %i(key class_name required type)
|
21
21
|
|
22
|
-
attr_reader :name, :model, :key, :required
|
22
|
+
attr_reader :name, :model, :key, :required, :type
|
23
23
|
|
24
24
|
def initialize(synchronizer, name)
|
25
25
|
@synchronizer, @name = synchronizer, name.to_sym
|
@@ -30,6 +30,7 @@ module Synchronisable
|
|
30
30
|
|
31
31
|
@key = options[:key]
|
32
32
|
@required = options[:required]
|
33
|
+
@type = options[:type]
|
33
34
|
|
34
35
|
if options[:class_name].present?
|
35
36
|
@model = options[:class_name].constantize
|
@@ -44,6 +45,8 @@ module Synchronisable
|
|
44
45
|
|
45
46
|
def set_defaults
|
46
47
|
@required ||= false
|
48
|
+
@type ||= :ids
|
49
|
+
|
47
50
|
@model ||= @name.to_s.classify.constantize
|
48
51
|
@key = "#{@name}_#{self.class.key_suffix}" unless @key.present?
|
49
52
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Synchronisable
|
2
|
+
class InputDescriptor
|
3
|
+
attr_reader :data
|
4
|
+
|
5
|
+
def initialize(data)
|
6
|
+
@data = data
|
7
|
+
end
|
8
|
+
|
9
|
+
def empty?
|
10
|
+
@data.blank?
|
11
|
+
end
|
12
|
+
|
13
|
+
def remote_id?
|
14
|
+
@data.is_a?(String)
|
15
|
+
end
|
16
|
+
|
17
|
+
def local_id?
|
18
|
+
@data.is_a?(Integer)
|
19
|
+
end
|
20
|
+
|
21
|
+
def array_of_ids?
|
22
|
+
enumerable? && (
|
23
|
+
first_element.is_a?(String) ||
|
24
|
+
first_element.is_a?(Integer)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def element_class
|
29
|
+
first_element.try(:class)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def first_element
|
35
|
+
@data.try(:first)
|
36
|
+
end
|
37
|
+
|
38
|
+
def enumerable?
|
39
|
+
@data.is_a?(Enumerable)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -12,6 +12,6 @@ en:
|
|
12
12
|
gateway_method_missing: Method %{method} is not implemented for gateway
|
13
13
|
messages:
|
14
14
|
result: |
|
15
|
-
|
16
|
-
|
17
|
-
deleted
|
15
|
+
parent: %{parent},
|
16
|
+
records before: %{before}, records after: %{after},
|
17
|
+
deleted records: %{deleted}, errors: %{errors}
|
data/lib/synchronisable/model.rb
CHANGED
@@ -3,23 +3,29 @@ module Synchronisable
|
|
3
3
|
class Source
|
4
4
|
attr_accessor :import_record
|
5
5
|
attr_reader :model, :remote_attrs,
|
6
|
-
:remote_id, :local_attrs,
|
6
|
+
:remote_id, :local_attrs,
|
7
|
+
:associations, :import_ids
|
7
8
|
|
8
9
|
def initialize(model, parent, remote_attrs)
|
9
10
|
@model, @parent, @synchronizer = model, parent, model.synchronizer
|
10
11
|
@remote_attrs = remote_attrs.with_indifferent_access
|
11
12
|
end
|
12
13
|
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
14
|
+
# Prepares synchronization source:
|
15
|
+
# `remote_id`, `local_attributes`, `import_record` and `associations`.
|
16
|
+
# Sets foreign key if current model is specified as `has_one` or `has_many`
|
17
|
+
# association of parent model.
|
16
18
|
#
|
17
19
|
# @api private
|
18
|
-
def
|
20
|
+
def prepare
|
19
21
|
@remote_id = @synchronizer.extract_remote_id(@remote_attrs)
|
20
22
|
@local_attrs = @synchronizer.map_attributes(@remote_attrs)
|
21
23
|
@associations = @synchronizer.associations_for(@local_attrs)
|
22
24
|
|
25
|
+
# TODO: implement destroy_missed, somehow pass @import_ids,
|
26
|
+
# get all import records if nil
|
27
|
+
|
28
|
+
# remove associations keys from local attributes
|
23
29
|
@local_attrs.delete_if do |key, _|
|
24
30
|
@associations.keys.any? { |a| a.key == key }
|
25
31
|
end
|
@@ -29,20 +35,34 @@ module Synchronisable
|
|
29
35
|
:synchronisable_type => @model
|
30
36
|
)
|
31
37
|
|
32
|
-
|
38
|
+
set_foreign_key
|
33
39
|
end
|
34
40
|
|
35
41
|
def updatable?
|
36
42
|
@import_record.present? && local_record.present?
|
37
43
|
end
|
38
44
|
|
45
|
+
def update_record
|
46
|
+
local_record.update_attributes!(@local_attrs)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_record_pair
|
50
|
+
record = @model.create!(@local_attrs)
|
51
|
+
@import_record = Import.create!(
|
52
|
+
:synchronisable_id => record.id,
|
53
|
+
:synchronisable_type => @model.to_s,
|
54
|
+
:remote_id => @remote_id,
|
55
|
+
:attrs => @local_attrs
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
39
59
|
def local_record
|
40
60
|
@import_record.try(:synchronisable)
|
41
61
|
end
|
42
62
|
|
43
63
|
def dump_message
|
44
64
|
%Q(
|
45
|
-
|
65
|
+
remote id: '#{remote_id}',
|
46
66
|
remote attributes: #{remote_attrs},
|
47
67
|
local attributes: #{local_attrs}
|
48
68
|
)
|
@@ -50,13 +70,13 @@ module Synchronisable
|
|
50
70
|
|
51
71
|
private
|
52
72
|
|
53
|
-
def
|
73
|
+
def set_foreign_key
|
54
74
|
return unless @parent
|
55
|
-
name =
|
75
|
+
name = foreign_key_name
|
56
76
|
@local_attrs[name] = @parent.local_record.id if name
|
57
77
|
end
|
58
78
|
|
59
|
-
def
|
79
|
+
def foreign_key_name
|
60
80
|
return nil unless parent_has_model_as_reflection?
|
61
81
|
parent_name = @parent.model.table_name.singularize
|
62
82
|
"#{parent_name}_id"
|