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.
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