strong_csv 0.2.0 → 0.5.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 +4 -4
- data/README.md +347 -26
- data/lib/strong_csv/i18n.rb +47 -0
- data/lib/strong_csv/let.rb +38 -8
- data/lib/strong_csv/row.rb +1 -1
- data/lib/strong_csv/types/boolean.rb +1 -1
- data/lib/strong_csv/types/float.rb +16 -2
- data/lib/strong_csv/types/integer.rb +16 -2
- data/lib/strong_csv/types/literal.rb +22 -8
- data/lib/strong_csv/types/string.rb +2 -2
- data/lib/strong_csv/types/time.rb +2 -2
- data/lib/strong_csv/version.rb +1 -1
- data/lib/strong_csv.rb +8 -0
- metadata +24 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3e78f36b30b9033ba93a1295d6086f94c1da0aab0da2f9d06050c8d64f85a3c
|
4
|
+
data.tar.gz: c841993791e0cb0918852368c88b5cc19bb0ab021e9234f9f56150eb6d837f77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1ede33a7d67bb74f2d2f56704109d7ed5ba00d31b632a4c0c73a11ff95f5198cb9a63f7fa51dbc583b98adbde12e4cdebb86dcaa5d14775fde6ce3a3ee8aeca
|
7
|
+
data.tar.gz: 1cd0e73ee52d825dbc61acbdb45749b8c9fc66086006cbf81b5e2229e7e5736141c852ebb9e7a3ebbed91b22cfcf1857aef629874511cee5d02a36e544b61b46
|
data/README.md
CHANGED
@@ -2,11 +2,9 @@
|
|
2
2
|
|
3
3
|
<a href="https://rubygems.org/gems/strong_csv"><img alt="strong_csv" src="https://img.shields.io/gem/v/strong_csv"></a>
|
4
4
|
|
5
|
-
NOTE: This repository is still under development 🚧🚜🚧
|
6
|
-
|
7
5
|
Type checker for a CSV file inspired by [strong_json](https://github.com/soutaro/strong_json).
|
8
6
|
|
9
|
-
|
7
|
+
## Motivation
|
10
8
|
|
11
9
|
Some applications have a feature to receive a CSV file uploaded by a user,
|
12
10
|
and in general, it needs to validate each cell of the CSV file.
|
@@ -40,7 +38,18 @@ gem install strong_csv
|
|
40
38
|
|
41
39
|
## Usage
|
42
40
|
|
43
|
-
|
41
|
+
The most important APIs of strong_csv are `StrongCSV.new` and `StrongCSV#parse`.
|
42
|
+
`StrongCSV.new` lets you declare types for each CSV column with Ruby's block syntax.
|
43
|
+
Inside the block, you will mainly use `let` and declare types for a column.
|
44
|
+
|
45
|
+
After defining types, you can parse CSV content with `StrongCSV#parse`.
|
46
|
+
`StrongCSV#parse` won't raise errors as possible and just store error messages in its rows.
|
47
|
+
The reason why it won't raise errors is CSV content may contain _invalid_ rows,
|
48
|
+
but sometimes, it makes sense to ignore them and process something for _valid_ rows.
|
49
|
+
If you want to stop all processes with invalid rows,
|
50
|
+
check whether all rows are valid before proceeding with computation.
|
51
|
+
|
52
|
+
Here is an example usage of this gem:
|
44
53
|
|
45
54
|
```ruby
|
46
55
|
require "strong_csv"
|
@@ -51,8 +60,7 @@ strong_csv = StrongCSV.new do
|
|
51
60
|
let :name, string(within: 1..255)
|
52
61
|
let :description, string?(within: 1..1000)
|
53
62
|
let :active, boolean
|
54
|
-
let :started_at, time?
|
55
|
-
let :data, any?
|
63
|
+
let :started_at, time?(format: "%Y-%m-%dT%H:%M:%S")
|
56
64
|
|
57
65
|
# Literal declaration
|
58
66
|
let :status, 0..6
|
@@ -71,7 +79,7 @@ strong_csv = StrongCSV.new do
|
|
71
79
|
# Regular expressions
|
72
80
|
let :url, %r{\Ahttps://}
|
73
81
|
|
74
|
-
# Custom validation
|
82
|
+
# Custom validation
|
75
83
|
#
|
76
84
|
# This example sees the database to fetch exactly stored `User` IDs,
|
77
85
|
# and it checks the `:user_id` cell really exists in the `users` table.
|
@@ -93,32 +101,345 @@ strong_csv.parse(data, field_size_limit: 2048) do |row|
|
|
93
101
|
row[:active] # => true
|
94
102
|
# do something with row
|
95
103
|
else
|
96
|
-
row.errors # => { user_id: ["
|
104
|
+
row.errors # => { user_id: ["`nil` can't be casted to Integer"] }
|
97
105
|
# do something with row.errors
|
98
106
|
end
|
99
107
|
end
|
100
108
|
```
|
101
109
|
|
110
|
+
You can also define types without CSV headers by specifying column numbers.
|
111
|
+
Note the numbers must start from `0` (zero-based index).
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
StrongCSV.new do
|
115
|
+
let 0, integer
|
116
|
+
let 1, string
|
117
|
+
let 2, 1..10
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
This declaration expects a CSV has the contents like this:
|
122
|
+
|
123
|
+
```csv
|
124
|
+
123,abc,3
|
125
|
+
830,mno,10
|
126
|
+
```
|
127
|
+
|
102
128
|
## Available types
|
103
129
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
130
|
+
<table>
|
131
|
+
<tr>
|
132
|
+
<th>Type</th>
|
133
|
+
<th>Description</th>
|
134
|
+
</tr>
|
135
|
+
<tr>
|
136
|
+
<td><a href="#integer-and-integer"><code>integer</code> and <code>integer?</code></a></td>
|
137
|
+
<td>The value must be casted to <code>Integer</code>.</td>
|
138
|
+
</tr>
|
139
|
+
<tr>
|
140
|
+
<td><a href="#float-and-float"><code>float</code> and <code>float?</code></a></td>
|
141
|
+
<td>The value must be casted to <code>Float</code>.</td>
|
142
|
+
</tr>
|
143
|
+
<tr>
|
144
|
+
<td><a href="#boolean-and-boolean"><code>boolean</code> and <code>boolean?</code></a></td>
|
145
|
+
<td>The value must be casted to Boolean (<code>true</code> or <code>false</code>).</td>
|
146
|
+
</tr>
|
147
|
+
<tr>
|
148
|
+
<td><a href="#string-and-string"><code>string</code> and <code>string?</code></a></td>
|
149
|
+
<td>The value must be casted to <code>String</code>.</td>
|
150
|
+
</tr>
|
151
|
+
<tr>
|
152
|
+
<td><a href="#time-and-time"><code>time</code> and <code>time?</code></a></td>
|
153
|
+
<td>The value must be casted to <code>Time</code>.</td>
|
154
|
+
</tr>
|
155
|
+
<tr>
|
156
|
+
<td><a href="#optional"><code>optional</code></a></td>
|
157
|
+
<td>The value can be <code>nil</code>. If the value exists, it must satisfy the given type constraint.</td>
|
158
|
+
</tr>
|
159
|
+
<tr>
|
160
|
+
<td><a href="#literal"><code>23</code> (Integer literal)</a></td>
|
161
|
+
<td>The value must be casted to the specific <code>Integer</code> literal.</td>
|
162
|
+
</tr>
|
163
|
+
<tr>
|
164
|
+
<td><a href="#literal"><code>15.12</code> (Float literal)</a></td>
|
165
|
+
<td>The value must be casted to the specific <code>Float</code> literal.</td>
|
166
|
+
</tr>
|
167
|
+
<tr>
|
168
|
+
<td><a href="#literal"><code>1..10</code> (Range literal)</a></td>
|
169
|
+
<td>The value must be casted to the beginning of <code>Range</code> and be covered with it.</td>
|
170
|
+
</tr>
|
171
|
+
<tr>
|
172
|
+
<td><a href="#literal"><code>"abc"</code> (String literal)</a></td>
|
173
|
+
<td>The value must be casted to the specific <code>String</code> literal.</td>
|
174
|
+
</tr>
|
175
|
+
<tr>
|
176
|
+
<td><a href="#literal"><code>%r{\Ahttps://}</code> (Regexp literal)</a></td>
|
177
|
+
<td>The value must be casted to a <code>String</code> that matches the specified Regexp.</td>
|
178
|
+
</tr>
|
179
|
+
<tr>
|
180
|
+
<td><a href="#union"><code>,</code> (Union type)</a></td>
|
181
|
+
<td>The value must satisfy one of the subtypes.</td>
|
182
|
+
</tr>
|
183
|
+
</table>
|
184
|
+
|
185
|
+
### `integer` and `integer?`
|
186
|
+
|
187
|
+
The value must be casted to Integer. `integer?` allows the value to be `nil`, so you can declare optional integer type
|
188
|
+
for columns. It also lets you allow values that satisfy the specified limitation through `:constraint`.
|
189
|
+
|
190
|
+
_Example_
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
strong_csv = StrongCSV.new do
|
194
|
+
let :stock, integer
|
195
|
+
let :state, integer?
|
196
|
+
let :user_id, integer(constraint: ->(v) { user_ids.include?(v)})
|
197
|
+
pick :user_id, as: :user_ids do |values|
|
198
|
+
User.where(id: values).ids
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
result = strong_csv.parse(<<~CSV)
|
203
|
+
stock,state,user_id
|
204
|
+
12,0,1
|
205
|
+
20,,2
|
206
|
+
non-integer,1,4
|
207
|
+
CSV
|
208
|
+
|
209
|
+
result.map(&:valid?) # => [true, true, false]
|
210
|
+
result[0].slice(:stock, :state, :user_id) # => {:stock=>12, :state=>0, :user_id=>1}
|
211
|
+
result[1].slice(:stock, :state, :user_id) # => {:stock=>20, :state=>nil, :user_id=>2}
|
212
|
+
result[2].slice(:stock, :state, :user_id) # => {:stock=>"non-integer", :state=>1, :user_id=>"4"}
|
213
|
+
result[2].errors.slice(:stock, :user_id) # => {:stock=>["`\"non-integer\"` can't be casted to Integer"], :user_id=>["`\"4\"` does not satisfy the specified constraint"]}
|
214
|
+
```
|
215
|
+
|
216
|
+
### `float` and `float?`
|
217
|
+
|
218
|
+
The value must be casted to Float. `float?` allows the value to be `nil`, so you can declare optional float type for
|
219
|
+
columns. It also lets you allow values that satisfy the specified limitation through `:constraint`.
|
220
|
+
|
221
|
+
_Example_
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
strong_csv = StrongCSV.new do
|
225
|
+
let :tax_rate, float
|
226
|
+
let :fail_rate, float?
|
227
|
+
end
|
228
|
+
|
229
|
+
result = strong_csv.parse(<<~CSV)
|
230
|
+
tax_rate,fail_rate
|
231
|
+
0.02,0.1
|
232
|
+
0.05,
|
233
|
+
,0.8
|
234
|
+
CSV
|
235
|
+
|
236
|
+
result.map(&:valid?) # => [true, true, false]
|
237
|
+
result[0].slice(:tax_rate, :fail_rate) # => {:tax_rate=>0.02, :fail_rate=>0.1}
|
238
|
+
result[1].slice(:tax_rate, :fail_rate) # => {:tax_rate=>0.05, :fail_rate=>nil}
|
239
|
+
result[2].slice(:tax_rate, :fail_rate) # => {:tax_rate=>nil, :fail_rate=>0.8} (`nil` is not allowed for `tax_rate`)
|
240
|
+
```
|
241
|
+
|
242
|
+
### `boolean` and `boolean?`
|
243
|
+
|
244
|
+
The value must be casted to Boolean (`true` of `false`).
|
245
|
+
`"true"`, `"True"`, and `"TRUE"` are casted to `true`,
|
246
|
+
while `"false"`, `"False"`, and `"FALSE"` are casted to `false`.
|
247
|
+
`boolean?` allows the value to be `nil` as an optional boolean
|
248
|
+
value.
|
249
|
+
|
250
|
+
_Example_
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
strong_csv = StrongCSV.new do
|
254
|
+
let :enabled, boolean
|
255
|
+
let :active, boolean?
|
256
|
+
end
|
257
|
+
|
258
|
+
result = strong_csv.parse(<<~CSV)
|
259
|
+
enabled,active
|
260
|
+
True,True
|
261
|
+
False,
|
262
|
+
,
|
263
|
+
CSV
|
264
|
+
|
265
|
+
result.map(&:valid?) # => [true, true, false]
|
266
|
+
result[0].slice(:enabled, :active) # => {:enabled=>true, :active=>true}
|
267
|
+
result[1].slice(:enabled, :active) # => {:enabled=>false, :active=>nil}
|
268
|
+
result[2].slice(:enabled, :active) # => {:enabled=>nil, :active=>nil} (`nil` is not allowed for `enabled`)
|
269
|
+
```
|
270
|
+
|
271
|
+
### `string` and `string?`
|
272
|
+
|
273
|
+
The value must be casted to String. `string?` allows the value to be `nil` as an optional string value.
|
274
|
+
They also support `:within` in its arguments, and it limits the length of the string value within the specified `Range`.
|
275
|
+
|
276
|
+
_Example_
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
strong_csv = StrongCSV.new do
|
280
|
+
let :name, string(within: 1..4)
|
281
|
+
let :description, string?
|
282
|
+
end
|
283
|
+
|
284
|
+
result = strong_csv.parse(<<~CSV)
|
285
|
+
name,description
|
286
|
+
JB,Hello
|
287
|
+
yykamei,
|
288
|
+
,🤷
|
289
|
+
CSV
|
290
|
+
|
291
|
+
result.map(&:valid?) # => [true, false, false]
|
292
|
+
result[0].slice(:name, :description) # => {:name=>"JB", :description=>"Hello"}
|
293
|
+
result[1].slice(:name, :description) # => {:name=>"yykamei", :description=>nil} ("yykamei" exceeds the `Range` specified with `:within`)
|
294
|
+
result[2].slice(:name, :description) # => {:name=>nil, :description=>"🤷"} (`nil` is not allowed for `name`)
|
295
|
+
```
|
296
|
+
|
297
|
+
### `time` and `time?`
|
298
|
+
|
299
|
+
The value must be casted to Time. `time?` allows the value to be `nil` as an optional time value.
|
300
|
+
They have the `:format` argument, which is used as the format
|
301
|
+
of [`Time.strptime`](https://rubydoc.info/stdlib/time/Time.strptime);
|
302
|
+
it means you can ensure the value must satisfy the time format. The default value of `:format` is `"%Y-%m-%d"`.
|
303
|
+
|
304
|
+
_Example_
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
strong_csv = StrongCSV.new do
|
308
|
+
let :start_on, time
|
309
|
+
let :updated_at, time?(format: "%FT%T")
|
310
|
+
end
|
311
|
+
|
312
|
+
result = strong_csv.parse(<<~CSV)
|
313
|
+
start_on,updated_at
|
314
|
+
2022-04-01,2022-04-30T15:30:59
|
315
|
+
2022-05-03
|
316
|
+
05-03,2021-09-03T09:48:23
|
317
|
+
CSV
|
318
|
+
|
319
|
+
result.map(&:valid?) # => [true, true, false]
|
320
|
+
result[0].slice(:start_on, :updated_at) # => {:start_on=>2022-04-01 00:00:00 +0900, :updated_at=>2022-04-30 15:30:59 +0900}
|
321
|
+
result[1].slice(:start_on, :updated_at) # => {:start_on=>2022-05-03 00:00:00 +0900, :updated_at=>nil}
|
322
|
+
result[2].slice(:start_on, :updated_at) # => {:start_on=>"05-03", :updated_at=>2021-09-03 09:48:23 +0900} ("05-03" does not satisfy the default format `"%Y-%m-%d"`)
|
323
|
+
```
|
324
|
+
|
325
|
+
### `optional`
|
326
|
+
|
327
|
+
While each type above has its optional type with `?`, literals cannot be suffixed with `?`.
|
328
|
+
However, there would be a case to have an optional literal type.
|
329
|
+
In this case, `optional` might be useful and lets you declare such types.
|
330
|
+
|
331
|
+
_Example_
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
strong_csv = StrongCSV.new do
|
335
|
+
let :foo, optional(123)
|
336
|
+
let :bar, optional("test")
|
337
|
+
end
|
338
|
+
|
339
|
+
result = strong_csv.parse(<<~CSV)
|
340
|
+
foo,bar
|
341
|
+
123,test
|
342
|
+
,
|
343
|
+
124
|
344
|
+
CSV
|
345
|
+
|
346
|
+
result.map(&:valid?) # => [true, true, false]
|
347
|
+
result[0].slice(:foo, :bar) # => {:foo=>123, :bar=>"test"}
|
348
|
+
result[1].slice(:foo, :bar) # => {:foo=>nil, :bar=>nil}
|
349
|
+
result[2].slice(:foo, :bar) # => {:foo=>"124", :bar=>nil} (124 is not equal to 123)
|
350
|
+
```
|
351
|
+
|
352
|
+
### Literal
|
353
|
+
|
354
|
+
You can declare literal value as types. The supported literals are `Integer`, `Float`, `String`, and `Range`.
|
355
|
+
|
356
|
+
_Example_
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
strong_csv = StrongCSV.new do
|
360
|
+
let 0, 123
|
361
|
+
let 1, "test"
|
362
|
+
let 2, 2.5
|
363
|
+
let 3, 1..10
|
364
|
+
let 4, /[a-z]+/
|
365
|
+
end
|
366
|
+
|
367
|
+
result = strong_csv.parse(<<~CSV)
|
368
|
+
123,test,2.5,9,abc
|
369
|
+
123,test,2.5,0,xyz
|
370
|
+
123,Hey,2.5,10,!
|
371
|
+
CSV
|
372
|
+
|
373
|
+
result.map(&:valid?) # => [true, false, false]
|
374
|
+
result[0].slice(0, 1, 2, 3, 4) # => {0=>123, 1=>"test", 2=>2.5, 3=>9, 4=>"abc"}
|
375
|
+
result[1].slice(0, 1, 2, 3, 4) # => {0=>123, 1=>"test", 2=>2.5, 3=>"0", 4=>"xyz"} (0 is out of 1..10)
|
376
|
+
result[2].slice(0, 1, 2, 3, 4) # => {0=>123, 1=>"Hey", 2=>2.5, 3=>10, 4=>"!"} ("Hey" is not equal to "test", and "!" does not match /[a-z]+/)
|
377
|
+
```
|
378
|
+
|
379
|
+
### Union
|
380
|
+
|
381
|
+
There would be a case that it's alright if a value satisfies one of the types.
|
382
|
+
Union types are useful for such a case.
|
383
|
+
|
384
|
+
_Example_
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
strong_csv = StrongCSV.new do
|
388
|
+
let :priority, 10, 20, 30
|
389
|
+
let :size, "S", "M", "L"
|
390
|
+
end
|
391
|
+
|
392
|
+
result = strong_csv.parse(<<~CSV)
|
393
|
+
priority,size
|
394
|
+
10,M
|
395
|
+
30,A
|
396
|
+
11,S
|
397
|
+
CSV
|
398
|
+
|
399
|
+
result.map(&:valid?) # => [true, false, false]
|
400
|
+
result[0].slice(:priority, :size) # => {:priority=>10, :size=>"M"}
|
401
|
+
result[1].slice(:priority, :size) # => {:priority=>30, :size=>"A"} ("A" is not one of "S", "M", and "L")
|
402
|
+
result[2].slice(:priority, :size) # => {:priority=>"11", :size=>"S"} (11 is not one of 10, 20, and 30)
|
403
|
+
```
|
404
|
+
|
405
|
+
## I18n (Internationalization)
|
406
|
+
|
407
|
+
strong_csv depends on [i18n](https://rubygems.org/gems/i18n) for internationalization.
|
408
|
+
If you want to have a locale-specific error message, put the message catalog in your locale files.
|
409
|
+
Here is an example of a locale file.
|
410
|
+
|
411
|
+
```yaml
|
412
|
+
ja:
|
413
|
+
strong_csv:
|
414
|
+
boolean:
|
415
|
+
cant_be_casted: "`%{value}`はBooleanに変換できません"
|
416
|
+
float:
|
417
|
+
cant_be_casted: "`%{value}`はFloatに変換できません"
|
418
|
+
constraint_error: "`%{value}`は指定された成約を満たしていません",
|
419
|
+
integer:
|
420
|
+
cant_be_casted: "`%{value}`はIntegerに変換できません"
|
421
|
+
constraint_error: "`%{value}`は指定された成約を満たしていません",
|
422
|
+
literal:
|
423
|
+
integer:
|
424
|
+
unexpected: "`%{expected}`ではなく`%{value}`が入力されています"
|
425
|
+
cant_be_casted: "`%{expected}`ではなく`%{value}`が入力されています"
|
426
|
+
float:
|
427
|
+
unexpected: "`%{expected}`ではなく`%{value}`が入力されています"
|
428
|
+
cant_be_casted: "`%{value}`はFloatに変換できません"
|
429
|
+
string:
|
430
|
+
unexpected: "`%{expected}`ではなく`%{value}`が入力されています"
|
431
|
+
range:
|
432
|
+
cant_be_casted: "`%{value}`は`%{expected}`の始端に変換できません"
|
433
|
+
out_of_range: "`%{value}`は`%{range}`の範囲外です"
|
434
|
+
regexp:
|
435
|
+
cant_be_casted: "`%{value}`はStringに変換できません"
|
436
|
+
unexpected: "`%{value}`は`%{expected}`とマッチしませんでした"
|
437
|
+
string:
|
438
|
+
cant_be_casted: "`%{value}`はStringに変換できません"
|
439
|
+
out_of_range: "`%{value}`の文字数は`%{range}`の範囲外です"
|
440
|
+
time:
|
441
|
+
cant_be_casted: "`%{value}`は`%{time_format}`でTimeに変換できません"
|
442
|
+
```
|
122
443
|
|
123
444
|
## Contributing
|
124
445
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
I18n.backend.store_translations(
|
4
|
+
:en, {
|
5
|
+
_strong_csv: {
|
6
|
+
boolean: {
|
7
|
+
cant_be_casted: "`%{value}` can't be casted to Boolean",
|
8
|
+
},
|
9
|
+
float: {
|
10
|
+
cant_be_casted: "`%{value}` can't be casted to Float",
|
11
|
+
constraint_error: "`%{value}` does not satisfy the specified constraint",
|
12
|
+
},
|
13
|
+
integer: {
|
14
|
+
cant_be_casted: "`%{value}` can't be casted to Integer",
|
15
|
+
constraint_error: "`%{value}` does not satisfy the specified constraint",
|
16
|
+
},
|
17
|
+
literal: {
|
18
|
+
integer: {
|
19
|
+
unexpected: "`%{expected}` is expected, but `%{value}` was given",
|
20
|
+
cant_be_casted: "`%{value}` can't be casted to Integer",
|
21
|
+
},
|
22
|
+
float: {
|
23
|
+
unexpected: "`%{expected}` is expected, but `%{value}` was given",
|
24
|
+
cant_be_casted: "`%{value}` can't be casted to Float",
|
25
|
+
},
|
26
|
+
string: {
|
27
|
+
unexpected: "`%{expected}` is expected, but `%{value}` was given",
|
28
|
+
},
|
29
|
+
range: {
|
30
|
+
cant_be_casted: "`%{value}` can't be casted to the beginning of `%{expected}`",
|
31
|
+
out_of_range: "`%{value}` is not within `%{range}`",
|
32
|
+
},
|
33
|
+
regexp: {
|
34
|
+
cant_be_casted: "`%{value}` can't be casted to String",
|
35
|
+
unexpected: "`%{value}` did not match `%{expected}`",
|
36
|
+
},
|
37
|
+
},
|
38
|
+
string: {
|
39
|
+
cant_be_casted: "`%{value}` can't be casted to String",
|
40
|
+
out_of_range: "The length of `%{value}` is out of range `%{range}`",
|
41
|
+
},
|
42
|
+
time: {
|
43
|
+
cant_be_casted: "`%{value}` can't be casted to Time with the format `%{time_format}`",
|
44
|
+
},
|
45
|
+
},
|
46
|
+
},
|
47
|
+
)
|
data/lib/strong_csv/let.rb
CHANGED
@@ -9,9 +9,14 @@ class StrongCSV
|
|
9
9
|
# @return [Boolean]
|
10
10
|
attr_reader :headers
|
11
11
|
|
12
|
+
# @return [Hash]
|
13
|
+
attr_reader :pickers
|
14
|
+
|
12
15
|
def initialize
|
13
16
|
@types = {}
|
14
17
|
@headers = false
|
18
|
+
@pickers = {}
|
19
|
+
@picked = {}
|
15
20
|
end
|
16
21
|
|
17
22
|
# @param name [String, Symbol, Integer]
|
@@ -30,12 +35,35 @@ class StrongCSV
|
|
30
35
|
validate_columns
|
31
36
|
end
|
32
37
|
|
33
|
-
|
34
|
-
|
38
|
+
# #pick is intended for defining a singleton method with `:as`.
|
39
|
+
# This might be useful for the case where you want to receive IDs that are stored in a database,
|
40
|
+
# and you want to make sure the IDs actually exist.
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# pick :user_id, as: :user_ids do |user_ids|
|
44
|
+
# User.where(id: user_ids).ids
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @param column [Symbol, Integer]
|
48
|
+
# @param as [Symbol]
|
49
|
+
# @yieldparam values [Array<String>] The values for the column. NOTE: This is an array of String, not casted values.
|
50
|
+
def pick(column, as:, &block)
|
51
|
+
define_singleton_method(as) do
|
52
|
+
@picked[as]
|
53
|
+
end
|
54
|
+
@pickers[as] = lambda do |csv|
|
55
|
+
@picked[as] = block.call(csv.map { |row| row[column] })
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param options [Hash] See `Types::Integer#initialize` for more details.
|
60
|
+
def integer(**options)
|
61
|
+
Types::Integer.new(**options)
|
35
62
|
end
|
36
63
|
|
37
|
-
|
38
|
-
|
64
|
+
# @param options [Hash] See `Types::Integer#initialize` for more details.
|
65
|
+
def integer?(**options)
|
66
|
+
optional(integer(**options))
|
39
67
|
end
|
40
68
|
|
41
69
|
def boolean
|
@@ -46,12 +74,14 @@ class StrongCSV
|
|
46
74
|
optional(boolean)
|
47
75
|
end
|
48
76
|
|
49
|
-
|
50
|
-
|
77
|
+
# @param options [Hash] See `Types::Float#initialize` for more details.
|
78
|
+
def float(**options)
|
79
|
+
Types::Float.new(**options)
|
51
80
|
end
|
52
81
|
|
53
|
-
|
54
|
-
|
82
|
+
# @param options [Hash] See `Types::Float#initialize` for more details.
|
83
|
+
def float?(**options)
|
84
|
+
optional(float(**options))
|
55
85
|
end
|
56
86
|
|
57
87
|
# @param options [Hash] See `Types::String#initialize` for more details.
|
data/lib/strong_csv/row.rb
CHANGED
@@ -17,7 +17,7 @@ class StrongCSV
|
|
17
17
|
boolean = FALSE_VALUES.include?(value) ? false : nil
|
18
18
|
return ValueResult.new(value: boolean, original_value: value) unless boolean.nil?
|
19
19
|
|
20
|
-
ValueResult.new(original_value: value, error_messages: ["
|
20
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.boolean.cant_be_casted", value: value.inspect, default: :"_strong_csv.boolean.cant_be_casted")])
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -4,13 +4,27 @@ class StrongCSV
|
|
4
4
|
module Types
|
5
5
|
# Float type
|
6
6
|
class Float < Base
|
7
|
+
DEFAULT_CONSTRAINT = ->(_) { true }
|
8
|
+
private_constant :DEFAULT_CONSTRAINT
|
9
|
+
|
10
|
+
# @param constraint [Proc]
|
11
|
+
def initialize(constraint: DEFAULT_CONSTRAINT)
|
12
|
+
super()
|
13
|
+
@constraint = constraint
|
14
|
+
end
|
15
|
+
|
7
16
|
# @todo Use :exception for Float after we drop the support of Ruby 2.5
|
8
17
|
# @param value [Object] Value to be casted to Float
|
9
18
|
# @return [ValueResult]
|
10
19
|
def cast(value)
|
11
|
-
|
20
|
+
float = Float(value)
|
21
|
+
if @constraint.call(float)
|
22
|
+
ValueResult.new(value: float, original_value: value)
|
23
|
+
else
|
24
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.float.constraint_error", value: value.inspect, default: :"_strong_csv.float.constraint_error")])
|
25
|
+
end
|
12
26
|
rescue ArgumentError, TypeError
|
13
|
-
ValueResult.new(original_value: value, error_messages: ["
|
27
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.float.cant_be_casted", value: value.inspect, default: :"_strong_csv.float.cant_be_casted")])
|
14
28
|
end
|
15
29
|
end
|
16
30
|
end
|
@@ -4,13 +4,27 @@ class StrongCSV
|
|
4
4
|
module Types
|
5
5
|
# Integer type
|
6
6
|
class Integer < Base
|
7
|
+
DEFAULT_CONSTRAINT = ->(_) { true }
|
8
|
+
private_constant :DEFAULT_CONSTRAINT
|
9
|
+
|
10
|
+
# @param constraint [Proc]
|
11
|
+
def initialize(constraint: DEFAULT_CONSTRAINT)
|
12
|
+
super()
|
13
|
+
@constraint = constraint
|
14
|
+
end
|
15
|
+
|
7
16
|
# @todo Use :exception for Integer after we drop the support of Ruby 2.5
|
8
17
|
# @param value [Object] Value to be casted to Integer
|
9
18
|
# @return [ValueResult]
|
10
19
|
def cast(value)
|
11
|
-
|
20
|
+
int = Integer(value)
|
21
|
+
if @constraint.call(int)
|
22
|
+
ValueResult.new(value: int, original_value: value)
|
23
|
+
else
|
24
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.integer.constraint_error", value: value.inspect, default: :"_strong_csv.integer.constraint_error")])
|
25
|
+
end
|
12
26
|
rescue ArgumentError, TypeError
|
13
|
-
ValueResult.new(original_value: value, error_messages: ["
|
27
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.integer.cant_be_casted", value: value.inspect, default: :"_strong_csv.integer.cant_be_casted")])
|
14
28
|
end
|
15
29
|
end
|
16
30
|
end
|
@@ -13,10 +13,10 @@ class StrongCSV
|
|
13
13
|
if int == self
|
14
14
|
ValueResult.new(value: int, original_value: value)
|
15
15
|
else
|
16
|
-
ValueResult.new(original_value: value, error_messages: ["
|
16
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.integer.unexpected", value: int.inspect, expected: inspect, default: :"_strong_csv.literal.integer.unexpected")])
|
17
17
|
end
|
18
18
|
rescue ArgumentError, TypeError
|
19
|
-
ValueResult.new(original_value: value, error_messages: ["
|
19
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.integer.cant_be_casted", value: value.inspect, default: :"_strong_csv.literal.integer.cant_be_casted")])
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
@@ -28,10 +28,10 @@ class StrongCSV
|
|
28
28
|
if float == self
|
29
29
|
ValueResult.new(value: float, original_value: value)
|
30
30
|
else
|
31
|
-
ValueResult.new(original_value: value, error_messages: ["
|
31
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.float.unexpected", value: float.inspect, expected: inspect, default: :"_strong_csv.literal.float.unexpected")])
|
32
32
|
end
|
33
33
|
rescue ArgumentError, TypeError
|
34
|
-
ValueResult.new(original_value: value, error_messages: ["
|
34
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.float.cant_be_casted", value: value.inspect, default: :"_strong_csv.literal.float.cant_be_casted")])
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
@@ -39,7 +39,7 @@ class StrongCSV
|
|
39
39
|
# @param value [Object] Value to be casted to Range
|
40
40
|
# @return [ValueResult]
|
41
41
|
def cast(value)
|
42
|
-
return ValueResult.new(original_value: value, error_messages: ["
|
42
|
+
return ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.range.cant_be_casted", value: value.inspect, expected: inspect, default: :"_strong_csv.literal.range.cant_be_casted")]) if value.nil?
|
43
43
|
|
44
44
|
casted = case self.begin
|
45
45
|
when ::Float
|
@@ -54,10 +54,10 @@ class StrongCSV
|
|
54
54
|
if cover?(casted)
|
55
55
|
ValueResult.new(value: casted, original_value: value)
|
56
56
|
else
|
57
|
-
ValueResult.new(original_value: value, error_messages: ["
|
57
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.range.out_of_range", value: casted.inspect, range: inspect, default: :"_strong_csv.literal.range.out_of_range")])
|
58
58
|
end
|
59
59
|
rescue ArgumentError
|
60
|
-
ValueResult.new(original_value: value, error_messages: ["
|
60
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.range.cant_be_casted", value: value.inspect, expected: inspect, default: :"_strong_csv.literal.range.cant_be_casted")])
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
@@ -68,7 +68,21 @@ class StrongCSV
|
|
68
68
|
if self == value
|
69
69
|
ValueResult.new(value: self, original_value: value)
|
70
70
|
else
|
71
|
-
ValueResult.new(original_value: value, error_messages: ["
|
71
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.string.unexpected", value: value.inspect, expected: inspect, default: :"_strong_csv.literal.string.unexpected")])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
refine ::Regexp do
|
77
|
+
# @param value [Object] Value to be casted to String
|
78
|
+
# @return [ValueResult]
|
79
|
+
def cast(value)
|
80
|
+
return ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.regexp.cant_be_casted", value: value.inspect, default: :"_strong_csv.literal.regexp.cant_be_casted")]) if value.nil?
|
81
|
+
|
82
|
+
if self =~ value
|
83
|
+
ValueResult.new(value: value, original_value: value)
|
84
|
+
else
|
85
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.literal.regexp.unexpected", value: value.inspect, expected: inspect, default: :"_strong_csv.literal.regexp.unexpected")])
|
72
86
|
end
|
73
87
|
end
|
74
88
|
end
|
@@ -15,11 +15,11 @@ class StrongCSV
|
|
15
15
|
# @param value [Object] Value to be casted to Boolean
|
16
16
|
# @return [ValueResult]
|
17
17
|
def cast(value)
|
18
|
-
return ValueResult.new(original_value: value, error_messages: ["
|
18
|
+
return ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.string.cant_be_casted", value: value.inspect, default: :"_strong_csv.string.cant_be_casted")]) if value.nil?
|
19
19
|
|
20
20
|
casted = String(value)
|
21
21
|
if @within && !@within.cover?(casted.size)
|
22
|
-
ValueResult.new(original_value: value, error_messages: ["
|
22
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.string.out_of_range", value: value.inspect, range: @within.inspect, default: :"_strong_csv.string.out_of_range")])
|
23
23
|
else
|
24
24
|
ValueResult.new(value: casted, original_value: value)
|
25
25
|
end
|
@@ -15,11 +15,11 @@ class StrongCSV
|
|
15
15
|
# @param value [Object] Value to be casted to Time
|
16
16
|
# @return [ValueResult]
|
17
17
|
def cast(value)
|
18
|
-
return ValueResult.new(original_value: value, error_messages: ["
|
18
|
+
return ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.time.cant_be_casted", value: value.inspect, time_format: @format.inspect, default: :"_strong_csv.time.cant_be_casted")]) if value.nil?
|
19
19
|
|
20
20
|
ValueResult.new(value: ::Time.strptime(value.to_s, @format), original_value: value)
|
21
21
|
rescue ArgumentError
|
22
|
-
ValueResult.new(original_value: value, error_messages: ["
|
22
|
+
ValueResult.new(original_value: value, error_messages: [I18n.t("strong_csv.time.cant_be_casted", value: value.inspect, time_format: @format.inspect, default: :"_strong_csv.time.cant_be_casted")])
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
data/lib/strong_csv/version.rb
CHANGED
data/lib/strong_csv.rb
CHANGED
@@ -4,8 +4,10 @@ require "csv"
|
|
4
4
|
require "forwardable"
|
5
5
|
require "set"
|
6
6
|
require "time"
|
7
|
+
require "i18n"
|
7
8
|
|
8
9
|
require_relative "strong_csv/version"
|
10
|
+
require_relative "strong_csv/i18n"
|
9
11
|
require_relative "strong_csv/value_result"
|
10
12
|
require_relative "strong_csv/types/base"
|
11
13
|
require_relative "strong_csv/types/boolean"
|
@@ -35,6 +37,12 @@ class StrongCSV
|
|
35
37
|
options.delete(:nil_value)
|
36
38
|
options = options.merge(headers: @let.headers, header_converters: :symbol)
|
37
39
|
csv = CSV.new(csv, **options)
|
40
|
+
|
41
|
+
@let.pickers.each_value do |picker|
|
42
|
+
picker.call(csv)
|
43
|
+
csv.rewind
|
44
|
+
end
|
45
|
+
|
38
46
|
if block_given?
|
39
47
|
csv.each do |row|
|
40
48
|
yield Row.new(row: row, types: @let.types, lineno: csv.lineno)
|
metadata
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strong_csv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yutaka Kamei
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
12
|
-
dependencies:
|
11
|
+
date: 2022-05-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: i18n
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.8.11
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '2'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.8.11
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2'
|
13
33
|
description: strong_csv is a type checker for a CSV file. It lets developers declare
|
14
34
|
types for each column to ensure all cells are satisfied with desired types.
|
15
35
|
email:
|
@@ -21,6 +41,7 @@ files:
|
|
21
41
|
- LICENSE
|
22
42
|
- README.md
|
23
43
|
- lib/strong_csv.rb
|
44
|
+
- lib/strong_csv/i18n.rb
|
24
45
|
- lib/strong_csv/let.rb
|
25
46
|
- lib/strong_csv/row.rb
|
26
47
|
- lib/strong_csv/types/base.rb
|