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.
@@ -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