strong_csv 0.4.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +33 -51
- data/lib/strong_csv/let.rb +40 -8
- data/lib/strong_csv/row.rb +3 -0
- 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 +10 -10
- data/lib/strong_csv/types/string.rb +2 -2
- data/lib/strong_csv/types/time.rb +2 -2
- data/lib/strong_csv/value_result.rb +4 -2
- data/lib/strong_csv/version.rb +1 -1
- data/lib/strong_csv.rb +7 -3
- data/sig/strong_csv.rbs +163 -0
- metadata +4 -24
- data/lib/strong_csv/i18n.rb +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c242c3b580ec02d431ae78bb7b8f370ef21248dae5fb89ea7ee158b0c9c04c6
|
4
|
+
data.tar.gz: 2d880a823b44c1da78eeb6b79196c56ff46e937efe86ac17e8a522ea8b97f4a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81bcf7718f3b792f6ecc394cccc5eb610b32ecb89b9dd492d3f39ab5dadebb633b1c9f4bd804de355902632fd1790fb3bb7b5789a7bb79a6eff6ee1860078491
|
7
|
+
data.tar.gz: 39d417143c5262d1ac781bdd5414d5021e5018b5791a0d6ed6df7c0954b008154c8bc6542ce5cdc184b00b3c4c40a5bcfc4366872610a123feb3af93c991901d
|
data/README.md
CHANGED
@@ -2,8 +2,6 @@
|
|
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
|
@@ -81,8 +79,6 @@ strong_csv = StrongCSV.new do
|
|
81
79
|
# Regular expressions
|
82
80
|
let :url, %r{\Ahttps://}
|
83
81
|
|
84
|
-
# TODO: The followings are not implemented so far.
|
85
|
-
|
86
82
|
# Custom validation
|
87
83
|
#
|
88
84
|
# This example sees the database to fetch exactly stored `User` IDs,
|
@@ -105,12 +101,30 @@ strong_csv.parse(data, field_size_limit: 2048) do |row|
|
|
105
101
|
row[:active] # => true
|
106
102
|
# do something with row
|
107
103
|
else
|
108
|
-
row.errors # => { user_id: ["
|
104
|
+
row.errors # => { user_id: ["`nil` can't be casted to Integer"] }
|
109
105
|
# do something with row.errors
|
110
106
|
end
|
111
107
|
end
|
112
108
|
```
|
113
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
|
+
|
114
128
|
## Available types
|
115
129
|
|
116
130
|
<table>
|
@@ -171,7 +185,7 @@ end
|
|
171
185
|
### `integer` and `integer?`
|
172
186
|
|
173
187
|
The value must be casted to Integer. `integer?` allows the value to be `nil`, so you can declare optional integer type
|
174
|
-
for columns.
|
188
|
+
for columns. It also lets you allow values that satisfy the specified limitation through `:constraint`.
|
175
189
|
|
176
190
|
_Example_
|
177
191
|
|
@@ -179,25 +193,30 @@ _Example_
|
|
179
193
|
strong_csv = StrongCSV.new do
|
180
194
|
let :stock, integer
|
181
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
|
182
200
|
end
|
183
201
|
|
184
202
|
result = strong_csv.parse(<<~CSV)
|
185
|
-
stock,state
|
186
|
-
12,0
|
187
|
-
20
|
188
|
-
non-integer,1
|
203
|
+
stock,state,user_id
|
204
|
+
12,0,1
|
205
|
+
20,,2
|
206
|
+
non-integer,1,4
|
189
207
|
CSV
|
190
208
|
|
191
209
|
result.map(&:valid?) # => [true, true, false]
|
192
|
-
result[0].slice(:stock, :state) # => {:stock=>12, :state=>0}
|
193
|
-
result[1].slice(:stock, :state) # => {:stock=>20, :state=>nil}
|
194
|
-
result[2].slice(:stock, :state) # => {:stock=>"non-integer", :state=>1
|
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"]}
|
195
214
|
```
|
196
215
|
|
197
216
|
### `float` and `float?`
|
198
217
|
|
199
218
|
The value must be casted to Float. `float?` allows the value to be `nil`, so you can declare optional float type for
|
200
|
-
columns.
|
219
|
+
columns. It also lets you allow values that satisfy the specified limitation through `:constraint`.
|
201
220
|
|
202
221
|
_Example_
|
203
222
|
|
@@ -383,43 +402,6 @@ result[1].slice(:priority, :size) # => {:priority=>30, :size=>"A"} ("A" is not o
|
|
383
402
|
result[2].slice(:priority, :size) # => {:priority=>"11", :size=>"S"} (11 is not one of 10, 20, and 30)
|
384
403
|
```
|
385
404
|
|
386
|
-
## I18n (Internationalization)
|
387
|
-
|
388
|
-
strong_csv depends on [i18n](https://rubygems.org/gems/i18n) for internationalization.
|
389
|
-
If you want to have a locale-specific error message, put the message catalog in your locale files.
|
390
|
-
Here is an example of a locale file.
|
391
|
-
|
392
|
-
```yaml
|
393
|
-
ja:
|
394
|
-
strong_csv:
|
395
|
-
boolean:
|
396
|
-
cant_be_casted: "`%{value}`はBooleanに変換できません"
|
397
|
-
float:
|
398
|
-
cant_be_casted: "`%{value}`はFloatに変換できません"
|
399
|
-
integer:
|
400
|
-
cant_be_casted: "`%{value}`はIntegerに変換できません"
|
401
|
-
literal:
|
402
|
-
integer:
|
403
|
-
unexpected: "`%{expected}`ではなく`%{value}`が入力されています"
|
404
|
-
cant_be_casted: "`%{expected}`ではなく`%{value}`が入力されています"
|
405
|
-
float:
|
406
|
-
unexpected: "`%{expected}`ではなく`%{value}`が入力されています"
|
407
|
-
cant_be_casted: "`%{value}`はFloatに変換できません"
|
408
|
-
string:
|
409
|
-
unexpected: "`%{expected}`ではなく`%{value}`が入力されています"
|
410
|
-
range:
|
411
|
-
cant_be_casted: "`%{value}`は`%{expected}`の始端に変換できません"
|
412
|
-
out_of_range: "`%{value}`は`%{range}`の範囲外です"
|
413
|
-
regexp:
|
414
|
-
cant_be_casted: "`%{value}`はStringに変換できません"
|
415
|
-
unexpected: "`%{value}`は`%{expected}`とマッチしませんでした"
|
416
|
-
string:
|
417
|
-
cant_be_casted: "`%{value}`はStringに変換できません"
|
418
|
-
out_of_range: "`%{value}`の文字数は`%{range}`の範囲外です"
|
419
|
-
time:
|
420
|
-
cant_be_casted: "`%{value}`は`%{time_format}`でTimeに変換できません"
|
421
|
-
```
|
422
|
-
|
423
405
|
## Contributing
|
424
406
|
|
425
407
|
Bug reports and pull requests are welcome on the [GitHub repository](https://github.com/yykamei/strong_csv).
|
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
|
35
57
|
end
|
36
58
|
|
37
|
-
|
38
|
-
|
59
|
+
# @param options [Hash] See `Types::Integer#initialize` for more details.
|
60
|
+
def integer(**options)
|
61
|
+
Types::Integer.new(**options)
|
62
|
+
end
|
63
|
+
|
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.
|
@@ -59,6 +89,7 @@ class StrongCSV
|
|
59
89
|
Types::String.new(**options)
|
60
90
|
end
|
61
91
|
|
92
|
+
# @param options [Hash] See `Types::String#initialize` for more details.
|
62
93
|
def string?(**options)
|
63
94
|
optional(string(**options))
|
64
95
|
end
|
@@ -68,6 +99,7 @@ class StrongCSV
|
|
68
99
|
Types::Time.new(**options)
|
69
100
|
end
|
70
101
|
|
102
|
+
# @param options [Hash] See `Types::Time#initialize` for more details.
|
71
103
|
def time?(**options)
|
72
104
|
optional(time(**options))
|
73
105
|
end
|
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: ["`#{value.inspect}` can't be casted to Boolean"])
|
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: ["`#{value.inspect}` does not satisfy the specified constraint"])
|
25
|
+
end
|
12
26
|
rescue ArgumentError, TypeError
|
13
|
-
ValueResult.new(original_value: value, error_messages: [
|
27
|
+
ValueResult.new(original_value: value, error_messages: ["`#{value.inspect}` can't be casted to Float"])
|
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: ["`#{value.inspect}` does not satisfy the specified constraint"])
|
25
|
+
end
|
12
26
|
rescue ArgumentError, TypeError
|
13
|
-
ValueResult.new(original_value: value, error_messages: [
|
27
|
+
ValueResult.new(original_value: value, error_messages: ["`#{value.inspect}` can't be casted to Integer"])
|
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: ["`#{inspect}` is expected, but `#{int.inspect}` was given"])
|
17
17
|
end
|
18
18
|
rescue ArgumentError, TypeError
|
19
|
-
ValueResult.new(original_value: value, error_messages: [
|
19
|
+
ValueResult.new(original_value: value, error_messages: ["`#{value.inspect}` can't be casted to Integer"])
|
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: ["`#{inspect}` is expected, but `#{float.inspect}` was given"])
|
32
32
|
end
|
33
33
|
rescue ArgumentError, TypeError
|
34
|
-
ValueResult.new(original_value: value, error_messages: [
|
34
|
+
ValueResult.new(original_value: value, error_messages: ["`#{value.inspect}` can't be casted to Float"])
|
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: ["`#{value.inspect}` can't be casted to the beginning of `#{inspect}`"]) 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: ["`#{casted.inspect}` is not within `#{inspect}`"])
|
58
58
|
end
|
59
59
|
rescue ArgumentError
|
60
|
-
ValueResult.new(original_value: value, error_messages: [
|
60
|
+
ValueResult.new(original_value: value, error_messages: ["`#{value.inspect}` can't be casted to the beginning of `#{inspect}`"])
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
@@ -68,7 +68,7 @@ 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: ["`#{inspect}` is expected, but `#{value.inspect}` was given"])
|
72
72
|
end
|
73
73
|
end
|
74
74
|
end
|
@@ -77,12 +77,12 @@ class StrongCSV
|
|
77
77
|
# @param value [Object] Value to be casted to String
|
78
78
|
# @return [ValueResult]
|
79
79
|
def cast(value)
|
80
|
-
return ValueResult.new(original_value: value, error_messages: [
|
80
|
+
return ValueResult.new(original_value: value, error_messages: ["`#{value.inspect}` can't be casted to String"]) if value.nil?
|
81
81
|
|
82
82
|
if self =~ value
|
83
83
|
ValueResult.new(value: value, original_value: value)
|
84
84
|
else
|
85
|
-
ValueResult.new(original_value: value, error_messages: [
|
85
|
+
ValueResult.new(original_value: value, error_messages: ["`#{value.inspect}` did not match `#{inspect}`"])
|
86
86
|
end
|
87
87
|
end
|
88
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: ["`#{value.inspect}` can't be casted to String"]) 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: ["The length of `#{value.inspect}` is out of range `#{@within.inspect}`"])
|
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: ["`#{value.inspect}` can't be casted to Time with the format `#{@format.inspect}`"]) 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: ["`#{value.inspect}` can't be casted to Time with the format `#{@format.inspect}`"])
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class StrongCSV
|
4
|
-
# ValueResult represents a CSV field is valid or not and contains its casted value if it's valid.
|
4
|
+
# ValueResult represents whether a CSV field is valid or not, and contains its casted value if it's valid.
|
5
5
|
class ValueResult
|
6
6
|
DEFAULT_VALUE = Object.new
|
7
7
|
private_constant :DEFAULT_VALUE
|
@@ -15,11 +15,13 @@ class StrongCSV
|
|
15
15
|
@error_messages = error_messages
|
16
16
|
end
|
17
17
|
|
18
|
-
# @return [Object] The casted value if it's valid. Otherwise, returns the original value.
|
18
|
+
# @return [::Float, ::Integer, ::Object, ::String, ::Range, Boolean, ::Time, nil] The casted value if it's valid. Otherwise, returns the original value.
|
19
19
|
def value
|
20
20
|
success? ? @value : @original_value
|
21
21
|
end
|
22
22
|
|
23
|
+
# It returns true if the value is successfully casted.
|
24
|
+
#
|
23
25
|
# @return [Boolean]
|
24
26
|
def success?
|
25
27
|
@error_messages.nil?
|
data/lib/strong_csv/version.rb
CHANGED
data/lib/strong_csv.rb
CHANGED
@@ -4,10 +4,8 @@ require "csv"
|
|
4
4
|
require "forwardable"
|
5
5
|
require "set"
|
6
6
|
require "time"
|
7
|
-
require "i18n"
|
8
7
|
|
9
8
|
require_relative "strong_csv/version"
|
10
|
-
require_relative "strong_csv/i18n"
|
11
9
|
require_relative "strong_csv/value_result"
|
12
10
|
require_relative "strong_csv/types/base"
|
13
11
|
require_relative "strong_csv/types/boolean"
|
@@ -21,7 +19,7 @@ require_relative "strong_csv/types/union"
|
|
21
19
|
require_relative "strong_csv/let"
|
22
20
|
require_relative "strong_csv/row"
|
23
21
|
|
24
|
-
#
|
22
|
+
# StrongCSV is a library for parsing CSV contents with type checking.
|
25
23
|
class StrongCSV
|
26
24
|
class Error < StandardError; end
|
27
25
|
|
@@ -37,6 +35,12 @@ class StrongCSV
|
|
37
35
|
options.delete(:nil_value)
|
38
36
|
options = options.merge(headers: @let.headers, header_converters: :symbol)
|
39
37
|
csv = CSV.new(csv, **options)
|
38
|
+
|
39
|
+
@let.pickers.each_value do |picker|
|
40
|
+
picker.call(csv)
|
41
|
+
csv.rewind
|
42
|
+
end
|
43
|
+
|
40
44
|
if block_given?
|
41
45
|
csv.each do |row|
|
42
46
|
yield Row.new(row: row, types: @let.types, lineno: csv.lineno)
|
data/sig/strong_csv.rbs
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# TypeProf 0.21.2
|
2
|
+
|
3
|
+
type literals = ::Integer | ::Float | ::Range[untyped] | ::String | ::Regexp
|
4
|
+
type declarable = StrongCSV::Types::Base | literals
|
5
|
+
type casted = bool | ::Float | ::Integer | ::Range[untyped] | ::String | ::Regexp | ::Time
|
6
|
+
type column = ::Integer | ::Symbol
|
7
|
+
|
8
|
+
# Classes
|
9
|
+
class StrongCSV
|
10
|
+
VERSION: String
|
11
|
+
|
12
|
+
@let: Let
|
13
|
+
|
14
|
+
def initialize: -> void
|
15
|
+
|
16
|
+
def parse: (String | IO, **untyped) -> Array[Row]
|
17
|
+
| (String | IO, **untyped) { (Row) -> void } -> untyped
|
18
|
+
|
19
|
+
class ValueResult
|
20
|
+
DEFAULT_VALUE: Object
|
21
|
+
|
22
|
+
@value: (casted)?
|
23
|
+
@original_value: untyped
|
24
|
+
|
25
|
+
attr_reader error_messages: [::String]?
|
26
|
+
|
27
|
+
def initialize: (original_value: ::String | nil, ?value: (casted | ::Object)?, ?error_messages: [::String]?) -> void
|
28
|
+
|
29
|
+
def value: -> ((casted | ::Object | nil)?)
|
30
|
+
|
31
|
+
def success?: -> bool
|
32
|
+
end
|
33
|
+
|
34
|
+
module Types
|
35
|
+
class Base
|
36
|
+
def cast: (untyped _value) -> bot
|
37
|
+
end
|
38
|
+
|
39
|
+
class Boolean < Base
|
40
|
+
TRUE_VALUES: Set[::String]
|
41
|
+
FALSE_VALUES: Set[::String]
|
42
|
+
|
43
|
+
def cast: (::String | nil) -> ValueResult
|
44
|
+
end
|
45
|
+
|
46
|
+
class Float < Base
|
47
|
+
DEFAULT_CONSTRAINT: ^(::Float) -> true
|
48
|
+
|
49
|
+
@constraint: ^(::Float) -> boolish
|
50
|
+
|
51
|
+
def initialize: (?constraint: ^(::Float) -> boolish) -> void
|
52
|
+
|
53
|
+
def cast: (::String | nil) -> ValueResult
|
54
|
+
end
|
55
|
+
|
56
|
+
class Integer < Base
|
57
|
+
DEFAULT_CONSTRAINT: ^(::Integer) -> true
|
58
|
+
|
59
|
+
@constraint: ^(::Integer) -> boolish
|
60
|
+
|
61
|
+
def initialize: (?constraint: ^(::Integer) -> boolish) -> void
|
62
|
+
|
63
|
+
def cast: (::String | nil) -> ValueResult
|
64
|
+
end
|
65
|
+
|
66
|
+
module Literal
|
67
|
+
def cast: (untyped value) -> ValueResult
|
68
|
+
| (untyped value) -> ValueResult
|
69
|
+
| (untyped value) -> ValueResult
|
70
|
+
| (untyped value) -> ValueResult
|
71
|
+
| (untyped value) -> ValueResult
|
72
|
+
end
|
73
|
+
|
74
|
+
class Optional < Base
|
75
|
+
@type: Boolean | Float | Integer | String | Time | Literal
|
76
|
+
|
77
|
+
def initialize: (declarable) -> void
|
78
|
+
|
79
|
+
def cast: (::String | nil) -> ValueResult
|
80
|
+
end
|
81
|
+
|
82
|
+
class String < Base
|
83
|
+
@within: Range[untyped] | nil
|
84
|
+
|
85
|
+
def initialize: (?within: Range[untyped] | nil) -> void
|
86
|
+
|
87
|
+
def cast: (::String | nil) -> ValueResult
|
88
|
+
end
|
89
|
+
|
90
|
+
class Time < Base
|
91
|
+
@format: ::String
|
92
|
+
|
93
|
+
def initialize: (?format: ::String) -> void
|
94
|
+
|
95
|
+
def cast: (::String | nil) -> ValueResult
|
96
|
+
end
|
97
|
+
|
98
|
+
class Union < Base
|
99
|
+
@types: Array[untyped]
|
100
|
+
|
101
|
+
def initialize: (untyped `type`, *untyped types) -> void
|
102
|
+
|
103
|
+
def cast: (::String | nil) -> untyped
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Let
|
108
|
+
@picked: Hash[untyped, untyped]
|
109
|
+
|
110
|
+
attr_reader types: Hash[column, [declarable, ^(casted) -> untyped | nil]]
|
111
|
+
attr_reader headers: bool
|
112
|
+
attr_reader pickers: Hash[untyped, Proc]
|
113
|
+
|
114
|
+
def initialize: -> void
|
115
|
+
|
116
|
+
def let: (::String | column, declarable, *declarable) -> void
|
117
|
+
| (::String | column, declarable, *declarable) { (casted) -> untyped } -> void
|
118
|
+
|
119
|
+
def pick: (column, as: ::Symbol) { (Array[::String]) -> untyped } -> void
|
120
|
+
|
121
|
+
def integer: (**untyped) -> Types::Integer
|
122
|
+
|
123
|
+
def integer?: (**untyped) -> Types::Optional
|
124
|
+
|
125
|
+
def boolean: -> Types::Boolean
|
126
|
+
|
127
|
+
def boolean?: -> Types::Optional
|
128
|
+
|
129
|
+
def float: (**untyped) -> Types::Float
|
130
|
+
|
131
|
+
def float?: (**untyped) -> Types::Optional
|
132
|
+
|
133
|
+
def string: (**untyped) -> Types::String
|
134
|
+
|
135
|
+
def string?: (**untyped) -> Types::Optional
|
136
|
+
|
137
|
+
def time: (**untyped) -> Types::Time
|
138
|
+
|
139
|
+
def time?: (**untyped) -> Types::Optional
|
140
|
+
|
141
|
+
def optional: (*declarable) -> Types::Optional
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def validate_columns: -> bool
|
146
|
+
end
|
147
|
+
|
148
|
+
class Row
|
149
|
+
extend Forwardable
|
150
|
+
|
151
|
+
@values: Hash[column, casted | ::Object | nil]
|
152
|
+
|
153
|
+
attr_reader errors: Hash[column, Array[::String]]
|
154
|
+
attr_reader lineno: ::Integer
|
155
|
+
|
156
|
+
def initialize: (row: Array[::String] | CSV::Row, types: Hash[column, [declarable, (^(casted | ::Object | nil) -> untyped | nil)]], lineno: ::Integer) -> void
|
157
|
+
|
158
|
+
def valid?: -> bool
|
159
|
+
end
|
160
|
+
|
161
|
+
class Error < StandardError
|
162
|
+
end
|
163
|
+
end
|
metadata
CHANGED
@@ -1,35 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strong_csv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.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:
|
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'
|
11
|
+
date: 2022-07-20 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
33
13
|
description: strong_csv is a type checker for a CSV file. It lets developers declare
|
34
14
|
types for each column to ensure all cells are satisfied with desired types.
|
35
15
|
email:
|
@@ -41,7 +21,6 @@ files:
|
|
41
21
|
- LICENSE
|
42
22
|
- README.md
|
43
23
|
- lib/strong_csv.rb
|
44
|
-
- lib/strong_csv/i18n.rb
|
45
24
|
- lib/strong_csv/let.rb
|
46
25
|
- lib/strong_csv/row.rb
|
47
26
|
- lib/strong_csv/types/base.rb
|
@@ -55,6 +34,7 @@ files:
|
|
55
34
|
- lib/strong_csv/types/union.rb
|
56
35
|
- lib/strong_csv/value_result.rb
|
57
36
|
- lib/strong_csv/version.rb
|
37
|
+
- sig/strong_csv.rbs
|
58
38
|
homepage: https://github.com/yykamei/strong_csv
|
59
39
|
licenses:
|
60
40
|
- MIT
|
data/lib/strong_csv/i18n.rb
DELETED
@@ -1,45 +0,0 @@
|
|
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
|
-
},
|
12
|
-
integer: {
|
13
|
-
cant_be_casted: "`%{value}` can't be casted to Integer",
|
14
|
-
},
|
15
|
-
literal: {
|
16
|
-
integer: {
|
17
|
-
unexpected: "`%{expected}` is expected, but `%{value}` was given",
|
18
|
-
cant_be_casted: "`%{value}` can't be casted to Integer",
|
19
|
-
},
|
20
|
-
float: {
|
21
|
-
unexpected: "`%{expected}` is expected, but `%{value}` was given",
|
22
|
-
cant_be_casted: "`%{value}` can't be casted to Float",
|
23
|
-
},
|
24
|
-
string: {
|
25
|
-
unexpected: "`%{expected}` is expected, but `%{value}` was given",
|
26
|
-
},
|
27
|
-
range: {
|
28
|
-
cant_be_casted: "`%{value}` can't be casted to the beginning of `%{expected}`",
|
29
|
-
out_of_range: "`%{value}` is not within `%{range}`",
|
30
|
-
},
|
31
|
-
regexp: {
|
32
|
-
cant_be_casted: "`%{value}` can't be casted to String",
|
33
|
-
unexpected: "`%{value}` did not match `%{expected}`",
|
34
|
-
},
|
35
|
-
},
|
36
|
-
string: {
|
37
|
-
cant_be_casted: "`%{value}` can't be casted to String",
|
38
|
-
out_of_range: "The length of `%{value}` is out of range `%{range}`",
|
39
|
-
},
|
40
|
-
time: {
|
41
|
-
cant_be_casted: "`%{value}` can't be casted to Time with the format `%{time_format}`",
|
42
|
-
},
|
43
|
-
},
|
44
|
-
},
|
45
|
-
)
|