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