csv_model 0.1.0 → 0.2.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: 670a14423c708636825d5a2cebed117a0ac186e2
4
- data.tar.gz: 7c39766c75b1b629c743036a5791da36c93032c9
3
+ metadata.gz: ccb22507d47dc2e7db940efdbb20d6366de3fb94
4
+ data.tar.gz: b7c9bf17cfac53e97271d63f6d3ea78634b2f8f6
5
5
  SHA512:
6
- metadata.gz: 8fb5608b94c655f60c36c89b469f651ddd4a725a64fa5359b6534ec4cc37197cb80387e3bc213d40cb09b49569a0417ebfa63c60ccf4e81e54d2db41d71f46e4
7
- data.tar.gz: 03c07a434abc2fec5416f62cff6453a4bffd2758f017e5c91cadb35daeca408adf26c03b1592e721ee6051c9be864f74f04e6d05c2c062a8a28bfffada80bc54
6
+ metadata.gz: 05ee82ff370463e138682ac77425d21e3dff73b6a876c4588a681a0efe45069936ce0b03d823f75ef086d0334ffd99cba8a80706b58341ed2336bba0a6f6492a
7
+ data.tar.gz: 31679c8cc0fc3314c9b27512940e1a2590997245791a41f33173c63887ea04d5a26b20c391621e795772130e81569676511dba30242dc8345365c193b8000cd9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- csv_model (0.1.0)
4
+ csv_model (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -4,3 +4,7 @@ csv_model
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
+
8
+ TODO: ability to have multiple alternate primary keys
9
+
10
+ active_record?
@@ -31,7 +31,7 @@ module CSVModel
31
31
  end
32
32
 
33
33
  def underscore
34
- tr(' ', '_').tr("-", "_")
34
+ tr(' ', '_').tr("-", "_")
35
35
  end
36
36
  end
37
37
 
@@ -9,6 +9,7 @@ module CSVModel
9
9
  def initialize(data, options = {})
10
10
  @data = data
11
11
  @options = options
12
+ validate_options
12
13
  end
13
14
 
14
15
  def columns
@@ -24,24 +25,43 @@ module CSVModel
24
25
  end
25
26
 
26
27
  def errors
27
- duplicate_column_errors + illegal_column_errors + missing_column_errors
28
+ duplicate_column_errors + illegal_column_errors + missing_column_errors + missing_key_column_errors
28
29
  end
29
30
 
30
- # TODO: Remove? Not currently used.
31
31
  def has_column?(key)
32
32
  !column_index(key).nil?
33
33
  end
34
34
 
35
35
  def primary_key_columns
36
- @primary_key_columns ||= columns.select { |x| primary_key_column_keys.include?(x.key) }
36
+ @primary_key_columns ||= begin
37
+ if has_primary_key? && has_primary_key_columns?
38
+ primary_primary_key_columns
39
+ elsif has_alternate_primary_key? && has_alternate_primary_key_columns?
40
+ alternate_primary_key_columns
41
+ else
42
+ []
43
+ end
44
+ end
37
45
  end
38
46
 
39
47
  def valid?
40
- has_required_columns? && !has_duplicate_columns? && !has_illegal_columns?
48
+ has_required_columns? && has_required_key_columns? && !has_duplicate_columns? && !has_illegal_columns?
41
49
  end
42
50
 
43
51
  protected
44
52
 
53
+ def alternate_primary_key_columns
54
+ columns.select { |x| alternate_primary_key_column_keys.include?(x.key) }
55
+ end
56
+
57
+ def alternate_primary_key_column_keys
58
+ alternate_primary_key_column_names.collect { |x| x.to_column_key }
59
+ end
60
+
61
+ def alternate_primary_key_column_names
62
+ option(:alternate_primary_key, [])
63
+ end
64
+
45
65
  def column_keys
46
66
  @column_keys ||= columns.collect { |x| x.key }
47
67
  end
@@ -65,6 +85,14 @@ module CSVModel
65
85
  .collect { |key, count| column_name(key) }
66
86
  end
67
87
 
88
+ def has_alternate_primary_key?
89
+ alternate_primary_key_column_names.any?
90
+ end
91
+
92
+ def has_alternate_primary_key_columns?
93
+ missing_alternate_primary_key_column_keys.empty?
94
+ end
95
+
68
96
  def has_duplicate_columns?
69
97
  data.count != column_map.keys.count
70
98
  end
@@ -77,6 +105,18 @@ module CSVModel
77
105
  missing_column_keys.empty?
78
106
  end
79
107
 
108
+ def has_required_key_columns?
109
+ !has_primary_key? || (has_primary_key? && has_primary_key_columns?) || (has_alternate_primary_key? && has_alternate_primary_key_columns?)
110
+ end
111
+
112
+ def has_primary_key?
113
+ primary_key_column_names.any?
114
+ end
115
+
116
+ def has_primary_key_columns?
117
+ missing_primary_key_column_keys.empty?
118
+ end
119
+
80
120
  def illegal_column_errors
81
121
  illegal_column_names.collect { |name| "Unknown column #{name}" }
82
122
  end
@@ -97,6 +137,10 @@ module CSVModel
97
137
  legal_column_names.collect { |x| x.to_column_key }
98
138
  end
99
139
 
140
+ def missing_alternate_primary_key_column_keys
141
+ alternate_primary_key_column_keys - column_keys
142
+ end
143
+
100
144
  def missing_column_errors
101
145
  missing_column_names.collect { |name| "Missing column #{name}" }
102
146
  end
@@ -109,10 +153,34 @@ module CSVModel
109
153
  missing_column_keys.collect { |key| required_column_name(key) }
110
154
  end
111
155
 
156
+ def missing_primary_key_column_keys
157
+ primary_key_column_keys - column_keys
158
+ end
159
+
160
+ def missing_primary_key_column_names
161
+ missing_primary_key_column_keys.collect { |key| primary_key_column_name(key) }
162
+ end
163
+
164
+ def missing_key_column_errors
165
+ has_alternate_primary_key? && has_alternate_primary_key_columns? ? [] : primary_key_column_errors
166
+ end
167
+
168
+ def primary_primary_key_columns
169
+ columns.select { |x| primary_key_column_keys.include?(x.key) }
170
+ end
171
+
172
+ def primary_key_column_errors
173
+ missing_primary_key_column_names.collect { |name| "Missing primary key column #{name}" }
174
+ end
175
+
112
176
  def primary_key_column_keys
113
177
  primary_key_column_names.collect { |x| x.to_column_key }
114
178
  end
115
179
 
180
+ def primary_key_column_name(column_key)
181
+ primary_key_column_names.find { |entry| entry.to_column_key == column_key }
182
+ end
183
+
116
184
  def primary_key_column_names
117
185
  option(:primary_key, [])
118
186
  end
@@ -129,5 +197,27 @@ module CSVModel
129
197
  option(:required_columns, [])
130
198
  end
131
199
 
200
+ def validate_options
201
+ if !option(:primary_key).nil? && primary_key_column_keys.empty?
202
+ raise ArgumentError.new("The primary_key cannot be be empty.")
203
+ end
204
+
205
+ if legal_column_keys.any? && (primary_key_column_keys - legal_column_keys).any?
206
+ raise ArgumentError.new("The primary_key cannot contain columns that are not included in legal_column_keys.")
207
+ end
208
+
209
+ if primary_key_column_names.empty? && alternate_primary_key_column_names.any?
210
+ raise ArgumentError.new("The alternate_primary_key cannot be specified if no primary_key is specified.")
211
+ end
212
+
213
+ if !option(:alternate_primary_key).nil? && primary_key_column_keys == alternate_primary_key_column_keys
214
+ raise ArgumentError.new("The alternate_primary_key cannot be identical to the primary_key.")
215
+ end
216
+
217
+ if legal_column_keys.any? && (alternate_primary_key_column_keys - legal_column_keys).any?
218
+ raise ArgumentError.new("The alternate_primary_key cannot contain columns that are not included in legal_column_keys.")
219
+ end
220
+ end
132
221
  end
222
+
133
223
  end
@@ -47,12 +47,16 @@ module CSVModel
47
47
  row
48
48
  end
49
49
 
50
+ def detect_duplicate_rows?
51
+ option(:detect_duplicate_rows, true)
52
+ end
53
+
50
54
  def header_class
51
55
  option(:header_class, HeaderRow)
52
56
  end
53
57
 
54
58
  def is_duplicate_key?(value)
55
- return false if value.nil? || value == "" || (value.is_a?(Array) && value.all_values_blank?)
59
+ return false if !detect_duplicate_rows? || value.nil? || value == "" || (value.is_a?(Array) && value.all_values_blank?)
56
60
  !keys.add?(value)
57
61
  end
58
62
 
@@ -7,7 +7,10 @@ module CSVModel
7
7
  attr_reader :options
8
8
 
9
9
  def option(key, default = nil)
10
- options.try(:[], key) || options.try(key) || default
10
+ value = options.try(:[], key)
11
+ value = options.try(key) if value.nil?
12
+ value = default if value.nil?
13
+ value
11
14
  end
12
15
 
13
16
  end
@@ -1,3 +1,3 @@
1
1
  module CSVModel
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -5,9 +5,31 @@ describe CSVModel::HeaderRow do
5
5
  let(:data) { ["Column One"] }
6
6
  let(:subject) { described_class.new(data) }
7
7
 
8
- # TODO: should detect invalid opbions
9
- # e.g. when primar_key is not a subset of columns
10
- # raise ArgumentError, 'Message'
8
+ describe "#new" do
9
+ it "raises an ArgumentError when primary_key is not a subset of legal columns" do
10
+ expect { described_class.new(data, legal_columns: ["Column One"], primary_key: ["Column Two"]) }.to raise_error(ArgumentError)
11
+ end
12
+
13
+ it "does not raises an ArgumentError when primary_key is a subset of legal columns" do
14
+ expect { described_class.new(data, legal_columns: ["Column One"], primary_key: ["column one"]) }.to_not raise_error
15
+ end
16
+
17
+ it "raises an ArgumentError when primary_key is specified but empty" do
18
+ expect { described_class.new(data, primary_key: []) }.to raise_error(ArgumentError)
19
+ end
20
+
21
+ it "raises an ArgumentError when alternative_primary_key is specified but no primary_key is specified" do
22
+ expect { described_class.new(data, alternate_primary_key: ["Column One"]) }.to raise_error(ArgumentError)
23
+ end
24
+
25
+ it "raises an ArgumentError when alternative_primary_key is not a subset of legal columns" do
26
+ expect { described_class.new(data, legal_columns: ["Column One"], primary_key: ["Column One"], alternate_primary_key: ["Column Two"]) }.to raise_error(ArgumentError)
27
+ end
28
+
29
+ it "raises an ArgumentError when alternative_primary_key colums are idential to primary key columns" do
30
+ expect { described_class.new(data, primary_key: ["Column One"], alternate_primary_key: ["Column One"]) }.to raise_error(ArgumentError)
31
+ end
32
+ end
11
33
 
12
34
  describe "#columns" do
13
35
  it "returns an empty array when no columns exist" do
@@ -77,10 +99,25 @@ describe CSVModel::HeaderRow do
77
99
  expect(subject.errors).to eq(["Unknown column Column One"])
78
100
  end
79
101
 
80
- it "returns returns required column message when header is missing required columns" do
102
+ it "returns required column message when header is missing required columns" do
81
103
  subject = described_class.new(data, required_columns: ["Column Two"])
82
104
  expect(subject.errors).to eq(["Missing column Column Two"])
83
105
  end
106
+
107
+ it "returns required column message when header is missing required primary key columns" do
108
+ subject = described_class.new(data, primary_key: ["Column Two"])
109
+ expect(subject.errors).to eq(["Missing primary key column Column Two"])
110
+ end
111
+
112
+ it "returns empty array when header is missing required primary key column but alternate is available" do
113
+ subject = described_class.new(data, primary_key: ["Column Two"], alternate_primary_key: ["Column One"])
114
+ expect(subject.errors).to eq([])
115
+ end
116
+
117
+ it "returns required column message when header is missing required primary key and alternate is not available " do
118
+ subject = described_class.new(data, primary_key: ["Column Two"], alternate_primary_key: ["Column Three"])
119
+ expect(subject.errors).to eq(["Missing primary key column Column Two"])
120
+ end
84
121
  end
85
122
 
86
123
  describe "#has_column?" do
@@ -118,6 +155,15 @@ describe CSVModel::HeaderRow do
118
155
  expect(column.name).to eq("Column One")
119
156
  end
120
157
  end
158
+
159
+ context "when the primary key column is not present but an alternate is available and present" do
160
+ let(:subject) { described_class.new(data, OpenStruct.new(primary_key: ["column four"], alternate_primary_key: ["column one"])) }
161
+
162
+ it "returns the alternate key column" do
163
+ column = subject.primary_key_columns.first
164
+ expect(column.name).to eq("Column One")
165
+ end
166
+ end
121
167
  end
122
168
 
123
169
  context "when the row has multiple primary key columns" do
@@ -145,10 +191,25 @@ describe CSVModel::HeaderRow do
145
191
  expect(subject.valid?).to eq(false)
146
192
  end
147
193
 
194
+ it "returns false when header is missing a primary key column" do
195
+ subject = described_class.new(data, primary_key: ["Column Two"])
196
+ expect(subject.valid?).to eq(false)
197
+ end
198
+
148
199
  it "returns false when header is missing required columns" do
149
200
  subject = described_class.new(data, required_columns: ["Column Two"])
150
201
  expect(subject.valid?).to eq(false)
151
202
  end
203
+
204
+ it "returns true when header is missing required primary key column but alternate is available" do
205
+ subject = described_class.new(data, primary_key: ["Column Two"], alternate_primary_key: ["Column One"])
206
+ expect(subject.valid?).to eq(true)
207
+ end
208
+
209
+ it "returns false when header is missing required primary key and alternate is not available " do
210
+ subject = described_class.new(data, primary_key: ["Column Two"], alternate_primary_key: ["Column Three"])
211
+ expect(subject.valid?).to eq(false)
212
+ end
152
213
  end
153
214
 
154
215
  describe "internals" do
@@ -167,6 +228,47 @@ describe CSVModel::HeaderRow do
167
228
  end
168
229
  end
169
230
 
231
+ describe "#has_alternate_primary_key?" do
232
+ it "doesn't respond to has_alternate_primary_key?" do
233
+ expect(subject.respond_to?(:has_alternate_primary_key?)).to eq(false)
234
+ end
235
+
236
+ it "returns false when an alternate primary key is not specified" do
237
+ expect(subject.send(:has_alternate_primary_key?)).to eq(false)
238
+ end
239
+
240
+ context "with an alternate primary key" do
241
+ let(:subject) { described_class.new(data, primary_key: ["Column Four"], alternate_primary_key: ["Column One"]) }
242
+
243
+ it "returns true" do
244
+ expect(subject.send(:has_alternate_primary_key?)).to eq(true)
245
+ end
246
+ end
247
+ end
248
+
249
+ describe "#has_alternate_primary_key_columns?" do
250
+ it "doesn't respond to has_alternate_primary_key_columns?" do
251
+ expect(subject.respond_to?(:has_alternate_primary_key_columns?)).to eq(false)
252
+ end
253
+
254
+ it "returns true when no primary key columns specified" do
255
+ expect(subject.send(:has_alternate_primary_key_columns?)).to eq(true)
256
+ end
257
+
258
+ context "with alternate key columns specified" do
259
+ let(:subject) { described_class.new(data, primary_key: ["Column Four"], alternate_primary_key: ["Column One"]) }
260
+
261
+ it "returns true when all alternate primary key columns are present" do
262
+ expect(subject.send(:has_alternate_primary_key_columns?)).to eq(true)
263
+ end
264
+
265
+ it "returns false when an alternate primry key column is missing" do
266
+ data[0] = "Column Two"
267
+ expect(subject.send(:has_alternate_primary_key_columns?)).to eq(false)
268
+ end
269
+ end
270
+ end
271
+
170
272
  describe "#has_duplicate_columns?" do
171
273
  it "doesn't respond to has_duplicate_columns?" do
172
274
  expect(subject.respond_to?(:has_duplicate_columns?)).to eq(false)
@@ -205,6 +307,47 @@ describe CSVModel::HeaderRow do
205
307
  end
206
308
  end
207
309
 
310
+ describe "#has_primary_key?" do
311
+ it "doesn't respond to has_primary_key?" do
312
+ expect(subject.respond_to?(:has_primary_key?)).to eq(false)
313
+ end
314
+
315
+ it "returns false when a primary key is not specified" do
316
+ expect(subject.send(:has_primary_key?)).to eq(false)
317
+ end
318
+
319
+ context "with a primary key" do
320
+ let(:subject) { described_class.new(data, primary_key: ["Column One"]) }
321
+
322
+ it "returns true" do
323
+ expect(subject.send(:has_primary_key?)).to eq(true)
324
+ end
325
+ end
326
+ end
327
+
328
+ describe "#has_primary_key_columns?" do
329
+ it "doesn't respond to has_primary_key_columns?" do
330
+ expect(subject.respond_to?(:has_primary_key_columns?)).to eq(false)
331
+ end
332
+
333
+ it "returns true when no primary key columns specified" do
334
+ expect(subject.send(:has_primary_key_columns?)).to eq(true)
335
+ end
336
+
337
+ context "with primary key columns specified" do
338
+ let(:subject) { described_class.new(data, primary_key: ["Column One"]) }
339
+
340
+ it "returns true when all primary key columns are present" do
341
+ expect(subject.send(:has_primary_key_columns?)).to eq(true)
342
+ end
343
+
344
+ it "returns false when a primry key column is missing" do
345
+ data[0] = "Column Two"
346
+ expect(subject.send(:has_primary_key_columns?)).to eq(false)
347
+ end
348
+ end
349
+ end
350
+
208
351
  describe "#has_required_columns?" do
209
352
  it "doesn't respond to has_required_columns?" do
210
353
  expect(subject.respond_to?(:has_required_columns?)).to eq(false)
@@ -228,7 +371,49 @@ describe CSVModel::HeaderRow do
228
371
  end
229
372
  end
230
373
 
231
- describe "illegal_column_names" do
374
+ describe "#has_required_key_columns?" do
375
+ it "doesn't respond to has_required_key_columns?" do
376
+ expect(subject.respond_to?(:has_required_key_columns?)).to eq(false)
377
+ end
378
+
379
+ it "returns true when no primary key is specified" do
380
+ expect(subject.send(:has_required_key_columns?)).to eq(true)
381
+ end
382
+
383
+ context "with primary key columns specified" do
384
+ let(:subject) { described_class.new(data, primary_key: ["Column One"]) }
385
+
386
+ it "returns true when all primary key columns are present" do
387
+ expect(subject.send(:has_required_key_columns?)).to eq(true)
388
+ end
389
+
390
+ it "returns false when a primary key columns is missing" do
391
+ data[0] = "Column Two"
392
+ expect(subject.send(:has_required_key_columns?)).to eq(false)
393
+ end
394
+ end
395
+
396
+ context "with primary and alternate key columns specified" do
397
+ let(:subject) { described_class.new(data, primary_key: ["Column Two"], alternate_primary_key: ["Column Four"]) }
398
+
399
+ it "returns true when all primary key columns are present" do
400
+ data[0] = "Column Two"
401
+ expect(subject.send(:has_required_key_columns?)).to eq(true)
402
+ end
403
+
404
+ it "returns true when a primary key columns is missing but all alternate primary key columns are present" do
405
+ data[0] = "Column Four"
406
+ expect(subject.send(:has_required_key_columns?)).to eq(true)
407
+ end
408
+
409
+ it "returns false when a primary key column is missing and no alternative is available" do
410
+ data[0] = "Column Three"
411
+ expect(subject.send(:has_required_key_columns?)).to eq(false)
412
+ end
413
+ end
414
+ end
415
+
416
+ describe "#illegal_column_names" do
232
417
  it "doesn't respond to illegal_colum_names" do
233
418
  expect(subject.respond_to?(:illegal_colum_names)).to eq(false)
234
419
  end
@@ -273,6 +458,29 @@ describe CSVModel::HeaderRow do
273
458
  end
274
459
  end
275
460
  end
461
+
462
+ describe "#missing_primary_key_column_names" do
463
+ it "doesn't respond to missing_primary_key_column_names" do
464
+ expect(subject.respond_to?(:missing_primary_key_column_names)).to eq(false)
465
+ end
466
+
467
+ it "returns emptry array when no primary key columns specified" do
468
+ expect(subject.send(:missing_primary_key_column_names)).to eq([])
469
+ end
470
+
471
+ context "with primary key columns specified" do
472
+ let(:subject) { described_class.new(data, OpenStruct.new(primary_key: ["Column One"])) }
473
+
474
+ it "returns empty array when all primary key columns are present" do
475
+ expect(subject.send(:missing_primary_key_column_names)).to eq([])
476
+ end
477
+
478
+ it "returns array of missing column names when a primary key column is missing" do
479
+ data[0] = "Column Two"
480
+ expect(subject.send(:missing_primary_key_column_names)).to eq(["Column One"])
481
+ end
482
+ end
483
+ end
276
484
  end
277
485
 
278
486
  end
@@ -139,12 +139,19 @@ describe CSVModel::Model do
139
139
  subject.send(:parse_data)
140
140
  end
141
141
 
142
+ context "with duplicate checking disabled" do
143
+ let(:subject) { described_class.new(data, detect_duplicate_rows: false, primary_key: [header_row.first]) }
144
+
145
+ it "does not mark any row as a duplicate" do
146
+ subject.rows.each { |row| expect(row.marked_as_duplicate?).to eq(false) }
147
+ end
148
+ end
149
+
142
150
  context "with a single column primary key" do
143
151
  let(:subject) { described_class.new(data, primary_key: [header_row.first]) }
144
152
 
145
153
  context "when primary key values are present" do
146
154
  it "does not mark the first row as a duplicate" do
147
- puts subject.structure_errors
148
155
  expect(subject.rows.first.marked_as_duplicate?).to eq(false)
149
156
  end
150
157
 
@@ -185,7 +192,6 @@ describe CSVModel::Model do
185
192
  end
186
193
  end
187
194
  end
188
-
189
195
  end
190
196
  end
191
197
  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.1.0
4
+ version: 0.2.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-01 00:00:00.000000000 Z
11
+ date: 2014-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler