csv_model 0.2.1 → 1.0.0

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
  SHA1:
3
- metadata.gz: b2c3dcb21cddf9d14f4d44336770c35d737c4165
4
- data.tar.gz: 96510b317ed2ec88d19ad86a51ee15c1b1c5c8ce
3
+ metadata.gz: e5c7053ec54a07cf27b8e903276c04a728c01511
4
+ data.tar.gz: fbc25ab237e583a936f6e0e4619c078b966631a8
5
5
  SHA512:
6
- metadata.gz: 565a7e1407cdcbbedcea280b6cf1cc63e050954275fd87b9c45e6b4073b69463aefc49baa77df3809f134259a69164ca6c95415e877fd8c40e4ed6e1e89a3de9
7
- data.tar.gz: 90f26d172765c9b1e28f49cd85045f753c896b41d6ca77fc4fe47e9b9bbe5aa5ba5f0e41cd4225c778eec950eb6df5e4f11af60f1e66fc088e39fd930d82c970
6
+ metadata.gz: 318238c605891112715aa69677b15f4bcfc6d35c8fcfa3376a1aeccd4e4da2b96059a87df834d167e741b5247998475850c7cfa7caed135b99f976854b5b3130
7
+ data.tar.gz: 34cddace105c379953649d4a477962d486b2d512558302437189d88335e37490959450fbe7454f5d34b41e41fac944d56bafb561ec21e8f5a83d581b5e9a05fa
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- csv_model (0.2.1)
4
+ csv_model (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,17 +1,71 @@
1
- csv_model
1
+ CSV Model
2
2
  =========
3
3
 
4
4
  [![Build Status](https://travis-ci.org/Scrimmage/csv_model.svg)](https://travis-ci.org/Scrimmage/csv_model)
5
5
 
6
6
  [![Code Climate](https://codeclimate.com/github/Scrimmage/csv_model.png)](https://codeclimate.com/github/Scrimmage/csv_model)
7
7
 
8
- TODO: ability to have multiple alternate primary keys
8
+ CSV Model is a bulk import library for ActiveRecord. It parses a CSV into memory, validates the structure, can check for duplicates, and creates or updates the relevant record for each row.
9
9
 
10
- active_record?
11
10
 
11
+ Requirements
12
+ ------------
12
13
 
13
- gem build csv_model.gemspec
14
- gem push csv_model-0.2.1.gem
14
+ ### Ruby
15
+
16
+ CSV Model requires Ruby version **>= 2.0.0**
17
+
18
+
19
+ Installation
20
+ ------------
21
+
22
+ CSVModel is distributed as a gem, which is how it should be used in your app.
23
+
24
+ Include the gem in your Gemfile:
25
+
26
+ ```ruby
27
+ gem "csv_model", "~> 1.0"
28
+ ```
29
+
30
+ Or, if you want to get the latest, you can get master from the main repository:
31
+
32
+ ```ruby
33
+ gem "csv_model", :git => "git://github.com/Scrimmage/csv_model.git"
34
+ ```
15
35
 
16
- gem install csv_model-0.2.1.gem
17
- gem uninstall csv_model -v 0.2.0
36
+
37
+ Example Usage
38
+ -----------
39
+
40
+ ```ruby
41
+ options = {
42
+ dry_run: is_dry_run?,
43
+ legal_columns: structure.legal_columns,
44
+ primary_key: structure.primary_key_columns,
45
+ required_columns: structure.required_columns,
46
+ row_model_finder: model_finder,
47
+ row_model_mapper: model_mapper,
48
+ }
49
+
50
+ cm = CSVModel::Model.new(data, options)
51
+
52
+ if cm.structure_valid?
53
+ puts "Found structural errors: #{csv.structure_errors.inspect}"
54
+ else
55
+ cm.rows.each do |row|
56
+ status = row.process_row
57
+ puts "Found row #{row.key} with status #{status} and errors #{row.errors.inspect}"
58
+ end
59
+ end
60
+ ```
61
+
62
+
63
+ Internal Notes
64
+ --------------
65
+
66
+ Deployment:
67
+
68
+ ```bash
69
+ gem build csv_model.gemspec
70
+ gem push csv_model-x.y.z.gem
71
+ ```
@@ -41,8 +41,8 @@ module CSVModel
41
41
  header_class.new(row, options)
42
42
  end
43
43
 
44
- def create_row(row)
45
- row = row_class.new(header, row, options)
44
+ def create_row(row, index)
45
+ row = row_class.new(header, row, index, options)
46
46
  row.mark_as_duplicate if is_duplicate_key?(row.key)
47
47
  row
48
48
  end
@@ -80,7 +80,7 @@ module CSVModel
80
80
  # end
81
81
 
82
82
  if index > 0
83
- @rows << create_row(row)
83
+ @rows << create_row(row, index)
84
84
  end
85
85
  end
86
86
  rescue CSV::MalformedCSVError => e
data/lib/csv_model/row.rb CHANGED
@@ -4,14 +4,17 @@ module CSVModel
4
4
  class Row
5
5
  include Utilities::Options
6
6
 
7
- attr_reader :data, :header, :marked_as_duplicate
7
+ attr_reader :data, :header, :model_index, :marked_as_duplicate
8
8
 
9
- def initialize(header, data, options = {})
9
+ def initialize(header, data, model_index, options = {})
10
10
  @header = header
11
11
  @data = data
12
+ @model_index = model_index
12
13
  @options = options
13
14
  end
14
15
 
16
+ alias_method :csv_index, :model_index
17
+
15
18
  def index(value)
16
19
  index = column_index(value) || value
17
20
  data[index] if index.is_a?(Fixnum) && index >= 0
@@ -36,6 +39,14 @@ module CSVModel
36
39
  end
37
40
  end
38
41
 
42
+ def map_all_attributes(attrs)
43
+ attrs
44
+ end
45
+
46
+ def map_key_attributes(attrs)
47
+ attrs
48
+ end
49
+
39
50
  def marked_as_duplicate?
40
51
  !!marked_as_duplicate
41
52
  end
@@ -44,6 +55,16 @@ module CSVModel
44
55
  @marked_as_duplicate = true
45
56
  end
46
57
 
58
+ def process_row
59
+ return model_instance.status if @processed
60
+ @processed = true
61
+
62
+ model_instance.assign_attributes(all_attributes)
63
+ model_instance.mark_as_duplicate if marked_as_duplicate?
64
+ model_instance.save(dry_run: is_dry_run?)
65
+ model_instance.status
66
+ end
67
+
47
68
  def status
48
69
  model_instance.status
49
70
  end
@@ -63,7 +84,7 @@ module CSVModel
63
84
  private
64
85
 
65
86
  def all_attributes
66
- @all_attributes ||= column_attributes_with_values(columns)
87
+ @all_attributes ||= model_mapper.map_all_attributes(column_attributes_with_values(columns))
67
88
  end
68
89
 
69
90
  def columns
@@ -87,41 +108,39 @@ module CSVModel
87
108
  option(:dry_run, false)
88
109
  end
89
110
 
90
- def model
91
- option(:model)
111
+ def model_adaptor
112
+ CSVModel::RowActiveRecordAdaptor
113
+ end
114
+
115
+ def model_finder
116
+ option(:row_model_finder)
92
117
  end
93
118
 
94
119
  def model_instance
95
120
  @model_instance ||= begin
96
121
  x = inherit_or_delegate(:find_row_model, key_attributes)
97
122
  x ||= inherit_or_delegate(:new_row_model, key_attributes)
98
- x = CSVModel::ObjectWithStatusSnapshot.new(x)
123
+ x = model_adaptor.new(x)
99
124
  end
100
125
  end
101
126
 
127
+ def model_mapper
128
+ option(:row_model_mapper, self)
129
+ end
130
+
102
131
  def key_attributes
103
132
  cols = primary_key_columns.any? ? primary_key_columns : columns
104
- @key_attributes ||= column_attributes_with_values(cols)
133
+ @key_attributes ||= model_mapper.map_key_attributes(column_attributes_with_values(cols))
105
134
  end
106
135
 
107
-
108
136
  def primary_key_columns
109
137
  header.primary_key_columns
110
138
  end
111
139
 
112
- def process_row
113
- return if @processed
114
- @processed = true
115
-
116
- model_instance.assign_attributes(all_attributes)
117
- model_instance.mark_as_duplicate if marked_as_duplicate?
118
- model_instance.save(dry_run: is_dry_run?)
119
- end
120
-
121
140
  private
122
141
 
123
142
  def inherit_or_delegate(method, *args)
124
- try(method, *args) || model.try(method, *args)
143
+ try(method, *args) || model_finder.try(method, *args)
125
144
  end
126
145
 
127
146
  end
@@ -1,7 +1,7 @@
1
1
  using CSVModel::Extensions
2
2
 
3
3
  module CSVModel
4
- class ObjectWithStatusSnapshot < SimpleDelegator
4
+ class RowActiveRecordAdaptor < SimpleDelegator
5
5
  include RecordStatus
6
6
 
7
7
  def assign_attributes(attributes)
@@ -1,3 +1,3 @@
1
1
  module CSVModel
2
- VERSION = "0.2.1"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/csv_model.rb CHANGED
@@ -7,10 +7,10 @@ require 'csv_model/record_status'
7
7
  require 'csv_model/utilities'
8
8
 
9
9
  require 'csv_model/column'
10
- require 'csv_model/row'
11
10
  require 'csv_model/header_row'
12
11
  require 'csv_model/model'
13
- require 'csv_model/object_with_status_snapshot'
12
+ require 'csv_model/row'
13
+ require 'csv_model/row_active_record_adaptor'
14
14
  require 'csv_model/version'
15
15
 
16
16
  module CSVModel
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe CSVModel::ObjectWithStatusSnapshot do
3
+ describe CSVModel::RowActiveRecordAdaptor do
4
4
  let(:model) { double("model", changed?: false, marked_for_destruction?: false, new_record?: false, valid?: false) }
5
5
  let(:subject) { described_class.new(model) }
6
6
 
@@ -5,10 +5,11 @@ describe CSVModel::Row do
5
5
  let(:header) { double("header") }
6
6
  let(:column) { double("column", key: "column one", model_attribute: :column_one, name: "Column One") }
7
7
  let(:columns) { [column] }
8
+ let(:index) { 1 }
8
9
  let(:primary_key_columns) { [] }
9
10
 
10
11
  let(:data) { ["Value One"] }
11
- let(:subject) { described_class.new(header, data) }
12
+ let(:subject) { described_class.new(header, data, index) }
12
13
 
13
14
  before do
14
15
  allow(header).to receive(:columns).and_return(columns)
@@ -17,14 +18,21 @@ describe CSVModel::Row do
17
18
  allow(header).to receive(:primary_key_columns).and_return(primary_key_columns)
18
19
  end
19
20
 
21
+ describe "#csv_index, #model_index" do
22
+ it "returns the index the row was instantiated with" do
23
+ expect(subject.csv_index).to eq(index)
24
+ expect(subject.model_index).to eq(index)
25
+ end
26
+ end
27
+
20
28
  describe "#errors" do
21
29
  context "when no errors" do
22
- let(:model) { double("model") }
23
- let(:subject) { described_class.new(header, data, model: model) }
30
+ let(:model_finder) { double("model-finder") }
31
+ let(:subject) { described_class.new(header, data, index, row_model_finder: model_finder) }
24
32
  let(:model_instance) { double("model-instance", changed?: false, marked_for_destruction?: false, new_record?: false, valid?: true) }
25
33
 
26
34
  before do
27
- allow(model).to receive(:find_row_model).and_return(model_instance)
35
+ allow(model_finder).to receive(:find_row_model).and_return(model_instance)
28
36
  allow(model_instance).to receive(:save)
29
37
  end
30
38
 
@@ -45,7 +53,7 @@ describe CSVModel::Row do
45
53
  end
46
54
 
47
55
  context "during a dry-run" do
48
- let(:subject) { described_class.new(header, data, dry_run: true) }
56
+ let(:subject) { described_class.new(header, data, index, dry_run: true) }
49
57
 
50
58
  it "returns an error" do
51
59
  expect(subject.errors).to include("Duplicate row")
@@ -62,13 +70,13 @@ describe CSVModel::Row do
62
70
  end
63
71
 
64
72
  context "when the model instance has errors" do
65
- let(:model) { double("model") }
66
- let(:subject) { described_class.new(header, data, model: model) }
73
+ let(:model_finder) { double("model-finder") }
74
+ let(:subject) { described_class.new(header, data, index, row_model_finder: model_finder) }
67
75
  let(:model_instance) { double("model-instance", changed?: false, errors: errors, marked_for_destruction?: false, new_record?: false, valid?: false) }
68
76
  let(:errors) { ["Message one", "Message two"] }
69
77
 
70
78
  before do
71
- allow(model).to receive(:find_row_model).and_return(model_instance)
79
+ allow(model_finder).to receive(:find_row_model).and_return(model_instance)
72
80
  end
73
81
 
74
82
  it "includes the model instance errors" do
@@ -156,6 +164,30 @@ describe CSVModel::Row do
156
164
  end
157
165
  end
158
166
 
167
+ describe "#process_row" do
168
+ let(:model_finder) { double("model-finder") }
169
+ let(:model_instance) { double("model-instance", changed?: true, marked_for_destruction?: false, new_record?: false, valid?: true) }
170
+ let(:subject) { described_class.new(header, data, index, row_model_finder: model_finder) }
171
+
172
+ before do
173
+ allow(model_finder).to receive(:find_row_model).and_return(model_instance)
174
+ allow(subject).to receive(:all_attributes).and_return(:all_attributes)
175
+ end
176
+
177
+ it "assigns attributes and saves the model instance" do
178
+ expect(model_instance).to receive(:assign_attributes).with(:all_attributes).ordered
179
+ expect(model_instance).to receive(:save)
180
+ subject.process_row
181
+ end
182
+
183
+ it "only processes a record once" do
184
+ expect(model_instance).to receive(:assign_attributes).with(:all_attributes).ordered
185
+ expect(model_instance).to receive(:save)
186
+ subject.process_row
187
+ subject.process_row
188
+ end
189
+ end
190
+
159
191
  describe "#status" do
160
192
  let(:model) { double("model") }
161
193
  let(:wrapper) { double("wrapper", status: :some_status) }
@@ -167,7 +199,7 @@ describe CSVModel::Row do
167
199
  end
168
200
 
169
201
  it "is delegates status to the model" do
170
- expect(CSVModel::ObjectWithStatusSnapshot).to receive(:new).and_return(wrapper)
202
+ expect(CSVModel::RowActiveRecordAdaptor).to receive(:new).and_return(wrapper)
171
203
  expect(subject.status).to eq(:some_status)
172
204
  end
173
205
  end
@@ -194,6 +226,19 @@ describe CSVModel::Row do
194
226
  expect(subject.send(:all_attributes)).to eq(column_one: "Value One")
195
227
  end
196
228
 
229
+ context "with a column mapping" do
230
+ let(:model_mapper) { double("model-mapper") }
231
+ let(:subject) { described_class.new(header, data, index, row_model_mapper: model_mapper) }
232
+
233
+ before do
234
+ allow(model_mapper).to receive(:map_all_attributes).with({ column_one: "Value One" }).and_return(mapped_key: "mapped_value")
235
+ end
236
+
237
+ it "maps attributes" do
238
+ expect(subject.send(:all_attributes)).to eq(mapped_key: "mapped_value")
239
+ end
240
+ end
241
+
197
242
  context "with multiple columns" do
198
243
  let(:columns) { [
199
244
  double("column", key: "column one", model_attribute: :column_one),
@@ -214,8 +259,8 @@ describe CSVModel::Row do
214
259
  end
215
260
 
216
261
  describe "#inherit_or_delegate" do
217
- let(:model) { double("model") }
218
- let(:subject) { described_class.new(header, data, model: model) }
262
+ let(:model_finder) { double("model-finder") }
263
+ let(:subject) { described_class.new(header, data, index, row_model_finder: model_finder) }
219
264
 
220
265
  it "doesn't respond to inherit_or_delegate" do
221
266
  expect(subject.respond_to?(:inherit_or_delegate)).to eq(false)
@@ -233,7 +278,7 @@ describe CSVModel::Row do
233
278
  end
234
279
 
235
280
  it "invokes delegate method if delegate exists and internal method is not defined" do
236
- expect(model).to receive(:some_method).with(:multiple, :args).and_return(:some_value)
281
+ expect(model_finder).to receive(:some_method).with(:multiple, :args).and_return(:some_value)
237
282
  expect(subject.send(:inherit_or_delegate, :some_method, :multiple, :args)).to eq(:some_value)
238
283
  end
239
284
  end
@@ -275,12 +320,25 @@ describe CSVModel::Row do
275
320
  expect(subject.send(:key_attributes)).to eq(column_one: "Value One", column_two: "Value Two")
276
321
  end
277
322
  end
323
+
324
+ context "with a column mapping" do
325
+ let(:model_mapper) { double("model-mapper") }
326
+ let(:subject) { described_class.new(header, data, index, row_model_mapper: model_mapper) }
327
+
328
+ before do
329
+ allow(model_mapper).to receive(:map_key_attributes).with({ column_one: "Value One" }).and_return(mapped_key: "mapped_value")
330
+ end
331
+
332
+ it "maps attributes" do
333
+ expect(subject.send(:key_attributes)).to eq(mapped_key: "mapped_value")
334
+ end
335
+ end
278
336
  end
279
337
 
280
338
  describe "#model_instance" do
281
- let(:model) { double("model") }
339
+ let(:model_finder) { double("model-finder") }
282
340
  let(:model_instance) { double("model-instance") }
283
- let(:subject) { described_class.new(header, data, model: model) }
341
+ let(:subject) { described_class.new(header, data, index, row_model_finder: model_finder) }
284
342
 
285
343
  before do
286
344
  allow(subject).to receive(:key_attributes).and_return(:key_attributes)
@@ -298,7 +356,7 @@ describe CSVModel::Row do
298
356
  end
299
357
 
300
358
  it "tries to find an instance via model#find_row_model when #find_row_model does not exist" do
301
- expect(model).to receive(:find_row_model).with(:key_attributes).and_return(model_instance)
359
+ expect(model_finder).to receive(:find_row_model).with(:key_attributes).and_return(model_instance)
302
360
  expect(subject.send(:model_instance)).to eq(model_instance)
303
361
  end
304
362
 
@@ -313,39 +371,11 @@ describe CSVModel::Row do
313
371
  end
314
372
 
315
373
  it "tries to instantiate a new model via model#new_row_model when a model cannot be found and #new_row_model does not exist" do
316
- expect(model).to receive(:find_row_model).with(:key_attributes).and_return(nil)
317
- expect(model).to receive(:new_row_model).with(:key_attributes).and_return(model_instance)
374
+ expect(model_finder).to receive(:find_row_model).with(:key_attributes).and_return(nil)
375
+ expect(model_finder).to receive(:new_row_model).with(:key_attributes).and_return(model_instance)
318
376
  expect(subject.send(:model_instance)).to eq(model_instance)
319
377
  end
320
378
  end
321
-
322
- describe "#process_row" do
323
- let(:model) { double("model") }
324
- let(:model_instance) { double("model-instance", changed?: true, marked_for_destruction?: false, new_record?: false, valid?: true) }
325
- let(:subject) { described_class.new(header, data, model: model) }
326
-
327
- before do
328
- allow(model).to receive(:find_row_model).and_return(model_instance)
329
- allow(subject).to receive(:all_attributes).and_return(:all_attributes)
330
- end
331
-
332
- it "doesn't respond to process_row" do
333
- expect(subject.respond_to?(:process_row)).to eq(false)
334
- end
335
-
336
- it "assigns attributes and saves the model instance" do
337
- expect(model_instance).to receive(:assign_attributes).with(:all_attributes).ordered
338
- expect(model_instance).to receive(:save)
339
- subject.send(:process_row)
340
- end
341
-
342
- it "only processes a record once" do
343
- expect(model_instance).to receive(:assign_attributes).with(:all_attributes).ordered
344
- expect(model_instance).to receive(:save)
345
- subject.send(:process_row)
346
- subject.send(:process_row)
347
- end
348
- end
349
379
  end
350
380
 
351
381
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csv_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Chadwick
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-11 00:00:00.000000000 Z
11
+ date: 2014-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -73,15 +73,15 @@ files:
73
73
  - lib/csv_model/extensions.rb
74
74
  - lib/csv_model/header_row.rb
75
75
  - lib/csv_model/model.rb
76
- - lib/csv_model/object_with_status_snapshot.rb
77
76
  - lib/csv_model/record_status.rb
78
77
  - lib/csv_model/row.rb
78
+ - lib/csv_model/row_active_record_adaptor.rb
79
79
  - lib/csv_model/utilities.rb
80
80
  - lib/csv_model/version.rb
81
81
  - spec/csv_model/column_spec.rb
82
82
  - spec/csv_model/header_row_spec.rb
83
83
  - spec/csv_model/model_spec.rb
84
- - spec/csv_model/object_with_status_snapshot_spec.rb
84
+ - spec/csv_model/row_active_record_adaptor_spec.rb
85
85
  - spec/csv_model/row_spec.rb
86
86
  - spec/csv_model/utilities_spec.rb
87
87
  - spec/spec_helper.rb
@@ -113,7 +113,7 @@ test_files:
113
113
  - spec/csv_model/column_spec.rb
114
114
  - spec/csv_model/header_row_spec.rb
115
115
  - spec/csv_model/model_spec.rb
116
- - spec/csv_model/object_with_status_snapshot_spec.rb
116
+ - spec/csv_model/row_active_record_adaptor_spec.rb
117
117
  - spec/csv_model/row_spec.rb
118
118
  - spec/csv_model/utilities_spec.rb
119
119
  - spec/spec_helper.rb