csv_converter 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 20286f21df3a885fb2b08dffcf7eb1ab51c79adbd3962042b8ee7cfcec5ffdf7
4
+ data.tar.gz: b3d6d7ffa1b284f14d3122d5cbf6cb407f829befda0496996f6a950c13414a2c
5
+ SHA512:
6
+ metadata.gz: b929f7e217e5aa0d8d4d31828a31b35102a520824b172dd27a80e0b98275d2eb4765d8e94e04bb617de172b7ee8840c9c474e171191ac42ad8aecbaafe1b5512
7
+ data.tar.gz: c1c5decb31c6fdae2086400ff514709b8201ccdf526ff06502d48c86b963e25d382f1cd18ed9e05b256accc61f86b48d1a9bc850e1e99f5fdb8eb0488c765a28
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .history/
10
+ .byebug_history
11
+ .DS_Store
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,17 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+
4
+ Metrics/LineLength:
5
+ Max: 120
6
+
7
+ Metrics/BlockLength:
8
+ Exclude:
9
+ - 'spec/**/*'
10
+
11
+ Metrics/MethodLength:
12
+ Exclude:
13
+ - 'spec/**/*'
14
+
15
+ Metrics/ModuleLength:
16
+ Exclude:
17
+ - 'spec/**/*'
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.2
7
+ before_install: gem install bundler -v 2.0.2
File without changes
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in csv_converter.gemspec
6
+ gemspec
@@ -0,0 +1,70 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ csv_converter (0.1.0)
5
+ activesupport (~> 6.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (6.0.0)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (>= 0.7, < 2)
13
+ minitest (~> 5.1)
14
+ tzinfo (~> 1.1)
15
+ zeitwerk (~> 2.1, >= 2.1.8)
16
+ ast (2.4.0)
17
+ byebug (11.0.1)
18
+ concurrent-ruby (1.1.5)
19
+ diff-lcs (1.3)
20
+ i18n (1.7.0)
21
+ concurrent-ruby (~> 1.0)
22
+ jaro_winkler (1.5.3)
23
+ minitest (5.12.2)
24
+ parallel (1.18.0)
25
+ parser (2.6.5.0)
26
+ ast (~> 2.4.0)
27
+ rainbow (3.0.0)
28
+ rake (10.5.0)
29
+ rb-readline (0.5.5)
30
+ rspec (3.9.0)
31
+ rspec-core (~> 3.9.0)
32
+ rspec-expectations (~> 3.9.0)
33
+ rspec-mocks (~> 3.9.0)
34
+ rspec-core (3.9.0)
35
+ rspec-support (~> 3.9.0)
36
+ rspec-expectations (3.9.0)
37
+ diff-lcs (>= 1.2.0, < 2.0)
38
+ rspec-support (~> 3.9.0)
39
+ rspec-mocks (3.9.0)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.9.0)
42
+ rspec-support (3.9.0)
43
+ rubocop (0.75.1)
44
+ jaro_winkler (~> 1.5.1)
45
+ parallel (~> 1.10)
46
+ parser (>= 2.6)
47
+ rainbow (>= 2.2.2, < 4.0)
48
+ ruby-progressbar (~> 1.7)
49
+ unicode-display_width (>= 1.4.0, < 1.7)
50
+ ruby-progressbar (1.10.1)
51
+ thread_safe (0.3.6)
52
+ tzinfo (1.2.5)
53
+ thread_safe (~> 0.1)
54
+ unicode-display_width (1.6.0)
55
+ zeitwerk (2.2.0)
56
+
57
+ PLATFORMS
58
+ ruby
59
+
60
+ DEPENDENCIES
61
+ bundler (~> 2.0)
62
+ byebug (~> 11.0)
63
+ csv_converter!
64
+ rake (~> 10.0)
65
+ rb-readline (~> 0.5)
66
+ rspec (~> 3.0)
67
+ rubocop (~> 0.75)
68
+
69
+ BUNDLED WITH
70
+ 2.0.2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2019] [Jose Francisco Rojas Soto]
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.
@@ -0,0 +1,132 @@
1
+ # csv_converter [![Build Status](https://travis-ci.org/francisco-rojas/basic_ruby.svg?branch=master)](https://travis-ci.org/francisco-rojas/basic_ruby)
2
+
3
+ *csv_converter* is a library for facilitating the grouping and transformation of tabulated data contained in files such as csv or spreadsheets files. This is **not** a library for parsing files. There are already plenty of libraries out there for reading and parsing files in different formats.
4
+
5
+ Instead, this library focuses on the conversion of the data provided in the files. Often times, it is required to cast the text data into a ruby object, perform validations on that data, and map it to the corresponding db tables/models and columns/attributes. This library aims to simplify that process.
6
+
7
+ For example, given the following csv content:
8
+
9
+ ```
10
+ First Name,Last Name,Make,Model,Year,Color,Purchase Date
11
+ John,Smith,Ford,Mustang,2000,Black,25/01/99
12
+ Julian,Moore,Toyota,Yaris,2005,Red,13/04/05
13
+ Joe,Black,Volvo,V40,2015,Gold,03/02/16
14
+ ```
15
+
16
+ | First Name | Last Name | Make | Model | Year | Color | Purchase Date |
17
+ | ----------- | --------- | ------- | ------- | ---- | ----- | ------------- |
18
+ | John | Smith | Ford | Mustang | 2000 | Black | 25/01/99 |
19
+ | Julian | Moore | Toyota | Yaris | 2005 | Red | 13/04/05 |
20
+ | Joe | Black | Volvo | V40 | 2015 | Gold | 03/02/16 |
21
+
22
+
23
+ you might want something like this:
24
+
25
+ ```ruby
26
+ [
27
+ {
28
+ "owner" => {
29
+ "first_name" => "John",
30
+ "last_name" => "Smith"
31
+ },
32
+ "vehicle" => {
33
+ "make" => "Ford",
34
+ "model" => "Mustang",
35
+ "year" => 2000,
36
+ "color" => "Black",
37
+ "purchase_date" => #<Date: 1999-01-25 ((2451204j,0s,0n),+0s,2299161j)>
38
+ }
39
+ },
40
+ {
41
+ "owner" => {
42
+ "first_name" => "Julian",
43
+ "last_name" => "Moore"
44
+ },
45
+ "vehicle" => {
46
+ "make" => "Toyota",
47
+ "model" => "Yaris",
48
+ "year" => 2005,
49
+ "color" => "Red",
50
+ "purchase_date" => #<Date: 2005-04-15 ((2453476j,0s,0n),+0s,2299161j)>
51
+ }
52
+ },
53
+ {
54
+ "owner" => {
55
+ "first_name" => "Joe",
56
+ "last_name" => "Black"
57
+ },
58
+ "vehicle" => {
59
+ "make" => "Volvo",
60
+ "model" => "V40",
61
+ "year" => 2015,
62
+ "color" => "Gold",
63
+ "purchase_date" => #<Date: 2016-03-02 ((2457450j,0s,0n),+0s,2299161j)>
64
+ }
65
+ }
66
+ ]
67
+ ```
68
+
69
+ In this example, each column from the csv has been grouped according to the configuration provided. Also, the data for each column has been converted to the expected data type.
70
+
71
+ This is performed by *csv_converter* based on the configuration provided in a .yml file (or a ruby hash) that lookes like this:
72
+
73
+ ```
74
+ owner:
75
+ first_name:
76
+ header: First Name
77
+ last_name:
78
+ header: Last Name
79
+ vehicle:
80
+ make:
81
+ header: Make
82
+ model:
83
+ header: Model
84
+ year:
85
+ header: Year
86
+ converters:
87
+ integer:
88
+ color:
89
+ header: Color
90
+ purchase_date:
91
+ header: Purchase Date
92
+ converters:
93
+ date:
94
+ date_format: "%m/%d/%y"
95
+ ```
96
+
97
+ ## Usage
98
+
99
+ *csv_converter* supports more advanced data conversions, processing data based on column position instead of headers and nested records within a column.
100
+
101
+ Please refer to the [*wiki page*](https://github.com/francisco-rojas/csv_converter/wiki) for further instructions and more advanced examples.
102
+
103
+ ## Installation
104
+
105
+ Add this line to your application's Gemfile:
106
+
107
+ ```ruby
108
+ gem 'csv_converter'
109
+ ```
110
+
111
+ And then execute:
112
+
113
+ $ bundle
114
+
115
+ Or install it yourself as:
116
+
117
+ $ gem install csv_converter
118
+
119
+ ## Development
120
+
121
+ Currently, the library is stable. I believe it supports the most common use cases so most likely the code won't be updated very frequently. However, if you have any feature requests or find a bug feel free to open a github issue and I will reply as soon as I can.
122
+
123
+ - for **feature requests** please use the **enhancement** label.
124
+ - for **bugs** please use the **bugs** label.
125
+
126
+ ## Contributing
127
+
128
+ Bug reports and pull requests are welcome on GitHub at https://github.com/francisco-rojas/csv_converter
129
+
130
+ ## License
131
+
132
+ MIT License. Copyright 2019 Francisco Rojas. https://github.com/francisco-rojas
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'csv_converter'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'csv_converter/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'csv_converter'
9
+ spec.version = CSVConverter::VERSION
10
+ spec.authors = ['Francisco Rojas']
11
+ spec.email = ['josefcorojas@gmail.com']
12
+
13
+ spec.summary = 'Groups and converts tabulated data'
14
+ spec.description = 'Groups and converts tabulated data based on the mappings provided'
15
+ spec.homepage = 'https://github.com/francisco-rojas/csv_converter'
16
+ spec.license = 'MIT'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['wiki_uri'] = 'https://github.com/francisco-rojas/csv_converter/wiki'
20
+ spec.metadata['source_code_uri'] = 'https://github.com/francisco-rojas/csv_converter'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/francisco-rojas/csv_converter/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_dependency 'activesupport', '~> 6.0'
33
+ spec.add_development_dependency 'bundler', '~> 2.0'
34
+ spec.add_development_dependency 'byebug', '~> 11.0'
35
+ spec.add_development_dependency 'rake', '~> 10.0'
36
+ spec.add_development_dependency 'rb-readline', '~> 0.5'
37
+ spec.add_development_dependency 'rspec', '~> 3.0'
38
+ spec.add_development_dependency 'rubocop', '~> 0.75'
39
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ require 'active_support'
6
+ require 'active_support/core_ext/object/blank'
7
+ require 'active_support/core_ext/hash/indifferent_access'
8
+ require 'active_support/inflector'
9
+
10
+ require 'csv_converter/version'
11
+ require 'csv_converter/converters/base_converter'
12
+ require 'csv_converter/converters/array_converter'
13
+ require 'csv_converter/converters/hash_converter'
14
+ require 'csv_converter/converters/big_decimal_converter'
15
+ require 'csv_converter/converters/boolean_converter'
16
+ require 'csv_converter/converters/date_converter'
17
+ require 'csv_converter/converters/float_converter'
18
+ require 'csv_converter/converters/integer_converter'
19
+ require 'csv_converter/converters/string_converter'
20
+ require 'csv_converter/converters/uppercase_converter'
21
+ require 'csv_converter/converters/lowercase_converter'
22
+ require 'csv_converter/file_processor'
23
+ require 'csv_converter/entity_processor'
24
+ require 'csv_converter/attribute_processor'
25
+
26
+ # CSVConverter groups and transforms tabulated data contained in files such as csv or spreadsheets.
27
+ module CSVConverter
28
+ # Error holds error messages as well as details of the data being processed.
29
+ class Error < StandardError
30
+ # Details of the data being processed when the error happened. By default this includes:
31
+ # filename: the name of the file being processed
32
+ # row_num: number of the row being processed
33
+ # entity: the name of the entity being processed as provided in the mappings
34
+ # row: the raw data of the row being processed
35
+ # attr: the name of the attribute being processed as provided in the mappings
36
+ # Additionally it contains all the options provided to the converter in the mappings.
37
+ # @return [Hash]
38
+ attr_reader :details
39
+
40
+ # A new instance of Error.
41
+ # @param message [String] the error description
42
+ # @param details [Hash] info about the data being proccesed at the time of the error
43
+ def initialize(message, details = {})
44
+ @details = details
45
+ super("#{message} on: #{details}")
46
+ end
47
+ end
48
+
49
+ ALIASES = {
50
+ array: 'CSVConverter::Converters::ArrayConverter',
51
+ boolean: 'CSVConverter::Converters::BooleanConverter',
52
+ date: 'CSVConverter::Converters::DateConverter',
53
+ decimal: 'CSVConverter::Converters::BigDecimalConverter',
54
+ float: 'CSVConverter::Converters::FloatConverter',
55
+ hash: 'CSVConverter::Converters::HashConverter',
56
+ integer: 'CSVConverter::Converters::IntegerConverter',
57
+ lowercase: 'CSVConverter::Converters::LowercaseConverter',
58
+ string: 'CSVConverter::Converters::StringConverter',
59
+ uppercase: 'CSVConverter::Converters::UppercaseConverter'
60
+ }.with_indifferent_access
61
+
62
+ # When no custom aliases are included it returns CSVConverter::ALIASES.
63
+ # When custom converter alises are included it returns the whole list of aliases.
64
+ # @return [Hash] list of aliases for each converter class.
65
+ def self.aliases
66
+ @aliases || ALIASES
67
+ end
68
+
69
+ # Adds an alias to the list of aliases
70
+ # @param new_alias [Symbol, String] the name of the alias
71
+ # @param klass [Symbol, String] class name of the converter
72
+ # @return (@see #aliases)
73
+ def self.add_alias(new_alias, klass)
74
+ @aliases = aliases.merge(new_alias.to_sym => klass.to_s)
75
+ end
76
+
77
+ # Adds one or more alieases to the list of aliases
78
+ # @param new_aliases [Hash] list of aliases to append to the list,
79
+ # where the key is the name of the alias and the value is the class name of the converter
80
+ # @return (@see #aliases)
81
+ def self.add_aliases(new_aliases)
82
+ @aliases = aliases.merge(new_aliases)
83
+ end
84
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ # Extracts the value for a column from the csv row and applies conversions to it.
5
+ class AttributeProcessor
6
+ # The row being processed.
7
+ # @return [Array, Hash]
8
+ attr_reader :row
9
+
10
+ # The column being processed.
11
+ # @return [String]
12
+ attr_reader :data
13
+
14
+ # Attribute mappings.
15
+ # @return [Hash] configuration used to process the data.
16
+ attr_reader :attr_mappings
17
+
18
+ # Details of the data being processed. By default this includes:
19
+ # filename: the name of the file being processed.
20
+ # row_num: number of the row being processed.
21
+ # entity: the name of the entity being processed as provided in the mappings.
22
+ # row: the raw data of the row being processed.
23
+ # attr: the name of the attribute being processed as provided in the mappings.
24
+ # Additionally it will contain all the options provided to the converter in the mappings.
25
+ # @return [Hash]
26
+ attr_reader :options
27
+
28
+ # A new instance of AttributeProcessor.
29
+ # @param row (@see #row)
30
+ # @param attr_mappings (@see #attr_mappings)
31
+ # @param options (@see #options)
32
+ def initialize(row, attr_mappings, options = {})
33
+ @row = row
34
+ @attr_mappings = attr_mappings
35
+ @options = options
36
+ @data = row[attr_mappings[:header]]
37
+ end
38
+
39
+ # Converts the data of the attribute into the type provided in the mappings by invoking the converters.
40
+ # @return [Object] an object of the expected type.
41
+ # If an error occurs during conversion the nullable value for the attributes is returned.
42
+ def call
43
+ convert_attr(&:call)
44
+ end
45
+
46
+ # Converts the data of the attribute into the type provided in the mappings by invoking the converters.
47
+ # @return [Object] an object of the expected type.
48
+ # If an error occurs during conversion an error is raised.
49
+ def call!
50
+ convert_attr(&:call!)
51
+ end
52
+
53
+ private
54
+
55
+ def convert_attr(&block)
56
+ return data if attr_mappings.dig(:converters).blank?
57
+
58
+ attr_mappings.dig(:converters).inject(data) do |d, (converter, opts)|
59
+ opts = (opts || {}).merge(options)
60
+ invoke_converter(converter, d, opts, &block)
61
+ end
62
+ end
63
+
64
+ def invoke_converter(converter, data, opts)
65
+ return converter.call(data, opts) if converter.is_a?(Proc)
66
+
67
+ converter = converter.to_s
68
+ converter = CSVConverter::ALIASES[converter] if CSVConverter::ALIASES.keys.include?(converter)
69
+ converter = converter.constantize.new(data, opts)
70
+
71
+ yield(converter)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Converts a string separated by a given char into an array of strings.
6
+ class ArrayConverter < BaseConverter
7
+ # A new instance of ArrayConverter.
8
+ # @param raw_data [String] the raw data of the attribute being processed.
9
+ # @param options [Hash] the options for the converter provided in the mappings.
10
+ # Additionally, contains the details of the data being processed. See BaseConverter#option.
11
+ # The *separator* key is required. If *separator* is nil then an error is raised.
12
+ def initialize(raw_data, options = { separator: ',' })
13
+ super(raw_data, options)
14
+
15
+ validate_options
16
+ end
17
+
18
+ # Converts *data* into an array by splitting the string on the *separator* provided in the mappings.
19
+ # @return [Array] if an error occurs during conversion an empty array is returned.
20
+ def call
21
+ call!
22
+ rescue CSVConverter::Error
23
+ nullable_object
24
+ end
25
+
26
+ # Converts *data* into an array by splitting the string on the *separator* provided in the mappings.
27
+ # @return [Array] if an error occurs during conversion an error is raised.
28
+ def call!
29
+ data.split(options[:separator]).map(&:strip)
30
+ rescue StandardError => e
31
+ raise CSVConverter::Error.new(e.message, options)
32
+ end
33
+
34
+ private
35
+
36
+ def validate_options
37
+ return if options && !options[:separator].nil?
38
+
39
+ raise CSVConverter::Error.new('no `key_value_separator` provided', options)
40
+ end
41
+
42
+ def nullable_object
43
+ []
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Defines the interface that all converters must implement
6
+ class BaseConverter
7
+ # @return [String] the raw data of the attribute being processed.
8
+ attr_reader :raw_data
9
+ # Details of the data being processed. By default this includes:
10
+ # filename: the name of the file being processed
11
+ # row_num: number of the row being processed
12
+ # entity: the name of the entity being processed as provided in the mappings
13
+ # row: the raw data of the row being processed
14
+ # attr: the name of the attribute being processed as provided in the mappings
15
+ # Additionally it contains all the options provided to the converter in the mappings.
16
+ # @return [Hash]
17
+ attr_reader :options
18
+
19
+ # A new instance of BaseConverter.
20
+ # @param raw_data [String] the raw data of the attribute being processed.
21
+ # @param options [Hash] the options for the converter provided in the mappings.
22
+ # Additionally, contains the details of the data being processed.
23
+ def initialize(raw_data, options = {})
24
+ @raw_data = raw_data.to_s.strip
25
+ @options = options || {}
26
+ end
27
+
28
+ # Converts raw_data into the type specified in the mappings.
29
+ # Must be implemented by children
30
+ def call
31
+ raise NotImplementedError
32
+ end
33
+
34
+ # Converts raw_data into the type specified in the mappings.
35
+ # Must be implemented by children
36
+ def call!
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # Evaluates raw_data and returns the proper value for it.
41
+ # @return [String]
42
+ # If raw_data is not empty returns raw_data.
43
+ # If raw_data is empty and no default value is provided in the mappings returns the nullable object.
44
+ # If raw_data is empty and a default value is provided in the mappings returns the default value.
45
+ def data
46
+ @data ||= begin
47
+ return raw_data if raw_data.present? && !empty_value?
48
+
49
+ return nullable_object if options.dig(:default).blank?
50
+
51
+ options.dig(:default)
52
+ end
53
+ end
54
+
55
+ # Checks if raw_data is contained in the list of *empty_values* provided in the mapping.
56
+ # @return [Boolean]
57
+ def empty_value?
58
+ return false unless options.dig(:empty_values)
59
+
60
+ options.dig(:empty_values).include?(raw_data)
61
+ end
62
+
63
+ private
64
+
65
+ def nullable_object
66
+ raise NotImplementedError
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+
5
+ module CSVConverter
6
+ module Converters
7
+ # Converts a string into a decimal number
8
+ class BigDecimalConverter < BaseConverter
9
+ # Converts *data* into a BigDecimal.
10
+ # If the decimal separator is a comma it is replaced by a period before parsing.
11
+ # @return [BigDecimal] if an error occurs during conversion nil is returned.
12
+ def call
13
+ call!
14
+ rescue CSVConverter::Error
15
+ nullable_object
16
+ end
17
+
18
+ # Converts *data* into a BigDecimal.
19
+ # If the decimal separator is a comma it is replaced by a period before parsing.
20
+ # @return [BigDecimal] if an error occurs during conversion an error is raised.
21
+ def call!
22
+ BigDecimal(data.sub(',', '.'))
23
+ rescue StandardError => e
24
+ raise CSVConverter::Error.new(e.message, options)
25
+ end
26
+
27
+ private
28
+
29
+ def nullable_object
30
+ nil
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Converts a string into a boolean
6
+ class BooleanConverter < BaseConverter
7
+ # A new instance of BooleanConverter.
8
+ # @param raw_data [String] the raw data of the attribute being processed.
9
+ # @param options [Hash] the options for the converter provided in the mappings.
10
+ # Additionally, contains the details of the data being processed. See BaseConverter#option.
11
+ # The *truthy_values* key is required. If *truthy_values* is blank then an error is raised.
12
+ def initialize(raw_data, options = { truthy_values: %w[true false] })
13
+ super(raw_data, options)
14
+
15
+ validate_options
16
+ end
17
+
18
+ # Converts *data* into a Boolean by checking if *data*
19
+ # is contained in the list of *truthy_values* provided in the mappings.
20
+ # @return [Boolean] if an error occurs during conversion `false` is returned.
21
+ def call
22
+ call!
23
+ rescue CSVConverter::Error
24
+ nullable_object
25
+ end
26
+
27
+ # Converts *data* into a Boolean by checking if *data*
28
+ # is contained in the list of *truthy_values* provided in the mappings.
29
+ # @return [Boolean] if an error occurs during conversion an error is raised.
30
+ def call!
31
+ options[:truthy_values].include?(data)
32
+ rescue StandardError => e
33
+ raise CSVConverter::Error.new(e.message, options)
34
+ end
35
+
36
+ private
37
+
38
+ def validate_options
39
+ return if options && options[:truthy_values].present?
40
+
41
+ raise CSVConverter::Error.new('no `truthy_values` provided', options)
42
+ end
43
+
44
+ def nullable_object
45
+ false
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Converts a string into a date
6
+ class DateConverter < BaseConverter
7
+ # A new instance of DateConverter.
8
+ # @param raw_data [String] the raw data of the attribute being processed.
9
+ # @param options [Hash] the options for the converter provided in the mappings.
10
+ # Additionally, contains the details of the data being processed. See BaseConverter#option.
11
+ # The *date_format* key is required. If *date_format* is blank then an error is raised.
12
+ def initialize(raw_data, options = { date_format: '%m/%d/%y' })
13
+ super(raw_data, options)
14
+
15
+ validate_options
16
+ end
17
+
18
+ # Converts *data* into a Date using the format provided in the mappings.
19
+ # @return [Date] if an error occurs during conversion `nil` is returned.
20
+ def call
21
+ call!
22
+ rescue CSVConverter::Error
23
+ nullable_object
24
+ end
25
+
26
+ # Converts *data* into a Date using the format provided in the mappings.
27
+ # @return [Date] if an error occurs during conversion an error is raised.
28
+ def call!
29
+ Date.strptime(data, options[:date_format])
30
+ rescue StandardError => e
31
+ raise CSVConverter::Error.new(e.message, options)
32
+ end
33
+
34
+ private
35
+
36
+ def validate_options
37
+ return if options && options[:date_format].present?
38
+
39
+ raise CSVConverter::Error.new('no `date_format` provided', options)
40
+ end
41
+
42
+ def nullable_object
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Converts a string into a float
6
+ class FloatConverter < BaseConverter
7
+ # Converts *data* into a Float.
8
+ # If the decimal separator is a comma it is replaced by a period before parsing.
9
+ # @return [Float] if an error occurs during conversion nil is returned.
10
+ def call
11
+ call!
12
+ rescue CSVConverter::Error
13
+ nullable_object
14
+ end
15
+
16
+ # Converts *data* into a Float.
17
+ # If the decimal separator is a comma it is replaced by a period before parsing.
18
+ # @return [Float] if an error occurs during conversion an error is raised.
19
+ def call!
20
+ Float(data.sub(',', '.'))
21
+ rescue StandardError => e
22
+ raise CSVConverter::Error.new(e.message, options)
23
+ end
24
+
25
+ private
26
+
27
+ def nullable_object
28
+ nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Converts a string with key pair values into ruby hashes
6
+ class HashConverter < BaseConverter
7
+ # A new instance of HashConverter.
8
+ # @param raw_data [String] the raw data of the attribute being processed.
9
+ # @param options [Hash] the options for the converter provided in the mappings.
10
+ # Additionally, contains the details of the data being processed. See BaseConverter#option.
11
+ # The *item_separator* key is required. If *item_separator* is nil then an error is raised.
12
+ # The *key_value_separator* key is required. If *key_value_separator* is nil then an error is raised.
13
+ def initialize(raw_data, options = {})
14
+ super(raw_data, options)
15
+
16
+ validate_options
17
+ end
18
+
19
+ # Converts *data* into a hash by splitting the string on the *item_separator* to get the items
20
+ # and then by spliting the items on *key_value_separator* to get the key/value.
21
+ # @return [Hash] if an error occurs during conversion an empty hash is returned.
22
+ def call
23
+ call!
24
+ rescue CSVConverter::Error
25
+ nullable_object
26
+ end
27
+
28
+ # Converts *data* into a hash by splitting the string on the *item_separator* to get the items
29
+ # and then by spliting the items on *key_value_separator* to get the key/value.
30
+ # @return [Hash] if an error occurs during conversion an error is raised.
31
+ def call!
32
+ data.split(options[:item_separator]).map do |items|
33
+ items.split(options[:key_value_separator]).map(&:strip)
34
+ end.to_h
35
+ rescue StandardError => e
36
+ raise CSVConverter::Error.new(e.message, options)
37
+ end
38
+
39
+ private
40
+
41
+ def validate_options
42
+ validate_separator
43
+ validate_key_value_separator
44
+ end
45
+
46
+ def validate_separator
47
+ return unless options[:item_separator].nil?
48
+
49
+ raise CSVConverter::Error.new('no `item_separator` provided', options)
50
+ end
51
+
52
+ def validate_key_value_separator
53
+ return unless options[:key_value_separator].nil?
54
+
55
+ raise CSVConverter::Error.new('no `key_value_separator` provided', options)
56
+ end
57
+
58
+ def nullable_object
59
+ {}
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Converts a string into an integer
6
+ class IntegerConverter < BaseConverter
7
+ # Converts *data* into an Integer.
8
+ # @return [Integer] if an error occurs during conversion nil is returned.
9
+ def call
10
+ call!
11
+ rescue CSVConverter::Error
12
+ nullable_object
13
+ end
14
+
15
+ # Converts *data* into an Integer.
16
+ # @return [Integer] if an error occurs during conversion an error is raised.
17
+ def call!
18
+ Integer(data)
19
+ rescue StandardError => e
20
+ raise CSVConverter::Error.new(e.message, options)
21
+ end
22
+
23
+ private
24
+
25
+ def nullable_object
26
+ nil
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Converts a string to lowercase
6
+ class LowercaseConverter < BaseConverter
7
+ # Converts a string to lowercase
8
+ # @return [String] if *data* is empty returns an empty string.
9
+ def call
10
+ call!
11
+ rescue CSVConverter::Error
12
+ nullable_object
13
+ end
14
+
15
+ # Converts a string to lowercase
16
+ # @return [String] if *data* is empty an error is raised.
17
+ def call!
18
+ raise ArgumentError, 'no data provided' if data.blank?
19
+
20
+ data.downcase
21
+ rescue StandardError => e
22
+ raise CSVConverter::Error.new(e.message, options)
23
+ end
24
+
25
+ private
26
+
27
+ def nullable_object
28
+ ''
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Cleans up a string
6
+ class StringConverter < BaseConverter
7
+ # Cleans up the *data* string.
8
+ # @return [String] if *data* is empty returns an empty string.
9
+ def call
10
+ call!
11
+ rescue CSVConverter::Error
12
+ nullable_object
13
+ end
14
+
15
+ # Cleans up the *data* string.
16
+ # @return [String] if *data* is empty an error is raised.
17
+ def call!
18
+ raise ArgumentError, 'no data provided' if data.blank?
19
+
20
+ data
21
+ rescue StandardError => e
22
+ raise CSVConverter::Error.new(e.message, options)
23
+ end
24
+
25
+ private
26
+
27
+ def nullable_object
28
+ ''
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ module Converters
5
+ # Converts a string to uppercase
6
+ class UppercaseConverter < BaseConverter
7
+ # Converts a string to uppercase
8
+ # @return [String] if *data* is empty returns an empty string.
9
+ def call
10
+ call!
11
+ rescue CSVConverter::Error
12
+ nullable_object
13
+ end
14
+
15
+ # Converts a string to uppercase
16
+ # @return [String] if *data* is empty an error is raised.
17
+ def call!
18
+ raise ArgumentError, 'no data provided' if data.blank?
19
+
20
+ data.upcase
21
+ rescue StandardError => e
22
+ raise CSVConverter::Error.new(e.message, options)
23
+ end
24
+
25
+ private
26
+
27
+ def nullable_object
28
+ ''
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ # Iterates over the columns of a row and processes the data accordingly.
5
+ class EntityProcessor
6
+ # The row being processed.
7
+ # @return [Array, Hash]
8
+ attr_reader :row
9
+
10
+ # Entity mappings.
11
+ # @return [Hash] configuration used to process the data.
12
+ attr_reader :entity_mappings
13
+
14
+ # Details of the data being processed. By default this includes:
15
+ # filename: the name of the file being processed.
16
+ # row_num: number of the row being processed.
17
+ # entity: the name of the entity being processed as provided in the mappings.
18
+ # row: the raw data of the row being processed.
19
+ # attr: the name of the attribute being processed as provided in the mappings.
20
+ # Additionally it will contain all the options provided to the converter in the mappings.
21
+ # @return [Hash]
22
+ attr_reader :options
23
+
24
+ # A new instance of EntityProcessor.
25
+ # @param row (@see #row)
26
+ # @param entity_mappings (@see #entity_mappings)
27
+ # @param options (@see #options)
28
+ def initialize(row, entity_mappings, options = {})
29
+ @row = row
30
+ @entity_mappings = entity_mappings
31
+ @options = options
32
+ end
33
+
34
+ # Iterates over the attributes of each entity converting the data into the format expected by the mappings.
35
+ # @return [Hash] the attributes for each entity.
36
+ # If an error occurs while processing the error is rescued and nullable values returned for the
37
+ # attributes that caused the error.
38
+ def call
39
+ entity_attrs(&:call)
40
+ end
41
+
42
+ # Iterates over the attributes of each entity converting the data into the format expected by the mappings.
43
+ # @return [Hash] the attributes for each entity.
44
+ # If an error occurs while processing the an error is raised.
45
+ def call!
46
+ entity_attrs(&:call!)
47
+ end
48
+
49
+ private
50
+
51
+ def entity_attrs(&block)
52
+ return nested_entity(&block) if entity_mappings[:nested]
53
+
54
+ entity_mappings.each_with_object({}) do |(attr, attr_mappings), hash|
55
+ processor = CSVConverter::AttributeProcessor.new(row, attr_mappings, options.merge(attr: attr))
56
+ hash[attr] = block.call(processor)
57
+ end
58
+ end
59
+
60
+ def nested_entity
61
+ separator = entity_mappings[:separator] || ','
62
+ nested_row = row[entity_mappings[:header]].split(separator)
63
+ processor = self.class.new(nested_row, entity_mappings[:mappings], options)
64
+ yield(processor)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ # Iterates over a collection and processes the data accordingly.
5
+ class FileProcessor
6
+ # Name of the file being processed.
7
+ # @return [String] the name of the file.
8
+ attr_reader :filename
9
+
10
+ # Collection being processed.
11
+ # @return [Array] collection of rows being processed.
12
+ attr_reader :rows
13
+
14
+ # File mappings.
15
+ # @return [Hash] configuration used to process the data.
16
+ attr_reader :file_mappings
17
+
18
+ # A new instance of FileProcessor.
19
+ # @param filename (@see #filename)
20
+ # @param rows (@see #rows)
21
+ # @param file_mappings (@see #file_mappings)
22
+ def initialize(filename, rows, file_mappings)
23
+ @filename = filename
24
+ @rows = rows
25
+ @file_mappings = file_mappings.with_indifferent_access
26
+ end
27
+
28
+ # Iterates over the rows grouping and converting the data as expected based on the mappings.
29
+ # @return [Array] Collection of hashes containing the entities obtained after processing the rows.
30
+ # If an error occurs while processing the error is rescued and nullable values returned for the
31
+ # attributes that caused the error.
32
+ def call
33
+ rows.map.with_index do |row, row_num|
34
+ process_entities(row, row_num, &:call)
35
+ end
36
+ end
37
+
38
+ # Iterates over the rows grouping and converting the data as expected based on the mappings.
39
+ # @return [Array] Collection of hashes containing the entities obtained after processing the rows.
40
+ # If an error occurs while processing the rows an error is raised.
41
+ def call!
42
+ rows.map.with_index do |row, row_num|
43
+ process_entities(row, row_num, &:call!)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def process_entities(row, row_num)
50
+ file_mappings.each_with_object({}) do |(entity, entity_mappings), hash|
51
+ hash[entity] = {} unless hash.key?(entity)
52
+ options = { filename: filename, row_num: row_num, entity: entity, row: row }
53
+ processor = CSVConverter::EntityProcessor.new(row, entity_mappings, options)
54
+ hash[entity] = yield(processor)
55
+ end.with_indifferent_access
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVConverter
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: csv_converter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Francisco Rojas
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-11-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '11.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '11.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rb-readline
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.75'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.75'
111
+ description: Groups and converts tabulated data based on the mappings provided
112
+ email:
113
+ - josefcorojas@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".rubocop.yml"
121
+ - ".travis.yml"
122
+ - CHANGELOG.md
123
+ - Gemfile
124
+ - Gemfile.lock
125
+ - LICENSE
126
+ - README.md
127
+ - Rakefile
128
+ - bin/console
129
+ - bin/setup
130
+ - csv_converter.gemspec
131
+ - lib/csv_converter.rb
132
+ - lib/csv_converter/attribute_processor.rb
133
+ - lib/csv_converter/converters/array_converter.rb
134
+ - lib/csv_converter/converters/base_converter.rb
135
+ - lib/csv_converter/converters/big_decimal_converter.rb
136
+ - lib/csv_converter/converters/boolean_converter.rb
137
+ - lib/csv_converter/converters/date_converter.rb
138
+ - lib/csv_converter/converters/float_converter.rb
139
+ - lib/csv_converter/converters/hash_converter.rb
140
+ - lib/csv_converter/converters/integer_converter.rb
141
+ - lib/csv_converter/converters/lowercase_converter.rb
142
+ - lib/csv_converter/converters/string_converter.rb
143
+ - lib/csv_converter/converters/uppercase_converter.rb
144
+ - lib/csv_converter/entity_processor.rb
145
+ - lib/csv_converter/file_processor.rb
146
+ - lib/csv_converter/version.rb
147
+ homepage: https://github.com/francisco-rojas/csv_converter
148
+ licenses:
149
+ - MIT
150
+ metadata:
151
+ homepage_uri: https://github.com/francisco-rojas/csv_converter
152
+ wiki_uri: https://github.com/francisco-rojas/csv_converter/wiki
153
+ source_code_uri: https://github.com/francisco-rojas/csv_converter
154
+ changelog_uri: https://github.com/francisco-rojas/csv_converter/CHANGELOG.md
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubygems_version: 3.0.3
171
+ signing_key:
172
+ specification_version: 4
173
+ summary: Groups and converts tabulated data
174
+ test_files: []