active_harmony 1.0.1
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.
- data/.bundle/config +2 -0
- data/CHANGELOG +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +62 -0
- data/LICENSE +20 -0
- data/README.markdown +110 -0
- data/Rakefile +14 -0
- data/VERSION +1 -0
- data/active_harmony.gemspec +77 -0
- data/lib/active_harmony.rb +14 -0
- data/lib/active_harmony/queue.rb +50 -0
- data/lib/active_harmony/queue_item.rb +48 -0
- data/lib/active_harmony/service.rb +293 -0
- data/lib/active_harmony/service_manager.rb +31 -0
- data/lib/active_harmony/service_url.rb +18 -0
- data/lib/active_harmony/synchronizable.rb +7 -0
- data/lib/active_harmony/synchronizable/core.rb +50 -0
- data/lib/active_harmony/synchronizable/mongoid.rb +30 -0
- data/lib/active_harmony/synchronizer.rb +131 -0
- data/lib/active_harmony/synchronizer_configuration.rb +71 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/unit/active_harmony/queue_item_spec.rb +41 -0
- data/spec/unit/active_harmony/queue_spec.rb +62 -0
- data/spec/unit/active_harmony/service_manager_spec.rb +44 -0
- data/spec/unit/active_harmony/service_spec.rb +313 -0
- data/spec/unit/active_harmony/synchronizable/core_spec.rb +53 -0
- data/spec/unit/active_harmony/synchronizable/mongoid_spec.rb +26 -0
- data/spec/unit/active_harmony/synchronizer_configuration_spec.rb +71 -0
- data/spec/unit/active_harmony/synchronizer_spec.rb +185 -0
- metadata +104 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActiveHarmony
|
2
|
+
class ServiceManager
|
3
|
+
# include Singleton
|
4
|
+
|
5
|
+
##
|
6
|
+
# Initializes new Service Manager.
|
7
|
+
def initialize
|
8
|
+
@services = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# Adds service for identifier
|
13
|
+
# @param [Service] Service
|
14
|
+
# @param [Symbol] Identifier
|
15
|
+
def add_service_for_identifier(service, identifier)
|
16
|
+
@services[identifier] = service
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Returns service for identifier
|
21
|
+
# @param [Symbol] identifier
|
22
|
+
def service_with_identifier(identifier)
|
23
|
+
service = @services[identifier]
|
24
|
+
if service
|
25
|
+
service
|
26
|
+
else
|
27
|
+
raise "There's no service with identifier #{identifier}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiveHarmony
|
2
|
+
class ServiceUrl
|
3
|
+
attr_accessor :path, :request_method
|
4
|
+
|
5
|
+
def initialize(path, request_method = :get)
|
6
|
+
@path = path
|
7
|
+
@request_method = request_method
|
8
|
+
end
|
9
|
+
|
10
|
+
def method
|
11
|
+
request_method
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
path
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module ActiveHarmony
|
2
|
+
module Synchronizable
|
3
|
+
module Core
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
cattr_accessor :synchronizer
|
8
|
+
self.synchronizer = Synchronizer.new
|
9
|
+
self.synchronizer.factory = self
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
|
14
|
+
def updates
|
15
|
+
updates = {}
|
16
|
+
changes.each do |atr, values|
|
17
|
+
updates[atr] = values[1]
|
18
|
+
end
|
19
|
+
updates
|
20
|
+
end
|
21
|
+
|
22
|
+
def contexts
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns Synchronization Queue
|
27
|
+
def queue
|
28
|
+
@queue ||= ActiveHarmony::Queue.instance
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Adds changes to queue
|
33
|
+
# @param [Boolean] Instant push, don't wait for queue
|
34
|
+
def push(instant = false)
|
35
|
+
if instant
|
36
|
+
synchronizer.push_object(self)
|
37
|
+
else
|
38
|
+
queue.queue_push(self)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Reteurns synchronizer
|
44
|
+
def synchronizer
|
45
|
+
self.class.synchronizer
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ActiveHarmony
|
2
|
+
module Synchronizable
|
3
|
+
module Mongoid
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
field :_remote_id, :type => Integer
|
8
|
+
field :_collection_order, :type => Float, :default => 9999.0
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def object_name
|
13
|
+
self.name.downcase
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_remote_id(id)
|
17
|
+
self.find(:first, :conditions => {:_remote_id => id.to_i})
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_remote_id(id)
|
22
|
+
self._remote_id = id
|
23
|
+
end
|
24
|
+
|
25
|
+
def changed_attributes
|
26
|
+
self.changes.keys
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ActiveHarmony
|
4
|
+
class Synchronizer
|
5
|
+
attr_accessor :factory, :service, :configuration
|
6
|
+
|
7
|
+
##
|
8
|
+
# Initializes new Service Synchronizer to do some
|
9
|
+
# synchronizing magic.
|
10
|
+
def initialize
|
11
|
+
@contexts = {}
|
12
|
+
@configuration = SynchronizerConfiguration.new
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns object name for currently used factory
|
17
|
+
# @return [Symbol] Object name
|
18
|
+
def object_name
|
19
|
+
@factory.object_name.to_sym
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Sets context for this synchronizer
|
24
|
+
# @param [Symbol] Name of context
|
25
|
+
# @param [String, Integer] Value
|
26
|
+
def set_context(context_name, context_value)
|
27
|
+
@contexts[context_name.to_sym] = context_value
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Takes block to configure synchronization
|
32
|
+
# @yield Configuration block
|
33
|
+
def configure
|
34
|
+
yield(@configuration)
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Pulls object from remote service
|
39
|
+
# @param [Integer] Remote ID of object.
|
40
|
+
# @return [Boolean] Result of pulling
|
41
|
+
def pull_object(id)
|
42
|
+
local_object = @factory.with_remote_id(id)
|
43
|
+
if local_object
|
44
|
+
# FIXME What if there's no local object and we still want to set some
|
45
|
+
# contexts?
|
46
|
+
@service.set_contexts(local_object.contexts)
|
47
|
+
else
|
48
|
+
local_object = @factory.new
|
49
|
+
end
|
50
|
+
local_object.before_pull(self) if local_object.respond_to?(:before_pull)
|
51
|
+
object_hash = @service.show(object_name, id)
|
52
|
+
@service.clear_contexts
|
53
|
+
local_object._remote_id = object_hash.delete('id')
|
54
|
+
fields = configuration.synchronizable_for_pull
|
55
|
+
fields.each do |key|
|
56
|
+
value = object_hash[key.to_s]
|
57
|
+
local_object.send("#{key}=", value)
|
58
|
+
end
|
59
|
+
local_object.after_pull(self) if local_object.respond_to?(:after_pull)
|
60
|
+
local_object.save
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Pushes local object to remote services.
|
65
|
+
# Er, I mean, its attributes.
|
66
|
+
# Like not object itself. Just attributes.
|
67
|
+
# @param [Object] Local object
|
68
|
+
def push_object(local_object)
|
69
|
+
object_name = @factory.object_name.to_sym
|
70
|
+
|
71
|
+
local_object.before_push(self) if local_object.respond_to?(:before_push)
|
72
|
+
|
73
|
+
changes = {}
|
74
|
+
fields = configuration.synchronizable_for_push
|
75
|
+
fields.each do |atr|
|
76
|
+
value = local_object.send(atr)
|
77
|
+
changes[atr.to_s] = value
|
78
|
+
end
|
79
|
+
|
80
|
+
@service.set_contexts(local_object.contexts)
|
81
|
+
|
82
|
+
if local_object._remote_id
|
83
|
+
@service.update(object_name, local_object._remote_id, changes)
|
84
|
+
else
|
85
|
+
result = @service.create(object_name, changes)
|
86
|
+
if result
|
87
|
+
local_object._remote_id = result['id']
|
88
|
+
fields = configuration.synchronizable_for_pull
|
89
|
+
fields.each do |atr|
|
90
|
+
local_object.write_attribute(atr, result[atr.to_s])
|
91
|
+
end
|
92
|
+
local_object.save
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
local_object.after_push(self) if local_object.respond_to?(:after_push)
|
97
|
+
@service.clear_contexts
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Pulls whole remote collection. If it cannot find
|
102
|
+
# matching local object, it will create one.
|
103
|
+
# This method is slow, useful for initial import, not
|
104
|
+
# for regular updates. For regular updates, only
|
105
|
+
# changed remote objects should be updates using pull_object
|
106
|
+
# @see pull_object
|
107
|
+
def pull_collection
|
108
|
+
@service.set_contexts(@contexts)
|
109
|
+
collection = @service.list(object_name)
|
110
|
+
@service.clear_contexts
|
111
|
+
collection.each_with_index do |remote_object_hash, index|
|
112
|
+
remote_id = remote_object_hash.delete("id")
|
113
|
+
local_object = @factory.with_remote_id(remote_id)
|
114
|
+
unless local_object
|
115
|
+
local_object = @factory.new
|
116
|
+
local_object.update_remote_id(remote_id)
|
117
|
+
end
|
118
|
+
local_object.before_pull(self) if local_object.respond_to?(:before_pull)
|
119
|
+
local_object._collection_order = index
|
120
|
+
fields = configuration.synchronizable_for_pull
|
121
|
+
fields.each do |field|
|
122
|
+
value = remote_object_hash[field.to_s]
|
123
|
+
local_object.send("#{field}=", value)
|
124
|
+
end
|
125
|
+
local_object.after_pull(self) if local_object.respond_to?(:after_pull)
|
126
|
+
local_object.save
|
127
|
+
end
|
128
|
+
collection.count
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module ActiveHarmony
|
2
|
+
class SynchronizerConfiguration
|
3
|
+
TYPE_PUSH = :push
|
4
|
+
TYPE_PULL = :pull
|
5
|
+
TYPE_ALL = :all
|
6
|
+
|
7
|
+
##
|
8
|
+
# Initializes new Synchronizer Configuration.
|
9
|
+
def initialize
|
10
|
+
@synchronizable_fields = []
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Configures field for push type of synchronization.
|
15
|
+
# @param [Symbol] Field
|
16
|
+
def push(field)
|
17
|
+
synchronize field, :type => :push
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Configures field for pull type of synchronization.
|
22
|
+
# @param [Symbol] Field
|
23
|
+
def pull(field)
|
24
|
+
synchronize field, :type => :pull
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Configures field for any type of synchronization.
|
29
|
+
# If no type is specified, defaults to TYPE_ALL.
|
30
|
+
# @param [Symbol] Field
|
31
|
+
# @param [Hash] Options
|
32
|
+
# @option Options [Symbol] :type Type of synchronization.
|
33
|
+
def synchronize(field, options = {})
|
34
|
+
options = {
|
35
|
+
:type => TYPE_ALL
|
36
|
+
}.merge(options)
|
37
|
+
|
38
|
+
@synchronizable_fields << {
|
39
|
+
:field => field,
|
40
|
+
:type => options[:type]
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Fields that should be synchronized on push type
|
46
|
+
# @return [Array<Symbol>] Fields for push
|
47
|
+
def synchronizable_for_push
|
48
|
+
synchronizable_for_types([TYPE_PUSH, TYPE_ALL])
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Fields that should be synchronized on pull type
|
53
|
+
# @return [Array<Symbol>] Fields for pull
|
54
|
+
def synchronizable_for_pull
|
55
|
+
synchronizable_for_types([TYPE_PULL, TYPE_ALL])
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Fields that should be synchronized on types specified
|
60
|
+
# in argument
|
61
|
+
# @param [Array<Symbol>] Types
|
62
|
+
# @return [Array<Symbol>] Fields
|
63
|
+
def synchronizable_for_types(types)
|
64
|
+
@synchronizable_fields.select do |field_description|
|
65
|
+
types.include?(field_description[:type])
|
66
|
+
end.collect do |field_description|
|
67
|
+
field_description[:field]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'webmock'
|
3
|
+
require 'webmock/rspec'
|
4
|
+
require 'mongoid'
|
5
|
+
|
6
|
+
Mongoid.configure do |config|
|
7
|
+
name = "active_harmony_test"
|
8
|
+
host = "localhost"
|
9
|
+
config.master = Mongo::Connection.new.db(name)
|
10
|
+
config.persist_in_safe_mode = false
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'active_harmony'
|
14
|
+
|
15
|
+
|
16
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.mock_with :mocha
|
20
|
+
config.include WebMock
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
class MyClass
|
4
|
+
include Mongoid::Document
|
5
|
+
include ActiveHarmony::Synchronizable::Core
|
6
|
+
field :foo
|
7
|
+
end
|
8
|
+
|
9
|
+
module ActiveHarmony
|
10
|
+
describe QueueItem do
|
11
|
+
let(:queue) { ActiveHarmony::Queue.instance }
|
12
|
+
|
13
|
+
context "type push" do
|
14
|
+
describe "#process_item" do
|
15
|
+
it "should tell synchronizer to push object" do
|
16
|
+
my_object = MyClass.new
|
17
|
+
my_object.foo = "bar"
|
18
|
+
my_object.save
|
19
|
+
queue.queue_push(my_object)
|
20
|
+
item = QueueItem.last
|
21
|
+
item.object_type.should == "MyClass"
|
22
|
+
my_object.synchronizer.expects(:push_object).with(my_object)
|
23
|
+
item.process_item
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "type pull" do
|
29
|
+
describe "#process_item" do
|
30
|
+
it "should tell synchronizer to pull object" do
|
31
|
+
queue.queue_pull(::MyClass, 123)
|
32
|
+
item = QueueItem.last
|
33
|
+
item.object_type.should == "MyClass"
|
34
|
+
item.kind.should == "pull"
|
35
|
+
MyClass.synchronizer.expects(:pull_object).with('123')
|
36
|
+
item.process_item
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
class Bacon
|
4
|
+
include Mongoid::Document
|
5
|
+
end
|
6
|
+
|
7
|
+
module ActiveHarmony
|
8
|
+
describe Queue do
|
9
|
+
before do
|
10
|
+
@queue = Queue.instance
|
11
|
+
@bacon = Bacon.create
|
12
|
+
end
|
13
|
+
|
14
|
+
before :each do
|
15
|
+
QueueItem.delete_all
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#queue_push' do
|
19
|
+
it 'should create queue item for push' do
|
20
|
+
@queue.queue_push(@bacon)
|
21
|
+
last_item = QueueItem.last
|
22
|
+
last_item.kind.should == 'push'
|
23
|
+
last_item.object_type.should == 'Bacon'
|
24
|
+
last_item.object_local_id.should == @bacon.id.to_s
|
25
|
+
last_item.object_remote_id.should be_nil
|
26
|
+
last_item.state.should == 'new'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#queue_pull' do
|
31
|
+
it 'should create queue item for pull' do
|
32
|
+
@queue.queue_pull(Bacon, '123')
|
33
|
+
last_item = QueueItem.last
|
34
|
+
last_item.kind.should == 'pull'
|
35
|
+
last_item.object_type.should == 'Bacon'
|
36
|
+
last_item.object_remote_id.should == '123'
|
37
|
+
last_item.state.should == 'new'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#run' do
|
42
|
+
it 'should queued items to process' do
|
43
|
+
items = (1..5).collect do
|
44
|
+
item = QueueItem.new(:state => 'new')
|
45
|
+
item.expects(:process_item)
|
46
|
+
item
|
47
|
+
end
|
48
|
+
@queue.expects(:queued_items).returns(items)
|
49
|
+
@queue.run
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#queued_items' do
|
54
|
+
it 'should return all items with state new' do
|
55
|
+
item_with_no_state = QueueItem.create(:state => nil)
|
56
|
+
item_with_new_state = QueueItem.create(:state => 'new')
|
57
|
+
queued_items = QueueItem.where(:state => "new").to_a
|
58
|
+
@queue.queued_items.to_a.should == queued_items
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|