strong_csv 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class StrongCSV
4
+ module Types
5
+ # Base class for all types.
6
+ class Base
7
+ def cast(_value)
8
+ raise NotImplementedError
9
+ end
10
+ end
11
+ end
12
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class StrongCSV
4
+ VERSION = "0.1.0"
5
+ 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: []