topographer 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/lib/topographer/exceptions.rb +5 -3
  3. data/lib/topographer/importer.rb +25 -9
  4. data/lib/topographer/importer/helpers.rb +19 -13
  5. data/lib/topographer/importer/helpers/write_log_to_csv.rb +75 -67
  6. data/lib/topographer/importer/importable.rb +7 -3
  7. data/lib/topographer/importer/input.rb +11 -4
  8. data/lib/topographer/importer/input/base.rb +21 -15
  9. data/lib/topographer/importer/input/delimited_spreadsheet.rb +55 -0
  10. data/lib/topographer/importer/input/roo.rb +31 -24
  11. data/lib/topographer/importer/input/source_data.rb +14 -8
  12. data/lib/topographer/importer/logger.rb +10 -6
  13. data/lib/topographer/importer/logger/base.rb +75 -66
  14. data/lib/topographer/importer/logger/fatal_error_entry.rb +22 -16
  15. data/lib/topographer/importer/logger/log_entry.rb +31 -25
  16. data/lib/topographer/importer/logger/simple.rb +25 -19
  17. data/lib/topographer/importer/mapper.rb +58 -53
  18. data/lib/topographer/importer/mapper/default_field_mapping.rb +23 -17
  19. data/lib/topographer/importer/mapper/field_mapping.rb +56 -49
  20. data/lib/topographer/importer/mapper/ignored_field_mapping.rb +14 -7
  21. data/lib/topographer/importer/mapper/mapper_builder.rb +57 -44
  22. data/lib/topographer/importer/mapper/mapping_columns.rb +51 -44
  23. data/lib/topographer/importer/mapper/mapping_validator.rb +39 -32
  24. data/lib/topographer/importer/mapper/result.rb +21 -15
  25. data/lib/topographer/importer/mapper/validation_field_mapping.rb +35 -28
  26. data/lib/topographer/importer/strategy.rb +12 -6
  27. data/lib/topographer/importer/strategy/base.rb +43 -38
  28. data/lib/topographer/importer/strategy/create_or_update_record.rb +39 -34
  29. data/lib/topographer/importer/strategy/import_new_record.rb +16 -10
  30. data/lib/topographer/importer/strategy/import_status.rb +27 -20
  31. data/lib/topographer/importer/strategy/update_record.rb +28 -24
  32. data/lib/topographer/version.rb +1 -1
  33. data/spec/assets/test_files/a_csv.csv +3 -0
  34. data/spec/topographer/importer/helpers/write_log_to_csv_spec.rb +4 -4
  35. data/spec/topographer/importer/importer_spec.rb +21 -5
  36. data/spec/topographer/importer/input/delimited_spreadsheet_spec.rb +90 -0
  37. data/spec/topographer/importer/input/source_data_spec.rb +2 -2
  38. data/spec/topographer/importer/logger/base_spec.rb +19 -0
  39. data/spec/topographer/importer/logger/fatal_error_entry_spec.rb +2 -2
  40. data/spec/topographer/importer/logger/simple_spec.rb +3 -3
  41. data/spec/topographer/importer/mapper/default_field_mapping_spec.rb +2 -2
  42. data/spec/topographer/importer/mapper/field_mapping_spec.rb +21 -21
  43. data/spec/topographer/importer/mapper/mapper_builder_spec.rb +9 -9
  44. data/spec/topographer/importer/mapper/mapping_validator_spec.rb +10 -10
  45. data/spec/topographer/importer/mapper/validation_field_mapping_spec.rb +3 -3
  46. data/spec/topographer/importer/mapper_spec.rb +25 -25
  47. data/spec/topographer/importer/strategy/base_spec.rb +16 -5
  48. data/spec/topographer/importer/strategy/create_or_update_record_spec.rb +3 -3
  49. data/spec/topographer/importer/strategy/import_new_records_spec.rb +5 -5
  50. data/spec/topographer/importer/strategy/mapped_model.rb +9 -0
  51. data/spec/topographer/importer/strategy/update_record_spec.rb +3 -3
  52. data/topographer.gemspec +3 -2
  53. metadata +101 -102
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f03c6ff70ebbd6f7dcb8c6b4d72deb2de304aee0
4
+ data.tar.gz: 22c2fd768c6c4d17586b46f0755d4ee56590fe6c
5
+ SHA512:
6
+ metadata.gz: 0c51ad3208acba7412522563cb4d1693c4de97fe30f9e392ca07bddbc3832a050bbb2dd96a8c48313e5025beda6ad50c132c4f310053050d17985c010f884e6b
7
+ data.tar.gz: a307fa166260f5dc6e8e44b6099fe5842b4e477bb941922ed0f44f92b8e4fca2c619e2c3d24d861655bca0477fe99ae2fbc372381d85a3b8f13172c9fac062b6
@@ -1,4 +1,6 @@
1
- class Topographer::InvalidMappingError < StandardError; end
2
- class Topographer::InvalidStructureError < Topographer::InvalidMappingError; end
3
- class Topographer::MappingFailure < Topographer::InvalidMappingError; end
1
+ module Topographer
2
+ class InvalidMappingError < StandardError; end
3
+ class InvalidStructureError < Topographer::InvalidMappingError; end
4
+ class MappingFailure < Topographer::InvalidMappingError; end
5
+ end
4
6
 
@@ -17,17 +17,21 @@ class Topographer::Importer
17
17
  importer.logger
18
18
  end
19
19
 
20
- def initialize(input, import_class, strategy_class, logger, options = {})
20
+ # @param mapping_generator [#get_mapper] the object responsible for deciding which mapping to use for the strategy
21
+ # @param strategy either a Class that inherits from Topographer::Importer::Strategy::Base or an instance of a strategy
22
+ def initialize(input, mapping_generator, strategy, logger, options = {})
21
23
  @logger = logger
22
24
  @fatal_errors = []
23
25
 
24
26
  dry_run = options.fetch(:dry_run, false)
25
27
  ignore_unmapped_columns = options.fetch(:ignore_unmapped_columns, false)
26
28
 
27
- mapper = import_class.get_mapper(strategy_class)
29
+ strategy_class = strategy.is_a?(Class) ? strategy : strategy.class
30
+
31
+ mapper = mapping_generator.get_mapper(strategy_class)
28
32
 
29
33
  if importable?(input, mapper, ignore_unmapped_columns)
30
- strategy = strategy_class.new(mapper)
34
+ strategy = setup_strategy(mapper, strategy, strategy_class)
31
35
  strategy.dry_run = dry_run
32
36
  import_data(strategy, input, mapper.model_class.name)
33
37
  else
@@ -35,6 +39,14 @@ class Topographer::Importer
35
39
  end
36
40
  end
37
41
 
42
+ def setup_strategy(mapper, strategy, strategy_class)
43
+ if strategy == strategy_class
44
+ strategy_class.new(mapper) # supports legacy code
45
+ else
46
+ strategy.mapper = mapper
47
+ strategy
48
+ end
49
+ end
38
50
 
39
51
  def import_data(strategy, input, import_class)
40
52
  input.each do |data|
@@ -52,11 +64,15 @@ class Topographer::Importer
52
64
  end
53
65
  end
54
66
 
55
- def invalid_header_message(mapper)
56
- 'Invalid Input Header - Missing Columns: ' +
57
- mapper.missing_columns.join(', ') +
58
- ' Invalid Columns: ' +
59
- mapper.bad_columns.join(', ')
67
+ def invalid_header_message(mapper, ignore_unmapped_columns = false)
68
+ error = 'Invalid Input Header -'
69
+ if mapper.missing_columns.any?
70
+ error << " Missing Columns: #{mapper.missing_columns.join(', ')}"
71
+ end
72
+ if mapper.bad_columns.any? && !ignore_unmapped_columns
73
+ error << " Invalid Columns: #{mapper.bad_columns.join(', ')}"
74
+ end
75
+ error
60
76
  end
61
77
 
62
78
  def importable?(input, mapper, ignore_unmapped_columns)
@@ -72,7 +88,7 @@ class Topographer::Importer
72
88
  def valid_header?(input, mapper, ignore_unmapped_columns)
73
89
  valid = mapper.input_structure_valid?(input.get_header, ignore_unmapped_columns: ignore_unmapped_columns)
74
90
 
75
- fatal_errors << invalid_header_message(mapper) unless valid
91
+ fatal_errors << invalid_header_message(mapper, ignore_unmapped_columns) unless valid
76
92
 
77
93
  valid
78
94
  end
@@ -1,18 +1,24 @@
1
- module Topographer::Importer::Helpers
2
- require_relative 'helpers/write_log_to_csv'
1
+ require_relative 'helpers/write_log_to_csv'
3
2
 
4
- def boolify(word)
5
- return nil if word.nil?
3
+ module Topographer
4
+ class Importer
5
+ module Helpers
6
6
 
7
- case word.downcase
8
- when 'yes'
9
- true
10
- when 'no'
11
- false
12
- when 'true'
13
- true
14
- when 'false'
15
- false
7
+ def boolify(word)
8
+ return nil if word.nil?
9
+
10
+ case word.downcase
11
+ when 'yes'
12
+ true
13
+ when 'no'
14
+ false
15
+ when 'true'
16
+ true
17
+ when 'false'
18
+ false
19
+ end
20
+ end
16
21
  end
17
22
  end
18
23
  end
24
+
@@ -1,85 +1,93 @@
1
1
  require 'singleton'
2
2
  require 'csv'
3
- class Topographer::Importer::Helpers::WriteLogToCSV
4
- include Singleton
5
3
 
6
- def initialize
4
+ module Topographer
5
+ class Importer
6
+ module Helpers
7
+ class WriteLogToCSV
8
+ include Singleton
7
9
 
8
- end
10
+ def initialize
11
+
12
+ end
9
13
 
10
- def write_log_to_csv(log, output_file_path, options = {})
11
- @log = log
12
- @write_all = options.fetch(:write_all, true)
13
- CSV.open(output_file_path, 'wb') do |csv_file|
14
- csv_file << get_detail_header
15
- csv_file << get_details
16
- csv_file << get_log_header if @log.entries?
17
- @log.all_entries.each do |entry|
18
- if entry.failure? || @write_all
14
+ def write_log_to_csv(log, output_file_path, options = {})
15
+ @log = log
16
+ @write_all = options.fetch(:write_all, true)
17
+ CSV.open(output_file_path, 'wb') do |csv_file|
18
+ csv_file << get_detail_header
19
+ csv_file << get_details
20
+ csv_file << get_log_header if @log.entries?
21
+ @log.all_entries.each do |entry|
22
+ if entry.failure? || @write_all
19
23
 
20
- csv_file << format_log_entry(entry)
24
+ csv_file << format_log_entry(entry)
25
+ end
26
+ end
27
+ end
21
28
  end
22
- end
23
- end
24
- end
25
29
 
26
- private
30
+ private
27
31
 
28
- def get_detail_header
29
- if @write_all
30
- ['Fatal Errors', 'Total Imports', 'Successful Imports', 'Failed Imports']
31
- else
32
- ['Fatal Errors', 'Failed Imports']
33
- end
34
- end
32
+ def get_detail_header
33
+ if @write_all
34
+ ['Fatal Errors', 'Total Imports', 'Successful Imports', 'Failed Imports']
35
+ else
36
+ ['Fatal Errors', 'Failed Imports']
37
+ end
38
+ end
35
39
 
36
- def get_details
37
- details = []
38
- details << ((@log.fatal_error?) ? @log.fatal_errors.size : 'None')
39
- if @write_all
40
- details << @log.total_imports
41
- details << @log.successful_imports
42
- end
43
- details << @log.failed_imports
44
- end
40
+ def get_details
41
+ details = []
42
+ details << ((@log.fatal_error?) ? @log.fatal_errors.size : 'None')
43
+ if @write_all
44
+ details << @log.total_imports
45
+ details << @log.successful_imports
46
+ end
47
+ details << @log.failed_imports
48
+ end
45
49
 
46
- def get_log_header
47
- header = []
48
- header << 'Input Identifier'
49
- header << 'Source Identifier'
50
- header << 'Model Class'
51
- header << 'Timestamp'
52
- header << 'Status'
53
- header << 'Message'
54
- header << 'Details'
55
- end
50
+ def get_log_header
51
+ header = []
52
+ header << 'Input Identifier'
53
+ header << 'Source Identifier'
54
+ header << 'Model Class'
55
+ header << 'Timestamp'
56
+ header << 'Status'
57
+ header << 'Message'
58
+ header << 'Details'
59
+ end
56
60
 
57
- def format_log_entry(log_entry)
58
- entry = []
59
- entry << log_entry.input_identifier
60
- entry << log_entry.source_identifier
61
- entry << log_entry.model_name
62
- entry << log_entry.timestamp.strftime('%F - %T:%L')
63
- entry << ((log_entry.success?) ? 'Success' : 'Failure')
64
- entry << log_entry.message
65
- entry << format_log_entry_details(log_entry.details)
66
- end
61
+ def format_log_entry(log_entry)
62
+ entry = []
63
+ entry << log_entry.input_identifier
64
+ entry << log_entry.source_identifier
65
+ entry << log_entry.model_name
66
+ entry << log_entry.timestamp.strftime('%F - %T:%L')
67
+ entry << ((log_entry.success?) ? 'Success' : 'Failure')
68
+ entry << log_entry.message
69
+ entry << format_log_entry_details(log_entry.details)
70
+ end
67
71
 
68
- def format_log_entry_details(details)
69
- if details
70
- formatted_details = []
71
- details.keys.each do |key|
72
- detail_string = ''
73
- detail_messages = Array(details[key])
74
- if detail_messages.any?
75
- detail_string << key.to_s.capitalize << ': '
76
- detail_string << detail_messages.join(', ')
72
+ def format_log_entry_details(details)
73
+ if details
74
+ formatted_details = []
75
+ details.keys.each do |key|
76
+ detail_string = ''
77
+ detail_messages = Array(details[key])
78
+ if detail_messages.any?
79
+ detail_string << key.to_s.capitalize << ': '
80
+ detail_string << detail_messages.join(', ')
81
+ end
82
+ formatted_details << detail_string unless detail_string.empty?
83
+ end
84
+ formatted_details.join("; ")
85
+ else
86
+ ''
77
87
  end
78
- formatted_details << detail_string unless detail_string.empty?
79
88
  end
80
- formatted_details.join("; ")
81
- else
82
- ''
83
89
  end
84
90
  end
91
+ end
85
92
  end
93
+
@@ -1,5 +1,9 @@
1
- module Topographer::Importer::Importable
2
- def get_mapper(strategy)
3
- raise NotImplementedError
1
+ module Topographer
2
+ class Importer
3
+ module Importable
4
+ def get_mapper(strategy)
5
+ raise NotImplementedError
6
+ end
7
+ end
4
8
  end
5
9
  end
@@ -1,5 +1,12 @@
1
- class Topographer::Importer::Input
2
- require_relative 'input/source_data'
3
- require_relative 'input/base'
4
- require_relative 'input/roo'
1
+ require_relative 'input/source_data'
2
+ require_relative 'input/base'
3
+ require_relative 'input/roo'
4
+ require_relative 'input/delimited_spreadsheet'
5
+
6
+ module Topographer
7
+ class Importer
8
+ module Input
9
+
10
+ end
11
+ end
5
12
  end
@@ -1,21 +1,27 @@
1
- class Topographer::Importer::Input::Base
2
- def get_header
3
- raise NotImplementedError
4
- end
1
+ module Topographer
2
+ class Importer
3
+ module Input
4
+ class Base
5
+ def get_header
6
+ raise NotImplementedError
7
+ end
5
8
 
6
- def input_identifier
7
- raise NotImplementedError
8
- end
9
+ def input_identifier
10
+ raise NotImplementedError
11
+ end
9
12
 
10
- def each
11
- raise NotImplementedError
12
- end
13
+ def each
14
+ raise NotImplementedError
15
+ end
13
16
 
14
- def importable?
15
- true
16
- end
17
+ def importable?
18
+ true
19
+ end
17
20
 
18
- def failure_message
19
- ''
21
+ def failure_message
22
+ ''
23
+ end
24
+ end
25
+ end
20
26
  end
21
27
  end
@@ -0,0 +1,55 @@
1
+ module Topographer
2
+ class Importer
3
+ module Input
4
+ class DelimitedSpreadsheet < Topographer::Importer::Input::Base
5
+ include Enumerable
6
+
7
+ # Creates a new DelimitedSpreadsheet input wrapper. NOTE: Since Topographer relies on headers
8
+ # to map from input to output columns, you should enable header parsing in the CSV object passed in
9
+ #
10
+ # NOTE: the CSV used to construct this object should have the :return_headers flag set or the first
11
+ # row of data will be lost!
12
+ #
13
+ # @param name [String] the name of the delimited file being dealt with (e.g. My Data File 1)
14
+ # @param spreadsheet [CSV] the spreadsheet object to be parsed
15
+ def initialize(name, spreadsheet)
16
+ @sheet = spreadsheet
17
+ @name = name
18
+ end
19
+
20
+ # Returns the headers in the CSV file, or if header parsing is not enabled, an empty array
21
+ #
22
+ # @return [Array<String>] the headers in the file
23
+ def get_header
24
+ unless @header
25
+ if @sheet.headers === true
26
+ @sheet.shift
27
+ elsif @sheet.headers.nil?
28
+ @header = []
29
+ end
30
+ @header ||= @sheet.headers
31
+ end
32
+
33
+ @header
34
+ end
35
+
36
+ def input_identifier
37
+ @name
38
+ end
39
+
40
+ def each
41
+ @sheet.each_with_index do |data, index|
42
+ row_number = index + 2
43
+ source_identifier = "Row: #{row_number}"
44
+
45
+
46
+ yield Topographer::Importer::Input::SourceData.new(
47
+ source_identifier,
48
+ data.to_h
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,33 +1,40 @@
1
- class Topographer::Importer::Input::Roo < Topographer::Importer::Input::Base
2
- include Enumerable
1
+ module Topographer
2
+ class Importer
3
+ module Input
4
+ class Roo < Topographer::Importer::Input::Base
5
+ include Enumerable
3
6
 
4
- def initialize(roo_sheet, header_row=1, data_row=2)
5
- @sheet = roo_sheet
6
- @header = @sheet.row(header_row).map(&:strip)
7
- @start_data_row = data_row
8
- @end_data_row = @sheet.last_row
9
- end
7
+ def initialize(roo_sheet, header_row=1, data_row=2)
8
+ @sheet = roo_sheet
9
+ @header = @sheet.row(header_row).map(&:strip)
10
+ @start_data_row = data_row
11
+ @end_data_row = @sheet.last_row
12
+ end
10
13
 
11
- def get_header
12
- @header
13
- end
14
+ def get_header
15
+ @header
16
+ end
14
17
 
15
- def input_identifier
16
- #This is apparently how you get the name of the sheet...this makes me sad
17
- @sheet.default_sheet
18
- end
18
+ def input_identifier
19
+ #This is apparently how you get the name of the sheet...this makes me sad
20
+ @sheet.default_sheet
21
+ end
19
22
 
20
- def each
21
- @start_data_row.upto @end_data_row do |row_number|
22
- data = @sheet.row(row_number)
23
- source_identifier = "Row: #{row_number}"
23
+ def each
24
+ @start_data_row.upto @end_data_row do |row_number|
25
+ data = @sheet.row(row_number)
26
+ source_identifier = "Row: #{row_number}"
24
27
 
25
- if data.reject{ |column| column.blank? }.any?
26
- yield Topographer::Importer::Input::SourceData.new(
27
- source_identifier,
28
- Hash[@header.zip(data)]
29
- )
28
+ if data.reject{ |column| column.blank? }.any?
29
+ yield Topographer::Importer::Input::SourceData.new(
30
+ source_identifier,
31
+ Hash[@header.zip(data)]
32
+ )
33
+ end
34
+ end
35
+ end
30
36
  end
37
+
31
38
  end
32
39
  end
33
40
  end