strong_csv 0.4.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 +35 -14
- data/lib/strong_csv/i18n.rb +2 -0
- data/lib/strong_csv/let.rb +38 -8
- data/lib/strong_csv/types/float.rb +15 -1
- data/lib/strong_csv/types/integer.rb +15 -1
- data/lib/strong_csv/version.rb +1 -1
- data/lib/strong_csv.rb +6 -0
- metadata +2 -2
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,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
|
|
@@ -396,8 +415,10 @@ ja:
|
|
396
415
|
cant_be_casted: "`%{value}`はBooleanに変換できません"
|
397
416
|
float:
|
398
417
|
cant_be_casted: "`%{value}`はFloatに変換できません"
|
418
|
+
constraint_error: "`%{value}`は指定された成約を満たしていません",
|
399
419
|
integer:
|
400
420
|
cant_be_casted: "`%{value}`はIntegerに変換できません"
|
421
|
+
constraint_error: "`%{value}`は指定された成約を満たしていません",
|
401
422
|
literal:
|
402
423
|
integer:
|
403
424
|
unexpected: "`%{expected}`ではなく`%{value}`が入力されています"
|
data/lib/strong_csv/i18n.rb
CHANGED
@@ -8,9 +8,11 @@ I18n.backend.store_translations(
|
|
8
8
|
},
|
9
9
|
float: {
|
10
10
|
cant_be_casted: "`%{value}` can't be casted to Float",
|
11
|
+
constraint_error: "`%{value}` does not satisfy the specified constraint",
|
11
12
|
},
|
12
13
|
integer: {
|
13
14
|
cant_be_casted: "`%{value}` can't be casted to Integer",
|
15
|
+
constraint_error: "`%{value}` does not satisfy the specified constraint",
|
14
16
|
},
|
15
17
|
literal: {
|
16
18
|
integer: {
|
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.
|
@@ -4,11 +4,25 @@ 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
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
|
@@ -4,11 +4,25 @@ 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
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
|
data/lib/strong_csv/version.rb
CHANGED
data/lib/strong_csv.rb
CHANGED
@@ -37,6 +37,12 @@ class StrongCSV
|
|
37
37
|
options.delete(:nil_value)
|
38
38
|
options = options.merge(headers: @let.headers, header_converters: :symbol)
|
39
39
|
csv = CSV.new(csv, **options)
|
40
|
+
|
41
|
+
@let.pickers.each_value do |picker|
|
42
|
+
picker.call(csv)
|
43
|
+
csv.rewind
|
44
|
+
end
|
45
|
+
|
40
46
|
if block_given?
|
41
47
|
csv.each do |row|
|
42
48
|
yield Row.new(row: row, types: @let.types, lineno: csv.lineno)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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-05-
|
11
|
+
date: 2022-05-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: i18n
|