untied-consumer-sync 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swp
19
+ *.swo
20
+ .rspec
21
+ bin/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - 1.8.7
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in untied-consumer-sync.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Redu Educational Technologies
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Untied::Consumer::Sync
2
+
3
+ Process the messages comming from your [Untied::Publisher](https://github.com/redu/untied-publisher) and syncs it directly to the database.
4
+
5
+ **Build status**
6
+
7
+ [![Build Status](https://travis-ci.org/redu/untied-consumer-sync.png?branch=master)](https://travis-ci.org/redu/untied-consumer-sync)
8
+
9
+ ## Instalation
10
+
11
+ Add this line to your Gemfile:
12
+ ```ruby
13
+ gem 'untied-consumer-sync'
14
+ ```
15
+
16
+ Execute bundle:
17
+ ```
18
+ $ bundle
19
+ ```
20
+
21
+ Or install it yourself:
22
+ ```
23
+ $ gem install untied-consumer-sync
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ You will need to mark which model should be syncronized and what attributes you are interested in. So let's see how to do it.
29
+
30
+ Mark your model as a Zombie, so it can be created without all necessary attributes, but to the application it will be as if it does not even exist. When another Untied message completes this Zombie, it will not be a Zombie anymore and your application can count with it.
31
+
32
+ **Why this Zombie thing?** It's a good option to syncronize associated models, because Untied does not guarantee the messages order, so you could have an invalid reference (and you don't want it, so zombificate your models!).
33
+
34
+ ```ruby
35
+ class User < ActiveRecord::Base
36
+ include Untied::Consumer::Sync::Zombificator::ActsAsZombie
37
+ end
38
+ ```
39
+
40
+ The next step is specify which attributes you want to store for each model in your application. To do so you need to create a yml file and say to `Untied::Consumer::Sync` where it can find this.
41
+
42
+ Say to `Untied::Consumer::Sync` where the file is:
43
+
44
+ ```ruby
45
+ Untied::Consumer::Sync.configure do |config|
46
+ config.model_data = "#{Rails.root}/config/model_data.yml"
47
+ end
48
+ # Sets ActiveRecord as backend
49
+ Untied::Consumer::Sync.backend = :active_record
50
+ ```
51
+
52
+ Inform the attributes:
53
+
54
+ ```yml
55
+ User: # Payload's type
56
+ attributes: # Needed attributes
57
+ - id
58
+ - login
59
+ - first_name
60
+ - last_name
61
+ mappings:
62
+ id: core_id # Maps payload's id key to model's core_id column
63
+ name: User # Model name
64
+ ```
65
+
66
+ To know more about the options you can add to this yml, checkout this [wiki article](http://).
67
+
68
+ **Note:** If a `destroy` message comes before the `create` one for the entity, the `destoy` message will be ignored.
69
+
70
+ ## What need to be done?
71
+
72
+ - Test Zombificator::ActsAsZombie
73
+ - Test Backend::Base
74
+ - Update only the changed attributes in `after_update`
75
+ - Limit which callbacks should be listened per model
76
+ - Snake case on the yml
77
+ - Enable `check_for` to accept more than one attribute for one entity
78
+
79
+ ## Contributing
80
+
81
+ 1. Fork it
82
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
83
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
84
+ 4. Push to the branch (`git push origin my-new-feature`)
85
+ 5. Create new Pull Request
86
+
87
+
88
+ <img src="https://github.com/downloads/redu/redupy/redutech-marca.png" alt="Redu Educational Technologies" width="300">
89
+
90
+ This project is maintained and funded by [Redu Educational Techologies](http://tech.redu.com.br).
91
+
92
+ # Copyright
93
+
94
+ Copyright (c) 2012 Redu Educational Technologies
95
+
96
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
97
+
98
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
99
+
100
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,55 @@
1
+ require "untied-consumer-sync/version"
2
+ require "untied-consumer-sync/config"
3
+ require "untied-consumer-sync/payload_proccessor"
4
+ require "untied-consumer-sync/observer_helper"
5
+ require "untied-consumer-sync/zombificator"
6
+ require "untied-consumer-sync/untied_general_observer"
7
+ require "untied-consumer-sync/backend/base"
8
+
9
+ module Untied
10
+ module Consumer
11
+ module Sync
12
+ def self.config
13
+ @config ||= Config.new
14
+ end
15
+
16
+ # Configures untied-publisher. The options are defined at
17
+ # lib/untied-consumer-sync/config.rb
18
+ def self.configure(&block)
19
+ yield(config) if block_given?
20
+
21
+ if config.model_data.blank?
22
+ raise "Configure where your yml file is."
23
+ end
24
+
25
+ self.init_untied
26
+ end
27
+
28
+ # Loads model data structure
29
+ def self.model_data
30
+ @model_data ||= YAML.load_file(Sync.config.model_data)
31
+ end
32
+
33
+ # Initializes Untied Consumer
34
+ def self.init_untied
35
+ Untied::Consumer.configure do |config_untied|
36
+ config_untied.observers = [UntiedGeneralObserver]
37
+ end
38
+ end
39
+
40
+ # Sets the backend that will be used
41
+ def self.backend=(backend)
42
+ if backend.is_a? Symbol
43
+ require "untied-consumer-sync-#{backend.to_s.gsub('_', '')}/backend/#{backend}"
44
+ backend = "#{Sync}::Backend::#{backend.to_s.classify}::ModelHelper".
45
+ constantize
46
+ end
47
+ @@backend = backend
48
+ end
49
+
50
+ def self.backend
51
+ @@backend
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,103 @@
1
+ shared_examples_for 'a untied-consumer-sync backend' do
2
+ let(:subject) { described_class.new(user_config) }
3
+ let(:user_payload) { { 'my_id'=> 22, 'login' => 'sexy_jedi_3000',
4
+ 'name' => 'Luke Skywalker' } }
5
+ let(:user_config) do
6
+ {
7
+ 'attributes' => ['login', 'name', 'id'],
8
+ 'mappings' => { 'id' => 'my_id' },
9
+ 'name' => 'User'
10
+ }
11
+ end
12
+
13
+ after do
14
+ User.delete_all
15
+ end
16
+
17
+ describe '#find' do
18
+ context 'when user does not exist yet' do
19
+ it 'should not find object if it doesnt exist' do
20
+ subject.find(3333).should be_nil
21
+ end
22
+ end
23
+
24
+ context 'when user exists' do
25
+ before do
26
+ User.create do |u|
27
+ u.my_id = 1
28
+ u.login = 'luke'
29
+ end
30
+ end
31
+
32
+ it 'should find object by id' do
33
+ subject.find(1).should_not be_nil
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '#create_zombie' do
39
+ it 'should create zombie user' do
40
+ subject.create_zombie(99)
41
+ User.unscoped.where(:my_id => 99).should exist
42
+ User.unscoped.where(:my_id => 99).first.should be_zombie
43
+ end
44
+ end
45
+
46
+ describe '#create_model' do
47
+ context "with valid payload" do
48
+ context "when user does not exist yet" do
49
+ it 'should create user' do
50
+ subject.create_model(user_payload)
51
+ User.where(:my_id => user_payload['my_id']).should exist
52
+ end
53
+ end
54
+
55
+ context "when user exists" do
56
+ before do
57
+ User.unscoped.new(:my_id => user_payload['my_id']).
58
+ save(:validate => false)
59
+ end
60
+
61
+ it 'should update zombie user' do
62
+ subject.create_model(user_payload)
63
+ User.where(:my_id => user_payload['my_id']).first.should be_valid
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ describe '#update_model' do
70
+ context 'when user does not exist yet' do
71
+ it 'should create user' do
72
+ subject.update_model(user_payload)
73
+ User.where(:my_id => user_payload['my_id']).should exist
74
+ end
75
+ end
76
+
77
+ context 'when user exists' do
78
+ before do
79
+ User.unscoped.new(:my_id => user_payload['my_id']).
80
+ save(:validate => false)
81
+ end
82
+
83
+ it 'should update user on database' do
84
+ subject.update_model(user_payload)
85
+ User.where(:my_id => user_payload['my_id']).first.should be_valid
86
+ end
87
+ end
88
+ end
89
+
90
+ describe '#destroy_model' do
91
+ context 'when user exists' do
92
+ before do
93
+ User.unscoped.new(:my_id => user_payload['my_id']).
94
+ save(:validate => false)
95
+ end
96
+
97
+ it 'should delete from database' do
98
+ subject.destroy_model(user_payload)
99
+ User.where(:my_id => user_payload['my_id']).should_not exist
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,87 @@
1
+ module Untied
2
+ module Consumer
3
+ module Sync
4
+ module Backend
5
+ module Base
6
+ @@instances = {}
7
+
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ # Public: Lida com a manipulação dos modelos através de payloads.
13
+ def initialize(model_data)
14
+ @model = model_data['name'].constantize
15
+ @model_data = model_data
16
+ end
17
+
18
+ # Public: Cria um modelo zumbie temporário.
19
+ #
20
+ # id - Inteiro que indentifica o objeto de acordo com a configuração
21
+ #
22
+ # Retorna o modelo recém criado.
23
+ def create_zombie(id)
24
+ zombie = @model.unscoped.new do |z|
25
+ z.send("#{ @model_data['mappings']['id'] }=", id)
26
+ end
27
+ zombie.save(:validate => false)
28
+
29
+ zombie
30
+ end
31
+
32
+ # Public: Cria o modelo se o mesmo não existir no banco de dados.
33
+ #
34
+ # payload - Hash com os dados a serem inseridos.
35
+ #
36
+ # Retorna True se a operação for bem sucedida e False no caso contrário.
37
+ def create_model(payload)
38
+ temp_model = (find(payload[@model_data['mappings']['id']]) or
39
+ @model.unscoped.new)
40
+
41
+ # Seta os atributos
42
+ payload.each_pair { |key, value| temp_model.send("#{key.to_s}=", value) } if temp_model.zombie
43
+
44
+ temp_model.save
45
+ end
46
+
47
+ # Public: Atualiza o modelo ou o cria se o mesmo não existir no banco.
48
+ #
49
+ # payload - Hash com os dados a serem inseridos.
50
+ #
51
+ # Retorna True se a operação for bem sucedida e False no caso contrário.
52
+ def update_model(payload)
53
+ temp_model = (find(payload[@model_data['mappings']['id']]) or
54
+ @model.unscoped.new)
55
+ payload.each_pair {|key, value| temp_model.send("#{key.to_s}=", value)}
56
+
57
+ temp_model.save
58
+ end
59
+
60
+ # Public: Destroi o modelo se o mesmo não existir no banco de dados.
61
+ #
62
+ # payload - Hash com os dados do modelo.
63
+ #
64
+ # Retorna True se a operação for bem sucedida e False no caso contrário.
65
+ def destroy_model(payload)
66
+ temp_model = find(payload[@model_data['mappings']['id']])
67
+
68
+ temp_model.destroy if temp_model
69
+ end
70
+
71
+ module ClassMethods
72
+ def self.new(*args, &block)
73
+ old_instance = @@instances[args[0]['name']]
74
+ return old_instance if old_instance
75
+
76
+ obj = ModelHelper.allocate
77
+ obj.send(:initialize, *args, &block)
78
+ @@instances[args[0]['name']] = obj
79
+
80
+ obj
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'configurable'
3
+
4
+ module Untied
5
+ module Consumer
6
+ module Sync
7
+ class Config
8
+ include Configurable
9
+
10
+ # Config file with models (and its attributes) that will be sync
11
+ config :model_data, ""
12
+ # Publisher's identifier so Sync know where to listen.
13
+ # Default: untied_publisher
14
+ config :service_name, 'untied_publisher'
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,136 @@
1
+ require 'untied-consumer'
2
+
3
+ module Untied
4
+ module Consumer
5
+ module Sync
6
+ class ObserverHelper < Untied::Consumer::Observer
7
+ # Public: Helper que lida com a logica de manipulção de modelos para o untied.
8
+ # Observer do untied podem herdar dessa classe para adicionar os callbacks.
9
+
10
+ # Public: Metódo proxy que abstrai a complexidade real do create.
11
+ #
12
+ # kind - String com o nome do modelo.
13
+ # payload - Hash com os dados para a criação do modelo.
14
+ #
15
+ # Retorna True se a operação foi realizada com sucesso e False no caso
16
+ # contrário.
17
+ def create_proxy(kind, payload)
18
+ call_method("create", kind, payload)
19
+ end
20
+
21
+ # Public: Metódo proxy que abstrai a complexidade real do update.
22
+ #
23
+ # kind - String com o nome do modelo.
24
+ # payload - Hash com os dados para a criação do modelo.
25
+ #
26
+ # Retorna True se a operação foi realizada com sucesso e False no caso
27
+ # contrário.
28
+ def update_proxy(kind, payload)
29
+ call_method("update", kind, payload)
30
+ end
31
+
32
+ # Public: Metódo proxy que abstrai a complexidade real do destroy.
33
+ #
34
+ # kind - String com o nome do modelo.
35
+ # payload - Hash com os dados para a criação do modelo.
36
+ #
37
+ # Retorna True se a operação foi realizada com sucesso e False no caso.
38
+ # contrário.
39
+ def destroy_proxy(kind, payload)
40
+ call_method("destroy", kind, payload)
41
+ end
42
+
43
+ # Public: Metódo para facilitar o acesso as configurações
44
+ #
45
+ # Retorna um Hash com as configurações dos modelos
46
+ def config
47
+ Sync.model_data
48
+ end
49
+
50
+ protected
51
+
52
+ # Protected: Metodo auxiliar usado pelos proxies, codigo comum ao update, create
53
+ # e destroy.
54
+ #
55
+ # method - String com o nome do metódo a ser chamado: create, update ou destroy.
56
+ # kind - String com o nome do modelo.
57
+ # payload - Hash com os dados para a criação do modelo.
58
+ #
59
+ # Retorna o True se a operação foi realizada com sucesso e False no caso
60
+ # contrário.
61
+ def call_method(method, kind, payload)
62
+ model_data = config.fetch(kind.classify) {|k| raise "Kind #{k} not found in model_data.yml"}
63
+ model_helper = Sync.backend.new(model_data)
64
+ payload_proccessor = PayloadProccessor.new(model_data)
65
+ new_payload = payload_proccessor.proccess(payload) #Remove dados inuteis
66
+
67
+ self.send(method, new_payload, model_helper, payload_proccessor)
68
+ end
69
+
70
+ # Protected: Metódo que cria o modelo.
71
+ #
72
+ # payload - Hash com os dados para a criação do modelo.
73
+ # model_helper - ModelHelper que lida com a criação de modelos usando a payload.
74
+ # payload_proccessor - PayloadProccessor que adequa a payload remota ao banco
75
+ # local.
76
+ #
77
+ # Retorna True se a operação foi realizada com sucesso e False no caso.
78
+ # contrário.
79
+ def create(payload, model_helper, payload_proccessor)
80
+ new_payload = create_dep(payload, payload_proccessor)
81
+
82
+ model_helper.create_model(new_payload)
83
+ end
84
+
85
+ # Protected: Metódo que atauliza o modelo.
86
+ #
87
+ # payload - Hash com os dados para a criação do modelo.
88
+ # model_helper - ModelHelper que lida com a criação de modelos usando a payload.
89
+ # payload_proccessor - PayloadProccessor que adequa a payload remota ao banco
90
+ # local.
91
+ #
92
+ # Retorna True se a operação foi realizada com sucesso e False no caso.
93
+ # contrário.
94
+ def update(payload, model_helper, payload_proccessor)
95
+ new_payload = create_dep(payload, payload_proccessor)
96
+
97
+ model_helper.update_model(new_payload)
98
+ end
99
+
100
+ # Protected: Metódo que destroi o modelo.
101
+ #
102
+ # payload - Hash com os dados para a criação do modelo.
103
+ # model_helper - ModelHelper que lida com a criação de modelos usando a payload.
104
+ # payload_proccessor - PayloadProccessor que adequa a payload remota ao banco
105
+ # local.
106
+ #
107
+ # Retorna True se a operação foi realizada com sucesso e False no caso.
108
+ # contrário.
109
+ def destroy(payload, model_helper, payload_proccessor)
110
+ model_helper.destroy_model(payload)
111
+ end
112
+
113
+ # Protected: Gera as dependencias para o modelo em questão, cria modelos zombies
114
+ # se for necesário e traduz as referencias presentes no payload para
115
+ # referencias locais.
116
+ #
117
+ # payload - Hash com os dados para a criação do modelo.
118
+ # payload_proccessor - PayloadProccessor que adequa a payload remota ao banco
119
+ # local.
120
+ #
121
+ # Retorna um Hash que representa a payload com as dependencias traduzidas
122
+ def create_dep(payload, payload_proccessor)
123
+ new_payload = payload.clone
124
+ payload_proccessor.dependencies.each do |key, value|
125
+ id = payload[value]
126
+ aux_helper = Sync.backend.new(config[key.classify])
127
+ modelo = (aux_helper.find(id) or aux_helper.create_zombie(id))
128
+ new_payload.merge!(value => modelo.id)
129
+ end
130
+
131
+ new_payload
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,72 @@
1
+ module Untied
2
+ module Consumer
3
+ module Sync
4
+ class PayloadProccessor
5
+ # Public: Traduz payload recebido via Untied de acordo com os atributos e
6
+ # mapeamentos do config/model_data.yml.
7
+
8
+ @@instances = {}
9
+
10
+ def initialize(model_data)
11
+ @model_data = model_data
12
+ end
13
+
14
+ # Public: Faz o processamento geral da payload
15
+ #
16
+ # payload - Hash com os dados do modelo
17
+ #
18
+ # Retorna um novo Hash com os dados processados
19
+ def proccess(payload)
20
+ new_payload = slice_useless_attrs(payload)
21
+
22
+ map_attrs(new_payload)
23
+ end
24
+
25
+ # Public: Metódo de conveniencia para checar as dependencias
26
+ #
27
+ # Retorna um Array com as dependencias presentes na configuração do modelo
28
+ def dependencies
29
+ @model_data.fetch('check_for', [])
30
+ end
31
+
32
+ def self.new(*args, &block)
33
+ old_instance = @@instances[args[0]['name']]
34
+ return old_instance if old_instance
35
+
36
+ obj = PayloadProccessor.allocate
37
+ obj.send(:initialize, *args, &block)
38
+ @@instances[args[0]['name']] = obj
39
+ obj
40
+ end
41
+
42
+ protected
43
+
44
+ # Protected: Faz mapeamento de atributos
45
+ #
46
+ # payload - Hash com os dados do modelo
47
+ #
48
+ # Retorna um novo Hash com os atributos mapeados
49
+ def map_attrs(payload)
50
+ mappings = @model_data.fetch('mappings', {})
51
+
52
+ mappings.each do |k, v|
53
+ payload.merge!(v => payload.delete(k))
54
+ end
55
+
56
+ payload
57
+ end
58
+
59
+ # Protected: Remove atributos irrelevantes
60
+ #
61
+ # payload - Hash com os dados do modelo
62
+ #
63
+ # Retorna um novo Hash só com os atributos relevantes
64
+ def slice_useless_attrs(payload)
65
+ payload.reject do |key, value|
66
+ !@model_data['attributes'].include?(key)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,31 @@
1
+ module Untied
2
+ module Consumer
3
+ module Sync
4
+ # Observes all entities listed on config file
5
+ class UntiedGeneralObserver < ObserverHelper
6
+ def initialize
7
+ super
8
+
9
+ elements = self.config.values.collect {|v| v['name'].underscore.to_sym }
10
+ args = elements << {:from => Sync.config.service_name }
11
+ self.class.observe(*args)
12
+ end
13
+
14
+ def after_create(payload)
15
+ kind = payload.keys[0]
16
+ self.create_proxy(kind, payload.values[0])
17
+ end
18
+
19
+ def after_update(payload)
20
+ kind = payload.keys[0]
21
+ self.update_proxy(kind, payload.values[0])
22
+ end
23
+
24
+ def after_destroy(payload)
25
+ kind = payload.keys[0]
26
+ self.destroy_proxy(kind, payload.values[0])
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module Untied
2
+ module Consumer
3
+ module Sync
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ module Untied
2
+ module Consumer
3
+ module Sync
4
+ module Zombificator
5
+ module ActsAsZombie
6
+ # Modulo que adiciona suporte a modelos zombies. Se um modelo for criado sem validação
7
+ # ele automaticamente é marcado como zombie, caso a validação seja feita,
8
+ # o modelo perde essa tag se não ocorrer nenhum erro.
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ attr_accessible :zombie
13
+
14
+ # Modelos zombies não devem aparecer em consultas normais.
15
+ default_scope where(:zombie => false)
16
+
17
+ after_validation { self.zombie = false if self.errors.empty? }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+
3
+ module Untied::Consumer::Sync
4
+ describe ObserverHelper do
5
+ let(:subject){ ObserverHelper.instance }
6
+
7
+ after do
8
+ User.delete_all
9
+ end
10
+
11
+ context "#create_proxy" do
12
+ context "with a complete (valid) User" do
13
+ let(:user) { { 'id' => 1, 'login' => 'sexy_jedi_3000',
14
+ 'name' => 'Luke Skywalker', :thingy => false } }
15
+
16
+ it 'should return true' do
17
+ subject.create_proxy("user", user).should == true
18
+ end
19
+
20
+ it "should add user to database" do
21
+ expect{
22
+ subject.create_proxy("user", user)
23
+ }.to change(User, :count).by(1)
24
+ end
25
+ end
26
+
27
+ context "with, for now, invalid references" do
28
+ let(:toy) { { 'id' => 1, 'user_id' => 1 } }
29
+
30
+ it 'should return true' do
31
+ subject.create_proxy("toy", toy).should == true
32
+ end
33
+
34
+ it "should add toy to database" do
35
+ expect{
36
+ subject.create_proxy("toy", toy)
37
+ }.to change(Toy, :count).by(1)
38
+ end
39
+
40
+ it "creates zombie entity for user_id" do
41
+ subject.create_proxy("toy", toy)
42
+ User.unscoped.find_by_my_id(toy['user_id']).should_not be_nil
43
+ end
44
+
45
+ after do
46
+ Toy.delete_all
47
+ end
48
+ end
49
+
50
+ context "with a invalid User" do
51
+ let(:invalid_user) { { 'id' => 1 } }
52
+
53
+ it "should return false" do
54
+ subject.create_proxy("user", invalid_user).should be_false
55
+ end
56
+
57
+ it "shouldn't add user to database" do
58
+ expect{
59
+ subject.create_proxy("user", invalid_user)
60
+ }.to_not change(User, :count)
61
+ end
62
+ end
63
+
64
+ context "with a existent User zombie" do
65
+ before do
66
+ @user_zombie = User.unscoped.new(:my_id => 1)
67
+ @user_zombie.save(:validate => false)
68
+ end
69
+ let(:user) { { 'id' => 1, 'login' => 'sexy_jedi_3000',
70
+ 'name' => 'Luke Skywalker' } }
71
+
72
+ it 'should return true' do
73
+ subject.create_proxy("user", user).should be_true
74
+ end
75
+
76
+ it 'should complete User zombie' do
77
+ subject.create_proxy("user", user)
78
+ @user_zombie.reload
79
+ @user_zombie.login.should == user['login']
80
+ @user_zombie.name.should == user['name']
81
+ end
82
+ end
83
+ end
84
+
85
+ context "#update_proxy" do
86
+ let(:updated_user) { { 'id' => 1, 'login' => 'sexy_jedi_3000',
87
+ 'name' => 'Luke Vader' } }
88
+ before do
89
+ @user = User.create(:my_id => 1, :login => "sexy_jedi_3000",
90
+ :name => "Luke Skywalker")
91
+ end
92
+
93
+ it "should return true" do
94
+ subject.update_proxy("user", updated_user).should be_true
95
+ end
96
+
97
+ it "should update user on database" do
98
+ subject.update_proxy("user", updated_user)
99
+ @user.reload
100
+ @user.name.should == updated_user['name']
101
+ end
102
+ end
103
+
104
+ context "#destroy_proxy" do
105
+ let(:destroyed_user) { { 'id' => 1, 'login' => 'sexy_jedi_3000',
106
+ 'name' => 'Luke Skywalker' } }
107
+ before do
108
+ @user = User.create(:my_id => 1, :login => "sexy_jedi_3000",
109
+ :name => "Luke Skywalker")
110
+ end
111
+
112
+ it "should return true" do
113
+ subject.destroy_proxy("user", destroyed_user).should be_true
114
+ end
115
+
116
+ it "should erase user from database" do
117
+ subject.destroy_proxy("user", destroyed_user)
118
+ expect {
119
+ @user.reload
120
+ }.to raise_error
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ module Untied::Consumer::Sync
4
+ describe PayloadProccessor do
5
+ let(:subject) { PayloadProccessor.new(config) }
6
+ let(:config) { Untied::Consumer::Sync.model_data['User'] }
7
+
8
+ describe 'proccess' do
9
+ let(:payload) {{"id"=> 22, 'login' => 'sexy_jedi_3000',
10
+ 'name' => 'Luke Skywalker','thingy' => 'aaaa', 'useless_thing' => 2}}
11
+
12
+ it 'should translate mappings' do
13
+ new_load = subject.proccess(payload)
14
+ new_load.fetch("my_id", nil).should == payload["id"]
15
+ end
16
+
17
+ it 'should remove useless data' do
18
+ new_load = subject.proccess(payload)
19
+ new_load.fetch("useless_thing", nil).should be_nil
20
+ end
21
+
22
+ context "when there are no mappings" do
23
+ let(:subject) do
24
+ new_config = config.clone
25
+ new_config.delete(:mappings)
26
+
27
+ PayloadProccessor.new(new_config)
28
+ end
29
+
30
+ it "should not raise error" do
31
+ expect {
32
+ subject.proccess(payload)
33
+ }.to_not raise_error
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,29 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require 'untied-consumer-sync'
9
+ require 'support/setup_ar_and_schema'
10
+ require 'untied-consumer-sync/backend/backend_shared_example'
11
+
12
+ RSpec.configure do |config|
13
+
14
+ Untied::Consumer::Sync.configure do |c|
15
+ c.model_data = "spec/support/model_data.yml"
16
+ c.service_name = "my_service"
17
+ end
18
+ Untied::Consumer::Sync.backend = :active_record
19
+
20
+ config.treat_symbols_as_metadata_keys_with_true_values = true
21
+ config.run_all_when_everything_filtered = true
22
+ config.filter_run :focus
23
+
24
+ # Run specs in random order to surface order dependencies. If you find an
25
+ # order dependency and want to debug it, you can fix the order by providing
26
+ # the seed, which is printed after each run.
27
+ # --seed 1234
28
+ config.order = 'random'
29
+ end
@@ -0,0 +1,18 @@
1
+ User:
2
+ attributes:
3
+ - login
4
+ - name
5
+ - id
6
+ mappings:
7
+ id: my_id
8
+ name: User
9
+
10
+ Toy:
11
+ attributes:
12
+ - user_id
13
+ - id
14
+ mappings:
15
+ id: my_id
16
+ check_for:
17
+ User: user_id
18
+ name: Toy
@@ -0,0 +1,39 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'active_record'
3
+
4
+ module SetupActiveRecord
5
+ # Connection
6
+ ar_config = { :test => { :adapter => 'sqlite3', :database => ":memory:" } }
7
+ ActiveRecord::Base.configurations = ar_config
8
+ ActiveRecord::Base.
9
+ establish_connection(ActiveRecord::Base.configurations[:test])
10
+
11
+ # Schema
12
+ ActiveRecord::Schema.define do
13
+ create_table :users, :force => true do |t|
14
+ t.string :my_id
15
+ t.string :login
16
+ t.string :name
17
+ t.boolean :zombie, :default => true
18
+ t.timestamps
19
+ end
20
+ create_table :toys, :force => true do |t|
21
+ t.string :my_id
22
+ t.integer :user_id
23
+ t.boolean :zombie, :default => true
24
+ t.timestamps
25
+ end
26
+ end
27
+
28
+ # Models
29
+ class ::User < ActiveRecord::Base
30
+ include Untied::Consumer::Sync::Zombificator::ActsAsZombie
31
+ attr_accessible :my_id, :login, :name
32
+
33
+ validates_presence_of :login
34
+ end
35
+ class ::Toy < ActiveRecord::Base
36
+ include Untied::Consumer::Sync::Zombificator::ActsAsZombie
37
+ attr_accessible :my_id, :user_id
38
+ end
39
+ end
data/spec/sync_spec.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ module Untied::Consumer
4
+ describe Sync do
5
+ describe '.configure' do
6
+ it 'calls init_untied' do
7
+ Sync.should_receive(:init_untied)
8
+ Sync.configure
9
+ end
10
+ end
11
+
12
+ describe '.model_data' do
13
+ it 'returns the read yml' do
14
+ Sync.model_data.should == {
15
+ 'User' => {
16
+ 'attributes' => ['login', 'name', 'id'],
17
+ 'mappings' => { 'id' => 'my_id' },
18
+ 'name' => 'User'
19
+ },
20
+ 'Toy' => {
21
+ 'attributes' => ['user_id', 'id'],
22
+ 'mappings' => { 'id' => 'my_id' },
23
+ 'check_for' => { 'User' => 'user_id' },
24
+ 'name' => 'Toy'
25
+ },
26
+ }
27
+ end
28
+ end
29
+
30
+ describe '.backend=' do
31
+ before do
32
+ Sync.backend = nil
33
+ end
34
+
35
+ after do
36
+ Sync.backend = :active_record
37
+ end
38
+
39
+ let(:klass) { Class.new }
40
+
41
+ it 'sets backend to klass' do
42
+ Sync.backend = klass
43
+ Sync.backend.should == klass
44
+ end
45
+
46
+ it 'sets backend using symbol' do
47
+ Sync.backend = :active_record
48
+ Sync.backend.to_s.should == "#{Sync}::Backend::ActiveRecord" \
49
+ "::ModelHelper"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ require "untied-consumer-sync"
2
+
3
+ module Untied
4
+ module Consumer
5
+ module Sync
6
+ module Backend
7
+ module ActiveRecord
8
+ class ModelHelper
9
+ include Sync::Backend::Base
10
+
11
+ # Public: Procura o modelo pelo id.
12
+ #
13
+ # id - Inteiro que indentifica o objeto de acordo a configuração.
14
+ #
15
+ # Retorna o caso o modelo seja encontrado ou nil caso o modelo não exista
16
+ # no banco.
17
+ def find(id)
18
+ # Unscoped para encontrar zombies
19
+ @model.unscoped.send("find_by_#{@model_data['mappings']['id']}", id)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ module Untied::Consumer::Sync
4
+ describe UntiedGeneralObserver do
5
+ let(:subject) { UntiedGeneralObserver.instance }
6
+
7
+ describe ".initialize" do
8
+ before do
9
+ subject
10
+ end
11
+
12
+ # Observed classes defined at spec/support/model_data.yml
13
+ it "should define observed classes" do
14
+ subject.observed_classes.to_set.should == [:toy, :user].to_set
15
+ end
16
+
17
+ # Observed service name defined at spec_helper
18
+ it "should define observed service" do
19
+ subject.observed_service.should == :my_service
20
+ end
21
+ end
22
+
23
+ describe "#after_create" do
24
+ it "should call create_proxy with kind and payload" do
25
+ subject.should_receive(:create_proxy).with('user', { 'id' => 1})
26
+ subject.after_create({ 'user' => { 'id' => 1} })
27
+ end
28
+ end
29
+
30
+ describe "#after_update" do
31
+ it "should call update_proxy with kind and payload" do
32
+ subject.should_receive(:update_proxy).with('user', { 'id' => 1})
33
+ subject.after_update({ 'user' => { 'id' => 1} })
34
+ end
35
+ end
36
+
37
+ describe "#after_destroy" do
38
+ it "should call destroy_proxy with kind and payload" do
39
+ subject.should_receive(:destroy_proxy).with('user', { 'id' => 1})
40
+ subject.after_destroy({ 'user' => { 'id' => 1} })
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'untied-consumer-sync/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "untied-consumer-sync"
8
+ gem.version = Untied::Consumer::Sync::VERSION
9
+ gem.authors = ["Igor Calabria", "Guilherme Cavalcanti", "Tiago Ferreira", "Juliana Lucena"]
10
+ gem.email = ["igor.calabria@gmail.com", "guiocavalcanti@gmail.com", "fltiago@gmail.com", "julianalucenaa@gmail.com"]
11
+ gem.description = %q{Untied Consumer Synchronizer.}
12
+ gem.summary = %q{Process the messages comming from your Untied::Publisher and syncs it directly to the database.}
13
+ gem.homepage = "http://github.com/redu/untied-consumer-sync"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(spec)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency 'rake'
21
+ gem.add_development_dependency 'rspec'
22
+ gem.add_development_dependency 'sqlite3'
23
+ gem.add_development_dependency 'activerecord'
24
+
25
+ gem.add_runtime_dependency 'untied-consumer', '~> 0.0.5'
26
+ gem.add_runtime_dependency 'configurable'
27
+
28
+ if RUBY_VERSION < "1.9"
29
+ gem.add_development_dependency "ruby-debug"
30
+ else
31
+ gem.add_development_dependency "debugger"
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: untied-consumer-sync
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Igor Calabria
14
+ - Guilherme Cavalcanti
15
+ - Tiago Ferreira
16
+ - Juliana Lucena
17
+ autorequire:
18
+ bindir: bin
19
+ cert_chain: []
20
+
21
+ date: 2013-01-03 00:00:00 Z
22
+ dependencies:
23
+ - !ruby/object:Gem::Dependency
24
+ name: rake
25
+ prerelease: false
26
+ requirement: &id001 !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ hash: 3
32
+ segments:
33
+ - 0
34
+ version: "0"
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: sqlite3
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ type: :development
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: activerecord
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ type: :development
78
+ version_requirements: *id004
79
+ - !ruby/object:Gem::Dependency
80
+ name: untied-consumer
81
+ prerelease: false
82
+ requirement: &id005 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ hash: 21
88
+ segments:
89
+ - 0
90
+ - 0
91
+ - 5
92
+ version: 0.0.5
93
+ type: :runtime
94
+ version_requirements: *id005
95
+ - !ruby/object:Gem::Dependency
96
+ name: configurable
97
+ prerelease: false
98
+ requirement: &id006 !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ type: :runtime
108
+ version_requirements: *id006
109
+ - !ruby/object:Gem::Dependency
110
+ name: ruby-debug
111
+ prerelease: false
112
+ requirement: &id007 !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ type: :development
122
+ version_requirements: *id007
123
+ description: Untied Consumer Synchronizer.
124
+ email:
125
+ - igor.calabria@gmail.com
126
+ - guiocavalcanti@gmail.com
127
+ - fltiago@gmail.com
128
+ - julianalucenaa@gmail.com
129
+ executables: []
130
+
131
+ extensions: []
132
+
133
+ extra_rdoc_files: []
134
+
135
+ files:
136
+ - .gitignore
137
+ - .travis.yml
138
+ - Gemfile
139
+ - LICENSE.txt
140
+ - README.md
141
+ - Rakefile
142
+ - lib/untied-consumer-sync.rb
143
+ - lib/untied-consumer-sync/backend/backend_shared_example.rb
144
+ - lib/untied-consumer-sync/backend/base.rb
145
+ - lib/untied-consumer-sync/config.rb
146
+ - lib/untied-consumer-sync/observer_helper.rb
147
+ - lib/untied-consumer-sync/payload_proccessor.rb
148
+ - lib/untied-consumer-sync/untied_general_observer.rb
149
+ - lib/untied-consumer-sync/version.rb
150
+ - lib/untied-consumer-sync/zombificator.rb
151
+ - spec/observer_helper_spec.rb
152
+ - spec/payload_proccessor_spec.rb
153
+ - spec/spec_helper.rb
154
+ - spec/support/model_data.yml
155
+ - spec/support/setup_ar_and_schema.rb
156
+ - spec/sync_spec.rb
157
+ - spec/untied-consumer-sync-activerecord/backend/active_record.rb
158
+ - spec/untied_general_observer_spec.rb
159
+ - untied-consumer-sync.gemspec
160
+ homepage: http://github.com/redu/untied-consumer-sync
161
+ licenses: []
162
+
163
+ post_install_message:
164
+ rdoc_options: []
165
+
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ hash: 3
174
+ segments:
175
+ - 0
176
+ version: "0"
177
+ required_rubygems_version: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ hash: 3
183
+ segments:
184
+ - 0
185
+ version: "0"
186
+ requirements: []
187
+
188
+ rubyforge_project:
189
+ rubygems_version: 1.8.24
190
+ signing_key:
191
+ specification_version: 3
192
+ summary: Process the messages comming from your Untied::Publisher and syncs it directly to the database.
193
+ test_files:
194
+ - spec/observer_helper_spec.rb
195
+ - spec/payload_proccessor_spec.rb
196
+ - spec/spec_helper.rb
197
+ - spec/support/model_data.yml
198
+ - spec/support/setup_ar_and_schema.rb
199
+ - spec/sync_spec.rb
200
+ - spec/untied-consumer-sync-activerecord/backend/active_record.rb
201
+ - spec/untied_general_observer_spec.rb