edi4r 0.9.4.1

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 (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,75 @@
1
+ AAI;ACCOMMODATION ALLOCATION INFORMATION;010;E997;M;20;
2
+ ADI;HEALTH CARE CLAIM ADJUDICATION INFORMATION;010;E206;M;3;020;E021;C;3;030;1229;C;1;040;E017;C;15;050;6060;C;3;060;E030;C;15;070;9620;C;4;080;5389;C;1;090;E507;C;1;100;9639;C;1;110;5482;C;1;
3
+ ADS;ADDRESS;010;E817;C;1;020;E001;C;1;030;3164;C;1;040;3251;C;1;050;3207;C;1;060;E819;C;1;070;E517;C;1;
4
+ ALS;ADDITIONAL LOCATION INFORMATION;010;3227;M;1;020;E975;M;99;030;6000;C;1;040;6002;C;1;
5
+ APD;ADDITIONAL TRANSPORT DETAILS;010;E961;C;1;020;E962;C;2;030;E963;C;1;040;E964;C;1;050;E965;C;10;
6
+ ASD;SERVICE DETAILS;010;E959;M;9;020;E013;C;99;030;2160;C;1;
7
+ ATI;TOUR INFORMATION;010;E993;C;1;020;E994;C;99;
8
+ ATR;ATTRIBUTE;010;9017;M;1;020;E003;M;1;
9
+ BLI;BILLABLE INFORMATION;010;5004;C;3;020;E029;C;9;030;E507;C;9;040;7402;C;5;050;9607;C;3;060;E028;C;9;
10
+ CLT;CLEAR TERMINATE INFORMATION;010;1229;M;1;020;1225;C;2;030;4440;C;1;
11
+ CMN;COMMISSION INFORMATION;010;E002;M;9;
12
+ CNX;CONNECTION DETAILS;010;E999;C;9;
13
+ CNY;COUNTRY INFORMATION;010;3207;M;1;020;E013;C;2;030;2031;C;2;040;6345;C;1;050;3453;C;9;
14
+ CON;CONTACT INFORMATION;010;E966;C;20;
15
+ CRI;CONSUMER REFERENCE INFORMATION;010;E967;M;20;
16
+ CUR;CURRENCIES;010;E504;C;1;020;E504;C;1;030;5402;C;1;040;6341;C;1;
17
+ DAV;DAILY AVAILABILITY;010;7037;M;1;020;E009;M;31;
18
+ DIS;DISCOUNT INFORMATION;010;E998;M;20;
19
+ DNT;DENTAL INFORMATION;010;7402;C;1;020;7383;C;5;030;9647;C;5;040;6060;C;6;050;4405;C;1;
20
+ DTI;DATE AND TIME INFORMATION;010;E013;M;9;020;E014;C;99;
21
+ ERI;APPLICATION ERROR INFORMATION;010;E901;M;1;
22
+ FRM;FOLLOW-UP ACTION;010;7402;M;1;020;9013;M;7;030;1229;C;1;
23
+ FRQ;FREQUENCY;010;E520;M;99;
24
+ FTI;FREQUENT TRAVELLER INFORMATION;010;E970;M;9;
25
+ HDI;HARDWARE DEVICE INFORMATION;010;3148;C;1;020;3413;C;1;030;1511;C;1;040;E003;C;9;
26
+ HDR;HEADER INFORMATION;010;4405;M;1;020;E013;M;4;030;1154;C;1;040;4440;C;1;050;7135;C;2;060;3453;C;1;
27
+ ICI;INSURANCE COVER INFORMATION;010;E016;M;1;020;E017;C;5;
28
+ IFT;INTERACTIVE FREE TEXT;010;E971;C;1;020;4440;C;99;
29
+ ITC;INSTITUTIONAL CLAIM;010;E027;M;1;020;6060;M;4;030;E026;M;1;040;9447;M;1;050;E025;C;9;
30
+ ITD;INFORMATION TYPE DATA;010;9601;C;1;020;3453;C;1;030;9603;C;10;040;1503;C;1;
31
+ ITM;ITEM NUMBER;010;E212;C;99;
32
+ LKP;LEVEL INDICATION;010;E778;M;1;
33
+ LNG;LANGUAGE;010;3455;M;1;020;E033;C;1;
34
+ MAP;MESSAGE APPLICATION PRODUCT INFORMATION;010;E022;C;1;020;3036;C;1;030;E031;C;1;
35
+ MES;MEASUREMENTS;010;E175;M;9;
36
+ MOV;CAR DELIVERY INSTRUCTION;010;E995;C;1;020;6350;C;1;030;3453;C;1;
37
+ MSD;MESSAGE ACTION DETAILS;010;E972;C;1;020;4343;C;5;030;E206;C;5;
38
+ NAA;NAME AND ADDRESS;010;3035;M;1;020;E082;C;1;030;3124;C;5;040;E080;C;1;050;3042;C;1;060;3164;C;1;070;3229;C;1;080;3251;C;1;090;3207;C;1;
39
+ NME;NAME;010;E012;M;9;
40
+ NUN;NUMBER OF UNITS;010;E523;M;9;
41
+ ODI;ORIGIN AND DESTINATION DETAILS;010;3225;C;2;020;1050;C;2;
42
+ ODS;ADDITIONAL PRODUCT DETAILS;010;9605;M;1;020;E015;M;999;
43
+ ORG;ORIGINATOR OF REQUEST DETAILS;010;E973;C;1;020;E974;C;1;030;E975;C;1;040;3036;C;1;050;3457;C;1;060;E976;C;1;070;3503;C;1;
44
+ OTI;OTHER INSURANCE;010;E206;M;5;020;9645;M;1;030;E507;C;2;040;5267;C;10;050;5004;C;4;060;E030;C;15;070;4497;C;1;080;9143;C;1;090;9607;C;1;
45
+ PDT;PRODUCT INFORMATION;010;7133;C;1;020;E996;C;26;
46
+ PLI;PRODUCT LOCATION INFORMATION;010;E008;M;99;
47
+ PMT;PAYMENT INFORMATION;010;E977;C;99;020;E978;C;99;
48
+ POP;PERIOD OF OPERATION;010;E013;M;1;020;2160;C;1;030;4405;C;1;
49
+ POR;LOCATION AND/OR RELATED TIME INFORMATION;010;E517;M;1;020;E362;C;2;030;E992;C;2;040;3227;C;1;050;1050;C;1;
50
+ POS;POINT OF SALE INFORMATION;010;E032;C;999;020;E975;C;999;
51
+ PRD;PRODUCT IDENTIFICATION;010;E989;C;9;020;3036;C;6;
52
+ PRE;PRICE DETAILS;010;E018;C;20;020;5213;C;1;
53
+ PRO;PROMOTIONS;010;E019;M;20;
54
+ PRT;PARTY INFORMATION;010;3035;M;1;020;E206;C;9;030;2000;C;3;040;E023;C;1;
55
+ PSI;SERVICE INFORMATION;010;7402;M;4;020;E507;C;5;030;E021;C;1;040;6060;C;2;050;5004;C;5;060;5030;C;4;070;9039;C;1;080;5267;C;1;090;4183;C;9;100;E024;C;1;
56
+ QTI;QUANTITY;010;E035;M;9;
57
+ RCI;RESERVATION CONTROL INFORMATION;010;E979;C;99;
58
+ RFR;REFERENCE;010;E506;M;1;
59
+ RLS;RELATIONSHIP;010;9141;M;1;020;E941;C;1;
60
+ RPI;QUANTITY AND ACTION DETAILS;010;E958;M;9;
61
+ RTC;RATE TYPES;010;5263;M;99;
62
+ RTI;RATE DETAILS;010;E011;M;99;
63
+ RUL;RULE INFORMATION;010;E004;C;99;020;E005;C;9;030;E006;C;9;
64
+ SDT;SELECTION DETAILS;010;E010;M;99;
65
+ SER;FACILITY INFORMATION;010;E965;C;99;020;1229;C;1;030;6350;C;1;040;E013;C;99;050;2160;C;1;
66
+ SSR;SPECIAL REQUIREMENT DETAILS;010;E980;M;1;020;E981;C;999;
67
+ TCE;TIME AND CERTAINTY;010;2380;C;1;020;E946;C;1;
68
+ TDI;TRAVELLER DOCUMENT INFORMATION;010;E968;M;1;020;E969;C;1;030;3500;C;1;040;3460;C;99;
69
+ TFF;TARIFF INFORMATION;010;E982;C;99;020;E983;C;99;030;E984;C;99;
70
+ TIF;TRAVELLER INFORMATION;010;E985;M;1;020;E986;C;99;
71
+ TIZ;TIME ZONE INFORMATION;010;E034;M;1;
72
+ TRF;TRAFFIC RESTRICTION DETAILS;010;E007;M;5;
73
+ TVL;TRAVEL PRODUCT INFORMATION;010;E987;C;1;020;E975;C;2;030;E988;C;1;040;E989;C;1;050;E990;C;1;060;1082;C;1;070;7365;C;1;
74
+ TXS;TAXES;010;E020;M;99;
75
+ VEH;VEHICLE;010;8053;C;1;020;E991;C;1;030;E211;C;1;040;6314;C;1;050;E992;C;1;060;1145;C;1;
@@ -0,0 +1,928 @@
1
+ =begin rdoc
2
+ :main:README
3
+ :title:edi4r
4
+
5
+ = UN/EDIFACT module
6
+
7
+ An API to parse and create UN/EDIFACT and other EDI data
8
+ * Abstract classes are maintained in this file.
9
+ * See other files in edi4r/ for specific EDI syntax standards.
10
+
11
+ $Id: edi4r.rb,v 1.6 2006/08/01 11:12:39 werntges Exp $
12
+
13
+ :include: ../AuthorCopyright
14
+
15
+ == Background
16
+
17
+ We anticipate to support several EDI syntax standards with this module:
18
+
19
+ C Name Description
20
+ ===========================================================================
21
+ A ANSI ASC X.12 the U.S. EDI standard; a precursor of UN/EDIFACT
22
+ E UN/EDIFACT the global EDI standard under the auspices of the UNO
23
+ G GENCOD an early French EDI standard, consumer goods branch
24
+ I SAP IDoc not an EDI standard, but a very popular in-house format
25
+ S SEDAS an early Austrian/German EDI standard, see GENCOD
26
+ T Tradacoms the British EDI standard; a precursor of UN/EDIFACT
27
+ X XML (DTDs / Schemas still to be supplied)
28
+
29
+ Our focus will be on UN/EDIFACT, the only global EDI standard we have
30
+ that is independent of industry branches.
31
+
32
+ Terms used will be borrowed from EDIFACT and applied to the other
33
+ syntax standards whenever possible.
34
+
35
+ A, E, and T are technically related in that they employ a compact
36
+ data representation based on a hierarchy of separator characters.
37
+ G and S as well as I are fixed-record formats, X is a markup syntax.
38
+
39
+ == Data model
40
+
41
+ We use the EDIFACT model as the name-giving, most general model.
42
+ Other EDI standards might not support all features.
43
+
44
+ The basic unit exchanged between trading partners is the "Interchange".
45
+ An interchange consists of an envelope and content. Content is
46
+ either a sequence of messages or a sequence of message groups.
47
+ Message groups - if used - comprise a (group level) envelope and
48
+ a sequence of messages.
49
+
50
+ A message is a sequence of segments (sometimes also called records).
51
+ A segment consists of a sequence of data elements (aka. fields),
52
+ either simple ones (DE) or composites (CDE).
53
+ Composites are sequences of simple data elements.
54
+
55
+ Hence:
56
+
57
+ Interchange > [ MsgGroup > ] Message > Segment > [ CDE > ] DE
58
+
59
+ Syntax related information is maintained at the top (i.e. interchange) level.
60
+ Lower-level objects like segments and DEs are aware of their syntax context
61
+ through attibute "root", even though this context originates at the
62
+ interchange level.
63
+
64
+ Lower levels may add information. E.g. a message may add its message type,
65
+ or a segment its hierarchy level, and its segment group - depending
66
+ on the syntax standard in use.
67
+
68
+ This basic structure is always maintained, even in cases like SAP IDocs
69
+ where the Interchange level is just an abstraction.
70
+
71
+ Note that this data model describes the data as they are parsed or built,
72
+ essentially lists of lists or strings. In contrast, EDI documents
73
+ frequently publish specifications in a hierarchical way, using terms like
74
+ "segment group", "instance", "level" and alike.
75
+ Here we regard such information as metadata, or additional properties
76
+ which may or may not apply or be required.
77
+
78
+ === Example
79
+
80
+ You can build a valid EDIFACT interchange simply by adding
81
+ messages and segments - just follow your specifications.
82
+
83
+ However, if you want this Ruby module to *validate* your result,
84
+ the metadata are required. Similarly, in order to map from EDIFACT to
85
+ other formats, accessing inbound segments though their hierarchical
86
+ representation is much more convenient than processing them linearly.
87
+
88
+ == EDI Class hierarchy (overview)
89
+
90
+ EDI_Object:: Collection, DE
91
+ Collection:: Collection_HT, Collection_S
92
+ Collection_HT:: Interchange, MsgGroup, Message
93
+ Collection_S:: Segment, CDE
94
+ =end
95
+
96
+ # To-do list:
97
+ # validate - add still more functionality, e.g. codelists
98
+ # charset - check for valid chars in more charsets
99
+ # NDB - support codelists
100
+ # (much more, including general cleanup & tuning ...)
101
+
102
+ require 'enumerator'
103
+
104
+ BEGIN {
105
+ require 'pathname'
106
+ # 'realpath' fails on Windows platforms!
107
+ # ENV['EDI_NDB_PATH'] = Pathname.new(__FILE__).dirname.parent.realpath + 'data'
108
+ pdir = Pathname.new(__FILE__).dirname.parent
109
+ ENV['EDI_NDB_PATH'] = File.expand_path(pdir) + File::Separator + 'data'
110
+ }
111
+
112
+ require "edi4r/standards"
113
+ require "edi4r/diagrams"
114
+
115
+
116
+ module EDI
117
+
118
+ #########################################################################
119
+ #
120
+ # Basic (abstract) class: Makes sure that all derived
121
+ # EDI objects have at least following attributes:
122
+ #
123
+ # +parent+:: Reference to parent EDI object (a +Collection+)
124
+ # +root+:: Reference to root EDI object (typically an +Interchange+)
125
+ # +name+:: The name of this instance (a +String+ object)
126
+ #
127
+ # Caveat:: Setters are used only internally during message construction.
128
+ # Avoid using them!
129
+
130
+
131
+ class Object
132
+ attr_accessor :parent, :root, :name
133
+
134
+ def initialize (parent, root, name)
135
+ @parent, @root, @name = parent, root, name
136
+ end
137
+ end
138
+
139
+
140
+ #########################################################################
141
+ #
142
+ # A simple utility class that fills a need not covered by "zlib".
143
+ #
144
+ # It is stripped to the essentials needed here internally.
145
+ # Not recommended for general use! The overhead of starting
146
+ # "bzcat" processes all the time is considerable, binding to a library
147
+ # similar to 'zib' for the BZIP2 format would give much better results.
148
+
149
+ class Bzip2Reader
150
+ attr_accessor :path
151
+
152
+ def initialize( hnd )
153
+ @path = hnd.path
154
+ @pipe = IO.popen("bzcat #@path",'r' )
155
+ end
156
+
157
+ def read( len=0 )
158
+ len==0 ? @pipe.read : @pipe.read( len )
159
+ end
160
+
161
+ def rewind
162
+ @pipe.close
163
+ @pipe = IO.popen("bzcat #@path",'r' )
164
+ end
165
+
166
+ def close
167
+ @pipe.close
168
+ end
169
+ end
170
+
171
+ #########################################################################
172
+ #
173
+ # An EDI collection instance behaves like a simplified array.
174
+ # In addition, it permits access to its elements through their names.
175
+ # This implies that only objects with a +name+ may be stored,
176
+ # i.e. derivatives of EDI::Object.
177
+
178
+ class Collection < EDI::Object
179
+
180
+ def initialize( parent, root, name )
181
+ super
182
+ @a = []
183
+ end
184
+
185
+
186
+ def root= (rt)
187
+ super( rt )
188
+ each {|obj| obj.root = rt }
189
+ end
190
+
191
+
192
+ # Similar to Array#push(), but automatically setting obj's
193
+ # parent and root to self and self's root. Returns obj.
194
+ def add( obj )
195
+ push obj
196
+ obj.parent = self
197
+ obj.root = self.root
198
+ obj
199
+ end
200
+
201
+ alias append add
202
+
203
+
204
+ def ==(obj)
205
+ self.object_id == obj.object_id
206
+ end
207
+
208
+ # Delegate to array:
209
+ # index, each, find_all, length, size, first, last
210
+ def index(obj); @a.index(obj); end
211
+ def each(&b); @a.each(&b); end
212
+ def find_all(&b); @a.find_all(&b); end
213
+ def size; @a.size; end
214
+ def length; @a.length; end
215
+ def first; @a.first; end
216
+ def last; @a.last; end
217
+
218
+ # The element reference operator [] supports two access modes:
219
+ # Array-like:: Return indexed element when passing an integer
220
+ # By name:: Return array of element(s) whose name(s) match given string
221
+ def [](i)
222
+ lookup(i)
223
+ end
224
+
225
+
226
+ # This implementation of +inspect()+ is very verbose in that it
227
+ # inspects also all contained objects in a recursive manner.
228
+ #
229
+ # indent:: String offset to use for indentation / pretty-printing
230
+ # symlist:: Array of getter names (passed as symbols) whose values are
231
+ # to be listed. Note that :name is included automatically.
232
+
233
+ def inspect( indent='', symlist=[] )
234
+ headline = indent + self.name+': ' + symlist.map do |sym|
235
+ "#{sym} = #{(s=send(sym)).nil? ? 'nil' : s.to_s}"
236
+ end.join(', ') + "\n"
237
+ headline << @header.inspect(indent+' ') if @header
238
+ s = @a.inject( headline ){|s,obj| s << obj.inspect(indent+' ')}
239
+ @trailer ? s << @trailer.inspect(indent+' ') : s
240
+ end
241
+
242
+
243
+ # Returns an array of names of all included objects in proper sequence;
244
+ # primarily for internal use.
245
+
246
+ def names
247
+ @a.collect {|e| e.name}
248
+ end
249
+
250
+
251
+ # Helper method: Turns e.g. "EDI::E::Interchange" into "Interchange".
252
+ # For internal use only!
253
+
254
+ def normalized_class_name # :nodoc:
255
+ if self.class.to_s !~ /^(\w*::)?(\w::)(\w+)?$/
256
+ raise "Cannot normalize class name: #{self.class.to_s}"
257
+ end
258
+ $3
259
+ end
260
+
261
+
262
+ private
263
+
264
+ # push: Similar to Array#push, except that it requires objects
265
+ # with getter :name
266
+ # Low-level method, avoid. Use "add" instead.
267
+
268
+ def push( obj )
269
+ raise TypeError unless obj.is_a? EDI::Object # obj.respond_to? :name
270
+ @a << obj
271
+ end
272
+
273
+
274
+ alias << push
275
+
276
+
277
+ def lookup(i)
278
+ if i.is_a?(Integer)
279
+ @a[i]
280
+ else
281
+ @a.find_all {|x| x.name == i}
282
+ end
283
+ end
284
+
285
+ # Here we perform the "magic" that provides us with dynamically
286
+ # "generated" getters and setters for just those DE and CDE
287
+ # available in the given Collection_S instance.
288
+ #
289
+ # UN/EDIFACT examples:
290
+ # d3055, d1004=(value), cC105, a7174[1].value
291
+
292
+ def method_missing(sym, *par)
293
+ if sym.id2name =~ /^([acds])(\w+)(=)?/
294
+ rc = lookup($2)
295
+ if rc.is_a? Array
296
+ if rc.size==1
297
+ rc = rc.first
298
+ elsif rc.size==0
299
+ return super
300
+ end
301
+ end
302
+ if $3
303
+ # Setter
304
+ raise TypeError, "Can't assign to array #$2" if rc.is_a? Array
305
+ raise TypeError, "Can only assign to a DE value" if $1 != 'd'
306
+ rc.value = par[0]
307
+ else
308
+ # Getter
309
+ return rc.value if rc.is_a? DE and $1 == 'd'
310
+ return rc if rc.is_a? CDE and $1 == 'c'
311
+ return rc if rc.is_a? Segment and $1 == 's'
312
+ err_msg = "Method prefix '#$1' not matching result '#{rc.class}'!"
313
+ raise TypeError, err_msg unless rc.is_a? Array
314
+ # Don't let whole DEs be overwritten - enforce usage of "value":
315
+ rc.freeze
316
+ return rc if $1 == 'a'
317
+ raise TypeError,"Array found - use 'a#$2[i]' to access component i"
318
+ end
319
+ else
320
+ super
321
+ end
322
+ end
323
+
324
+ end
325
+
326
+
327
+ #########################################################################
328
+ #
329
+ # A collection with header and trailer, common to Interchange, MsgGroup,
330
+ # and Message. Typically, header and trailer are Segment instances.
331
+ #
332
+ class Collection_HT < Collection
333
+ attr_accessor :header, :trailer # make private or protected?
334
+
335
+ def root= (rt)
336
+ super( rt )
337
+ @header.root = rt if @header
338
+ @trailer.root = rt if @trailer
339
+ end
340
+
341
+
342
+ # Experimental: Ignore content of header / trailer,
343
+ # regard object as empty when nothing "add"ed to it.
344
+
345
+ def empty?
346
+ @a.empty?
347
+ end
348
+
349
+
350
+ def validate( err_count=0 )
351
+ err_count += @header.validate if @header
352
+ err_count += @trailer.validate if @trailer
353
+ each {|obj| err_count += obj.validate}
354
+ err_count
355
+ end
356
+
357
+
358
+ def to_s( postfix='' )
359
+ s = @header ? @header.to_s+postfix : ''
360
+ each {|obj| s << (obj.is_a?(Segment) ? obj.to_s+postfix : obj.to_s)}
361
+ s << @trailer.to_s+postfix if @trailer
362
+ s
363
+ end
364
+
365
+ end
366
+
367
+
368
+ #########################################################################
369
+ #
370
+ # A "segment-like" collection, base class of Segment and CDE
371
+ #
372
+ class Collection_S < Collection
373
+ attr_accessor :status, :rep, :maxrep
374
+
375
+
376
+ def initialize(parent, name, status=nil)
377
+ @status = status
378
+ super( parent, parent.root, name)
379
+ end
380
+
381
+
382
+ def validate( err_count=0 )
383
+ location = "#{parent.name} - #{@name}"
384
+ if empty?
385
+ if required?
386
+ warn "#{location}: Empty though mandatory!"
387
+ err_count += 1
388
+ end
389
+ else
390
+ if rep && maxrep && rep > maxrep
391
+ warn "#{location}: Too often repeated: #{rep} > #{maxrep}!"
392
+ err_count += 1
393
+ end
394
+ each {|obj| err_count += obj.validate}
395
+ end
396
+ err_count
397
+ end
398
+
399
+
400
+ def fmt_of_DE(id) # :nodoc:
401
+ @parent.fmt_of_DE(id)
402
+ end
403
+
404
+
405
+ def each_BCDS(id, &b) # :nodoc:
406
+ @parent.each_BCDS(id, &b)
407
+ end
408
+
409
+
410
+ # Returns +true+ if all contained elements are empty.
411
+
412
+ def empty?
413
+ empty = true
414
+ each {|obj| empty &= obj.empty? } # DE or CDE
415
+ empty
416
+ end
417
+
418
+
419
+ # Returns +true+ if this segment or CDE is mandatory / required
420
+ # according to its defining "Diagram".
421
+
422
+ def required?
423
+ @status == 'M' or @status == 'R'
424
+ end
425
+
426
+
427
+ def inspect( indent='', symlist=[] )
428
+ symlist += [:status, :rep, :maxrep]
429
+ super
430
+ end
431
+
432
+ end
433
+
434
+
435
+ #########################################################################
436
+ #
437
+ # Base class of all interchanges
438
+ #
439
+ class Interchange < Collection_HT
440
+
441
+ attr_accessor :output_mode
442
+ attr_reader :syntax, :version, :output_mode, :illegal_charset_pattern
443
+
444
+ # Abstract class - don't instantiate directly
445
+ #
446
+ def initialize( user_par=nil )
447
+ super( nil, self, 'Interchange' )
448
+ @illegal_charset_pattern = /^$/ # Default: Never match a non-empty string
449
+ @content = nil # nil if empty, :messages, or :groups
450
+ end
451
+
452
+ # Auto-detect file content, optionally decompress, return an
453
+ # Interchange object of the sub-class that matches the (unzipped) content.
454
+ #
455
+ # This is a convenience method.
456
+ # When you know the file contents, consider a direct call to
457
+ # Interchange::E::parse etc.
458
+ #
459
+ # NOTES:
460
+ # * Make sure to <tt>require 'zlib'</tt> when applying this method
461
+ # to gzipped files.
462
+ # * BZIP2 is indirectly supported by calling "bzcat". Make sure that
463
+ # "bzcat" is available when applying this method to *.bz2 files.
464
+ # * Do not pass $stdin to this method - we could not "rewind" it!
465
+
466
+ def Interchange.parse( hnd, auto_validate=true )
467
+ case rc=Interchange.detect( hnd )
468
+ when 'BZ': Interchange.parse( EDI::Bzip2Reader.new( hnd ) ) # see "peek"
469
+ when 'GZ': Interchange.parse( Zlib::GzipReader.new( hnd ) )
470
+ when 'E': EDI::E::Interchange.parse( hnd, auto_validate )
471
+ when 'I': EDI::I::Interchange.parse( hnd, auto_validate )
472
+ when 'XE': EDI::E::Interchange.parse_xml( REXML::Document.new(hnd) )
473
+ when 'XI': EDI::I::Interchange.parse_xml( REXML::Document.new(hnd) )
474
+ else raise "#{rc}: Unsupported format key - don\'t know how to proceed!"
475
+ end
476
+ end
477
+
478
+ # Auto-detect file content, optionally decompress, return an
479
+ # empty Interchange object of that sub-class with only the header filled.
480
+ #
481
+ # This is a convenience method.
482
+ # When you know the file contents, consider a direct call to
483
+ # Interchange::E::peek etc.
484
+ #
485
+ # NOTES: See Interchange.parse
486
+
487
+ def Interchange.peek( hnd=$stdin)
488
+ case rc=Interchange.detect( hnd )
489
+ # Does not exist yet!
490
+ # when 'BZ': Interchange.peek( Zlib::Bzip2Reader.new( hnd ) )
491
+ # Temporary substitute, Unix/Linux only, low performance:
492
+ when 'BZ': Interchange.peek( EDI::Bzip2Reader.new( hnd ) )
493
+
494
+ when 'GZ': Interchange.peek( Zlib::GzipReader.new( hnd ) )
495
+ when 'E': EDI::E::Interchange.peek( hnd )
496
+ when 'I': EDI::I::Interchange.peek( hnd )
497
+ when 'XE': EDI::E::Interchange.peek_xml( REXML::Document.new(hnd) )
498
+ when 'XI': EDI::I::Interchange.peek_xml( REXML::Document.new(hnd) )
499
+ else raise "#{rc}: Unsupported format key - don\'t know how to proceed!"
500
+ end
501
+ end
502
+
503
+ # Auto-detect the given file format & content, return format key
504
+ #
505
+ # Convenience method, intended for internal use only
506
+ #
507
+ def Interchange.detect( hnd ) # :nodoc:
508
+ buf = hnd.read( 256 )
509
+ #
510
+ # NOTE: "rewind" fails when applied to $stdin!
511
+ # If you really need to read from $stdin, call Interchange::E::parse()
512
+ # and Interchange::E::peek() etc. directly to bypass auto-detection
513
+ hnd.rewind
514
+
515
+ re = /(<\?xml.*?)?DOCTYPE\s+Interchange.*?\<Interchange\s+.*?standard\_key\s*=\s*(['"])(.)\2/m
516
+ case buf
517
+ when /^(UNA......)?\r?\n?U[IN]B.UNO[A-Z].[1-4]/: 'E' # UN/EDIFACT
518
+ when /^EDI_DC/: 'I' # SAP IDoc
519
+ when re : 'X'+$3 # XML, Doctype = Interchange, syntax standard key (E, I, ...) postfix
520
+ when /^\037\213/: 'GZ' # gzip
521
+ when /^\037\235/: 'Z' # compress
522
+ when /^\037\036/: 'z' # pack
523
+ when /^BZh[0-\377]/: 'BZ' # bzip2
524
+ else; "?? (stream starts with: #{buf[0..15]})"
525
+ end
526
+ end
527
+
528
+
529
+ def fmt_of_DE(id) # :nodoc:
530
+ de = @basedata.de(id)
531
+ de.nil? ? nil : de.format
532
+ end
533
+
534
+
535
+ def each_BCDS(id, &b) # :nodoc:
536
+ begin
537
+ @basedata.each_BCDS(id, &b )
538
+ rescue EDILookupError # NoMethodError
539
+ raise "Lookup failure for BCDS entry id '#{id}'"
540
+ end
541
+ end
542
+
543
+
544
+ # Add either Message objects or MsgGroup objects to an interchange;
545
+ # mixing both types raises a TypeError.
546
+
547
+ def add( obj, auto_validate=true )
548
+ err_msg = "Added object must also be a "
549
+ if obj.is_a? Message
550
+ @content = :messages unless @content
551
+ raise TypeError, err_msg+"'Message'" if @content != :messages
552
+ elsif obj.is_a? MsgGroup
553
+ @content = :groups unless @content
554
+ raise TypeError, err_msg+"'MsgGroup'" if @content != :groups
555
+ else
556
+ raise TypeError, "Only Message or MsgGroup allowed here"
557
+ end
558
+ obj.validate if auto_validate
559
+ super( obj )
560
+ end
561
+
562
+ end
563
+
564
+
565
+ #########################################################################
566
+ #
567
+ # A "MsgGroup" is a special "Collection with header and trailer"
568
+ # It collects "Message" objects and is only rarely used.
569
+
570
+
571
+ class MsgGroup < Collection_HT
572
+
573
+ def initialize(p, user_par = nil)
574
+ super(p, p.root, 'MsgGroup')
575
+ # ...
576
+ end
577
+
578
+
579
+ def fmt_of_DE(id) # :nodoc:
580
+ @parent.fmt_of_DE(id)
581
+ end
582
+
583
+
584
+ def each_BCDS(id, &b) # :nodoc:
585
+ @parent.each_BCDS(id, &b)
586
+ end
587
+
588
+ # Add only Message objects to a message group!
589
+
590
+ def add (msg)
591
+ raise "Only Messages allowed here" unless msg.is_a? Message
592
+ super
593
+ end
594
+
595
+ end
596
+
597
+
598
+ #########################################################################
599
+ #
600
+ # A "Message" is a special "Collection with header and trailer"
601
+ # It collects "Segment" objects.
602
+
603
+ class Message < Collection_HT
604
+
605
+ # @@msgCounter = 1
606
+
607
+ def initialize( p, user_par=nil )
608
+ super(p, p.root, 'Message')
609
+ end
610
+
611
+
612
+ def fmt_of_DE(id) # :nodoc:
613
+ de = @maindata.de(id)
614
+ return @parent.fmt_of_DE(id) if de.nil?
615
+ de.format
616
+ end
617
+
618
+
619
+ def each_BCDS(id, &b) # :nodoc:
620
+ begin
621
+ @maindata.each_BCDS(id, &b )
622
+ rescue EDILookupError
623
+ @parent.each_BCDS(id, &b)
624
+ end
625
+ end
626
+
627
+
628
+ # Add only Segment objects to a message!
629
+
630
+ def add (seg)
631
+ raise "Only Segments allowed here" unless seg.is_a? Segment
632
+ super
633
+ end
634
+
635
+ end
636
+
637
+
638
+ #########################################################################
639
+ #
640
+ # A "Segment" is a special Collection of type "S" (segment-like),
641
+ # similar to "CDE". Special Segment attributes are:
642
+ # +sg_name+:: The name of its segment group (optional)
643
+ # +level+:: The segment's hierarchy level, an integer
644
+
645
+
646
+ class Segment < Collection_S
647
+
648
+ attr_reader :sg_name, :level
649
+
650
+ # Returns true if segment is a TNode (i.e. a trigger segment).
651
+ # Note that only TNodes may have descendants.
652
+
653
+ def is_tnode?
654
+ @tnode
655
+ end
656
+
657
+ # Returns array of all segments that have the current segment
658
+ # as ancestor.
659
+
660
+ def descendants
661
+ self['descendant::*']
662
+ end
663
+
664
+ # Returns array of all segments with the current segment
665
+ # as ancestor, including the current segment.
666
+ # For trigger segments, this method returns all segments
667
+ # of one instance of the corresponding segment group.
668
+
669
+ def descendants_and_self
670
+ self['descendant-or-self::*']
671
+ end
672
+
673
+ # Returns all child elements of the current segment.
674
+
675
+ def children
676
+ self['child::*']
677
+ end
678
+
679
+ # Returns the current segment and all of its child elements.
680
+ # Useful e.g. to deal with one instance of a segment group
681
+ # without traversing included segment groups.
682
+
683
+ def children_and_self
684
+ self['child-or-self::*']
685
+ end
686
+
687
+
688
+ # Access by XPath expression (support is very limited currently)
689
+ # or by name of the dependent component. Pass them as strings.
690
+ #
691
+ # Used internally - try to avoid at user level!
692
+ # Currently supported XPath expressions:
693
+ #
694
+ # - descendant::*
695
+ # - descendant-or-self::*
696
+ # - child::*
697
+ # - child-or-self::*
698
+
699
+ def []( xpath_expr )
700
+ return super( xpath_expr ) if xpath_expr.is_a? Integer
701
+
702
+ msg_unsupported = "Unsupported XPath expression: #{xpath_expr}"
703
+
704
+ case xpath_expr
705
+
706
+ when /\A(descendant|child)(-or-self)?::(.*)/
707
+ return xpath_matches($1, $2, $3, msg_unsupported)
708
+
709
+ # Currently no real path, no predicate available supported
710
+ when /\//, /\[/, /\]/
711
+ raise IndexError, msg_unsupported
712
+
713
+ when /child::(\w+)/ # ignore & accept default axis "child"
714
+ return super( $1 )
715
+
716
+ when /::/ # No other axes supported for now
717
+ raise IndexError, msg_unsupported
718
+
719
+ else # assume simple element name
720
+ return super( xpath_expr )
721
+ end
722
+ end
723
+
724
+
725
+ def inspect( indent='', symlist=[] )
726
+ symlist += [:sg_name, :level]
727
+ super
728
+ end
729
+
730
+
731
+ # Update attributes with information from a corresponding node instance
732
+
733
+ def update_with( ni ) # :nodoc:
734
+ return nil if ni.name != @name # Names must match; consider a raise!
735
+ @status, @maxrep, @sg_name, @rep, @index, @level, @tnode = ni.status,\
736
+ ni.maxrep, ni.sg_name, ni.inst_cnt, ni.index, ni.level, ni.is_tnode?
737
+ self
738
+ end
739
+
740
+ private
741
+
742
+ def add (obj)
743
+ raise "Only DE or CDE allowed here" unless obj.is_a? DE or obj.is_a? CDE
744
+ super( obj )
745
+ end
746
+
747
+ def xpath_matches( axis, or_self, element, msg_unsupported )
748
+ raise IndexError, msg_unsupported if element != '*'
749
+ results = []
750
+ results << self if or_self
751
+ child_mode = (axis=='child')
752
+ return results unless self.is_tnode?
753
+
754
+ # Now add all segments in self's "tail"
755
+ msg = parent
756
+ index = msg.index(self)
757
+ raise IndexError, "#{name} not found in own message?!" unless index
758
+ loop do
759
+ index += 1
760
+ seg = msg[index]
761
+ next if child_mode and seg.level > level+1 # other descendants
762
+ break if seg.level <= level
763
+ results << seg
764
+ end
765
+ results
766
+ end
767
+ end
768
+
769
+
770
+ #########################################################################
771
+ #
772
+ # Composite data element, primarily used in the EDIFACT context.
773
+ # A "CDE" is a special Collection of type "S" (segment-like),
774
+ # similar to "Segment".
775
+ #
776
+ class CDE < Collection_S
777
+
778
+ private
779
+
780
+ # Add a DE
781
+
782
+ def add( obj )
783
+ raise "Only DE allowed here" unless obj.is_a? DE
784
+ super
785
+ end
786
+
787
+ end
788
+
789
+ #########################################################################
790
+ #
791
+ # A basic data element. Its content is accessible through methods +value+
792
+ # and <tt>value=</tt>. Allowed contents is described by attribute +format+.
793
+ #
794
+ # Note that values are usually Strings, or Numerics when the format indicates
795
+ # a numeric value. Other objects are conceivable, as long as they
796
+ # maintain a reasonable +to_s+ for their representation.
797
+ #
798
+ # The external representation of the (abstract) value may further depend on
799
+ # rules of the unterlying EDI standard. E.g., UN/EDIFACT comes with a set
800
+ # of reserved characters and an escaping mechanism.
801
+
802
+ class DE < EDI::Object
803
+ attr_accessor :value
804
+ attr_reader :format, :status
805
+
806
+ def initialize( p, name, status, fmt )
807
+ @parent, @root, @name, @format, @status = p, p.root, name, fmt, status
808
+ raise "#{location}: 'nil' is not an allowed format." if fmt.nil?
809
+ raise "#{location}: 'nil' is not an allowed status." if status.nil?
810
+ @value = nil
811
+ end
812
+
813
+
814
+ def to_s
815
+ str = self.value
816
+ return str if str.is_a? String
817
+ str = str.to_s; len = str.length
818
+ return str unless format =~ /n(\d+)/ && len != (fixlen=$1.to_i)
819
+ raise "#{location}: Too long (#{l}) for fmt #{format}" if len > fixlen
820
+ '0' * (fixlen - len) + str
821
+ end
822
+
823
+
824
+ def inspect( indent='' )
825
+ indent + self.name+': ' + [:value, :format, :status].map do |sym|
826
+ "#{sym} = #{(s=send(sym)).nil? ? 'nil' : s.to_s}"
827
+ end.join(', ') + "\n"
828
+ end
829
+
830
+
831
+ # Performs various validation checks and returns the number of
832
+ # issues found (plus the value of +err_count+):
833
+ #
834
+ # - empty while mandatory?
835
+ # - character set limitations violated?
836
+ # - various format restrictions violated?
837
+
838
+ def validate( err_count=0 )
839
+ location = "DE #{parent.name}/#{@name}"
840
+ if empty?
841
+ if required?
842
+ warn "#{location}: Empty though mandatory!"
843
+ err_count += 1
844
+ end
845
+ else
846
+ #
847
+ # Charset check
848
+ #
849
+ if (pos = (value =~ root.illegal_charset_pattern))# != nil
850
+ warn "#{location}: Illegal character: #{value[pos].chr}"
851
+ err_count += 1
852
+ end
853
+ #
854
+ # Format check, raise error if not consistent!
855
+ #
856
+ if @format =~ /^(a|n|an)(..)?(\d+)$/
857
+ _a_n_an, _upto, _size = $1, $2, $3
858
+ case _a_n_an
859
+
860
+ when 'n'
861
+ strval = value.to_s
862
+ re = Regexp.new('^(-)?(\d+)([.,]\d+)?$')
863
+ md = re.match strval
864
+ if md.nil?
865
+ raise "#{location}: '#{strval}' - not matching format #@format"
866
+ # warn "#{strval} - not matching format #@format"
867
+ # err_count += 1
868
+ end
869
+
870
+ len = strval.length
871
+ # Sign char does not go into length count:
872
+ len -= 1 if md[1]=='-'
873
+ # Decimal char does not go into length count:
874
+ len -= 1 if not md[3].nil?
875
+ # len -= 1 if (md[1]=='-' and md[3]) || (md[1] != '' and not md[3])
876
+ break if not required? and len == 0
877
+ if len > _size.to_i
878
+ # if _upto.nil? and len != _size.to_i or len > _size.to_i
879
+ warn "Context in #{location}: #{_a_n_an}, #{_upto}, #{_size}; #{md[1]}, #{md[2]}, #{md[3]}"
880
+ warn "Length # mismatch in #{location}: #{len} vs. #{_size}"
881
+ err_count += 1
882
+ # warn " (strval was: '#{strval}')"
883
+ end
884
+ if md[1] =~/^0+/
885
+ warn "#{strval} contains leading zeroes"
886
+ err_count += 1
887
+ end
888
+ if md[3] and md[3]=~ /.0+$/
889
+ warn "#{strval} contains trailing decimal sign/zeroes"
890
+ err_count += 1
891
+ end
892
+
893
+ when 'a', 'an'
894
+ # len = value.is_a?(Numeric) ? value.to_s.length : value.length
895
+ len = value.to_s.length
896
+ if _upto.nil? and len != $3.to_i or len > $3.to_i
897
+ warn "Length mismatch in #{location}: #{len} vs. #{_size}"
898
+ err_count += 1
899
+ end
900
+ else
901
+ raise "#{location}: Illegal format prefix #{_a_n_an}"
902
+ # err_count += 1
903
+ end
904
+
905
+ else
906
+ warn "#{location}: Illegal format: #{@format}!"
907
+ err_count += 1
908
+ end
909
+ end
910
+ err_count
911
+ end
912
+
913
+
914
+ # Returns +true+ if value is not +nil+.
915
+ # Note that assigning an empty string to a DE makes it "not empty".
916
+ def empty?
917
+ @value == nil
918
+ end
919
+
920
+
921
+ # Returns +true+ if this is a required / mandatory segment.
922
+ def required?
923
+ @status == 'M' or @status == 'R'
924
+ end
925
+
926
+ end
927
+
928
+ end # module EDI