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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 74edc4cb8982b9426d2c97b67495eb2d0dda6b8f
4
- data.tar.gz: dd6178322ddf0fe3642937607c595f55fac96aa7
3
+ metadata.gz: c77556e632df1d2847e884f9d04c400824821c50
4
+ data.tar.gz: cafe395798bdf26ab40f7e82321c78aab1091ab7
5
5
  SHA512:
6
- metadata.gz: 00f1778963481c83eeb234510718d4c6397fa22eeb1a0453186bf244b7c4efa0b4dbd84729e03b28c282dca12e2f841800ede8a72612df4a594bd1fae4f3a1bd
7
- data.tar.gz: f6b4e7a3b970eecf9299b6060b9d6784241490e7260476e7783c1cc061b6fa1e099700404858c5f212d5b833d96c854e5ce0177bf59e93a949d2e7078e4f935e
6
+ metadata.gz: 29c7443111f823fe2c72d2a2715870745ef174c41681be3317455b4c438366885e785712fb780ab0e18b13736dbf63d7c3503361fe53bf8eb8fb65e3eefa7f8a
7
+ data.tar.gz: 7525a909eee3c42cf34d035084698376f09be22f77c6a680d4b4183ef5a5844ec4d30873248497de7d2a36e47a9001607233a8e763e619737b954a3b0b8170f4
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
  /tmp/
10
10
  .ruby-gemset
11
11
  .ruby-version
12
+ .byebug_history
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # REX12
4
4
 
5
- This gem helps read files or text in the ASNI X.12 format.
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
- Array form:
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
- sgements = REX12::Document.read('myfilepath.edi')
32
- some_date = segments.find {|seg| seg.segment_type=='DTM' && seg.elements[1].value=='056'}
33
- if some_date
34
- puts some_date.elements[2].value
35
- else
36
- puts "This is not the date you're looking for"
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`. 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).
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
@@ -1,3 +1,3 @@
1
1
  module REX12
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
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
- # Your code goes here...
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
@@ -1,119 +1,25 @@
1
- # methods for reading a full EDI file
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
- # Parse the EDI document from file
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
- # Parse the EDI document from text or IO object
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
- return r
17
+ @segments.to_enum { @segments.length }
44
18
  end
45
19
  end
46
20
 
47
- # Parses the EDI document from text or IO object, returning or yielding every transaction from the
48
- # document.
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, sub_element_separator, position
13
- @value = value
14
- @sub_element_separator = sub_element_separator
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
- @value.index(@sub_element_separator) ? true : false
19
+ false
21
20
  end
22
21
 
23
- # Get all sub elements as an array or yield them to a block
24
- # @return [Array<String>, nil]
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;
@@ -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
- # REX12::Segment that was being parsed when error was generated (may be nil)
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
@@ -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 value, element_separator, sub_element_separator, position
9
- @value = value
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
- @elements.each {|el| yield el}
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
- # making a fresh array so nobody can screw with the internals of the class
21
- @elements.clone
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
- @elements.first.value
26
+ self[0]
27
27
  end
28
28
 
29
- def make_elements value, element_separator, sub_element_separator
30
- @elements = []
31
- value.split(element_separator).each_with_index do |str,pos|
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
- private :make_elements
33
+
34
+ def isa_segment?
35
+ false
36
+ end
37
+
36
38
  end; end
@@ -0,0 +1,14 @@
1
+ module REX12; class SubElement
2
+
3
+ attr_reader :value
4
+ attr_reader :position
5
+
6
+ def initialize value, position
7
+ @value = value.freeze
8
+ @position = position.freeze
9
+ end
10
+
11
+ def to_s
12
+ value
13
+ end
14
+ end; end;
data/rex12.gemspec CHANGED
@@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
25
  spec.add_development_dependency "rspec", "~> 3.0"
26
26
  spec.add_development_dependency "yard", "~> 0.9"
27
+ spec.add_development_dependency "byebug", "~> 9.1.0"
27
28
  end
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.1.4
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-01-09 00:00:00.000000000 Z
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