strong_csv 0.4.0 → 0.7.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 +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
|
-
)
|