edi4r 0.9.4.1 → 0.9.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/AuthorCopyright +3 -3
  3. data/{ChangeLog → Changelog} +60 -0
  4. data/README +15 -10
  5. data/Tutorial +2 -3
  6. data/VERSION +1 -1
  7. data/bin/edi2xml.rb +12 -16
  8. data/bin/editool.rb +9 -5
  9. data/bin/sedas2eancom02.rb +1385 -0
  10. data/bin/xml2edi.rb +7 -12
  11. data/data/edifact/iso9735/SDCD.20000.csv +1 -0
  12. data/data/edifact/iso9735/SDCD.3for2.csv +1 -0
  13. data/data/edifact/iso9735/SDED.20000.csv +6 -0
  14. data/data/edifact/iso9735/SDED.30000.csv +43 -43
  15. data/data/edifact/iso9735/SDED.3for2.csv +6 -0
  16. data/data/edifact/iso9735/SDED.40000.csv +129 -129
  17. data/data/edifact/iso9735/SDED.40100.csv +130 -130
  18. data/data/edifact/iso9735/SDMD.20000.csv +6 -0
  19. data/data/edifact/iso9735/SDMD.30000.csv +6 -6
  20. data/data/edifact/iso9735/SDMD.3for2.csv +6 -0
  21. data/data/edifact/iso9735/SDMD.40000.csv +17 -17
  22. data/data/edifact/iso9735/SDMD.40100.csv +17 -17
  23. data/data/edifact/iso9735/SDSD.20000.csv +5 -0
  24. data/data/edifact/iso9735/SDSD.3for2.csv +5 -0
  25. data/data/edifact/untdid/EDMD.d01b.csv +1 -1
  26. data/data/sedas/EDCD..csv +0 -0
  27. data/data/sedas/EDED..csv +859 -0
  28. data/data/sedas/EDMD..csv +16 -0
  29. data/data/sedas/EDSD..csv +44 -0
  30. data/lib/edi4r.rb +147 -67
  31. data/lib/edi4r/ansi_x12-rexml.rb +91 -0
  32. data/lib/edi4r/ansi_x12.rb +1684 -0
  33. data/lib/edi4r/diagrams.rb +75 -14
  34. data/lib/edi4r/edifact-rexml.rb +4 -3
  35. data/lib/edi4r/edifact.rb +505 -202
  36. data/lib/edi4r/rexml.rb +13 -7
  37. data/lib/edi4r/sedas.rb +854 -0
  38. data/lib/edi4r/standards.rb +150 -33
  39. data/test/damaged_file.edi +1 -0
  40. data/test/eancom2webedi.rb +1 -0
  41. data/test/groups.edi +1 -1
  42. data/test/test_basics.rb +16 -9
  43. data/test/test_edi_split.rb +30 -0
  44. data/test/test_loopback.rb +7 -2
  45. data/test/test_rexml.rb +34 -2
  46. data/test/test_service_messages.rb +190 -0
  47. data/test/test_streaming.rb +167 -0
  48. data/test/test_tut_examples.rb +3 -1
  49. data/test/webedi2eancom.rb +1 -0
  50. 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