edi4r 0.9.4.1 → 0.9.6.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 +7 -0
- data/AuthorCopyright +3 -3
- data/{ChangeLog → Changelog} +60 -0
- data/README +15 -10
- data/Tutorial +2 -3
- data/VERSION +1 -1
- data/bin/edi2xml.rb +12 -16
- data/bin/editool.rb +9 -5
- data/bin/sedas2eancom02.rb +1385 -0
- data/bin/xml2edi.rb +7 -12
- data/data/edifact/iso9735/SDCD.20000.csv +1 -0
- data/data/edifact/iso9735/SDCD.3for2.csv +1 -0
- data/data/edifact/iso9735/SDED.20000.csv +6 -0
- data/data/edifact/iso9735/SDED.30000.csv +43 -43
- data/data/edifact/iso9735/SDED.3for2.csv +6 -0
- data/data/edifact/iso9735/SDED.40000.csv +129 -129
- data/data/edifact/iso9735/SDED.40100.csv +130 -130
- data/data/edifact/iso9735/SDMD.20000.csv +6 -0
- data/data/edifact/iso9735/SDMD.30000.csv +6 -6
- data/data/edifact/iso9735/SDMD.3for2.csv +6 -0
- data/data/edifact/iso9735/SDMD.40000.csv +17 -17
- data/data/edifact/iso9735/SDMD.40100.csv +17 -17
- data/data/edifact/iso9735/SDSD.20000.csv +5 -0
- data/data/edifact/iso9735/SDSD.3for2.csv +5 -0
- data/data/edifact/untdid/EDMD.d01b.csv +1 -1
- data/data/sedas/EDCD..csv +0 -0
- data/data/sedas/EDED..csv +859 -0
- data/data/sedas/EDMD..csv +16 -0
- data/data/sedas/EDSD..csv +44 -0
- data/lib/edi4r.rb +147 -67
- data/lib/edi4r/ansi_x12-rexml.rb +91 -0
- data/lib/edi4r/ansi_x12.rb +1684 -0
- data/lib/edi4r/diagrams.rb +75 -14
- data/lib/edi4r/edifact-rexml.rb +4 -3
- data/lib/edi4r/edifact.rb +505 -202
- data/lib/edi4r/rexml.rb +13 -7
- data/lib/edi4r/sedas.rb +854 -0
- data/lib/edi4r/standards.rb +150 -33
- data/test/damaged_file.edi +1 -0
- data/test/eancom2webedi.rb +1 -0
- data/test/groups.edi +1 -1
- data/test/test_basics.rb +16 -9
- data/test/test_edi_split.rb +30 -0
- data/test/test_loopback.rb +7 -2
- data/test/test_rexml.rb +34 -2
- data/test/test_service_messages.rb +190 -0
- data/test/test_streaming.rb +167 -0
- data/test/test_tut_examples.rb +3 -1
- data/test/webedi2eancom.rb +1 -0
- metadata +121 -77
@@ -0,0 +1,91 @@
|
|
1
|
+
# -*- encoding: iso-8859-1 -*-
|
2
|
+
# UN/EDIFACT add-ons to EDI module,
|
3
|
+
# Methods for XML support for the ANSI X12 module
|
4
|
+
#
|
5
|
+
# :include: ../../AuthorCopyright
|
6
|
+
#
|
7
|
+
# $Id$
|
8
|
+
#--
|
9
|
+
# $Log$
|
10
|
+
#
|
11
|
+
# Derived from "edifact-rexml.rb" by HWW
|
12
|
+
#
|
13
|
+
# To-do list:
|
14
|
+
# all - Just starting...
|
15
|
+
#++
|
16
|
+
#
|
17
|
+
# This is the XML add-on for the ANSI X12 module of edi4r (hence '::A')
|
18
|
+
#
|
19
|
+
# It leaves all real work to the base classes.
|
20
|
+
|
21
|
+
module EDI::A
|
22
|
+
|
23
|
+
class Interchange
|
24
|
+
#
|
25
|
+
# Returns a REXML document that represents the interchange
|
26
|
+
#
|
27
|
+
# xdoc:: REXML document that contains the XML representation of
|
28
|
+
# a ANSI X12 interchange
|
29
|
+
#
|
30
|
+
def Interchange.parse_xml( xdoc )
|
31
|
+
_root = xdoc.root
|
32
|
+
_header = _root.elements["Header"]
|
33
|
+
_trailer = _root.elements["Trailer"]
|
34
|
+
_version = _root.attributes["version"]
|
35
|
+
_ce_sep = REXML::XPath.first(xdoc, "/Interchange/Header/Segment/DE[@name='I15']").text.to_i
|
36
|
+
params = { :ce_sep => _ce_sep, :version => _version }
|
37
|
+
ic = Interchange.new( params )
|
38
|
+
if _root.elements["Message"].nil? # correct ??
|
39
|
+
_root.elements.each('MsgGroup') do |xel|
|
40
|
+
ic.add( MsgGroup.parse_xml( ic, xel ), false )
|
41
|
+
end
|
42
|
+
else
|
43
|
+
_root.elements.each('Message') do |xel|
|
44
|
+
ic.add( Message.parse_xml( ic, xel ), false )
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
ic.header = Segment.parse_xml( ic, _header.elements["Segment"] )
|
49
|
+
ic.header.dI15 = _ce_sep
|
50
|
+
ic.trailer = Segment.parse_xml( ic, _trailer.elements["Segment"] )
|
51
|
+
ic.validate
|
52
|
+
ic
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Read +maxlen+ bytes from $stdin (default) or from given stream
|
57
|
+
# (ANSI X12 data expected), and peek into first segment (ISA).
|
58
|
+
#
|
59
|
+
# Returns an empty Interchange object with a properly header filled.
|
60
|
+
#
|
61
|
+
# Intended use:
|
62
|
+
# Efficient routing by reading just ISA data: sender/recipient/ref/test
|
63
|
+
#
|
64
|
+
def Interchange.peek_xml(xdoc) # Handle to REXML document
|
65
|
+
_root = xdoc.root
|
66
|
+
_header = _root.elements["Header"]
|
67
|
+
_trailer = _root.elements["Trailer"]
|
68
|
+
_version = _root.attributes["version"]
|
69
|
+
_ce_sep = REXML::XPath.first(xdoc, "/Interchange/Header/Segment/DE[@name='I15']").text.to_i
|
70
|
+
params = { :ce_sep => _ce_sep, :version => _version }
|
71
|
+
ic = Interchange.new( params )
|
72
|
+
|
73
|
+
ic.header = Segment.parse_xml( ic, _header.elements["Segment"] )
|
74
|
+
ic.header.dI15 = _ce_sep
|
75
|
+
ic.trailer = Segment.parse_xml( ic, _trailer.elements["Segment"] )
|
76
|
+
|
77
|
+
ic
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Returns a REXML document that represents the interchange
|
82
|
+
#
|
83
|
+
def to_xml( xdoc = REXML::Document.new )
|
84
|
+
rc = super
|
85
|
+
# Add parameter(s) to header in rc[1]
|
86
|
+
# rc
|
87
|
+
xdoc
|
88
|
+
end
|
89
|
+
|
90
|
+
end # class Interchange
|
91
|
+
end # module EDI::A
|
@@ -0,0 +1,1684 @@
|
|
1
|
+
# -*- encoding: iso-8859-1 -*-
|
2
|
+
# ANSI X.12 add-ons to EDI module,
|
3
|
+
# API to parse and create ANSI X.12 data
|
4
|
+
#
|
5
|
+
# :include: ../../AuthorCopyright
|
6
|
+
#
|
7
|
+
# $Id$
|
8
|
+
#--
|
9
|
+
#
|
10
|
+
# Derived from "edifact.rb" v 1.10 on 2006/08/01 11:14:07 by HWW
|
11
|
+
#
|
12
|
+
# To-do list:
|
13
|
+
# all - just starting
|
14
|
+
#++
|
15
|
+
#
|
16
|
+
# This is the ANSI X12 module of edi4r (hence '::A')
|
17
|
+
#
|
18
|
+
# It implements ANSI X12 versions of classes Interchange, MsgGroup, Message,
|
19
|
+
# Segment, CDE, and DE in sub-module 'A' of module 'EDI'.
|
20
|
+
|
21
|
+
module EDI::A
|
22
|
+
|
23
|
+
#
|
24
|
+
# Use pattern for allowed chars of extended charset if none given explicitly
|
25
|
+
#
|
26
|
+
Illegal_Charset_Patterns = Hash.new(/[^-A-Za-z0-9!"&'()*+,.\/:;?= %~@\[\]_{}\\|<>#\$\x01-\x07\x09\x0A-\x0D\x11-\x17\x1C-\x1F]+/) # Default is Extended set!
|
27
|
+
Illegal_Charset_Patterns['Basic'] = /[^-A-Z0-9!"&'()*+,.\/:;?= \x07\x09\x0A-\x0D\x1C-\x1F]+/
|
28
|
+
Illegal_Charset_Patterns['Extended'] = /[^-A-Za-z0-9!"&'()*+,.\/:;?= %~@\[\]_{}\\|<>#\$\x01-\x07\x09\x0A-\x0D\x11-\x17\x1C-\x1F]+/
|
29
|
+
# more to come...
|
30
|
+
|
31
|
+
|
32
|
+
class EDISyntaxError < ArgumentError
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
#######################################################################
|
37
|
+
#
|
38
|
+
# Interchange: Class of the top-level objects of ANSI X12 data
|
39
|
+
#
|
40
|
+
class Interchange < EDI::Interchange
|
41
|
+
|
42
|
+
# attr_accessor :show_una
|
43
|
+
attr_reader :e_linebreak, :e_indent # :nodoc:
|
44
|
+
attr_reader :charset # :nodoc:
|
45
|
+
attr_reader :groups_created
|
46
|
+
attr_reader :ce_sep, :de_sep, :seg_term, :rep_sep
|
47
|
+
attr_reader :re_ce_sep, :re_de_sep
|
48
|
+
|
49
|
+
@@interchange_defaults = {
|
50
|
+
:charset => 'UNOB',
|
51
|
+
:version => "00401",
|
52
|
+
:ce_sep => ?\\,
|
53
|
+
:de_sep => ?*,
|
54
|
+
:rep_sep => ?^,
|
55
|
+
:seg_term => ?~,
|
56
|
+
|
57
|
+
:sender => nil, :recipient => nil,
|
58
|
+
:interchange_control_reference => 1,
|
59
|
+
:interchange_control_standards_id => 'U',
|
60
|
+
:acknowledgment_request => 0, :test_indicator => 'P',
|
61
|
+
:output_mode => :verbatim
|
62
|
+
}
|
63
|
+
@@interchange_default_keys = @@interchange_defaults.keys
|
64
|
+
|
65
|
+
# Create an empty ANSI X.12 interchange
|
66
|
+
#
|
67
|
+
# == Supported parameters (passed hash-style):
|
68
|
+
#
|
69
|
+
# === Essentials, should not be changed later
|
70
|
+
# :charset :: Not applicable, do not use. Default = 'UNOB'
|
71
|
+
# :version :: Sets I11 (ISA12), default = '00401'
|
72
|
+
#
|
73
|
+
# === Optional parameters affecting to_s, with corresponding setters
|
74
|
+
# :output_mode :: See setter output_mode=(), default = :verbatim
|
75
|
+
# :ce_sep :: Component element separator, default = ?\\
|
76
|
+
# :de_sep :: Data element separator, default = ?*
|
77
|
+
# :rep_sep :: Repetition element separator, default = ?^ (version 5)
|
78
|
+
# :seg_term :: Segment terminator, default = ?~
|
79
|
+
#
|
80
|
+
# === Optional ISA presets for your convenience, may be changed later
|
81
|
+
# :sender :: Presets DE I07, default = nil
|
82
|
+
# :recipient :: Presets DE I09, default = nil
|
83
|
+
# :interchange_control_reference :: Presets DE I12, default = '1'
|
84
|
+
# :interchange_control_standards_id :: Presets DE I10, default = 'U'
|
85
|
+
# :acknowledgment_request :: Presets DE I13, default = 0
|
86
|
+
# :test_indicator :: Presets DE I14, default = 'P'
|
87
|
+
#
|
88
|
+
# === Notes
|
89
|
+
# * Date (I08) and time (I09) are set to the current values automatically.
|
90
|
+
# * Add or change any data element later.
|
91
|
+
#
|
92
|
+
# === Examples:
|
93
|
+
# - ic = EDI::A::Interchange.new # Empty interchange, default settings
|
94
|
+
# - ic = EDI::A::Interchange.new(:output_mode=> :linebreak)
|
95
|
+
|
96
|
+
def initialize( user_par={} )
|
97
|
+
super( user_par ) # just in case...
|
98
|
+
if (illegal_keys = user_par.keys - @@interchange_default_keys) != []
|
99
|
+
msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
|
100
|
+
msg += "Valid param keys (symbols): #{@@interchange_default_keys.join(', ')}"
|
101
|
+
raise ArgumentError, msg
|
102
|
+
end
|
103
|
+
par = @@interchange_defaults.merge( user_par )
|
104
|
+
|
105
|
+
@groups_created = 0
|
106
|
+
|
107
|
+
@syntax = 'A' # par[:syntax] # A = ANSI X12
|
108
|
+
@charset = par[:charset] # FIXME: Outdated?
|
109
|
+
|
110
|
+
@version = par[:version]
|
111
|
+
|
112
|
+
@ce_sep = par[:ce_sep]
|
113
|
+
@re_ce_sep = Regexp.new( Regexp.escape( @ce_sep.chr ) )
|
114
|
+
@de_sep = par[:de_sep]
|
115
|
+
@re_de_sep = Regexp.new( Regexp.escape( @de_sep.chr ) )
|
116
|
+
@rep_sep = par[:rep_sep]
|
117
|
+
@seg_term = par[:seg_term]
|
118
|
+
|
119
|
+
self.output_mode = par[:output_mode]
|
120
|
+
|
121
|
+
check_consistencies
|
122
|
+
init_ndb( @version )
|
123
|
+
|
124
|
+
@header = new_segment('ISA')
|
125
|
+
@trailer = new_segment('IEA')
|
126
|
+
#@header.cS001.d0001 = par[:charset]
|
127
|
+
|
128
|
+
@header.dI06 = par[:sender] unless par[:sender].nil?
|
129
|
+
@header.dI07 = par[:recipient] unless par[:recip].nil?
|
130
|
+
|
131
|
+
@header.dI10 = par[:interchange_control_standards_id]
|
132
|
+
@header.dI11 = par[:version]
|
133
|
+
@header.dI12 = par[:interchange_control_reference]
|
134
|
+
@header.dI13 = par[:acknowledgment_request]
|
135
|
+
@header.dI14 = par[:test_indicator]
|
136
|
+
@header.dI15 = @ce_sep
|
137
|
+
|
138
|
+
t = Time.now
|
139
|
+
@header.dI08 = t.strftime('%y%m%d')
|
140
|
+
@header.dI09 = t.strftime('%H%M')
|
141
|
+
|
142
|
+
@trailer.dI16 = 0
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
# Reads EDI data from given stream (default: $stdin),
|
147
|
+
# parses it and returns an Interchange object
|
148
|
+
#
|
149
|
+
def Interchange.parse( hnd=$stdin, auto_validate=true )
|
150
|
+
builder = StreamingBuilder.new( auto_validate )
|
151
|
+
builder.go( hnd )
|
152
|
+
builder.interchange
|
153
|
+
end
|
154
|
+
|
155
|
+
#
|
156
|
+
# Read +maxlen+ bytes from $stdin (default) or from given stream
|
157
|
+
# (ANSI data expected), and peek into first segment (ISA).
|
158
|
+
#
|
159
|
+
# Returns an empty Interchange object with a properly header filled.
|
160
|
+
#
|
161
|
+
# Intended use:
|
162
|
+
# Efficient routing by reading just ISA data: sender/recipient/ref/test
|
163
|
+
#
|
164
|
+
def Interchange.peek(hnd=$stdin, params={}) # Handle to input stream
|
165
|
+
builder = StreamingBuilder.new( false )
|
166
|
+
if params[:deep_peek]
|
167
|
+
def builder.on_segment( s, tag )
|
168
|
+
end
|
169
|
+
else
|
170
|
+
def builder.on_gs( s )
|
171
|
+
throw :done
|
172
|
+
end
|
173
|
+
def builder.on_st( s, tag )
|
174
|
+
throw :done # FIXME: Redundant, since GS must occur?
|
175
|
+
end
|
176
|
+
end
|
177
|
+
builder.go( hnd )
|
178
|
+
builder.interchange
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# This method modifies the behaviour of method to_s():
|
183
|
+
# ANSI X12 interchanges and their components are turned into strings
|
184
|
+
# either "verbatim" (default) or in some more readable way.
|
185
|
+
# This method corresponds to a parameter with the same name
|
186
|
+
# that may be set at creation time.
|
187
|
+
#
|
188
|
+
# Valid values:
|
189
|
+
#
|
190
|
+
# :linebreak :: One-segment-per-line representation
|
191
|
+
# :indented :: Like :linebreak but with additional indentation
|
192
|
+
# (2 blanks per hierarchy level).
|
193
|
+
# :verbatim :: No linebreak (default), ISO compliant
|
194
|
+
#
|
195
|
+
def output_mode=( value )
|
196
|
+
super( value )
|
197
|
+
@e_linebreak = @e_indent = ''
|
198
|
+
case value
|
199
|
+
when :verbatim
|
200
|
+
# NOP (default)
|
201
|
+
when :linebreak
|
202
|
+
@e_linebreak = "\n"
|
203
|
+
when :indented
|
204
|
+
@e_linebreak = "\n"
|
205
|
+
@e_indent = ' '
|
206
|
+
else
|
207
|
+
raise "Unknown output mode '#{value}'. Supported modes: :linebreak, :indented, :verbatim (default)"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
# Add a MsgGroup (Functional Group) object to the interchange.
|
213
|
+
#
|
214
|
+
# GE counter is automatically incremented.
|
215
|
+
|
216
|
+
def add( obj, auto_validate=true )
|
217
|
+
super
|
218
|
+
@trailer.dI16 += 1 #if @trailer
|
219
|
+
# FIXME: Warn/fail if ST id is not unique (at validation?)
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
# Derive an empty message group from this interchange context.
|
224
|
+
# Parameters may be passed hash-like. See MsgGroup.new for details
|
225
|
+
#
|
226
|
+
def new_msggroup(params={}) # to be completed ...
|
227
|
+
@groups_created += 1
|
228
|
+
MsgGroup.new(self, params)
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
# Derive an empty segment from this interchange context
|
233
|
+
# For internal use only (header / trailer segment generation)
|
234
|
+
#
|
235
|
+
def new_segment(tag) # :nodoc:
|
236
|
+
Segment.new(self, tag)
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
# Parse a message group (when group mode detected)
|
241
|
+
# Internal use only.
|
242
|
+
|
243
|
+
def parse_msggroup(list) # :nodoc:
|
244
|
+
MsgGroup.parse(self, list)
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
# Parse a segment (header or trailer expected)
|
249
|
+
# Internal use only.
|
250
|
+
|
251
|
+
def parse_segment(buf, tag) # :nodoc:
|
252
|
+
Segment.parse(self, buf, tag)
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
# Returns the string representation of the interchange.
|
257
|
+
#
|
258
|
+
# Type conversion and escaping are provided.
|
259
|
+
# See +output_mode+ for modifiers.
|
260
|
+
|
261
|
+
def to_s
|
262
|
+
s = ''
|
263
|
+
postfix = '' << seg_term << @e_linebreak
|
264
|
+
s << super( postfix )
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
# Yields a readable, properly indented list of all contained objects,
|
269
|
+
# including the empty ones. This may be a very long string!
|
270
|
+
|
271
|
+
def inspect( indent='', symlist=[] )
|
272
|
+
# symlist << :una
|
273
|
+
super
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
# Returns the number of warnings found and logs them
|
278
|
+
|
279
|
+
def validate( err_count=0 )
|
280
|
+
if (h=self.size) != (t=@trailer.dI16)
|
281
|
+
EDI::logger.warn "Counter IEA, DE I16 does not match content: #{t} vs. #{h}"
|
282
|
+
EDI::logger.warn "classes: #{t.class} vs. #{h.class}"
|
283
|
+
err_count += 1
|
284
|
+
end
|
285
|
+
#if (h=@header.cS001.d0001) != @charset
|
286
|
+
# warn "Charset UNZ/UIZ, S001/0001 mismatch: #{h} vs. #@charset"
|
287
|
+
# err_count += 1
|
288
|
+
#end
|
289
|
+
if (h=@header.dI11) != @version
|
290
|
+
EDI::logger.warn "Syntax version number ISA, ISA12 mismatch: #{h} vs. #@version"
|
291
|
+
err_count += 1
|
292
|
+
end
|
293
|
+
check_consistencies
|
294
|
+
|
295
|
+
if (t=@trailer.dI12) != (h=@header.dI12)
|
296
|
+
EDI::logger.warn "ISA/IEA mismatch in refno (I12): #{h} vs. #{t}"
|
297
|
+
err_count += 1
|
298
|
+
end
|
299
|
+
|
300
|
+
# FIXME: Check if messages/groups are uniquely numbered
|
301
|
+
|
302
|
+
super
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
#
|
308
|
+
# Private method: Loads EDI norm database
|
309
|
+
#
|
310
|
+
def init_ndb(d0002, d0076 = nil)
|
311
|
+
@basedata = EDI::Dir::Directory.create(root.syntax,
|
312
|
+
:ISA12 => @version )
|
313
|
+
end
|
314
|
+
|
315
|
+
#
|
316
|
+
# Private method: Check if basic UNB elements are set properly
|
317
|
+
#
|
318
|
+
def check_consistencies
|
319
|
+
# FIXME - @syntax should be completely avoided, use sub-module name
|
320
|
+
if not ['A'].include?(@syntax) # More anticipated here
|
321
|
+
raise "#{@syntax} - syntax not supported!"
|
322
|
+
end
|
323
|
+
=begin
|
324
|
+
case @version
|
325
|
+
when 1
|
326
|
+
if @charset != 'UNOA'
|
327
|
+
raise "Syntax version 1 permits only charset UNOA!"
|
328
|
+
end
|
329
|
+
when 2
|
330
|
+
if not @charset =~ /UNO[AB]/
|
331
|
+
raise "Syntax version 2 permits only charsets UNOA, UNOB!"
|
332
|
+
end
|
333
|
+
when 3
|
334
|
+
if not @charset =~ /UNO[A-F]/
|
335
|
+
raise "Syntax version 3 permits only charsets UNOA...UNOF!"
|
336
|
+
end
|
337
|
+
when 4
|
338
|
+
# A,B: ISO 646 subsets, C-K: ISO-8859-x, X: ISO 2022, Y: ISO 10646-1
|
339
|
+
if not @charset =~ /UNO[A-KXY]/
|
340
|
+
raise "Syntax version 4 permits only charsets UNOA...UNOZ!"
|
341
|
+
end
|
342
|
+
else
|
343
|
+
raise "#{@version} - no such syntax version!"
|
344
|
+
end
|
345
|
+
=end
|
346
|
+
@illegal_charset_pattern = Illegal_Charset_Patterns['@version']
|
347
|
+
# Add more rules ...
|
348
|
+
end
|
349
|
+
|
350
|
+
end
|
351
|
+
|
352
|
+
|
353
|
+
#########################################################################
|
354
|
+
#
|
355
|
+
# Class EDI::A::MsgGroup
|
356
|
+
#
|
357
|
+
# This class implements a group of business documents of the same type
|
358
|
+
# Its header unites features from UNB as well as from UNH.
|
359
|
+
#
|
360
|
+
class MsgGroup < EDI::MsgGroup
|
361
|
+
|
362
|
+
attr_reader :messages_created
|
363
|
+
|
364
|
+
@@msggroup_defaults = {
|
365
|
+
:msg_type => '837', :func_ident => 'HC',
|
366
|
+
:version => '004', :release => '01',
|
367
|
+
:sub_version => '0', :assigned_code => nil # e.g. 'X098A1'
|
368
|
+
}
|
369
|
+
@@msggroup_default_keys = @@msggroup_defaults.keys
|
370
|
+
|
371
|
+
# Creates an empty ANSI X12 message group (functional group)
|
372
|
+
# Don't use directly - use +new_msggroup+ of class Interchange instead!
|
373
|
+
#
|
374
|
+
# == First parameter
|
375
|
+
#
|
376
|
+
# This is always the parent object (an interchange object).
|
377
|
+
# Use method +new_msggroup+ in the corresponding object instead
|
378
|
+
# of creating message groups unattended - the parent reference
|
379
|
+
# will be accounted for automatically.
|
380
|
+
#
|
381
|
+
# == Second parameter
|
382
|
+
#
|
383
|
+
# List of supported hash keys:
|
384
|
+
#
|
385
|
+
# === GS presets for your convenience, may be changed later
|
386
|
+
#
|
387
|
+
# :msg_type :: (for ST), default = '837'
|
388
|
+
# :func_ident :: Sets DE 479, default = 'HC'
|
389
|
+
# :version :: Merges into DE 480, default = '004'
|
390
|
+
# :release :: Merges into DE 480, default = '01'
|
391
|
+
# :sub_version :: Merges into DE 480, default = '0'
|
392
|
+
#
|
393
|
+
# === Optional parameters, required depending upon use case
|
394
|
+
#
|
395
|
+
# :assigned_code :: Merges into DE 480 (subset), default = nil
|
396
|
+
# :sender :: Presets DE 142, default = nil
|
397
|
+
# :recipient :: Presets DE 124, default = nil
|
398
|
+
# :group_reference :: Presets DE 28, auto-incremented
|
399
|
+
#
|
400
|
+
# == Notes
|
401
|
+
#
|
402
|
+
# * The functional group control number in GS and GE (28) is set
|
403
|
+
# automatically to a number that is unique for this message group and
|
404
|
+
# the running process (auto-increment).
|
405
|
+
# * The counter in GE (97) is set automatically to the number
|
406
|
+
# of included messages.
|
407
|
+
# * The trailer segment (GE) is generated automatically.
|
408
|
+
# * Whenever possible, <b>avoid writing to the counters of
|
409
|
+
# the message header or trailer segments</b>!
|
410
|
+
|
411
|
+
def initialize( p, user_par={} )
|
412
|
+
super( p, user_par )
|
413
|
+
@messages_created = 0
|
414
|
+
|
415
|
+
if user_par.is_a? Hash
|
416
|
+
preset_group( user_par )
|
417
|
+
@header = new_segment('GS')
|
418
|
+
@trailer = new_segment('GE')
|
419
|
+
@trailer.d97 = 0
|
420
|
+
@header.d479 = @func_ident # @name
|
421
|
+
@header.d455 = @resp_agency
|
422
|
+
@header.d480 = @version+@release+@sub_version
|
423
|
+
#cde.d0054 = @release
|
424
|
+
#cde.d0057 = @subset
|
425
|
+
|
426
|
+
@header.d142 = user_par[:sender] || root.header.dI06
|
427
|
+
@header.d124 = user_par[:recipient] || root.header.dI07
|
428
|
+
@header.d28 = user_par[:group_reference] || p.groups_created
|
429
|
+
# @trailer.d28 = @header.d28
|
430
|
+
@header.d455 = 'X'
|
431
|
+
|
432
|
+
t = Time.now
|
433
|
+
@header.d373 = t.strftime('%Y%m%d')
|
434
|
+
@header.d337 = t.strftime("%H%M")
|
435
|
+
|
436
|
+
elsif user_par.is_a? Segment
|
437
|
+
|
438
|
+
@header = user_par
|
439
|
+
raise "GS expected, #{@header.name} found!" if @header.name != 'GS'
|
440
|
+
@header.parent = self
|
441
|
+
@header.root = self.root
|
442
|
+
|
443
|
+
# Assign a temporary GS segment
|
444
|
+
de_sep = root.de_sep
|
445
|
+
@trailer = Segment.parse(root, 'GE' << de_sep << '0' << de_sep << '0')
|
446
|
+
|
447
|
+
@name = @header.d479 # FIXME: HC??
|
448
|
+
s480 = @header.d480
|
449
|
+
@version = s480[0,3]
|
450
|
+
@release = s480[3,2]
|
451
|
+
@sub_version = s480[5,1]
|
452
|
+
# @subset = s008.d0057
|
453
|
+
@resp_agency = @header.d455
|
454
|
+
else
|
455
|
+
raise "First parameter: Illegal type!"
|
456
|
+
end
|
457
|
+
|
458
|
+
end
|
459
|
+
|
460
|
+
|
461
|
+
# Internal use only!
|
462
|
+
|
463
|
+
def preset_group(user_par) # :nodoc:
|
464
|
+
if (illegal_keys = user_par.keys - @@msggroup_default_keys) != []
|
465
|
+
msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
|
466
|
+
msg += "Valid param keys (symbols): #{@@msggroup_default_keys.join(', ')}"
|
467
|
+
raise ArgumentError, msg
|
468
|
+
end
|
469
|
+
par = @@msggroup_defaults.merge( user_par )
|
470
|
+
|
471
|
+
@name = par[:msg_type]
|
472
|
+
@func_ident = par[:func_ident]
|
473
|
+
@version = par[:version]
|
474
|
+
@release = par[:release]
|
475
|
+
@sub_version = par[:sub_version]
|
476
|
+
@resp_agency = par[:resp_agency]
|
477
|
+
# @subset = par[:assigned_code]
|
478
|
+
# FIXME: Eliminate use of @version, @release, @resp_agency, @subset
|
479
|
+
# They get outdated whenever their UNG counterparts are changed
|
480
|
+
# Try to keep @name updated, or pass it a generic name
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
def MsgGroup.parse (p, segment_list) # List of segments
|
485
|
+
grp = p.new_msggroup(:msg_type => 'DUMMY')
|
486
|
+
|
487
|
+
# We now expect a sequence of segments that comprises one group,
|
488
|
+
# starting with ST and ending with SE, and with messages in between.
|
489
|
+
# We process the ST/SE envelope separately, then work on the content.
|
490
|
+
|
491
|
+
header = grp.parse_segment(segment_list.shift, 'GS')
|
492
|
+
trailer = grp.parse_segment(segment_list.pop, 'GE')
|
493
|
+
|
494
|
+
init_seg = Regexp.new('^ST')
|
495
|
+
exit_seg = Regexp.new('^SE')
|
496
|
+
|
497
|
+
while segbuf = segment_list.shift
|
498
|
+
case segbuf
|
499
|
+
|
500
|
+
when init_seg
|
501
|
+
sub_list = Array.new
|
502
|
+
sub_list.push segbuf
|
503
|
+
|
504
|
+
when exit_seg
|
505
|
+
sub_list.push segbuf
|
506
|
+
grp.add grp.parse_message(sub_list)
|
507
|
+
|
508
|
+
else
|
509
|
+
sub_list.push segbuf
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
grp.header = header
|
514
|
+
grp.trailer = trailer
|
515
|
+
grp
|
516
|
+
end
|
517
|
+
|
518
|
+
|
519
|
+
def new_message(params={})
|
520
|
+
@messages_created += 1
|
521
|
+
Message.new(self, params)
|
522
|
+
end
|
523
|
+
|
524
|
+
def new_segment(tag) # :nodoc:
|
525
|
+
Segment.new(self, tag)
|
526
|
+
end
|
527
|
+
|
528
|
+
|
529
|
+
def parse_message(list) # :nodoc:
|
530
|
+
Message.parse(self, list)
|
531
|
+
end
|
532
|
+
|
533
|
+
def parse_segment(buf, tag) # :nodoc:
|
534
|
+
Segment.parse(self, buf, tag)
|
535
|
+
end
|
536
|
+
|
537
|
+
|
538
|
+
def add( msg, auto_validate=true )
|
539
|
+
super( msg )
|
540
|
+
@trailer.d97 = @trailer.d97.to_i if @trailer.d97.is_a? String
|
541
|
+
@trailer.d97 += 1
|
542
|
+
end
|
543
|
+
|
544
|
+
|
545
|
+
def to_s
|
546
|
+
postfix = '' << root.seg_term << root.e_linebreak
|
547
|
+
super( postfix )
|
548
|
+
end
|
549
|
+
|
550
|
+
|
551
|
+
def validate( err_count=0 )
|
552
|
+
# Consistency checks
|
553
|
+
if (a=@trailer.d97) != (b=self.size)
|
554
|
+
EDI::logger.warn "GE: DE 97 (#{a}) does not match number of messages (#{b})"
|
555
|
+
err_count += 1
|
556
|
+
end
|
557
|
+
a, b = @trailer.d28, @header.d28
|
558
|
+
if a != b
|
559
|
+
EDI::logger.warn "GE: DE 28 (#{a}) does not match reference in GS (#{b})"
|
560
|
+
err_count += 1
|
561
|
+
end
|
562
|
+
|
563
|
+
# FIXME: Check if messages are uniquely numbered
|
564
|
+
|
565
|
+
super
|
566
|
+
end
|
567
|
+
|
568
|
+
end
|
569
|
+
|
570
|
+
|
571
|
+
#########################################################################
|
572
|
+
#
|
573
|
+
# Class EDI::A::Message
|
574
|
+
#
|
575
|
+
# This class implements a single business document according to ANSI X12
|
576
|
+
|
577
|
+
class Message < EDI::Message
|
578
|
+
# private_class_method :new
|
579
|
+
|
580
|
+
@@message_defaults = {
|
581
|
+
:msg_type => 837, :version=> '004010', :ref_no => nil
|
582
|
+
}
|
583
|
+
@@message_default_keys = @@message_defaults.keys
|
584
|
+
|
585
|
+
# Creates an empty ANSI X12 message.
|
586
|
+
#
|
587
|
+
# Don't use directly - call method +new_message+ of class Interchange
|
588
|
+
# or MsgGroup instead!
|
589
|
+
#
|
590
|
+
# == First parameter
|
591
|
+
#
|
592
|
+
# This is always the parent object, either a message group
|
593
|
+
# or an interchange object.
|
594
|
+
# Use method +new_message+ in the corresponding object instead
|
595
|
+
# of creating messages unattended, and the parent reference
|
596
|
+
# will be accounted for automatically.
|
597
|
+
#
|
598
|
+
# == Second parameter, case "Hash"
|
599
|
+
#
|
600
|
+
# List of supported hash keys:
|
601
|
+
#
|
602
|
+
# === Essentials, should not be changed later
|
603
|
+
#
|
604
|
+
# :msg_type :: Sets DE 143, default = '837'
|
605
|
+
# :ref_no :: Sets DE 329, default is a built-in counter
|
606
|
+
# :version :: Sets S009.0052, default = 'D'
|
607
|
+
# :release :: Sets S009.0054, default = '96A'
|
608
|
+
# :resp_agency :: Sets S009.0051, default = 'UN'
|
609
|
+
#
|
610
|
+
# === Optional parameters, required depending upon use case
|
611
|
+
#
|
612
|
+
# :assigned_code :: Sets S009.0057 (subset), default = nil
|
613
|
+
#
|
614
|
+
# == Second parameter, case "Segment"
|
615
|
+
#
|
616
|
+
# This mode is only used internally when parsing data.
|
617
|
+
#
|
618
|
+
# == Notes
|
619
|
+
#
|
620
|
+
# * The counter in ST (329) is set automatically to a
|
621
|
+
# number that is unique for the running process.
|
622
|
+
# * The trailer segment (SE) is generated automatically.
|
623
|
+
# * Whenever possible, <b>avoid write access to the
|
624
|
+
# message header or trailer segments</b>!
|
625
|
+
|
626
|
+
def initialize( p, user_par={} )
|
627
|
+
super( p, user_par )
|
628
|
+
|
629
|
+
# First param is either a hash or segment ST
|
630
|
+
# - If Hash: Build ST from given parameters
|
631
|
+
# - If Segment: Extract some crucial parameters
|
632
|
+
if user_par.is_a? Hash
|
633
|
+
preset_msg( user_par )
|
634
|
+
par = {
|
635
|
+
:GS08 => @version
|
636
|
+
# :d0065 => @msg_type, :d0052=> @version, :d0054=> @release,
|
637
|
+
# :d0051 => @resp_agency, :d0057 => @subset, :is_iedi => root.is_iedi?
|
638
|
+
}
|
639
|
+
@maindata = EDI::Dir::Directory.create(root.syntax, par )
|
640
|
+
|
641
|
+
@header = new_segment('ST')
|
642
|
+
@trailer = new_segment('SE')
|
643
|
+
@header.d143 = @name
|
644
|
+
@header.d329 = user_par[:ref_no].nil? ? p.messages_created : user_par[:ref_no]
|
645
|
+
@trailer.d329 = @header.d329
|
646
|
+
|
647
|
+
=begin
|
648
|
+
cde = @header.cS009
|
649
|
+
cde.d0065 = @name
|
650
|
+
cde.d0052 = @version
|
651
|
+
cde.d0054 = @release
|
652
|
+
cde.d0051 = @resp_agency
|
653
|
+
cde.d0057 = @subset
|
654
|
+
=end
|
655
|
+
|
656
|
+
elsif user_par.is_a? Segment
|
657
|
+
@header = user_par
|
658
|
+
raise "ST expected, #{@header.name} found!" if @header.name != 'ST'
|
659
|
+
@header.parent = self
|
660
|
+
@header.root = self.root
|
661
|
+
@trailer = Segment.new(root, 'SE') # temporary
|
662
|
+
#s009 = @header.cS009
|
663
|
+
#@name = s009.d0065
|
664
|
+
@version = p.header.d480 # GS08
|
665
|
+
#@release = s009.d0054
|
666
|
+
#@resp_agency = s009.d0051
|
667
|
+
#@subset = s009.d0057
|
668
|
+
par = {
|
669
|
+
:GS08 => @version,
|
670
|
+
:ST01 => @header.d143
|
671
|
+
# :d0065 => @name, :d0052=> @version, :d0054=> @release,
|
672
|
+
# :d0051 => @resp_agency, :d0057 => @subset, :is_iedi => root.is_iedi?
|
673
|
+
}
|
674
|
+
@maindata = EDI::Dir::Directory.create(root.syntax, par )
|
675
|
+
else
|
676
|
+
raise "First parameter: Illegal type!"
|
677
|
+
end
|
678
|
+
|
679
|
+
@trailer.d96 = 2 if @trailer # Just ST and SE so far
|
680
|
+
end
|
681
|
+
|
682
|
+
#
|
683
|
+
# Derive a new segment with the given name from this message context.
|
684
|
+
# The call will fail if the message name is unknown to this message's
|
685
|
+
# Directory (not in EDMD).
|
686
|
+
#
|
687
|
+
# == Example:
|
688
|
+
# seg = msg.new_segment( 'BHT' )
|
689
|
+
# seg.d353 = '00'
|
690
|
+
# # etc.
|
691
|
+
# msg.add seg
|
692
|
+
#
|
693
|
+
def new_segment( tag )
|
694
|
+
Segment.new(self, tag)
|
695
|
+
end
|
696
|
+
|
697
|
+
# Internal use only!
|
698
|
+
|
699
|
+
def parse_segment(buf, tag=nil) # :nodoc:
|
700
|
+
Segment.parse(self, buf, tag)
|
701
|
+
end
|
702
|
+
|
703
|
+
# Internal use only!
|
704
|
+
|
705
|
+
def preset_msg(user_par) # :nodoc:
|
706
|
+
if (illegal_keys = user_par.keys - @@message_default_keys) != []
|
707
|
+
msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
|
708
|
+
msg += "Valid param keys (symbols): #{@@message_default_keys.join(', ')}"
|
709
|
+
raise ArgumentError, msg
|
710
|
+
end
|
711
|
+
|
712
|
+
par = @@message_defaults.merge( user_par )
|
713
|
+
|
714
|
+
@name = par[:msg_type]
|
715
|
+
@version = par[:version]
|
716
|
+
@release = par[:release]
|
717
|
+
@resp_agency = par[:resp_agency]
|
718
|
+
@subset = par[:assigned_code]
|
719
|
+
# FIXME: Eliminate use of @version, @release, @resp_agency, @subset
|
720
|
+
# They get outdated whenever their UNH counterparts are changed
|
721
|
+
# Try to keep @name updated, or pass it a generic name
|
722
|
+
end
|
723
|
+
|
724
|
+
|
725
|
+
# Returns a new Message object that contains the data of the
|
726
|
+
# strings passed in the +segment_list+ array. Uses the context
|
727
|
+
# of the given +parent+ object and configures message as a child.
|
728
|
+
|
729
|
+
def Message.parse (parent, segment_list)
|
730
|
+
|
731
|
+
h, t, re_t = 'ST', 'SE', /^SE/
|
732
|
+
|
733
|
+
# Segments comprise a single message
|
734
|
+
# Temporarily assign a parent, or else service segment lookup fails
|
735
|
+
header = parent.parse_segment(segment_list.shift, h)
|
736
|
+
msg = parent.new_message(header)
|
737
|
+
trailer = msg.parse_segment( segment_list.pop, t )
|
738
|
+
|
739
|
+
segment_list.each do |segbuf|
|
740
|
+
seg = Segment.parse( msg, segbuf )
|
741
|
+
if segbuf =~ re_t # FIXME: Should that case ever occur?
|
742
|
+
msg.trailer = seg
|
743
|
+
else
|
744
|
+
msg.add(seg)
|
745
|
+
end
|
746
|
+
end
|
747
|
+
msg.trailer = trailer
|
748
|
+
msg
|
749
|
+
end
|
750
|
+
|
751
|
+
|
752
|
+
#
|
753
|
+
# Add a previously derived segment to the end of this message (append)
|
754
|
+
# Make sure that all mandatory elements have been supplied.
|
755
|
+
#
|
756
|
+
# == Notes
|
757
|
+
#
|
758
|
+
# * Strictly add segments in the sequence described by this message's
|
759
|
+
# branching diagram!
|
760
|
+
#
|
761
|
+
# * Adding a segment will automatically increase the corresponding
|
762
|
+
# counter in the message trailer.
|
763
|
+
#
|
764
|
+
# == Example:
|
765
|
+
# seg = msg.new_segment( 'BHT' )
|
766
|
+
# seg.d353 = 837
|
767
|
+
# # etc.
|
768
|
+
# msg.add seg
|
769
|
+
#
|
770
|
+
def add( seg )
|
771
|
+
super
|
772
|
+
@trailer.d96 = @trailer.d96.to_i if @trailer.d96.is_a? String
|
773
|
+
@trailer.d96 += 1 # What if new segment is/remains empty??
|
774
|
+
end
|
775
|
+
|
776
|
+
|
777
|
+
def validate( err_count=0 )
|
778
|
+
# Check sequence of segments against library,
|
779
|
+
# thereby adding location information to each segment
|
780
|
+
|
781
|
+
par = {
|
782
|
+
:ST01 => @header.d143, # :d0052=> @version, :d0054=> @release,
|
783
|
+
# :d0051 => @resp_agency, :d0057 => @subset,
|
784
|
+
# :ISA12 => root.version, # :is_iedi => root.is_iedi?,
|
785
|
+
:GS08 => parent.header.d480
|
786
|
+
}
|
787
|
+
diag = EDI::Diagram::Diagram.create( root.syntax, par )
|
788
|
+
ni = EDI::Diagram::NodeInstance.new(diag)
|
789
|
+
|
790
|
+
ni.seek!( @header )
|
791
|
+
@header.update_with( ni )
|
792
|
+
each do |seg|
|
793
|
+
if ni.seek!(seg)
|
794
|
+
seg.update_with( ni )
|
795
|
+
else
|
796
|
+
# FIXME: Do we really have to fail here, or would a "warn" suffice?
|
797
|
+
raise "seek! failed for #{seg.name} when starting at #{ni.name}"
|
798
|
+
end
|
799
|
+
end
|
800
|
+
ni.seek!( @trailer )
|
801
|
+
@trailer.update_with( ni )
|
802
|
+
|
803
|
+
|
804
|
+
# Consistency checks
|
805
|
+
|
806
|
+
if (a=@trailer.d96) != (b=self.size+2)
|
807
|
+
EDI::logger.warn "DE 96 (#{a}) does not match number of segments (#{b})"
|
808
|
+
err_count += 1
|
809
|
+
end
|
810
|
+
|
811
|
+
a, b = @trailer.d329, @header.d329
|
812
|
+
if a != b
|
813
|
+
EDI::logger.warn "Trailer reference (#{a}) does not match header reference (#{b})"
|
814
|
+
err_count += 1
|
815
|
+
end
|
816
|
+
|
817
|
+
=begin
|
818
|
+
if parent.is_a? MsgGroup
|
819
|
+
ung = parent.header; s008 = ung.cS008; s009 = header.cS009
|
820
|
+
a, b = s009.d0065, ung.d0038
|
821
|
+
if a != b
|
822
|
+
warn "Message type (#{a}) does not match that of group (#{b})"
|
823
|
+
err_count += 1
|
824
|
+
end
|
825
|
+
a, b = s009.d0052, s008.d0052
|
826
|
+
if a != b
|
827
|
+
warn "Message version (#{a}) does not match that of group (#{b})"
|
828
|
+
err_count += 1
|
829
|
+
end
|
830
|
+
a, b = s009.d0054, s008.d0054
|
831
|
+
if a != b
|
832
|
+
warn "Message release (#{a}) does not match that of group (#{b})"
|
833
|
+
err_count += 1
|
834
|
+
end
|
835
|
+
a, b = s009.d0051, ung.d0051
|
836
|
+
if a != b
|
837
|
+
warn "Message responsible agency (#{a}) does not match that of group (#{b})"
|
838
|
+
err_count += 1
|
839
|
+
end
|
840
|
+
a, b = s009.d0057, s008.d0057
|
841
|
+
if a != b
|
842
|
+
warn "Message association assigned code (#{a}) does not match that of group (#{b})"
|
843
|
+
err_count += 1
|
844
|
+
end
|
845
|
+
end
|
846
|
+
=end
|
847
|
+
|
848
|
+
# Now check each segment
|
849
|
+
super( err_count )
|
850
|
+
end
|
851
|
+
|
852
|
+
|
853
|
+
def to_s
|
854
|
+
postfix = '' << root.seg_term << root.e_linebreak
|
855
|
+
super( postfix )
|
856
|
+
end
|
857
|
+
|
858
|
+
end
|
859
|
+
|
860
|
+
|
861
|
+
#########################################################################
|
862
|
+
#
|
863
|
+
# Class EDI::A::Segment
|
864
|
+
#
|
865
|
+
# This class implements UN/EDIFACT segments like BGM, NAD etc.,
|
866
|
+
# including the service segments UNB, UNH ...
|
867
|
+
#
|
868
|
+
|
869
|
+
class Segment < EDI::Segment
|
870
|
+
|
871
|
+
# A new segment must have a parent (usually, a message). This is the
|
872
|
+
# first parameter. The second is a string with the desired segment tag.
|
873
|
+
#
|
874
|
+
# Don't create segments without their context - use Message#new_segment()
|
875
|
+
# instead.
|
876
|
+
|
877
|
+
def initialize(p, tag)
|
878
|
+
super( p, tag )
|
879
|
+
|
880
|
+
each_BCDS('s'+tag) do |entry| # FIXME: Workaround for X12 segment names
|
881
|
+
id = entry.name
|
882
|
+
status = entry.status
|
883
|
+
|
884
|
+
# FIXME: Code redundancy in type detection - remove later!
|
885
|
+
case id
|
886
|
+
when /C\d{3}/ # Composite
|
887
|
+
add new_CDE(id, status)
|
888
|
+
when /I\d{2}|\d{1,4}/ # Simple DE
|
889
|
+
add new_DE(id, status, fmt_of_DE(id))
|
890
|
+
else # Should never occur
|
891
|
+
raise "Not a legal DE or CDE id: #{id}"
|
892
|
+
end
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
|
897
|
+
def new_CDE(id, status)
|
898
|
+
CDE.new(self, id, status)
|
899
|
+
end
|
900
|
+
|
901
|
+
|
902
|
+
def new_DE(id, status, fmt)
|
903
|
+
DE.new(self, id, status, fmt)
|
904
|
+
end
|
905
|
+
|
906
|
+
|
907
|
+
# Reserved for internal use
|
908
|
+
|
909
|
+
def Segment.parse (p, buf, tag_expected=nil)
|
910
|
+
# Buffer contains a single segment
|
911
|
+
# obj_list = buf.split( Regexp.new('\\'+p.root.de_sep.chr) ) # FIXME: Pre-calc the regex!
|
912
|
+
obj_list = buf.split( p.root.re_de_sep )
|
913
|
+
tag = obj_list.shift # First entry must be the segment tag
|
914
|
+
|
915
|
+
raise "Illegal tag: #{tag}" unless tag =~ /[A-Z][A-Z0-9]{1,2}/
|
916
|
+
if tag_expected and tag_expected != tag
|
917
|
+
raise "Wrong segment name! Expected: #{tag_expected}, found: #{tag}"
|
918
|
+
end
|
919
|
+
|
920
|
+
seg = p.new_segment(tag)
|
921
|
+
seg.each {|obj| obj.parse( obj_list.shift ) }
|
922
|
+
seg
|
923
|
+
# Error handling needed here if obj_list is not exhausted now!
|
924
|
+
end
|
925
|
+
|
926
|
+
|
927
|
+
def to_s
|
928
|
+
s = ''
|
929
|
+
return s if empty?
|
930
|
+
|
931
|
+
rt = self.root
|
932
|
+
|
933
|
+
indent = rt.e_indent * (self.level || 0)
|
934
|
+
s << indent << name << rt.de_sep
|
935
|
+
skip_count = 0
|
936
|
+
each {|obj|
|
937
|
+
if obj.empty?
|
938
|
+
skip_count += 1
|
939
|
+
else
|
940
|
+
if skip_count > 0
|
941
|
+
s << rt.de_sep.chr * skip_count
|
942
|
+
skip_count = 0
|
943
|
+
end
|
944
|
+
s << obj.to_s
|
945
|
+
skip_count += 1
|
946
|
+
end
|
947
|
+
}
|
948
|
+
s # name=='ISA' ? s.chop : s
|
949
|
+
end
|
950
|
+
|
951
|
+
|
952
|
+
# Some exceptional setters, required for data consistency
|
953
|
+
|
954
|
+
# Don't change DE 0002! d0002=() raises an exception when called.
|
955
|
+
def d0002=( value ); fail "ANSI version not modifiable!"; end
|
956
|
+
|
957
|
+
# Setter for DE I12 in ISA & IEA (interchange control reference)
|
958
|
+
def dI12=( value )
|
959
|
+
return super unless self.name=~/I[SE]A/
|
960
|
+
parent.header['I12'].first.value = value
|
961
|
+
parent.trailer['I12'].first.value = value
|
962
|
+
end
|
963
|
+
|
964
|
+
# Setter for DE 28 in GS & GE (group reference)
|
965
|
+
def d28=( value )
|
966
|
+
return super unless self.name=~/G[SE]/
|
967
|
+
parent.header['28'].first.value = value
|
968
|
+
parent.trailer['28'].first.value = value
|
969
|
+
end
|
970
|
+
|
971
|
+
# Setter for DE 329 in ST & SE (TS control number)
|
972
|
+
def d329=( value )
|
973
|
+
return super unless self.name=~/S[TE]/
|
974
|
+
parent.header['329'].first.value = value
|
975
|
+
parent.trailer['329'].first.value = value
|
976
|
+
end
|
977
|
+
|
978
|
+
end
|
979
|
+
|
980
|
+
|
981
|
+
#########################################################################
|
982
|
+
#
|
983
|
+
# Class EDI::A::CDE
|
984
|
+
#
|
985
|
+
# This class implements ANSI X12 composite data elements C001 etc.
|
986
|
+
#
|
987
|
+
# For internal use only.
|
988
|
+
|
989
|
+
class CDE < EDI::CDE
|
990
|
+
|
991
|
+
def initialize(p, name, status)
|
992
|
+
super(p, name, status)
|
993
|
+
|
994
|
+
each_BCDS(name) do |entry|
|
995
|
+
id = entry.name
|
996
|
+
status = entry.status
|
997
|
+
# FIXME: Code redundancy in type detection - remove later!
|
998
|
+
if id =~ /\d{1,4}/
|
999
|
+
add new_DE(id, status, fmt_of_DE(id))
|
1000
|
+
else # Should never occur
|
1001
|
+
raise "Not a legal DE: #{id}"
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
def new_DE(id, status, fmt)
|
1007
|
+
DE.new(self, id, status, fmt)
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
|
1011
|
+
def parse (buf) # Buffer contains content of a single CDE
|
1012
|
+
return nil unless buf
|
1013
|
+
obj_list = buf.split( root.re_ce_sep )
|
1014
|
+
each {|obj| obj.parse( obj_list.shift ) }
|
1015
|
+
# FIXME: Error handling needed here if obj_list is not exhausted now!
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
|
1019
|
+
def to_s
|
1020
|
+
rt = self.root
|
1021
|
+
s = ''; skip_count = 0
|
1022
|
+
ce_sep = rt.ce_sep.chr
|
1023
|
+
each {|de|
|
1024
|
+
if de.empty?
|
1025
|
+
skip_count += 1
|
1026
|
+
else
|
1027
|
+
if skip_count > 0
|
1028
|
+
s << ce_sep * skip_count
|
1029
|
+
skip_count = 0
|
1030
|
+
end
|
1031
|
+
s << de.to_s
|
1032
|
+
skip_count += 1
|
1033
|
+
end
|
1034
|
+
}
|
1035
|
+
s
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
|
1041
|
+
#########################################################################
|
1042
|
+
#
|
1043
|
+
# Class EDI::A::DE
|
1044
|
+
#
|
1045
|
+
# This class implements ANSI X12 data elements 1004, 2005 etc.,
|
1046
|
+
# including the service DEs I01, ..., I16
|
1047
|
+
#
|
1048
|
+
# For internal use only.
|
1049
|
+
|
1050
|
+
class DE < EDI::DE
|
1051
|
+
|
1052
|
+
def initialize( p, name, status, fmt )
|
1053
|
+
super( p, name, status, fmt )
|
1054
|
+
raise "Illegal DE name: #{name}" unless name =~ /\d{1,4}/
|
1055
|
+
# check if supported format syntax
|
1056
|
+
# check if supported status value
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
|
1060
|
+
# Generate the DE content from the given string representation.
|
1061
|
+
# +buf+ contains a single DE string
|
1062
|
+
|
1063
|
+
def parse( buf, already_escaped=false ) # 2nd par is a dummy for X12
|
1064
|
+
return nil unless buf
|
1065
|
+
return @value = nil if buf.empty?
|
1066
|
+
self.value = buf
|
1067
|
+
# if format[0] == ?n
|
1068
|
+
# # Select appropriate Numeric, FIXME: Also match exponents!
|
1069
|
+
# self.value = @value=~/\d+\.\d+/ ? @value.to_f : @value.to_i
|
1070
|
+
# end
|
1071
|
+
@value
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
|
1075
|
+
def to_s( no_escape=false ) # Parameter is a dummy for X12
|
1076
|
+
location = "DE #{parent.name}/#{@name}"
|
1077
|
+
if @format =~ /^(AN|B|DT|ID|N\d+?|R|TM|XX) (\d+)\/(\d+)$/
|
1078
|
+
_type, min_size, max_size = $1, $2.to_i, $3.to_i
|
1079
|
+
else
|
1080
|
+
raise "#{location}: Illegal format #{format}"
|
1081
|
+
end
|
1082
|
+
case _type
|
1083
|
+
when 'AN', 'ID', 'DT', 'TM'
|
1084
|
+
if empty? then return( required? ? ' '* min_size : '' ) end
|
1085
|
+
str = @value.to_s; fixlen = str.length
|
1086
|
+
return @value.to_s[0,max_size] if fixlen > max_size # Truncate if too long
|
1087
|
+
fixlen < min_size ? str + ' ' * (min_size - fixlen) : str # Right-pad with blanks if too short
|
1088
|
+
when /N(\d+)/
|
1089
|
+
x = @value.to_f
|
1090
|
+
$1.to_i.times { x *= 10 }
|
1091
|
+
str = (x+0.0001).to_i.to_s; fixlen = str.length
|
1092
|
+
raise "#{location}: '#{value}' too long (#{fixlen}) for fmt #{format}" if fixlen > max_size
|
1093
|
+
return '0' * (min_size - fixlen) + str if fixlen < min_size # Left-pad with zeroes
|
1094
|
+
str
|
1095
|
+
when 'R'
|
1096
|
+
@value.to_s
|
1097
|
+
# FIXME: Add length control!
|
1098
|
+
when 'XX'
|
1099
|
+
# @value.to_s
|
1100
|
+
@value
|
1101
|
+
else
|
1102
|
+
raise "#{location}: Format #{format} not supported"
|
1103
|
+
end
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
# The proper method to assign values to a DE.
|
1107
|
+
# The passed value must respond to +to_i+ .
|
1108
|
+
|
1109
|
+
def value=( val )
|
1110
|
+
if @format =~ /^(AN|B|DT|ID|N\d+?|R|TM|XX) (\d+)\/(\d+)$/
|
1111
|
+
_type, min_size, max_size = $1, $2.to_i, $3.to_i
|
1112
|
+
else
|
1113
|
+
location = "DE #{parent.name}/#{@name}"
|
1114
|
+
raise "#{location}: Illegal format #{format}"
|
1115
|
+
end
|
1116
|
+
|
1117
|
+
case _type
|
1118
|
+
when 'AN', 'ID', 'DT', 'TM'
|
1119
|
+
# super
|
1120
|
+
when 'R'
|
1121
|
+
val = val.to_f
|
1122
|
+
when /N(\d+)/
|
1123
|
+
if $1==0
|
1124
|
+
val = val.to_i
|
1125
|
+
else
|
1126
|
+
val = val.to_f
|
1127
|
+
$1.to_i.times { val /= 10.0 }
|
1128
|
+
end
|
1129
|
+
when 'XX'
|
1130
|
+
# p "case XX: name, val = ", name, val
|
1131
|
+
val = val[0] if val.is_a?(String)
|
1132
|
+
# raise "#{location}: Illegal value #{val} for format XX" unless val.is_a? Fixnum
|
1133
|
+
# return super
|
1134
|
+
else
|
1135
|
+
location = "DE #{parent.name}/#{@name}"
|
1136
|
+
raise "#{location}: Format #{format} not supported"
|
1137
|
+
end
|
1138
|
+
# Suppress trailing decimal part if Integer value
|
1139
|
+
if val.is_a? Float
|
1140
|
+
ival = val.to_i
|
1141
|
+
val = ival if val == ival
|
1142
|
+
end
|
1143
|
+
EDI::logger.info "***** I15='#{val}'" if name == 'I15' && val.is_a?(String) && val.size > 1
|
1144
|
+
super
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
|
1148
|
+
# Performs various validation checks and returns the number of
|
1149
|
+
# issues found (plus the value of +err_count+):
|
1150
|
+
#
|
1151
|
+
# - empty while mandatory?
|
1152
|
+
# - character set limitations violated?
|
1153
|
+
# - various format restrictions violated?
|
1154
|
+
#
|
1155
|
+
# Note: X12 comes with its own format definitions, so we overwrite
|
1156
|
+
# validate() of the base class here entirely.
|
1157
|
+
|
1158
|
+
def validate( err_count=0, fmt=@format )
|
1159
|
+
location = "DE #{parent.name}/#{@name}"
|
1160
|
+
if empty?
|
1161
|
+
if required?
|
1162
|
+
EDI::logger.warn "#{location}: Empty though mandatory!"
|
1163
|
+
err_count += 1
|
1164
|
+
end
|
1165
|
+
else
|
1166
|
+
#
|
1167
|
+
# Charset check
|
1168
|
+
#
|
1169
|
+
if (pos = (value =~ root.illegal_charset_pattern)) # != nil
|
1170
|
+
EDI::logger.warn "#{location}: Illegal character: #{value[pos].chr} (#{value[pos]})"
|
1171
|
+
err_count += 1
|
1172
|
+
end
|
1173
|
+
#
|
1174
|
+
# Format check, raise error if not consistent!
|
1175
|
+
#
|
1176
|
+
if fmt =~ /^(AN|B|DT|ID|N\d|R|TM|XX) (\d+)\/(\d+)$/
|
1177
|
+
_type, min_size, max_size = $1, $2.to_i, $3.to_i
|
1178
|
+
case _type
|
1179
|
+
|
1180
|
+
when 'R'
|
1181
|
+
strval = value.to_s
|
1182
|
+
re = Regexp.new('^(-)?(\d+)(\.\d+)?$')
|
1183
|
+
md = re.match strval
|
1184
|
+
if md.nil?
|
1185
|
+
raise "#{location}: '#{strval}' - not matching format #{fmt}"
|
1186
|
+
# warn "#{strval} - not matching format #{fmt}"
|
1187
|
+
# err_count += 1
|
1188
|
+
end
|
1189
|
+
|
1190
|
+
len = strval.length
|
1191
|
+
# Sign char does not go into length count:
|
1192
|
+
len -= 1 if md[1]=='-'
|
1193
|
+
# Decimal char does not go into length count:
|
1194
|
+
len -= 1 if not md[3].nil?
|
1195
|
+
|
1196
|
+
# break if not required? and len == 0
|
1197
|
+
if required? or len != 0
|
1198
|
+
if len > max_size.to_i
|
1199
|
+
# if _upto.nil? and len != _size.to_i or len > _size.to_i
|
1200
|
+
EDI::logger.warn "Context in #{location}: #{_type}, #{min_size}, #{max_size}; #{md[1]}, #{md[2]}, #{md[3]}"
|
1201
|
+
EDI::logger.warn "Max length exceeded in #{location}: #{len} vs. #{max_size}"
|
1202
|
+
err_count += 1
|
1203
|
+
# warn " (strval was: '#{strval}')"
|
1204
|
+
end
|
1205
|
+
if md[1] =~/^0+/
|
1206
|
+
EDI::logger.warn "#{strval} contains leading zeroes"
|
1207
|
+
err_count += 1
|
1208
|
+
end
|
1209
|
+
if md[3] and md[3]=~ /.0+$/
|
1210
|
+
EDI::logger.warn "#{strval} contains trailing decimal sign/zeroes"
|
1211
|
+
err_count += 1
|
1212
|
+
end
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
when /N\d+/
|
1216
|
+
len = (str=value.to_s).length
|
1217
|
+
len -= 1 if str[0]==?- # Don't count sign in length
|
1218
|
+
if len > max_size # len < min_size is ok, would be left-padded
|
1219
|
+
EDI::logger.warn "#{@name}: Value is '#{value}'"
|
1220
|
+
EDI::logger.warn "Length mismatch in #{location}: #{len} vs. #{min_size}/#{max_size}"
|
1221
|
+
err_count += 1
|
1222
|
+
end
|
1223
|
+
|
1224
|
+
when 'AN'
|
1225
|
+
len = value.to_s.length
|
1226
|
+
if len > max_size
|
1227
|
+
EDI::logger.warn "#{@name}: Value is '#{value}'"
|
1228
|
+
EDI::logger.warn "Length mismatch in #{location}: #{len} vs. #{min_size}/#{max_size} - content will be truncated!"
|
1229
|
+
err_count += 1
|
1230
|
+
elsif len < min_size
|
1231
|
+
EDI::logger.warn "#{@name}: Value is '#{value}'"
|
1232
|
+
EDI::logger.warn "Length mismatch in #{location}: #{len} vs. #{min_size}/#{max_size} (content will be right-padded)"
|
1233
|
+
# err_count += 1
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
when 'ID', 'DT', 'TM'
|
1237
|
+
len = value.to_s.length
|
1238
|
+
unless len.between?( min_size, max_size )
|
1239
|
+
EDI::logger.warn "#{@name}: Value is '#{value}'"
|
1240
|
+
EDI::logger.warn "Length mismatch in #{location}: #{len} vs. #{min_size}/#{max_size}"
|
1241
|
+
err_count += 1
|
1242
|
+
end
|
1243
|
+
when 'XX'
|
1244
|
+
# Currently, this case only affects I15, which is a Fixnum,
|
1245
|
+
# but represents a character
|
1246
|
+
if RUBY_VERSION < '1.9'
|
1247
|
+
x_from, x_to = 1, 255
|
1248
|
+
else
|
1249
|
+
x_from, x_to = "\001", "\377"
|
1250
|
+
end
|
1251
|
+
unless value.between?(x_from, x_to)
|
1252
|
+
EDI::logger.warn "#{@name}: Value is '#{value}'"
|
1253
|
+
EDI::logger.warn "Cannot be encoded as a character!"
|
1254
|
+
err_count += 1
|
1255
|
+
end
|
1256
|
+
else
|
1257
|
+
raise "#{location}: Illegal format prefix #{_type}"
|
1258
|
+
# err_count += 1
|
1259
|
+
end
|
1260
|
+
|
1261
|
+
else
|
1262
|
+
EDI::logger.warn "#{location}: Illegal format: #{fmt}!"
|
1263
|
+
err_count += 1
|
1264
|
+
end
|
1265
|
+
end
|
1266
|
+
err_count
|
1267
|
+
end
|
1268
|
+
end
|
1269
|
+
|
1270
|
+
|
1271
|
+
#########################################################################
|
1272
|
+
#
|
1273
|
+
# = Class StreamingParser
|
1274
|
+
#
|
1275
|
+
# == Introduction
|
1276
|
+
#
|
1277
|
+
# Turning a whole EDI interchange into an EDI::A::Interchange object
|
1278
|
+
# with method +parse+ is both convenient and memory consuming.
|
1279
|
+
# Sometimes, interchanges become just too large to keep them completely
|
1280
|
+
# in memory.
|
1281
|
+
# The same reasoning holds for large XML documents, where there is a
|
1282
|
+
# common solution: The SAX/SAX2 API, a streaming approach. This class
|
1283
|
+
# implements the same idea for EDI data.
|
1284
|
+
#
|
1285
|
+
# Use StreamingParser instances to parse ANSI X12 data *sequentially*.
|
1286
|
+
# Sequential parsing saves main memory and is applicable to
|
1287
|
+
# arbitrarily large interchanges.
|
1288
|
+
#
|
1289
|
+
# At its core lies method +go+. It scans the input stream and
|
1290
|
+
# employs callbacks <tt>on_*</tt> which implement most of the parser tasks.
|
1291
|
+
#
|
1292
|
+
# == Syntax check
|
1293
|
+
#
|
1294
|
+
# Without your customizing the callbacks, this parser just scans
|
1295
|
+
# through the data. Only callback <tt>on_error()</tt> contains code:
|
1296
|
+
# It raises an exception telling you about the location and kind
|
1297
|
+
# of syntax error encountered.
|
1298
|
+
#
|
1299
|
+
# === Example: Syntax check
|
1300
|
+
#
|
1301
|
+
# parser = EDI::A::StreamingParser.new
|
1302
|
+
# parser.go( File.open 'damaged_file.x12' )
|
1303
|
+
# --> EDI::EDISyntaxError at offset 1234, last chars = UNt+1+0
|
1304
|
+
#
|
1305
|
+
#
|
1306
|
+
# == Callbacks
|
1307
|
+
#
|
1308
|
+
# Most callbacks provided here are just empty shells. They usually receive
|
1309
|
+
# a string of interest (a segment content, i.e. everything from the segment
|
1310
|
+
# tag to and excluding the segment terminator) and also the
|
1311
|
+
# segment tag as a separate string when tags could differ.
|
1312
|
+
#
|
1313
|
+
# Overwrite them to adapt the parser to your needs!
|
1314
|
+
#
|
1315
|
+
# === Example: Counting segments
|
1316
|
+
#
|
1317
|
+
# class MyParser < EDI::A::StreamingParser
|
1318
|
+
# attr_reader :counters
|
1319
|
+
#
|
1320
|
+
# def initialize
|
1321
|
+
# @counters = Hash.new(0)
|
1322
|
+
# super
|
1323
|
+
# end
|
1324
|
+
#
|
1325
|
+
# def on_segment( s, tag )
|
1326
|
+
# @counters[tag] += 1
|
1327
|
+
# end
|
1328
|
+
# end
|
1329
|
+
#
|
1330
|
+
# parser = MyParser.new
|
1331
|
+
# parser.go( File.open 'myfile.x12' )
|
1332
|
+
# puts "Segment tag statistics:"
|
1333
|
+
# parser.counters.keys.sort.each do |tag|
|
1334
|
+
# print "%03s: %4d\n" % [ tag, parser.counters[tag] ]
|
1335
|
+
# end
|
1336
|
+
#
|
1337
|
+
# == Want to save time? Throw <tt>:done</tt> when already done!
|
1338
|
+
#
|
1339
|
+
# Most callbacks may <b>terminate further parsing</b> by throwing
|
1340
|
+
# symbol <tt>:done</tt>. This saves a lot of time e.g. if you already
|
1341
|
+
# found what you were looking for. Otherwise, parsing continues
|
1342
|
+
# until +getc+ hits +EOF+ or an error occurs.
|
1343
|
+
#
|
1344
|
+
# === Example: A simple search
|
1345
|
+
#
|
1346
|
+
# parser = EDI::A::StreamingParser.new
|
1347
|
+
# def parser.on_segment( s, tag ) # singleton
|
1348
|
+
# if tag == 'CLM'
|
1349
|
+
# puts "Interchange contains at least one segment CLM !"
|
1350
|
+
# puts "Here is its contents: #{s}"
|
1351
|
+
# throw :done # Skip further parsing
|
1352
|
+
# end
|
1353
|
+
# end
|
1354
|
+
# parser.go( File.open 'myfile.x12' )
|
1355
|
+
|
1356
|
+
class StreamingParser
|
1357
|
+
|
1358
|
+
def initialize
|
1359
|
+
@path = 'input stream'
|
1360
|
+
end
|
1361
|
+
|
1362
|
+
# Convenience method. Returns the path of the File object
|
1363
|
+
# passed to method +go+ or just string 'input stream'
|
1364
|
+
def path
|
1365
|
+
@path
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
# Called at start of reading - overwrite for your init purposes.
|
1369
|
+
# Note: Must *not* throw <tt>:done</tt> !
|
1370
|
+
#
|
1371
|
+
def on_interchange_start
|
1372
|
+
end
|
1373
|
+
|
1374
|
+
# Called at EOF - overwrite for your cleanup purposes.
|
1375
|
+
# Note: Must *not* throw <tt>:done</tt> !
|
1376
|
+
#
|
1377
|
+
def on_interchange_end
|
1378
|
+
end
|
1379
|
+
|
1380
|
+
# Called when ISA encountered
|
1381
|
+
#
|
1382
|
+
def on_isa( s, tag )
|
1383
|
+
end
|
1384
|
+
|
1385
|
+
# Called when IEA encountered
|
1386
|
+
#
|
1387
|
+
def on_iea( s, tag )
|
1388
|
+
end
|
1389
|
+
|
1390
|
+
# Called when GS encountered
|
1391
|
+
#
|
1392
|
+
def on_gs( s )
|
1393
|
+
end
|
1394
|
+
|
1395
|
+
# Called when GE encountered
|
1396
|
+
#
|
1397
|
+
def on_ge( s )
|
1398
|
+
end
|
1399
|
+
|
1400
|
+
# Called when ST encountered
|
1401
|
+
#
|
1402
|
+
def on_st( s, tag )
|
1403
|
+
end
|
1404
|
+
|
1405
|
+
# Called when SE encountered
|
1406
|
+
#
|
1407
|
+
def on_se( s, tag )
|
1408
|
+
end
|
1409
|
+
|
1410
|
+
# Called when any other segment encountered
|
1411
|
+
#
|
1412
|
+
def on_segment( s, tag )
|
1413
|
+
end
|
1414
|
+
|
1415
|
+
# This callback is usually kept empty. It is called when the parser
|
1416
|
+
# finds strings between segments or in front of or trailing an interchange.
|
1417
|
+
#
|
1418
|
+
# Strictly speaking, such strings are not permitted by the ANSI X12
|
1419
|
+
# syntax rules. However, it is quite common to put a line break
|
1420
|
+
# between segments for better readability. The default settings thus
|
1421
|
+
# ignore such occurrences.
|
1422
|
+
#
|
1423
|
+
# If you need strict conformance checking, feel free to put some code
|
1424
|
+
# into this callback method, otherwise just ignore it.
|
1425
|
+
#
|
1426
|
+
#
|
1427
|
+
def on_other( s )
|
1428
|
+
end
|
1429
|
+
|
1430
|
+
# Called upon syntax errors. Parsing should be aborted now.
|
1431
|
+
#
|
1432
|
+
def on_error(err, offset, fragment, c=nil)
|
1433
|
+
raise err, "offset = %d, last chars = %s%s" %
|
1434
|
+
[offset, fragment, c.nil? ? '<EOF>' : c.chr]
|
1435
|
+
end
|
1436
|
+
|
1437
|
+
#
|
1438
|
+
# The one-pass reader & dispatcher of segments, SAX-style.
|
1439
|
+
#
|
1440
|
+
# It reads sequentially through the given stream of octets and
|
1441
|
+
# generates calls to the callbacks <tt>on_...</tt>
|
1442
|
+
# Parameter +hnd+ may be any object supporting method +getc+.
|
1443
|
+
#
|
1444
|
+
def go( hnd )
|
1445
|
+
state, offset, item, tag = :outside, 0, '', ''
|
1446
|
+
seg_term, de_sep, ce_sep, rep_sep = nil, nil, nil, nil
|
1447
|
+
isa_count = nil
|
1448
|
+
|
1449
|
+
@path = hnd.path if hnd.respond_to? :path
|
1450
|
+
|
1451
|
+
self.on_interchange_start
|
1452
|
+
|
1453
|
+
catch(:done) do
|
1454
|
+
loop do
|
1455
|
+
c = hnd.getc
|
1456
|
+
|
1457
|
+
case state # State machine
|
1458
|
+
|
1459
|
+
# Characters outside of a segment context
|
1460
|
+
when :outside
|
1461
|
+
case c
|
1462
|
+
|
1463
|
+
when nil
|
1464
|
+
break # Regular exit at EOF
|
1465
|
+
|
1466
|
+
when (?A..?Z)
|
1467
|
+
unless item.empty? # Flush
|
1468
|
+
self.on_other( item )
|
1469
|
+
item = ''
|
1470
|
+
end
|
1471
|
+
item << c; tag << c
|
1472
|
+
state = :tag1
|
1473
|
+
|
1474
|
+
else
|
1475
|
+
item << c
|
1476
|
+
end
|
1477
|
+
|
1478
|
+
# Found first tag char, now expecting second
|
1479
|
+
when :tag1
|
1480
|
+
case c
|
1481
|
+
|
1482
|
+
when (?A..?Z),(?0..?9)
|
1483
|
+
item << c; tag << c
|
1484
|
+
state = :tag2
|
1485
|
+
|
1486
|
+
else # including 'nil'
|
1487
|
+
self.on_error(EDISyntaxError, offset, item, c)
|
1488
|
+
end
|
1489
|
+
|
1490
|
+
# Found second tag char, now expecting optional last
|
1491
|
+
when :tag2
|
1492
|
+
case c
|
1493
|
+
when (?A..?Z),(?0..?9)
|
1494
|
+
item << c; tag << c
|
1495
|
+
if tag=='ISA'
|
1496
|
+
state = :in_isa
|
1497
|
+
isa_count = 0
|
1498
|
+
else
|
1499
|
+
state = :in_segment
|
1500
|
+
end
|
1501
|
+
when de_sep
|
1502
|
+
item << c
|
1503
|
+
state = :in_segment
|
1504
|
+
else # including 'nil'
|
1505
|
+
self.on_error(EDISyntaxError, offset, item, c)
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
when :in_isa
|
1509
|
+
self.on_error(EDISyntaxError, offset, item) if c.nil?
|
1510
|
+
item << c; isa_count += 1
|
1511
|
+
case isa_count
|
1512
|
+
when 1; de_sep = c
|
1513
|
+
when 80; rep_sep = c # FIXME: Version 5.x only
|
1514
|
+
when 102; ce_sep = c
|
1515
|
+
when 103
|
1516
|
+
seg_term = c
|
1517
|
+
dispatch_item( item , tag,
|
1518
|
+
[ce_sep, de_sep, rep_sep||' ', seg_term] )
|
1519
|
+
item, tag = '', ''
|
1520
|
+
state = :outside
|
1521
|
+
end
|
1522
|
+
if isa_count > 103 # Should never occur
|
1523
|
+
EDI::logger.warn "isa_count = #{isa_count}"
|
1524
|
+
self.on_error(EDISyntaxError, offset, item, c)
|
1525
|
+
end
|
1526
|
+
|
1527
|
+
when :in_segment
|
1528
|
+
case c
|
1529
|
+
when nil
|
1530
|
+
self.on_error(EDISyntaxError, offset, item)
|
1531
|
+
when seg_term
|
1532
|
+
dispatch_item( item , tag )
|
1533
|
+
item, tag = '', ''
|
1534
|
+
state = :outside
|
1535
|
+
else
|
1536
|
+
item << c
|
1537
|
+
end
|
1538
|
+
|
1539
|
+
else # Should never occur...
|
1540
|
+
raise ArgumentError, "unexpected state: #{state}"
|
1541
|
+
end
|
1542
|
+
offset += 1
|
1543
|
+
end # loop
|
1544
|
+
# self.on_error(EDISyntaxError, offset, item) unless state==:outside
|
1545
|
+
end # catch(:done)
|
1546
|
+
self.on_interchange_end
|
1547
|
+
offset
|
1548
|
+
end
|
1549
|
+
|
1550
|
+
private
|
1551
|
+
|
1552
|
+
# Private dispatch method to simplify the parser
|
1553
|
+
|
1554
|
+
def dispatch_item( item, tag, other=nil ) # :nodoc:
|
1555
|
+
case tag
|
1556
|
+
when 'ISA'
|
1557
|
+
on_isa( item, tag, other )
|
1558
|
+
when 'IEA'
|
1559
|
+
on_iea( item, tag )
|
1560
|
+
when 'GS'
|
1561
|
+
on_gs( item )
|
1562
|
+
when 'GE'
|
1563
|
+
on_ge( item )
|
1564
|
+
when 'ST'
|
1565
|
+
on_st( item, tag )
|
1566
|
+
when 'SE'
|
1567
|
+
on_se( item, tag )
|
1568
|
+
when /[A-Z][A-Z0-9]{1,2}/
|
1569
|
+
on_segment( item, tag )
|
1570
|
+
else
|
1571
|
+
self.on_error(EDISyntaxError, offset, "Illegal tag: #{tag}")
|
1572
|
+
end
|
1573
|
+
end
|
1574
|
+
|
1575
|
+
end # StreamingParser
|
1576
|
+
|
1577
|
+
#########################################################################
|
1578
|
+
#
|
1579
|
+
# = Class StreamingBuilder
|
1580
|
+
#
|
1581
|
+
# The StreamingBuilder parses the input stream just like StreamingParser
|
1582
|
+
# and in addition builds the complete interchange.
|
1583
|
+
#
|
1584
|
+
# This method is the new basis of Interchange.parse. You might want to
|
1585
|
+
# study its callbacks to get some ideas on how to create a special-purpose
|
1586
|
+
# parser/builder of your own.
|
1587
|
+
#
|
1588
|
+
|
1589
|
+
class StreamingBuilder < StreamingParser
|
1590
|
+
def initialize(auto_validate=true)
|
1591
|
+
@ic = nil
|
1592
|
+
@curr_group = @curr_msg = nil
|
1593
|
+
@auto_validate = auto_validate
|
1594
|
+
end
|
1595
|
+
|
1596
|
+
|
1597
|
+
def interchange
|
1598
|
+
@ic
|
1599
|
+
end
|
1600
|
+
|
1601
|
+
|
1602
|
+
def on_isa( s, tag, seps ) # Expecting: "ISA*...~"
|
1603
|
+
params = { :version => s[84,5] } # '00401', '00500',
|
1604
|
+
[:ce_sep, :de_sep, :rep_sep, :seg_term].each_with_index do |sep, i|
|
1605
|
+
params[sep] = seps[i]
|
1606
|
+
end
|
1607
|
+
@ic = Interchange.new( params )
|
1608
|
+
@ic.header = Segment.parse( @ic, s )
|
1609
|
+
end
|
1610
|
+
|
1611
|
+
def on_iea( s, tag )
|
1612
|
+
@ic.trailer = Segment.parse( @ic, s )
|
1613
|
+
end
|
1614
|
+
|
1615
|
+
def on_gs( s )
|
1616
|
+
@curr_group = @ic.new_msggroup( @ic.parse_segment(s,'GS') )
|
1617
|
+
@curr_group.header = Segment.parse( @curr_group, s )
|
1618
|
+
end
|
1619
|
+
|
1620
|
+
def on_ge( s )
|
1621
|
+
@curr_group.trailer = Segment.parse( @curr_group, s )
|
1622
|
+
@ic.add( @curr_group, @auto_validate )
|
1623
|
+
end
|
1624
|
+
|
1625
|
+
def on_st( s, tag )
|
1626
|
+
seg = @ic.parse_segment(s,tag)
|
1627
|
+
@curr_msg = @curr_group.new_message( seg )
|
1628
|
+
# @curr_msg = (@curr_group || @ic).new_message( @ic.parse_segment(s,tag) )
|
1629
|
+
@curr_msg.header = Segment.parse( @curr_msg, s )
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
def on_se( s, tag )
|
1633
|
+
@curr_msg.trailer = Segment.parse( @curr_msg, s )
|
1634
|
+
# puts "on_unt_uit: #@curr_msg"
|
1635
|
+
@curr_group.add( @curr_msg )
|
1636
|
+
end
|
1637
|
+
|
1638
|
+
# Overwrite this method to react on segments of interest
|
1639
|
+
#
|
1640
|
+
# Note: For a skeleton Builder (just ISA/GS/ST etc), overwrite with
|
1641
|
+
# an empty method.
|
1642
|
+
#
|
1643
|
+
def on_segment( s, tag )
|
1644
|
+
@curr_msg.add @curr_msg.parse_segment( s )
|
1645
|
+
super
|
1646
|
+
end
|
1647
|
+
|
1648
|
+
|
1649
|
+
def on_interchange_end
|
1650
|
+
if @auto_validate
|
1651
|
+
@ic.header.validate
|
1652
|
+
@ic.trailer.validate
|
1653
|
+
# Content is already validated through @ic.add() and @curr_group.add()
|
1654
|
+
end
|
1655
|
+
end
|
1656
|
+
|
1657
|
+
end # StreamingBuilder
|
1658
|
+
|
1659
|
+
|
1660
|
+
# Just an idea - not sure it's worth an implementation...
|
1661
|
+
#########################################################################
|
1662
|
+
#
|
1663
|
+
# = Class StreamingSkimmer
|
1664
|
+
#
|
1665
|
+
# The StreamingSkimmer works as a simplified StreamingBuilder.
|
1666
|
+
# It only skims through the service segements of an interchange and
|
1667
|
+
# builds an interchange skeleton from them containing just the interchange,
|
1668
|
+
# group, and message level, but *not* the regular messages.
|
1669
|
+
# Thus, all messages are *empty* and not fit for validation
|
1670
|
+
# (use class StreamingBuilder to build a complete interchange).
|
1671
|
+
#
|
1672
|
+
# StreamingSkimmer lacks an implementation of callback
|
1673
|
+
# method <tt>on_segment()</tt>. The interchange skeletons it produces are
|
1674
|
+
# thus quicky built and hace a small memory footprint.
|
1675
|
+
# Customize the class by overwriting <tt>on_segment()</tt>.
|
1676
|
+
#
|
1677
|
+
|
1678
|
+
class StreamingSkimmer < StreamingBuilder
|
1679
|
+
def on_segment( s, tag )
|
1680
|
+
# Deliberately left empty
|
1681
|
+
end
|
1682
|
+
end
|
1683
|
+
|
1684
|
+
end # module EDI::A
|