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,360 @@
|
|
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
|
+
lib_path = File.dirname(__FILE__) + '/../..'
|
17
|
+
$:.unshift lib_path unless $:.include?(lib_path) || $:.include?(File.expand_path(lib_path))
|
18
|
+
|
19
|
+
require 'active_support'
|
20
|
+
require 'ruby_sync/util/metaid'
|
21
|
+
require 'yaml'
|
22
|
+
|
23
|
+
|
24
|
+
module RubySync
|
25
|
+
module Pipelines
|
26
|
+
|
27
|
+
# This pipeline is the base for those that synchronize content bi-directionally between
|
28
|
+
# two datastores. These are commonly used for synchronizing identity information between
|
29
|
+
# directories.
|
30
|
+
#
|
31
|
+
# One of the data-stores is called the identity-vault. This is generally the central repository for
|
32
|
+
# identity information (typically, an LDAP server or relational database).
|
33
|
+
# We'll call the other data-store the client for want of a better term. This is could be anything
|
34
|
+
# that an EndPoint has been written for (an LDAP server, a text file, an application, etc).
|
35
|
+
#
|
36
|
+
# We refer to the flow of events from the client to the identity-vault as incoming and those from
|
37
|
+
# the identity vault to the client as out-going. Methods in this class prefixed with 'in_' or 'out_'
|
38
|
+
# work on the incoming or outgoing flows respectively.
|
39
|
+
class BasePipeline
|
40
|
+
|
41
|
+
include RubySync::Utilities
|
42
|
+
|
43
|
+
def initialize
|
44
|
+
end
|
45
|
+
|
46
|
+
def name
|
47
|
+
self.class.name
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.client(connector_name, options={})
|
51
|
+
class_name = RubySync::Connectors::BaseConnector.class_name_for(connector_name)
|
52
|
+
options[:name] ||= "#{self.name}(client)"
|
53
|
+
options[:is_vault] = false
|
54
|
+
class_def 'client' do
|
55
|
+
@client ||= eval("::#{class_name}").new(options)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.vault(connector_name, options={})
|
60
|
+
class_name = RubySync::Connectors::BaseConnector.class_name_for(connector_name)
|
61
|
+
options[:name] ||= "#{self.name}(vault)"
|
62
|
+
options[:is_vault] = true
|
63
|
+
class_def 'vault' do
|
64
|
+
@vault ||= eval("::" + class_name).new(options)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.map_client_to_vault mappings
|
69
|
+
remove_method :client_to_vault_map if method_defined? :client_to_vault_map
|
70
|
+
class_def 'client_to_vault_map' do
|
71
|
+
unless @client_to_vault_map
|
72
|
+
@client_to_vault_map = {}
|
73
|
+
mappings.each {|k,v| @client_to_vault_map[k.to_s] = v.to_s}
|
74
|
+
end
|
75
|
+
@client_to_vault_map
|
76
|
+
end
|
77
|
+
unless method_defined? :vault_to_client_map
|
78
|
+
class_def 'vault_to_client_map' do
|
79
|
+
@vault_to_client_map ||= client_to_vault_map.invert
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.map_vault_to_client mappings
|
85
|
+
remove_method :vault_to_client_map if method_defined? :vault_to_client_map
|
86
|
+
class_def 'vault_to_client_map' do
|
87
|
+
unless @vault_to_client_map
|
88
|
+
@vault_to_client_map = {}
|
89
|
+
mappings.each {|k,v| @vault_to_client_map[k.to_s] = v.to_s}
|
90
|
+
end
|
91
|
+
@vault_to_client_map
|
92
|
+
end
|
93
|
+
unless method_defined? :client_to_vault_map
|
94
|
+
class_def 'client_to_vault_map' do
|
95
|
+
@client_to_vault_map ||= vault_to_client_map.invert
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.out_transform &blk
|
101
|
+
define_method :out_transform do |event|
|
102
|
+
event.meta_def :transform, &blk
|
103
|
+
event.transform
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
# Called by the identity-vault connector in the 'out' thread to process events generated
|
110
|
+
# by the identity vault.
|
111
|
+
def out_handler(event)
|
112
|
+
|
113
|
+
event.retrieve_association(association_context)
|
114
|
+
event.convert_to_modify if event.associated? and event.type == :add
|
115
|
+
|
116
|
+
hint = " (#{vault.name} => #{client.name})"
|
117
|
+
log.info "Processing out-going #{event.type} event #{hint}"
|
118
|
+
log.info YAML.dump(event)
|
119
|
+
return unless out_event_filter event
|
120
|
+
|
121
|
+
# Remove unwanted attributes
|
122
|
+
perform_transform :out_filter, event
|
123
|
+
|
124
|
+
unless event.associated?
|
125
|
+
if [:delete, :remove_association].include? event.type
|
126
|
+
log.info "#{name}: No action for #{event.type} of unassociated entry"
|
127
|
+
log.info YAML.dump(event)
|
128
|
+
return
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
if event.type == :modify
|
133
|
+
unless event.associated? and client.has_entry_for_key?(event.association.key)
|
134
|
+
event.convert_to_add
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
if event.type == :add
|
139
|
+
match = out_match(event)
|
140
|
+
log.info "Attempting to match"
|
141
|
+
if match # exactly one event record on the client matched
|
142
|
+
log.info "Match found, merging"
|
143
|
+
event.merge(match)
|
144
|
+
association = Association.new(self.association_context, match.src_path)
|
145
|
+
vault.associate asssociation, event.source_path
|
146
|
+
return
|
147
|
+
end
|
148
|
+
log.info "No match found, creating"
|
149
|
+
return unless out_create(event)
|
150
|
+
perform_transform :out_place, event
|
151
|
+
end
|
152
|
+
|
153
|
+
perform_transform :out_map_schema, event
|
154
|
+
perform_transform :out_transform, event
|
155
|
+
association_key = nil
|
156
|
+
with_rescue("#{client.name}: Processing command") do
|
157
|
+
association_key = client.process(event)
|
158
|
+
end
|
159
|
+
if association_key
|
160
|
+
association = Association.new(association_context, association_key)
|
161
|
+
with_rescue("#{client.name}: Storing association #{association} in vault") do
|
162
|
+
vault.associate(association, event.source_path)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Override to map schema from vault namespace to client namespace
|
168
|
+
# def out_map_schema event
|
169
|
+
# end
|
170
|
+
|
171
|
+
# Override to implement some kind of matching
|
172
|
+
def out_match event
|
173
|
+
log.debug "Default matching rule - source path exists on client?"
|
174
|
+
client.respond_to?(:'[]') and client[event.source_path]
|
175
|
+
false
|
176
|
+
end
|
177
|
+
|
178
|
+
# Override to restrict creation on the client
|
179
|
+
def out_create event
|
180
|
+
log.debug "Create allowed through default rule"
|
181
|
+
true
|
182
|
+
end
|
183
|
+
|
184
|
+
# Override to restrict creation on the vault
|
185
|
+
def in_create event
|
186
|
+
log.debug "Create allowed through default rule"
|
187
|
+
true
|
188
|
+
end
|
189
|
+
|
190
|
+
# Override to modify the target path for creation on the client
|
191
|
+
def out_place(event)
|
192
|
+
log.debug "Default placement rule target_path = source_path"
|
193
|
+
event.target_path = event.source_path
|
194
|
+
end
|
195
|
+
|
196
|
+
# Override to modify the target path for creation in the vault
|
197
|
+
def in_place(event)
|
198
|
+
log.debug "Default placement rule target_path = source_path"
|
199
|
+
event.target_path = event.source_path
|
200
|
+
end
|
201
|
+
|
202
|
+
def perform_transform name, event, hint=""
|
203
|
+
call_if_exists name, event, hint
|
204
|
+
event.commit_changes
|
205
|
+
log_progress name, event, hint
|
206
|
+
end
|
207
|
+
|
208
|
+
# Transform the out-going event before the client receives it
|
209
|
+
# def out_transform(event)
|
210
|
+
# end
|
211
|
+
|
212
|
+
# Execute the pipeline once then return.
|
213
|
+
# TODO Consider making this run in and out simultaneously
|
214
|
+
def run_once
|
215
|
+
log.info "Running #{name} pipeline once"
|
216
|
+
run_in_once
|
217
|
+
run_out_once
|
218
|
+
end
|
219
|
+
|
220
|
+
# Execute the in pipe once and then return
|
221
|
+
def run_in_once
|
222
|
+
log.info "Running #{name} 'in' pipeline once"
|
223
|
+
client.once_only = true
|
224
|
+
client.start {|event| in_handler(event)}
|
225
|
+
end
|
226
|
+
|
227
|
+
# Execute the out pipe once and then return
|
228
|
+
def run_out_once
|
229
|
+
log.info "Running #{name} 'out' pipeline once"
|
230
|
+
vault.once_only = true
|
231
|
+
vault.start {|event| out_handler(event)}
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
# Override to process the event generated by the publisher before any other processing is done.
|
236
|
+
# Return false to veto the event.
|
237
|
+
def out_event_filter(event); true; end
|
238
|
+
|
239
|
+
# Called by the 'in' connector in the 'in' thread to process events generated by the client.
|
240
|
+
def in_handler(event)
|
241
|
+
event.retrieve_association(association_context)
|
242
|
+
|
243
|
+
hint = " (#{client.name} => #{vault.name})"
|
244
|
+
log.info "Processing incoming #{event.type} event"+hint
|
245
|
+
log.info YAML.dump(event)
|
246
|
+
perform_transform :in_map_schema, event, hint
|
247
|
+
perform_transform :in_transform, event, hint
|
248
|
+
perform_transform :in_filter, event, hint
|
249
|
+
|
250
|
+
# The client can't really know whether its an add or a modify because it doesn't store
|
251
|
+
# the association.
|
252
|
+
if event.type == :modify
|
253
|
+
event.convert_to_add unless event.associated? and vault.find_associated(event.association)
|
254
|
+
elsif event.type == :add and event.associated? and vault.find_associated(event.association)
|
255
|
+
event.convert_to_modify
|
256
|
+
end
|
257
|
+
|
258
|
+
if event.type == :add
|
259
|
+
match = in_match(event) # exactly one event record in the vault matched
|
260
|
+
if match
|
261
|
+
event.merge(match)
|
262
|
+
return
|
263
|
+
end
|
264
|
+
|
265
|
+
if in_create(event)
|
266
|
+
perform_transform :in_place, event, hint
|
267
|
+
else
|
268
|
+
return
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
with_rescue("#{vault.name}: Processing command") {vault.process(event)}
|
273
|
+
|
274
|
+
end
|
275
|
+
|
276
|
+
# The context for all association keys used by this pipeline.
|
277
|
+
# By default, defer to the client
|
278
|
+
def association_context
|
279
|
+
@client.association_context
|
280
|
+
end
|
281
|
+
|
282
|
+
def in_match event
|
283
|
+
log.debug "Default match rule - source path exists in vault"
|
284
|
+
vault.respond_to?(:'[]') and vault[event.source_path]
|
285
|
+
end
|
286
|
+
|
287
|
+
# If client_to_vault_map is defined (usually by map_client_to_vault)
|
288
|
+
# then fix up the contents of the payload to refer to the fields by
|
289
|
+
# the names in the vault namespace
|
290
|
+
def in_map_schema event
|
291
|
+
map_schema event, client_to_vault_map if respond_to? :client_to_vault_map
|
292
|
+
end
|
293
|
+
|
294
|
+
def out_map_schema event
|
295
|
+
map_schema event, vault_to_client_map if respond_to? :vault_to_client_map
|
296
|
+
end
|
297
|
+
|
298
|
+
def map_schema event, map
|
299
|
+
return unless map and event.payload
|
300
|
+
event.payload.each do |op|
|
301
|
+
op.subject = map[op.subject] || op.subject if op.subject
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
|
307
|
+
|
308
|
+
# Override to perform whatever transformation on the event is required
|
309
|
+
#def in_transform(event); event; end
|
310
|
+
|
311
|
+
# Convert fields in the incoming event to those used by the identity vault
|
312
|
+
#def in_map_schema(event); end
|
313
|
+
|
314
|
+
# Specify which fields will be allowed through the incoming filter
|
315
|
+
# If nil (the default), all fields are allowed.
|
316
|
+
def self.allow_in *fields
|
317
|
+
class_def 'allowed_in' do
|
318
|
+
fields.map {|f| f.to_s}
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# default allowed_in in case allow_in doesn't get called
|
323
|
+
def allowed_in; nil; end
|
324
|
+
|
325
|
+
# Default method for allowed_in. Override by calling allow_in
|
326
|
+
#def allowed_in; false; end
|
327
|
+
def in_filter(event)
|
328
|
+
if allowed_in
|
329
|
+
event.drop_all_but_changes_to allowed_in
|
330
|
+
else
|
331
|
+
event
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
|
336
|
+
# Specify which fields will be allowed through the incoming filter
|
337
|
+
# If nil (the default), all fields are allowed.
|
338
|
+
def self.allow_out *fields
|
339
|
+
class_def 'allowed_out' do
|
340
|
+
fields.map {|f| f.to_s }
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# default allowed_out in case allow_out doesn't get called
|
345
|
+
def allowed_out; nil; end
|
346
|
+
|
347
|
+
# Default method for allowed_out. Override by calling allow_in
|
348
|
+
#def allowed_out; false; end
|
349
|
+
def out_filter(event)
|
350
|
+
if allowed_out
|
351
|
+
event.drop_all_but_changes_to allowed_out
|
352
|
+
else
|
353
|
+
event
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This file is extracted from an article published by "why the lucky stiff" at
|
4
|
+
# http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
|
5
|
+
#
|
6
|
+
# Hopefully Why will publish a meta-programming gem later and RubySync can
|
7
|
+
# just make that a dependency.
|
8
|
+
#
|
9
|
+
|
10
|
+
class Object
|
11
|
+
# The hidden singleton lurks behind everyone
|
12
|
+
def metaclass; class << self; self; end; end
|
13
|
+
def meta_eval(&blk); metaclass.instance_eval(&blk); end
|
14
|
+
|
15
|
+
# Adds methods to a metaclass
|
16
|
+
def meta_def name, &blk
|
17
|
+
meta_eval { define_method name, &blk }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Defines an instance method within a class
|
21
|
+
def class_def name, &blk
|
22
|
+
class_eval { define_method name, &blk }
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,115 @@
|
|
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 'fileutils'
|
18
|
+
require 'irb'
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
# Generally useful methods
|
23
|
+
module RubySync
|
24
|
+
module Utilities
|
25
|
+
|
26
|
+
|
27
|
+
# Perform an action and rescue any exceptions thrown, display the exception with the specified text
|
28
|
+
def with_rescue text
|
29
|
+
begin
|
30
|
+
yield
|
31
|
+
rescue Exception => exception
|
32
|
+
log.warn "#{text}: #{exception.message}"
|
33
|
+
log.debug exception.backtrace.join("\n")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def call_if_exists(method, event, hint="")
|
38
|
+
result = nil
|
39
|
+
if respond_to? method
|
40
|
+
with_rescue("#{method} #{hint}") {result = send method, event}
|
41
|
+
else
|
42
|
+
log.debug "No #{method}(event) method, continuing #{hint}"
|
43
|
+
end
|
44
|
+
return result
|
45
|
+
end
|
46
|
+
|
47
|
+
def log_progress last_action, event, hint=""
|
48
|
+
log.info "Result of #{last_action}: #{hint}\n" + YAML.dump(event)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# Ensure that a given path exists as a directory
|
53
|
+
def ensure_dir_exists path
|
54
|
+
raise Exception.new("Can't create nil directory") unless path
|
55
|
+
if File.exist? path
|
56
|
+
unless File.directory? path
|
57
|
+
raise Exception.new("'#{path}' exists but is not a directory")
|
58
|
+
end
|
59
|
+
else
|
60
|
+
log.info "Creating directory '#{path}'"
|
61
|
+
FileUtils.mkpath path
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def pipeline_called name
|
66
|
+
something_called name, "pipeline"
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def connector_called name
|
71
|
+
something_called name, "connector"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Locates and returns an instance of a class for
|
75
|
+
# the given name.
|
76
|
+
def something_called name, extension
|
77
|
+
filename = "#{name.to_s}_#{extension}"
|
78
|
+
$".include?(filename) or require filename or return nil
|
79
|
+
eval(filename.camelize).new
|
80
|
+
end
|
81
|
+
|
82
|
+
# Ensure that path is in the search path
|
83
|
+
# prepends it if it's not
|
84
|
+
def include_in_search_path path
|
85
|
+
path = File.expand_path(path)
|
86
|
+
$:.unshift path unless $:.include?(path)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return the base_path
|
90
|
+
def base_path
|
91
|
+
@base_path = find_base_path unless defined? @base_path
|
92
|
+
@base_path
|
93
|
+
end
|
94
|
+
|
95
|
+
# Locate a configuration directory by checking the current directory and
|
96
|
+
# all of it's ancestors until it finds one that looks like a rubysync configuration
|
97
|
+
# directory.
|
98
|
+
# Returns false if no suitable directory was found
|
99
|
+
def find_base_path
|
100
|
+
base_path = File.expand_path(".")
|
101
|
+
last = nil
|
102
|
+
# Keep going up until we start repeating ourselves
|
103
|
+
while File.directory?(base_path) && base_path != last && base_path != "/"
|
104
|
+
return base_path if File.directory?("#{base_path}/pipelines") &&
|
105
|
+
File.directory?("#{base_path}/connectors")
|
106
|
+
last = base_path
|
107
|
+
base_path = File.expand_path("#{base_path}/..")
|
108
|
+
end
|
109
|
+
return false
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
data/lib/ruby_sync.rb
ADDED
@@ -0,0 +1,81 @@
|
|
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
|
+
lib_path = File.dirname(__FILE__)
|
17
|
+
$:.unshift lib_path unless $:.include?(lib_path) || $:.include?(File.expand_path(lib_path))
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'active_support'
|
21
|
+
require 'ruby_sync/util/utilities'
|
22
|
+
#require 'ruby_sync/connectors/base_connector'
|
23
|
+
#require 'ruby_sync/pipelines/base_pipeline'
|
24
|
+
require 'ruby_sync/operation'
|
25
|
+
require 'ruby_sync/event'
|
26
|
+
|
27
|
+
# Make the log method globally available
|
28
|
+
class Object
|
29
|
+
|
30
|
+
def log
|
31
|
+
unless defined? @@log
|
32
|
+
@@log = Logger.new(STDOUT)
|
33
|
+
#@@log.level = Logger::DEBUG
|
34
|
+
@@log.datetime_format = "%H:%M:%S"
|
35
|
+
end
|
36
|
+
@@log
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Configuration
|
41
|
+
|
42
|
+
include RubySync::Utilities
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
include_in_search_path "#{base_path}/pipelines"
|
46
|
+
include_in_search_path "#{base_path}/connectors"
|
47
|
+
|
48
|
+
lib_path = File.dirname(__FILE__)
|
49
|
+
require_all_in_dir "#{lib_path}/ruby_sync/connectors", "*_connector.rb"
|
50
|
+
require_all_in_dir "#{lib_path}/ruby_sync/pipelines", "*_pipeline.rb"
|
51
|
+
end
|
52
|
+
|
53
|
+
# We find the first directory in the search path that is a parent of the specified
|
54
|
+
# directory and do our requires relative to that in order to increase the likelihood
|
55
|
+
# that duplicate requires will be recognised.
|
56
|
+
def require_all_in_dir(dir, glob="*.rb")
|
57
|
+
expanded = File.expand_path dir
|
58
|
+
base = $:.detect do |path_dir|
|
59
|
+
expanded_pd = File.expand_path(path_dir)
|
60
|
+
expanded[0, expanded_pd.length] == expanded_pd
|
61
|
+
end
|
62
|
+
prefix = (base)? expanded[File.expand_path(base).length+1, expanded.length]+"/" : ""
|
63
|
+
|
64
|
+
# puts $:.join "\n"
|
65
|
+
# puts "expanded = '#{expanded}'"
|
66
|
+
# puts "base = '#{base}'"
|
67
|
+
# puts "prefix = '#{prefix}'"
|
68
|
+
|
69
|
+
Dir.chdir dir do |cwd|
|
70
|
+
Dir.glob(glob) do |filename|
|
71
|
+
require prefix + filename
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
Configuration.new
|
79
|
+
|
80
|
+
|
81
|
+
|
@@ -0,0 +1,111 @@
|
|
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
|
+
# Tests that assume that the connectors can be treated like hashes. That is, they can be accessed
|
18
|
+
# using the [] and []= operators.
|
19
|
+
module HashlikeTests
|
20
|
+
|
21
|
+
# Override this if :bob isn't a good path for your directory
|
22
|
+
# def path
|
23
|
+
# :bob
|
24
|
+
# end
|
25
|
+
|
26
|
+
def client_path
|
27
|
+
'bob'
|
28
|
+
end
|
29
|
+
|
30
|
+
def vault_path
|
31
|
+
'bob'
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_client_to_vault
|
35
|
+
banner "test_client_to_vault"
|
36
|
+
assoc_key = @client.add client_path, @client.create_operations_for(@bob_details)
|
37
|
+
assert_not_nil @client.entry_for_own_association_key(assoc_key)
|
38
|
+
assert_nil @vault[vault_path], "Vault already contains bob"
|
39
|
+
@pipeline.run_once
|
40
|
+
assert_not_nil @vault[vault_path], "#{vault_path} wasn't created on the vault"
|
41
|
+
assert_equal @bob_details, @vault[vault_path].reject {|k,v| ['modifier',:association].include? k}
|
42
|
+
if @client.respond_to? :delete
|
43
|
+
@client.delete client_path
|
44
|
+
assert_equal @bob_details, @vault[vault_path].reject {|k,v| ['modifier',:association].include? k}
|
45
|
+
assert_nil @client[client_path], "Bob wasn't deleted from the client"
|
46
|
+
@pipeline.run_once
|
47
|
+
assert_nil @client[client_path], "Bob reappeared on the client"
|
48
|
+
assert_nil @vault[vault_path], "Bob wasn't deleted from the vault"
|
49
|
+
@pipeline.run_once # run again in case of echoes
|
50
|
+
assert_nil @client[client_path], "Bob reappeared on the client"
|
51
|
+
assert_nil @vault[vault_path], "Bob reappeared in the vault. He may have been created by an echoed add event"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def test_vault_to_client
|
57
|
+
banner "test_vault_to_client"
|
58
|
+
@vault.add vault_path, @vault.create_operations_for(@bob_details)
|
59
|
+
assert_nil @client[client_path], "Client already contains bob"
|
60
|
+
@pipeline.run_once
|
61
|
+
assert_not_nil @client[client_path], "#{client_path} wasn't created on the client"
|
62
|
+
assert_equal normalise(@bob_details), normalise(@client[client_path])
|
63
|
+
@vault.delete vault_path
|
64
|
+
assert_equal normalise(@bob_details), normalise(@client[client_path])
|
65
|
+
assert_nil @vault[vault_path], "Bob wasn't deleted from the vault"
|
66
|
+
assert_not_nil @client[client_path], "Bob was deleted from the client before we ran the pipe"
|
67
|
+
@pipeline.run_once
|
68
|
+
assert_nil @client[client_path], "Bob wasn't deleted from the client"
|
69
|
+
@pipeline.run_once # run again in case there were unhandled echos
|
70
|
+
assert_nil @client[client_path], "Bob is back in client, he may have been recreated by an echoed add event"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Names of attributes that can't be synched
|
74
|
+
def unsynchable
|
75
|
+
[:modifier]
|
76
|
+
end
|
77
|
+
|
78
|
+
def normalise details
|
79
|
+
normal = {}
|
80
|
+
@unsynchable ||= unsynchable.map{|u| u.to_s.downcase}
|
81
|
+
details.each_pair do |k,v|
|
82
|
+
key = k.to_s.downcase
|
83
|
+
unless @unsynchable.include?(key)
|
84
|
+
normal[key] = v
|
85
|
+
# puts "[#{@unsynchable.join ','}] doesn't include '#{key}'"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
normal
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
def test_vault
|
93
|
+
banner :test_vault
|
94
|
+
assert @vault.can_act_as_vault?
|
95
|
+
assert @vault.is_vault?
|
96
|
+
@vault.add vault_path, @vault.create_operations_for(@bob_details)
|
97
|
+
association = RubySync::Association.new(@pipeline.association_context, 'blah')
|
98
|
+
@vault.associate association, vault_path
|
99
|
+
assert_equal vault_path, @vault.path_for_association(association)
|
100
|
+
assert_equal 'blah', @vault.association_key_for(@pipeline.association_context, vault_path)
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_perform_operations
|
104
|
+
banner :test_perform_operations
|
105
|
+
result = @vault.perform_operations [
|
106
|
+
RubySync::Operation.new(:add, :name, 'Fred'),
|
107
|
+
RubySync::Operation.new(:add, :email, 'fred@test.com')
|
108
|
+
]
|
109
|
+
assert_equal({'name'=>['Fred'], 'email'=>['fred@test.com']}, result)
|
110
|
+
end
|
111
|
+
end
|