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 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: []