active_harmony 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ require 'active_harmony/synchronizable/core'
2
+ require 'active_harmony/synchronizable/mongoid'
3
+
4
+ module ActiveHarmony
5
+ module Synchronizable
6
+ end
7
+ 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
@@ -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