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 +52 -6
- data/lib/xml_active.rb +146 -50
- data/lib/xml_active/version.rb +1 -1
- metadata +40 -66
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
|
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"
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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.
|
26
|
+
if self.name.pluralize.underscore.eql? current_node.name.underscore
|
27
27
|
ids = []
|
28
|
-
|
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
|
-
|
32
|
-
|
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
|
38
|
+
records[records.length] = self.one_from_xml current_node
|
36
39
|
end
|
37
40
|
|
38
|
-
|
39
|
-
|
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?
|
50
|
-
doc = Nokogiri::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
|
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
|
74
|
+
pk_node = current_node.xpath self.primary_key.to_s
|
59
75
|
if pk_node
|
60
76
|
begin
|
61
|
-
ar = find
|
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
|
-
|
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
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
93
|
-
|
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?
|
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
|
118
|
-
|
119
|
-
|
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
|
-
|
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
|
data/lib/xml_active/version.rb
CHANGED
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
|
-
|
5
|
-
prerelease:
|
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
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
44
|
-
segments:
|
45
|
-
- 0
|
46
|
-
version: "0"
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
47
33
|
type: :development
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
81
|
-
|
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
|
-
|
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.
|
71
|
+
rubygems_version: 1.8.10
|
97
72
|
signing_key:
|
98
73
|
specification_version: 3
|
99
|
-
summary: xml_active 0.0.
|
74
|
+
summary: xml_active 0.0.3
|
100
75
|
test_files: []
|
101
|
-
|