csv_model 0.2.1 → 1.0.0

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