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