norma43_parser 1.0.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/ISSUE_TEMPLATE.md +13 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +5 -0
- data/.rubocop.yml +234 -0
- data/Gemfile +7 -1
- data/Gemfile.lock +73 -0
- data/README.md +1 -1
- data/Rakefile +2 -0
- data/lib/norma43.rb +9 -3
- data/lib/norma43/line_handlers.rb +2 -3
- data/lib/norma43/line_parsers/account_end.rb +18 -0
- data/lib/norma43/line_parsers/account_start.rb +18 -0
- data/lib/norma43/line_parsers/additional_currency.rb +12 -0
- data/lib/norma43/line_parsers/additional_item.rb +11 -0
- data/lib/norma43/line_parsers/document_end.rb +9 -0
- data/lib/norma43/line_parsers/document_start.rb +13 -0
- data/lib/norma43/line_parsers/file_format_validator.rb +15 -12
- data/lib/norma43/line_parsers/line_parser.rb +29 -28
- data/lib/norma43/line_parsers/transaction.rb +18 -0
- data/lib/norma43/line_processors.rb +2 -2
- data/lib/norma43/models.rb +5 -4
- data/lib/norma43/parser.rb +38 -42
- data/lib/norma43/utils/contexts.rb +40 -37
- data/lib/norma43/utils/string_helpers.rb +10 -6
- data/lib/norma43/utils/typecaster.rb +16 -12
- data/lib/norma43/version.rb +3 -1
- data/norma43_parser.gemspec +11 -9
- data/spec/example1_parse_spec.rb +12 -11
- data/spec/norma43/line_parsers/account_end_spec.rb +1 -1
- data/spec/norma43/line_parsers/account_start_spec.rb +1 -2
- data/spec/norma43/line_parsers/additional_currency_spec.rb +1 -1
- data/spec/norma43/line_parsers/additional_items_spec.rb +1 -1
- data/spec/norma43/line_parsers/document_end_spec.rb +1 -2
- data/spec/norma43/line_parsers/document_start_spec.rb +1 -1
- data/spec/norma43/line_parsers/transaction_spec.rb +1 -3
- data/spec/norma43/line_processors/account_end_spec.rb +6 -8
- data/spec/norma43/line_processors/account_start_spec.rb +8 -9
- data/spec/norma43/line_processors/additional_currency_spec.rb +6 -7
- data/spec/norma43/line_processors/additional_items_spec.rb +6 -7
- data/spec/norma43/line_processors/document_end_spec.rb +5 -6
- data/spec/norma43/line_processors/document_start_spec.rb +3 -4
- data/spec/norma43/line_processors/transaction_spec.rb +6 -7
- data/spec/norma43/parser_spec.rb +5 -4
- data/spec/norma43_spec.rb +2 -0
- data/spec/spec_helper.rb +1 -4
- data/spec/support/shared_examples_for_values_line_parsers.rb +2 -0
- metadata +41 -32
- data/lib/norma43/line_parsers/line_parsers.rb +0 -69
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Norma43
|
4
|
+
module LineParsers
|
5
|
+
class AdditionalCurrency < LineParser
|
6
|
+
field :data_code, 2..3, :integer
|
7
|
+
field :currency_code, 4..6, :integer
|
8
|
+
field :amount, 7..20, :integer
|
9
|
+
field :free, 21..79
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Norma43
|
4
|
+
module LineParsers
|
5
|
+
class DocumentStart < LineParser
|
6
|
+
field :id, 2..13
|
7
|
+
field :created_at, 14..33, :time
|
8
|
+
field :delivery_number, 34..35, :integer
|
9
|
+
field :file_type, 36..38
|
10
|
+
field :name, 39..48
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,19 +1,22 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Norma43
|
3
|
-
|
4
|
-
|
5
|
-
|
4
|
+
module LineParsers
|
5
|
+
class FileFormatValidator < LineParser
|
6
|
+
field :record_type, 0..1, :raw
|
7
|
+
field :file_type, 36..38
|
6
8
|
|
7
|
-
|
9
|
+
def has_document?; file_type == "00" end
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
def valid?
|
12
|
+
errors.empty?
|
13
|
+
end
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def errors
|
16
|
+
errors = []
|
17
|
+
%w(11 00).include?(record_type) || errors << ("Must start with 00 (was ”#{record_type}”)")
|
18
|
+
errors
|
19
|
+
end
|
17
20
|
end
|
18
21
|
end
|
19
22
|
end
|
@@ -1,42 +1,43 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Norma43
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
module LineParsers
|
5
|
+
class LineParser
|
6
|
+
attr_reader :line
|
7
|
+
def initialize(line)
|
8
|
+
@line = line
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
def attributes
|
12
|
+
self.class.field_names.each_with_object({}) do |field, attrs|
|
13
|
+
attrs[field] = self.public_send(field)
|
14
|
+
end
|
13
15
|
end
|
14
|
-
end
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
def self.field(name, range, type = :string)
|
18
|
+
self.field_names.push name
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
define_method name do
|
21
|
+
if range.is_a?(Array) # let multivalued attribute
|
22
|
+
range.map { |r| value_at_position(r, type) }.compact
|
23
|
+
else
|
24
|
+
value_at_position range, type
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
28
|
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
private
|
30
|
+
def self.field_names
|
31
|
+
@field_names ||= []
|
32
|
+
end
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
def value_at_position(range, type)
|
35
|
+
typecast line[range].to_s.strip, type
|
36
|
+
end
|
37
37
|
|
38
|
-
|
39
|
-
|
38
|
+
def typecast(value, type)
|
39
|
+
Norma43::Utils::Typecaster.cast value, type
|
40
|
+
end
|
40
41
|
end
|
41
42
|
end
|
42
43
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Norma43
|
4
|
+
module LineParsers
|
5
|
+
class Transaction < LineParser
|
6
|
+
field :origin_branch_code, 6..9, :integer
|
7
|
+
field :transaction_date, 10..15, :date
|
8
|
+
field :value_date, 16..21, :date
|
9
|
+
field :shared_item, 22..23, :integer
|
10
|
+
field :own_item, 24..26, :integer
|
11
|
+
field :amount_code, 27, :integer
|
12
|
+
field :amount, 28..41, :integer
|
13
|
+
field :document_number, 42..51, :integer
|
14
|
+
field :reference_1, 52..63, :integer
|
15
|
+
field :reference_2, 64..79
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Norma43
|
3
4
|
module LineProcessors
|
4
5
|
DocumentStart = ->(line, contexts) {
|
@@ -55,6 +56,5 @@ module Norma43
|
|
55
56
|
contexts.current.additional_currency = additional_currency
|
56
57
|
contexts
|
57
58
|
}
|
58
|
-
|
59
59
|
end
|
60
60
|
end
|
data/lib/norma43/models.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "virtus"
|
3
4
|
|
4
5
|
module Norma43
|
5
6
|
module Models
|
6
|
-
#forward declarations
|
7
|
+
# forward declarations
|
7
8
|
class Account; end
|
8
9
|
class Transaction; end
|
9
10
|
class AdditionalItem; end
|
@@ -62,7 +63,7 @@ module Norma43
|
|
62
63
|
attribute :reference_2
|
63
64
|
attribute :additional_items, Array[AdditionalItem]
|
64
65
|
attribute :additional_currency, AdditionalCurrency
|
65
|
-
def debit?; self.amount_code==DEBIT_CODE end
|
66
|
+
def debit?; self.amount_code == DEBIT_CODE end
|
66
67
|
end
|
67
68
|
|
68
69
|
class AdditionalItem
|
data/lib/norma43/parser.rb
CHANGED
@@ -1,25 +1,23 @@
|
|
1
|
-
|
2
|
-
require "norma43/line_handlers"
|
3
|
-
require "norma43/utils/contexts"
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
3
|
module Norma43
|
6
|
-
class InvalidFileFormatError < ArgumentError; end
|
4
|
+
class InvalidFileFormatError < ArgumentError; end
|
7
5
|
|
8
6
|
class Parser
|
9
7
|
attr_reader :file
|
10
8
|
|
11
9
|
# Parser.new accepts a File instance or a String
|
12
10
|
# A InvalidFileFormatError will be raised if file isn't in the Norma43 format
|
13
|
-
def initialize
|
11
|
+
def initialize(file)
|
14
12
|
@file = file
|
15
13
|
validator = validate_file_format
|
16
14
|
@contexts = if validator.has_document?
|
17
|
-
Contexts.new
|
15
|
+
Norma43::Utils::Contexts.new
|
18
16
|
else
|
19
17
|
# in theory Norma43 says that files should start with DocumentStart but
|
20
18
|
# practically doesn't happen, so that we create one artificially
|
21
19
|
# to avoid corner cases in the processors
|
22
|
-
Contexts.new().tap { |ctx| ctx.add Models::Document.new }
|
20
|
+
Norma43::Utils::Contexts.new().tap { |ctx| ctx.add Models::Document.new }
|
23
21
|
end
|
24
22
|
end
|
25
23
|
|
@@ -28,52 +26,50 @@ module Norma43
|
|
28
26
|
end
|
29
27
|
|
30
28
|
protected
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
end
|
29
|
+
def lines
|
30
|
+
@lines ||= file.each_line
|
31
|
+
end
|
35
32
|
|
36
33
|
private
|
34
|
+
def validate_file_format
|
35
|
+
validator = Norma43::LineParsers::FileFormatValidator.new first_line
|
36
|
+
raise InvalidFileFormatError.new(validator.errors.join(", ")) unless validator.valid?
|
37
|
+
validator
|
38
|
+
end
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
raise InvalidFileFormatError.new(validator.errors.join(", ")) unless validator.valid?
|
41
|
-
validator
|
42
|
-
end
|
43
|
-
|
44
|
-
def parse_lines contexts
|
45
|
-
parse_lines parse_line(self.lines.next, contexts)
|
40
|
+
def parse_lines(contexts)
|
41
|
+
parse_lines parse_line(self.lines.next, contexts)
|
46
42
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
43
|
+
rescue StopIteration # because lines is an enumerator raises StopIteration on end
|
44
|
+
self.lines.rewind # Ensure we do not bomb out when calling result multiple times
|
45
|
+
contexts
|
46
|
+
end
|
51
47
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
48
|
+
# Look up a matching handler for the line and process it
|
49
|
+
# The process method on a handler always returns a Contexts object
|
50
|
+
def parse_line(line, contexts)
|
51
|
+
line = line.encode Encoding::UTF_8 if encode_lines?
|
56
52
|
|
57
|
-
|
53
|
+
handler = handler_for_line line
|
58
54
|
|
59
|
-
|
60
|
-
|
55
|
+
handler.process line, contexts
|
56
|
+
end
|
61
57
|
|
62
|
-
|
63
|
-
|
64
|
-
|
58
|
+
def handler_for_line(line)
|
59
|
+
LineHandlers.mapping.fetch line[0..1]
|
60
|
+
end
|
65
61
|
|
66
|
-
|
67
|
-
|
68
|
-
|
62
|
+
def encode_lines?
|
63
|
+
first_line.encoding != Encoding::UTF_8
|
64
|
+
end
|
69
65
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
66
|
+
def first_line
|
67
|
+
@first_line ||= begin
|
68
|
+
line = self.lines.peek
|
69
|
+
self.lines.rewind # peek seems to move the pointer when file is an actual File object
|
74
70
|
|
75
|
-
|
71
|
+
line
|
72
|
+
end
|
76
73
|
end
|
77
|
-
end
|
78
74
|
end
|
79
75
|
end
|
@@ -1,54 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Norma43
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
4
|
+
module Utils
|
5
|
+
class Contexts
|
6
|
+
def initialize(containers = nil)
|
7
|
+
Array(containers).compact.each do |container|
|
8
|
+
add container
|
9
|
+
end
|
6
10
|
end
|
7
|
-
end
|
8
|
-
|
9
|
-
def result
|
10
|
-
contexts.first
|
11
|
-
end
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def add container
|
18
|
-
contexts.push container
|
19
|
-
end
|
12
|
+
def result
|
13
|
+
contexts.first
|
14
|
+
end
|
20
15
|
|
21
|
-
|
22
|
-
|
23
|
-
|
16
|
+
def current
|
17
|
+
contexts.last
|
18
|
+
end
|
24
19
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end if contexts.any?
|
29
|
-
end
|
20
|
+
def add(container)
|
21
|
+
contexts.push container
|
22
|
+
end
|
30
23
|
|
31
|
-
|
32
|
-
|
24
|
+
def move_up
|
25
|
+
contexts.pop
|
26
|
+
end
|
33
27
|
|
34
|
-
|
35
|
-
|
28
|
+
def move_to(container_class)
|
29
|
+
until current.is_a?(container_class) || current.nil?
|
30
|
+
move_up
|
31
|
+
end if contexts.any?
|
36
32
|
end
|
37
33
|
|
38
|
-
|
34
|
+
def move_to_or_add_to_parent(container_class, parent_container_class)
|
35
|
+
return self if current.is_a?(container_class)
|
39
36
|
|
40
|
-
|
41
|
-
|
37
|
+
until current.kind_of?(parent_container_class)
|
38
|
+
move_up
|
39
|
+
end
|
42
40
|
|
43
|
-
|
41
|
+
entity = container_class.new
|
44
42
|
|
45
|
-
|
46
|
-
|
43
|
+
setter_name = StringHelpers.underscore container_class.name.split("::").last
|
44
|
+
current.public_send "#{setter_name}=", entity
|
47
45
|
|
48
|
-
|
46
|
+
add entity
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
|
51
|
+
private
|
52
|
+
def contexts
|
53
|
+
@contexts ||= []
|
54
|
+
end
|
52
55
|
end
|
53
56
|
end
|
54
57
|
end
|
@@ -1,10 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Norma43
|
2
|
-
module
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
module Utils
|
5
|
+
module StringHelpers
|
6
|
+
def self.underscore(word)
|
7
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
8
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
9
|
+
word.tr!("-", "_")
|
10
|
+
word.downcase
|
11
|
+
end
|
8
12
|
end
|
9
13
|
end
|
10
14
|
end
|