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 +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
|
+
|