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.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +14 -0
- data/.hound.yml +4 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1038 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +140 -0
- data/Rakefile +5 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/images/parxer-response.png +0 -0
- data/docs/images/superheroes-xls.png +0 -0
- data/lib/parxer.rb +10 -0
- data/lib/parxer/collections/attributes.rb +16 -0
- data/lib/parxer/collections/callbacks.rb +22 -0
- data/lib/parxer/collections/row_errors.rb +19 -0
- data/lib/parxer/collections/validators.rb +34 -0
- data/lib/parxer/dsl/dsl.rb +92 -0
- data/lib/parxer/formatters/base_formatter.rb +39 -0
- data/lib/parxer/formatters/boolean_formatter.rb +14 -0
- data/lib/parxer/formatters/custom_formatter.rb +13 -0
- data/lib/parxer/formatters/number_formatter.rb +25 -0
- data/lib/parxer/formatters/rut_formatter.rb +33 -0
- data/lib/parxer/formatters/string_formatter.rb +9 -0
- data/lib/parxer/parsers/base_parser.rb +80 -0
- data/lib/parxer/parsers/concerns/attributes.rb +25 -0
- data/lib/parxer/parsers/concerns/callback.rb +24 -0
- data/lib/parxer/parsers/concerns/config.rb +23 -0
- data/lib/parxer/parsers/concerns/formatter.rb +14 -0
- data/lib/parxer/parsers/concerns/validator.rb +75 -0
- data/lib/parxer/parsers/csv_parser.rb +13 -0
- data/lib/parxer/parsers/xls_parser.rb +21 -0
- data/lib/parxer/utils/context.rb +26 -0
- data/lib/parxer/utils/errors.rb +10 -0
- data/lib/parxer/utils/formatter_builder.rb +16 -0
- data/lib/parxer/utils/inherited_resource.rb +35 -0
- data/lib/parxer/utils/row_builder.rb +22 -0
- data/lib/parxer/validators/base_validator.rb +22 -0
- data/lib/parxer/validators/boolean_validator.rb +13 -0
- data/lib/parxer/validators/columns_validator.rb +19 -0
- data/lib/parxer/validators/custom_validator.rb +17 -0
- data/lib/parxer/validators/datetime_validator.rb +54 -0
- data/lib/parxer/validators/email_validator.rb +14 -0
- data/lib/parxer/validators/file_format_validator.rb +18 -0
- data/lib/parxer/validators/file_presence_validator.rb +9 -0
- data/lib/parxer/validators/inclusion_validator.rb +26 -0
- data/lib/parxer/validators/number_validator.rb +63 -0
- data/lib/parxer/validators/presence_validator.rb +9 -0
- data/lib/parxer/validators/rows_count_validator.rb +9 -0
- data/lib/parxer/validators/rut_validator.rb +32 -0
- data/lib/parxer/validators/url_validator.rb +11 -0
- data/lib/parxer/values/attribute.rb +19 -0
- data/lib/parxer/values/callback.rb +22 -0
- data/lib/parxer/values/row.rb +17 -0
- data/lib/parxer/version.rb +3 -0
- data/parxer.gemspec +32 -0
- 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,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,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
|