topographer 0.0.7 → 0.0.8

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