ach_builder 0.2.1 → 0.2.2
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 +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +1 -1
- data/ach_builder.gemspec +1 -1
- data/lib/ach/batch/builder.rb +27 -8
- data/lib/ach/component.rb +57 -12
- data/lib/ach/component/has_many_association.rb +57 -19
- data/lib/ach/constants.rb +8 -1
- data/lib/ach/file.rb +5 -2
- data/lib/ach/file/builder.rb +45 -16
- data/lib/ach/file/reader.rb +31 -0
- data/lib/ach/file/transmission_header.rb +19 -5
- data/lib/ach/formatter.rb +33 -11
- data/lib/ach/formatter/rule.rb +6 -3
- data/lib/ach/record/base.rb +54 -14
- data/lib/ach/record/dynamic.rb +18 -2
- data/lib/ach/record/entry.rb +12 -4
- data/lib/ach/validations.rb +23 -9
- data/lib/ach/version.rb +2 -1
- data/lib/ach_builder.rb +5 -0
- data/tmp/metric_fu/_data/20111221.yml +7231 -0
- metadata +22 -5
data/lib/ach/file.rb
CHANGED
@@ -45,8 +45,11 @@ module ACH
|
|
45
45
|
|
46
46
|
has_many :batches, :proc_defaults => lambda{ {:batch_number => batches.length + 1} }
|
47
47
|
|
48
|
-
#
|
49
|
-
# enum to scan for ACH contents line by line.
|
48
|
+
# Open a +filename+ and pass its handler to the ACH::Reader object,
|
49
|
+
# which uses it as an enum to scan for ACH contents line by line.
|
50
|
+
#
|
51
|
+
# @param [String] filename
|
52
|
+
# @return [ACH::File]
|
50
53
|
def self.read(filename)
|
51
54
|
::File.open(filename) do |fh|
|
52
55
|
Reader.new(fh).to_ach
|
data/lib/ach/file/builder.rb
CHANGED
@@ -2,50 +2,70 @@ module ACH
|
|
2
2
|
# This module hosts all the methods required for building string representation of an ACH file,
|
3
3
|
# and writing it to an actual file in the filesystem. Included by the ACH::File.
|
4
4
|
module File::Builder
|
5
|
-
#
|
5
|
+
# Return amount of +batches+ in file.
|
6
|
+
#
|
7
|
+
# @return [Fixnum]
|
6
8
|
def batch_count
|
7
9
|
batches.length
|
8
10
|
end
|
9
11
|
|
10
|
-
#
|
12
|
+
# Return amount of blocks, used in count. This amount is based on <tt>blocking factor</tt>,
|
11
13
|
# which is usually equals to 10, and on overall amount of records in a file. Return value
|
12
14
|
# represents the least amount of blocks taken by records in file.
|
15
|
+
#
|
16
|
+
# @return [Fixnum]
|
13
17
|
def block_count
|
14
18
|
(record_count.to_f / Constants::BLOCKING_FACTOR).ceil
|
15
19
|
end
|
16
20
|
|
17
|
-
#
|
21
|
+
# Return total amount of +entry+ and +addenda+ records of all batches within file.
|
22
|
+
#
|
23
|
+
# @return [Fixnum]
|
18
24
|
def file_entry_addenda_count
|
19
25
|
batches.map{ |batch| batch.entry_addenda_count }.inject(&:+) || 0
|
20
26
|
end
|
21
27
|
|
22
|
-
#
|
28
|
+
# Return sum of +entry_hash+ values of all batches within self.
|
29
|
+
#
|
30
|
+
# @return [Fixnum]
|
23
31
|
def entry_hash
|
24
32
|
batch_sum_of(:entry_hash)
|
25
33
|
end
|
26
34
|
|
27
|
-
#
|
35
|
+
# Return sum of +total_debit_amount+ values of all batches within +self+.
|
36
|
+
#
|
37
|
+
# @return [Fixnum]
|
28
38
|
def total_debit_amount
|
29
39
|
batch_sum_of(:total_debit_amount)
|
30
40
|
end
|
31
41
|
|
32
|
-
#
|
42
|
+
# Return sum of +total_credit_amount+ values of all batches within +self+.
|
43
|
+
#
|
44
|
+
# @return [Fixnum]
|
33
45
|
def total_credit_amount
|
34
46
|
batch_sum_of(:total_credit_amount)
|
35
47
|
end
|
36
48
|
|
37
|
-
#
|
38
|
-
# to a string and joining the result by
|
49
|
+
# Return a complete string representation of an ACH file by converting each
|
50
|
+
# interval record to a string and joining the result by
|
51
|
+
# +ACH::Constants::ROWS_DELIMITER+.
|
52
|
+
#
|
53
|
+
# @return [String]
|
39
54
|
def to_s!
|
40
55
|
to_ach.map(&:to_s!).join(Constants::ROWS_DELIMITER)
|
41
56
|
end
|
42
57
|
|
43
|
-
#
|
58
|
+
# Return total amount of records hosted by a file.
|
59
|
+
#
|
60
|
+
# @return [Fixnum]
|
44
61
|
def record_count
|
45
62
|
2 + batch_count * 2 + file_entry_addenda_count
|
46
63
|
end
|
47
64
|
|
48
|
-
#
|
65
|
+
# Write string representation of self to passed +filename+.
|
66
|
+
#
|
67
|
+
# @param [String] filename
|
68
|
+
# @return [::File]
|
49
69
|
def write(filename)
|
50
70
|
return false unless valid?
|
51
71
|
::File.open(filename, 'w') do |fh|
|
@@ -53,29 +73,38 @@ module ACH
|
|
53
73
|
end
|
54
74
|
end
|
55
75
|
|
56
|
-
# Helper method for calculating different properties of batches within file
|
76
|
+
# Helper method for calculating different properties of batches within file.
|
77
|
+
#
|
78
|
+
# @param [String] meth
|
79
|
+
# @return [Fixnum]
|
57
80
|
def batch_sum_of(meth)
|
58
81
|
batches.map(&meth).compact.inject(&:+)
|
59
82
|
end
|
60
83
|
private :batch_sum_of
|
61
84
|
|
62
|
-
#
|
85
|
+
# Return well-fetched array of all ACH records in the file, appending proper
|
63
86
|
# amount, based on number of blocks, of tail records to it.
|
87
|
+
#
|
88
|
+
# @return [Array<ACH::Record::Base>]
|
64
89
|
def to_ach
|
65
90
|
head = [ header ]
|
66
91
|
head.unshift(transmission_header) if have_transmission_header?
|
67
92
|
head + batches.map(&:to_ach).flatten + [control] + tail
|
68
93
|
end
|
69
94
|
|
70
|
-
#
|
95
|
+
# Return array of ACH::Record::Tail records, based on +tails_count+.
|
96
|
+
#
|
97
|
+
# @return [Array<ACH::Record::Tail>]
|
71
98
|
def tail
|
72
|
-
[
|
99
|
+
[Record::Tail.new] * tails_count
|
73
100
|
end
|
74
101
|
|
75
|
-
#
|
102
|
+
# Return amount of ACH::Record::Tail records, required to append to
|
76
103
|
# string representation of a file to match proper amount of blocks.
|
104
|
+
#
|
105
|
+
# @return [Fixnum]
|
77
106
|
def tails_count
|
78
107
|
block_count * Constants::BLOCKING_FACTOR - record_count
|
79
108
|
end
|
80
109
|
end
|
81
|
-
end
|
110
|
+
end
|
data/lib/ach/file/reader.rb
CHANGED
@@ -7,10 +7,16 @@ module ACH
|
|
7
7
|
class File::Reader
|
8
8
|
include Constants
|
9
9
|
|
10
|
+
# Initialize reader.
|
11
|
+
#
|
12
|
+
# @param [#each] enum
|
10
13
|
def initialize(enum)
|
11
14
|
@enum = enum
|
12
15
|
end
|
13
16
|
|
17
|
+
# Build a {ACH::File} object using data obtained from +@enum+ source.
|
18
|
+
#
|
19
|
+
# @return [ACH::File]
|
14
20
|
def to_ach
|
15
21
|
header_line, batch_lines, control_line = ach_data
|
16
22
|
|
@@ -39,6 +45,9 @@ module ACH
|
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
48
|
+
# Process the source and return header batch and control records.
|
49
|
+
#
|
50
|
+
# @return [Array<ACH::Record::Base, Array>]
|
42
51
|
def ach_data
|
43
52
|
process! unless processed?
|
44
53
|
|
@@ -46,6 +55,10 @@ module ACH
|
|
46
55
|
end
|
47
56
|
private :ach_data
|
48
57
|
|
58
|
+
# Process each line by iterating over strings in +@enum+ source one by one and
|
59
|
+
# generating ACH information corresponding to record line.
|
60
|
+
#
|
61
|
+
# @return [true]
|
49
62
|
def process!
|
50
63
|
each_line do |record_type, line|
|
51
64
|
case record_type
|
@@ -68,11 +81,16 @@ module ACH
|
|
68
81
|
end
|
69
82
|
private :process!
|
70
83
|
|
84
|
+
# Return +true+ if +@enum+ has been processed.
|
85
|
+
#
|
86
|
+
# @return [Boolean]
|
71
87
|
def processed?
|
72
88
|
!!@processed
|
73
89
|
end
|
74
90
|
private :processed?
|
75
91
|
|
92
|
+
# Iterate over +enum+ and yield first symbol of the string as
|
93
|
+
# record type and string itself.
|
76
94
|
def each_line
|
77
95
|
@enum.each do |line|
|
78
96
|
yield line[0..0].to_i, line.chomp
|
@@ -80,21 +98,34 @@ module ACH
|
|
80
98
|
end
|
81
99
|
private :each_line
|
82
100
|
|
101
|
+
# Return array of batches in hash representation.
|
102
|
+
#
|
103
|
+
# @return [Array<Hash>]
|
83
104
|
def batches
|
84
105
|
@batches ||= []
|
85
106
|
end
|
86
107
|
private :batches
|
87
108
|
|
109
|
+
# Add a new hash representation of a batch to the batch list
|
110
|
+
# to be filled out during processing.
|
111
|
+
#
|
112
|
+
# @return [Array<Hash>]
|
88
113
|
def initialize_batch!
|
89
114
|
batches << {:entries => [], :addendas => {}}
|
90
115
|
end
|
91
116
|
private :initialize_batch!
|
92
117
|
|
118
|
+
# Current (the last one) batch to fill.
|
119
|
+
#
|
120
|
+
# @return [Hash]
|
93
121
|
def current_batch
|
94
122
|
batches.last
|
95
123
|
end
|
96
124
|
private :current_batch
|
97
125
|
|
126
|
+
# Current (the last one) entry string of the last batch that was processed.
|
127
|
+
#
|
128
|
+
# @return [String]
|
98
129
|
def current_entry
|
99
130
|
current_batch[:entries].last
|
100
131
|
end
|
@@ -22,8 +22,9 @@ module ACH
|
|
22
22
|
module File::TransmissionHeader
|
23
23
|
extend ActiveSupport::Concern
|
24
24
|
|
25
|
-
# Raised when (descendant of) ACH File tries to redeclare it's +TransmissionHeader
|
25
|
+
# Raised when (descendant of) ACH File tries to redeclare it's +TransmissionHeader+.
|
26
26
|
class RedefinedTransmissionHeaderError < RuntimeError
|
27
|
+
# Initialize error with descriptive message.
|
27
28
|
def initialize
|
28
29
|
super "TransmissionHeader record may be defined only once"
|
29
30
|
end
|
@@ -31,13 +32,19 @@ module ACH
|
|
31
32
|
|
32
33
|
# Raised when +TransmissionHeader+ is declared with no fields in it
|
33
34
|
class EmptyTransmissionHeaderError < RuntimeError
|
35
|
+
# Initialize error with descriptive message.
|
34
36
|
def initialize
|
35
37
|
super "Transmission_header should declare it's fields"
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
41
|
+
# Class macros.
|
39
42
|
module ClassMethods
|
40
|
-
# Defines and declares +TransmissionHeader+ class within scope of +self
|
43
|
+
# Defines and declares +TransmissionHeader+ class within scope of +self+.
|
44
|
+
#
|
45
|
+
# @return [Boolean]
|
46
|
+
# @raise [RedefinedTransmissionHeaderError]
|
47
|
+
# @raise [EmptyTransmissionHeaderError]
|
41
48
|
def transmission_header(&block)
|
42
49
|
raise RedefinedTransmissionHeaderError if have_transmission_header?
|
43
50
|
|
@@ -50,27 +57,34 @@ module ACH
|
|
50
57
|
end
|
51
58
|
|
52
59
|
# Returns +true+ if +TransmissionHeader+ is defined within scope of the class.
|
60
|
+
#
|
61
|
+
# @return [Boolean]
|
53
62
|
def have_transmission_header?
|
54
63
|
@have_transmission_header
|
55
64
|
end
|
56
65
|
end
|
57
66
|
|
58
67
|
# Helper instance method. Returns +true+ if +TransmissionHeader+ is defined within
|
59
|
-
# scope of it's class
|
68
|
+
# scope of it's class.
|
69
|
+
#
|
70
|
+
# @return [Boolean]
|
60
71
|
def have_transmission_header?
|
61
72
|
self.class.have_transmission_header?
|
62
73
|
end
|
63
74
|
|
64
75
|
# Builds +TransmissionHeader+ record for self. Yields it to +block+, if passed.
|
65
76
|
# Returns nil if no +TransmissionHeader+ is defined within scope of class.
|
66
|
-
|
77
|
+
#
|
78
|
+
# @param [Hash] fields
|
79
|
+
# @return [ACH::File::TransmissionHeader]
|
80
|
+
def transmission_header(fields = {})
|
67
81
|
return nil unless have_transmission_header?
|
68
82
|
|
69
83
|
merged_fields = fields_for(self.class::TransmissionHeader).merge(fields)
|
70
84
|
|
71
85
|
@transmission_header ||= self.class::TransmissionHeader.new(merged_fields)
|
72
86
|
@transmission_header.tap do |head|
|
73
|
-
head.instance_eval(&
|
87
|
+
head.instance_eval(&Proc.new) if block_given?
|
74
88
|
end
|
75
89
|
end
|
76
90
|
end
|
data/lib/ach/formatter.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module ACH
|
2
|
-
#
|
2
|
+
# Ensures that every field is formatted with its own rule.
|
3
3
|
# Rules are defined in ACH::RULES constant.
|
4
4
|
#
|
5
5
|
# == Rule Format
|
@@ -80,40 +80,62 @@ module ACH
|
|
80
80
|
:entry_details_sequence_num => '->7'
|
81
81
|
}
|
82
82
|
|
83
|
-
#
|
83
|
+
# Return +true+ if +field_name+ is one of the keys of +RULES+.
|
84
|
+
#
|
85
|
+
# @param [Symbol] field_name
|
86
|
+
# @return [Boolean]
|
84
87
|
def self.defined?(field_name)
|
85
88
|
RULES.key?(field_name)
|
86
89
|
end
|
87
90
|
|
88
|
-
#
|
91
|
+
# Add a +field+ with corresponding +format+ to +RULES+.
|
92
|
+
#
|
93
|
+
# @param [Symbol] field
|
94
|
+
# @param [String] format
|
95
|
+
# @return [Hash]
|
89
96
|
def self.define(field, format)
|
90
97
|
RULES[field] = format
|
91
98
|
end
|
92
99
|
|
93
|
-
# If missing method name is one of the defined rules,
|
94
|
-
# and the rest of arguments to the +format+ method
|
100
|
+
# If missing method name is one of the defined rules, pass its name
|
101
|
+
# and the rest of arguments to the +format+ method.
|
102
|
+
#
|
103
|
+
# @param [Symbol] meth
|
104
|
+
# @param [*Object] args
|
95
105
|
def self.method_missing(meth, *args)
|
96
106
|
self.defined?(meth) ? format(meth, *args) : super
|
97
107
|
end
|
98
108
|
|
99
|
-
#
|
109
|
+
# Format the +value+ using the rule defined by +field_name+ format.
|
110
|
+
#
|
111
|
+
# @param [Symbol] field_name
|
112
|
+
# @param [String] value
|
113
|
+
# @return [String]
|
100
114
|
def self.format(field_name, value)
|
101
115
|
rule_for_field(field_name).call(value)
|
102
116
|
end
|
103
117
|
|
104
|
-
#
|
105
|
-
# definition of a +field
|
118
|
+
# Return ACH::Formatter::Rule rule, built from the corresponding format
|
119
|
+
# definition of a +field+.
|
120
|
+
#
|
121
|
+
# @param [Symbol] field
|
122
|
+
# @return [ACH::Formatter::Rule]
|
106
123
|
def self.rule_for_field(field)
|
107
124
|
compiled_rules[field] ||= Rule.new(RULES[field])
|
108
125
|
end
|
109
126
|
|
110
|
-
#
|
127
|
+
# Return a hash of all rules that have been built at the moment of method call.
|
128
|
+
#
|
129
|
+
# @return [Hash]
|
111
130
|
def self.compiled_rules
|
112
131
|
@compiled_rules ||= {}
|
113
132
|
end
|
114
133
|
|
115
|
-
#
|
116
|
-
# that will correspond to passed +fields+ parameter. Used for ACH reading purposes
|
134
|
+
# Return a regular expression that can be used to split string into matched parts,
|
135
|
+
# that will correspond to passed +fields+ parameter. Used for ACH reading purposes.
|
136
|
+
#
|
137
|
+
# @param [Array<Symbol>] fields
|
138
|
+
# @return [Regexp]
|
117
139
|
def self.matcher_for(fields)
|
118
140
|
/^#{fields.map{ |f| "(.{#{rule_for_field(f).length}})" }.join}$/
|
119
141
|
end
|
data/lib/ach/formatter/rule.rb
CHANGED
@@ -8,9 +8,12 @@ module ACH
|
|
8
8
|
|
9
9
|
attr_reader :length
|
10
10
|
|
11
|
-
#
|
12
|
-
# values, such as width, justification, etc.
|
13
|
-
#
|
11
|
+
# Initialize the instance with the formatting data. Parses the given string
|
12
|
+
# for formatting values, such as width, justification, etc. With the result,
|
13
|
+
# it builds a Proc object to be used to format the given string according
|
14
|
+
# to the formatting rule.
|
15
|
+
#
|
16
|
+
# @param [ACH::Formatter::Rule] rule
|
14
17
|
def initialize(rule)
|
15
18
|
just, width, pad, transf = rule.match(RULE_PARSER_REGEX)[1..-1]
|
16
19
|
@length = width.to_i
|
data/lib/ach/record/base.rb
CHANGED
@@ -27,37 +27,56 @@ module ACH
|
|
27
27
|
class Base
|
28
28
|
include Validations
|
29
29
|
include Constants
|
30
|
-
|
30
|
+
|
31
31
|
# Raises when unknown field passed to ACH::Record::Base.fields method.
|
32
32
|
class UnknownFieldError < ArgumentError
|
33
|
+
# Initialize error with descriptive message.
|
34
|
+
#
|
35
|
+
# @param [Symbol] field
|
36
|
+
# @param [String] class_name
|
33
37
|
def initialize(field, class_name)
|
34
38
|
super "Unrecognized field '#{field}' in class #{class_name}"
|
35
39
|
end
|
36
40
|
end
|
37
|
-
|
41
|
+
|
38
42
|
# Raises when value of record's field is not specified and there is no
|
39
43
|
# default value.
|
40
44
|
class EmptyFieldError < ArgumentError
|
45
|
+
# Initialize error with descriptive message.
|
46
|
+
#
|
47
|
+
# @param [Symbol] field
|
48
|
+
# @param [ACH::Record::Base] record
|
41
49
|
def initialize(field, record)
|
42
50
|
super "Empty field '#{field}' for #{record}"
|
43
51
|
end
|
44
52
|
end
|
45
|
-
|
46
|
-
#
|
53
|
+
|
54
|
+
# Specify the fields of the record. Order is important. All fields
|
47
55
|
# must be declared in ACH::Formatter +RULES+. See class description
|
48
|
-
# for example
|
56
|
+
# for example.
|
57
|
+
#
|
58
|
+
# @param [*Symbol] field_names
|
59
|
+
# @return [Array<Symbol>]
|
49
60
|
def self.fields(*field_names)
|
50
61
|
return @fields if field_names.empty?
|
51
62
|
@fields = field_names
|
52
63
|
@fields.each{ |field| define_field_methods(field) }
|
53
64
|
end
|
54
|
-
|
55
|
-
#
|
65
|
+
|
66
|
+
# Set default values for fields. See class description for example.
|
67
|
+
#
|
68
|
+
# @param [Hash, nil] default_values
|
69
|
+
# @return [Hash]
|
56
70
|
def self.defaults(default_values = nil)
|
57
71
|
return @defaults if default_values.nil?
|
58
72
|
@defaults = default_values.freeze
|
59
73
|
end
|
60
74
|
|
75
|
+
# Define accessor methods for a given field name if it can be found
|
76
|
+
# in the keys of {ACH::Formatter::RULES}.
|
77
|
+
#
|
78
|
+
# @param [Symbol] field
|
79
|
+
# @raise [UnknownFieldError]
|
61
80
|
def self.define_field_methods(field)
|
62
81
|
raise UnknownFieldError.new(field, name) unless Formatter::RULES.key?(field)
|
63
82
|
define_method(field) do |*args|
|
@@ -69,20 +88,31 @@ module ACH
|
|
69
88
|
end
|
70
89
|
private_class_method :define_field_methods
|
71
90
|
|
91
|
+
# Build a new instance of a record from its string representation.
|
92
|
+
#
|
93
|
+
# @param [String] string
|
94
|
+
# @return [ACH::Record::Base]
|
72
95
|
def self.from_s(string)
|
73
96
|
field_matcher_regexp = Formatter.matcher_for(fields)
|
74
97
|
new Hash[*fields.zip(string.match(field_matcher_regexp)[1..-1]).flatten]
|
75
98
|
end
|
76
|
-
|
77
|
-
|
99
|
+
|
100
|
+
# Initialize object with field values. If block is given, it will be
|
101
|
+
# evaluated in context of isntance.
|
102
|
+
#
|
103
|
+
# @param [Hash] fields
|
104
|
+
def initialize(fields = {})
|
78
105
|
defaults.each do |key, value|
|
79
106
|
self.fields[key] = Proc === value ? value.call : value
|
80
107
|
end
|
81
108
|
self.fields.merge!(fields)
|
82
|
-
instance_eval(&
|
109
|
+
instance_eval(&Proc.new) if block_given?
|
83
110
|
end
|
84
|
-
|
85
|
-
#
|
111
|
+
|
112
|
+
# Build a string from record object.
|
113
|
+
#
|
114
|
+
# @return [Stirng]
|
115
|
+
# @raise [EmptyFieldError]
|
86
116
|
def to_s!
|
87
117
|
self.class.fields.map do |name|
|
88
118
|
raise EmptyFieldError.new(name, self) if @fields[name].nil?
|
@@ -90,16 +120,26 @@ module ACH
|
|
90
120
|
end.join
|
91
121
|
end
|
92
122
|
|
93
|
-
#
|
123
|
+
# Return a hash where key is the field's name and value is the field's value.
|
124
|
+
#
|
125
|
+
# @return [Hash]
|
94
126
|
def fields
|
95
127
|
@fields ||= {}
|
96
128
|
end
|
97
129
|
|
130
|
+
# Return field default values defined in class.
|
131
|
+
#
|
132
|
+
# @return [Hash]
|
98
133
|
def defaults
|
99
134
|
self.class.defaults
|
100
135
|
end
|
101
136
|
private :defaults
|
102
|
-
|
137
|
+
|
138
|
+
# Delegate bracket-assignment to +fields+.
|
139
|
+
#
|
140
|
+
# @param [Symbol] name
|
141
|
+
# @param [String] val
|
142
|
+
# @return [String]
|
103
143
|
def []=(name, val)
|
104
144
|
fields[name] = val
|
105
145
|
end
|