strong_csv 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +109 -0
- data/lib/strong_csv/let.rb +42 -0
- data/lib/strong_csv/row.rb +34 -0
- data/lib/strong_csv/types/base.rb +12 -0
- data/lib/strong_csv/types/integer.rb +15 -0
- data/lib/strong_csv/value_result.rb +28 -0
- data/lib/strong_csv/version.rb +5 -0
- data/lib/strong_csv.rb +35 -0
- metadata +57 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6a6324b338c8922470db715a4db9aac4d29a7e658313326df6b860a51eda09a5
|
4
|
+
data.tar.gz: '095654b19ea937ca862c91b98e4c59d29f8ddd148d0cb2feac594edf1ea54a1f'
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9d729e901fcbe7b6cecbe7b0470d5dffaa4c51f7a11269b1f0b2db188c633ee46fb569fab333343268d9f2235ce790508bc0070e6d716888bdfb8ae08c75011c
|
7
|
+
data.tar.gz: 3cfc376f86564afea311afae002488172c9b01ec4676791e7c78b3192495361ddc9f9531a57987c7235c7420acf04819c265c29213a9282c23a93c45af5aae4f
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2022 Yutaka Kamei
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# strong_csv
|
2
|
+
|
3
|
+
NOTE: This repository is still under development 🚧🚜🚧
|
4
|
+
|
5
|
+
Type checker for a CSV file inspired by [strong_json](https://github.com/soutaro/strong_json).
|
6
|
+
|
7
|
+
**Motivation**
|
8
|
+
|
9
|
+
Some applications have a feature to receive a CSV file uploaded by a user,
|
10
|
+
and in general, it needs to validate each cell of the CSV file.
|
11
|
+
|
12
|
+
How should applications validate them?
|
13
|
+
Of course, it depends, but there would be common validation logic for CSV files.
|
14
|
+
For example, some columns may have to be integers because of database requirements.
|
15
|
+
It would be cumbersome to write such validations always.
|
16
|
+
|
17
|
+
strong_json helps you to mitigate such a drudgery by letting you declare desired types beforehand.
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'strong_csv'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
```console
|
30
|
+
bundle
|
31
|
+
```
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
```console
|
36
|
+
gem install strong_csv
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
TBD: This hasn't yet been implemented.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
strong_csv = StrongCSV.new do
|
45
|
+
let :stock, integer
|
46
|
+
let :tax_rate, float
|
47
|
+
let :name, string(255)
|
48
|
+
let :description, string?(1000)
|
49
|
+
let :active, boolean
|
50
|
+
let :started_at, time?
|
51
|
+
let :data, any?
|
52
|
+
|
53
|
+
# Literal declaration
|
54
|
+
let :status, 0..6
|
55
|
+
let :priority, 10 | 20 | 30 | 40 | 50
|
56
|
+
let :size, "S" | "M" | "L" do |value|
|
57
|
+
case value
|
58
|
+
when "S"
|
59
|
+
1
|
60
|
+
when "M"
|
61
|
+
2
|
62
|
+
when "L"
|
63
|
+
3
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Regular expressions
|
68
|
+
let :url, %r{\Ahttps://}
|
69
|
+
|
70
|
+
# Custom validation.
|
71
|
+
#
|
72
|
+
# This example sees the database to fetch exactly stored `User` IDs,
|
73
|
+
# and it checks the `:user_id` cell really exists in the `users` table.
|
74
|
+
# `pick` would be useful to avoid N+1 problems.
|
75
|
+
pick :user_id, as: :user_ids do |ids|
|
76
|
+
User.where(id: ids).ids
|
77
|
+
end
|
78
|
+
let :user_id, integer { |i| user_ids.include?(i) }
|
79
|
+
end
|
80
|
+
|
81
|
+
data = <<~CSV
|
82
|
+
stock,tax_rate,name,active,status,priority,size,url
|
83
|
+
12,0.8,special item,True,4,20,M,https://example.com
|
84
|
+
CSV
|
85
|
+
|
86
|
+
strong_csv.parse(data, field_size_limit: 2048) do |row|
|
87
|
+
if row.valid?
|
88
|
+
row[:tax_rate] # => 0.8
|
89
|
+
row[:active] # => true
|
90
|
+
# do something with row
|
91
|
+
else
|
92
|
+
row.errors # => [{ row: 2, column: :user_id, messages: ["must be present", "must be an Integer", "must satisfy the custom validation"] }]
|
93
|
+
# do something with row.errors
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
## Available types
|
99
|
+
|
100
|
+
| Type | Description |
|
101
|
+
| ------- | ----------------------------------- |
|
102
|
+
| integer | The value must be casted to Integer |
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
Bug reports and pull requests are welcome on the [GitHub repository](https://github.com/yykamei/strong_csv).
|
107
|
+
This project is intended to be a safe, welcoming space for collaboration,
|
108
|
+
and contributors are expected to adhere to the
|
109
|
+
[code of conduct](https://github.com/yykamei/strong_csv/blob/main/CODE_OF_CONDUCT.md).
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class StrongCSV
|
4
|
+
# Let is a class that is used to define types for columns.
|
5
|
+
class Let
|
6
|
+
attr_reader :types, :headers
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@types = {}
|
10
|
+
@headers = false
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param name [String, Symbol, Integer]
|
14
|
+
def let(name, type)
|
15
|
+
case name
|
16
|
+
when Integer
|
17
|
+
@types[name] = type
|
18
|
+
when String, Symbol
|
19
|
+
@types[name.to_sym] = type
|
20
|
+
else
|
21
|
+
raise TypeError, "Invalid type specified for `name`. `name` must be String, Symbol, or Integer: #{name.inspect}"
|
22
|
+
end
|
23
|
+
validate_columns
|
24
|
+
end
|
25
|
+
|
26
|
+
def integer
|
27
|
+
Types::Integer.new
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def validate_columns
|
33
|
+
if @types.keys.all? { |k| k.is_a?(Integer) }
|
34
|
+
@headers = false
|
35
|
+
elsif @types.keys.all? { |k| k.is_a?(Symbol) }
|
36
|
+
@headers = true
|
37
|
+
else
|
38
|
+
raise ArgumentError, "`types` cannot be mixed with Integer and Symbol keys: #{@types.keys.inspect}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class StrongCSV
|
4
|
+
# Row is a representation of a row in a CSV file, which has casted values with specified types.
|
5
|
+
class Row
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@values, :[], :fetch
|
9
|
+
|
10
|
+
# @return [Hash]
|
11
|
+
attr_reader :errors
|
12
|
+
|
13
|
+
# @return [Integer]
|
14
|
+
attr_reader :lineno
|
15
|
+
|
16
|
+
# @param row [Array<String>, CSV::Row]
|
17
|
+
# @param types [Hash]
|
18
|
+
# @param lineno [Integer]
|
19
|
+
def initialize(row:, types:, lineno:)
|
20
|
+
@values = {}
|
21
|
+
@errors = {}
|
22
|
+
@lineno = lineno
|
23
|
+
types.each do |key, type|
|
24
|
+
value_result = type.cast(row[key])
|
25
|
+
@values[key] = value_result.value
|
26
|
+
@errors[key] = value_result.error_message unless value_result.success?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def valid?
|
31
|
+
@errors.empty?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class StrongCSV
|
4
|
+
module Types
|
5
|
+
# Integer type
|
6
|
+
class Integer < Base
|
7
|
+
# @todo Use :exception for Integer after we drop the support of Ruby 2.5
|
8
|
+
def cast(value)
|
9
|
+
ValueResult.new(value: Integer(value), original_value: value)
|
10
|
+
rescue ArgumentError, TypeError
|
11
|
+
ValueResult.new(original_value: value, error_message: "`#{value.inspect}` can't be casted to Integer")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class StrongCSV
|
4
|
+
# ValueResult represents a CSV field is valid or not and contains its casted value if it's valid.
|
5
|
+
class ValueResult
|
6
|
+
DEFAULT_VALUE = Object.new
|
7
|
+
private_constant :DEFAULT_VALUE
|
8
|
+
|
9
|
+
# @return [String, nil] The error message for the field.
|
10
|
+
attr_reader :error_message
|
11
|
+
|
12
|
+
def initialize(original_value:, value: DEFAULT_VALUE, error_message: nil)
|
13
|
+
@value = value
|
14
|
+
@original_value = original_value
|
15
|
+
@error_message = error_message
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Object] The casted value if it's valid. Otherwise, returns the original value.
|
19
|
+
def value
|
20
|
+
success? ? @value : @original_value
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Boolean]
|
24
|
+
def success?
|
25
|
+
@error_message.nil?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/strong_csv.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "csv"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
require_relative "strong_csv/version"
|
7
|
+
require_relative "strong_csv/let"
|
8
|
+
require_relative "strong_csv/value_result"
|
9
|
+
require_relative "strong_csv/types/base"
|
10
|
+
require_relative "strong_csv/types/integer"
|
11
|
+
require_relative "strong_csv/row"
|
12
|
+
|
13
|
+
# The top-level namespace for the strong_csv gem.
|
14
|
+
class StrongCSV
|
15
|
+
class Error < StandardError; end
|
16
|
+
|
17
|
+
def initialize(&block)
|
18
|
+
@let = Let.new
|
19
|
+
@let.instance_eval(&block) if block_given?
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param csv [String, IO]
|
23
|
+
# @param options [Hash] CSV options for parsing.
|
24
|
+
def parse(csv, **options)
|
25
|
+
options = options.merge(headers: @let.headers, header_converters: :symbol)
|
26
|
+
csv = CSV.new(csv, **options)
|
27
|
+
if block_given?
|
28
|
+
csv.each do |row|
|
29
|
+
yield Row.new(row: row, types: @let.types, lineno: csv.lineno)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
csv.each.map { |row| Row.new(row: row, types: @let.types, lineno: csv.lineno) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: strong_csv
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yutaka Kamei
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-04-26 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: strong_csv is a type checker for a CSV file. It lets developers declare
|
14
|
+
types for each column to ensure all cells are satisfied with desired types.
|
15
|
+
email:
|
16
|
+
- kamei@yykamei.me
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- LICENSE
|
22
|
+
- README.md
|
23
|
+
- lib/strong_csv.rb
|
24
|
+
- lib/strong_csv/let.rb
|
25
|
+
- lib/strong_csv/row.rb
|
26
|
+
- lib/strong_csv/types/base.rb
|
27
|
+
- lib/strong_csv/types/integer.rb
|
28
|
+
- lib/strong_csv/value_result.rb
|
29
|
+
- lib/strong_csv/version.rb
|
30
|
+
homepage: https://github.com/yykamei/strong_csv
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata:
|
34
|
+
homepage_uri: https://github.com/yykamei/strong_csv
|
35
|
+
source_code_uri: https://github.com/yykamei/strong_csv
|
36
|
+
changelog_uri: https://github.com/yykamei/strong_csv/blob/main/CHANGELOG.md
|
37
|
+
rubygems_mfa_required: 'true'
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 2.5.5
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubygems_version: 3.3.7
|
54
|
+
signing_key:
|
55
|
+
specification_version: 4
|
56
|
+
summary: Type check CSV objects
|
57
|
+
test_files: []
|