smarter_csv 1.10.3 → 1.11.0.pre1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbc3d3e97a602c4b67a88a94eea6bb66feed23c061da6a1c07b04f84b820e8b0
4
- data.tar.gz: be5eea03fa0baeec95557772aef9d2bfd6c3e200103e9b686078c05c392a0a74
3
+ metadata.gz: f4f709c1e38fd4e755802a17103231bd17ba58f54ce2d3fd9f8ad57222704114
4
+ data.tar.gz: 3ac00625a1478793b9cf9272862e448de4ca80784db71ebce3060d0ae34623ad
5
5
  SHA512:
6
- metadata.gz: bb204a933e3f8ae93b626cd6c4c45e6296ece98f136b96389ae3d3459bb8826de3d128481faedb3e10fa85a547483568cce7fbb54de38ba6c8074b0276775123
7
- data.tar.gz: 724279536101f6561299ffc619c12393395c019ed771cc1e7b37b376804a05bee22b179fbd30770f29f6a9c099864268ecb222a2af7497a254b43982942aec90
6
+ metadata.gz: dec4c77b2b4019788ff576e0d58c913e4eb5d284f7c06bc95554dc3a83985a6a5ce38fbd3fbe851d7a5944ea53621bab7dc70d270b0fdf786624d7b32a631395
7
+ data.tar.gz: 3b9cdea9f632698d78c749196c225726ff37bc807a0f10ca6b9f1df5c9e4cd5ad15415c81042277461bd1a2449f1f07e24ecebb1b2e1dcff63759c26cc92f67f
data/.rubocop.yml CHANGED
@@ -88,12 +88,18 @@ Style/IfInsideElse:
88
88
  Style/IfUnlessModifier:
89
89
  Enabled: false
90
90
 
91
+ Style/InverseMethods:
92
+ Enabled: false
93
+
91
94
  Style/NestedTernaryOperator:
92
95
  Enabled: false
93
96
 
94
97
  Style/PreferredHashMethods:
95
98
  Enabled: false
96
99
 
100
+ Style/Proc:
101
+ Enabled: false
102
+
97
103
  Style/NumericPredicate:
98
104
  Enabled: false
99
105
 
@@ -129,6 +135,9 @@ Style/SymbolProc: # old Ruby versions can't do this
129
135
  Style/TrailingCommaInHashLiteral:
130
136
  Enabled: false
131
137
 
138
+ Style/TrailingCommaInArrayLiteral:
139
+ Enabled: false
140
+
132
141
  Style/TrailingUnderscoreVariable:
133
142
  Enabled: false
134
143
 
@@ -138,6 +147,9 @@ Style/TrivialAccessors:
138
147
  # Style/UnlessModifier:
139
148
  # Enabled: false
140
149
 
150
+ Style/WordArray:
151
+ Enabled: false
152
+
141
153
  Style/ZeroLengthPredicate:
142
154
  Enabled: false
143
155
 
data/CHANGELOG.md CHANGED
@@ -1,13 +1,42 @@
1
1
 
2
2
  # SmarterCSV 1.x Change Log
3
3
 
4
- ## 1.10.3 (2024-03-10)
5
- * fixed issue when frozen options are handed in (thanks to Daniel Pepper)
6
- * cleaned-up rspec tests (thanks to Daniel Pepper)
7
- * fixed link in README (issue #251)
4
+ ## T.B.D.
8
5
 
9
- ## 1.10.2 (2024-02-11)
10
- * improve error message for missing keys
6
+ * code refactor
7
+
8
+ * NEW BEHAVIOR:
9
+ - hidden `:v2_mode` options (incomplete!)
10
+ - pre-processing for v2 options
11
+ - implemented v2 `:header_transformations` (DO NOT USE YET!)
12
+ + -> check if all v1 transformations are correctly done
13
+ How are we going to
14
+ * disambiguate headers?
15
+
16
+
17
+ * do key_mapping? -> seems to work
18
+ - remove_unmapped_keys ?
19
+ - silence missing keys ... a missing mapped key should raise an exception, except when silenced
20
+ - required_keys needs to be a header-validation
21
+
22
+
23
+ * keep original headers? -> :none
24
+ * do strings_as_* ? -> either :keys_as_symbols, :keys_as_strings
25
+ * remove quote_chars? -> included in keys_as_*
26
+ * strip whitespace? -> included in keys_as_*
27
+
28
+ TODO:
29
+
30
+ - add tests for header_validations
31
+
32
+ - modify options to handle v1 and v2 options
33
+ - add v1 defaults in v2 processing
34
+ - add tests for all options processing
35
+ - 100% backwards compatibility when working in v1 mode
36
+
37
+
38
+ ## 1.10.1 (2024-01-07)
39
+ * fix incorrect warning about UTF-8 (issue #268, thanks hirowatari)
11
40
 
12
41
  ## 1.10.1 (2024-01-07)
13
42
  * fix incorrect warning about UTF-8 (issue #268, thanks hirowatari)
data/CONTRIBUTORS.md CHANGED
@@ -51,5 +51,4 @@ A Big Thank you to everyone who filed issues, sent comments, and who contributed
51
51
  * [Rahul Chaudhary](https://github.com/rahulch95)
52
52
  * [Alessandro Fazzi](https://github.com/pioneerskies)
53
53
  * [JP Camara](https://github.com/jpcamara)
54
- * [Kenton Hirowatari](https://github.com/hirowatari)
55
- * [Daniel Pepper](https://github.com/dpep)
54
+ * [Hiro Watari](https://github.com/hirowatari)
data/README.md CHANGED
@@ -23,8 +23,13 @@
23
23
 
24
24
  * default branch is `main` for 1.x development
25
25
 
26
- * 2.x development is [MOVED TO THIS PR](https://github.com/tilo/smarter_csv/pull/267)
27
- - 2.x behavior is still EXPERIMENTAL - DO NOT USE in production
26
+ * 2.x development is on `2.0-development` (check this branch for 2.0 documentation)
27
+ - This is an EXPERIMENTAL branch - DO NOT USE in production
28
+
29
+ #### Work towards Future Version 2.x
30
+
31
+ * Work towards SmarterCSV 2.x is still ongoing, with improved features, and more streamlined options, but consider it as experimental at this time.
32
+ Please check the [2.0-develop branch](https://github.com/tilo/smarter_csv/tree/2.0-develop), open any issues and pull requests with mention of tag v2.0.
28
33
 
29
34
  ---------------
30
35
 
@@ -389,9 +394,10 @@ And header and data validations will also be supported in 2.x
389
394
  * some CSV files use un-escaped quotation characters inside fields. This can cause the import to break. To get around this, use the `:force_simple_split => true` option in combination with `:strip_chars_from_headers => /[\-"]/` . This will also significantly speed up the import.
390
395
  If you would force a different :quote_char instead (setting it to a non-used character), then the import would be up to 5-times slower than using `:force_simple_split`.
391
396
 
392
- ## The original post that started SmarterCSV:
397
+ ## See also:
398
+
399
+ http://www.unixgods.org/~tilo/Ruby/process_csv_as_hashes.html
393
400
 
394
- http://www.unixgods.org/Ruby/process_csv_as_hashes.html
395
401
 
396
402
 
397
403
  ## Installation
@@ -2,7 +2,16 @@
2
2
 
3
3
  module SmarterCSV
4
4
  class << self
5
+ # this is processing the headers from the input file
5
6
  def hash_transformations(hash, options)
7
+ if options[:v2_mode]
8
+ hash_transformations_v2(hash, options)
9
+ else
10
+ hash_transformations_v1(hash, options)
11
+ end
12
+ end
13
+
14
+ def hash_transformations_v1(hash, options)
6
15
  # there may be unmapped keys, or keys purposedly mapped to nil or an empty key..
7
16
  # make sure we delete any key/value pairs from the hash, which the user wanted to delete:
8
17
  remove_empty_values = options[:remove_empty_values] == true
@@ -33,46 +42,117 @@ module SmarterCSV
33
42
  end
34
43
  end
35
44
 
36
- # def hash_transformations(hash, options)
37
- # # there may be unmapped keys, or keys purposedly mapped to nil or an empty key..
38
- # # make sure we delete any key/value pairs from the hash, which the user wanted to delete:
39
- # hash.delete(nil)
40
- # hash.delete('')
41
- # hash.delete(:"")
42
-
43
- # if options[:remove_empty_values] == true
44
- # hash.delete_if{|_k, v| has_rails ? v.blank? : blank?(v)}
45
- # end
46
-
47
- # hash.delete_if{|_k, v| !v.nil? && v =~ /^(0+|0+\.0+)$/} if options[:remove_zero_values] # values are Strings
48
- # hash.delete_if{|_k, v| v =~ options[:remove_values_matching]} if options[:remove_values_matching]
49
-
50
- # if options[:convert_values_to_numeric]
51
- # hash.each do |k, v|
52
- # # deal with the :only / :except options to :convert_values_to_numeric
53
- # next if limit_execution_for_only_or_except(options, :convert_values_to_numeric, k)
54
-
55
- # # convert if it's a numeric value:
56
- # case v
57
- # when /^[+-]?\d+\.\d+$/
58
- # hash[k] = v.to_f
59
- # when /^[+-]?\d+$/
60
- # hash[k] = v.to_i
61
- # end
62
- # end
63
- # end
64
-
65
- # if options[:value_converters]
66
- # hash.each do |k, v|
67
- # converter = options[:value_converters][k]
68
- # next unless converter
69
-
70
- # hash[k] = converter.convert(v)
71
- # end
72
- # end
73
-
74
- # hash
75
- # end
45
+ def hash_transformations_v2(hash, options)
46
+ return hash if options[:hash_transformations].nil? || options[:hash_transformations].empty?
47
+
48
+ # do the header transformations the user requested:
49
+ if options[:hash_transformations]
50
+ options[:hash_transformations].each do |transformation|
51
+ if transformation.respond_to?(:call) # this is used when a user-provided Proc is passed in
52
+ hash = transformation.call(hash, options)
53
+ else
54
+ case transformation
55
+ when Symbol # this is used for pre-defined transformations that are defined in the SmarterCSV module
56
+ hash = public_send(transformation, hash, options)
57
+ when Hash # this is called for hash arguments, e.g. hash_transformations
58
+ trans, args = transformation.first # .first treats the hash first element as an array
59
+ hash = apply_transformation(trans, hash, args, options)
60
+ when Array # this can be used for passing additional arguments in array form (e.g. into a Proc)
61
+ trans, *args = transformation
62
+ hash = apply_transformation(trans, hash, args, options)
63
+ else
64
+ raise SmarterCSV::IncorrectOption, "Invalid transformation type: #{transformation.class}"
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ hash
71
+ end
72
+
73
+ #
74
+ # To handle v1-backward-compatible behavior, it is faster to roll all behavior into one method
75
+ #
76
+ def v1_backwards_compatibility(hash, options)
77
+ hash.each_with_object({}) do |(k, v), new_hash|
78
+ next if k.nil? || k == '' || k == :"" # remove_empty_keys
79
+ next if has_rails ? v.blank? : blank?(v) # remove_empty_values
80
+
81
+ # convert_values_to_numeric:
82
+ # deal with the :only / :except options to :convert_values_to_numeric
83
+ unless limit_execution_for_only_or_except(options, :convert_values_to_numeric, k)
84
+ if v =~ /^[+-]?\d+\.\d+$/
85
+ v = v.to_f
86
+ elsif v =~ /^[+-]?\d+$/
87
+ v = v.to_i
88
+ end
89
+ end
90
+
91
+ new_hash[k] = v
92
+ end
93
+ end
94
+
95
+ #
96
+ # Building Blocks in case you want to build your own flow:
97
+ #
98
+
99
+ def value_converters(hash, _options)
100
+ #
101
+ # TO BE IMPLEMENTED
102
+ #
103
+ end
104
+
105
+ def strip_spaces(hash, _options)
106
+ hash.each_key {|key| hash[key].strip! unless hash[key].nil? } # &. syntax was introduced in Ruby 2.3 - need to stay backwards compatible
107
+ end
108
+
109
+ def remove_blank_values(hash, _options)
110
+ hash.each_key {|key| hash.delete(key) if hash[key].nil? || hash[key].is_a?(String) && hash[key] !~ /[^[:space:]]/ }
111
+ end
112
+
113
+ def remove_zero_values(hash, _options)
114
+ hash.each_key {|key| hash.delete(key) if hash[key].is_a?(Numeric) && hash[key].zero? }
115
+ end
116
+
117
+ def remove_empty_keys(hash, _options)
118
+ hash.reject!{|key, _v| key.nil? || key.empty?}
119
+ end
120
+
121
+ def convert_values_to_numeric(hash, _options)
122
+ hash.each_key do |k|
123
+ case hash[k]
124
+ when /^[+-]?\d+\.\d+$/
125
+ hash[k] = hash[k].to_f
126
+ when /^[+-]?\d+$/
127
+ hash[k] = hash[k].to_i
128
+ end
129
+ end
130
+ end
131
+
132
+ def convert_values_to_numeric_unless_leading_zeroes(hash, _options)
133
+ hash.each_key do |k|
134
+ case hash[k]
135
+ when /^[+-]?[1-9]\d*\.\d+$/
136
+ hash[k] = hash[k].to_f
137
+ when /^[+-]?[1-9]\d*$/
138
+ hash[k] = hash[k].to_i
139
+ end
140
+ end
141
+ end
142
+
143
+ # IMPORTANT NOTE:
144
+ # this can lead to cases where a nil or empty value gets converted into 0 or 0.0,
145
+ # and can then not be properly removed!
146
+ #
147
+ # you should first try to use convert_values_to_numeric or convert_values_to_numeric_unless_leading_zeroes
148
+ #
149
+ def convert_to_integer(hash, _options)
150
+ hash.each_key {|key| hash[key] = hash[key].to_i }
151
+ end
152
+
153
+ def convert_to_float(hash, _options)
154
+ hash.each_key {|key| hash[key] = hash[key].to_f }
155
+ end
76
156
 
77
157
  protected
78
158
 
@@ -2,8 +2,18 @@
2
2
 
3
3
  module SmarterCSV
4
4
  class << self
5
- # transform the headers that were in the file:
5
+ # this is processing the headers from the input file
6
6
  def header_transformations(header_array, options)
7
+ if options[:v2_mode]
8
+ header_transformations_v2(header_array, options)
9
+ else
10
+ header_transformations_v1(header_array, options)
11
+ end
12
+ end
13
+
14
+ # ---- V1.x Version: transform the headers that were in the file: ------------------------------------------
15
+ #
16
+ def header_transformations_v1(header_array, options)
7
17
  header_array.map!{|x| x.gsub(%r/#{options[:quote_char]}/, '')}
8
18
  header_array.map!{|x| x.strip} if options[:strip_whitespace]
9
19
 
@@ -57,7 +67,99 @@ module SmarterCSV
57
67
  header
58
68
  end
59
69
  end
70
+
60
71
  headers
61
72
  end
73
+
74
+ # ---- V2.x Version: transform the headers that were in the file: ------------------------------------------
75
+ #
76
+ def header_transformations_v2(header_array, options)
77
+ return header_array if options[:header_transformations].nil? || options[:header_transformations].empty?
78
+
79
+ # do the header transformations the user requested:
80
+ if options[:header_transformations]
81
+ options[:header_transformations].each do |transformation|
82
+ if transformation.respond_to?(:call) # this is used when a user-provided Proc is passed in
83
+ header_array = transformation.call(header_array, options)
84
+ else
85
+ case transformation
86
+ when Symbol # this is used for pre-defined transformations that are defined in the SmarterCSV module
87
+ header_array = public_send(transformation, header_array, options)
88
+ when Hash # this is called for hash arguments, e.g. header_transformations
89
+ trans, args = transformation.first # .first treats the hash first element as an array
90
+ header_array = apply_transformation(trans, header_array, args, options)
91
+ when Array # this can be used for passing additional arguments in array form (e.g. into a Proc)
92
+ trans, *args = transformation
93
+ header_array = apply_transformation(trans, header_array, args, options)
94
+ else
95
+ raise SmarterCSV::IncorrectOption, "Invalid transformation type: #{transformation.class}"
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ header_array
102
+ end
103
+
104
+ def apply_transformation(transformation, header_array, args, options)
105
+ if transformation.respond_to?(:call)
106
+ # If transformation is a callable object (like a Proc)
107
+ transformation.call(header_array, args, options)
108
+ else
109
+ # If transformation is a symbol (method name)
110
+ public_send(transformation, header_array, args, options)
111
+ end
112
+ end
113
+
114
+ # pre-defined v2 header transformations:
115
+
116
+ # these are some pre-defined header transformations which can be used
117
+ # all these take the headers array as the input
118
+ #
119
+ # the computed options can be accessed via @options
120
+
121
+ def keys_as_symbols(headers, options)
122
+ headers.map do |header|
123
+ header.strip.downcase.gsub(%r{#{options[:quote_char]}}, '').gsub(/(\s|-)+/, '_').to_sym
124
+ end
125
+ end
126
+
127
+ def keys_as_strings(headers, options)
128
+ headers.map do |header|
129
+ header.strip.gsub(%r{#{options[:quote_char]}}, '').downcase.gsub(/(\s|-)+/, '_')
130
+ end
131
+ end
132
+
133
+ def downcase_headers(headers, _options)
134
+ headers.map do |header|
135
+ header.strip.downcase!
136
+ end
137
+ end
138
+
139
+ def key_mapping(headers, mapping = {}, options)
140
+ raise(SmarterCSV::IncorrectOption, "ERROR: incorrect format for key_mapping! Expecting hash with from -> to mappings") if mapping.empty? || !mapping.is_a?(Hash)
141
+
142
+ headers_set = headers.to_set
143
+ mapping_keys_set = mapping.keys.to_set
144
+ silence_keys_set = (options[:silence_missing_keys] || []).to_set
145
+
146
+ # Check for missing keys
147
+ missing_keys = mapping_keys_set - headers_set - silence_keys_set
148
+ raise SmarterCSV::KeyMappingError, "ERROR: cannot map headers: #{missing_keys.to_a.join(', ')}" if missing_keys.any? && !options[:silence_missing_keys]
149
+
150
+ # Apply key mapping, retaining nils for explicitly mapped headers
151
+ headers.map do |header|
152
+ if mapping.key?(header)
153
+ # Maps the key according to the mapping, including nil mapping
154
+ mapping[header]
155
+ elsif options[:remove_unmapped_keys]
156
+ # Remove headers not specified in the mapping
157
+ nil
158
+ else
159
+ # Keep the original header if not specified in the mapping
160
+ header
161
+ end
162
+ end
163
+ end
62
164
  end
63
165
  end
@@ -3,11 +3,21 @@
3
3
  module SmarterCSV
4
4
  class << self
5
5
  def header_validations(headers, options)
6
- check_duplicate_headers(headers, options)
7
- check_required_headers(headers, options)
6
+ if options[:v2_mode]
7
+ header_validations_v2(headers, options)
8
+ else
9
+ header_validations_v1(headers, options)
10
+ end
11
+ end
12
+
13
+ # ---- V1.x Version: validate the headers -----------------------------------------------------------------
14
+
15
+ def header_validations_v1(headers, options)
16
+ check_duplicate_headers_v1(headers, options)
17
+ check_required_headers_v1(headers, options)
8
18
  end
9
19
 
10
- def check_duplicate_headers(headers, _options)
20
+ def check_duplicate_headers_v1(headers, _options)
11
21
  header_counts = Hash.new(0)
12
22
  headers.each { |header| header_counts[header] += 1 unless header.nil? }
13
23
 
@@ -18,17 +28,109 @@ module SmarterCSV
18
28
  end
19
29
  end
20
30
 
21
- require 'set'
22
-
23
- def check_required_headers(headers, options)
31
+ def check_required_headers_v1(headers, options)
24
32
  if options[:required_keys] && options[:required_keys].is_a?(Array)
25
33
  headers_set = headers.to_set
26
34
  missing_keys = options[:required_keys].select { |k| !headers_set.include?(k) }
27
35
 
28
36
  unless missing_keys.empty?
29
- raise SmarterCSV::MissingKeys, "ERROR: missing attributes: #{missing_keys.join(',')}. Check `SmarterCSV.headers` for original headers."
37
+ raise SmarterCSV::MissingKeys, "ERROR: missing attributes: #{missing_keys.join(',')}"
30
38
  end
31
39
  end
32
40
  end
41
+
42
+ # ---- V2.x Version: validate the headers -----------------------------------------------------------------
43
+
44
+ # def header_validations_v2(headers, options)
45
+ # return unless options[:header_validations]
46
+
47
+ # options[:header_validations].each do |validation|
48
+ # if validation.respond_to?(:call)
49
+ # # Directly call if it's a Proc or lambda
50
+ # validation.call(headers)
51
+ # else
52
+ # binding.pry
53
+ # # Handle Symbol, Hash, or Array
54
+ # method_name, args = validation.is_a?(Symbol) ? [validation, []] : validation
55
+ # public_send(method_name, headers, *Array(args))
56
+ # end
57
+ # end
58
+ # end
59
+
60
+ def header_validations_v2(headers, options)
61
+ return unless options[:header_validations]
62
+
63
+ # do the header validations the user requested:
64
+ # Header validations typically raise errors directly
65
+ #
66
+ options[:header_validations].each do |validation|
67
+ if validation.respond_to?(:call)
68
+ # Directly call if it's a Proc or lambda
69
+ validation.call(headers)
70
+ else
71
+ case validation
72
+ when Symbol
73
+ public_send(validation, headers)
74
+ when Hash
75
+ val, args = validation.first
76
+ public_send(val, headers, args)
77
+ when Array
78
+ val, *args = validation
79
+ public_send(val, headers, args)
80
+ else
81
+ raise SmarterCSV::IncorrectOption, "Invalid validation type: #{validation.class}"
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # def header_validations_v2_orig(headers, options)
88
+ # # do the header validations the user requested:
89
+ # # Header validations typically raise errors directly
90
+ # #
91
+ # if options[:header_validations]
92
+ # options[:header_validations].each do |validation|
93
+ # case validation
94
+ # when Symbol
95
+ # public_send(validation, headers)
96
+ # when Hash
97
+ # val, args = validation.first
98
+ # public_send(val, headers, args)
99
+ # when Array
100
+ # val, args = validation
101
+ # public_send(val, headers, args)
102
+ # else
103
+ # validation.call(headers) unless validation.nil?
104
+ # end
105
+ # end
106
+ # end
107
+ # end
108
+
109
+ # these are some pre-defined header validations which can be used
110
+ # all these take the headers array as the input
111
+ #
112
+ # the computed options can be accessed via @options
113
+
114
+ def unique_headers(headers)
115
+ header_counts = Hash.new(0)
116
+ headers.each { |header| header_counts[header] += 1 unless header.nil? }
117
+
118
+ duplicates = header_counts.select { |_, count| count > 1 }
119
+
120
+ unless duplicates.empty?
121
+ raise(SmarterCSV::DuplicateHeaders, "Duplicate Headers in CSV: #{duplicates.inspect}")
122
+ end
123
+ end
124
+
125
+ def required_headers(headers, required = [])
126
+ raise(SmarterCSV::IncorrectOption, "ERROR: required_headers validation needs an array argument") unless required.is_a?(Array)
127
+
128
+ headers_set = headers.to_set
129
+ missing = required.select { |r| !headers_set.include?(r) }
130
+
131
+ unless missing.empty?
132
+ raise(SmarterCSV::MissingKeys, "Missing Headers in CSV: #{missing.inspect}")
133
+ end
134
+ end
33
135
  end
34
136
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmarterCSV
4
- DEFAULT_OPTIONS = {
4
+ COMMON_OPTIONS = {
5
5
  acceleration: true,
6
6
  auto_row_sep_chars: 500,
7
7
  chunk_size: nil,
@@ -15,39 +15,66 @@ module SmarterCSV
15
15
  force_utf8: false,
16
16
  headers_in_file: true,
17
17
  invalid_byte_sequence: '',
18
+ quote_char: '"',
19
+ remove_unmapped_keys: false,
20
+ row_sep: :auto, # was: $/,
21
+ silence_deprecations: false, # new in 1.11
22
+ silence_missing_keys: false,
23
+ skip_lines: nil,
24
+ user_provided_headers: nil,
25
+ verbose: false,
26
+ with_line_numbers: false,
27
+ v2_mode: false,
28
+ }.freeze
29
+
30
+ V1_DEFAULT_OPTIONS = {
18
31
  keep_original_headers: false,
19
32
  key_mapping: nil,
20
- quote_char: '"',
21
33
  remove_empty_hashes: true,
22
34
  remove_empty_values: true,
23
- remove_unmapped_keys: false,
24
35
  remove_values_matching: nil,
25
36
  remove_zero_values: false,
26
37
  required_headers: nil,
27
38
  required_keys: nil,
28
- row_sep: :auto, # was: $/,
29
- silence_missing_keys: false,
30
- skip_lines: nil,
31
39
  strings_as_keys: false,
32
40
  strip_chars_from_headers: nil,
33
41
  strip_whitespace: true,
34
- user_provided_headers: nil,
35
42
  value_converters: nil,
36
- verbose: false,
37
- with_line_numbers: false,
43
+ v2_mode: false,
38
44
  }.freeze
39
45
 
46
+ DEPRECATED_OPTIONS = [
47
+ :convert_values_to_numeric,
48
+ :downcase_headers,
49
+ :keep_original_headers,
50
+ :key_mapping,
51
+ :remove_empty_hashes,
52
+ :remove_empty_values,
53
+ :remove_values_matching,
54
+ :remove_zero_values,
55
+ :required_headers,
56
+ :required_keys,
57
+ :stirngs_as_keys,
58
+ :strip_cars_from_headers,
59
+ :strip_whitespace,
60
+ :value_converters,
61
+ ].freeze
62
+
40
63
  class << self
41
64
  # NOTE: this is not called when "parse" methods are tested by themselves
42
65
  def process_options(given_options = {})
43
66
  puts "User provided options:\n#{pp(given_options)}\n" if given_options[:verbose]
44
67
 
45
- @options = DEFAULT_OPTIONS.dup.merge!(given_options)
46
-
47
68
  # fix invalid input
48
- @options[:invalid_byte_sequence] ||= ''
69
+ given_options[:invalid_byte_sequence] = '' if given_options[:invalid_byte_sequence].nil?
70
+
71
+ # warn about deprecated options / raises error for v2_mode
72
+ handle_deprecations(given_options)
73
+
74
+ given_options = preprocess_v2_options(given_options) if given_options[:v2_mode]
49
75
 
50
- puts "Computed options:\n#{pp(@options)}\n" if @options[:verbose]
76
+ @options = compute_default_options(given_options).merge!(given_options)
77
+ puts "Computed options:\n#{pp(@options)}\n" if given_options[:verbose]
51
78
 
52
79
  validate_options!(@options)
53
80
  @options
@@ -57,11 +84,35 @@ module SmarterCSV
57
84
  #
58
85
  # ONLY FOR BACKWARDS-COMPATIBILITY
59
86
  def default_options
60
- DEFAULT_OPTIONS
87
+ COMMON_OPTIONS.merge(V1_DEFAULT_OPTIONS)
61
88
  end
62
89
 
63
90
  private
64
91
 
92
+ def compute_default_options(options = {})
93
+ return COMMON_OPTIONS.merge(V1_DEFAULT_OPTIONS) unless options[:v2_mode]
94
+
95
+ default_options = {}
96
+ if options[:defaults].to_s != 'none'
97
+ default_options = COMMON_OPTIONS.dup.merge(V2_DEFAULT_OPTIONS)
98
+ if options[:defaults].to_s == 'v1'
99
+ default_options.merge(V1_TRANSFORMATIONS)
100
+ else
101
+ default_options.merge(V2_TRANSFORMATIONS)
102
+ end
103
+ end
104
+ end
105
+
106
+ def handle_deprecations(options)
107
+ used_deprecated_options = DEPRECATED_OPTIONS & options.keys
108
+ message = "SmarterCSV #{VERSION} DEPRECATED OPTIONS: #{pp(used_deprecated_options)}"
109
+ if options[:v2_mode]
110
+ raise(SmarterCSV::DeprecatedOptions, "ERROR: #{message}") unless used_deprecated_options.empty? || options[:silence_deprecations]
111
+ else
112
+ puts "DEPRECATION WARNING: #{message}" unless used_deprecated_options.empty? || options[:silence_deprecations]
113
+ end
114
+ end
115
+
65
116
  def validate_options!(options)
66
117
  # deprecate required_headers
67
118
  unless options[:required_headers].nil?
@@ -90,5 +141,57 @@ module SmarterCSV
90
141
  def pp(value)
91
142
  defined?(AwesomePrint) ? value.awesome_inspect(index: nil) : value.inspect
92
143
  end
144
+
145
+ # ---- V2 code ----------------------------------------------------------------------------------------
146
+
147
+ V2_DEFAULT_OPTIONS = {
148
+ # These need to go to the COMMON_OPTIONS:
149
+ remove_empty_hashes: true, # this might need a transformation or move to common options
150
+ # ------------
151
+ header_transformations: [:keys_as_symbols],
152
+ header_validations: [:unique_headers],
153
+ # data_transformations: [:replace_blank_with_nil],
154
+ # data_validations: [],
155
+ hash_transformations: [:strip_spaces, :remove_blank_values],
156
+ hash_validations: [],
157
+ v2_mode: true,
158
+ }.freeze
159
+
160
+ V2_TRANSFORMATIONS = {
161
+ header_transformations: [:keys_as_symbols],
162
+ header_validations: [:unique_headers],
163
+ # data_transformations: [:replace_blank_with_nil],
164
+ # data_validations: [],
165
+ hash_transformations: [:v1_backwards_compatibility],
166
+ # hash_transformations: [:remove_empty_keys, :strip_spaces, :remove_blank_values, :convert_values_to_numeric], # ??? :convert_values_to_numeric]
167
+ hash_validations: [],
168
+ }.freeze
169
+
170
+ V1_TRANSFORMATIONS = {
171
+ header_transformations: [:keys_as_symbols],
172
+ header_validations: [:unique_headers],
173
+ # data_transformations: [:replace_blank_with_nil],
174
+ # data_validations: [],
175
+ hash_transformations: [:strip_spaces, :remove_blank_values, :convert_values_to_numeric],
176
+ hash_validations: [],
177
+ }.freeze
178
+
179
+ def preprocess_v2_options(options)
180
+ return options unless options[:v2_mode] || options[:header_transformations]
181
+
182
+ # We want to provide safe defaults for easy processing, that is why we have a special keyword :none
183
+ # to not do any header transformations..
184
+ #
185
+ # this is why we need to remove the 'none' here:
186
+ #
187
+ requested_header_transformations = options[:header_transformations]
188
+ if requested_header_transformations.to_s == 'none'
189
+ requested_header_transformations = []
190
+ else
191
+ requested_header_transformations = requested_header_transformations.reject {|x| x.to_s == 'none'} unless requested_header_transformations.nil?
192
+ end
193
+ options[:header_transformations] = requested_header_transformations || []
194
+ options
195
+ end
93
196
  end
94
197
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module SmarterCSV
4
4
  class SmarterCSVException < StandardError; end
5
+ class DeprecatedOptions < SmarterCSVException; end
5
6
  class HeaderSizeMismatch < SmarterCSVException; end
6
7
  class IncorrectOption < SmarterCSVException; end
7
8
  class ValidationError < SmarterCSVException; end
@@ -108,6 +109,10 @@ module SmarterCSV
108
109
 
109
110
  next if options[:remove_empty_hashes] && hash.empty?
110
111
 
112
+ #
113
+ # should HASH VALIDATIONS go here instead?
114
+ #
115
+
111
116
  puts "CSV Line #{@file_line_count}: #{pp(hash)}" if @verbose == '2' # very verbose setting
112
117
  # optional adding of csv_line_number to the hash to help debugging
113
118
  hash[:csv_line_number] = @csv_line_count if options[:with_line_numbers]
@@ -15,6 +15,7 @@ module SmarterCSV
15
15
  @raw_header = nil # header as it appears in the file
16
16
  @result = []
17
17
  @warnings = {}
18
+ @v2_mode = false
18
19
  @enforce_utf8 = false # only set to true if needed (after options parsing)
19
20
  end
20
21
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmarterCSV
4
- VERSION = "1.10.3"
4
+ VERSION = "1.11.0.pre1"
5
5
  end
data/lib/smarter_csv.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  require "smarter_csv/version"
4
6
  require "smarter_csv/file_io"
5
7
  require "smarter_csv/options_processing"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smarter_csv
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.3
4
+ version: 1.11.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tilo Sloboda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-10 00:00:00.000000000 Z
11
+ date: 2024-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_print
@@ -104,7 +104,6 @@ extensions:
104
104
  - ext/smarter_csv/extconf.rb
105
105
  extra_rdoc_files: []
106
106
  files:
107
- - ".rspec"
108
107
  - ".rubocop.yml"
109
108
  - ".rvmrc"
110
109
  - CHANGELOG.md
@@ -148,9 +147,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
147
  version: 2.5.0
149
148
  required_rubygems_version: !ruby/object:Gem::Requirement
150
149
  requirements:
151
- - - ">="
150
+ - - ">"
152
151
  - !ruby/object:Gem::Version
153
- version: '0'
152
+ version: 1.3.1
154
153
  requirements: []
155
154
  rubygems_version: 3.2.3
156
155
  signing_key:
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --require spec_helper