csvbuilder-importer 0.1.3 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46f2394667af938e31e1d8741500adab3b9137ebc62266130fc4e9d51e3ffba9
4
- data.tar.gz: 1fe82f6065b3a555b1f65379e06aa8dcf1af20f52176c5c1e8a5c4c41afd7b8a
3
+ metadata.gz: b630db32ac5aff6725cbdb9f66414aed575f0b7e5529dd982af31f2002c0ca88
4
+ data.tar.gz: d474369caa5696069c0273ca414e27e7f4aeb281e20312682fd99ec96d5880cc
5
5
  SHA512:
6
- metadata.gz: 66aba6fed78608302a0d7a6ec0fb51677b1343e221fe2c0ad6875750965acd5622147cee35755810b901ca51fd7c304ef9519dc7ed22d269987a06f44a2e1a7a
7
- data.tar.gz: eecc211fe2ab65813f26bbc2d8852e19f2dd56f1c8f5d150bea728372920c97189a032f4e03270e77320aa0332c9cbd050ca7c427b4f58e63ab7276e4d883bc9
6
+ metadata.gz: d188fde51bb5aafa126c10b0493f49ea085141d752cce03a4ed34c33ecbbc21712d3a06a3cb3232d2c56f9c40bd3b400224015ac42139e502844bdca497b1c06
7
+ data.tar.gz: c58f400d80291c30e4b9198381240b7a7ecfa4ddb0db4266b4fcd6eb0a744a0d7a7902a3f448ba8b73f8438a0c913c7195794750d2388aecee868fdccf8a3ba6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [Released]
4
+
5
+ ## [0.1.5] - 2023-07-26
6
+
7
+ - Add a way to abort at the importer level, handy to handle wrong headers check https://github.com/joel/csvbuilder-importer/pull/12
8
+ - Using Less Memory And Quicker Line Counter https://github.com/joel/csvbuilder-importer/pull/11
9
+
10
+ ## [0.1.4] - 2023-04-21
11
+
12
+ - Potential Security Fix
13
+
14
+ https://github.com/joel/csvbuilder-importer/compare/v0.1.2...v0.1.4
15
+
3
16
  ## [0.1.0] - 2022-12-16
4
17
 
5
18
  - Initial release
data/Gemfile.lock ADDED
@@ -0,0 +1,89 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ csvbuilder-importer (0.1.5)
5
+ activemodel (>= 5.2)
6
+ activesupport (>= 5.2)
7
+ csvbuilder-core
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activemodel (7.0.4.2)
13
+ activesupport (= 7.0.4.2)
14
+ activesupport (7.0.4.2)
15
+ concurrent-ruby (~> 1.0, >= 1.0.2)
16
+ i18n (>= 1.6, < 2)
17
+ minitest (>= 5.1)
18
+ tzinfo (~> 2.0)
19
+ ast (2.4.2)
20
+ concurrent-ruby (1.2.2)
21
+ csvbuilder-core (0.1.3)
22
+ activesupport (>= 5.2, < 8)
23
+ diff-lcs (1.5.0)
24
+ i18n (1.12.0)
25
+ concurrent-ruby (~> 1.0)
26
+ json (2.6.3)
27
+ minitest (5.17.0)
28
+ parallel (1.23.0)
29
+ parser (3.2.2.0)
30
+ ast (~> 2.4.1)
31
+ rainbow (3.1.1)
32
+ rake (13.0.6)
33
+ regexp_parser (2.8.0)
34
+ rexml (3.2.5)
35
+ rspec (3.12.0)
36
+ rspec-core (~> 3.12.0)
37
+ rspec-expectations (~> 3.12.0)
38
+ rspec-mocks (~> 3.12.0)
39
+ rspec-core (3.12.1)
40
+ rspec-support (~> 3.12.0)
41
+ rspec-expectations (3.12.2)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.12.0)
44
+ rspec-mocks (3.12.3)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.12.0)
47
+ rspec-support (3.12.0)
48
+ rubocop (1.50.2)
49
+ json (~> 2.3)
50
+ parallel (~> 1.10)
51
+ parser (>= 3.2.0.0)
52
+ rainbow (>= 2.2.2, < 4.0)
53
+ regexp_parser (>= 1.8, < 3.0)
54
+ rexml (>= 3.2.5, < 4.0)
55
+ rubocop-ast (>= 1.28.0, < 2.0)
56
+ ruby-progressbar (~> 1.7)
57
+ unicode-display_width (>= 2.4.0, < 3.0)
58
+ rubocop-ast (1.28.0)
59
+ parser (>= 3.2.1.0)
60
+ rubocop-capybara (2.18.0)
61
+ rubocop (~> 1.41)
62
+ rubocop-performance (1.17.1)
63
+ rubocop (>= 1.7.0, < 2.0)
64
+ rubocop-ast (>= 0.4.0)
65
+ rubocop-rake (0.6.0)
66
+ rubocop (~> 1.0)
67
+ rubocop-rspec (2.20.0)
68
+ rubocop (~> 1.33)
69
+ rubocop-capybara (~> 2.17)
70
+ ruby-progressbar (1.13.0)
71
+ tzinfo (2.0.6)
72
+ concurrent-ruby (~> 1.0)
73
+ unicode-display_width (2.4.2)
74
+
75
+ PLATFORMS
76
+ arm64-darwin-22
77
+ x86_64-linux
78
+
79
+ DEPENDENCIES
80
+ csvbuilder-importer!
81
+ rake
82
+ rspec
83
+ rubocop
84
+ rubocop-performance
85
+ rubocop-rake
86
+ rubocop-rspec
87
+
88
+ BUNDLED WITH
89
+ 2.4.7
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Csvbuilder::Importer
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/csvbuilder/importer`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ [Csvbuilder::Importer](https://github.com/joel/csvbuilder-importer) is part of the [csvbuilder-collection](https://github.com/joel/csvbuilder)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ The importer contains the implementation for importing data from a CSV file.
6
6
 
7
7
  ## Installation
8
8
 
@@ -14,9 +14,211 @@ If bundler is not being used to manage dependencies, install the gem by executin
14
14
 
15
15
  $ gem install csvbuilder-importer
16
16
 
17
- ## Usage
17
+ # Usage
18
18
 
19
- TODO: Write usage instructions here
19
+ Importing data from a CSV is critical and requires two validation layers. First, you need to ensure data from the CSV are correct, and Second, you need to check that data respects the business logic before inserting it into the system.
20
+
21
+ To do that, `Csvbuilder::Import` use `ActiveModel::Validations` so you can write your validations in the `CsvImportModel`.
22
+
23
+ ```ruby
24
+ class UserCsvImportModel
25
+ include Csvbuilder::Model
26
+ include Csvbuilder::Import
27
+
28
+ column :first_name
29
+ column :last_name
30
+
31
+ validates :first_name, presence: true, length: { minimum: 2 }
32
+
33
+ def abort?
34
+ "#{first_name} #{last_name}" == "Bill Gates"
35
+ end
36
+ end
37
+ ```
38
+
39
+ The import takes the CSV file and the Import class.
40
+
41
+ ```ruby
42
+ rows = Csvbuilder::Import::File.new(file.path, UserCsvImportModel).each
43
+ row_enumerator = rows.each # It's essential to go through the Enumerator to benefit from the callbacks. See References[^1]
44
+ row_model_instance = row_enumerator.next
45
+ ```
46
+
47
+ `Csvbuilder::Import` implement two essential methods:
48
+
49
+ 1. skip?
50
+ 2. abort? # NOTE: abort can be trigger at the importer level too.
51
+
52
+ You have to provide your implementation of the method `abort?`. If the method `abort?` returns true, the iteration will stop.
53
+
54
+ By default, `skip?` return true if the `CsvImportClass` is invalid, but it is safe to override. This means the previous iteration will not return any invalid row.
55
+
56
+ # Integration
57
+
58
+ Let's say we want to do something useful and add users if those users are valid.
59
+
60
+ Let's extract the `CsvRowModel` for more clarity:
61
+
62
+ ```ruby
63
+ class UserCsvRowModel
64
+ include Csvbuilder::Model
65
+
66
+ column :first_name
67
+ column :last_name
68
+ end
69
+ ```
70
+
71
+ ```ruby
72
+ class UserCsvImportModel < UserCsvRowModel
73
+ include Csvbuilder::Import
74
+
75
+ validates :first_name, presence: true, length: { minimum: 2 }
76
+ validates :last_name, presence: true, length: { minimum: 2 }
77
+
78
+ def user
79
+ User.new(first_name: first_name, last_name: last_name)
80
+ end
81
+
82
+ # Skip if the row is not valid,
83
+ # the user is not valid or
84
+ # the user already exists
85
+ def skip?
86
+ super || !user.valid? || user.exists?
87
+ end
88
+ end
89
+ ```
90
+
91
+ Now, we can safely import our users.
92
+
93
+ ```ruby
94
+ [
95
+ ["First name", "Last name"],
96
+ ["John" , "Doe" ],
97
+ ]
98
+
99
+ Csvbuilder::Import::File.new(file.path, UserCsvImportModel).each do |row_model|
100
+ row_model.user.save
101
+ end
102
+ ```
103
+
104
+ # Advance Integration
105
+
106
+ `Csvbuilder::Import::File` implement callbacks. It provides the following:
107
+
108
+ 1. before_each_iteration
109
+ 2. around_each_iteration
110
+ 3. after_each_iteration
111
+ 4. before_next
112
+ 5. around_next
113
+ 6. after_next
114
+ 7. before_abort
115
+ 8. before_skip
116
+
117
+ Let's extend `Csvbuilder::Import::File`
118
+
119
+ ```ruby
120
+ class Importer < Csvbuilder::Import::File
121
+ attr_reader :row_in_errors
122
+
123
+ def initialize(*args)
124
+ super
125
+ @row_in_errors = RowErrors.new
126
+ end
127
+
128
+ after_next do
129
+ next true unless current_row_model # End of File
130
+ next true if current_row_model.valid? # No Errors To Collect
131
+
132
+ row_in_errors.append_errors(current_row_model)
133
+ end
134
+ end
135
+ ```
136
+
137
+ Now the importer can report the errors encountered instead of ignoring them.
138
+
139
+ For documentation purposes, here is a possible implementation of the errors collector:
140
+
141
+ ```ruby
142
+ class RowErrors
143
+ attr_reader :headers, :errors
144
+
145
+ def initialize
146
+ @errors = []
147
+ end
148
+
149
+ def append_errors(row_model)
150
+ @headers ||= begin
151
+ errors << row_model.class.headers
152
+ row_model.class.headers
153
+ end
154
+
155
+ row_in_error = []
156
+ row_model.source_attributes.map do |key, value|
157
+ row_in_error << if row_model.errors.messages[key].present?
158
+ "Initial Value: [#{value}] - Errors: #{row_model.errors.messages[key].join(", ")}"
159
+ else
160
+ value
161
+ end
162
+ end
163
+ errors << row_in_error
164
+ end
165
+ end
166
+ ```
167
+
168
+ Now we can nicely show the errors which occur over the import.
169
+
170
+ ```ruby
171
+ [
172
+ ["First name", "Last name"],
173
+ ["J", "Doe"]
174
+ ]
175
+
176
+ importer.row_in_errors.errors
177
+ # => [
178
+ # => ["First Name", "Last Name"],
179
+ # => ["Initial Value: [J] - Errors: is too short (minimum is 2 characters)", "Doe"]
180
+ # => ]
181
+ ```
182
+
183
+ Thanks to the callback mechanism, the opportunities to interact with the import are immense. For instance, you can show the errors on a Web Form and offer the chance to the user to change the data and re-attempt.
184
+
185
+ For long imports, you can show a progress bar to help customers cope with the import time; as you know, if errors have occurred, you can change the colour of the progress bar accordingly and offer the possibility to stop the import earlier.
186
+
187
+ ## Aborting an import
188
+
189
+ There is a design challenge to handling an import line-by-line. If it makes the code more efficient and decoupled, we might have cases when we want to check something shared with all lines. The obvious ones are the headers. Let's say we want to check them and abort all imports if something wrong is detected. We probably don't want to add the abort conditioning on every line (Csvbuilder::Model or, more precisely, its extension Csvbuilder::Import). We would rather have it in the Importer itself. In that case, we can stop the Importer from invoking "abort!". Let's consider the following example:
190
+
191
+ ```ruby
192
+ class Importer < Csvbuilder::Import::File
193
+
194
+ after_next do
195
+ if HeaderChecker.new(current_row_model).invalid?
196
+ abort!
197
+ next true # Keep going into #each and hit the callbacks
198
+ end
199
+ end
200
+
201
+ end
202
+ ```
203
+
204
+ ```ruby
205
+ context "with incorrect headers" do
206
+
207
+ should "not import data" do
208
+ importer = Importer.new(@file.path, @importer_class, @context)
209
+
210
+ row_enumerator = importer.each
211
+
212
+ assert_raises StopIteration do
213
+ row_enumerator.next
214
+ end
215
+ end
216
+ end
217
+ ```
218
+
219
+ ## References
220
+
221
+ - [^1] Csvbuilder::Import::File#each https://github.com/joel/csvbuilder-importer/blob/e8e6633a03dda4ae0e5d6775ec9d395dec553fbe/lib/csvbuilder/importer/public/import/file.rb#L66-L68
20
222
 
21
223
  ## Development
22
224
 
@@ -26,7 +228,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
26
228
 
27
229
  ## Contributing
28
230
 
29
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/csvbuilder-importer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/csvbuilder-importer/blob/main/CODE_OF_CONDUCT.md).
231
+ Bug reports and pull requests are welcome on GitHub at https://github.com/joel/csvbuilder-importer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/csvbuilder-importer/blob/main/CODE_OF_CONDUCT.md).
30
232
 
31
233
  ## License
32
234
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Runtime dependencies
6
+ gem "activemodel", github: "rails/rails", branch: "main"
7
+ gem "activesupport", github: "rails/rails", branch: "main"
8
+
9
+ gem "csvbuilder-core"
10
+
11
+ # Development dependencies
12
+ gem "rake"
13
+ gem "rspec"
14
+ gem "rubocop"
15
+ gem "rubocop-performance"
16
+ gem "rubocop-rake"
17
+ gem "rubocop-rspec"
@@ -20,10 +20,10 @@ module Csvbuilder
20
20
  reset
21
21
  end
22
22
 
23
- # http://stackoverflow.com/questions/2650517/count-the-number-of-lines-in-a-file-without-reading-entire-file-into-memory
23
+ # https://gist.github.com/guilhermesimoes/d69e547884e556c3dc95?permalink_comment_id=4502636#gistcomment-4502636
24
24
  # @return [Integer] the number of rows in the file, including empty new lines
25
25
  def size
26
- @size ||= `wc -l #{file_path}`.split[0].to_i + 1
26
+ @size ||= ::File.read(file_path).count($/)
27
27
  end
28
28
 
29
29
  # If the current position is at the headers, skip it and return it. Otherwise, only return false.
@@ -9,7 +9,7 @@ module Csvbuilder
9
9
  extend ActiveModel::Callbacks
10
10
  include ActiveModel::Validations
11
11
 
12
- attr_reader :csv, :row_model_class, :index, :current_row_model, :previous_row_model, :context # -1 = start of file, 0 to infinity = index of row_model, nil = end of file, no row_model
12
+ attr_reader :interrupt, :csv, :row_model_class, :index, :current_row_model, :previous_row_model, :context # -1 = start of file, 0 to infinity = index of row_model, nil = end of file, no row_model
13
13
 
14
14
  delegate :size, :end_of_file?, :line_number, to: :csv
15
15
 
@@ -26,6 +26,7 @@ module Csvbuilder
26
26
  @csv = ::Csvbuilder::Import::Csv.new(file_path) # Full namespace provided to avoid confusion with Ruby CSV class.
27
27
  @row_model_class = row_model_class
28
28
  @context = context.to_h.symbolize_keys
29
+ @interrupt = false
29
30
  reset
30
31
  end
31
32
 
@@ -39,6 +40,7 @@ module Csvbuilder
39
40
  csv.reset
40
41
  @index = -1
41
42
  @current_row_model = nil
43
+ @interrupt = false
42
44
  end
43
45
 
44
46
  # Gets the next row model based on the context
@@ -74,7 +76,13 @@ module Csvbuilder
74
76
 
75
77
  # @return [Boolean] returns true, if the file should abort reading
76
78
  def abort?
77
- !valid? || !!current_row_model.try(:abort?)
79
+ interrupt || !valid? || !!current_row_model.try(:abort?)
80
+ end
81
+
82
+ def abort!
83
+ @interrupt = true
84
+
85
+ nil
78
86
  end
79
87
 
80
88
  # @return [Boolean] returns true, if the file should skip `current_row_model`
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Csvbuilder
4
4
  module Importer
5
- VERSION = "0.1.3"
5
+ VERSION = "0.1.5"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csvbuilder-importer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Azemar
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-12-20 00:00:00.000000000 Z
11
+ date: 2023-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -17,9 +17,6 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '5.2'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '8'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,9 +24,6 @@ dependencies:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '5.2'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '8'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: activesupport
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -37,9 +31,6 @@ dependencies:
37
31
  - - ">="
38
32
  - !ruby/object:Gem::Version
39
33
  version: '5.2'
40
- - - "<"
41
- - !ruby/object:Gem::Version
42
- version: '8'
43
34
  type: :runtime
44
35
  prerelease: false
45
36
  version_requirements: !ruby/object:Gem::Requirement
@@ -47,9 +38,6 @@ dependencies:
47
38
  - - ">="
48
39
  - !ruby/object:Gem::Version
49
40
  version: '5.2'
50
- - - "<"
51
- - !ruby/object:Gem::Version
52
- version: '8'
53
41
  - !ruby/object:Gem::Dependency
54
42
  name: csvbuilder-core
55
43
  requirement: !ruby/object:Gem::Requirement
@@ -77,12 +65,14 @@ files:
77
65
  - CHANGELOG.md
78
66
  - CODE_OF_CONDUCT.md
79
67
  - Gemfile
68
+ - Gemfile.lock
80
69
  - LICENSE.txt
81
70
  - README.md
82
71
  - Rakefile
83
72
  - gemfiles/Gemfile.5.2._gemfile
84
73
  - gemfiles/Gemfile.6.1._gemfile
85
74
  - gemfiles/Gemfile.7.0._gemfile
75
+ - gemfiles/rails_edge.gemfile
86
76
  - lib/csvbuilder/importer.rb
87
77
  - lib/csvbuilder/importer/concerns/import/attributes.rb
88
78
  - lib/csvbuilder/importer/concerns/import/base.rb
@@ -115,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
105
  - !ruby/object:Gem::Version
116
106
  version: '0'
117
107
  requirements: []
118
- rubygems_version: 3.3.22
108
+ rubygems_version: 3.4.17
119
109
  signing_key:
120
110
  specification_version: 4
121
111
  summary: Csvbuilder Importer contain the components to handle CSV importing