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 +44 -0
- data/Rakefile +14 -0
- data/lib/sax-mapper.rb +101 -0
- data/spec/sax-mapper/sax-mapper_spec.rb +89 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +13 -0
- metadata +78 -0
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
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|