MikeSofaer-sax-mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+