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.
Files changed (161) hide show
  1. data/bin/rubysync +312 -0
  2. data/examples/ar_client_webapp/README +182 -0
  3. data/examples/ar_client_webapp/Rakefile +10 -0
  4. data/examples/ar_client_webapp/app/controllers/application.rb +7 -0
  5. data/examples/ar_client_webapp/app/controllers/user_controller.rb +5 -0
  6. data/examples/ar_client_webapp/app/helpers/application_helper.rb +3 -0
  7. data/examples/ar_client_webapp/app/helpers/user_helper.rb +2 -0
  8. data/examples/ar_client_webapp/app/models/user.rb +2 -0
  9. data/examples/ar_client_webapp/config/boot.rb +45 -0
  10. data/examples/ar_client_webapp/config/database.yml +36 -0
  11. data/examples/ar_client_webapp/config/environment.rb +60 -0
  12. data/examples/ar_client_webapp/config/environments/development.rb +21 -0
  13. data/examples/ar_client_webapp/config/environments/production.rb +18 -0
  14. data/examples/ar_client_webapp/config/environments/test.rb +19 -0
  15. data/examples/ar_client_webapp/config/routes.rb +23 -0
  16. data/examples/ar_client_webapp/db/migrate/001_create_users.rb +13 -0
  17. data/examples/ar_client_webapp/db/schema.rb +13 -0
  18. data/examples/ar_client_webapp/doc/README_FOR_APP +2 -0
  19. data/examples/ar_client_webapp/public/404.html +30 -0
  20. data/examples/ar_client_webapp/public/500.html +30 -0
  21. data/examples/ar_client_webapp/public/dispatch.cgi +10 -0
  22. data/examples/ar_client_webapp/public/dispatch.fcgi +24 -0
  23. data/examples/ar_client_webapp/public/dispatch.rb +10 -0
  24. data/examples/ar_client_webapp/public/favicon.ico +0 -0
  25. data/examples/ar_client_webapp/public/images/rails.png +0 -0
  26. data/examples/ar_client_webapp/public/index.html +277 -0
  27. data/examples/ar_client_webapp/public/javascripts/application.js +2 -0
  28. data/examples/ar_client_webapp/public/javascripts/controls.js +833 -0
  29. data/examples/ar_client_webapp/public/javascripts/dragdrop.js +942 -0
  30. data/examples/ar_client_webapp/public/javascripts/effects.js +1088 -0
  31. data/examples/ar_client_webapp/public/javascripts/prototype.js +2515 -0
  32. data/examples/ar_client_webapp/public/robots.txt +1 -0
  33. data/examples/ar_client_webapp/script/about +3 -0
  34. data/examples/ar_client_webapp/script/breakpointer +3 -0
  35. data/examples/ar_client_webapp/script/console +3 -0
  36. data/examples/ar_client_webapp/script/destroy +3 -0
  37. data/examples/ar_client_webapp/script/generate +3 -0
  38. data/examples/ar_client_webapp/script/performance/benchmarker +3 -0
  39. data/examples/ar_client_webapp/script/performance/profiler +3 -0
  40. data/examples/ar_client_webapp/script/plugin +3 -0
  41. data/examples/ar_client_webapp/script/process/inspector +3 -0
  42. data/examples/ar_client_webapp/script/process/reaper +3 -0
  43. data/examples/ar_client_webapp/script/process/spawner +3 -0
  44. data/examples/ar_client_webapp/script/runner +3 -0
  45. data/examples/ar_client_webapp/script/server +3 -0
  46. data/examples/ar_client_webapp/test/fixtures/users.yml +5 -0
  47. data/examples/ar_client_webapp/test/functional/user_controller_test.rb +18 -0
  48. data/examples/ar_client_webapp/test/test_helper.rb +28 -0
  49. data/examples/ar_client_webapp/test/unit/user_test.rb +10 -0
  50. data/examples/ar_webapp/README +1 -0
  51. data/examples/ar_webapp/Rakefile +10 -0
  52. data/examples/ar_webapp/app/controllers/application.rb +7 -0
  53. data/examples/ar_webapp/app/controllers/hobbies_controller.rb +10 -0
  54. data/examples/ar_webapp/app/controllers/interests_controller.rb +9 -0
  55. data/examples/ar_webapp/app/controllers/people_controller.rb +14 -0
  56. data/examples/ar_webapp/app/controllers/ruby_sync_associations_controller.rb +10 -0
  57. data/examples/ar_webapp/app/helpers/application_helper.rb +3 -0
  58. data/examples/ar_webapp/app/models/hobby.rb +5 -0
  59. data/examples/ar_webapp/app/models/interest.rb +6 -0
  60. data/examples/ar_webapp/app/models/person.rb +9 -0
  61. data/examples/ar_webapp/app/models/ruby_sync_association.rb +5 -0
  62. data/examples/ar_webapp/app/models/ruby_sync_event.rb +9 -0
  63. data/examples/ar_webapp/app/models/ruby_sync_observer.rb +28 -0
  64. data/examples/ar_webapp/app/models/ruby_sync_operation.rb +20 -0
  65. data/examples/ar_webapp/app/models/ruby_sync_state.rb +2 -0
  66. data/examples/ar_webapp/app/models/ruby_sync_value.rb +7 -0
  67. data/examples/ar_webapp/app/views/layouts/application.rhtml +19 -0
  68. data/examples/ar_webapp/app/views/people/show.rhtml +18 -0
  69. data/examples/ar_webapp/config/boot.rb +45 -0
  70. data/examples/ar_webapp/config/database.yml +36 -0
  71. data/examples/ar_webapp/config/environment.rb +61 -0
  72. data/examples/ar_webapp/config/environments/development.rb +21 -0
  73. data/examples/ar_webapp/config/environments/production.rb +18 -0
  74. data/examples/ar_webapp/config/environments/test.rb +19 -0
  75. data/examples/ar_webapp/config/routes.rb +23 -0
  76. data/examples/ar_webapp/db/migrate/001_create_people.rb +12 -0
  77. data/examples/ar_webapp/db/migrate/002_create_interests.rb +12 -0
  78. data/examples/ar_webapp/db/migrate/003_create_hobbies.rb +11 -0
  79. data/examples/ar_webapp/db/migrate/004_create_ruby_sync_associations.rb +18 -0
  80. data/examples/ar_webapp/db/migrate/005_create_ruby_sync_events.rb +16 -0
  81. data/examples/ar_webapp/db/migrate/006_create_ruby_sync_operations.rb +15 -0
  82. data/examples/ar_webapp/db/migrate/007_create_ruby_sync_values.rb +12 -0
  83. data/examples/ar_webapp/db/migrate/008_ruby_sync_tracking.rb +16 -0
  84. data/examples/ar_webapp/db/migrate/009_create_ruby_sync_states.rb +10 -0
  85. data/examples/ar_webapp/db/schema.rb +56 -0
  86. data/examples/ar_webapp/doc/README_FOR_APP +2 -0
  87. data/examples/ar_webapp/public/404.html +30 -0
  88. data/examples/ar_webapp/public/500.html +30 -0
  89. data/examples/ar_webapp/public/dispatch.cgi +10 -0
  90. data/examples/ar_webapp/public/dispatch.fcgi +24 -0
  91. data/examples/ar_webapp/public/dispatch.rb +10 -0
  92. data/examples/ar_webapp/public/favicon.ico +0 -0
  93. data/examples/ar_webapp/public/images/rails.png +0 -0
  94. data/examples/ar_webapp/public/index.html +277 -0
  95. data/examples/ar_webapp/public/javascripts/application.js +2 -0
  96. data/examples/ar_webapp/public/javascripts/controls.js +833 -0
  97. data/examples/ar_webapp/public/javascripts/dragdrop.js +942 -0
  98. data/examples/ar_webapp/public/javascripts/effects.js +1088 -0
  99. data/examples/ar_webapp/public/javascripts/prototype.js +2515 -0
  100. data/examples/ar_webapp/public/robots.txt +1 -0
  101. data/examples/ar_webapp/script/about +3 -0
  102. data/examples/ar_webapp/script/breakpointer +3 -0
  103. data/examples/ar_webapp/script/console +3 -0
  104. data/examples/ar_webapp/script/destroy +3 -0
  105. data/examples/ar_webapp/script/generate +3 -0
  106. data/examples/ar_webapp/script/performance/benchmarker +3 -0
  107. data/examples/ar_webapp/script/performance/profiler +3 -0
  108. data/examples/ar_webapp/script/plugin +3 -0
  109. data/examples/ar_webapp/script/process/inspector +3 -0
  110. data/examples/ar_webapp/script/process/reaper +3 -0
  111. data/examples/ar_webapp/script/process/spawner +3 -0
  112. data/examples/ar_webapp/script/runner +3 -0
  113. data/examples/ar_webapp/script/server +3 -0
  114. data/examples/ar_webapp/test/fixtures/association_keys.yml +5 -0
  115. data/examples/ar_webapp/test/fixtures/hobbies.yml +5 -0
  116. data/examples/ar_webapp/test/fixtures/interests.yml +5 -0
  117. data/examples/ar_webapp/test/fixtures/people.yml +9 -0
  118. data/examples/ar_webapp/test/fixtures/ruby_sync_events.yml +5 -0
  119. data/examples/ar_webapp/test/fixtures/ruby_sync_operations.yml +5 -0
  120. data/examples/ar_webapp/test/fixtures/ruby_sync_states.yml +5 -0
  121. data/examples/ar_webapp/test/fixtures/ruby_sync_values.yml +5 -0
  122. data/examples/ar_webapp/test/test_helper.rb +28 -0
  123. data/examples/ar_webapp/test/unit/association_key_test.rb +8 -0
  124. data/examples/ar_webapp/test/unit/hobby_test.rb +10 -0
  125. data/examples/ar_webapp/test/unit/interest_test.rb +10 -0
  126. data/examples/ar_webapp/test/unit/person_test.rb +10 -0
  127. data/examples/ar_webapp/test/unit/ruby_sync_event_test.rb +12 -0
  128. data/examples/ar_webapp/test/unit/ruby_sync_observer_test.rb +57 -0
  129. data/examples/ar_webapp/test/unit/ruby_sync_operation_test.rb +10 -0
  130. data/examples/ar_webapp/test/unit/ruby_sync_state_test.rb +10 -0
  131. data/examples/ar_webapp/test/unit/ruby_sync_value_test.rb +10 -0
  132. data/examples/ims2/connectors/hr_db_connector.rb +8 -0
  133. data/examples/ims2/connectors/my_csv_connector.rb +12 -0
  134. data/examples/ims2/pipelines/hr_import_pipeline.rb +33 -0
  135. data/examples/my_ims/connectors/corp_directory_connector.rb +12 -0
  136. data/examples/my_ims/connectors/finance_connector.rb +7 -0
  137. data/examples/my_ims/connectors/hr_db_connector.rb +7 -0
  138. data/examples/my_ims/pipelines/finance_pipeline.rb +33 -0
  139. data/examples/my_ims/pipelines/hr_import_pipeline.rb +29 -0
  140. data/lib/ruby_sync/connectors/active_record_connector.rb +198 -0
  141. data/lib/ruby_sync/connectors/base_connector.rb +317 -0
  142. data/lib/ruby_sync/connectors/connector_event_processing.rb +78 -0
  143. data/lib/ruby_sync/connectors/csv_file_connector.rb +95 -0
  144. data/lib/ruby_sync/connectors/file_connector.rb +74 -0
  145. data/lib/ruby_sync/connectors/ldap_connector.rb +192 -0
  146. data/lib/ruby_sync/connectors/memory_connector.rb +185 -0
  147. data/lib/ruby_sync/event.rb +220 -0
  148. data/lib/ruby_sync/operation.rb +82 -0
  149. data/lib/ruby_sync/pipelines/base_pipeline.rb +360 -0
  150. data/lib/ruby_sync/util/metaid.rb +24 -0
  151. data/lib/ruby_sync/util/utilities.rb +115 -0
  152. data/lib/ruby_sync.rb +81 -0
  153. data/test/hashlike_tests.rb +111 -0
  154. data/test/ruby_sync_test.rb +48 -0
  155. data/test/test_active_record_vault.rb +113 -0
  156. data/test/test_csv_file_connector.rb +75 -0
  157. data/test/test_event.rb +40 -0
  158. data/test/test_ldap_connector.rb +89 -0
  159. data/test/test_memory_connectors.rb +40 -0
  160. data/test/ts_rubysync.rb +20 -0
  161. 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