REX12 0.1.4 → 0.2.0
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/.gitignore +1 -0
- data/.rspec +0 -1
- data/README.md +45 -10
- data/lib/REX12/version.rb +1 -1
- data/lib/rex12.rb +83 -1
- data/lib/rex12/document.rb +8 -102
- data/lib/rex12/element.rb +15 -14
- data/lib/rex12/element_with_subelements.rb +40 -0
- data/lib/rex12/isa_segment.rb +17 -0
- data/lib/rex12/parse_error.rb +1 -9
- data/lib/rex12/parser.rb +235 -0
- data/lib/rex12/segment.rb +19 -17
- data/lib/rex12/subelement.rb +14 -0
- data/rex12.gemspec +1 -0
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c77556e632df1d2847e884f9d04c400824821c50
|
4
|
+
data.tar.gz: cafe395798bdf26ab40f7e82321c78aab1091ab7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 29c7443111f823fe2c72d2a2715870745ef174c41681be3317455b4c438366885e785712fb780ab0e18b13736dbf63d7c3503361fe53bf8eb8fb65e3eefa7f8a
|
7
|
+
data.tar.gz: 7525a909eee3c42cf34d035084698376f09be22f77c6a680d4b4183ef5a5844ec4d30873248497de7d2a36e47a9001607233a8e763e619737b954a3b0b8170f4
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# REX12
|
4
4
|
|
5
|
-
|
5
|
+
A simple gem to read EDI data in the ASNI X.12 format.
|
6
6
|
|
7
7
|
|
8
8
|
## Installation
|
@@ -15,7 +15,7 @@ gem 'rex12'
|
|
15
15
|
|
16
16
|
And then execute:
|
17
17
|
|
18
|
-
$ bundle
|
18
|
+
$ bundle install
|
19
19
|
|
20
20
|
Or install it yourself as:
|
21
21
|
|
@@ -23,17 +23,48 @@ Or install it yourself as:
|
|
23
23
|
|
24
24
|
## Usage
|
25
25
|
|
26
|
-
|
26
|
+
To simply read all data from an EDI data file into a REX12::Document...
|
27
|
+
|
28
|
+
*NOTE - All public REX12 methods for parsing EDI data (REX12#document, REX12#each_segment, and REX12#each_transaction) can receive a string path, a Pathname, an IO, or Tempfile object as the means for supplying your EDI data to the methods*
|
27
29
|
|
28
30
|
```ruby
|
29
31
|
require 'rex12'
|
30
32
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
33
|
+
document = REX12.document 'path/to/file.edi'
|
34
|
+
|
35
|
+
# yielded segments are of type REX12::Segment
|
36
|
+
document.segments.each do |segment|
|
37
|
+
if segment.isa_segment?
|
38
|
+
# Print out the sender from the ISA segment
|
39
|
+
puts "Reading an EDI document from '#{segment[8]}'"
|
40
|
+
else
|
41
|
+
if segment.segment_type == "DTM"
|
42
|
+
# Look for a specific date
|
43
|
+
if segment[1] == "056"
|
44
|
+
puts "Sender sent an '056' date of #{segment[2]}"
|
45
|
+
else
|
46
|
+
puts "This is not the date you're looking for."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
To get each individual transaction (.ie each individual unit defined between ST - SE segment pairs) yielded to your code:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
File.open("/path/to/file.edi", "r") do |file|
|
57
|
+
REX12.each_transaction(file) do |transaction|
|
58
|
+
# transaction is type REX12::Transaction
|
59
|
+
edi_type = transaction.segments[0][1]
|
60
|
+
if edi_type == "850"
|
61
|
+
process_purchase_order(transaction)
|
62
|
+
elsif edi_type == "856"
|
63
|
+
process_shipment(transaction)
|
64
|
+
else
|
65
|
+
puts "Not sure what to do with #{edi_type} documents."
|
66
|
+
end
|
67
|
+
end
|
37
68
|
end
|
38
69
|
```
|
39
70
|
|
@@ -41,7 +72,11 @@ end
|
|
41
72
|
|
42
73
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
43
74
|
|
44
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
75
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
76
|
+
|
77
|
+
## Releases (Repository Owners Only)
|
78
|
+
|
79
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
45
80
|
|
46
81
|
## Contributing
|
47
82
|
|
data/lib/REX12/version.rb
CHANGED
data/lib/rex12.rb
CHANGED
@@ -4,7 +4,89 @@ require "rex12/element"
|
|
4
4
|
require "rex12/segment"
|
5
5
|
require "rex12/document"
|
6
6
|
require 'rex12/transaction'
|
7
|
+
require 'rex12/element_with_subelements'
|
8
|
+
require 'rex12/isa_segment'
|
9
|
+
require 'rex12/parser'
|
10
|
+
require 'rex12/subelement'
|
7
11
|
|
8
12
|
module REX12
|
9
|
-
|
13
|
+
|
14
|
+
# Reads EDI data from the given IO object or file path and returns or yields an enumerator returning every "transaction" in the EDI data.
|
15
|
+
#
|
16
|
+
# @param [String, IO] - If a String, expected to be the path to a file that can be read. If IO, needs to be able to be read from AND be able to be rewound.
|
17
|
+
#
|
18
|
+
# @return [Enumerator<REX12::Transaction>] - If no block given, returns an Enumerator for iterating over the Transactions in the file
|
19
|
+
# @yield [REX12::Transaction] - Progressively yields each transaction in the given EDI data.
|
20
|
+
def self.each_transaction file_or_io
|
21
|
+
val = nil
|
22
|
+
with_io(file_or_io) do |io|
|
23
|
+
if block_given?
|
24
|
+
val = parser.each_transaction io, &Proc.new
|
25
|
+
else
|
26
|
+
val = parser.each_transaction io
|
27
|
+
end
|
28
|
+
end
|
29
|
+
val
|
30
|
+
end
|
31
|
+
|
32
|
+
# Reads EDI data from the given IO object or file path and returns or yields each EDI segement encountered in the data.
|
33
|
+
# The ISA segment is returned/yielded as a specialized REX12::IsaSegment subclass of REX12::Segment.
|
34
|
+
#
|
35
|
+
# Example:
|
36
|
+
#
|
37
|
+
# REX12.each_segment("path/to/file.edi") do |segment|
|
38
|
+
# if segment.isa_segment?
|
39
|
+
# # Do something w/ the ISA
|
40
|
+
# else
|
41
|
+
# # Do something w/ some other segment type
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
#
|
46
|
+
#
|
47
|
+
# @param [String, IO] - If a String, expected to be the path to a file that can be read. If IO, needs to be able to be read from AND be able to be rewound.
|
48
|
+
#
|
49
|
+
# @return [Enumerator<REX12::Segment>] - If no block given, returns an Enumerator for iterating over the Segements in the file
|
50
|
+
# @yield [REX12::Segment] - Progressively yields each segment in the given EDI data.
|
51
|
+
def self.each_segment file_or_io
|
52
|
+
val = nil
|
53
|
+
with_io(file_or_io) do |io|
|
54
|
+
if block_given?
|
55
|
+
val = parser.each_segment io, &Proc.new
|
56
|
+
else
|
57
|
+
val = parser.each_segment io
|
58
|
+
end
|
59
|
+
end
|
60
|
+
val
|
61
|
+
end
|
62
|
+
|
63
|
+
# Reads EDI data from the given IO object or file path and returns a REX12::Document representing every segment from the given datasource.
|
64
|
+
#
|
65
|
+
# @return [REX12::Document] - a REX12 object representing an ordered enumeration of all the segments in the given data source.
|
66
|
+
def self.document file_or_io
|
67
|
+
val = nil
|
68
|
+
with_io(file_or_io) do |io|
|
69
|
+
val = parser.document io
|
70
|
+
end
|
71
|
+
val
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.parser
|
75
|
+
REX12::Parser.new
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.with_io file_or_io
|
79
|
+
# If we got an actual IO or Tempfile, then use them as given, else we'll assume the
|
80
|
+
# value is a path and
|
81
|
+
if REX12::Parser.required_io_methods.map {|m| file_or_io.respond_to?(m) }.all?
|
82
|
+
yield file_or_io
|
83
|
+
else
|
84
|
+
path = file_or_io.respond_to?(:path) ? file_or_io.path : file_or_io.to_s
|
85
|
+
|
86
|
+
File.open(path, "r") {|f| yield f }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
private_class_method :parser, :with_io
|
10
92
|
end
|
data/lib/rex12/document.rb
CHANGED
@@ -1,119 +1,25 @@
|
|
1
|
-
#
|
1
|
+
# Represents a full (raw) EDI file.
|
2
2
|
#
|
3
3
|
# currently, the full text of the file is read into memory,
|
4
4
|
# but if you use the block form of the methods, then the subsequent ruby objects
|
5
5
|
# are created within the block loops so they can be garbage collected
|
6
6
|
module REX12; class Document
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
# @return (see #parse)
|
11
|
-
def self.read path_or_io
|
12
|
-
if block_given?
|
13
|
-
parse(file_text(path_or_io)) {|s| yield s}
|
14
|
-
return nil
|
15
|
-
else
|
16
|
-
return parse(file_text(path_or_io))
|
17
|
-
end
|
8
|
+
def initialize segments
|
9
|
+
@segments = segments.freeze
|
18
10
|
end
|
19
11
|
|
20
|
-
|
21
|
-
#
|
22
|
-
# @return [Array<REX12::Segment>,nil] all segments or nil for block form
|
23
|
-
def self.parse text
|
24
|
-
validate_isa(text)
|
25
|
-
element_separator = text[3]
|
26
|
-
segment_terminator = determine_segment_terminator(text)
|
27
|
-
sub_element_separator = text[104]
|
28
|
-
r = []
|
29
|
-
|
30
|
-
text.split(segment_terminator).each_with_index do |seg_text,pos|
|
31
|
-
next if seg_text.length == 0
|
32
|
-
seg = REX12::Segment.new(seg_text,element_separator,sub_element_separator,pos)
|
33
|
-
if block_given?
|
34
|
-
yield seg
|
35
|
-
else
|
36
|
-
r << seg
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
12
|
+
def segments
|
40
13
|
if block_given?
|
14
|
+
@segments.each {|segment| yield segment}
|
41
15
|
return nil
|
42
16
|
else
|
43
|
-
|
17
|
+
@segments.to_enum { @segments.length }
|
44
18
|
end
|
45
19
|
end
|
46
20
|
|
47
|
-
|
48
|
-
|
49
|
-
#
|
50
|
-
# @return [Array<REX12::Transaction>,nil] all transactions or nil for block form
|
51
|
-
def self.each_transaction text
|
52
|
-
isa = nil
|
53
|
-
current_gs = nil
|
54
|
-
current_segments = []
|
55
|
-
transactions = []
|
56
|
-
|
57
|
-
parse(text) do |segment|
|
58
|
-
segment_type = segment.segment_type
|
59
|
-
case segment_type
|
60
|
-
when "ISA"
|
61
|
-
isa = segment
|
62
|
-
when "GS"
|
63
|
-
current_gs = segment
|
64
|
-
when "IEA", "GE"
|
65
|
-
# Do nothing, we don't care about the trailer segments
|
66
|
-
else
|
67
|
-
current_segments << segment
|
68
|
-
|
69
|
-
# If we found the transaction trailer, it means we can take all the current segments we have and process them
|
70
|
-
if segment_type == "SE"
|
71
|
-
transaction = REX12::Transaction.new(isa, current_gs, current_segments)
|
72
|
-
|
73
|
-
if block_given?
|
74
|
-
yield transaction
|
75
|
-
else
|
76
|
-
transactions << transaction
|
77
|
-
end
|
78
|
-
|
79
|
-
current_gs = nil
|
80
|
-
current_segments = []
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
block_given? ? nil : transactions
|
21
|
+
def [] x
|
22
|
+
@segments[x]
|
86
23
|
end
|
87
24
|
|
88
|
-
def self.determine_segment_terminator text
|
89
|
-
# Technically, allowing multi-character terminators is not valid EDI, but you
|
90
|
-
# see it happen ALL the time with EDI that's been hand editted from a client (especially on Windows).
|
91
|
-
# It's a valid enough use-case that we're accounting for it.
|
92
|
-
|
93
|
-
# no segement terminator, just CRLF
|
94
|
-
return "\r\n" if text[105..106]=="\r\n"
|
95
|
-
# no segement terminator, just CR or LF
|
96
|
-
return text[105] if ["\r","\n"].include?(text[105])
|
97
|
-
# segment terminator without CR or LF
|
98
|
-
return text[105] if text[106]=='G'
|
99
|
-
# segment terminator with CRLF
|
100
|
-
return text[105..107] if text[106..107]=="\r\n"
|
101
|
-
return text[105..106] if ["\r","\n"].include?(text[106]) && text[107]=="G"
|
102
|
-
raise REX12::ParseError, "Invalid ISA / GS segements. Could not determine segment terminator."
|
103
|
-
end
|
104
|
-
private_class_method :determine_segment_terminator
|
105
|
-
|
106
|
-
def self.validate_isa text
|
107
|
-
raise REX12::ParseError, "EDI file must be at least 191 characters to include a valid envelope." unless text.length > 191
|
108
|
-
str = text[0..2]
|
109
|
-
raise REX12::ParseError, "First 3 characters must be ISA. They were '#{str}'." unless str=='ISA'
|
110
|
-
return
|
111
|
-
end
|
112
|
-
private_class_method :validate_isa
|
113
|
-
|
114
|
-
# Allow reading from an object that responds to #read or assume the given param is a string file path and read with IO
|
115
|
-
def self.file_text path_or_io
|
116
|
-
path_or_io.respond_to?(:read) ? path_or_io.read : IO.read(path_or_io)
|
117
|
-
end
|
118
|
-
private_class_method :file_text
|
119
25
|
end; end
|
data/lib/rex12/element.rb
CHANGED
@@ -9,25 +9,26 @@ module REX12; class Element
|
|
9
9
|
# @param value [String] base text value of the element
|
10
10
|
# @param sub_element_separator [String] character that should be used to split sub elements
|
11
11
|
# @param position [Integer] zero based position of this element in its parent segment
|
12
|
-
def initialize value,
|
13
|
-
@value = value
|
14
|
-
@
|
15
|
-
@position = position
|
12
|
+
def initialize value, position
|
13
|
+
@value = value.freeze
|
14
|
+
@position = position.freeze
|
16
15
|
end
|
17
16
|
|
18
17
|
# @return [true, false] does the element have sub elements
|
19
18
|
def sub_elements?
|
20
|
-
|
19
|
+
false
|
21
20
|
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
def sub_elements
|
26
|
-
r = @value.split(@sub_element_separator)
|
27
|
-
if block_given?
|
28
|
-
r.each {|se| yield se}
|
29
|
-
return nil
|
30
|
-
end
|
31
|
-
return r
|
22
|
+
def to_s
|
23
|
+
value
|
32
24
|
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
def value= v
|
28
|
+
@value = v
|
29
|
+
end
|
30
|
+
|
31
|
+
def position= pos
|
32
|
+
@position = pos
|
33
|
+
end
|
33
34
|
end; end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module REX12; class ElementWithSubElements < REX12::Element
|
2
|
+
|
3
|
+
attr_reader :sub_element_separator
|
4
|
+
|
5
|
+
def initialize sub_elements, position, separator
|
6
|
+
super(nil, position)
|
7
|
+
@sub_elements = sub_elements.freeze
|
8
|
+
@sub_element_separator = separator
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [true, false] does the element have sub elements
|
12
|
+
def sub_elements?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get all sub elements as an array or yield them to a block
|
17
|
+
# @return [Array<SubElement>, nil]
|
18
|
+
def sub_elements
|
19
|
+
if block_given?
|
20
|
+
@sub_elements.each {|se| yield se }
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
|
24
|
+
return @sub_elements.to_enum { @sub_elements.length }
|
25
|
+
end
|
26
|
+
|
27
|
+
def sub_element index
|
28
|
+
@sub_elements[index]
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
@sub_elements.map(&:to_s).join(@sub_element_separator)
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](index)
|
36
|
+
sub_el = sub_element(index)
|
37
|
+
sub_el.nil? ? nil : sub_el.value
|
38
|
+
end
|
39
|
+
|
40
|
+
end; end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module REX12; class IsaSegment < Segment
|
2
|
+
attr_reader :segment_terminator
|
3
|
+
attr_reader :element_delimiter
|
4
|
+
attr_reader :sub_element_separator
|
5
|
+
|
6
|
+
def initialize elements, position, segment_terminator, element_delimiter, sub_element_separator
|
7
|
+
super(elements, position)
|
8
|
+
@segment_terminator = segment_terminator.freeze
|
9
|
+
@element_delimiter = element_delimiter.freeze
|
10
|
+
@sub_element_separator = sub_element_separator.freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
def isa_segment?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
end; end;
|
data/lib/rex12/parse_error.rb
CHANGED
@@ -1,12 +1,4 @@
|
|
1
1
|
# Raised when there is an error parsing the syntax of the document
|
2
2
|
module REX12; class ParseError < StandardError
|
3
|
-
|
4
|
-
attr_reader :segment
|
5
|
-
# REX12::Element that was being parsed when error was generated (may be nil)
|
6
|
-
attr_reader :element
|
7
|
-
def initialize message, segment=nil, element=nil
|
8
|
-
super(message)
|
9
|
-
@segment = segment
|
10
|
-
@element = element
|
11
|
-
end
|
3
|
+
|
12
4
|
end; end
|
data/lib/rex12/parser.rb
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
# This classes sole purpose is to read an IO object containing EDI text and turn
|
2
|
+
# it into the REX12 component classes.
|
3
|
+
#
|
4
|
+
module REX12; class Parser
|
5
|
+
|
6
|
+
# Reads EDI data from the given IO object and returns a REX12::Document
|
7
|
+
# model of the whole EDI file
|
8
|
+
def document io
|
9
|
+
REX12::Document.new(each_segment(io).to_a)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Reads EDI data from the given IO object and returns an enumeration of the EDI segments as REX12::Segment objects.
|
13
|
+
# If a block is given, segments will be yielded.
|
14
|
+
|
15
|
+
# NOTE: The ISA line will be returned as a specialized REX12::IsaSegment class, which subclasses REX12::Segment
|
16
|
+
def each_segment io
|
17
|
+
metadata = parse_metadata io
|
18
|
+
segments = []
|
19
|
+
each_line(io, metadata) do |line_counter, segment|
|
20
|
+
if block_given?
|
21
|
+
yield segment
|
22
|
+
else
|
23
|
+
segments << segment
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
block_given? ? nil : segments.to_enum { segments.length }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Reads EDI data from the received IO object.
|
31
|
+
# If a block is given, yields all REX12::Transaction objects read from the EDI in sequence
|
32
|
+
# If no block is given, an Enumerator of REX12::Transactions is returned
|
33
|
+
def each_transaction io
|
34
|
+
metadata = parse_metadata io
|
35
|
+
|
36
|
+
# At this point we can read through the io "line" by "line" utilizing the segment_terminator as the linefeed character
|
37
|
+
# I'm not exactly sure what to use as the max line length here, so I'm just going to basically negate it by using
|
38
|
+
# a value of 1 million
|
39
|
+
isa_segment = nil
|
40
|
+
gs_segment = nil
|
41
|
+
segments = []
|
42
|
+
transactions = []
|
43
|
+
each_line(io, metadata) do |line_counter, segment|
|
44
|
+
|
45
|
+
if metadata.segment_markers[:isa] == segment.segment_type
|
46
|
+
isa_segment = segment
|
47
|
+
next
|
48
|
+
end
|
49
|
+
|
50
|
+
case segment.segment_type
|
51
|
+
when metadata.segment_markers[:gs]
|
52
|
+
gs_segment = segment
|
53
|
+
when metadata.segment_markers[:iea], metadata.segment_markers[:ge]
|
54
|
+
# Do nothing, we don't care about the trailer segments if we're procssing the data transaction by
|
55
|
+
# transaction...they do nothing for us except act as potential checksums for segment counts..which
|
56
|
+
# we're not bothering with
|
57
|
+
else
|
58
|
+
next if segment.segment_type.nil?
|
59
|
+
|
60
|
+
segments << segment
|
61
|
+
|
62
|
+
if segment.segment_type == metadata.segment_markers[:se]
|
63
|
+
transaction = REX12::Transaction.new(isa_segment, gs_segment, segments)
|
64
|
+
|
65
|
+
if block_given?
|
66
|
+
yield transaction
|
67
|
+
else
|
68
|
+
transactions << transaction
|
69
|
+
end
|
70
|
+
|
71
|
+
segments = []
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
block_given? ? nil : transactions.to_enum { transactions.length }
|
77
|
+
end
|
78
|
+
|
79
|
+
class DocumentMetadata
|
80
|
+
|
81
|
+
attr_reader :encoding, :segment_markers, :segment_terminator, :element_delimiter, :sub_element_separator
|
82
|
+
|
83
|
+
def initialize encoding, segment_markers, segment_terminator, element_delimiter, sub_element_separator
|
84
|
+
@encoding = encoding
|
85
|
+
@segment_markers = segment_markers.freeze
|
86
|
+
@segment_terminator = segment_terminator.freeze
|
87
|
+
@element_delimiter = element_delimiter.freeze
|
88
|
+
@sub_element_separator = sub_element_separator.freeze
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.required_io_methods
|
94
|
+
[:pos, :rewind, :readchar, :each_line]
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def each_line io, metadata
|
100
|
+
line_counter = -1
|
101
|
+
isa_seen = false
|
102
|
+
io.each_line(metadata.segment_terminator, 1_000_000).each do |segment_line|
|
103
|
+
next if segment_line.length == 0
|
104
|
+
|
105
|
+
line_counter += 1
|
106
|
+
|
107
|
+
# Strip the segment terminator off the line before we parse it
|
108
|
+
segment_line = segment_line[0..-(1 + metadata.segment_terminator.length)]
|
109
|
+
|
110
|
+
segment = (parse_edi_line(segment_line, line_counter, metadata))
|
111
|
+
|
112
|
+
if metadata.segment_markers[:isa] == segment.segment_type
|
113
|
+
raise "Invalid EDI. Only 1 ISA segment is allow per EDI file." if isa_seen
|
114
|
+
isa_seen = false
|
115
|
+
end
|
116
|
+
|
117
|
+
yield line_counter, segment
|
118
|
+
end
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_edi_line segment_line, line_counter, metadata
|
123
|
+
# Handle isa segments a little different
|
124
|
+
if segment_line.start_with?(metadata.segment_markers[:isa])
|
125
|
+
isa_segment = parse_isa(segment_line, line_counter, metadata)
|
126
|
+
else
|
127
|
+
segment = parse_line(segment_line, line_counter, metadata)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_line line, line_counter, metadata
|
132
|
+
elements = []
|
133
|
+
# Ruby's split function by default compresses elements together if there are no trailing positions, we don't want this here
|
134
|
+
# we want every position accounted for...hence the -1 argument as the limit value
|
135
|
+
# .ie NOT 'SLN*1****'.split('*') -> ["SLN", "1"] ---- We want ["SLN", "1", "", "", "", ""]
|
136
|
+
line.split(metadata.element_delimiter, -1).each_with_index do |element, index|
|
137
|
+
split_element = element.split(metadata.sub_element_separator, -1)
|
138
|
+
if split_element.length > 1
|
139
|
+
sub_elements = []
|
140
|
+
split_element.each_with_index {|v, x| sub_elements << REX12::SubElement.new(v, x) }
|
141
|
+
|
142
|
+
elements << REX12::ElementWithSubElements.new(sub_elements, index, metadata.sub_element_separator)
|
143
|
+
else
|
144
|
+
elements << REX12::Element.new(element, index)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
REX12::Segment.new elements, line_counter
|
149
|
+
end
|
150
|
+
|
151
|
+
def parse_isa line, line_counter, metadata
|
152
|
+
elements = []
|
153
|
+
split_segment = line.split(metadata.element_delimiter, -1)
|
154
|
+
split_segment.each_with_index do |element, index|
|
155
|
+
# There's no subelements in the isa
|
156
|
+
elements << REX12::Element.new(element, index)
|
157
|
+
end
|
158
|
+
|
159
|
+
REX12::IsaSegment.new elements, line_counter, metadata.segment_terminator, metadata.element_delimiter, metadata.sub_element_separator
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_metadata io
|
163
|
+
# Record the initial position so we can rewind back to it
|
164
|
+
initial_position = io.pos
|
165
|
+
|
166
|
+
# Read out 107 chars from the io object to determine ISA data
|
167
|
+
isa_chars = []
|
168
|
+
|
169
|
+
# Use readchar instead of bytes so that we're letting the IO stream handle any character
|
170
|
+
# encoding for us
|
171
|
+
|
172
|
+
106.times { isa_chars << io.readchar }
|
173
|
+
|
174
|
+
encoding = isa_chars[0].encoding
|
175
|
+
segment_markers = encoded_segment_markers(encoding)
|
176
|
+
# We should have a full isa segment now, interrogate it to determine the segment terminator, element separator and subelement separator
|
177
|
+
raise REX12::ParseError, "Invalid EDI. All EDI documents must start with an ISA segment." unless isa_chars[0..2].join == segment_markers[:isa]
|
178
|
+
|
179
|
+
element_delimiter = isa_chars[3]
|
180
|
+
segment_terminator = determine_segment_terminator(encoding, isa_chars, segment_markers, io)
|
181
|
+
sub_element_separator = isa_chars[104]
|
182
|
+
|
183
|
+
io.pos = initial_position
|
184
|
+
|
185
|
+
return REX12::Parser::DocumentMetadata.new(encoding, segment_markers, segment_terminator, element_delimiter, sub_element_separator)
|
186
|
+
rescue EOFError
|
187
|
+
raise REX12::ParseError, "Invalid EDI. All EDI documents must start with an ISA segment that is exactly 107 characters long - including the segment terminator."
|
188
|
+
end
|
189
|
+
|
190
|
+
def encoded_segment_markers document_encoding
|
191
|
+
{
|
192
|
+
isa: "ISA".encode(document_encoding),
|
193
|
+
gs: "GS".encode(document_encoding),
|
194
|
+
iea: "IEA".encode(document_encoding),
|
195
|
+
ge: "GE".encode(document_encoding),
|
196
|
+
st: "ST".encode(document_encoding),
|
197
|
+
se: "SE".encode(document_encoding)
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
def determine_segment_terminator encoding, isa_chars, markers, io
|
202
|
+
# Technically, allowing multi-character terminators is not valid EDI, but you
|
203
|
+
# see it happen ALL the time with EDI that's been hand editted from a client where \r\n is utilized.
|
204
|
+
# It's a valid enough use-case that we're accounting specifically for a terminator character and/or cr/lfs
|
205
|
+
terminator = isa_chars[105]
|
206
|
+
|
207
|
+
raise REX12::ParseError, "Invalid EDI. All EDI documents have a segment terminator character at position 106 of the ISA segment." if terminator.nil?
|
208
|
+
|
209
|
+
next_char = io.readchar
|
210
|
+
|
211
|
+
# If there's a single character terminator and the next char is the start of the GS segment, then everything is copacetic
|
212
|
+
# and no need to continue looking for more terminator characters
|
213
|
+
return terminator if markers[:gs][0] == next_char
|
214
|
+
|
215
|
+
cr = "\r".encode(encoding)
|
216
|
+
lf = "\n".encode(encoding)
|
217
|
+
|
218
|
+
if next_char == cr || next_char == lf
|
219
|
+
terminator << next_char
|
220
|
+
|
221
|
+
# The only valid char at this point we'll accept as part of the terminator is a linefeed
|
222
|
+
next_char = io.readchar
|
223
|
+
if next_char == lf
|
224
|
+
terminator << next_char
|
225
|
+
else
|
226
|
+
raise REX12::ParseError, "Invalid ISA segment. Could not determine segment terminator." unless markers[:gs][0] == next_char
|
227
|
+
end
|
228
|
+
elsif markers[:gs][0] != next_char
|
229
|
+
raise REX12::ParseError, "Invalid ISA segment. Could not determine segment terminator."
|
230
|
+
end
|
231
|
+
|
232
|
+
terminator
|
233
|
+
end
|
234
|
+
|
235
|
+
end; end;
|
data/lib/rex12/segment.rb
CHANGED
@@ -1,36 +1,38 @@
|
|
1
1
|
module REX12; class Segment
|
2
|
-
# @return [String] raw text of segment
|
3
|
-
attr_reader :value
|
4
|
-
|
5
2
|
# @return [Integer] zero based position in file
|
6
3
|
attr_reader :position
|
7
4
|
|
8
|
-
def initialize
|
9
|
-
@
|
10
|
-
@position = position
|
11
|
-
make_elements(value,element_separator,sub_element_separator)
|
5
|
+
def initialize elements, position
|
6
|
+
@segment_elements = elements.freeze
|
7
|
+
@position = position.freeze
|
12
8
|
end
|
13
9
|
|
14
10
|
# @return [Array<REX12::Element>, nil] get all elements as array or yield to block
|
15
11
|
def elements
|
16
12
|
if block_given?
|
17
|
-
@
|
13
|
+
@segment_elements.each {|el| yield el}
|
18
14
|
return nil
|
15
|
+
else
|
16
|
+
@segment_elements.to_enum { @segment_elements.length }
|
19
17
|
end
|
20
|
-
|
21
|
-
|
18
|
+
end
|
19
|
+
|
20
|
+
def element index
|
21
|
+
@segment_elements[index]
|
22
22
|
end
|
23
23
|
|
24
24
|
# @return [String] text representation of first element (like: ISA or REF)
|
25
25
|
def segment_type
|
26
|
-
|
26
|
+
self[0]
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
@elements << REX12::Element.new(str,sub_element_separator,pos)
|
33
|
-
end
|
29
|
+
def [](index)
|
30
|
+
el = element(index)
|
31
|
+
el.nil? ? nil : el.value
|
34
32
|
end
|
35
|
-
|
33
|
+
|
34
|
+
def isa_segment?
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
36
38
|
end; end
|
data/rex12.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: REX12
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Glick
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-12-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 9.1.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 9.1.0
|
69
83
|
description: Read ANSI X.12 files
|
70
84
|
email:
|
71
85
|
- brian@brian-glick.com
|
@@ -87,8 +101,12 @@ files:
|
|
87
101
|
- lib/rex12.rb
|
88
102
|
- lib/rex12/document.rb
|
89
103
|
- lib/rex12/element.rb
|
104
|
+
- lib/rex12/element_with_subelements.rb
|
105
|
+
- lib/rex12/isa_segment.rb
|
90
106
|
- lib/rex12/parse_error.rb
|
107
|
+
- lib/rex12/parser.rb
|
91
108
|
- lib/rex12/segment.rb
|
109
|
+
- lib/rex12/subelement.rb
|
92
110
|
- lib/rex12/transaction.rb
|
93
111
|
- rex12.gemspec
|
94
112
|
homepage: https://github.com/Vandegrift/rex12
|