rubysync 0.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/bin/rubysync +312 -0
- data/examples/ar_client_webapp/README +182 -0
- data/examples/ar_client_webapp/Rakefile +10 -0
- data/examples/ar_client_webapp/app/controllers/application.rb +7 -0
- data/examples/ar_client_webapp/app/controllers/user_controller.rb +5 -0
- data/examples/ar_client_webapp/app/helpers/application_helper.rb +3 -0
- data/examples/ar_client_webapp/app/helpers/user_helper.rb +2 -0
- data/examples/ar_client_webapp/app/models/user.rb +2 -0
- data/examples/ar_client_webapp/config/boot.rb +45 -0
- data/examples/ar_client_webapp/config/database.yml +36 -0
- data/examples/ar_client_webapp/config/environment.rb +60 -0
- data/examples/ar_client_webapp/config/environments/development.rb +21 -0
- data/examples/ar_client_webapp/config/environments/production.rb +18 -0
- data/examples/ar_client_webapp/config/environments/test.rb +19 -0
- data/examples/ar_client_webapp/config/routes.rb +23 -0
- data/examples/ar_client_webapp/db/migrate/001_create_users.rb +13 -0
- data/examples/ar_client_webapp/db/schema.rb +13 -0
- data/examples/ar_client_webapp/doc/README_FOR_APP +2 -0
- data/examples/ar_client_webapp/public/404.html +30 -0
- data/examples/ar_client_webapp/public/500.html +30 -0
- data/examples/ar_client_webapp/public/dispatch.cgi +10 -0
- data/examples/ar_client_webapp/public/dispatch.fcgi +24 -0
- data/examples/ar_client_webapp/public/dispatch.rb +10 -0
- data/examples/ar_client_webapp/public/favicon.ico +0 -0
- data/examples/ar_client_webapp/public/images/rails.png +0 -0
- data/examples/ar_client_webapp/public/index.html +277 -0
- data/examples/ar_client_webapp/public/javascripts/application.js +2 -0
- data/examples/ar_client_webapp/public/javascripts/controls.js +833 -0
- data/examples/ar_client_webapp/public/javascripts/dragdrop.js +942 -0
- data/examples/ar_client_webapp/public/javascripts/effects.js +1088 -0
- data/examples/ar_client_webapp/public/javascripts/prototype.js +2515 -0
- data/examples/ar_client_webapp/public/robots.txt +1 -0
- data/examples/ar_client_webapp/script/about +3 -0
- data/examples/ar_client_webapp/script/breakpointer +3 -0
- data/examples/ar_client_webapp/script/console +3 -0
- data/examples/ar_client_webapp/script/destroy +3 -0
- data/examples/ar_client_webapp/script/generate +3 -0
- data/examples/ar_client_webapp/script/performance/benchmarker +3 -0
- data/examples/ar_client_webapp/script/performance/profiler +3 -0
- data/examples/ar_client_webapp/script/plugin +3 -0
- data/examples/ar_client_webapp/script/process/inspector +3 -0
- data/examples/ar_client_webapp/script/process/reaper +3 -0
- data/examples/ar_client_webapp/script/process/spawner +3 -0
- data/examples/ar_client_webapp/script/runner +3 -0
- data/examples/ar_client_webapp/script/server +3 -0
- data/examples/ar_client_webapp/test/fixtures/users.yml +5 -0
- data/examples/ar_client_webapp/test/functional/user_controller_test.rb +18 -0
- data/examples/ar_client_webapp/test/test_helper.rb +28 -0
- data/examples/ar_client_webapp/test/unit/user_test.rb +10 -0
- data/examples/ar_webapp/README +1 -0
- data/examples/ar_webapp/Rakefile +10 -0
- data/examples/ar_webapp/app/controllers/application.rb +7 -0
- data/examples/ar_webapp/app/controllers/hobbies_controller.rb +10 -0
- data/examples/ar_webapp/app/controllers/interests_controller.rb +9 -0
- data/examples/ar_webapp/app/controllers/people_controller.rb +14 -0
- data/examples/ar_webapp/app/controllers/ruby_sync_associations_controller.rb +10 -0
- data/examples/ar_webapp/app/helpers/application_helper.rb +3 -0
- data/examples/ar_webapp/app/models/hobby.rb +5 -0
- data/examples/ar_webapp/app/models/interest.rb +6 -0
- data/examples/ar_webapp/app/models/person.rb +9 -0
- data/examples/ar_webapp/app/models/ruby_sync_association.rb +5 -0
- data/examples/ar_webapp/app/models/ruby_sync_event.rb +9 -0
- data/examples/ar_webapp/app/models/ruby_sync_observer.rb +28 -0
- data/examples/ar_webapp/app/models/ruby_sync_operation.rb +20 -0
- data/examples/ar_webapp/app/models/ruby_sync_state.rb +2 -0
- data/examples/ar_webapp/app/models/ruby_sync_value.rb +7 -0
- data/examples/ar_webapp/app/views/layouts/application.rhtml +19 -0
- data/examples/ar_webapp/app/views/people/show.rhtml +18 -0
- data/examples/ar_webapp/config/boot.rb +45 -0
- data/examples/ar_webapp/config/database.yml +36 -0
- data/examples/ar_webapp/config/environment.rb +61 -0
- data/examples/ar_webapp/config/environments/development.rb +21 -0
- data/examples/ar_webapp/config/environments/production.rb +18 -0
- data/examples/ar_webapp/config/environments/test.rb +19 -0
- data/examples/ar_webapp/config/routes.rb +23 -0
- data/examples/ar_webapp/db/migrate/001_create_people.rb +12 -0
- data/examples/ar_webapp/db/migrate/002_create_interests.rb +12 -0
- data/examples/ar_webapp/db/migrate/003_create_hobbies.rb +11 -0
- data/examples/ar_webapp/db/migrate/004_create_ruby_sync_associations.rb +18 -0
- data/examples/ar_webapp/db/migrate/005_create_ruby_sync_events.rb +16 -0
- data/examples/ar_webapp/db/migrate/006_create_ruby_sync_operations.rb +15 -0
- data/examples/ar_webapp/db/migrate/007_create_ruby_sync_values.rb +12 -0
- data/examples/ar_webapp/db/migrate/008_ruby_sync_tracking.rb +16 -0
- data/examples/ar_webapp/db/migrate/009_create_ruby_sync_states.rb +10 -0
- data/examples/ar_webapp/db/schema.rb +56 -0
- data/examples/ar_webapp/doc/README_FOR_APP +2 -0
- data/examples/ar_webapp/public/404.html +30 -0
- data/examples/ar_webapp/public/500.html +30 -0
- data/examples/ar_webapp/public/dispatch.cgi +10 -0
- data/examples/ar_webapp/public/dispatch.fcgi +24 -0
- data/examples/ar_webapp/public/dispatch.rb +10 -0
- data/examples/ar_webapp/public/favicon.ico +0 -0
- data/examples/ar_webapp/public/images/rails.png +0 -0
- data/examples/ar_webapp/public/index.html +277 -0
- data/examples/ar_webapp/public/javascripts/application.js +2 -0
- data/examples/ar_webapp/public/javascripts/controls.js +833 -0
- data/examples/ar_webapp/public/javascripts/dragdrop.js +942 -0
- data/examples/ar_webapp/public/javascripts/effects.js +1088 -0
- data/examples/ar_webapp/public/javascripts/prototype.js +2515 -0
- data/examples/ar_webapp/public/robots.txt +1 -0
- data/examples/ar_webapp/script/about +3 -0
- data/examples/ar_webapp/script/breakpointer +3 -0
- data/examples/ar_webapp/script/console +3 -0
- data/examples/ar_webapp/script/destroy +3 -0
- data/examples/ar_webapp/script/generate +3 -0
- data/examples/ar_webapp/script/performance/benchmarker +3 -0
- data/examples/ar_webapp/script/performance/profiler +3 -0
- data/examples/ar_webapp/script/plugin +3 -0
- data/examples/ar_webapp/script/process/inspector +3 -0
- data/examples/ar_webapp/script/process/reaper +3 -0
- data/examples/ar_webapp/script/process/spawner +3 -0
- data/examples/ar_webapp/script/runner +3 -0
- data/examples/ar_webapp/script/server +3 -0
- data/examples/ar_webapp/test/fixtures/association_keys.yml +5 -0
- data/examples/ar_webapp/test/fixtures/hobbies.yml +5 -0
- data/examples/ar_webapp/test/fixtures/interests.yml +5 -0
- data/examples/ar_webapp/test/fixtures/people.yml +9 -0
- data/examples/ar_webapp/test/fixtures/ruby_sync_events.yml +5 -0
- data/examples/ar_webapp/test/fixtures/ruby_sync_operations.yml +5 -0
- data/examples/ar_webapp/test/fixtures/ruby_sync_states.yml +5 -0
- data/examples/ar_webapp/test/fixtures/ruby_sync_values.yml +5 -0
- data/examples/ar_webapp/test/test_helper.rb +28 -0
- data/examples/ar_webapp/test/unit/association_key_test.rb +8 -0
- data/examples/ar_webapp/test/unit/hobby_test.rb +10 -0
- data/examples/ar_webapp/test/unit/interest_test.rb +10 -0
- data/examples/ar_webapp/test/unit/person_test.rb +10 -0
- data/examples/ar_webapp/test/unit/ruby_sync_event_test.rb +12 -0
- data/examples/ar_webapp/test/unit/ruby_sync_observer_test.rb +57 -0
- data/examples/ar_webapp/test/unit/ruby_sync_operation_test.rb +10 -0
- data/examples/ar_webapp/test/unit/ruby_sync_state_test.rb +10 -0
- data/examples/ar_webapp/test/unit/ruby_sync_value_test.rb +10 -0
- data/examples/ims2/connectors/hr_db_connector.rb +8 -0
- data/examples/ims2/connectors/my_csv_connector.rb +12 -0
- data/examples/ims2/pipelines/hr_import_pipeline.rb +33 -0
- data/examples/my_ims/connectors/corp_directory_connector.rb +12 -0
- data/examples/my_ims/connectors/finance_connector.rb +7 -0
- data/examples/my_ims/connectors/hr_db_connector.rb +7 -0
- data/examples/my_ims/pipelines/finance_pipeline.rb +33 -0
- data/examples/my_ims/pipelines/hr_import_pipeline.rb +29 -0
- data/lib/ruby_sync/connectors/active_record_connector.rb +198 -0
- data/lib/ruby_sync/connectors/base_connector.rb +317 -0
- data/lib/ruby_sync/connectors/connector_event_processing.rb +78 -0
- data/lib/ruby_sync/connectors/csv_file_connector.rb +95 -0
- data/lib/ruby_sync/connectors/file_connector.rb +74 -0
- data/lib/ruby_sync/connectors/ldap_connector.rb +192 -0
- data/lib/ruby_sync/connectors/memory_connector.rb +185 -0
- data/lib/ruby_sync/event.rb +220 -0
- data/lib/ruby_sync/operation.rb +82 -0
- data/lib/ruby_sync/pipelines/base_pipeline.rb +360 -0
- data/lib/ruby_sync/util/metaid.rb +24 -0
- data/lib/ruby_sync/util/utilities.rb +115 -0
- data/lib/ruby_sync.rb +81 -0
- data/test/hashlike_tests.rb +111 -0
- data/test/ruby_sync_test.rb +48 -0
- data/test/test_active_record_vault.rb +113 -0
- data/test/test_csv_file_connector.rb +75 -0
- data/test/test_event.rb +40 -0
- data/test/test_ldap_connector.rb +89 -0
- data/test/test_memory_connectors.rb +40 -0
- data/test/ts_rubysync.rb +20 -0
- metadata +316 -0
@@ -0,0 +1,192 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
|
+
#
|
5
|
+
# This file is part of RubySync.
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
11
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
|
14
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
15
|
+
|
16
|
+
|
17
|
+
lib_path = File.dirname(__FILE__) + '/..'
|
18
|
+
$:.unshift lib_path unless $:.include?(lib_path) || $:.include?(File.expand_path(lib_path))
|
19
|
+
|
20
|
+
require 'ruby_sync'
|
21
|
+
|
22
|
+
$VERBOSE = false
|
23
|
+
require 'net/ldap'
|
24
|
+
#$VERBOSE = true
|
25
|
+
|
26
|
+
class Net::LDAP::Entry
|
27
|
+
def to_hash
|
28
|
+
return @myhash.dup
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module RubySync::Connectors
|
33
|
+
class LdapConnector < RubySync::Connectors::BaseConnector
|
34
|
+
|
35
|
+
attr_accessor :host, :port, :bind_method, :username, :password,
|
36
|
+
:search_filter, :search_base,
|
37
|
+
:association_attribute # name of the attribute in which to store the association key(s)
|
38
|
+
|
39
|
+
def started
|
40
|
+
#TODO: If vault, check the schema to make sure that the association_attribute is there
|
41
|
+
@association_attribute ||= 'RubySyncAssociation'
|
42
|
+
end
|
43
|
+
|
44
|
+
def check
|
45
|
+
Net::LDAP.open(:host=>@host, :port=>@port, :auth=>auth) do |ldap|
|
46
|
+
ldap.search :base => @search_base, :filter => @search_filter do |entry|
|
47
|
+
operations = operations_for_entry(entry)
|
48
|
+
association_key = (is_vault?)? nil : entry.dn
|
49
|
+
yield RubySync::Event.add(self, entry.dn, association_key, operations)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Runs the query specified by the config, gets the objectclass of the first
|
55
|
+
# returned object and returns a list of its allowed attributes
|
56
|
+
def self.fields
|
57
|
+
log.warn "Fields method not yet implemented for LDAP - Sorry."
|
58
|
+
log.warn "Returning a likely sample set."
|
59
|
+
%w{ cn givenName sn }
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def stopped
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize options
|
67
|
+
super options
|
68
|
+
@bind_method ||= :simple
|
69
|
+
@host ||= 'localhost'
|
70
|
+
@port ||= 389
|
71
|
+
@search_filter ||= "cn=*"
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def self.sample_config
|
76
|
+
return <<END
|
77
|
+
options(
|
78
|
+
:host=>'localhost',
|
79
|
+
:port=>10389,
|
80
|
+
:username=>'uid=admin,ou=system',
|
81
|
+
:password=>'secret',
|
82
|
+
:search_filter=>"cn=*",
|
83
|
+
:search_base=>"dc=example,dc=com"
|
84
|
+
# :bind_method=>:simple,
|
85
|
+
)
|
86
|
+
END
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
def add(path, operations)
|
92
|
+
with_ldap do |ldap|
|
93
|
+
return false unless ldap.add :dn=>path, :attributes=>perform_operations(operations)
|
94
|
+
end
|
95
|
+
return true
|
96
|
+
rescue Net::LdapException
|
97
|
+
log.warning "Exception occurred while adding LDAP record"
|
98
|
+
false
|
99
|
+
end
|
100
|
+
|
101
|
+
def modify(path, operations)
|
102
|
+
log.debug "Modifying #{path} with the following operations:\n#{operations.inspect}"
|
103
|
+
with_ldap {|ldap| ldap.modify :dn=>path, :operations=>to_ldap_operations(operations) }
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete(path)
|
107
|
+
with_ldap {|ldap| ldap.delete :dn=>path }
|
108
|
+
end
|
109
|
+
|
110
|
+
def [](path)
|
111
|
+
with_ldap do |ldap|
|
112
|
+
result = ldap.search :base=>path, :scope=>Net::LDAP::SearchScope_BaseObject, :filter=>'objectclass=*'
|
113
|
+
return nil if !result or result.size == 0
|
114
|
+
result[0].to_hash
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def target_transform event
|
119
|
+
event.add_default 'objectclass', 'inetOrgUser'
|
120
|
+
# TODO: Add modifier and timestamp unless LDAP dir does this automatically
|
121
|
+
end
|
122
|
+
|
123
|
+
def associate_with_foreign_key key, path
|
124
|
+
with_ldap do |ldap|
|
125
|
+
ldap.add_attribute(path, @association_attribute, key.to_s)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def path_for_foreign_key key
|
130
|
+
entry = entry_for_foreign_key key
|
131
|
+
(entry)? entry.dn : nil
|
132
|
+
end
|
133
|
+
|
134
|
+
def foreign_key_for path
|
135
|
+
entry = self[path]
|
136
|
+
(entry)? entry.dn : nil # TODO: That doesn't look right. Should return an association key, not a path.
|
137
|
+
end
|
138
|
+
|
139
|
+
def remove_foreign_key key
|
140
|
+
with_ldap do |ldap|
|
141
|
+
entry = entry_for_foreign_key key
|
142
|
+
if entry
|
143
|
+
modify :dn=>entry.dn, :operations=>[ [:delete, @association_attribute, key] ]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def find_associated foreign_key
|
149
|
+
entry = entry_for_foreign_key key
|
150
|
+
(entry)? operations_for_entry(entry) : nil
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def operations_for_entry entry
|
157
|
+
# TODO: This could probably be done better by mixing Enumerable into Entry and then calling collect
|
158
|
+
ops = []
|
159
|
+
entry.each do |name, values|
|
160
|
+
ops << RubySync::Operation.add(name, values)
|
161
|
+
end
|
162
|
+
ops
|
163
|
+
end
|
164
|
+
|
165
|
+
def entry_for_foreign_key key
|
166
|
+
with_ldap do |ldap|
|
167
|
+
result = ldap.search :base=>@search_base, :filter=>"#{@association_attribute}=#{key}"
|
168
|
+
return nil if !result or result.size == 0
|
169
|
+
result[0]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
def with_ldap
|
175
|
+
result = nil
|
176
|
+
Net::LDAP.open(:host=>@host, :port=>@port, :auth=>auth) do |ldap|
|
177
|
+
result = yield ldap
|
178
|
+
end
|
179
|
+
result
|
180
|
+
end
|
181
|
+
|
182
|
+
def auth
|
183
|
+
{:method=>@bind_method, :username=>@username, :password=>@password}
|
184
|
+
end
|
185
|
+
|
186
|
+
# Produce an array of operation arrays suitable for the LDAP library
|
187
|
+
def to_ldap_operations operations
|
188
|
+
operations.map {|op| [op.type, op.subject, op.values]}
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
|
+
#
|
5
|
+
# This file is part of RubySync.
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
11
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
|
14
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
15
|
+
|
16
|
+
|
17
|
+
require "yaml"
|
18
|
+
|
19
|
+
class Object
|
20
|
+
|
21
|
+
# If not already an array, slip into one
|
22
|
+
def as_array
|
23
|
+
(instance_of? Array)? self : [self]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module RubySync::Connectors
|
28
|
+
class MemoryConnector < RubySync::Connectors::BaseConnector
|
29
|
+
|
30
|
+
def check
|
31
|
+
while event = @events.shift
|
32
|
+
yield event
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def is_echo? event
|
37
|
+
event.sets_value?(:modifier, 'rubysync')
|
38
|
+
end
|
39
|
+
|
40
|
+
def associate(association, path)
|
41
|
+
path = normalize(path)
|
42
|
+
log.info "Associating '#{association}' with '#{path}'"
|
43
|
+
entry = @data[path]
|
44
|
+
if entry
|
45
|
+
(@association_index[association.context] ||= {})[association.key] = path
|
46
|
+
(entry[:association] ||= {})[association.context] = association.key
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def path_for_association(association)
|
51
|
+
context = @association_index[association.context] || {}
|
52
|
+
context[association.key.to_s]
|
53
|
+
end
|
54
|
+
|
55
|
+
def association_key_for(context, path)
|
56
|
+
path = normalize(path)
|
57
|
+
log.debug "Retrieving association key for '#{path}' within '#{context}'"
|
58
|
+
entry = @data[path]
|
59
|
+
unless entry
|
60
|
+
p @data
|
61
|
+
end
|
62
|
+
if entry && entry[:association] && key = entry[:association][context]
|
63
|
+
return key
|
64
|
+
end
|
65
|
+
log.info "No association found."
|
66
|
+
log.debug entry.inspect
|
67
|
+
return nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def associations_for path
|
71
|
+
path = normalize path
|
72
|
+
entry = @data[path]
|
73
|
+
(entry[:association] || {}).map do |context, key|
|
74
|
+
RubySync::Association.new(context, key)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def remove_association(association)
|
79
|
+
path = path_for_association association
|
80
|
+
context = @association_index[association.context]
|
81
|
+
context.delete(association.key) if context
|
82
|
+
entry = @data[path]
|
83
|
+
if entry
|
84
|
+
entry_context = (entry[:association] || {})[association.context]
|
85
|
+
entry_context.delete(association.key) if entry_context
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def initialize options
|
91
|
+
super
|
92
|
+
@data = {}
|
93
|
+
@events = []
|
94
|
+
@association_index = {}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Normally, the add method is called by the pipeline and simply stores
|
98
|
+
# the data to the datastore and that's it.
|
99
|
+
# In this case, though, we also generate an add event.
|
100
|
+
# This simulates the likely effect that an add would have on a proper datastore
|
101
|
+
# where doing an add would very likely cause an event to be generated that the
|
102
|
+
# pipeline should rightly ignore because it's just a side-effect.
|
103
|
+
# In other words, we're simply simulating an undesirable behaviour for testing
|
104
|
+
# purposes.
|
105
|
+
def add id, operations
|
106
|
+
id = normalize id
|
107
|
+
raise Exception.new("Item already exists") if @data[id]
|
108
|
+
log.debug "Adding new record with key '#{id}'"
|
109
|
+
@data[id] = perform_operations operations
|
110
|
+
association_key = (is_vault?)? nil : [nil, own_association_key_for(id)]
|
111
|
+
log.info "#{name}: Injecting add event"
|
112
|
+
@events << RubySync::Event.add(self, id, association_key, operations.dup)
|
113
|
+
return id
|
114
|
+
end
|
115
|
+
|
116
|
+
def modify id, operations
|
117
|
+
id = normalize id
|
118
|
+
raise Exception.new("Attempting to modify non-existent record '#{id}'") unless @data[id]
|
119
|
+
perform_operations operations, @data[id]
|
120
|
+
association_key = (is_vault?)? nil : [nil, own_association_key_for(id)]
|
121
|
+
log.info "#{name}: Injecting modify event"
|
122
|
+
if is_vault?
|
123
|
+
associations_for(id).each do |association|
|
124
|
+
@events << (event = RubySync::Event.modify(self, id, association))
|
125
|
+
end
|
126
|
+
else
|
127
|
+
association = [nil, own_association_key_for(id)]
|
128
|
+
@events << (event = RubySync::Event.modify(self, id, association))
|
129
|
+
end
|
130
|
+
|
131
|
+
@events << RubySync::Event.modify(self, id, association_key, operations.dup)
|
132
|
+
return id
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def delete id
|
137
|
+
id = normalize id
|
138
|
+
unless @data[id]
|
139
|
+
log.warn "Can't delete non-existent item '#{id}'"
|
140
|
+
return
|
141
|
+
end
|
142
|
+
log.info "#{name}: Injecting delete events"
|
143
|
+
if is_vault?
|
144
|
+
associations_for(id).each do |association|
|
145
|
+
@events << (event = RubySync::Event.delete(self, id, association))
|
146
|
+
@association_index.delete association
|
147
|
+
end
|
148
|
+
else
|
149
|
+
association = [nil, own_association_key_for(id)]
|
150
|
+
@events << (event = RubySync::Event.delete(self, id, association))
|
151
|
+
end
|
152
|
+
@data.delete id
|
153
|
+
end
|
154
|
+
|
155
|
+
# Put a clue there that we did this change so that we can detect and filter
|
156
|
+
# out the echo.
|
157
|
+
def target_transform event
|
158
|
+
event.payload << RubySync::Operation.new(:add, :modifier, ['rubysync'])
|
159
|
+
end
|
160
|
+
|
161
|
+
def source_transform event
|
162
|
+
event.drop_changes_to [:modifier, :foreign_key]
|
163
|
+
end
|
164
|
+
|
165
|
+
def drop_pending_events
|
166
|
+
@events = []
|
167
|
+
end
|
168
|
+
|
169
|
+
def [](key)
|
170
|
+
@data[normalize(key)]
|
171
|
+
end
|
172
|
+
|
173
|
+
def []=(key, value)
|
174
|
+
@data[normalize(key)] = value
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def normalize(identifier)
|
180
|
+
identifier.to_s
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
@@ -0,0 +1,220 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
|
+
#
|
5
|
+
# This file is part of RubySync.
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
11
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
|
14
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
module RubySync
|
19
|
+
|
20
|
+
class Association
|
21
|
+
attr_accessor :context, # many associations will share the same context
|
22
|
+
# it is a function of pipeline and the client connector
|
23
|
+
# to which the association applies
|
24
|
+
:key # the key is unique within the context and vault
|
25
|
+
|
26
|
+
def initialize(context, key)
|
27
|
+
@context = context
|
28
|
+
@key = key
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"#{context}:#{key}"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class Event
|
38
|
+
|
39
|
+
attr_accessor :type, # delete, add, modify ...
|
40
|
+
:source,
|
41
|
+
:payload,
|
42
|
+
:source_path,
|
43
|
+
:target_path,
|
44
|
+
:association
|
45
|
+
|
46
|
+
|
47
|
+
def self.delete source, source_path, association=nil
|
48
|
+
self.new(:delete, source, source_path, association)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.add source, source_path, association=nil, payload=nil
|
52
|
+
self.new(:add, source, source_path, association, payload)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.modify source, source_path, association=nil, payload=nil
|
56
|
+
self.new(:modify, source, source_path, association, payload)
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize type, source, source_path=nil, association=nil, payload=nil
|
60
|
+
self.type = type.to_sym
|
61
|
+
self.source = source
|
62
|
+
self.source_path = source_path
|
63
|
+
self.association = make_association(association)
|
64
|
+
self.payload = payload
|
65
|
+
@target_path = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def retrieve_association(context)
|
69
|
+
if self.source.is_vault?
|
70
|
+
self.association ||= self.source.association_for(context, self.source_path)
|
71
|
+
else
|
72
|
+
if self.association # association key was supplied when the event was created
|
73
|
+
self.association.context = context # just add the context
|
74
|
+
else
|
75
|
+
key = self.source.own_association_key_for(self.source_path)
|
76
|
+
@association = Association.new(context, key)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def associated?
|
82
|
+
self.association && self.association.context && self.association.key
|
83
|
+
end
|
84
|
+
|
85
|
+
def merge other
|
86
|
+
# TODO implement merge
|
87
|
+
log.warning "Event.merge not yet implemented"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Retrieves all known values for the record affected by this event and
|
91
|
+
# sets the event's type to :add
|
92
|
+
# If the source connector doesn't implement retrieve we'll assume thats
|
93
|
+
# because it can't and that it gave us all it had to start with.
|
94
|
+
def convert_to_add
|
95
|
+
log.info "Converting '#{type}' event to add"
|
96
|
+
if (source.respond_to? :retrieve)
|
97
|
+
full = source.retrieve(source_path)
|
98
|
+
payload = full.payload
|
99
|
+
end
|
100
|
+
@type = :add
|
101
|
+
end
|
102
|
+
|
103
|
+
def convert_to_modify
|
104
|
+
log.info "Converting '#{type}' event to modify"
|
105
|
+
@type = :modify
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def to_yaml_properties
|
110
|
+
%w{ @type @source_path @target_path @association @payload}
|
111
|
+
end
|
112
|
+
|
113
|
+
# True if this event will lead to the field name given being set.
|
114
|
+
# If value is non-nil then if it will lead to it being set to
|
115
|
+
# the value given.
|
116
|
+
# Note: This implementation is not completely accurate. Just looks
|
117
|
+
# at the last operation in the payload. A better implementation would
|
118
|
+
# look at all items that affect the named field to work out the value.
|
119
|
+
def sets_value? subject, value=nil
|
120
|
+
return false if @payload == nil
|
121
|
+
@payload.reverse_each do |op|
|
122
|
+
return true if op.subject == subject.to_s && (value == nil || op.values == value.as_array)
|
123
|
+
end
|
124
|
+
return false
|
125
|
+
end
|
126
|
+
|
127
|
+
# Remove any operations from the payload that affect fields with the given key or
|
128
|
+
# keys (key can be a single field name or an array of field names).
|
129
|
+
def drop_changes_to subject
|
130
|
+
subjects = subject.as_array
|
131
|
+
uncommitted_operations
|
132
|
+
@uncommitted_operations = @uncommitted_operations.delete_if {|op| subjects.include? op.subject }
|
133
|
+
end
|
134
|
+
|
135
|
+
def drop_all_but_changes_to subject
|
136
|
+
subjects = subject.as_array.map {|s| s.to_s}
|
137
|
+
@uncommitted_operations = uncommitted_operations.delete_if {|op| !subjects.include?(op.subject.to_s)}
|
138
|
+
end
|
139
|
+
|
140
|
+
# Add a value to a given subject unless it already sets a value
|
141
|
+
def add_default field_name, value
|
142
|
+
add_value field_name, value unless sets_value? field_name
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
def add_value field_name, value
|
147
|
+
uncommitted_operations << Operation.new(:add, field_name, value.as_array)
|
148
|
+
end
|
149
|
+
|
150
|
+
def set_value field_name, value
|
151
|
+
uncommitted_operations << Operation.new(:replace, field_name, value.as_array)
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
def uncommitted_operations
|
156
|
+
@uncommitted_operations ||= @payload || []
|
157
|
+
return @uncommitted_operations
|
158
|
+
end
|
159
|
+
|
160
|
+
def uncommitted_operations= ops
|
161
|
+
@uncommitted_operations = ops
|
162
|
+
end
|
163
|
+
|
164
|
+
# Add one or more operations to the list to be performed.
|
165
|
+
# The operations won't be added to the payload until commit_changes
|
166
|
+
# is called and won't be added at all if rollback_changes is called
|
167
|
+
# first.
|
168
|
+
def append new_operations
|
169
|
+
uncommitted_operations
|
170
|
+
@uncommitted_operations += new_operations.as_array
|
171
|
+
end
|
172
|
+
|
173
|
+
# Rollback any changes that
|
174
|
+
def rollback_changes
|
175
|
+
@uncommitted_operations = nil
|
176
|
+
end
|
177
|
+
|
178
|
+
def commit_changes
|
179
|
+
if uncommitted_operations
|
180
|
+
@payload = uncommitted_operations
|
181
|
+
@uncommitted_operations = nil
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
protected
|
186
|
+
|
187
|
+
# Try to make a sensible association from the passed in object
|
188
|
+
def make_association o
|
189
|
+
if o.kind_of? Array
|
190
|
+
return Association.new(o[0],o[1])
|
191
|
+
elsif o.kind_of? RubySync::Association
|
192
|
+
return o
|
193
|
+
elsif o
|
194
|
+
return Association.new(nil, o)
|
195
|
+
else
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
|
202
|
+
# Yield to block for each operation in the payload for which the the subject is
|
203
|
+
# the specified subject
|
204
|
+
def each_operation_on subject
|
205
|
+
return unless payload
|
206
|
+
subjects = subject.as_array.map {|s| s.to_s}
|
207
|
+
payload.each do |op|
|
208
|
+
if subjects.include?(op.subject.to_s)
|
209
|
+
yield(op)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
|
+
#
|
5
|
+
# This file is part of RubySync.
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
11
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
|
14
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
15
|
+
|
16
|
+
module RubySync
|
17
|
+
|
18
|
+
# Operations that may be performed on an attribute
|
19
|
+
class Operation
|
20
|
+
|
21
|
+
attr_accessor :type, :subject, :values
|
22
|
+
|
23
|
+
|
24
|
+
def self.add subject, values
|
25
|
+
self.new(:add, subject, values)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.delete subject, values
|
29
|
+
self.new(:delete, subject, values)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.replace subject, values
|
33
|
+
self.new(:replace, subject, values)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def initialize type, subject, values
|
38
|
+
self.type = type.to_sym
|
39
|
+
self.subject = subject.to_s
|
40
|
+
self.values = values
|
41
|
+
end
|
42
|
+
|
43
|
+
remove_method :type=
|
44
|
+
def type=(type)
|
45
|
+
unless [:add, :delete, :replace].include? type.to_sym
|
46
|
+
raise Exception.new("Invalid operation type '#{value}'")
|
47
|
+
end
|
48
|
+
@type = type
|
49
|
+
end
|
50
|
+
|
51
|
+
remove_method :values=
|
52
|
+
def values=(values)
|
53
|
+
@values = values.as_array
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a duplicate of this operation but with the subject
|
57
|
+
# changed to the specified subject
|
58
|
+
def same_but_on subject
|
59
|
+
op = self.dup
|
60
|
+
op.subject = subject
|
61
|
+
op
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns a duplicate of this operation but with the type
|
65
|
+
# changed to the specified type
|
66
|
+
def same_but_as type
|
67
|
+
op = self.dup
|
68
|
+
op.type = type
|
69
|
+
op
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a duplicate of this operation but with the values
|
73
|
+
# changed to those specified
|
74
|
+
def same_but_with values
|
75
|
+
op = self.dub
|
76
|
+
op.values = values
|
77
|
+
op
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|