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