strong_csv 0.2.0 → 0.5.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
  SHA256:
3
- metadata.gz: 5c718425815c3296bf644f0206eb7f351a25565908eddf5ec9b5811c045a46ec
4
- data.tar.gz: fb8912e844d913b06c026e46d0b78084f7f0b3e81738ef3cc48476d8e784d03d
3
+ metadata.gz: b3e78f36b30b9033ba93a1295d6086f94c1da0aab0da2f9d06050c8d64f85a3c
4
+ data.tar.gz: c841993791e0cb0918852368c88b5cc19bb0ab021e9234f9f56150eb6d837f77
5
5
  SHA512:
6
- metadata.gz: cc0564f9036a150e8564ff3bd947d9e4d7197b25179d37ab1c0d9997c7517834c3c76d54b5e28a6c1c5631e5c64f2601989935052b21dd6d588fa7e521ec7c8d
7
- data.tar.gz: 205d2e29e1512e1508328f724b63ec40cf1d138c8d3bf83e586f6f0d8d7730f2d8f5fae648a589650efaff1e6104c9cb56ae0b8771fb7098a34d34aa08c525ad
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
- **Motivation**
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
- TBD: This hasn't yet been implemented.
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: ["must be present", "must be an integer"] }
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
- | Type | Arguments | Description | Example |
105
- | ------------------------ | --------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
106
- | `integer` | | The value must be casted to Integer | `let :stock, integer` |
107
- | `integer?` | | The value can be `nil`. If the value exists, it must satisfy `integer` constraint. | `let :stock, integer?` |
108
- | `float` | | The value must be casted to Float | `let :rate, float` |
109
- | `float?` | | The value can be `nil`. If the value exists, it must satisfy `float` constraint. | `let :rate, float?` |
110
- | `boolean` | | The value must be casted to Boolean | `let :active, boolean` |
111
- | `boolean?` | | The value can be `nil`. If the value exists, it must satisfy `boolean` constraint. | `let :active, boolean?` |
112
- | `string` | `:within` | The value must be casted to String | `let :name, string(within: 1..255)` |
113
- | `string?` | `:within` | The value can be `nil`. If the value exists, it must satisfy `string` constraint. | `let :name, string?(within: 1..255)` |
114
- | `time` | `:format` | The value must be casted to Time | `let :started_at, time(format: "%Y-%m-%dT%%H:%M:%S")` |
115
- | `time?` | `:format` | The value can be `nil`. If the value exists, it must satisfy `time` constraint. | `let :started_at, time?(format: "%Y-%m-%dT%%H:%M:%S")` |
116
- | `optional` | `type` | The value can be `nil`. If the value exists, it must satisfy the given type constraint. | `let :foo, optional(123)` |
117
- | `23` (Integer literal) | | The value must be casted to the specific Integer literal | `let :id, 3` |
118
- | `15.12` (Float literal) | | The value must be casted to the specific Float literal | `let :id, 3.8` |
119
- | `1..10` (Range literal) | | The value must be casted to the beginning of Range and be covered with it | `let :id, 10..30`, `let :id, 1.0..30`, `let :id, "a".."z"` |
120
- | `"abc"` (String literal) | | The value must be casted to the specific String literal | `let :drink, "coffee"` |
121
- | , (Union type) | | The value must satisfy one of the subtypes | `let :id, 1, 2, 3` |
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
+ )
@@ -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
- def integer
34
- Types::Integer.new
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
- def integer?
38
- optional(integer)
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
- def float
50
- Types::Float.new
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
- def float?
54
- optional(float)
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.
@@ -6,7 +6,7 @@ class StrongCSV
6
6
  extend Forwardable
7
7
  using Types::Literal
8
8
 
9
- def_delegators :@values, :[], :fetch
9
+ def_delegators :@values, :[], :fetch, :slice
10
10
 
11
11
  # @return [Hash]
12
12
  attr_reader :errors
@@ -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: ["`#{value.inspect}` can't be casted to Boolean"])
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
- ValueResult.new(value: Float(value), original_value: value)
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: ["`#{value.inspect}` can't be casted to Float"])
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
- ValueResult.new(value: Integer(value), original_value: value)
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: ["`#{value.inspect}` can't be casted to Integer"])
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: ["`#{inspect}` is expected, but `#{int}` was given"])
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: ["`#{value.inspect}` can't be casted to #{self.class.name}"])
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: ["`#{inspect}` is expected, but `#{float}` was given"])
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: ["`#{value.inspect}` can't be casted to #{self.class.name}"])
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: ["`nil` can't be casted to the beginning of `#{inspect}`"]) if value.nil?
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: ["`#{casted.inspect}` is not within `#{inspect}`"])
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: ["`#{value.inspect}` can't be casted to the beginning of `#{inspect}`"])
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: ["`#{inspect}` is expected, but `#{value.inspect}` was given"])
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: ["`#{value.inspect}` can't be casted to String"]) if value.nil?
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: ["`#{casted.inspect}` is out of range `#{@within.inspect}`"])
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: ["`#{value.inspect}` can't be casted to Time"]) if value.nil?
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: ["`#{value.inspect}` can't be casted to Time with the format `#{@format}`"])
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class StrongCSV
4
- VERSION = "0.2.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.2.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-04-29 00:00:00.000000000 Z
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