REX12 0.1.4 → 0.2.0

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