xml_active 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -84,7 +84,7 @@ XML Active relies on the XML documents created by ActiveRecord in that it utilis
84
84
 
85
85
  === Database Schema
86
86
 
87
- XML Active will detect the primary key of the tables in your schema even if is a name other than @id@. It will use the primary key to match records for update or deletion or creation when they don't exist. Given this it's very important that the tables that you wish to synchronise have a primary key with auto increment. The good news is by default Rails will create a primary key field called @id@ that is auto-increment. At this time XML Active doesn't support compound primary keys.
87
+ XML Active will detect the primary key of the tables in your schema even if is a name other than <b>id</b>. It will use the primary key to match records for update or deletion or creation when they don't exist. Given this it's very important that the tables that you wish to synchronise have a primary key with auto increment. The good news is by default Rails will create a primary key field called <b>id</b> that is auto-increment. At this time XML Active doesn't support compound primary keys.
88
88
 
89
89
  === The Model
90
90
 
@@ -110,7 +110,7 @@ At this stage XML Active has been tested with a limited number of associations.
110
110
 
111
111
  == Examples
112
112
 
113
- Now to the meaty part, the examples. XML Active currenly uses "Nokogiri":http://nokogiri.org/ to do its XML parsing so you can provide either a @Nokogiri::XML::Element@ or raw XML.
113
+ Now to the meaty part, the examples. XML Active currenly uses "Nokogiri"(http://nokogiri.org/) to do its XML parsing so you can provide either a <b>Nokogiri::XML::Element</b> or raw XML.
114
114
 
115
115
  === New Functions
116
116
 
@@ -154,11 +154,57 @@ Following is an example:
154
154
 
155
155
  You can combine any of the options (:create, :update or :destroy) and XML Active will only perform the actions provided in the options. However if you use the :sync option then all other options are ignored. Following are the current options:
156
156
 
157
- * @:update@ records in the database that match those in the provided XML document based on classes in the model, associations and the primary key.
157
+ * <b>:update</b> records in the database that match those in the provided XML document based on classes in the model, associations and the primary key.
158
158
 
159
- * @:create@ records in the database that exist in the provided XML document but not in the database. Matching is based on classes in the model, associations and the primary key.
159
+ * <b>:create</b> records in the database that exist in the provided XML document but not in the database. Matching is based on classes in the model, associations and the primary key.
160
160
 
161
- * @:destroy@ records in the database that don't exist in the provided XML document. Matching is based on classes in the model, associations and the primary key. This respects validation by using the ActiveRecord destroy rather than delete.
161
+ * <b>:destroy</b> records in the database that don't exist in the provided XML document. Matching is based on classes in the model, associations and the primary key. This respects validation by using the ActiveRecord destroy rather than delete.
162
162
 
163
- * @:sync@ is really the combination of :create, :update and :destroy. Using this option will cause XML Active to ignore :create, :update and :destroy options and will proceed to make your database records match those in the XML document.
163
+ * <b>:sync</b> is really the combination of :create, :update and :destroy. Using this option will cause XML Active to ignore :create, :update and :destroy options and will proceed to make your database records match those in the XML document.
164
164
 
165
+ === One to One Associations
166
+ XML Active supports has_one association with all options and following arr the behaviours of XML Active with each option:
167
+
168
+ <b>:update</b>
169
+
170
+ * <i>Record exists in XML but not in DB</i>: No Action
171
+ * <i>Record exists in DB but not in XML</i>: No Action
172
+ * <i>Record exists in XML and in DB</i>: DB record is updated if identifying features (eg id) match
173
+
174
+ <b>:create</b>
175
+
176
+ * <i>Record exists in XML but not in DB</i>: A new record in the DB is created
177
+ * <i>Record exists in DB but not in XML</i>: No Action
178
+ * <i>Record exists in XML and in DB</i>: No Action
179
+
180
+ <b>:destroy</b>
181
+
182
+ * <i>Record exists in XML but not in DB</i>: No Action
183
+ * <i>Record exists in DB but not in XML</i>: The DB Record is Destroyed
184
+ * <i>Record exists in XML and in DB</i>: DB record is destroyed if identifying features (eg id) don't match
185
+
186
+ <b>:sync</b>
187
+
188
+ * <i>Record exists in XML but not in DB</i>: A new record in the DB is created
189
+ * <i>Record exists in DB but not in XML</i>: The DB Record is Destroyed
190
+ * <i>Record exists in XML and in DB</i>: DB record is replaced if identifying features (eg id) don't match but if they do then the DB record is updated
191
+
192
+ === Versions
193
+
194
+ * 0.0.2 First Release
195
+ * 0.0.3 Addition of support for One to One associations and various bug fixes
196
+
197
+ === Future Features
198
+
199
+ * <b>Dry Run</b> allow xml_active to do a dry run and report potentail changes rather than making them
200
+ * <b>Events</b> Event hooks in key locations
201
+ * <b>Selective Upates</b> Specify a set of active record objects to effect and leave all the rest alone
202
+ * <b>Many to Many</b> Tested support for has_and_belongs_to_many
203
+ * <b>Polymorphic Associations</b> Tested support for these associations
204
+
205
+ === Testing
206
+ In order to perform testing I created an application specifically for the task. The resulting application is stored on GitHub: https://github.com/michael-harrison/xml_active_bubble
207
+
208
+ At this stage xml_active has been tested with Rails 3.0.7, 3.1.0 and 3.2.2
209
+
210
+ This testing will be incorporated in the primary repo in the next major release
data/lib/xml_active.rb CHANGED
@@ -23,21 +23,36 @@ module XmlActive
23
23
  end
24
24
 
25
25
  records = []
26
- if self.xml_node_matches_many_of_class(current_node)
26
+ if self.name.pluralize.underscore.eql? current_node.name.underscore
27
27
  ids = []
28
- if (self.xml_node_is_association(current_node))
28
+
29
+ if current_node.attributes['type'].try(:value) == "array"
29
30
  current_node.element_children.each do |node|
30
31
  record = self.one_from_xml(node, options)
31
- ids[ids.length] = record[primary_key.to_sym]
32
- records[records.length] = record
32
+ if record
33
+ ids[ids.length] = record[primary_key.to_sym]
34
+ records[records.length] = record
35
+ end
33
36
  end
34
37
  else
35
- records[records.length] = self.one_from_xml(current_node)
38
+ records[records.length] = self.one_from_xml current_node
36
39
  end
37
40
 
38
- if ids.length > 0 and (options.include?(:destroy) or options.include?(:sync))
39
- self.destroy_all [self.primary_key.to_s + " not in (?)", ids.collect]
41
+
42
+ if options.include?(:sync)
43
+ if ids.length > 0
44
+ self.destroy_all [self.primary_key.to_s + " not in (?)", ids.collect]
45
+ end
46
+ elsif options.include?(:destroy)
47
+ if ids.length > 0
48
+ self.destroy_all [self.primary_key.to_s + " not in (?)", ids.collect]
49
+ else
50
+ self.destroy_all
51
+ end
40
52
  end
53
+
54
+ elsif self.name.underscore.equ current_node.name.underscore
55
+ raise "The supplied XML (#{current_node.name}) is a single instance of '#{self.name}'. Please use one_from_xml"
41
56
  else
42
57
  raise "The supplied XML (#{current_node.name}) cannot be mapped to this class (#{self.name})"
43
58
  end
@@ -46,60 +61,150 @@ module XmlActive
46
61
  end
47
62
 
48
63
  def one_from_xml(xml, options = [])
49
- if xml.is_a?(String)
50
- doc = Nokogiri::XML(xml)
64
+ if xml.is_a? String
65
+ doc = Nokogiri::XML xml
51
66
  current_node = doc.children.first
52
67
  else
53
68
  current_node = xml
54
69
  end
55
70
 
56
- if self.xml_node_matches_single_class(current_node)
71
+ if xml_node_matches_class(current_node)
72
+ # Load or create a new record
57
73
  pk_value = 0
58
- pk_node = current_node.xpath(self.primary_key.to_s)
74
+ pk_node = current_node.xpath self.primary_key.to_s
59
75
  if pk_node
60
76
  begin
61
- ar = find(pk_node.text)
77
+ ar = find pk_node.text
62
78
  pk_value = pk_node.text
63
79
  rescue
64
80
  # No record exists, create a new one
65
- ar = self.new
81
+ if options.include?(:sync) or options.include?(:create)
82
+ ar = self.new
83
+ else
84
+ # must have only have :destroy and/or :update so exit
85
+ return nil
86
+ end
66
87
  end
67
88
  else
68
89
  # No primary key value, must be a new record
69
- ar = self.new
90
+ if options.include?(:sync) or options.include?(:create)
91
+ ar = self.new
92
+ else
93
+ # must have only have :destroy and/or :update so exit
94
+ return nil
95
+ end
70
96
  end
71
97
 
72
- if (ar.new_record? and options.include?(:update) and not options.include?(:sync))
73
- return(nil)
74
- end
98
+ # Check through associations and apply sync appropriately
99
+ self.reflect_on_all_associations.each do |association|
100
+ if ActiveRecord::Reflection::AssociationReflection.method_defined? :foreign_key
101
+ # Support for Rails 3.1 and later
102
+ foreign_key = association.foreign_key
103
+ elsif ActiveRecord::Reflection::AssociationReflection.method_defined? :primary_key_name
104
+ # Support for Rails earlier than 3.1
105
+ foreign_key = association.primary_key_name
106
+ else
107
+ raise "Unsupported version of ActiveRecord. Unable to identify the foreign key."
108
+ end
109
+ case
110
+ when association.macro == :has_many, association.macro == :has_and_belongs_to_many
111
+ # Check to see if xml contains elements for the association
112
+ if pk_value == 0
113
+ containers = current_node.xpath("//#{self.name.underscore}[#{self.primary_key}=#{pk_node.text}]/#{association.name}")
114
+ else
115
+ containers = current_node.xpath("//#{self.name.underscore}[#{self.primary_key}=#{pk_value}]/#{association.name}")
116
+ end
117
+ if containers.count > 0
118
+ container = containers[0]
119
+ klass = association.klass
120
+ child_ids = []
121
+ container.element_children.each do |single_obj|
122
+ # TODO: Allow for child node that doesn't have a primary key value
123
+ child_ids[child_ids.length] = single_obj.xpath(self.primary_key.to_s).text
124
+ new_record = klass.one_from_xml(single_obj, options)
125
+ if (new_record != nil)
126
+ ar.__send__(container.name.underscore.to_sym) << new_record
127
+ end
128
+ end
75
129
 
76
- current_node.element_children.each do |node|
77
- sym = node.name.underscore.to_sym
78
- if self.xml_node_is_association(node)
79
- # Association
80
- association = self.reflect_on_association(sym)
81
- if (association)
82
- # association exists, lets process it
130
+ if pk_value != 0
131
+ if options.include?(:sync)
132
+ if child_ids.length > 0
133
+ klass.destroy_all [klass.primary_key.to_s + " not in (?) and #{foreign_key} = ?", child_ids.collect, pk_value]
134
+ end
135
+ elsif options.include?(:destroy)
136
+ if child_ids.length > 0
137
+ klass.destroy_all [klass.primary_key.to_s + " not in (?) and #{foreign_key} = ?", child_ids.collect, pk_value]
138
+ else
139
+ klass.destroy_all
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ when association.macro == :has_one
146
+ single_objects = current_node.xpath("//#{self.name.underscore}[#{self.primary_key}=#{pk_value}]/#{association.name}")
83
147
  klass = association.klass
84
- child_ids = []
85
- node.element_children.each do |single_obj|
86
- child_ids[child_ids.length] = single_obj.xpath(self.primary_key.to_s).text
87
- new_record = klass.one_from_xml(single_obj, options)
88
- if (new_record != nil)
89
- ar.__send__(sym) << new_record
148
+ record = klass.where(foreign_key => pk_value).all
149
+ if single_objects.count == 1
150
+ # Check to see if the already record exists
151
+ if record.count == 1
152
+ db_pk_value = record[0][klass.primary_key]
153
+ xml_pk_value = Integer(single_objects[0].element_children.xpath("//#{self.name.underscore}/#{klass.primary_key}").text)
154
+
155
+ if db_pk_value != xml_pk_value
156
+ # Different record in xml
157
+ if options.include?(:sync) or options.include?(:destroy)
158
+ # Delete the one in the database
159
+ klass.destroy(record[0][klass.primary_key])
160
+ end
161
+ end
162
+ elsif record.count > 1
163
+ raise "Too many records for one to one association in the database. Found #{record.count} records of '#{association.name}' for association with '#{self.name}'"
164
+ end
165
+
166
+ if options.include?(:create) or options.include?(:update) or options.include?(:sync)
167
+ new_record = klass.one_from_xml(single_objects[0], options)
168
+ if new_record != nil
169
+ new_record[foreign_key.to_sym] = ar[self.primary_key]
170
+ new_record.save!
171
+ end
172
+ end
173
+ elsif single_objects.count > 1
174
+ # There are more than one associations
175
+ raise "Too many records for one to one association in the provided XML. Found #{single_objects.count} records of '#{association.name}' for association with '#{self.name}'"
176
+ else
177
+ # There are no records in the XML
178
+ if record.count > 0 and options.include?(:sync) or options.include?(:destroy)
179
+ # Found some in the database: destroy then
180
+ klass.destroy_all("#{foreign_key} = #{pk_value}")
90
181
  end
91
182
  end
92
- if (pk_value != 0 and child_ids.length > 0 and (options.include?(:destroy) or options.include?(:sync)))
93
- klass.destroy_all [klass.primary_key.to_s + " not in (?) and #{association.primary_key_name} = ?", child_ids.collect, pk_value]
183
+
184
+ when association.macro == :belongs_to
185
+
186
+ else
187
+ raise "unsupported association #{association.macro} for #{association.name } on #{self.name}"
188
+ end
189
+ end
190
+
191
+ if options.include? :update or options.include? :sync or options.include? :create
192
+ # Process the attributes
193
+ current_node.element_children.each do |node|
194
+ node_name = node.name.underscore.to_sym
195
+ association = self.reflect_on_association node_name
196
+
197
+ if association.nil?
198
+ if node.attributes['nil'].try(:value)
199
+ ar[node_name] = nil
200
+ else
201
+ ar[node_name] = node.text
94
202
  end
95
203
  end
96
- else
97
- # Attribute
98
- ar[sym] = node.text
99
204
  end
100
205
  end
101
206
 
102
- if options.include?(:sync)
207
+ if options.include? :sync
103
208
  # Doing complete synchronisation with XML
104
209
  ar.save
105
210
  elsif options.include?(:create) and ar.new_record?
@@ -114,20 +219,11 @@ module XmlActive
114
219
  end
115
220
  end
116
221
 
117
- def xml_node_matches_single_class(xml_node)
118
- self.name.downcase.eql?(xml_node.name.downcase)
119
- end
120
-
121
- def xml_node_matches_many_of_class(xml_node)
122
- self.name.pluralize.downcase.eql?(xml_node.name.downcase)
123
- end
124
-
125
- def xml_node_is_association(xml_node)
126
- attr = xml_node.attributes["type"]
127
- if (attr)
128
- attr.value == "array"
222
+ def xml_node_matches_class(xml_node)
223
+ if xml_node.attributes['type'].blank?
224
+ xml_node.name.underscore == self.name.underscore
129
225
  else
130
- false
226
+ xml_node.attributes['type'].value.underscore == self.name.underscore
131
227
  end
132
228
  end
133
229
  end
@@ -135,4 +231,4 @@ end
135
231
 
136
232
  class ActiveRecord::Base
137
233
  include XmlActive
138
- end
234
+ end
@@ -1,3 +1,3 @@
1
1
  module XmlActive
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
metadata CHANGED
@@ -1,61 +1,46 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: xml_active
3
- version: !ruby/object:Gem::Version
4
- hash: 27
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 0
9
- - 2
10
- version: 0.0.2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ prerelease:
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Michael Harrison
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2011-09-26 00:00:00 +10:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
12
+ date: 2012-03-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
22
15
  name: nokogiri
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: &70136090689960 !ruby/object:Gem::Requirement
25
17
  none: false
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- hash: 3
30
- segments:
31
- - 0
32
- version: "0"
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
33
22
  type: :runtime
34
- version_requirements: *id001
35
- - !ruby/object:Gem::Dependency
36
- name: rake
37
23
  prerelease: false
38
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: *70136090689960
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &70136090688920 !ruby/object:Gem::Requirement
39
28
  none: false
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- hash: 3
44
- segments:
45
- - 0
46
- version: "0"
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
47
33
  type: :development
48
- version_requirements: *id002
49
- description: XML Active is an extension of ActiveRecord that provides features to synchronise an ActiveRecord Model with a supplied XML document
50
- email:
34
+ prerelease: false
35
+ version_requirements: *70136090688920
36
+ description: XML Active is an extension of ActiveRecord that provides features to
37
+ synchronise an ActiveRecord Model with a supplied XML document
38
+ email:
51
39
  - michael@focalpause.com
52
40
  executables: []
53
-
54
41
  extensions: []
55
-
56
42
  extra_rdoc_files: []
57
-
58
- files:
43
+ files:
59
44
  - .gitignore
60
45
  - Gemfile
61
46
  - README.rdoc
@@ -63,39 +48,28 @@ files:
63
48
  - lib/xml_active.rb
64
49
  - lib/xml_active/version.rb
65
50
  - xml_active.gemspec
66
- has_rdoc: true
67
51
  homepage: https://github.com/michael-harrison/xml_active
68
52
  licenses: []
69
-
70
53
  post_install_message:
71
54
  rdoc_options: []
72
-
73
- require_paths:
55
+ require_paths:
74
56
  - lib
75
- required_ruby_version: !ruby/object:Gem::Requirement
57
+ required_ruby_version: !ruby/object:Gem::Requirement
76
58
  none: false
77
- requirements:
78
- - - ">="
79
- - !ruby/object:Gem::Version
80
- hash: 3
81
- segments:
82
- - 0
83
- version: "0"
84
- required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
64
  none: false
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- hash: 3
90
- segments:
91
- - 0
92
- version: "0"
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
93
69
  requirements: []
94
-
95
70
  rubyforge_project: xml_active
96
- rubygems_version: 1.3.7
71
+ rubygems_version: 1.8.10
97
72
  signing_key:
98
73
  specification_version: 3
99
- summary: xml_active 0.0.2
74
+ summary: xml_active 0.0.3
100
75
  test_files: []
101
-