edi4r 0.9.4.1 → 0.9.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|