edi4r 0.9.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/AuthorCopyright +10 -0
  2. data/COPYING +56 -0
  3. data/ChangeLog +106 -0
  4. data/README +66 -0
  5. data/TO-DO +35 -0
  6. data/Tutorial +609 -0
  7. data/VERSION +1 -0
  8. data/bin/edi2xml.rb +103 -0
  9. data/bin/editool.rb +151 -0
  10. data/bin/xml2edi.rb +50 -0
  11. data/data/edifact/iso9735/SDCD.10000.csv +10 -0
  12. data/data/edifact/iso9735/SDCD.20000.csv +10 -0
  13. data/data/edifact/iso9735/SDCD.30000.csv +11 -0
  14. data/data/edifact/iso9735/SDCD.40000.csv +31 -0
  15. data/data/edifact/iso9735/SDCD.40100.csv +31 -0
  16. data/data/edifact/iso9735/SDED.10000.csv +37 -0
  17. data/data/edifact/iso9735/SDED.20000.csv +37 -0
  18. data/data/edifact/iso9735/SDED.30000.csv +43 -0
  19. data/data/edifact/iso9735/SDED.40000.csv +129 -0
  20. data/data/edifact/iso9735/SDED.40100.csv +130 -0
  21. data/data/edifact/iso9735/SDMD.10000.csv +0 -0
  22. data/data/edifact/iso9735/SDMD.20000.csv +0 -0
  23. data/data/edifact/iso9735/SDMD.30000.csv +6 -0
  24. data/data/edifact/iso9735/SDMD.40000.csv +17 -0
  25. data/data/edifact/iso9735/SDMD.40100.csv +17 -0
  26. data/data/edifact/iso9735/SDSD.10000.csv +8 -0
  27. data/data/edifact/iso9735/SDSD.20000.csv +8 -0
  28. data/data/edifact/iso9735/SDSD.30000.csv +12 -0
  29. data/data/edifact/iso9735/SDSD.40000.csv +34 -0
  30. data/data/edifact/iso9735/SDSD.40100.csv +34 -0
  31. data/data/edifact/untdid/EDCD.d01b.csv +200 -0
  32. data/data/edifact/untdid/EDCD.d96a.csv +161 -0
  33. data/data/edifact/untdid/EDED.d01b.csv +641 -0
  34. data/data/edifact/untdid/EDED.d96a.csv +462 -0
  35. data/data/edifact/untdid/EDMD.d01b.csv +3419 -0
  36. data/data/edifact/untdid/EDMD.d96a.csv +2144 -0
  37. data/data/edifact/untdid/EDSD.d01b.csv +158 -0
  38. data/data/edifact/untdid/EDSD.d96a.csv +127 -0
  39. data/data/edifact/untdid/IDCD.d01b.csv +95 -0
  40. data/data/edifact/untdid/IDMD.d01b.csv +238 -0
  41. data/data/edifact/untdid/IDSD.d01b.csv +75 -0
  42. data/lib/edi4r.rb +928 -0
  43. data/lib/edi4r/diagrams.rb +567 -0
  44. data/lib/edi4r/edi4r-1.2.dtd +20 -0
  45. data/lib/edi4r/edifact-rexml.rb +221 -0
  46. data/lib/edi4r/edifact.rb +1627 -0
  47. data/lib/edi4r/rexml.rb +256 -0
  48. data/lib/edi4r/standards.rb +495 -0
  49. data/test/eancom2webedi.rb +380 -0
  50. data/test/groups.edi +1 -0
  51. data/test/in1.edi +1 -0
  52. data/test/in1.inh +3 -0
  53. data/test/in2.edi +1 -0
  54. data/test/in2.xml +350 -0
  55. data/test/test_basics.rb +209 -0
  56. data/test/test_edi_split.rb +53 -0
  57. data/test/test_loopback.rb +21 -0
  58. data/test/test_minidemo.rb +84 -0
  59. data/test/test_rexml.rb +98 -0
  60. data/test/test_tut_examples.rb +131 -0
  61. data/test/webedi2eancom.rb +408 -0
  62. metadata +110 -0
@@ -0,0 +1,567 @@
1
+ # Classes related to message diagrams and validation
2
+ # for the EDI module "edi4r", a class library
3
+ # to parse and create UN/EDIFACT and other EDI data
4
+ #
5
+ # :include: ../../AuthorCopyright
6
+ #
7
+ # $Id: diagrams.rb,v 1.8 2006/05/26 16:57:37 werntges Exp $
8
+ #--
9
+ # $Log: diagrams.rb,v $
10
+ # Revision 1.8 2006/05/26 16:57:37 werntges
11
+ # V 0.9.3 snapshot. RDoc added, some refactoring / renaming / cleanups
12
+ #
13
+ # Revision 1.7 2006/04/28 14:25:42 werntges
14
+ # 0.9.1 snapshot
15
+ #
16
+ # Revision 1.6 2006/03/28 22:24:58 werntges
17
+ # changed from strings to symbols as parameter keys, e.g. :d0051
18
+ #
19
+ # Revision 1.5 2006/03/22 16:57:17 werntges
20
+ # id --> object_id
21
+ #
22
+ # Revision 1.4 2004/02/19 17:34:58 heinz
23
+ # HWW: Snapshot after REMADV mapping
24
+ #
25
+ # Revision 1.3 2004/02/11 23:39:14 heinz
26
+ # HWW: IDoc support added, lots of dead code/data removed,
27
+ # caching bug fixed, seek! augmented to accept also Regexp, ...
28
+ #
29
+ # Revision 1.2 2004/02/10 18:17:52 heinz
30
+ # HWW: Temp. check-in, before adding support for multiple standards
31
+ #
32
+ # Revision 1.1 2003/10/22 16:56:30 heinz
33
+ # Initial revision
34
+ #
35
+ #
36
+ # To-do list:
37
+ # all - Just starting this...
38
+ #++
39
+ #
40
+ # Module "EDI::Diagram" bundles the classes needed to maintain
41
+ # branching diagrams, i.e. data structures that formally
42
+ # define message types.
43
+
44
+ module EDI::Diagram
45
+
46
+ #
47
+ # Diagram: A structure class to represent a message diagram (branching diagram)
48
+ #
49
+ # A Diagram is essentially
50
+ # - a Branch object (at top-level)
51
+ # - a description text
52
+ # - a node dictionary (a hash that allows index-like access)
53
+ #
54
+ # In contrast to a simple Branch, all nodes of a Diagram object
55
+ # are indexed (counting from 1) according to their natural sequence
56
+ # in the EDIFACT branching diagram. Thus, access by index is available
57
+ # for Diagram objects, but not for Branch objects.
58
+ #
59
+
60
+ class Diagram
61
+ @@cache = {}
62
+ @@caching = true
63
+ private_class_method :new
64
+
65
+ # A Diagram can become quite complex and memory-consuming.
66
+ # Therefore diagrams are cached after creation, so that they
67
+ # need to be created and maintained only once when there are several
68
+ # messages of the same type in an interchange.
69
+ #
70
+ # Turns off this caching mechanism, saving memory but costing time.
71
+ #
72
+ def Diagram.caching_off
73
+ @@caching = false
74
+ end
75
+
76
+ # Turns on caching (default setting), saving time but costing memory.
77
+ #
78
+ def Diagram.caching_on
79
+ @@caching = true
80
+ end
81
+
82
+ # Tells if caching is currently activated (returns a boolean)
83
+ #
84
+ def Diagram.caching?
85
+ @@caching
86
+ end
87
+
88
+ # Releases memory by flushing the cache. Needed primarily for unit tests,
89
+ # where many if not all available diagrams are created.
90
+
91
+ def Diagram.flush_cache
92
+ @@cache = {}
93
+ end
94
+
95
+ # Creates (and caches) a new diagram. Returns reference to existing diagram
96
+ # when already in cache.
97
+ # std:: The syntax standard key. Currently supported:
98
+ # - 'E' (EDIFACT),
99
+ # - 'I' (SAP IDOC)
100
+ # params:: A hash of parameters that uniquely identify the selected diagram.
101
+ # Internal use only - see source code for details.
102
+ #
103
+ def Diagram.create( std, params )
104
+ case std
105
+ when 'E' # UN/EDIFACT
106
+ par = {:d0051 => 'UN',
107
+ # :d0057 => '',
108
+ :is_iedi => false }.update( params )
109
+ when 'I' # SAP IDocs
110
+ par = params
111
+ # raise "Not implemented yet!"
112
+ else
113
+ raise "Unsupported syntax standard: #{std}"
114
+ end
115
+
116
+ if Diagram.caching?
117
+ #
118
+ # Use param set as key for caching
119
+ #
120
+ key = par.sort {|a,b| a.to_s <=> b.to_s}.hash
121
+ obj = @@cache[key]
122
+ return obj unless obj.nil?
123
+
124
+ obj = new( std, par )
125
+ @@cache[key] = obj # cache & return it
126
+
127
+ else
128
+ new( std, par )
129
+ end
130
+ end
131
+
132
+
133
+ def initialize( std, par ) # :nodoc:
134
+ case std
135
+ when 'E' # UN/EDIFACT
136
+ @base_key = [par[:d0065], # msg type
137
+ par[:d0052], # version
138
+ par[:d0054], # release,
139
+ par[:d0051], # resp. agency
140
+ '',
141
+ # par[:d0057], # assoc. assigned code (subset)
142
+ ''].join(':')
143
+ @msg_type = par[:d0065]
144
+
145
+ @dir = EDI::Dir::Directory.create(std,
146
+ :d0065 => @msg_type,
147
+ :d0052 => par[:d0052],
148
+ :d0054 => par[:d0054],
149
+ :d0051 => par[:d0051],
150
+ :d0057 => par[:d0057],
151
+ :is_iedi => par[:is_iedi])
152
+ when 'I' # SAP IDocs
153
+ @base_key = [par[:IDOCTYPE],
154
+ par[:EXTENSION],
155
+ par[:SAPTYPE],
156
+ '',
157
+ '',
158
+ # par[:d0057], # assoc. assigned code (subset)
159
+ ''].join(':')
160
+ @msg_type = par[:IDOCTYPE]
161
+
162
+ @dir = EDI::Dir::Directory.create(std,
163
+ :DOCTYPE => @msg_type,
164
+ :EXTENSION => par[:EXTENSION],
165
+ :SAPTYPE => par[:SAPTYPE])
166
+ # raise "Not implemented yet!"
167
+ else
168
+ raise "Unsupported syntax standard: #{std}"
169
+ end
170
+
171
+ top_branch = Branch.new( @base_key, nil, self )
172
+ raise "No branch found for key '#{@base_key}'" unless top_branch
173
+
174
+ @diag = top_branch.expand
175
+ @desc = @diag.desc
176
+
177
+ i = 0; @node_dict = {}
178
+ @diag.each { |node| i+=1; node.index=i; @node_dict[i]=node }
179
+ end
180
+
181
+
182
+ # Iterates recursively through all nodes of the diagram.
183
+ #
184
+ def each(&b)
185
+ @diag.each(&b)
186
+ end
187
+
188
+
189
+ # Index access through ordinal number of node, starting with 1 (!).
190
+ #
191
+ def [](i)
192
+ @node_dict[i] # efficient access via hash
193
+ end
194
+
195
+ # Getter for the directory object associated with this diagram.
196
+ #
197
+ def dir
198
+ @dir
199
+ end
200
+
201
+ # Returns the top branch of the diagram.
202
+ #
203
+ def branch
204
+ @diag
205
+ end
206
+
207
+ end
208
+
209
+
210
+ #
211
+ # A Branch is a sequence of Nodes. It corresponds to a segment group without
212
+ # its included groups (no sub-branches). A Branch has a name (+sg_name+)
213
+ # and comes with descriptory text (+desc+).
214
+ #
215
+ # Note that included TNodes may have side chains/branches ("tails").
216
+ #
217
+ class Branch
218
+ attr_accessor :desc
219
+ attr_reader :sg_name
220
+
221
+ # A new Branch object is uniquely identified by the +key+ argument
222
+ # that selects the directory entry and its +sg_name+ (if nto top branch).
223
+ # +root+ is a reference to the Diagram it belongs to.
224
+
225
+ def initialize(key, sg_name, root)
226
+ # puts "Creating branch for key `#{key+sg_name}'..."
227
+ @key = key
228
+ @sg_name = sg_name
229
+ @root = root
230
+
231
+ @nodelist=[]
232
+ b = @root.dir.message( key+sg_name.to_s )
233
+ raise "Lookup failed for key `#{key+sg_name.to_s}'" unless b
234
+ @desc = b.desc
235
+ b.each {|obj| @nodelist << Node.create( obj.name, obj.status, obj.maxrep )}
236
+ end
237
+
238
+ #
239
+ # Recursively add "tails" (branches, segment groups) to TNodes
240
+ #
241
+ def expand
242
+ each do |node|
243
+ if node.is_a? TNode and node.tail == nil
244
+ # puts "Expanding #{node}"
245
+ tail = Branch.new(@key, node.name, @root)
246
+
247
+ # Merge TNode with first tail node (trigger segment)
248
+ trigger_segment = tail.shift
249
+ node.name = trigger_segment.name
250
+ if trigger_segment.status != 'M' or trigger_segment.maxrep != 1
251
+ raise "#{trigger_segment.name}: Not a trigger seg!"
252
+ end
253
+ node.tail = tail.expand # Recursion!
254
+ end
255
+ end
256
+ self
257
+ end
258
+
259
+ # Removes and returns the first node from the node list, cf. Array#shift
260
+ #
261
+ def shift
262
+ @nodelist.shift
263
+ end
264
+
265
+ # Makes +node+ the first node of the node list, cf. Array#unshift
266
+ #
267
+ def unshift(node)
268
+ @nodelist.unshift(node)
269
+ end
270
+
271
+ # Iterate through each node of the node list
272
+ #
273
+ def each
274
+ @nodelist.each {|node|
275
+ yield(node)
276
+ if node.is_a? TNode and node.tail
277
+ node.tail.each {|tn| yield(tn)} # Recursion
278
+ end
279
+ }
280
+ end
281
+
282
+ # def each_pruned
283
+ # @nodelist.each {|node|
284
+ # yield(node) # Simple array interator, no sidechains
285
+ # }
286
+ # end
287
+
288
+ # Access node list by index, cf. Array
289
+ #
290
+ def [](index)
291
+ @nodelist[index]
292
+ end
293
+
294
+ # Returns size of the node list (number of nodes of this branch)
295
+ #
296
+ def size
297
+ @nodelist.size
298
+ end
299
+
300
+ end
301
+
302
+
303
+ #
304
+ # A Node is essentially the representation of a segment in a diagram.
305
+ # It comes in two flavors: Simple nodes (SNode) and T-nodes (TNode)
306
+ # which may have "tails".
307
+ # "Node" is an abstract class - either create a SNode or a TNode.
308
+ #
309
+ class Node
310
+ private_class_method :new # Make it an abstract class
311
+ attr_accessor :name, :index
312
+ attr_reader :status, :maxrep
313
+
314
+ # +name+ :: The node's name, e.g. the segment tag
315
+ # +status+ :: A one-char string like 'M' (mandatory), 'C' (conditional)
316
+ # +rep+ :: The max. number of repetitions of this node as allowed
317
+ # by the diagram specs.
318
+ #
319
+ def Node.create(name, status, rep)
320
+ case name
321
+ when /^SG\d+$/ # T-Node
322
+ return TNode.new(name, status, rep)
323
+ else # Simple node
324
+ return SNode.new(name, status, rep)
325
+ end
326
+ end
327
+
328
+ def initialize(name, status, rep) # :nodoc:
329
+ @name, @status, @maxrep = name, status, rep
330
+ # @template = EDI::Segment.new(name, nil, nil)
331
+ end
332
+
333
+ def to_s
334
+ "%3d - %s, %s, %d" % [@index, @name, @status, @maxrep]
335
+ end
336
+
337
+ def required?
338
+ (@status =~ /[MR]/) ? true : false
339
+ end
340
+
341
+ # Returns +nil+ for an SNode or a reference to the side branch ("tail")
342
+ # of a TNode.
343
+ def tail
344
+ return nil # Only TNode implements this non-trivially
345
+ end
346
+ end
347
+
348
+ #
349
+ # Simple node:
350
+ # Just store name (segment tag), status (M/C etc), max repetitions
351
+ #
352
+ class SNode < Node
353
+ public_class_method :new
354
+ end
355
+
356
+ #
357
+ # T-Node:
358
+ # Additionally maintain the tail branch and the segment group name
359
+ #
360
+ class TNode < Node
361
+ public_class_method :new
362
+ attr_accessor :tail
363
+ attr_reader :sg_name
364
+
365
+ def initialize(name, status, rep)
366
+ super
367
+ @tail, @sg_name = nil, @name
368
+ end
369
+
370
+ end
371
+
372
+ ####################################################################
373
+ #
374
+ # Nodes and more
375
+
376
+ NodeCoords = Struct.new( :branch, :offset, :inst_cnt )
377
+
378
+ # Node co-ordinates: A Struct consisting of the +branch+, its +offset+
379
+ # within the branch, and its instance counter +inst_cnt+ .
380
+
381
+ class NodeCoords
382
+ def to_s
383
+ "<NodeCoords>"+self.branch.object_id.to_s+', '+self.offset.to_s+', '+self.inst_cnt.to_s
384
+ end
385
+ end
386
+
387
+ # NodeInstance
388
+ #
389
+ # A given segment of a real message instance needs an instance counter
390
+ # in addition to its location in the diagram. This applies recursively
391
+ # to all segment groups in which it is embedded. This class is equipped
392
+ # with the additional attributes ("co-ordinates") of node instances.
393
+ #
394
+ # We also use this class to determine the node instance of a segment
395
+ # when parsing a message. This is done in a sequential manner, starting
396
+ # from the current instance, by following the diagram branches up to
397
+ # the next matching segment tag/name.
398
+ #
399
+ class NodeInstance
400
+
401
+ # A new NodeInstance is inititialized to a "virtual" 0th node
402
+ # before the first real node of the diagramm referenced by +diag+ .
403
+ #
404
+ def initialize( diag )
405
+ @diag = diag # Really needed later?
406
+
407
+ # Init. to first segment of top branch, e.g. "UNH" or "EDI_DC40"
408
+ @coord = NodeCoords.new(diag.branch, 0, 0)
409
+ @coord_stack = []
410
+ @down_flag = false
411
+ end
412
+
413
+ # Returns the node's instance counter
414
+ #
415
+ def inst_cnt
416
+ @coord.inst_cnt
417
+ end
418
+
419
+ alias rep inst_cnt
420
+
421
+
422
+ # Returns diagram node corresponding to this instance's co-ordinates
423
+ #
424
+ def node
425
+ @coord.branch[@coord.offset]
426
+ end
427
+
428
+ # Delegate some getters to the underlying diagram node:
429
+ # index, maxrep, name, status
430
+ #
431
+ def name; node.name; end
432
+ def status; node.status; end
433
+ def maxrep; node.maxrep; end
434
+ def index; node.index; end
435
+
436
+ # Returns this node instance's level in the diagram.
437
+ # Note that special UN/EDIFACT rules about level 0 are acknowledged:
438
+ # level == 0 for mandatory SNode instances of the main branch with maxrep==1.
439
+ def level
440
+ depth = @coord_stack.length+1
441
+ return 0 if depth == 1 and node.maxrep == 1 and node.required? and not is_tnode? # Special Level 0 case
442
+ depth # Else: Level is depth of segment group stack + 1 (1 if no SG)
443
+ end
444
+
445
+ # Returns the branch name (segment group name)
446
+ #
447
+ def sg_name
448
+ # (node.is_a? TNode) ? node.sg_name : nil
449
+ if node.is_a? TNode
450
+ return node.sg_name
451
+ end
452
+ @coord.branch.sg_name
453
+ end
454
+
455
+ # +true+ if this is a TNode (trigger node/segment)
456
+ def is_tnode?
457
+ node.is_a? TNode
458
+ end
459
+
460
+
461
+ # Main "workhorse": Seek for next matching segment tag/name
462
+ #
463
+ # Starts at current location, follows the diagram downstream
464
+ # while searching for next matching segment.
465
+ #
466
+ # Returns updated location (self) when found, nil otherwise.
467
+ #
468
+ # Notes:
469
+ # 1. Search fails when trying to skip a required node
470
+ # 2. Search fails if we hit the end of the diagram before a match
471
+ # 3. We might need to loop through segment groups repeatedly!
472
+ #
473
+ def seek!(seg) # Segment, Regexp or String expected
474
+ name = (seg.is_a? EDI::Segment) ? seg.name : seg
475
+ # name = (seg.is_a? String) ? seg : seg.name
476
+ begin
477
+ node = self.node
478
+ # print "Looking for #{name} in #{self.name} @ level #{self.level}..."
479
+ #
480
+ # Case "match"
481
+ #
482
+ if name === node.name # == name
483
+ # puts "match!"
484
+ @coord.inst_cnt += 1
485
+ msg = "Segment #{name} at #{@coord.to_s}: More than #{node.maxrep}!"
486
+ if @coord.inst_cnt > node.maxrep
487
+ raise EDI::EDILookupError, msg
488
+ else
489
+ @down_flag = true if node.is_a? TNode
490
+ return self# .node
491
+ end
492
+ end
493
+ #
494
+ # Missed a required node?
495
+ #
496
+ if node.required? and @coord.inst_cnt == 0 # @unmatched
497
+ msg = "Missing required segment #{node.name} at #{@coord.to_s}\n" + \
498
+ " while looking for segment #{name}!"
499
+ raise EDI::EDILookupError, msg
500
+ end
501
+ # puts
502
+ end while self.next!
503
+ # Already at top level - Error condition!
504
+ raise "End of diagram exceeded!"
505
+ end
506
+
507
+ # Navigation methods, for internal use only
508
+
509
+ protected
510
+
511
+ #
512
+ # Move "up" to the TNode of this branch.
513
+ # Returns +self+, or +nil+ if already at the top.
514
+ def up!
515
+ return nil if @coord_stack.empty?
516
+ @coord = @coord_stack.pop
517
+ self
518
+ end
519
+
520
+ #
521
+ # Move to the right of the current node in this branch.
522
+ # Returns +self+, or +nil+ if already at the branch end.
523
+ def right!
524
+ return nil if @coord.offset+1 == @coord.branch.size
525
+ @coord.offset += 1
526
+ @coord.inst_cnt = 0
527
+ self
528
+ end
529
+
530
+ #
531
+ # Move to the first node of the tail branch of this TNode.
532
+ # Returns +self+, or +nil+ if there is no tail node.
533
+ def down!
534
+ this_node = self.node
535
+ return nil if (tail=this_node.tail).nil?
536
+ # Save current co-ordinates on stack
537
+ @coord_stack.push( @coord )
538
+ # Init. co-ordinates for the new level:
539
+ @coord = NodeCoords.new(tail, 0, 0)
540
+ self
541
+ end
542
+
543
+ #
544
+ # Next: Move down if TNode and same as last match (another SG instance),
545
+ # else right. Move up if neither possible.
546
+ # Returns +self+, or +nil+ if end of diag encountered.
547
+ def next!
548
+ loop do
549
+ node = self.node; r = nil
550
+ if node.is_a? TNode and @down_flag
551
+ @down_flag = false
552
+ r = self.down!
553
+ end
554
+ break if r
555
+ # Down not applicable or available; now try "right!"
556
+ break if r = self.right!
557
+ # At end of this branch - try to move up:
558
+ break if r = self.up!
559
+ # Already at top level!
560
+ return nil
561
+ end
562
+ self
563
+ end
564
+
565
+ end
566
+
567
+ end # module EDI::Diagram