synchronisable 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1816035987d8bab6ea54e58aab1a9ee8a8477766
4
- data.tar.gz: 1c2e17f3e3ed31c29e87283a0065ac25861a210e
3
+ metadata.gz: 373108e0dab22db4d8c9ff814efc6df6c875a012
4
+ data.tar.gz: 01d16982d0a8085ed46b69a117b4189ef8d39be1
5
5
  SHA512:
6
- metadata.gz: 1903884dcb5008df7cc0703bfa294397faaa2b2cf806e3597f0f670529df88a62613464aeb230c40564a46cb6968b5bd03e4a1222a4cfa46d23948db15172ba5
7
- data.tar.gz: face867f4e7d462e427904b3e195aa16a6a40fa7d9b89bc28673bd6f0fc1d52b6dc359ee388acf33a24ee8e4bfd24f5e0e5ba839fad28e98d90cc1ad492a9ee3
6
+ metadata.gz: ecefc4f286f2227d86193e6aa5c16861ca1d2feb668ba874e28dbc0815cae88ba080c41978a57352ac4935f0c08db7bbe7b54b5bc943de60ddaf45263dde2465
7
+ data.tar.gz: 1a67ad8603495c3b6e41d0a6a95f512ad31f8b65213711f784a671504ffffc242bda2befed97e8ac2b0855f5af7e9dc3842ecd79f9aa1a71411b6c5904400c13
data/README.md CHANGED
@@ -1,15 +1,16 @@
1
1
  [![Build Status](https://travis-ci.org/vyorkin/synchronisable.png?branch=master)](https://travis-ci.org/vyorkin/synchronisable)
2
2
  [![Code Climate](https://codeclimate.com/github/vyorkin/synchronisable.png)](https://codeclimate.com/github/vyorkin/synchronisable)
3
+ [![Inline docs](http://inch-pages.github.io/github/vyorkin/synchronisable.png)](http://inch-pages.github.io/github/vyorkin/synchronisable)
3
4
  [![Coverage Status](https://coveralls.io/repos/vyorkin/synchronisable/badge.png)](https://coveralls.io/r/vyorkin/synchronisable)
4
- [![Inch Pages](http://inch-pages.github.io/github/vyorkin/synchronisable)](http://inch-pages.github.io/github/vyorkin/synchronisable)
5
5
  [![Gem Version](http://stillmaintained.com/vyorkin/synchronisable.png)](http://stillmaintained.com/vyorkin/synchronisable)
6
6
  [![Dependency Status](https://gemnasium.com/vyorkin/synchronisable.svg)](https://gemnasium.com/vyorkin/synchronisable)
7
7
 
8
-
9
-
10
8
  # Synchronisable
11
9
 
12
- Provides base fuctionality (models, DSL) for AR synchronization with external resources (apis, services etc)
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
- * general tests (DONE)
4
- * except/only (DONE)
5
- * sync method (DONE)
6
- * dependent syncronization & mapping (DONE)
7
- * tests for Synchronisable.sync (DONE)
8
- * destroy_missed
9
- * worker.rb refactoring
10
- * integrate with travis, stillmaintained, gemnasium
11
- * write a good README
12
- * extended interface
13
- * sync with include
14
- * sync with ids array
15
- * sync method for collection proxy (Model.where(condition).sync)
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
- * option for verbose logging (DONE)
20
- * colorized STDOUT
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
@@ -14,7 +14,7 @@ module Synchronisable
14
14
  def summary_message
15
15
  I18n.t('messages.result',
16
16
  :model => model,
17
- :parent => @parent.try(:model),
17
+ :parent => @parent.try(:model) || 'nil',
18
18
  :before => before,
19
19
  :after => after,
20
20
  :deleted => deleted,
@@ -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
- %{model} synchronization, parent: %{parent}.
16
- Record count before / after: %{before} / %{after},
17
- deleted record count: %{deleted}, error count: %{errors}
15
+ parent: %{parent},
16
+ records before: %{before}, records after: %{after},
17
+ deleted records: %{deleted}, errors: %{errors}
@@ -31,7 +31,7 @@ module Synchronisable
31
31
  extend Synchronisable::Model::Methods
32
32
 
33
33
  class_attribute :synchronizer
34
- has_one :import, as: :synchronisable
34
+ has_one :import, as: :synchronisable, class_name: 'Synchronisable::Import'
35
35
 
36
36
  set_defaults(args)
37
37
  end
@@ -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, :associations
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
- # Extracts the `remote_id` from remote attributes, maps remote attirubtes
14
- # to local attributes and tries to find import record for given model
15
- # by extracted remote id.
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 build
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
- set_parent_attribute
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
- remote_id: #{remote_id},
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 set_parent_attribute
73
+ def set_foreign_key
54
74
  return unless @parent
55
- name = parent_attribute_name
75
+ name = foreign_key_name
56
76
  @local_attrs[name] = @parent.local_record.id if name
57
77
  end
58
78
 
59
- def parent_attribute_name
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"