MikeSofaer-sax-mapper 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/README ADDED
@@ -0,0 +1,44 @@
1
+ SAXMapper is a database persistence extension to SAXMachine.
2
+ It uses DataObjects/MySQL going in, and DataMapper coming back out.
3
+
4
+ SAXual Replication supports getting multiple instances of an object without an
5
+ explicit wrapper class, just by specifying the wrapper tag, using parse_multiple.
6
+
7
+ You can also set a column as a remote primary key, and it will overwrite rather
8
+ than add records where that key is a duplicate (you need to separately set
9
+ the column to be have a unique index, since it uses ON DUPLICATE KEY UPDATE)
10
+
11
+ Finally, you can mark fields as required, and it will raise an expection if the
12
+ XML is missing that field.
13
+
14
+ Many thanks to Paul Dix, who helped me integrate this with SAXMachine, and Dan
15
+ Kubb, who helped me with DataObjects and gave hints on memory footprint.
16
+ All bugs and memory leaks are mine, though!
17
+
18
+ Example:
19
+
20
+ require 'sax-mapper'
21
+ class Person
22
+ include SaxMapper
23
+ element :sourced_id, :required => true
24
+ element :given, :as => :given_name, :required => true
25
+ element :family, :as => :family_name, :required => true
26
+ element :email, :required => true
27
+
28
+ table "people"
29
+ tag :person
30
+ key_column :sourced_id
31
+ end
32
+
33
+ Person.parse_multiple(xml) will return an array of Person objects, found inside
34
+ <person></person> tags. You can save them to the DB with Person.save(array)
35
+
36
+ Multiple values with the same sourced_id will replace each other in the DB.
37
+
38
+ Query batching is not built in, you have to slice the array.
39
+
40
+ gem install MikeSofaer-sax-mapper
41
+
42
+ Enjoy!
43
+
44
+ SAXMapper is written by Michael Sofaer, July 2009, and MIT licenced.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "spec"
2
+ require "spec/rake/spectask"
3
+ require 'lib/sax-mapper.rb'
4
+
5
+ Spec::Rake::SpecTask.new do |t|
6
+ t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
7
+ t.spec_files = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :install do
11
+ rm_rf "*.gem"
12
+ puts `gem build sax-mapper.gemspec`
13
+ puts `sudo gem install sax-mapper-#{SaxMapper::VERSION}.gem`
14
+ end
data/lib/sax-mapper.rb ADDED
@@ -0,0 +1,101 @@
1
+ require 'sax-machine'
2
+ require 'dm-core'
3
+
4
+ module SaxMapper
5
+ class MissingElementError < Exception; end
6
+
7
+ def self.included(base)
8
+ base.send(:include, SAXMachine)
9
+ base.extend SaverMethods
10
+ end
11
+
12
+ module SaverMethods
13
+ def parse(xml)
14
+ ret = super(xml)
15
+ ret.validate
16
+ return ret
17
+ end
18
+
19
+ def parse_multiple(xml)
20
+ klass = collection_class
21
+ ret = klass.parse(xml)
22
+ ret.rows.each{|o| o.validate}
23
+ ret.rows
24
+ end
25
+
26
+ def columns_with_types
27
+ column_names.each{|c| yield c, data_class(c) || String}
28
+ end
29
+
30
+ def connection
31
+ DataMapper.repository(:default).adapter
32
+ end
33
+
34
+ def table(value)
35
+ @table_name = value
36
+ end
37
+
38
+ def tag(value)
39
+ @tag = value
40
+ end
41
+
42
+ def key_column(value)
43
+ @key_column = value
44
+ end
45
+
46
+ def datamapper_class
47
+ klass = self.dup
48
+ klass.send(:include, DataMapper::Resource)
49
+ klass.storage_names[:default] = @table_name
50
+ klass.property(:id, DataMapper::Types::Serial)
51
+ klass.property(:created_at, DateTime, :nullable => false)
52
+ klass.property(:updated_at, DateTime, :nullable => false)
53
+ columns_with_types { |n, t| klass.property(n, t, :field => n.to_s) }
54
+ klass
55
+ end
56
+
57
+ def collection_class
58
+ klass = self
59
+ tag = @tag
60
+ Class.new do
61
+ include SaxMapper
62
+ elements tag, :as => :rows, :class => klass
63
+ end
64
+ end
65
+
66
+ def sql(rows)
67
+ _sql = "INSERT INTO "+ @table_name + "(" + column_names.join(', ') + ", created_at, updated_at) VALUES " +
68
+ ([("(" + (["?"] * (column_names.size + 2)).join(',') + ")")] * rows.size).join(',')
69
+ _sql << duplicate_key_clause if @key_column
70
+ _sql
71
+ end
72
+ def bind_values(rows)
73
+ names = column_names
74
+ datetime = DateTime.now
75
+ array = []
76
+ rows.each{|row| row.add_bind_values!(names, array, datetime)}
77
+ array
78
+ end
79
+ def duplicate_key_clause
80
+ " ON DUPLICATE KEY UPDATE " + (column_names - [:created_at, @key_column]).map {|c| c.to_s + "=VALUES(" + c.to_s + ")"}.join(', ')
81
+ end
82
+
83
+ def save(rows)
84
+ connection.execute sql(rows), *bind_values(rows)
85
+ end
86
+ end
87
+
88
+ def add_bind_values!(column_names, bind_array, datetime)
89
+ column_names.each do |c|
90
+ val = self.send(c)
91
+ bind_array << ((self.class.data_class(c) == DateTime && val) ? (DateTime.parse(val)) : val)
92
+ end
93
+ bind_array << datetime << datetime
94
+ end
95
+
96
+ def validate
97
+ self.class.instance_variable_get('@sax_config').instance_variable_get('@top_level_elements').select{ |e| e.required? }.each do |element|
98
+ raise MissingElementError.new("Missing the required attribute " + element.name) unless send(element.instance_variable_get('@as'))
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,89 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "SaxMapper" do
4
+ before :each do
5
+ @klass = Class.new do
6
+ include SaxMapper
7
+ element :title
8
+ element :written_on, :class => DateTime
9
+ table "documents"
10
+ tag :document
11
+ end
12
+ end
13
+ it "should function as a SAXMachine class" do
14
+ document = @klass.parse("<title>Hello, Everyone!</title>")
15
+ document.title.should == "Hello, Everyone!"
16
+ end
17
+ describe "DataMapper" do
18
+ before(:each) do
19
+ DataMapper.setup(:default, 'mysql://localhost/saxual_replication_test')
20
+ @adapter = DataMapper.repository.adapter
21
+ end
22
+ it "should have the database connection" do
23
+ @adapter.query "show tables"
24
+ end
25
+ it "should have a DataMapper class" do
26
+ @klass.datamapper_class.all.should be_a(Array)
27
+ end
28
+ it "should be able to auto-migrate the document table" do
29
+ DataMapper.auto_migrate!
30
+ end
31
+ describe "with multiple columns" do
32
+ before(:each) do
33
+ @document = @klass.parse("<xml><title>Someone's Cat</title><written_on>March 5 2007</written_on></xml>")
34
+ end
35
+ after(:each) do
36
+ @adapter.execute "delete from documents"
37
+ end
38
+ it "should generate the correct bind values for the specified columns" do
39
+ @klass.column_names.should =~ [:title, :written_on]
40
+ array = []
41
+ @document.add_bind_values!(@klass.column_names, array, DateTime.now)
42
+ array[0].should == "Someone's Cat"
43
+ array[1].should be_a(DateTime)
44
+ array[2].should be_a(DateTime)
45
+ array[3].should be_a(DateTime)
46
+ end
47
+ it "should generate the correct bind values from a class call" do
48
+ array = @klass.bind_values([@document,@document])
49
+ array[0].should == "Someone's Cat"
50
+ array[4].should == "Someone's Cat"
51
+ array[1].should be_a(DateTime)
52
+ array[5].should be_a(DateTime)
53
+ end
54
+ it "should generate the correct SQL from a class call" do
55
+ @klass.sql([@document,@document]).should == "INSERT INTO documents(title, written_on, created_at, updated_at) VALUES (?,?,?,?),(?,?,?,?)"
56
+ end
57
+ describe "multiple records" do
58
+ before(:each) do
59
+ @xml = "<xml><document><title>Hello, Everyone!</title></document><document><title>Someone's Cat</title></document></xml>"
60
+ end
61
+
62
+ it "should be possible to parse two records" do
63
+ rows = @klass.parse_multiple(@xml)
64
+ rows.size.should == 2
65
+ end
66
+
67
+ it "should be able to save two records" do
68
+ documents = @klass.parse_multiple(@xml)
69
+ @klass.save documents
70
+ @klass.datamapper_class.all[0].title.should == "Hello, Everyone!"
71
+ @klass.datamapper_class.all[1].title.should == "Someone's Cat"
72
+ end
73
+ end
74
+ describe "replication" do
75
+ it "should update fields on rows with a repeated primary key" do
76
+ @klass.key_column :written_on
77
+ @adapter.execute "create unique index key_column on documents(written_on)"
78
+ t = DateTime.now.to_s
79
+ xml1 = "<document><title>Hello, Everyone!</title><written_on>#{t}</written_on></document>"
80
+ xml2 = "<document><title>Someone's Cat</title><written_on>#{t}</written_on></document>"
81
+ @klass.save [@klass.parse(xml1)]
82
+ @klass.save [@klass.parse(xml2)]
83
+ @klass.datamapper_class.all.size.should == 1
84
+ @klass.datamapper_class.all[0].title.should == "Someone's Cat"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --diff
2
+ --color
@@ -0,0 +1,13 @@
1
+ require "rubygems"
2
+ require "spec"
3
+
4
+ # gem install redgreen for colored test output
5
+ begin require "redgreen" unless ENV['TM_CURRENT_LINE']; rescue LoadError; end
6
+
7
+ path = File.expand_path(File.dirname(__FILE__) + "/../lib/")
8
+ $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
9
+
10
+ require "lib/sax-mapper"
11
+
12
+ # Spec::Runner.configure do |config|
13
+ # end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: MikeSofaer-sax-mapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Michael Sofaer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-21 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: pauldix-sax-machine
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.14
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: dm-core
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.0
34
+ version:
35
+ description:
36
+ email: mike@sofaer.net
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - lib/sax-mapper.rb
45
+ - README
46
+ - Rakefile
47
+ - spec/spec.opts
48
+ - spec/spec_helper.rb
49
+ - spec/sax-mapper/sax-mapper_spec.rb
50
+ has_rdoc: false
51
+ homepage: http://github.com/MikeSofaer/sax-mapper
52
+ licenses:
53
+ post_install_message:
54
+ rdoc_options: []
55
+
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.3.5
74
+ signing_key:
75
+ specification_version: 2
76
+ summary: Database replication from XML with SAXMachine
77
+ test_files: []
78
+