itiel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +17 -0
  3. data/.gitignore +13 -0
  4. data/.gitlab-ci.yml +36 -0
  5. data/.rspec +2 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +9 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.rails.4.0 +7 -0
  10. data/Gemfile.rails.4.1 +7 -0
  11. data/Gemfile.rails.4.2 +7 -0
  12. data/README.markdown +106 -0
  13. data/Rakefile +13 -0
  14. data/build.sh +10 -0
  15. data/features/extract/database_table.feature +16 -0
  16. data/features/extract/sql_script.feature +17 -0
  17. data/features/load/database_table_loader.feature +21 -0
  18. data/features/lookup/csv_file.feature +41 -0
  19. data/features/lookup/database_table.feature +43 -0
  20. data/features/script/ruby_script.feature +19 -0
  21. data/features/step_definitions/csv_steps.rb +15 -0
  22. data/features/step_definitions/extractor/csv_file_steps.rb +3 -0
  23. data/features/step_definitions/extractor/custom_sql_steps.rb +6 -0
  24. data/features/step_definitions/extractor/database_steps.rb +27 -0
  25. data/features/step_definitions/extractor/database_table_steps.rb +8 -0
  26. data/features/step_definitions/extractor/extraction_steps.rb +3 -0
  27. data/features/step_definitions/flow_steps.rb +9 -0
  28. data/features/step_definitions/loader/csv_file_steps.rb +4 -0
  29. data/features/step_definitions/loader/database_table_steps.rb +14 -0
  30. data/features/step_definitions/lookup/lookup_steps.rb +35 -0
  31. data/features/step_definitions/scripting/ruby_script_steps.rb +5 -0
  32. data/features/step_definitions/stream_steps.rb +8 -0
  33. data/features/step_definitions/transformation/calculated_column_steps.rb +5 -0
  34. data/features/step_definitions/transformation/calculated_columns_steps.rb +7 -0
  35. data/features/step_definitions/transformation/constant_column_steps.rb +3 -0
  36. data/features/step_definitions/transformation/map_values_step.rb +4 -0
  37. data/features/step_definitions/transformation/rename_column_steps.rb +3 -0
  38. data/features/step_definitions/transformation/select_column_steps.rb +3 -0
  39. data/features/step_definitions/transformation/single_column_sort_steps.rb +3 -0
  40. data/features/support/database.yml +1 -0
  41. data/features/support/env.rb +13 -0
  42. data/features/transform/transformations.feature +123 -0
  43. data/itiel.gemspec +34 -0
  44. data/lib/itiel.rb +45 -0
  45. data/lib/itiel/db/connection.rb +24 -0
  46. data/lib/itiel/db/sql_connectable.rb +33 -0
  47. data/lib/itiel/db/truncator.rb +30 -0
  48. data/lib/itiel/extract/chained_step.rb +22 -0
  49. data/lib/itiel/extract/csv_file.rb +31 -0
  50. data/lib/itiel/extract/custom_sql.rb +38 -0
  51. data/lib/itiel/extract/database_table.rb +23 -0
  52. data/lib/itiel/job.rb +116 -0
  53. data/lib/itiel/load/chained_step.rb +37 -0
  54. data/lib/itiel/load/csv_file.rb +45 -0
  55. data/lib/itiel/load/database_table.rb +34 -0
  56. data/lib/itiel/load/input_output_behavior.rb +36 -0
  57. data/lib/itiel/logger.rb +47 -0
  58. data/lib/itiel/lookup/chained_step.rb +35 -0
  59. data/lib/itiel/lookup/csv_file.rb +16 -0
  60. data/lib/itiel/lookup/database_table.rb +36 -0
  61. data/lib/itiel/lookup/hash_lookup.rb +35 -0
  62. data/lib/itiel/nameable.rb +6 -0
  63. data/lib/itiel/script/chained_step.rb +18 -0
  64. data/lib/itiel/script/ruby_script.rb +31 -0
  65. data/lib/itiel/script/sql_script.rb +29 -0
  66. data/lib/itiel/transform/calculated_columns.rb +47 -0
  67. data/lib/itiel/transform/chained_step.rb +27 -0
  68. data/lib/itiel/transform/constant_column.rb +35 -0
  69. data/lib/itiel/transform/input_output_behavior.rb +44 -0
  70. data/lib/itiel/transform/map_values.rb +43 -0
  71. data/lib/itiel/transform/remove_column.rb +33 -0
  72. data/lib/itiel/transform/rename_column.rb +43 -0
  73. data/lib/itiel/transform/select_column.rb +37 -0
  74. data/lib/itiel/version.rb +3 -0
  75. data/spec/db/sql_connectable_spec.rb +20 -0
  76. data/spec/extract/chained_step_spec.rb +31 -0
  77. data/spec/extract/csv_file_spec.rb +22 -0
  78. data/spec/extract/custom_sql_spec.rb +19 -0
  79. data/spec/extract/database_table_spec.rb +22 -0
  80. data/spec/job_spec.rb +80 -0
  81. data/spec/loader/chained_step_spec.rb +39 -0
  82. data/spec/loader/csv_file_spec.rb +69 -0
  83. data/spec/loader/database_table_spec.rb +29 -0
  84. data/spec/lookup/hash_lookup_spec.rb +108 -0
  85. data/spec/nameable_spec.rb +17 -0
  86. data/spec/script/chained_step_spec.rb +24 -0
  87. data/spec/script/ruby_script_spec.rb +18 -0
  88. data/spec/script/sql_script_spec.rb +41 -0
  89. data/spec/spec_helper.rb +24 -0
  90. data/spec/support/config/database.yml +1 -0
  91. data/spec/support/config/sources.yml +9 -0
  92. data/spec/transform/calculated_columns_spec.rb +36 -0
  93. data/spec/transform/chained_step_spec.rb +36 -0
  94. data/spec/transform/constant_column_spec.rb +22 -0
  95. data/spec/transform/map_values_spec.rb +26 -0
  96. data/spec/transform/rename_column_spec.rb +25 -0
  97. data/spec/transform/select_column_spec.rb +21 -0
  98. metadata +344 -0
@@ -0,0 +1,43 @@
1
+ module Itiel
2
+ module Transform
3
+ #
4
+ # Renames a column in the data stream
5
+ #
6
+ # Usage:
7
+ #
8
+ # @transformer = Itiel::Transform::RenameColumn.new("order_id" => "id")
9
+ #
10
+ # This would rename the order_id column in the input stream to id
11
+ #
12
+ class RenameColumn
13
+ include ChainedStep
14
+ include Itiel::Nameable
15
+
16
+ attr_accessor :mappings
17
+
18
+ def initialize(*args)
19
+ self.mappings = args
20
+ end
21
+
22
+ def transform!(input_stream)
23
+ old_keys = self.mappings.first.keys
24
+ all_keys = input_stream.first.keys
25
+
26
+ transformed_output = []
27
+ input_stream.each do |object|
28
+ element = {}
29
+ all_keys.each do |k|
30
+ if old_keys.include?(k)
31
+ element[self.mappings.first[k]] = object[k]
32
+ else
33
+ element[k] = object[k]
34
+ end
35
+ end
36
+ transformed_output << element
37
+ end
38
+
39
+ transformed_output
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ module Itiel
2
+ module Transform
3
+ #
4
+ # This transformation only selects specific columns on the data stream
5
+ #
6
+ # Usage:
7
+ #
8
+ # @transformer = Itiel::Transform::SelectColumn.new("order_id", "name")
9
+ #
10
+ # In the example, the output stream would only have the order_id and the name column
11
+ # All other columns will be ignored
12
+ #
13
+ class SelectColumn
14
+ include ChainedStep
15
+ include Itiel::Nameable
16
+
17
+ attr_accessor :mappings
18
+
19
+ def input=(stream)
20
+ next_step.input = transform!(stream)
21
+ end
22
+
23
+ def initialize(*args)
24
+ self.mappings = args
25
+ end
26
+
27
+ def transform!(input_stream)
28
+ selected_output = []
29
+ input_stream.each do |object|
30
+ selected_output << object.select {|key, value| self.mappings.include? key }
31
+ end
32
+
33
+ selected_output
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module Itiel
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::DB::SQLConnectable do
4
+ before :each do
5
+ class Step
6
+ include Itiel::DB::SQLConnectable
7
+ end
8
+ end
9
+
10
+ it "sets and gets connection_file_path" do
11
+ expect(Step).to respond_to :connection_file_path
12
+ expect(Step).to respond_to :connection_file_path=
13
+ end
14
+
15
+ it "returns a sequel_connection object based on the connection file" do
16
+ Step.connection_file_path = File.dirname(__FILE__) + '/../support/config/database.yml'
17
+ expect(Step.sequel_connection(:test)).to_not be_nil
18
+ end
19
+ end
20
+
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Extract::ChainedStep do
4
+ before :each do
5
+ klass = Class.new
6
+ klass.send(:include, Itiel::Extract::ChainedStep)
7
+ @step = klass.new
8
+
9
+ @stream = double
10
+ end
11
+
12
+ describe "#start" do
13
+ before :each do
14
+ @next_step = double
15
+ @step.next_step = @next_step
16
+ end
17
+
18
+ it "extracts and sends the stream to the next step" do
19
+ allow(@step).to receive(:extract).and_return(@stream)
20
+ allow(@next_step).to receive(:input=).and_return(@stream)
21
+
22
+ @step.start
23
+ end
24
+ end
25
+
26
+ describe "#extract" do
27
+ it "raises an exception" do
28
+ expect { @step.extract }.to raise_error Itiel::MethodNotImplementedException
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Extract::CSVFile do
4
+ before :each do
5
+ @step = Itiel::Extract::CSVFile.new 'FILENAME'
6
+ end
7
+
8
+ describe "#extract" do
9
+ it "reads a csv file and returns it as a hash to the stream" do
10
+ row = double
11
+ allow(row).to receive(:to_hash).and_return({data: true})
12
+
13
+ @stream = [ row ]
14
+ allow(CSV).to receive(:read).
15
+ with('FILENAME', headers: true).
16
+ and_return(@stream)
17
+
18
+ expect(@step.extract).to eq [{ data: true }]
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Extract::CustomSQL do
4
+ describe "#extract" do
5
+ before :each do
6
+ @step = Itiel::Extract::CustomSQL.new 'SCRIPT'
7
+ @step.connection = :test
8
+ end
9
+
10
+ it "Runs a script on the database and returns its results to the stream" do
11
+ result = double
12
+ allow(result).to receive(:all).and_return double
13
+ db = { 'SCRIPT' => result }
14
+ allow(Itiel::Extract::CustomSQL).to receive(:sequel_connection).with(:test).and_return db
15
+
16
+ expect(@step.extract).to eq result.all
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Extract::DatabaseTable do
4
+ before :each do
5
+ @step = Itiel::Extract::DatabaseTable.new
6
+ @step.connection = :test
7
+ @step.table_name = 'table_name'
8
+ end
9
+
10
+ describe "#extract" do
11
+ it "returns all the rows in the specified database table" do
12
+ result = double
13
+ db = { table_name: result }
14
+ allow(result).to receive(:all).and_return double
15
+ expect(Itiel::Extract::DatabaseTable).
16
+ to receive(:sequel_connection).
17
+ with(:test).and_return db
18
+
19
+ expect(@step.extract).to eq result.all
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Job do
4
+ describe "#run" do
5
+ it "yields a new instance of Itiel::Job" do
6
+ Itiel::Job.run { |object| expect(object).to be_instance_of(Itiel::Job) }
7
+ end
8
+ end
9
+
10
+ describe "#define" do
11
+ it "returns an instance of Itiel::Job" do
12
+ expect(Itiel::Job.define).to be_instance_of(Itiel::Job)
13
+ end
14
+
15
+ it "yields a new instance of Itiel::Job" do
16
+ Itiel::Job.define {|object| assert_instance_of(Itiel::Job, object)}
17
+ end
18
+
19
+ it "sets given block as the block attribute of the instance it returns" do
20
+ block = Proc.new {}
21
+ job = Itiel::Job.define(&block)
22
+ expect(block).to eq job.block
23
+ end
24
+ end
25
+
26
+ describe "#run!" do
27
+ it "calls the block on self.block" do
28
+ block = Proc.new {}
29
+ expect(block).to receive(:call)
30
+ job = Itiel::Job.define(&block)
31
+ job.run!
32
+ end
33
+
34
+ it "responds to step inside the block" do
35
+ @object = double
36
+ instanced_job = Itiel::Job.define do |job|
37
+ job.step @object
38
+ end
39
+
40
+ expect(instanced_job).to receive(:step).with(@object)
41
+ instanced_job.run!
42
+ end
43
+ end
44
+
45
+ describe "#step" do
46
+ before :each do
47
+ @job = Itiel::Job.new
48
+ @source = double
49
+ @destination = double
50
+ @stream = double
51
+
52
+ allow(@source).to receive(:output).and_return @stream
53
+ expect(@destination).to receive(:input=).and_return @stream
54
+ end
55
+
56
+ describe "@source => @destination" do
57
+ it "hardwires the @destination's input with the @source output" do
58
+ @job.step @source => @destination
59
+ end
60
+ end
61
+
62
+ describe "step @source; step @destination" do
63
+ it "hardwires the @destination's input with the @source output" do
64
+ expect(@destination).to receive(:output)
65
+ @job.step @source
66
+ @job.step @destination
67
+ end
68
+ end
69
+
70
+ describe "step @source => [ @destination, @second_destination ]" do
71
+ it "hardwires both destination's inputs with the @source output" do
72
+ @second_destination = double
73
+ expect(@second_destination).to receive(:input=).and_return @stream
74
+
75
+ @job.step @source => [@destination, @second_destination]
76
+ end
77
+ end
78
+ end
79
+ end
80
+
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Load::ChainedStep do
4
+ before :each do
5
+ klass = Class.new
6
+ klass.send :include, Itiel::Load::ChainedStep
7
+
8
+ @step = klass.new
9
+ end
10
+
11
+ it "defines next_step" do
12
+ expect(@step).to respond_to(:next_step)
13
+ expect(@step).to respond_to(:next_step=)
14
+ end
15
+
16
+ describe :persist do
17
+ it "raises an error if undefined" do
18
+ expect { @step.persist([]) }.to raise_error Itiel::MethodNotImplementedException
19
+ end
20
+ end
21
+
22
+ describe :input= do
23
+ before :each do
24
+ @next_step = double
25
+ @input = [ double ]
26
+ @step.next_step = @next_step
27
+
28
+ expect(Itiel::Logger).to receive(:log_received).with(@step, @input.size)
29
+ expect(Itiel::Logger).to receive(:log_processed).with(@step, @input.size)
30
+ end
31
+
32
+ it "calls persist with the stream received through input= and sends it to next_step" do
33
+ expect(@step).to receive(:persist).with @input
34
+ expect(@next_step).to receive(:input=).with @input
35
+ @step.input = @input
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Load::CSVFile do
4
+ before :each do
5
+ @filename = File.expand_path("#{File.dirname(__FILE__)}/../../tmp/output.csv")
6
+ File.unlink(@filename) if File.exist?(@filename)
7
+
8
+ @input = [
9
+ {
10
+ "id" => 1,
11
+ "name" => "Subject Name"
12
+ },
13
+ {
14
+ "id" => 2,
15
+ "name" => "Subject Name"
16
+ }
17
+ ]
18
+
19
+ @expected_result = [
20
+ [ "id" , "name" ] ,
21
+ [ "1" , "Subject Name" ] ,
22
+ [ "2" , "Subject Name" ]
23
+ ]
24
+
25
+ @csv_output = Itiel::Load::CSVFile.new(@filename)
26
+ end
27
+
28
+ describe "the file does not exist" do
29
+ before :each do
30
+ @csv_output.persist(@input)
31
+ @result = CSV.read(@filename, :headers => true)
32
+ end
33
+
34
+ it "generates a CSV file with the input data" do
35
+ expect(@expected_result).to eq @result.to_a
36
+ end
37
+ end
38
+
39
+ describe "the file already exists" do
40
+ before :each do
41
+ FileUtils.touch(@filename)
42
+ @csv_output.persist(@input)
43
+ @result = CSV.read(@filename)
44
+ end
45
+
46
+ it "appends to the existing data" do
47
+ expect(@expected_result - [[ "id", "name" ]]).to eq @result
48
+ end
49
+ end
50
+
51
+ describe "use append = false" do
52
+ before do
53
+ @csv_output = Itiel::Load::CSVFile.new(@filename, false)
54
+ end
55
+
56
+ describe "the file already exists" do
57
+ before :each do
58
+ FileUtils.touch(@filename)
59
+ @csv_output.persist(@input)
60
+ @result = CSV.read(@filename)
61
+ end
62
+
63
+ it "replaces the existing file with the input data" do
64
+ expect(@expected_result).to eq @result
65
+ end
66
+ end
67
+ end
68
+ end
69
+
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Load::DatabaseTable do
4
+ before(:each) do
5
+ @output = Itiel::Load::DatabaseTable.new :test, "users"
6
+ @input = [
7
+ {
8
+ "id" => "1",
9
+ "name" => "Some Name"
10
+ },
11
+ {
12
+ "id" => "2",
13
+ "name" => "Some Other Name"
14
+ },
15
+ ]
16
+ end
17
+
18
+ it "inserts a record for each row" do
19
+ table = double
20
+ allow(@output).to receive(:table).and_return table
21
+
22
+ @input.each do |row|
23
+ expect(table).to receive(:insert).and_return row
24
+ end
25
+
26
+ @output.persist(@input)
27
+ end
28
+ end
29
+
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itiel::Lookup::HashLookup do
4
+ before(:each) do
5
+ @input = [
6
+ {
7
+ :id => 1,
8
+ :author => "Isaac"
9
+ },
10
+ {
11
+ :id => 2,
12
+ :author => "Carver"
13
+ }
14
+ ]
15
+
16
+ @lookup_source = [
17
+ {
18
+ :id => 1,
19
+ :name => "Isaac",
20
+ :active => "t"
21
+ },
22
+ {
23
+ :id => 2,
24
+ :name => "Carver",
25
+ :active => "f"
26
+ }
27
+ ]
28
+
29
+ @lookup_stream = {
30
+ "Isaac" => {
31
+ :author_id => 1,
32
+ :active => "t"
33
+ },
34
+
35
+ "Carver" => {
36
+ :author_id => 2,
37
+ :active => "f"
38
+ }
39
+ }
40
+
41
+ @expected_output = [
42
+ {
43
+ :id => 1,
44
+ :author => "Isaac",
45
+ :author_id => 1,
46
+ :active => "t"
47
+ },
48
+ {
49
+ :id => 2,
50
+ :author => "Carver",
51
+ :author_id => 2,
52
+ :active => "f"
53
+ }
54
+ ]
55
+
56
+ class FooLookup
57
+ include Itiel::Lookup::HashLookup
58
+ end
59
+
60
+ @lookup = FooLookup.new
61
+ @lookup.lookup_columns = { "author" => "name" }
62
+ @lookup.joined_columns = { "id" => "author_id", "active" => "active" }
63
+ end
64
+
65
+ describe "#lookup!" do
66
+ it "joins the data using the specified column" do
67
+ allow(@lookup).to receive(:lookup_stream).and_return @lookup_stream
68
+ expect(@expected_output).to eq @lookup.lookup!(@input)
69
+ end
70
+ end
71
+
72
+ describe "#lookup_stream" do
73
+ it do
74
+ allow(@lookup).to receive(:lookup_source).and_return @lookup_source
75
+ expect(@lookup_stream).to eq @lookup.lookup_stream
76
+ end
77
+ end
78
+
79
+ describe 'with nil values in the lookup column' do
80
+ before do
81
+ @input.append({ id: 3, author: nil })
82
+ @expected_output.append({ id: 3, author: nil, author_id: nil, active: nil })
83
+ end
84
+
85
+ describe "#lookup!" do
86
+ it 'joins the data setting the value to nil' do
87
+ allow(@lookup).to receive(:lookup_stream).and_return @lookup_stream
88
+ expect(@expected_output).to eq @lookup.lookup!(@input)
89
+ end
90
+ end
91
+ end
92
+
93
+ describe "When the lookup column doesn't exist in the input_stream" do
94
+ before do
95
+ @input.append({ id: 3 })
96
+ @expected_output.append({ id: 3, author_id: nil, active: nil })
97
+ end
98
+
99
+ describe "#lookup!" do
100
+ it 'joins the data setting the value to nil' do
101
+ allow(@lookup).to receive(:lookup_stream).and_return @lookup_stream
102
+
103
+ expect(@expected_output).to eq @lookup.lookup!(@input)
104
+ end
105
+ end
106
+ end
107
+ end
108
+