xml_active 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,32 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ pkg/*
4
+
5
+ ## MAC OS
6
+ .DS_Store
7
+
8
+ ## RubyMine
9
+ .idea
10
+
11
+ ## TEXTMATE
12
+ *.tmproj
13
+ tmtags
14
+
15
+ ## EMACS
16
+ *~
17
+ \#*
18
+ .\#*
19
+
20
+ ## VIM
21
+ *.swp
22
+
23
+ ## PROJECT::GENERAL
24
+ coverage
25
+ rdoc
26
+ pkg
27
+
28
+ ## PROJECT::SPECIFIC
29
+ tmp
30
+ .bundle
31
+ doc
32
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in xml_active.gemspec
4
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,164 @@
1
+ = XML Active
2
+
3
+ Rails has some really fantastic features for serialisation of ActiveRecord to XML however going back the other way is not so fantastic. XML Active aims to provide the import of XML into your database while utilising the strengths of ActiveRecord. It extends ActiveRecord allowing it to seamlessly work in with your model. Here are the features provided by XML Active:
4
+
5
+ * Updating of records based on an XML document containing elements matching primary key
6
+ * Creation of records based on elements in an XML
7
+ * Removal of records based on an XML document containing elements matching primary key
8
+ * Choose to only update, only create or only remove records
9
+ * Choose to combine update, create and remove actions
10
+ * Synchronise your database with an XML Document
11
+
12
+ == The Background
13
+
14
+ == XML Documents
15
+
16
+ XML Active relies on the XML documents created by ActiveRecord in that it utilises the element attributes and document layout. Following is a sample of an XML document. Note the @type@ attribute which helps to identify elements that are integers, arrays, etc.
17
+
18
+ <books type="array">
19
+ <book>
20
+ <id type="Integer">4</id>
21
+ <name>Book 1</name>
22
+ </book>
23
+ <book>
24
+ <id type="integer">5</id>
25
+ <name>Book 1</name>
26
+ <chapters type="array">
27
+ <chapter>
28
+ <id type="integer">1</id>
29
+ <title>Chapter 1</title>
30
+ <introduction>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</introduction>
31
+ <pages type="array">
32
+ <page>
33
+ <id type="integer">1</id>
34
+ <content>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</content>
35
+ <number>1</number>
36
+ </page>
37
+ <page>
38
+ <id type="integer">2</id>
39
+ <content>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</content>
40
+ <number>2</number>
41
+ </page>
42
+ <page>
43
+ <id type="integer">3</id>
44
+ <content>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</content>
45
+ <number>3</number>
46
+ </page>
47
+ </pages>
48
+ </chapter>
49
+ <chapter>
50
+ <id type="integer">2</id>
51
+ <title>Chapter 2</title>
52
+ <introduction>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</introduction>
53
+ <pages type="array">
54
+ <page>
55
+ <id type="integer">5</id>
56
+ <content>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</content>
57
+ <number>1</number>
58
+ </page>
59
+ <page>
60
+ <id type="integer">6</id>
61
+ <content>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</content>
62
+ <number>2</number>
63
+ </page>
64
+ <page>
65
+ <id type="integer">7</id>
66
+ <content>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</content>
67
+ <number>3</number>
68
+ </page>
69
+ <page>
70
+ <id type="integer">8</id>
71
+ <content>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus purus nulla, condimentum vitae hendrerit nec, blandit et felis. Suspendisse vulputate mollis suscipit. Vivamus non libero quis urna gravida euismod quis in nisi. Morbi turpis orci, posuere nec ultrices ut, egestas ac purus. Morbi id pretium erat. In ullamcorper, ligula id porta pellentesque, sem turpis ultricies libero, non elementum ipsum neque at dui. Donec auctor nulla id mi dapibus id faucibus felis mollis. Curabitur imperdiet tristique nisi, consectetur molestie purus accumsan id. Curabitur lacinia diam et nisl iaculis eleifend. Quisque turpis elit, volutpat eget dapibus sed, egestas nec leo. Mauris dignissim tellus non lorem fringilla pharetra.</content>
72
+ <number>4</number>
73
+ </page>
74
+ </pages>
75
+ </chapter>
76
+ </chapters>
77
+ </book>
78
+ <book>
79
+ <id type="integer">6</id>
80
+ <name>Book 1</name>
81
+ </book>
82
+ </books>
83
+ </code>
84
+
85
+ === Database Schema
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.
88
+
89
+ === The Model
90
+
91
+ In order to identify the relationships XML Active it relies on associations so you have to make sure you define your associations in your model. Following are is a sample model.
92
+
93
+ book.rb
94
+ class Book < ActiveRecord::Base
95
+ has_many :chapters, :dependent => :destroy
96
+ end
97
+
98
+ chapter.rb
99
+ class Chapter < ActiveRecord::Base
100
+ has_many :pages, :dependent => :destroy
101
+ belongs_to :book
102
+ end
103
+
104
+ page.rb
105
+ class Page < ActiveRecord::Base
106
+ belongs_to :chapter
107
+ end
108
+
109
+ At this stage XML Active has been tested with a limited number of associations. It is hoped in the future that a more complete set of tests will exist.
110
+
111
+ == Examples
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.
114
+
115
+ === New Functions
116
+
117
+ XML Active extends the ActiveRecord class giving you the following functions:
118
+
119
+ many_from_xml(xml, options)
120
+ Allows for the import of many records based on an XML document. This function expects an XML document similar to the following with many records in it:
121
+
122
+ books_changed.xml
123
+ <books type="array">
124
+ <book>
125
+ <id type="Integer">4</id>
126
+ <name>Book 1</name>
127
+ </book>
128
+ <book>
129
+ <id type="integer">5</id>
130
+ <name>Book 1</name>
131
+ </book>
132
+ <book>
133
+ <id type="integer">6</id>
134
+ <name>Book 1</name>
135
+ </book>
136
+ </books>
137
+
138
+ Following is an example:
139
+ Book.many_from_xml File.open("books_changed.xml").read, [:update]
140
+
141
+ one_from_xml(xml, options)
142
+ Allows for the import of one record based on an XML document. This function expects an XML document similar to the following with many records in it:
143
+
144
+ one_book_changed.xml
145
+ <book>
146
+ <id type="Integer">4</id>
147
+ <name>Book 1</name>
148
+ </book>
149
+
150
+ Following is an example:
151
+ Book.one_from_xml File.open("one_book_changed.xml").read, [:update]
152
+
153
+ === Options
154
+
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
+
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.
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.
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.
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.
164
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'rubygems'
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ module XmlActive
2
+ VERSION = "0.0.2"
3
+ end
data/lib/xml_active.rb ADDED
@@ -0,0 +1,138 @@
1
+ require "xml_active/version"
2
+
3
+ module XmlActive
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ def ensure_unique(name)
9
+ begin
10
+ self[name] = yield
11
+ end while self.class.exists?(name => self[name])
12
+ end
13
+
14
+ VALID_FROM_XML_OPTIONS = [:sync, :create, :update, :destroy]
15
+
16
+ module ClassMethods
17
+ def many_from_xml(xml, options = [])
18
+ if xml.is_a?(String)
19
+ doc = Nokogiri::XML(xml)
20
+ current_node = doc.children.first
21
+ else
22
+ current_node = xml
23
+ end
24
+
25
+ records = []
26
+ if self.xml_node_matches_many_of_class(current_node)
27
+ ids = []
28
+ if (self.xml_node_is_association(current_node))
29
+ current_node.element_children.each do |node|
30
+ record = self.one_from_xml(node, options)
31
+ ids[ids.length] = record[primary_key.to_sym]
32
+ records[records.length] = record
33
+ end
34
+ else
35
+ records[records.length] = self.one_from_xml(current_node)
36
+ end
37
+
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]
40
+ end
41
+ else
42
+ raise "The supplied XML (#{current_node.name}) cannot be mapped to this class (#{self.name})"
43
+ end
44
+
45
+ records
46
+ end
47
+
48
+ def one_from_xml(xml, options = [])
49
+ if xml.is_a?(String)
50
+ doc = Nokogiri::XML(xml)
51
+ current_node = doc.children.first
52
+ else
53
+ current_node = xml
54
+ end
55
+
56
+ if self.xml_node_matches_single_class(current_node)
57
+ pk_value = 0
58
+ pk_node = current_node.xpath(self.primary_key.to_s)
59
+ if pk_node
60
+ begin
61
+ ar = find(pk_node.text)
62
+ pk_value = pk_node.text
63
+ rescue
64
+ # No record exists, create a new one
65
+ ar = self.new
66
+ end
67
+ else
68
+ # No primary key value, must be a new record
69
+ ar = self.new
70
+ end
71
+
72
+ if (ar.new_record? and options.include?(:update) and not options.include?(:sync))
73
+ return(nil)
74
+ end
75
+
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
83
+ 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
90
+ end
91
+ 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]
94
+ end
95
+ end
96
+ else
97
+ # Attribute
98
+ ar[sym] = node.text
99
+ end
100
+ end
101
+
102
+ if options.include?(:sync)
103
+ # Doing complete synchronisation with XML
104
+ ar.save
105
+ elsif options.include?(:create) and ar.new_record?
106
+ ar.save
107
+ elsif options.include?(:update) and not ar.new_record?
108
+ ar.save
109
+ end
110
+
111
+ ar
112
+ else
113
+ raise "The supplied XML (#{current_node.name}) cannot be mapped to this class (#{self.name})"
114
+ end
115
+ end
116
+
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"
129
+ else
130
+ false
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ class ActiveRecord::Base
137
+ include XmlActive
138
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "xml_active/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "xml_active"
7
+ s.version = XmlActive::VERSION
8
+ s.authors = ["Michael Harrison"]
9
+ s.email = ["michael@focalpause.com"]
10
+ s.homepage = "https://github.com/michael-harrison/xml_active"
11
+ s.summary = "xml_active #{s.version}"
12
+ s.description = %q{XML Active is an extension of ActiveRecord that provides features to synchronise an ActiveRecord Model with a supplied XML document}
13
+
14
+ s.rubyforge_project = "xml_active"
15
+
16
+ s.add_dependency 'nokogiri'
17
+ s.add_development_dependency 'rake'
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+
24
+ # specify any dependencies here; for example:
25
+ # s.add_development_dependency "rspec"
26
+ # s.add_runtime_dependency "rest-client"
27
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
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
11
+ platform: ruby
12
+ authors:
13
+ - Michael Harrison
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-09-26 00:00:00 +10:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: nokogiri
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rake
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ 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:
51
+ - michael@focalpause.com
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - .gitignore
60
+ - Gemfile
61
+ - README.rdoc
62
+ - Rakefile
63
+ - lib/xml_active.rb
64
+ - lib/xml_active/version.rb
65
+ - xml_active.gemspec
66
+ has_rdoc: true
67
+ homepage: https://github.com/michael-harrison/xml_active
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options: []
72
+
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ 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
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project: xml_active
96
+ rubygems_version: 1.3.7
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: xml_active 0.0.2
100
+ test_files: []
101
+