parxer 0.1.1

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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +14 -0
  4. data/.hound.yml +4 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +1038 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +12 -0
  9. data/CHANGELOG.md +13 -0
  10. data/Gemfile +4 -0
  11. data/Guardfile +5 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +140 -0
  14. data/Rakefile +5 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/docs/images/parxer-response.png +0 -0
  18. data/docs/images/superheroes-xls.png +0 -0
  19. data/lib/parxer.rb +10 -0
  20. data/lib/parxer/collections/attributes.rb +16 -0
  21. data/lib/parxer/collections/callbacks.rb +22 -0
  22. data/lib/parxer/collections/row_errors.rb +19 -0
  23. data/lib/parxer/collections/validators.rb +34 -0
  24. data/lib/parxer/dsl/dsl.rb +92 -0
  25. data/lib/parxer/formatters/base_formatter.rb +39 -0
  26. data/lib/parxer/formatters/boolean_formatter.rb +14 -0
  27. data/lib/parxer/formatters/custom_formatter.rb +13 -0
  28. data/lib/parxer/formatters/number_formatter.rb +25 -0
  29. data/lib/parxer/formatters/rut_formatter.rb +33 -0
  30. data/lib/parxer/formatters/string_formatter.rb +9 -0
  31. data/lib/parxer/parsers/base_parser.rb +80 -0
  32. data/lib/parxer/parsers/concerns/attributes.rb +25 -0
  33. data/lib/parxer/parsers/concerns/callback.rb +24 -0
  34. data/lib/parxer/parsers/concerns/config.rb +23 -0
  35. data/lib/parxer/parsers/concerns/formatter.rb +14 -0
  36. data/lib/parxer/parsers/concerns/validator.rb +75 -0
  37. data/lib/parxer/parsers/csv_parser.rb +13 -0
  38. data/lib/parxer/parsers/xls_parser.rb +21 -0
  39. data/lib/parxer/utils/context.rb +26 -0
  40. data/lib/parxer/utils/errors.rb +10 -0
  41. data/lib/parxer/utils/formatter_builder.rb +16 -0
  42. data/lib/parxer/utils/inherited_resource.rb +35 -0
  43. data/lib/parxer/utils/row_builder.rb +22 -0
  44. data/lib/parxer/validators/base_validator.rb +22 -0
  45. data/lib/parxer/validators/boolean_validator.rb +13 -0
  46. data/lib/parxer/validators/columns_validator.rb +19 -0
  47. data/lib/parxer/validators/custom_validator.rb +17 -0
  48. data/lib/parxer/validators/datetime_validator.rb +54 -0
  49. data/lib/parxer/validators/email_validator.rb +14 -0
  50. data/lib/parxer/validators/file_format_validator.rb +18 -0
  51. data/lib/parxer/validators/file_presence_validator.rb +9 -0
  52. data/lib/parxer/validators/inclusion_validator.rb +26 -0
  53. data/lib/parxer/validators/number_validator.rb +63 -0
  54. data/lib/parxer/validators/presence_validator.rb +9 -0
  55. data/lib/parxer/validators/rows_count_validator.rb +9 -0
  56. data/lib/parxer/validators/rut_validator.rb +32 -0
  57. data/lib/parxer/validators/url_validator.rb +11 -0
  58. data/lib/parxer/values/attribute.rb +19 -0
  59. data/lib/parxer/values/callback.rb +22 -0
  60. data/lib/parxer/values/row.rb +17 -0
  61. data/lib/parxer/version.rb +3 -0
  62. data/parxer.gemspec +32 -0
  63. metadata +247 -0
@@ -0,0 +1,14 @@
1
+ module Parxer
2
+ module Formatter
3
+ class Boolean < Base
4
+ TRUE_OPTIONS = ["true", "t", "1", "1.0"]
5
+ FALSE_OPTIONS = ["false", "f", "0", "0.0"]
6
+
7
+ def format_value(v)
8
+ return true if TRUE_OPTIONS.include?(v)
9
+ return false if FALSE_OPTIONS.include?(v)
10
+ nil
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Parxer
2
+ module Formatter
3
+ class Custom < Base
4
+ def format_value(_v)
5
+ if !config[:formatter_proc].is_a?(Proc)
6
+ raise Parxer::FormatterError.new("'formatter_proc' needs to be a Proc")
7
+ end
8
+
9
+ instance_eval(&config[:formatter_proc])
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ module Parxer
2
+ module Formatter
3
+ class Number < Base
4
+ def format_value(v)
5
+ v = integer? ? v.to_i : v.to_f
6
+ v = v.round(round) if round?
7
+ v
8
+ end
9
+
10
+ private
11
+
12
+ def integer?
13
+ !!config[:integer]
14
+ end
15
+
16
+ def round?
17
+ !integer? && !!config[:round]
18
+ end
19
+
20
+ def round
21
+ config[:round].to_s.to_i
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ module Parxer
2
+ module Formatter
3
+ class Rut < Base
4
+ def format_value(rut)
5
+ rut = clean_rut(rut)
6
+ return nil if rut.empty?
7
+ return rut if clean_rut?
8
+ format_rut(rut)
9
+ end
10
+
11
+ def clean_rut(rut)
12
+ rut.scan(/(\d|k)/i).flatten.join("").upcase
13
+ end
14
+
15
+ def format_rut(rut)
16
+ last_digit = rut[-1]
17
+ digits = rut[0...-1].split("").reverse
18
+ result = []
19
+
20
+ digits.each_with_index do |number, idx|
21
+ result << "." if !idx.zero? && (idx % 3).zero?
22
+ result << number
23
+ end
24
+
25
+ result.reverse.join("") + "-" + last_digit
26
+ end
27
+
28
+ def clean_rut?
29
+ !!config[:clean]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Parxer
2
+ module Formatter
3
+ class String < Base
4
+ def format_value(v)
5
+ v
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,80 @@
1
+ module Parxer
2
+ class BaseParser
3
+ include Parxer::InheritedResource
4
+ include Parxer::ParserConfig
5
+ include Parxer::ParserAttributes
6
+ include Parxer::ParserValidator
7
+ include Parxer::ParserFormatter
8
+ include Parxer::ParserCallback
9
+ include Parxer::Dsl
10
+
11
+ attr_reader :file, :value, :attribute, :row, :prev_row
12
+
13
+ validate_file(:file_presence)
14
+
15
+ def run(file)
16
+ @file = file
17
+ return unless validate_file
18
+ row_class = Parxer::RowBuilder.build(attribute_ids)
19
+ Enumerator.new do |enum|
20
+ for_each_raw_row do |raw_row, idx|
21
+ @row = row_class.new(idx: idx)
22
+ parse_row(raw_row)
23
+ enum << row
24
+ @prev_row = row
25
+ end
26
+ end
27
+ end
28
+
29
+ def raw_rows
30
+ raise Parxer::ParserError.new("not implemented")
31
+ end
32
+
33
+ def extract_raw_attr_value(value)
34
+ value
35
+ end
36
+
37
+ def header
38
+ @header ||= raw_rows.first
39
+ end
40
+
41
+ def rows_count
42
+ raw_rows.count
43
+ end
44
+
45
+ def file_extension
46
+ ext = File.extname(file.to_s).delete(".")
47
+ return if ext.blank?
48
+ ext.to_sym
49
+ end
50
+
51
+ private
52
+
53
+ def parse_row(raw_row)
54
+ raw_row.each do |attribute_name, value|
55
+ @value = row.send("#{attribute_name}=", value)
56
+ @attribute = find_attribute(attribute_name)
57
+ format_attribute_value if validate_row_attribute
58
+ end
59
+
60
+ validate_row
61
+ after_parse_row
62
+ end
63
+
64
+ def for_each_raw_row
65
+ raw_rows.each_with_index do |raw_row, idx|
66
+ next if idx.zero?
67
+ yield(raw_row_to_hash(raw_row), idx + 1)
68
+ end
69
+ end
70
+
71
+ def raw_row_to_hash(raw_row)
72
+ pos = 0
73
+ attributes.inject({}) do |memo, column|
74
+ memo[column.id.to_sym] = extract_raw_attr_value(raw_row[pos])
75
+ pos += 1
76
+ memo
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,25 @@
1
+ module Parxer
2
+ module ParserAttributes
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ def attribute_ids
7
+ attributes.map(&:id)
8
+ end
9
+
10
+ def attributes
11
+ @attributes ||= inherited_collection(self, :attributes, Parxer::Attributes)
12
+ end
13
+
14
+ def find_attribute(attribute_name)
15
+ attributes.find_attribute(attribute_name)
16
+ end
17
+ end
18
+
19
+ class_methods do
20
+ def attributes
21
+ @attributes ||= Parxer::Attributes.new
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module Parxer
2
+ module ParserCallback
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ def parser_callbacks
7
+ @parser_callbacks ||= inherited_collection(self, :parser_callbacks, Parxer::Callbacks)
8
+ end
9
+
10
+ def after_parse_row
11
+ parser_callbacks.by_type(:after_parse_row).each do |callback|
12
+ callback.context = self
13
+ callback.run
14
+ end
15
+ end
16
+ end
17
+
18
+ class_methods do
19
+ def parser_callbacks
20
+ @parser_callbacks ||= Parxer::Callbacks.new
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ module Parxer
2
+ module ParserConfig
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_reader :parser_config
7
+
8
+ def parser_config
9
+ @parser_config ||= inherited_hash(self, :parser_config)
10
+ end
11
+ end
12
+
13
+ class_methods do
14
+ def parser_config
15
+ @parser_config ||= {}
16
+ end
17
+
18
+ def add_config_option(key, value)
19
+ parser_config[key.to_sym] = value
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ module Parxer
2
+ module ParserFormatter
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ def format_attribute_value
7
+ formatter = attribute.formatter
8
+ return unless formatter
9
+ formatter.context = self
10
+ row.send("#{attribute.id}=", formatter.apply)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,75 @@
1
+ module Parxer
2
+ module ParserValidator
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_reader :file_error
7
+
8
+ def file_validators
9
+ @file_validators ||= inherited_collection(self, :file_validators, Parxer::Validators)
10
+ end
11
+
12
+ def row_validators
13
+ @row_validators ||= inherited_collection(self, :row_validators, Parxer::Validators)
14
+ end
15
+
16
+ def valid_file?
17
+ !file_error
18
+ end
19
+
20
+ def validate_file
21
+ file_validators.each do |validator|
22
+ validator.context = self
23
+ next if !valid_file? || validator.validate
24
+ @file_error = validator.id
25
+ end
26
+
27
+ valid_file?
28
+ end
29
+
30
+ def validate_row
31
+ row_validators.each do |validator|
32
+ validator.context = self
33
+ next if !validate_row?(validator) || validator.validate
34
+ row.add_error(:base, validator.id)
35
+ end
36
+
37
+ !row.errors?
38
+ end
39
+
40
+ def validate_row?(validator)
41
+ attrs = validator.config[:if_valid]
42
+ return !row.errors? if attrs.blank?
43
+ attrs.select { |a| row.attribute_error?(a) }.none?
44
+ end
45
+
46
+ def validate_row_attribute
47
+ attribute.validators.each do |validator|
48
+ validator.context = self
49
+ next if row.attribute_error?(attribute.id) || validator.validate
50
+ row.add_error(attribute.id, validator.id)
51
+ end
52
+
53
+ !row.attribute_error?(attribute.id)
54
+ end
55
+ end
56
+
57
+ class_methods do
58
+ def file_validators
59
+ @file_validators ||= Parxer::Validators.new
60
+ end
61
+
62
+ def add_validator(validator_name, config, &block)
63
+ file_validators.add_validator(validator_name, config, &block)
64
+ end
65
+
66
+ def row_validators
67
+ @row_validators ||= Parxer::Validators.new
68
+ end
69
+
70
+ def add_row_validator(validator_name, config, &block)
71
+ row_validators.add_validator(validator_name, config, &block)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,13 @@
1
+ module Parxer
2
+ class CsvParser < Parxer::BaseParser
3
+ validate_file(:file_format, allowed_extensions: [:csv])
4
+
5
+ def raw_rows
6
+ csv
7
+ end
8
+
9
+ def csv
10
+ @csv ||= Roo::CSV.new(file, csv_options: parser_config)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module Parxer
2
+ class XlsParser < Parxer::BaseParser
3
+ validate_file(:file_format, allowed_extensions: [:xls, :xlsx])
4
+
5
+ def raw_rows
6
+ worksheet
7
+ end
8
+
9
+ def extract_raw_attr_value(value)
10
+ value.is_a?(Spreadsheet::Formula) ? value.value : value
11
+ end
12
+
13
+ def worksheet
14
+ @worksheet ||= workbook.sheet(0)
15
+ end
16
+
17
+ def workbook
18
+ @workbook ||= Roo::Spreadsheet.open(file, parser_config)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ module Parxer
2
+ module Context
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_writer :context
7
+
8
+ def context
9
+ raise Parxer::ContextError.new("'context' method not implemented") unless @context
10
+ @context
11
+ end
12
+
13
+ def method_missing(method_name, *arguments, &block)
14
+ if context.respond_to?(method_name)
15
+ return context.send(method_name, *arguments, &block)
16
+ end
17
+
18
+ super
19
+ end
20
+
21
+ def respond_to_missing?(method_name, include_private = false)
22
+ context.respond_to?(method_name) || super
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ class Parxer::Error < RuntimeError; end
2
+ class Parxer::ParserError < Parxer::Error; end
3
+ class Parxer::RowError < Parxer::Error; end
4
+ class Parxer::ValidatorError < Parxer::Error; end
5
+ class Parxer::XlsDslError < Parxer::Error; end
6
+ class Parxer::AttributesError < Parxer::Error; end
7
+ class Parxer::FormatterError < Parxer::Error; end
8
+ class Parxer::ContextError < Parxer::Error; end
9
+ class Parxer::DslError < Parxer::Error; end
10
+ class Parxer::CallbacksError < Parxer::Error; end
@@ -0,0 +1,16 @@
1
+ module Parxer
2
+ class FormatterBuilder
3
+ def self.build(formatter_name, config = {}, &block)
4
+ formatter_class = infer_formatter_class(formatter_name)
5
+ config[:formatter_proc] = block if formatter_class == Parxer::Formatter::Custom
6
+ formatter_class.new(config)
7
+ end
8
+
9
+ def self.infer_formatter_class(formatter_name)
10
+ return Parxer::Formatter::Custom if formatter_name.blank?
11
+ "Parxer::Formatter::#{formatter_name.to_s.camelize}".constantize
12
+ rescue NameError
13
+ Parxer::Formatter::Custom
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ module Parxer
2
+ module InheritedResource
3
+ def inherited_collection(object, method_name, collection_class)
4
+ result = collection_class.new
5
+
6
+ for_each_ancestor_with_method(object, method_name) do |collection|
7
+ collection.each { |item| result << item }
8
+ end
9
+
10
+ result
11
+ end
12
+
13
+ def inherited_hash(object, method_name)
14
+ result = {}
15
+
16
+ for_each_ancestor_with_method(object, method_name) do |hash|
17
+ result.merge!(hash)
18
+ end
19
+
20
+ result
21
+ end
22
+
23
+ def for_each_ancestor_with_method(object, method_name, &block)
24
+ object_ancestors(object).each do |klass|
25
+ next unless klass.respond_to?(method_name)
26
+ block.call(klass.send(method_name))
27
+ end
28
+ end
29
+
30
+ def object_ancestors(object)
31
+ klass = object.is_a?(Class) ? object : object.class
32
+ klass.ancestors.grep(Class).reverse
33
+ end
34
+ end
35
+ end