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.
Files changed (65) hide show
  1. data/.gitignore +14 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +38 -0
  6. data/History.md +6 -0
  7. data/LEGAL +5 -0
  8. data/LICENSE +22 -0
  9. data/README.md +33 -0
  10. data/Rakefile +40 -0
  11. data/bin/csvjoin +24 -0
  12. data/examples/family/README.md +24 -0
  13. data/examples/family/conf/children/fields.yaml +2 -0
  14. data/examples/family/conf/parents/defaults.yaml +3 -0
  15. data/examples/family/conf/parents/fields.yaml +6 -0
  16. data/examples/family/conf/parents/values.yaml +4 -0
  17. data/examples/family/data/children.csv +1 -0
  18. data/examples/family/data/parents.csv +1 -0
  19. data/examples/family/lib/shims.rb +17 -0
  20. data/jinx-migrate.gemspec +26 -0
  21. data/lib/jinx/csv/csvio.rb +214 -0
  22. data/lib/jinx/csv/joiner.rb +196 -0
  23. data/lib/jinx/migration/filter.rb +167 -0
  24. data/lib/jinx/migration/migratable.rb +244 -0
  25. data/lib/jinx/migration/migrator.rb +1029 -0
  26. data/lib/jinx/migration/reader.rb +16 -0
  27. data/lib/jinx/migration/version.rb +5 -0
  28. data/spec/bad/bad_spec.rb +25 -0
  29. data/spec/bad/fields.yaml +1 -0
  30. data/spec/bad/parents.csv +1 -0
  31. data/spec/bad/shims.rb +16 -0
  32. data/spec/csv/join/join_helper.rb +35 -0
  33. data/spec/csv/join/join_spec.rb +100 -0
  34. data/spec/csv/join/jumbled_src.csv +7 -0
  35. data/spec/csv/join/jumbled_tgt.csv +7 -0
  36. data/spec/csv/join/source.csv +7 -0
  37. data/spec/csv/join/target.csv +7 -0
  38. data/spec/extract/extract.rb +13 -0
  39. data/spec/extract/extract_spec.rb +33 -0
  40. data/spec/extract/fields.yaml +1 -0
  41. data/spec/extract/parents.csv +1 -0
  42. data/spec/family/child_spec.rb +27 -0
  43. data/spec/family/family.rb +13 -0
  44. data/spec/family/parent_spec.rb +57 -0
  45. data/spec/filter/fields.yaml +1 -0
  46. data/spec/filter/filter_spec.rb +20 -0
  47. data/spec/filter/parents.csv +1 -0
  48. data/spec/filter/values.yaml +4 -0
  49. data/spec/primitive/children.csv +1 -0
  50. data/spec/primitive/fields.yaml +4 -0
  51. data/spec/primitive/primitive_spec.rb +24 -0
  52. data/spec/skip/fields.yaml +1 -0
  53. data/spec/skip/parents.csv +1 -0
  54. data/spec/skip/skip_spec.rb +17 -0
  55. data/spec/spec_helper.rb +17 -0
  56. data/spec/support/model.rb +7 -0
  57. data/spec/unique/fields.yaml +1 -0
  58. data/spec/unique/parent.rb +6 -0
  59. data/spec/unique/parents.csv +1 -0
  60. data/spec/unique/shims.rb +10 -0
  61. data/spec/unique/unique_spec.rb +20 -0
  62. data/test/fixtures/csv/data/empty.csv +1 -0
  63. data/test/fixtures/csv/data/variety.csv +1 -0
  64. data/test/lib/csv/csvio_test.rb +74 -0
  65. 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,5 @@
1
+ module Jinx
2
+ module Migrate
3
+ VERSION = "2.1.1"
4
+ end
5
+ 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,7 @@
1
+ A,U,B
2
+ a1,u,b1
3
+ a1,v,b1
4
+ a1,u,b2
5
+ a2,u,b3
6
+ a2,u,b4
7
+ a4,u,b7
@@ -0,0 +1,7 @@
1
+ X,B,A
2
+ x,b1,a1
3
+ x,b2,a1
4
+ y,b2,a1
5
+ x,b3,a2
6
+ x,b5,a2
7
+ x,,a3
@@ -0,0 +1,7 @@
1
+ A,B,U
2
+ a1,b1,u
3
+ a1,b1,v
4
+ a1,b2,u
5
+ a2,b3,u
6
+ a2,b4,u
7
+ a4,b7,u
@@ -0,0 +1,7 @@
1
+ A,B,X
2
+ a1,b1,x
3
+ a1,b2,x
4
+ a1,b2,y
5
+ a2,b3,x
6
+ a2,b5,x
7
+ a3,,x
@@ -0,0 +1,13 @@
1
+ module Model
2
+ shims Parent
3
+
4
+ class Parent
5
+ @@id = 1
6
+
7
+ def extract(io)
8
+ io << [name, @@id]
9
+ @@id += 1
10
+ end
11
+ end
12
+ end
13
+
@@ -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,4 @@
1
+ Parent.name:
2
+ Joe : Joseph
3
+ /Chris/i : Christine
4
+ /.*/ : Other
@@ -0,0 +1 @@
1
+ Name,Flag,Cardinal,Decimal