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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8078bf75d84307eb32f647a418d2cd2214d6891a78a25a73f7589b82fec0716c
4
- data.tar.gz: 318af2a0c3a42ac7141819b06e729a5e98967c5106988bc4349b308f9c9d87f8
3
+ metadata.gz: b3e78f36b30b9033ba93a1295d6086f94c1da0aab0da2f9d06050c8d64f85a3c
4
+ data.tar.gz: c841993791e0cb0918852368c88b5cc19bb0ab021e9234f9f56150eb6d837f77
5
5
  SHA512:
6
- metadata.gz: 63aa30f83182e0cab08f816124945d9da91149faaa82017ba0a80ca8b39ef57fed5eab521db1efd8404045aadba189550bd779f195434cef2294975dc30216f3
7
- data.tar.gz: e4e74ea2446efcf32f6e67fe8be5df207ae8dbd66b392df4c2fb8895cd71aebf70894d062e64eee2814295aa00f29876afcb95b4c3a88ffffd7be0c8e9a9ba21
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: ["must be present", "must be an integer"] }
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} ("non-integer" cannot be casted to Integer)
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}`が入力されています"
@@ -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: {
@@ -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.
@@ -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
- 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
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
- 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
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class StrongCSV
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.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-10 00:00:00.000000000 Z
11
+ date: 2022-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: i18n