parxer 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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