jinx-migrate 2.1.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/.gitignore +14 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +38 -0
- data/History.md +6 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +33 -0
- data/Rakefile +40 -0
- data/bin/csvjoin +24 -0
- data/examples/family/README.md +24 -0
- data/examples/family/conf/children/fields.yaml +2 -0
- data/examples/family/conf/parents/defaults.yaml +3 -0
- data/examples/family/conf/parents/fields.yaml +6 -0
- data/examples/family/conf/parents/values.yaml +4 -0
- data/examples/family/data/children.csv +1 -0
- data/examples/family/data/parents.csv +1 -0
- data/examples/family/lib/shims.rb +17 -0
- data/jinx-migrate.gemspec +26 -0
- data/lib/jinx/csv/csvio.rb +214 -0
- data/lib/jinx/csv/joiner.rb +196 -0
- data/lib/jinx/migration/filter.rb +167 -0
- data/lib/jinx/migration/migratable.rb +244 -0
- data/lib/jinx/migration/migrator.rb +1029 -0
- data/lib/jinx/migration/reader.rb +16 -0
- data/lib/jinx/migration/version.rb +5 -0
- data/spec/bad/bad_spec.rb +25 -0
- data/spec/bad/fields.yaml +1 -0
- data/spec/bad/parents.csv +1 -0
- data/spec/bad/shims.rb +16 -0
- data/spec/csv/join/join_helper.rb +35 -0
- data/spec/csv/join/join_spec.rb +100 -0
- data/spec/csv/join/jumbled_src.csv +7 -0
- data/spec/csv/join/jumbled_tgt.csv +7 -0
- data/spec/csv/join/source.csv +7 -0
- data/spec/csv/join/target.csv +7 -0
- data/spec/extract/extract.rb +13 -0
- data/spec/extract/extract_spec.rb +33 -0
- data/spec/extract/fields.yaml +1 -0
- data/spec/extract/parents.csv +1 -0
- data/spec/family/child_spec.rb +27 -0
- data/spec/family/family.rb +13 -0
- data/spec/family/parent_spec.rb +57 -0
- data/spec/filter/fields.yaml +1 -0
- data/spec/filter/filter_spec.rb +20 -0
- data/spec/filter/parents.csv +1 -0
- data/spec/filter/values.yaml +4 -0
- data/spec/primitive/children.csv +1 -0
- data/spec/primitive/fields.yaml +4 -0
- data/spec/primitive/primitive_spec.rb +24 -0
- data/spec/skip/fields.yaml +1 -0
- data/spec/skip/parents.csv +1 -0
- data/spec/skip/skip_spec.rb +17 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/model.rb +7 -0
- data/spec/unique/fields.yaml +1 -0
- data/spec/unique/parent.rb +6 -0
- data/spec/unique/parents.csv +1 -0
- data/spec/unique/shims.rb +10 -0
- data/spec/unique/unique_spec.rb +20 -0
- data/test/fixtures/csv/data/empty.csv +1 -0
- data/test/fixtures/csv/data/variety.csv +1 -0
- data/test/lib/csv/csvio_test.rb +74 -0
- metadata +206 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module Jinx
|
2
|
+
module Migration
|
3
|
+
# A prototypical source reader which enumerates the input records.
|
4
|
+
module Reader
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
# @param [String] name the migration mapping source field name, e.g. +First Name+
|
8
|
+
# @return [Symbol] the record value accessor symbol, e.g. +:first_name+
|
9
|
+
def accessor(name); end
|
10
|
+
|
11
|
+
# @yield [rec] migrate the source record
|
12
|
+
# @yieldparam [{Symbol => Object}] rec the source accessor => value record
|
13
|
+
def each; end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
module Model
|
4
|
+
RESULTS = File.dirname(__FILE__) + '/../../test/results'
|
5
|
+
|
6
|
+
describe 'Bad' do
|
7
|
+
# The rejects file.
|
8
|
+
bad = RESULTS + '/bad/rejects.csv'
|
9
|
+
|
10
|
+
# Migrate the input.
|
11
|
+
migrated = Jinx::Migrator.new(:debug => true, :target => Parent, :bad => bad,
|
12
|
+
:mapping => File.expand_path('fields.yaml', File.dirname(__FILE__)),
|
13
|
+
:shims => File.expand_path('shims.rb', File.dirname(__FILE__)),
|
14
|
+
:input => File.expand_path('parents.csv', File.dirname(__FILE__))
|
15
|
+
).to_a
|
16
|
+
|
17
|
+
# Validate the migration.
|
18
|
+
it "should migrate one record" do
|
19
|
+
migrated.size.should be 1
|
20
|
+
end
|
21
|
+
it "should capture two bad records" do
|
22
|
+
File.open(bad).to_a.size.should be 2
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Name: Parent.name
|
@@ -0,0 +1 @@
|
|
1
|
+
Name
|
data/spec/bad/shims.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Model
|
2
|
+
shims Parent
|
3
|
+
|
4
|
+
class Parent
|
5
|
+
# Simulate an error.
|
6
|
+
def migrate_name(value, row)
|
7
|
+
raise StandardError.new("Simulated error") if value == 'Mark'
|
8
|
+
value
|
9
|
+
end
|
10
|
+
|
11
|
+
# Simulate invalidation.
|
12
|
+
def migration_valid?
|
13
|
+
name == 'Tom'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'jinx/csv/csvio'
|
4
|
+
|
5
|
+
SOURCE = File.expand_path('source.csv', File.dirname(__FILE__))
|
6
|
+
|
7
|
+
TARGET = File.expand_path('target.csv', File.dirname(__FILE__))
|
8
|
+
|
9
|
+
RESULTS = File.dirname(__FILE__) + '/../../../test/results/join'
|
10
|
+
|
11
|
+
OUTPUT = File.expand_path('output.csv', RESULTS)
|
12
|
+
|
13
|
+
module Jinx
|
14
|
+
module JoinHelper
|
15
|
+
# Joins the given source fixture to the target fixture on the specified fields.
|
16
|
+
#
|
17
|
+
# @param [Symbol] source the source file fixture in the join spec directory
|
18
|
+
# @param [Symbol] target the target file fixture in the join spec directory
|
19
|
+
# @param [<String>] fields the source fields (default is all source fields)
|
20
|
+
# @return [<<String>>] the output records
|
21
|
+
def join(source, target, *fields, &block)
|
22
|
+
FileUtils.rm_rf OUTPUT
|
23
|
+
sf = File.expand_path("#{source}.csv", File.dirname(__FILE__))
|
24
|
+
tf = File.expand_path("#{target}.csv", File.dirname(__FILE__))
|
25
|
+
Jinx::CsvIO.join(sf, :to => tf, :for => fields, :as => OUTPUT, &block)
|
26
|
+
if File.exists?(OUTPUT) then
|
27
|
+
File.readlines(OUTPUT).map do |line|
|
28
|
+
line.chomp.split(',').map { |s| s unless s.blank? }
|
29
|
+
end
|
30
|
+
else
|
31
|
+
Array::EMPTY_ARRAY
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec/csv/join/join_helper'
|
2
|
+
|
3
|
+
shared_examples 'a join for all source fields' do
|
4
|
+
it 'joins each record' do
|
5
|
+
@output.size.should be 10
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'writes the output header row' do
|
9
|
+
@output.first.should == ['A', 'B', 'U', 'X']
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'writes the matching source and target' do
|
13
|
+
@output[1].should == ['a1', 'b1', 'u', 'x']
|
14
|
+
@output[2].should == ['a1', 'b1', 'v', 'x']
|
15
|
+
@output[3].should == ['a1', 'b2', 'u', 'x']
|
16
|
+
@output[4].should == ['a1', 'b2', 'u', 'y']
|
17
|
+
@output[5].should == ['a2', 'b3', 'u', 'x']
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'writes the unmatched source' do
|
21
|
+
# Note that String split truncates the trailing blank array items,
|
22
|
+
# so the comparison is to ['a2', 'b4', 'u'] rather than ['a2', 'b4', 'u', nil].
|
23
|
+
@output[6].should == ['a2', 'b4', 'u']
|
24
|
+
@output[9].should == ['a4', 'b7', 'u']
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'writes the unmatched target' do
|
28
|
+
@output[7].should == ['a2', 'b5', nil, 'x']
|
29
|
+
@output[8].should == ['a3', nil, nil, 'x']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'Join' do
|
34
|
+
include Jinx::JoinHelper
|
35
|
+
|
36
|
+
context 'Join for all source fields' do
|
37
|
+
before(:all) { @output = join(:source, :target) }
|
38
|
+
|
39
|
+
it_behaves_like 'a join for all source fields'
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'Join with block' do
|
43
|
+
before(:all) do
|
44
|
+
@output = join(:source, :target) do |rec|
|
45
|
+
curr = rec[0..1]
|
46
|
+
if curr == @prev then
|
47
|
+
rec[1] = nil
|
48
|
+
else
|
49
|
+
@prev = curr
|
50
|
+
end
|
51
|
+
rec unless curr == ['a2', 'b3']
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'preserves the output header row' do
|
56
|
+
@output.first.should == ['A', 'B', 'U', 'X']
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'applies the block to the records before writing them to the ouput' do
|
60
|
+
@output[1].should == ['a1', 'b1', 'u', 'x']
|
61
|
+
@output[2].should == ['a1', nil, 'v', 'x']
|
62
|
+
@output[3].should == ['a1', 'b2', 'u', 'x']
|
63
|
+
@output[4].should == ['a1', nil, 'u', 'y']
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'omits the record if the block returns nil' do
|
67
|
+
@output[5].should == ['a2', 'b4', 'u']
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'Join for jumbled source and target fields' do
|
72
|
+
before(:all) { @output = join(:jumbled_src, :jumbled_tgt) }
|
73
|
+
|
74
|
+
it_behaves_like 'a join for all source fields'
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'Join for only the key source fields' do
|
78
|
+
before(:all) { @output = join(:source, :target, 'A', 'B') }
|
79
|
+
|
80
|
+
it 'joins each record' do
|
81
|
+
@output.size.should be 10
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'writes the output header row' do
|
85
|
+
@output.first.should == ['A', 'B', 'X']
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'writes the matching source and target records without the source-specific fields' do
|
89
|
+
@output[1].should == ['a1', 'b1', 'x']
|
90
|
+
@output[2].should == ['a1', 'b1', 'x']
|
91
|
+
@output[3].should == ['a1', 'b2', 'x']
|
92
|
+
@output[4].should == ['a1', 'b2', 'y']
|
93
|
+
@output[5].should == ['a2', 'b3', 'x']
|
94
|
+
@output[6].should == ['a2', 'b4']
|
95
|
+
@output[7].should == ['a2', 'b5', 'x']
|
96
|
+
@output[8].should == ['a3', nil, 'x']
|
97
|
+
@output[9].should == ['a4', 'b7']
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
module Model
|
4
|
+
describe 'Extract' do
|
5
|
+
EXTRACT = File.expand_path('ids.csv', Migration::Test::RESULTS + '/extract')
|
6
|
+
|
7
|
+
HEADERS = ['Name', 'Id']
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
# Migrate the input.
|
11
|
+
@migrated = Jinx::Migrator.new(
|
12
|
+
:debug => true,
|
13
|
+
:target => Parent,
|
14
|
+
:mapping => File.expand_path('fields.yaml', File.dirname(__FILE__)),
|
15
|
+
:extract => EXTRACT,
|
16
|
+
:extract_headers => HEADERS,
|
17
|
+
:shims => File.expand_path('extract.rb', File.dirname(__FILE__)),
|
18
|
+
:input => File.expand_path('parents.csv', File.dirname(__FILE__))
|
19
|
+
).to_a
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should migrate the records" do
|
23
|
+
@migrated.size.should be 3
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should create the extract" do
|
27
|
+
xtr = File.readlines(EXTRACT).map { |line| line.chomp }
|
28
|
+
xtr.size.should be 4
|
29
|
+
xtr[0].should == HEADERS.join(',')
|
30
|
+
1.upto(3) { |i| xtr[i].chomp.split(',').should == [@migrated[i - 1].name, i.to_s] }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Name: Parent.name
|
@@ -0,0 +1 @@
|
|
1
|
+
Name
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/family'
|
2
|
+
|
3
|
+
module Family
|
4
|
+
describe Child do
|
5
|
+
before(:all) do
|
6
|
+
# Migrate the input.
|
7
|
+
@migrated = Jinx::Migrator.new(
|
8
|
+
:debug => true,
|
9
|
+
:target => Child,
|
10
|
+
:mapping => CONFIGS + '/children/fields.yaml',
|
11
|
+
:shims => SHIMS,
|
12
|
+
:input => DATA + '/children.csv'
|
13
|
+
).to_a
|
14
|
+
end
|
15
|
+
|
16
|
+
# Validate the migration.
|
17
|
+
it "should migrate the records" do
|
18
|
+
@migrated.size.should be 3
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should migrate the parents" do
|
22
|
+
@migrated.each do |child|
|
23
|
+
child.parents.size.should be 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
# Load the jinx family example.
|
4
|
+
require Bundler.environment.specs.detect { |s| s.name == 'jinx' }.full_gem_path + '/examples/family/lib/family'
|
5
|
+
|
6
|
+
module Family
|
7
|
+
include Jinx::Migratable
|
8
|
+
|
9
|
+
ROOT = File.dirname(__FILE__) + '/../../examples/family'
|
10
|
+
DATA = ROOT + '/data'
|
11
|
+
CONFIGS = ROOT + '/conf'
|
12
|
+
SHIMS = ROOT + '/lib/shims.rb'
|
13
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/family'
|
2
|
+
|
3
|
+
module Family
|
4
|
+
# The specification for the family example.
|
5
|
+
describe Parent do
|
6
|
+
before(:all) do
|
7
|
+
# Migrate the input.
|
8
|
+
@migrated = Jinx::Migrator.new(
|
9
|
+
:debug => true,
|
10
|
+
:target => Parent,
|
11
|
+
:mapping => CONFIGS + '/parents/fields.yaml',
|
12
|
+
:defaults => CONFIGS + '/parents/defaults.yaml',
|
13
|
+
:filters => CONFIGS + '/parents/values.yaml',
|
14
|
+
:shims => SHIMS,
|
15
|
+
:input => DATA + '/parents.csv'
|
16
|
+
).to_a
|
17
|
+
end
|
18
|
+
|
19
|
+
# Validate the migration.
|
20
|
+
it "should migrate the records" do
|
21
|
+
@migrated.size.should be 2
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should create a household" do
|
25
|
+
@migrated.each do |parent|
|
26
|
+
parent.household.should_not be nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should migrate the addresses" do
|
31
|
+
@migrated.each do |parent|
|
32
|
+
parent.household.address.should_not be nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should abbreviate the street" do
|
37
|
+
addr = @migrated.first.household.address
|
38
|
+
addr.street1.should match /St/
|
39
|
+
addr.street1.should_not match /Street/
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should add the default state" do
|
43
|
+
@migrated.each do |parent|
|
44
|
+
parent.household.address.state.should == 'IL'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should migrate the spouse" do
|
49
|
+
@migrated.first.spouse.should_not be nil
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should set the spouse household" do
|
53
|
+
hh = @migrated.first.household
|
54
|
+
@migrated.first.spouse.household.should be hh
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Name: Parent.name
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
module Model
|
4
|
+
describe 'Filter' do
|
5
|
+
before(:all) do
|
6
|
+
@migrated = Jinx::Migrator.new(:debug => true, :target => Parent,
|
7
|
+
:mapping => File.expand_path('fields.yaml', File.dirname(__FILE__)),
|
8
|
+
:filters => File.expand_path('values.yaml', File.dirname(__FILE__)),
|
9
|
+
:input => File.expand_path('parents.csv', File.dirname(__FILE__))
|
10
|
+
).to_a
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should filter the name" do
|
14
|
+
@migrated.size.should be 3
|
15
|
+
@migrated[0].name.should == 'Joseph'
|
16
|
+
@migrated[1].name.should == 'Christine'
|
17
|
+
@migrated[2].name.should == 'Other'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Name
|
@@ -0,0 +1 @@
|
|
1
|
+
Name,Flag,Cardinal,Decimal
|