edi4r 0.9.4.1 → 0.9.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/AuthorCopyright +3 -3
- data/{ChangeLog → Changelog} +60 -0
- data/README +15 -10
- data/Tutorial +2 -3
- data/VERSION +1 -1
- data/bin/edi2xml.rb +12 -16
- data/bin/editool.rb +9 -5
- data/bin/sedas2eancom02.rb +1385 -0
- data/bin/xml2edi.rb +7 -12
- data/data/edifact/iso9735/SDCD.20000.csv +1 -0
- data/data/edifact/iso9735/SDCD.3for2.csv +1 -0
- data/data/edifact/iso9735/SDED.20000.csv +6 -0
- data/data/edifact/iso9735/SDED.30000.csv +43 -43
- data/data/edifact/iso9735/SDED.3for2.csv +6 -0
- data/data/edifact/iso9735/SDED.40000.csv +129 -129
- data/data/edifact/iso9735/SDED.40100.csv +130 -130
- data/data/edifact/iso9735/SDMD.20000.csv +6 -0
- data/data/edifact/iso9735/SDMD.30000.csv +6 -6
- data/data/edifact/iso9735/SDMD.3for2.csv +6 -0
- data/data/edifact/iso9735/SDMD.40000.csv +17 -17
- data/data/edifact/iso9735/SDMD.40100.csv +17 -17
- data/data/edifact/iso9735/SDSD.20000.csv +5 -0
- data/data/edifact/iso9735/SDSD.3for2.csv +5 -0
- data/data/edifact/untdid/EDMD.d01b.csv +1 -1
- data/data/sedas/EDCD..csv +0 -0
- data/data/sedas/EDED..csv +859 -0
- data/data/sedas/EDMD..csv +16 -0
- data/data/sedas/EDSD..csv +44 -0
- data/lib/edi4r.rb +147 -67
- data/lib/edi4r/ansi_x12-rexml.rb +91 -0
- data/lib/edi4r/ansi_x12.rb +1684 -0
- data/lib/edi4r/diagrams.rb +75 -14
- data/lib/edi4r/edifact-rexml.rb +4 -3
- data/lib/edi4r/edifact.rb +505 -202
- data/lib/edi4r/rexml.rb +13 -7
- data/lib/edi4r/sedas.rb +854 -0
- data/lib/edi4r/standards.rb +150 -33
- data/test/damaged_file.edi +1 -0
- data/test/eancom2webedi.rb +1 -0
- data/test/groups.edi +1 -1
- data/test/test_basics.rb +16 -9
- data/test/test_edi_split.rb +30 -0
- data/test/test_loopback.rb +7 -2
- data/test/test_rexml.rb +34 -2
- data/test/test_service_messages.rb +190 -0
- data/test/test_streaming.rb +167 -0
- data/test/test_tut_examples.rb +3 -1
- data/test/webedi2eancom.rb +1 -0
- metadata +121 -77
data/lib/edi4r/rexml.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
+
# -*- encoding: iso-8859-1 -*-
|
1
2
|
# Add-on to EDI module "EDI4R"
|
2
3
|
# Classes for XML support, here: Basic classes
|
3
4
|
#
|
4
5
|
# :include: ../../AuthorCopyright
|
5
6
|
#
|
6
|
-
# $Id
|
7
|
+
# $Id$
|
7
8
|
#--
|
8
9
|
# $Log: rexml.rb,v $
|
10
|
+
# Revision 1.2 2007/03/14 23:59:25 werntges
|
11
|
+
# Bug fix (Joko's report) in DE#to_xml: Attribute "instance" generated now
|
12
|
+
#
|
9
13
|
# Revision 1.1 2006/08/01 11:14:29 werntges
|
10
14
|
# Initial revision
|
11
15
|
#
|
@@ -23,6 +27,7 @@
|
|
23
27
|
|
24
28
|
require 'rexml/document'
|
25
29
|
# require 'diagrams-xml'
|
30
|
+
require 'edi4r/ansi_x12-rexml' if EDI.constants.include? 'A'
|
26
31
|
require 'edi4r/edifact-rexml' if EDI.constants.include? 'E'
|
27
32
|
require 'edi4r/idoc-rexml' if EDI.constants.include? 'I'
|
28
33
|
|
@@ -38,7 +43,7 @@ module EDI
|
|
38
43
|
def to_xml( xel_parent, instance=1 )
|
39
44
|
xel = REXML::Element.new( normalized_class_name )
|
40
45
|
xel.attributes["name"] = @name
|
41
|
-
xel.attributes["instance"] = instance if instance > 1
|
46
|
+
xel.attributes["instance"] = instance.to_s if instance > 1
|
42
47
|
xel_parent.elements << xel
|
43
48
|
instance_counter = Hash.new(0)
|
44
49
|
each do |obj|
|
@@ -102,7 +107,7 @@ module EDI
|
|
102
107
|
end
|
103
108
|
hnd = REXML::Document.new( hnd ) if hnd.is_a? IO or hnd.is_a? String
|
104
109
|
|
105
|
-
key = hnd.root.attributes['standard_key']
|
110
|
+
key = hnd.root.attributes['standard_key'].strip
|
106
111
|
raise "Unsupported standard key: #{key}" if key == 'generic'
|
107
112
|
EDI::const_get(key)::const_get('Interchange').parse_xml( hnd )
|
108
113
|
# class_sym = (key+'Interchange').intern
|
@@ -112,9 +117,9 @@ module EDI
|
|
112
117
|
|
113
118
|
def to_xml( xel_parent )
|
114
119
|
externalID = "PUBLIC\n" + " "*9
|
115
|
-
externalID += "'-//
|
120
|
+
externalID += "'-//Hochschule RheinMain FB DCSM//DTD XML Representation of EDI data V1.2//EN'\n"
|
116
121
|
externalID += " "*9
|
117
|
-
externalID += "'http://edi01.
|
122
|
+
externalID += "'http://edi01.cs.hs-rm.de/edi4r/edi4r-1.2.dtd'"
|
118
123
|
xel_parent << REXML::XMLDecl.new
|
119
124
|
xel_parent << REXML::DocType.new( normalized_class_name, externalID )
|
120
125
|
|
@@ -124,7 +129,7 @@ module EDI
|
|
124
129
|
pos = self.class.to_s =~ /EDI::((.*?)::)?Interchange/
|
125
130
|
raise "This is not an Interchange object: #{rc}!" unless pos==0
|
126
131
|
xel.attributes["standard_key"] = ($2 and not $2.empty?) ? $2 : "generic"
|
127
|
-
xel.attributes["version"] = @version
|
132
|
+
xel.attributes["version"] = @version.to_s
|
128
133
|
xel.attributes.delete "name"
|
129
134
|
rc
|
130
135
|
end
|
@@ -244,9 +249,10 @@ module EDI
|
|
244
249
|
|
245
250
|
class DE
|
246
251
|
|
247
|
-
def to_xml( xel_parent, instance=
|
252
|
+
def to_xml( xel_parent, instance=1 )
|
248
253
|
xel = REXML::Element.new( 'DE' )
|
249
254
|
xel.attributes["name"] = @name
|
255
|
+
xel.attributes["instance"] = instance.to_s if instance > 1
|
250
256
|
xel_parent.elements << xel
|
251
257
|
xel.text = self.to_s( true ) # don't escape!
|
252
258
|
xel
|
data/lib/edi4r/sedas.rb
ADDED
@@ -0,0 +1,854 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- encoding: iso-8859-1 -*-
|
3
|
+
#
|
4
|
+
# :main:README
|
5
|
+
# :title:sedas
|
6
|
+
#
|
7
|
+
# SEDAS module: API to parse (and create) SEDAS data
|
8
|
+
#
|
9
|
+
# == Synopsis
|
10
|
+
#
|
11
|
+
# It module adds basic SEDAS capabilities, as were needed for a limited
|
12
|
+
# SEDAS-to-EANCOM migration project.
|
13
|
+
# Note that SEDAS is a "dying" standard, so it is not fully supported here.
|
14
|
+
#
|
15
|
+
# == Usage
|
16
|
+
#
|
17
|
+
# require 'sedas'
|
18
|
+
#
|
19
|
+
# == Description
|
20
|
+
#
|
21
|
+
# The edi4r base gem comes with a limited set of SEDAS data structures
|
22
|
+
# focused on invoice data, as well as abstract classes for the
|
23
|
+
# handling of general EDI data structures.
|
24
|
+
# This add-on builds upon the same abstract classes and provides support
|
25
|
+
# for SEDAS data.
|
26
|
+
#
|
27
|
+
# Only one release (1993) is considered. Record types 00,01,98,99,
|
28
|
+
# 12-17/22-27, 29, 50, and 51 are supported, as well as follow-up types
|
29
|
+
# 01 (for 12/22, 15/25, 29), 02 (for 13/23, 16/26, 29), and 08
|
30
|
+
# (for 12-17/22-27).
|
31
|
+
#
|
32
|
+
# :include:../../AuthorCopyright
|
33
|
+
#--
|
34
|
+
# $Id: sedas.rb,v 1.2 2007/04/10 22:21:05 werntges Exp $
|
35
|
+
#
|
36
|
+
# $Log: sedas.rb,v $
|
37
|
+
# Revision 1.2 2007/04/10 22:21:05 werntges
|
38
|
+
# Final refinements, preparing pre-release
|
39
|
+
#
|
40
|
+
# Revision 1.1 2007/03/30 14:43:09 werntges
|
41
|
+
# Initial revision
|
42
|
+
#
|
43
|
+
#
|
44
|
+
#
|
45
|
+
# To-do list:
|
46
|
+
# * Support for generation of SEDAS data (low prio)
|
47
|
+
# * Removal of some remaining code from IDOC precursor source
|
48
|
+
# * Formal support of character sets
|
49
|
+
#
|
50
|
+
|
51
|
+
|
52
|
+
# SEDAS add-ons to EDI module
|
53
|
+
# API to parse and create SEDAS
|
54
|
+
#
|
55
|
+
|
56
|
+
module EDI
|
57
|
+
|
58
|
+
# Notes:
|
59
|
+
#
|
60
|
+
# SEDAS data are organized as record sets. A physical file or
|
61
|
+
# transmission unit is always framed by record types 00 and 99.
|
62
|
+
# Within such a physical unit, one or more logical units may occur,
|
63
|
+
# framed by records 01 and 98.
|
64
|
+
#
|
65
|
+
# SEDAS data are self-contained already at the message level.
|
66
|
+
# An "Interchange" of the SEDAS type is just a concatenation
|
67
|
+
# of messages; a single message per Interchange is quite acceptable.
|
68
|
+
# However, one such message will usually map to several messages
|
69
|
+
# of the UN/EDIFACT type. As an example, a sequence of record types 29
|
70
|
+
# may map to one or several INVOIC messages (BGM+393), depending on
|
71
|
+
# the number of group changes (document number changes).
|
72
|
+
#
|
73
|
+
# Also note that SEDAS record types 50, 51 are considered master data.
|
74
|
+
# They are referred to by earlier records to avoid redundant data.
|
75
|
+
# Here we treat this master data block as a single "message" - but not
|
76
|
+
# with mapping in mind.
|
77
|
+
#
|
78
|
+
# Here we use the Interchange level only to maintain the information
|
79
|
+
# about the physical/logical unit data.
|
80
|
+
#
|
81
|
+
|
82
|
+
module S
|
83
|
+
|
84
|
+
class Interchange < EDI::Interchange
|
85
|
+
|
86
|
+
attr_accessor :charset, :output_mode
|
87
|
+
attr_reader :basedata
|
88
|
+
|
89
|
+
def init_ndb
|
90
|
+
@basedata = EDI::Dir::Directory.create( root.syntax )
|
91
|
+
end
|
92
|
+
|
93
|
+
# Currently, this is almost a dummy method
|
94
|
+
# It might grow into something useful.
|
95
|
+
# Keep it to stay consistent with other module add-ons.
|
96
|
+
#
|
97
|
+
def check_consistencies
|
98
|
+
if 'S' != @syntax
|
99
|
+
raise "#{@syntax} - syntax must be 'S' (SEDAS)!"
|
100
|
+
end
|
101
|
+
|
102
|
+
case @charset
|
103
|
+
when 'ISO-8859-15'
|
104
|
+
# ok
|
105
|
+
# when '...'
|
106
|
+
else
|
107
|
+
raise "#{@charset} - character set not supported!"
|
108
|
+
end
|
109
|
+
# Add more rules ...
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
def initialize( user_par={} )
|
114
|
+
super( user_par ) # just in case...
|
115
|
+
par = {:syntax => 'S',
|
116
|
+
:output_mode => nil,
|
117
|
+
:charset => 'ISO-8859-15', # not used yet...
|
118
|
+
}.update( user_par )
|
119
|
+
|
120
|
+
@syntax = par[:syntax] # UN/EDIFACT
|
121
|
+
@output_mode = par[:output_mode]
|
122
|
+
@charset = par[:charset]
|
123
|
+
|
124
|
+
# Temporary - adjust to current SEDAS needs:
|
125
|
+
@illegal_charset_pattern = /[^-A-Za-z0-9 .,()\/=!%"&*;<>'+:?\xa0-\xff]+/
|
126
|
+
|
127
|
+
check_consistencies
|
128
|
+
init_ndb
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
#
|
133
|
+
# Reads SEDAS data from given stream (default: $stdin),
|
134
|
+
# parses it and returns an Interchange object
|
135
|
+
#
|
136
|
+
def Interchange.parse( hnd=$stdin, auto_validate=true )
|
137
|
+
builder = StreamingBuilder.new( auto_validate )
|
138
|
+
builder.go( hnd )
|
139
|
+
builder.interchange
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Read +maxlen+ bytes from $stdin (default) or from given stream
|
144
|
+
# (SEDAS data expected), and peek into first segment (00).
|
145
|
+
#
|
146
|
+
# Returns an empty Interchange object with a properly header filled.
|
147
|
+
#
|
148
|
+
# Intended use:
|
149
|
+
# Efficient routing by reading just 00 data: sender/recipient/ref/test
|
150
|
+
#
|
151
|
+
def Interchange.peek(hnd=$stdin, params={}) # Handle to input stream
|
152
|
+
builder = StreamingBuilder.new( false )
|
153
|
+
if params[:deep_peek]
|
154
|
+
def builder.on_segment( s, tag )
|
155
|
+
end
|
156
|
+
else
|
157
|
+
def builder.on_01( s )
|
158
|
+
throw :done
|
159
|
+
end
|
160
|
+
def builder.on_msg_start( s, tag )
|
161
|
+
throw :done # FIXME: UNZ??
|
162
|
+
end
|
163
|
+
end
|
164
|
+
builder.go( hnd )
|
165
|
+
builder.interchange
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
def Interchange.parse_xml(xdoc)
|
170
|
+
ic = Interchange.new # ({:sap_type => xdoc.root.attributes['version']})
|
171
|
+
xdoc.root.elements.each('Message') do |xel|
|
172
|
+
ic.add( Message.parse_xml( ic, xel ) )
|
173
|
+
end
|
174
|
+
ic
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
def new_msggroup(params)
|
179
|
+
MsgGroup.new(self, params)
|
180
|
+
end
|
181
|
+
|
182
|
+
def new_message(params)
|
183
|
+
Message.new(self, params)
|
184
|
+
end
|
185
|
+
|
186
|
+
def new_segment(tag)
|
187
|
+
Segment.new(self, tag)
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
def parse_message(list)
|
192
|
+
Message.parse(self, list)
|
193
|
+
end
|
194
|
+
|
195
|
+
def parse_segment(buf)
|
196
|
+
Segment.parse(self, buf)
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
|
203
|
+
class MsgGroup < EDI::MsgGroup
|
204
|
+
|
205
|
+
def new_message(params)
|
206
|
+
Message.new(self, params)
|
207
|
+
end
|
208
|
+
|
209
|
+
def new_segment(tag)
|
210
|
+
Segment.new(self, tag)
|
211
|
+
end
|
212
|
+
|
213
|
+
def parse_segment(buf)
|
214
|
+
Segment.parse(self, buf)
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
class Message < EDI::Message
|
221
|
+
attr_accessor :maindata
|
222
|
+
# private_class_method :new
|
223
|
+
|
224
|
+
# @@msgCounter = 1
|
225
|
+
|
226
|
+
def preset_msg(user_par={})
|
227
|
+
# lower-case names for internal keys,
|
228
|
+
# upper-case names for EDI_DC field names
|
229
|
+
par = {:sedas_type => '12',
|
230
|
+
}.update( user_par )
|
231
|
+
@pars = par
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
def initialize( p, user_par )
|
236
|
+
super( p, user_par )
|
237
|
+
# @parent, @root = p, p.root
|
238
|
+
|
239
|
+
# First param is either a hash or segment EDI_DC
|
240
|
+
# If Hash: Build EDI_DC from given parameters
|
241
|
+
# If Segment: Extract some crucial parameters
|
242
|
+
|
243
|
+
@maindata = Dir::Directory.create( root.syntax )
|
244
|
+
if user_par.is_a? Hash
|
245
|
+
preset_msg( user_par)
|
246
|
+
@name = @pars[:sedas_type]
|
247
|
+
|
248
|
+
@header = nil # new_segment(p.dc_sig.strip) # typically, "EDI_DC40"
|
249
|
+
# @header.dIDOCTYP = @pars[:IDOCTYP]
|
250
|
+
# etc.
|
251
|
+
=begin
|
252
|
+
elsif user_par.is_a? EDI::S::Segment
|
253
|
+
@header = user_par
|
254
|
+
if @header.name !~ /^[12]2/
|
255
|
+
raise "12/22 expected, '#{@header.name}' found!"
|
256
|
+
end
|
257
|
+
@header.parent = self
|
258
|
+
@header.root = self.root
|
259
|
+
@pars = Hash.new
|
260
|
+
@pars[:sedas_type]= header.name[0,2] # e.g. '12', '15'
|
261
|
+
# @pars[:IDOCTYP] = @header.dIDOCTYP
|
262
|
+
# @maindata = Dir::Directory.create( root.syntax,
|
263
|
+
# :SEDASTYPE=> @pars[:sedas_type]
|
264
|
+
# )
|
265
|
+
=end
|
266
|
+
else
|
267
|
+
raise "Parameter 'user_par': Illegal type!"
|
268
|
+
end
|
269
|
+
|
270
|
+
@trailer = nil
|
271
|
+
# @@msgCounter += 1
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
def new_segment(tag)
|
276
|
+
Segment.new(self, tag)
|
277
|
+
end
|
278
|
+
|
279
|
+
def parse_segment(buf)
|
280
|
+
Segment.parse(self, buf)
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
def Message.parse (p, segment_list)
|
285
|
+
# Segments comprise a single message
|
286
|
+
|
287
|
+
# Temporarily assign p as segment parent,
|
288
|
+
# or else service segment lookup fails:
|
289
|
+
raise "NOT SUPPORTED anymore!"
|
290
|
+
header = p.parse_segment(segment_list.shift, p.dc_sig.strip)
|
291
|
+
|
292
|
+
msg = p.new_message(header) # Now use header rec as template
|
293
|
+
|
294
|
+
segment_list.each {|segbuf| msg.add Segment.parse( msg, segbuf ) }
|
295
|
+
|
296
|
+
msg.trailer = nil
|
297
|
+
msg
|
298
|
+
end
|
299
|
+
|
300
|
+
|
301
|
+
def validate
|
302
|
+
# Check sequence of segments against library,
|
303
|
+
# thereby adding location information to each segment
|
304
|
+
diag = EDI::Diagram::Diagram.create(root.syntax,
|
305
|
+
:SEDASTYPE=> @pars[:sedas_type]
|
306
|
+
)
|
307
|
+
ni = EDI::Diagram::NodeInstance.new(diag)
|
308
|
+
# puts "Initial node instance is: #{ni.name}"
|
309
|
+
if @header
|
310
|
+
ni.seek!( @header )
|
311
|
+
@header.update_with( ni )
|
312
|
+
end
|
313
|
+
each {|seg|
|
314
|
+
# if (key = seg.name) !~ /.*\d\d\d/
|
315
|
+
# key = Regexp.new(key+'(\d\d\d)?')
|
316
|
+
# end
|
317
|
+
|
318
|
+
begin
|
319
|
+
if ni.seek!(seg.name) # key) # (seg)
|
320
|
+
seg.update_with( ni )
|
321
|
+
else
|
322
|
+
raise "seek! failed for #{seg.name} when starting at #{ni.name}"
|
323
|
+
end
|
324
|
+
=begin
|
325
|
+
rescue EDI::EDILookupError
|
326
|
+
warn key
|
327
|
+
if key =~ /(.*)\d\d\d/
|
328
|
+
key = $1
|
329
|
+
retry
|
330
|
+
else
|
331
|
+
raise
|
332
|
+
end
|
333
|
+
=end
|
334
|
+
end
|
335
|
+
}
|
336
|
+
# ni.seek!( @trailer )
|
337
|
+
# @trailer.update_with( ni )
|
338
|
+
|
339
|
+
# Now check each segment
|
340
|
+
super
|
341
|
+
end
|
342
|
+
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
|
347
|
+
class Segment < EDI::Segment
|
348
|
+
|
349
|
+
def initialize(p, tag)
|
350
|
+
super( p, tag )
|
351
|
+
|
352
|
+
segment_list = root.basedata.segment(tag)
|
353
|
+
raise "Segment \'#{tag}\' not found" if segment_list.nil?
|
354
|
+
|
355
|
+
# each_BCDS_Entry('s'+tag) do |entry| # This does not work here...
|
356
|
+
segment_list.each do |entry|
|
357
|
+
id = entry.name
|
358
|
+
status = entry.status
|
359
|
+
# puts "Seeking DE >>#{tag+':'+id}<<"
|
360
|
+
# Regular lookup
|
361
|
+
fmt = fmt_of_DE(tag+':'+id)
|
362
|
+
add new_DE(id, status, fmt)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
|
367
|
+
def new_DE(id, status, fmt)
|
368
|
+
DE.new(self, id, status, fmt)
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
# Buffer contains a single segment (line)
|
373
|
+
def Segment.parse (p, buf, tag_expected=nil)
|
374
|
+
case tag = buf[0,2]
|
375
|
+
when '00', '01', '03', '51', '96', '98','99'
|
376
|
+
# take just the rec_id as tag
|
377
|
+
else
|
378
|
+
# append "Folgesatz" id
|
379
|
+
tag +=buf[146,2]
|
380
|
+
end
|
381
|
+
seg = p.new_segment(tag)
|
382
|
+
|
383
|
+
if tag_expected and tag_expected != tag
|
384
|
+
raise "Wrong segment name! Expected: #{tag_expected}, found: #{tag}"
|
385
|
+
end
|
386
|
+
|
387
|
+
seg.each {|obj| obj.parse(buf) }
|
388
|
+
seg
|
389
|
+
end
|
390
|
+
|
391
|
+
|
392
|
+
def to_s
|
393
|
+
line = ''
|
394
|
+
crlf = "\x0d\x0a"
|
395
|
+
return line if empty?
|
396
|
+
if root.output_mode == :linebreak
|
397
|
+
each do |obj|
|
398
|
+
next if obj.empty?
|
399
|
+
line << name.ljust(12)+obj.name.ljust(12)+'['+obj.to_s+']' unless obj.empty?
|
400
|
+
end
|
401
|
+
else
|
402
|
+
last_nonempty_de = nil
|
403
|
+
each {|obj| last_nonempty_de = obj unless obj.empty? }
|
404
|
+
each {|obj| line += obj.to_s; break if obj.object_id == last_nonempty_de.object_id }
|
405
|
+
end
|
406
|
+
line << crlf
|
407
|
+
line
|
408
|
+
end
|
409
|
+
|
410
|
+
private
|
411
|
+
|
412
|
+
# SEDAS field names direcly qualify as Ruby method names,
|
413
|
+
# and there are neither composites nor arrays, so we can
|
414
|
+
# simplify access to fields here.
|
415
|
+
#
|
416
|
+
|
417
|
+
def method_missing(sym, *par)
|
418
|
+
if sym.id2name =~ /^(\w+)(=)?/
|
419
|
+
rc = lookup($1)
|
420
|
+
if rc.is_a? Array
|
421
|
+
if rc.size==1
|
422
|
+
rc = rc.first
|
423
|
+
elsif rc.size==0
|
424
|
+
return super
|
425
|
+
end
|
426
|
+
end
|
427
|
+
if $2
|
428
|
+
# Setter
|
429
|
+
raise TypeError, "Can't assign to a #{rc.class} object '#$1'" unless rc.is_a? DE
|
430
|
+
rc.value = par[0]
|
431
|
+
else
|
432
|
+
# Getter
|
433
|
+
return rc.value if rc.is_a? DE
|
434
|
+
err_msg = "No DE with name '#$1' found, instead there is a '#{rc.class}'!"
|
435
|
+
raise TypeError, err_msg
|
436
|
+
end
|
437
|
+
else
|
438
|
+
super
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
end
|
443
|
+
|
444
|
+
|
445
|
+
# There are no CDEs in IDocs:
|
446
|
+
# class CDE_E < CDE
|
447
|
+
# end
|
448
|
+
|
449
|
+
|
450
|
+
class DE < EDI::DE
|
451
|
+
|
452
|
+
# ae, oe, ue, sz, Ae, Oe, Ue, Paragraph, `, ´
|
453
|
+
@@umlaute_iso636_de = '{-}~[-]@`´'
|
454
|
+
@@umlaute_cp850 = "\x84\x94\x81\xE1\x8E\x99\x9A\xF5''"
|
455
|
+
@@umlaute_iso8859_1 = "\xE4\xF6\xFC\xDF\xC4\xD6\xDC\xA7''"
|
456
|
+
|
457
|
+
def initialize( p, name, status, fmt )
|
458
|
+
super( p, name, status, fmt )
|
459
|
+
fmt =~ /(a|an|n|d|t)(\.\.)?(\d+):(\d+)/
|
460
|
+
raise "Illegal format string in field #{name}: #{fmt}" if $3.nil? or $4.nil?
|
461
|
+
@length, @offset = $3.to_i, $4.to_i-1
|
462
|
+
# puts "#{name}: len=#{@length.to_s}, off=#{@offset.to_s}"
|
463
|
+
# check if supported format syntax
|
464
|
+
# check if supported status value
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
def parse( buf ) # Buffer contains segment line; extract sub-string!
|
469
|
+
# msg = "DE #{@name}: Buffer missing or too short"
|
470
|
+
if buf.nil? or buf.length < @offset# +@length
|
471
|
+
@value = nil
|
472
|
+
return
|
473
|
+
end
|
474
|
+
|
475
|
+
# Sure that "strip" is always ok, and that we can ignore whitespace??
|
476
|
+
# case @name
|
477
|
+
# when 'SEGNUM'
|
478
|
+
@value = buf[@offset...@offset+@length]
|
479
|
+
if self.format[0]==?n # Numerical?
|
480
|
+
if @value =~ /^ *$/ # Optional numerical field
|
481
|
+
@value = nil
|
482
|
+
return
|
483
|
+
end
|
484
|
+
code = @value[-1]
|
485
|
+
@value[-1] = ?0
|
486
|
+
@value = @value.to_i
|
487
|
+
|
488
|
+
if RUBY_VERSION >= '1.9'
|
489
|
+
case code
|
490
|
+
when ?A..?I
|
491
|
+
@value += (code.ord-?@.ord)
|
492
|
+
when ?J..?R
|
493
|
+
@value += (code.ord-?I.ord)
|
494
|
+
@value = -@value
|
495
|
+
when ?}, "\x81", "\xfc" # ü (0xFC) to be confirmed
|
496
|
+
@value = -@value
|
497
|
+
when ?{, ?0, ' ' # Blank, ä to be confirmed
|
498
|
+
# noop
|
499
|
+
when ?1..?9
|
500
|
+
@value += (code.ord-?0.ord)
|
501
|
+
else
|
502
|
+
raise "#{self.name}: #{code} is not a valid last char of a numerical field"
|
503
|
+
end
|
504
|
+
else # older Ruby version
|
505
|
+
case code
|
506
|
+
when ?A..?I
|
507
|
+
@value += (code-?@)
|
508
|
+
when ?J..?R
|
509
|
+
@value += (code-?I)
|
510
|
+
@value = -@value
|
511
|
+
when ?}, 0x81, 0xfc # ü (0xFC) to be confirmed
|
512
|
+
@value = -@value
|
513
|
+
when ?{, ?0, 0x20 # Blank, ä to be confirmed
|
514
|
+
# noop
|
515
|
+
when ?1..?9
|
516
|
+
@value += (code-?0)
|
517
|
+
else
|
518
|
+
raise "#{self.name}: #{code.chr} (#{code}) is not a valid last char of a numerical field"
|
519
|
+
end
|
520
|
+
end # Ruby version
|
521
|
+
elsif @value.is_a? String
|
522
|
+
@value.tr!(@@umlaute_cp850, @@umlaute_iso8859_1)
|
523
|
+
@value.tr!(@@umlaute_iso636_de, @@umlaute_iso8859_1)
|
524
|
+
end
|
525
|
+
# else
|
526
|
+
# @value = buf[@offset...@offset+@length].strip
|
527
|
+
# end
|
528
|
+
# @value = nil if @value =~ /^\s*$/
|
529
|
+
end
|
530
|
+
|
531
|
+
|
532
|
+
def validate( err_count=0 )
|
533
|
+
location = "#{parent.name} - #{@name}"
|
534
|
+
@format =~ /((a|an|n|d|t)(\.\.)?(\d+)):\d+/
|
535
|
+
fmt = [$2, $3, $4].join
|
536
|
+
case $1
|
537
|
+
when 'd8'
|
538
|
+
if !empty? && value !~ /^\d{8}$/
|
539
|
+
warn "#{location}: Format \'#@format\' violated: #@value"
|
540
|
+
err_count+=1
|
541
|
+
end
|
542
|
+
when 't6'
|
543
|
+
if !empty? && value !~ /^\d{6}$/
|
544
|
+
warn "#{location}: Format \'#@format\' violated: #@value"
|
545
|
+
err_count+=1
|
546
|
+
end
|
547
|
+
when /^[dt].*/
|
548
|
+
warn "validate in DE #@name: Format \'#@format\' not validated yet!"
|
549
|
+
else
|
550
|
+
return super( err_count, fmt )
|
551
|
+
end
|
552
|
+
err_count
|
553
|
+
end
|
554
|
+
|
555
|
+
# TODO: reversal of charset mapping, debugging (round-circle ok?)
|
556
|
+
#
|
557
|
+
def to_s
|
558
|
+
# return '' if empty?
|
559
|
+
if self.format[0]==?n # Numerical?
|
560
|
+
if @value && @value < 0
|
561
|
+
@value = -@value
|
562
|
+
value_str = @value.to_s
|
563
|
+
value_str[-1] = case value_str[-1]
|
564
|
+
when '0' then '}'
|
565
|
+
when '1' then 'J'
|
566
|
+
when '2' then 'K'
|
567
|
+
when '3' then 'L'
|
568
|
+
when '4' then 'M'
|
569
|
+
when '5' then 'N'
|
570
|
+
when '6' then 'O'
|
571
|
+
when '7' then 'P'
|
572
|
+
when '8' then 'Q'
|
573
|
+
when '9' then 'R'
|
574
|
+
else raise "Illegal last digit in numeric value '#{value_str}'"
|
575
|
+
end
|
576
|
+
value_str.rjust(@length,'0')[0,@length] # left-padded with '0'
|
577
|
+
else
|
578
|
+
@value.to_s.rjust(@length,'0')[0,@length] # left-padded with '0'
|
579
|
+
end
|
580
|
+
else
|
581
|
+
@value.to_s.ljust(@length)[0,@length] # right-padded with ' '
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
end
|
586
|
+
|
587
|
+
|
588
|
+
#########################################################################
|
589
|
+
#
|
590
|
+
# = Class StreamingParser
|
591
|
+
#
|
592
|
+
# For a documentation, see class EDI::E::StreamingParser;
|
593
|
+
# this class is its SEDAS equivalent.
|
594
|
+
|
595
|
+
class StreamingParser
|
596
|
+
|
597
|
+
def initialize
|
598
|
+
@path = 'input stream'
|
599
|
+
end
|
600
|
+
|
601
|
+
# Convenience method. Returns the path of the File object
|
602
|
+
# passed to method +go+ or just string 'input stream'
|
603
|
+
def path
|
604
|
+
@path
|
605
|
+
end
|
606
|
+
|
607
|
+
# Called at start of reading - overwrite for your init purposes.
|
608
|
+
# Note: Must *not* throw <tt>:done</tt> !
|
609
|
+
#
|
610
|
+
def on_interchange_start
|
611
|
+
end
|
612
|
+
|
613
|
+
# Called at EOF - overwrite for your cleanup purposes.
|
614
|
+
# Note: Must *not* throw <tt>:done</tt> !
|
615
|
+
#
|
616
|
+
def on_interchange_end
|
617
|
+
end
|
618
|
+
|
619
|
+
# Called when UNB or UIB encountered
|
620
|
+
#
|
621
|
+
def on_00( s )
|
622
|
+
end
|
623
|
+
|
624
|
+
# Called when UNZ or UIZ encountered
|
625
|
+
#
|
626
|
+
def on_99( s )
|
627
|
+
end
|
628
|
+
|
629
|
+
# Called when UNG encountered
|
630
|
+
#
|
631
|
+
def on_01( s )
|
632
|
+
end
|
633
|
+
|
634
|
+
# Called when UNE encountered
|
635
|
+
#
|
636
|
+
def on_98( s )
|
637
|
+
end
|
638
|
+
|
639
|
+
# Called when UNH or UIH encountered
|
640
|
+
#
|
641
|
+
def on_msg_start( s )
|
642
|
+
end
|
643
|
+
|
644
|
+
# Called when UNT or UIT encountered
|
645
|
+
#
|
646
|
+
def on_msg_end
|
647
|
+
end
|
648
|
+
|
649
|
+
# Called when any other segment encountered
|
650
|
+
#
|
651
|
+
def on_segment( s, tag )
|
652
|
+
end
|
653
|
+
|
654
|
+
# This callback is usually kept empty. It is called when the parser
|
655
|
+
# finds strings between segments or in front of or trailing an interchange.
|
656
|
+
#
|
657
|
+
# Strictly speaking, such strings are not permitted by the SEDAS
|
658
|
+
# syntax rules. However, some people e.g. seem to add empty lines
|
659
|
+
# at the end of a file. The default settings thus ignore such occurrences.
|
660
|
+
#
|
661
|
+
# If you need strict conformance checking, feel free to put some code
|
662
|
+
# into this callback method, otherwise just ignore it.
|
663
|
+
#
|
664
|
+
#
|
665
|
+
def on_other( s )
|
666
|
+
end
|
667
|
+
|
668
|
+
# Called upon syntax errors. Parsing should be aborted now.
|
669
|
+
#
|
670
|
+
def on_error(err, offset, fragment, c=nil)
|
671
|
+
raise err, "offset = %d, last chars = %s%s" %
|
672
|
+
[offset, fragment, c.nil? ? '<EOF>' : c.chr]
|
673
|
+
end
|
674
|
+
|
675
|
+
#
|
676
|
+
# The one-pass reader & dispatcher of segments, SAX-style.
|
677
|
+
#
|
678
|
+
# It reads sequentially through the given records and
|
679
|
+
# generates calls to the callbacks <tt>on_...</tt>
|
680
|
+
# Parameter +hnd+ may be any object supporting method +gets+.
|
681
|
+
#
|
682
|
+
def go( hnd )
|
683
|
+
rec, rec_id, line_no, rec_no, pos_no, folgesatz = nil, nil, 0, 0, 0, nil
|
684
|
+
@path = hnd.path if hnd.respond_to? :path
|
685
|
+
@msg_begun = @msg_50_begun = false
|
686
|
+
@msg_29_nr = nil
|
687
|
+
|
688
|
+
self.on_interchange_start
|
689
|
+
|
690
|
+
catch(:done) do
|
691
|
+
loop do
|
692
|
+
begin
|
693
|
+
rec = hnd.gets
|
694
|
+
line_no += 1
|
695
|
+
break if rec.nil?
|
696
|
+
rec.chomp!
|
697
|
+
unless rec =~ /^\s*$/
|
698
|
+
pos_no = rec[125,6].to_i
|
699
|
+
rec_id = rec[0,2]
|
700
|
+
raise "Wrong record order at line #{line_no}! Expected: #{pos_no}, found: #{rec_no}" if rec_no != pos_no && rec_id !~ /00|01|99/
|
701
|
+
folgesatz = rec[146,2] # not valid for 00,01,03,51,96,98,99
|
702
|
+
end
|
703
|
+
case rec
|
704
|
+
when /^\s*$/
|
705
|
+
self.on_other( rec )
|
706
|
+
when /^00/
|
707
|
+
self.on_00( rec )
|
708
|
+
when /^99/
|
709
|
+
self.on_99( rec )
|
710
|
+
when /^01/
|
711
|
+
rec_no = pos_no
|
712
|
+
# raise "SA01: pos_nr=#{pos_no}, expected 1" if pos_no != 1
|
713
|
+
self.on_01( rec )
|
714
|
+
when /^98/
|
715
|
+
@msg_50_begun = false
|
716
|
+
self.on_98( rec )
|
717
|
+
|
718
|
+
when /^[12][25]/
|
719
|
+
self.on_msg_end if @msg_begun
|
720
|
+
self.on_msg_start( rec ) if folgesatz=='00'
|
721
|
+
self.on_segment( rec, rec_id+folgesatz )
|
722
|
+
|
723
|
+
when /^[12][3467]/
|
724
|
+
self.on_segment( rec, rec_id+folgesatz )
|
725
|
+
|
726
|
+
when /^29/ # Group change triggers new message!
|
727
|
+
if rec[30,1]=='2' # Reli-Nr.
|
728
|
+
nr_of_sa29 = rec[31..37]
|
729
|
+
else
|
730
|
+
raise "Unexpected SA29 condition"
|
731
|
+
end
|
732
|
+
if @msg_29_nr != nr_of_sa29 # Group change!
|
733
|
+
self.on_msg_end if @msg_begun
|
734
|
+
self.on_msg_start( rec )
|
735
|
+
@msg_29_nr = nr_of_sa29
|
736
|
+
end
|
737
|
+
self.on_segment( rec, rec_id+folgesatz )
|
738
|
+
|
739
|
+
when /^50/ # Only first occurrence of SA50 starts a message!
|
740
|
+
if not( @msg_50_begun )
|
741
|
+
self.on_msg_end if @msg_begun
|
742
|
+
raise "First SA50 - syntax error!" if folgesatz!='00'
|
743
|
+
self.on_msg_start( rec )
|
744
|
+
@msg_50_begun = true
|
745
|
+
end
|
746
|
+
self.on_segment( rec, rec_id+folgesatz )
|
747
|
+
|
748
|
+
when /^51/
|
749
|
+
self.on_segment( rec, '5100' )
|
750
|
+
|
751
|
+
else
|
752
|
+
$stderr.puts "Unsupported record type: '#{rec_id}#{folgesatz}' - ignored"
|
753
|
+
end
|
754
|
+
|
755
|
+
rescue
|
756
|
+
warn "Error at record #{rec_no}, record id=#{rec_id}"
|
757
|
+
raise
|
758
|
+
end
|
759
|
+
rec_no += 1
|
760
|
+
end # loop
|
761
|
+
|
762
|
+
end # catch(:done)
|
763
|
+
|
764
|
+
self.on_interchange_end
|
765
|
+
# offset
|
766
|
+
end
|
767
|
+
end # StreamingParser
|
768
|
+
|
769
|
+
#########################################################################
|
770
|
+
#
|
771
|
+
# = Class StreamingBuilder
|
772
|
+
#
|
773
|
+
# The StreamingBuilder parses the input stream just like StreamingParser
|
774
|
+
# and in addition builds the complete interchange.
|
775
|
+
#
|
776
|
+
# This method is the new basis of Interchange.parse. You might want to
|
777
|
+
# study its callbacks to get some ideas on how to create a special-purpose
|
778
|
+
# parser/builder of your own.
|
779
|
+
#
|
780
|
+
|
781
|
+
class StreamingBuilder < StreamingParser
|
782
|
+
def initialize(auto_validate=true)
|
783
|
+
@ic = nil
|
784
|
+
@curr_group = @curr_msg = nil
|
785
|
+
@una = nil
|
786
|
+
@is_iedi = false
|
787
|
+
@auto_validate = auto_validate
|
788
|
+
end
|
789
|
+
|
790
|
+
|
791
|
+
def interchange
|
792
|
+
@ic
|
793
|
+
end
|
794
|
+
|
795
|
+
|
796
|
+
def on_00( s )
|
797
|
+
@ic = Interchange.new
|
798
|
+
@ic.header = @ic.parse_segment( s )
|
799
|
+
end
|
800
|
+
|
801
|
+
def on_99( s )
|
802
|
+
@ic.trailer = @ic.parse_segment( s )
|
803
|
+
end
|
804
|
+
|
805
|
+
def on_01( s )
|
806
|
+
@curr_group = @ic.new_msggroup( @ic.parse_segment( s ) )
|
807
|
+
@curr_group.header = @curr_group.parse_segment( s )
|
808
|
+
end
|
809
|
+
|
810
|
+
def on_98( s )
|
811
|
+
self.on_msg_end if @msg_begun
|
812
|
+
@curr_group.trailer = @curr_group.parse_segment( s )
|
813
|
+
@ic.add( @curr_group, @auto_validate )
|
814
|
+
end
|
815
|
+
|
816
|
+
def on_msg_start( s )
|
817
|
+
@curr_msg = @curr_group.new_message( :sedas_type => s[0,2] )
|
818
|
+
@msg_begun = true
|
819
|
+
# @curr_msg.header = Segment.parse( @curr_msg, s )
|
820
|
+
end
|
821
|
+
|
822
|
+
def on_msg_end
|
823
|
+
@curr_group.add( @curr_msg )
|
824
|
+
@msg_begun = false
|
825
|
+
@msg_29_nr = nil
|
826
|
+
end
|
827
|
+
|
828
|
+
# Overwrite this method to react on segments of interest
|
829
|
+
#
|
830
|
+
# Note: For a skeleton Builder (just UNB/UNG/UNT etc), overwrite with
|
831
|
+
# an empty method.
|
832
|
+
#
|
833
|
+
def on_segment( s, tag )
|
834
|
+
@curr_msg.add @curr_msg.parse_segment( s )
|
835
|
+
super
|
836
|
+
end
|
837
|
+
|
838
|
+
|
839
|
+
def on_other( s )
|
840
|
+
warn "Empty record/line found - ignored!"
|
841
|
+
end
|
842
|
+
|
843
|
+
def on_interchange_end
|
844
|
+
if @auto_validate
|
845
|
+
@ic.header.validate
|
846
|
+
@ic.trailer.validate
|
847
|
+
# Content is already validated through @ic.add() and @curr_group.add()
|
848
|
+
end
|
849
|
+
end
|
850
|
+
|
851
|
+
end # StreamingBuilder
|
852
|
+
|
853
|
+
end # module S
|
854
|
+
end # module EDI
|