stupidedi 1.3.21 → 1.3.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stupidedi/builder/builder_dsl.rb +5 -0
  3. data/lib/stupidedi/builder/constraint_table.rb +100 -74
  4. data/lib/stupidedi/builder/generation.rb +73 -70
  5. data/lib/stupidedi/builder/instruction.rb +10 -0
  6. data/lib/stupidedi/builder/instruction_table.rb +19 -7
  7. data/lib/stupidedi/builder/state_machine.rb +9 -3
  8. data/lib/stupidedi/builder/states/abstract_state.rb +1 -1
  9. data/lib/stupidedi/builder/states/failure_state.rb +1 -1
  10. data/lib/stupidedi/builder/states/initial_state.rb +4 -4
  11. data/lib/stupidedi/contrib/002001/guides/SH856.rb +4 -4
  12. data/lib/stupidedi/contrib/002001/transaction_set_defs/PO830.rb +2 -2
  13. data/lib/stupidedi/contrib/003010/guides/PS830.rb +4 -4
  14. data/lib/stupidedi/contrib/003010/guides/RA820.rb +22 -12
  15. data/lib/stupidedi/contrib/003010/transaction_set_defs/PC860.rb +2 -2
  16. data/lib/stupidedi/contrib/003010/transaction_set_defs/PS830.rb +1 -1
  17. data/lib/stupidedi/contrib/003050/guides/PO850.rb +2 -2
  18. data/lib/stupidedi/contrib/003050/transaction_set_defs/PO850.rb +2 -2
  19. data/lib/stupidedi/contrib/004010/guides.rb +0 -1
  20. data/lib/stupidedi/contrib/004010/transaction_set_defs/AR943.rb +1 -1
  21. data/lib/stupidedi/contrib/004010/transaction_set_defs/IM210.rb +2 -2
  22. data/lib/stupidedi/contrib/004010/transaction_set_defs/RE944.rb +1 -1
  23. data/lib/stupidedi/contrib/004010/transaction_set_defs/SH856.rb +1 -7
  24. data/lib/stupidedi/editor.rb +0 -1
  25. data/lib/stupidedi/guides/004010/guide_builder.rb +1 -1
  26. data/lib/stupidedi/guides/005010/X223-HC837I.rb +1192 -1195
  27. data/lib/stupidedi/guides/005010/guide_builder.rb +1 -1
  28. data/lib/stupidedi/schema.rb +1 -0
  29. data/lib/stupidedi/schema/auditor.rb +435 -0
  30. data/lib/stupidedi/schema/loop_def.rb +18 -1
  31. data/lib/stupidedi/schema/transaction_set_def.rb +12 -0
  32. data/lib/stupidedi/version.rb +1 -1
  33. data/lib/stupidedi/versions/functional_groups/004010/transaction_set_defs/HP835.rb +3 -17
  34. data/lib/stupidedi/versions/functional_groups/005010/element_types/time_val.rb +3 -2
  35. data/lib/stupidedi/versions/functional_groups/005010/segment_defs.rb +9 -6
  36. data/lib/stupidedi/versions/functional_groups/005010/transaction_set_defs/HB271.rb +25 -9
  37. data/lib/stupidedi/versions/functional_groups/005010/transaction_set_defs/HP835.rb +2 -2
  38. data/lib/stupidedi/zipper.rb +20 -1
  39. data/lib/stupidedi/zipper/memoized_cursor.rb +2 -0
  40. data/lib/stupidedi/zipper/path.rb +10 -0
  41. data/lib/stupidedi/zipper/root_cursor.rb +1 -1
  42. data/lib/stupidedi/zipper/stack_cursor.rb +174 -0
  43. data/spec/examples/stupidedi/audit_spec.rb +58 -0
  44. data/spec/spec_helper.rb +21 -13
  45. data/spec/support/rcov.rb +9 -4
  46. metadata +4 -1
@@ -73,7 +73,7 @@ module Stupidedi
73
73
  unless elements.length == segment_def.element_uses.length
74
74
  raise Exceptions::InvalidSchemaError,
75
75
  "segment #{segment_def.id} has #{segment_def.element_uses.length}" +
76
- " elements but #{elements.length} arguments were specified"
76
+ " elements but #{elements.length} arguments were given"
77
77
  end
78
78
 
79
79
  element_index = "00"
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stupidedi
4
4
  module Schema
5
+ autoload :Auditor, "stupidedi/schema/auditor"
5
6
  autoload :AbstractDef, "stupidedi/schema/abstract_def"
6
7
  autoload :AbstractUse, "stupidedi/schema/abstract_use"
7
8
 
@@ -0,0 +1,435 @@
1
+ require "stupidedi"
2
+
3
+ module Stupidedi
4
+ using Refinements
5
+
6
+ module Schema
7
+
8
+ class Auditor
9
+ include Color
10
+ include Builder::Tokenization
11
+
12
+ attr_reader :config
13
+ attr_reader :machine
14
+ attr_reader :reader
15
+ attr_reader :queue
16
+
17
+ def initialize(machine, reader, isa_elements, gs_elements, st_elements)
18
+ @config = machine.config
19
+ @reader = reader
20
+ @queue = []
21
+ @queued = Set.new
22
+
23
+ @elements = Hash.new{|h,k| h[k] = [] }
24
+ @elements[:ISA] = isa_elements
25
+ @elements[:GS] = gs_elements
26
+ @elements[:ST] = st_elements
27
+
28
+ enqueue(machine.active.head)
29
+ end
30
+
31
+ # Throw an exception if the transaction set definition has any ambiguity
32
+ # that could cause non-deterministic parser state.
33
+ def audit
34
+ while cursor = @queue.shift
35
+ step(Stupidedi::Builder::StateMachine.new(@config, [cursor]))
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def enqueue(zipper)
42
+ # Avoid unecessary traversal over instruction tables that we've already
43
+ # queued. NOTE: This means some segments in the definition could be
44
+ # skipped entirely, due to have the same #successors as another segment.
45
+ #
46
+ # For instance, consider HP835's Table 1:
47
+ #
48
+ # d::TableDef.header("Table 1 - Header",
49
+ # b::Segment(100, s::ST, "Transaction Set Header",
50
+ # b::Segment(200, s::BPR, "Financial Information",<Paste>
51
+ # b::Segment(400, s::TRN, "Reassociation Trace Number",
52
+ # b::Segment(500, s::CUR, "Foreign Currency Information",
53
+ # b::Segment(600, s::REF, "Receiver Identification",
54
+ # b::Segment(600, s::REF, "Version Identification",
55
+ # b::Segment(700, s::DTM, "Production Date",
56
+ #
57
+ # It's important to know that two segments at the same position don't
58
+ # come "before" or "after" each other, even though they might be listed
59
+ # in a particular order.
60
+ #
61
+ # Starting at the CUR segment, its successors are the two REF segments
62
+ # and some other stuff. If we generate the first REF segment, due to
63
+ # having the same position (600) as the other REF segment, it also has
64
+ # both REF segments as successors. The second REF segment also has the
65
+ # same set of successors.
66
+ #
67
+ # Therefore, all three segments (CUR, REF, REF) have the same set of
68
+ # successors and it doesn't matter which one we choose; we only need
69
+ # to choose one.
70
+
71
+ id = zipper.node.instructions.hash
72
+
73
+ unless @queued.member?(id)
74
+ @queued.add(id)
75
+ @queue.push(zipper)
76
+ # puts "Queued: #{pp_zipper(zipper)}: #{id}"
77
+ end
78
+ end
79
+
80
+ # Starting from the given deterministic `StateMachine`, evaluate each of
81
+ # its `#successors` to detect when an input could make it impossible to
82
+ # choose only a single successor state, due to ambiguity in the definition
83
+ # of the transaction set. If this situation is detected, an exception will
84
+ # be thrown.
85
+ #
86
+ # Otherwise, for each successor, generate the corresponding segment and
87
+ # add the resulting StateMachine to the queue to be checked recursively.
88
+ def step(machine)
89
+ # @todo: when segment_id is :ISA, :GS, or :ST, use the correct values?
90
+ machine.successors.head.constraints.each do |segment_id, table|
91
+ case table
92
+ when Stupidedi::Builder::ConstraintTable::Shallowest,
93
+ Stupidedi::Builder::ConstraintTable::Deepest
94
+ segment_tok =
95
+ mksegment_tok(@reader.segment_dict, segment_id, @elements[segment_id], nil)
96
+
97
+ op = table.matches(segment_tok, false).head
98
+ as = machine.execute(op, machine.active.head, @reader, segment_tok)
99
+ enqueue(as)
100
+
101
+ when Stupidedi::Builder::ConstraintTable::Stub
102
+ if table.instructions.length == 1
103
+ segment_tok =
104
+ mksegment_tok(@reader.segment_dict, segment_id, @elements[segment_id], nil)
105
+
106
+ op = table.matches(nil, false).head
107
+ as = machine.execute(op, machine.active.head, @reader, segment_tok)
108
+ enqueue(as)
109
+ else
110
+ raise Exceptions::InvalidSchemaError,
111
+ "proceeding from [#{pp_machine(machine)}], there are no " +
112
+ "constraints on segment #{segment_id} to choose between:\n" +
113
+ table.instructions.map{|x| pp_instruction(x) }.join
114
+ end
115
+
116
+ when Stupidedi::Builder::ConstraintTable::ValueBased
117
+ disjoint, distinct = table.basis(table.instructions)
118
+
119
+ if disjoint.empty?
120
+ # We're definitely going to throw an error now, but we need to
121
+ # get more information to make a useful error message. First
122
+ # check that interesting elements are required
123
+ designators = optional_elements(distinct, table.instructions)
124
+
125
+ case designators.length
126
+ when 0
127
+ when 1
128
+ raise Exceptions::InvalidSchemaError,
129
+ "proceeding from [#{pp_machine(machine)}], the element " +
130
+ "#{designators.join(", ")} is designated optional, but " +
131
+ "is necessary to choose between:\n" +
132
+ table.instructions.map{|x| pp_instruction(x) }.join
133
+ else
134
+ raise Exceptions::InvalidSchemaError,
135
+ "proceeding from [#{pp_machine(machine)}], the elements " +
136
+ "#{designators.join(", ")} are designated optional, but " +
137
+ "at least one is necessary to choose between:\n" +
138
+ table.instructions.map{|x| pp_instruction(x) }.join
139
+ end
140
+
141
+ # Next report cases of element values that cause ambiguity
142
+ designators, ex_designator, ex_value, ex_instructions =
143
+ distinct_elements(distinct, table.instructions)
144
+
145
+ case designators.length
146
+ when 0
147
+ raise Exceptions::InvalidSchemaError,
148
+ "proceeding from [#{pp_machine(machine)}], there are no " +
149
+ "constraints on segment #{segment_id} to choose " +
150
+ "between:\n" + table.instructions.map{|x| pp_instruction(x) }.join
151
+ when 1
152
+ raise Exceptions::InvalidSchemaError,
153
+ "proceeding from [#{pp_machine(machine)}], the element " +
154
+ "#{ex_designator} has overlapping values among possible " +
155
+ "choices; for example when it has value the #{ex_value}, "+
156
+ "it is not possible to choose between:\n" +
157
+ ex_instructions.map{|x| pp_instruction(x) }.join
158
+ else
159
+ raise Exceptions::InvalidSchemaError,
160
+ "proceeding from [#{pp_machine(machine)}], the elements " +
161
+ "#{designators.join(", ")} each have overlapping values " +
162
+ "among choices; for example when #{ex_designator} has " +
163
+ "the value #{ex_value}, it is not possible to choose " +
164
+ "between:\n" + ex_instructions.map{|x| pp_instruction(x) }.join
165
+ end
166
+ else
167
+ # It's possible there is no ambiguity here, but we need to check
168
+ designators = optional_elements(disjoint, table.instructions)
169
+
170
+ case designators.length
171
+ when 0
172
+ when 1
173
+ raise Exceptions::InvalidSchemaError,
174
+ "proceeding from [#{pp_machine(machine)}], the element " +
175
+ "#{designators.join(", ")} is designated optional in at " +
176
+ "least one case, but is necessary to choose between:\n" +
177
+ table.instructions.map{|x| pp_instruction(x) }.join
178
+ else
179
+ raise Exceptions::InvalidSchemaError,
180
+ "proceeding from [#{pp_machine(machine)}], the elements " +
181
+ "#{designators.join(", ")} are designated optional in at " +
182
+ "least one case each, but at least one is necessary to " +
183
+ "choose between:\n" +
184
+ table.instructions.map{|x| pp_instruction(x) }.join
185
+ end
186
+
187
+ mksegments(table).each do |op, segment_tok|
188
+ enqueue(machine.execute(op, machine.active.head, @reader, segment_tok))
189
+ end
190
+ end
191
+ else
192
+ raise "unexpected kind of constraint table: #{constraint_table.class}"
193
+ end
194
+ end
195
+ end
196
+
197
+ # For each instruction in the given ConstraintTable::ValueBased, return
198
+ # the segment token which will be matched to that instruction.
199
+ #
200
+ # @return [Array<(Instruction, SegmentTok)>]
201
+ def mksegments(table)
202
+ disjoint, = table.basis(table.instructions)
203
+ remaining = Set.new(table.instructions)
204
+ segment_id = table.instructions.head.segment_use.id
205
+ segments = []
206
+
207
+ # Scan each interesting element location
208
+ disjoint.each do |(m, n), map|
209
+ if remaining.empty?
210
+ # We've already generated results for each possible instruction
211
+ break
212
+ end
213
+
214
+ # Likely many values are mapped to only a few unique keys; in this
215
+ # case, `instructions` is a singleton array (that's what makes the
216
+ # value non-ambiguous, there's only one possible instruction)
217
+ map.each do |value, instructions|
218
+ op, = instructions
219
+ repeatable = op.segment_use.definition.element_uses.at(m).repeatable?
220
+ elements = Array.new(m)
221
+ elements[m] =
222
+ if n.nil?
223
+ if repeatable
224
+ repeated(value)
225
+ else
226
+ value
227
+ end
228
+ else
229
+ components = Array.new(n)
230
+ components[n] = value
231
+
232
+ if repeatable
233
+ repeated(composite(components))
234
+ else
235
+ composite(components)
236
+ end
237
+ end
238
+
239
+ if remaining.member?(op)
240
+ remaining.delete(op)
241
+ segments.push([op, mksegment_tok(@reader.segment_dict, segment_id, elements, nil)])
242
+ end
243
+ end
244
+ end
245
+
246
+ segments
247
+ end
248
+
249
+ # Return a list of element designators that are needed to choose a single
250
+ # instruction, but are not #required? in the transaction set definition.
251
+ #
252
+ # @return [Array<String>]
253
+ def optional_elements(disjoint, instructions)
254
+ designators = []
255
+
256
+ disjoint.each do |(m, n), map|
257
+ required = true
258
+
259
+ # Element must be required for all possible segment_uses
260
+ instructions.each do |op|
261
+ element_use = op.segment_use.definition.element_uses.at(m)
262
+
263
+ if n.nil?
264
+ required &&= element_use.required?
265
+ else
266
+ component_use = element_use.definition.component_uses.at(n)
267
+ required &&= element_use.required? and component_use.required?
268
+ end
269
+ end
270
+
271
+ unless required
272
+ segment_id = instructions.head.segment_use.id
273
+ designators << "#{segment_id}-#{'%02d' % (m+1)}#{n.try{'-%02d' % (n+1)}}"
274
+ end
275
+ end
276
+
277
+ if designators.length < disjoint.length
278
+ # This means at least one designator *was* required, therefore we can
279
+ # be assured that valid input will not be ambiguous
280
+ []
281
+ else
282
+ # None of the useful elements were required, so even valid input (that
283
+ # has none of these elements present), will be ambiguous
284
+ designators
285
+ end
286
+ end
287
+
288
+ # Build a list of offending elements (eg, HL-03) and provide an example
289
+ # value that resolves to conflicting instructions.
290
+ #
291
+ # @return [(Array<String>, String, String, Array<Instruction>)]
292
+ def distinct_elements(distinct, instructions)
293
+ designators = []
294
+ ex_designator = nil
295
+ ex_value = nil
296
+ ex_instructions = nil
297
+
298
+ distinct.each do |(m, n), map|
299
+ designators <<
300
+ "#{instructions.head.segment_use.id}-#{'%02d' % (m+1)}#{n.try{'-%02d' % (n+1)}}"
301
+
302
+ if ex_designator.nil?
303
+ ex_designator = designators.last
304
+ ex_value, ex_instructions = map.max_by{|_, v| v.length }
305
+ end
306
+ end
307
+
308
+ return designators, ex_designator, ex_value, ex_instructions
309
+ end
310
+
311
+ # @return [String]
312
+ def pp_machine(machine)
313
+ pp_zipper(machine.active.head)
314
+ end
315
+
316
+ # @return [String]
317
+ def pp_zipper(zipper)
318
+ segment = zipper.node.zipper.node
319
+
320
+ if segment.valid?
321
+ id = '% 3s' % segment.definition.id.to_s
322
+ name = segment.definition.name
323
+ width = 18
324
+
325
+ if name.length > width - 2
326
+ id = id + ": #{name.slice(0, width - 2)}.."
327
+ else
328
+ id = id + ": #{name}"
329
+ end
330
+
331
+ args = "position:%d, repeat:%s" %
332
+ [segment.usage.position,
333
+ segment.usage.repeat_count.inspect]
334
+
335
+ ansi.segment("SegmentVal[#{ansi.bold(id)}](#{args})")
336
+ else
337
+ segment.pretty_inspect
338
+ end
339
+ end
340
+
341
+ # @return [String]
342
+ def pp_instruction(op)
343
+ id = '% 3s' % op.segment_id.to_s
344
+
345
+ unless op.segment_use.nil?
346
+ width = 30
347
+ name = op.segment_use.definition.name
348
+
349
+ # Truncate the segment name to `width` characters
350
+ if name.length > width - 2
351
+ id = id + ": #{name.slice(0, width - 2)}.."
352
+ else
353
+ id = id + ": #{name.ljust(width)}"
354
+ end
355
+ end
356
+
357
+ args = "position:%d, repeat:%s, pop:%d" %
358
+ [op.segment_use.position,
359
+ op.segment_use.repeat_count.inspect,
360
+ op.pop_count]
361
+
362
+ unless op.push.nil?
363
+ args += ", push:#{op.push.try{|c| c.name.split('::').last}}"
364
+ end
365
+
366
+ " Instruction[#{id}](#{args})\n"
367
+ end
368
+ end
369
+
370
+ class << Auditor
371
+
372
+ def build(definition)
373
+ # Use dummy identifiers to link definition to the parser
374
+ config = mkconfig(definition, "ISA11", "GS01", "GS08", "ST01")
375
+ builder = Builder::BuilderDsl.new(
376
+ Builder::StateMachine.build(config, Zipper::Stack), false)
377
+
378
+ # These lists of elements are re-used when Auditor needs to construct
379
+ # an ISA, GS, or ST segment as it walks the definition.
380
+ isa_elements =
381
+ [ "00", "AUTHORIZATION",
382
+ "00", "PASSWORD",
383
+ "ZZ", "SUBMITTER ID",
384
+ "ZZ", "RECEIVER ID",
385
+ "191225", "1230",
386
+ "^",
387
+ "ISA11",
388
+ "123456789", "0", "T", ":" ]
389
+
390
+ gs_elements =
391
+ [ "GS01",
392
+ "SENDER ID",
393
+ "RECEIVER ID",
394
+ "20191225", "1230", "1", "X",
395
+ "GS08" ]
396
+
397
+ st_elements =
398
+ [ "ST01",
399
+ "1234" ]
400
+
401
+ builder.ISA(*isa_elements)
402
+ builder.GS(*gs_elements)
403
+ builder.ST(*st_elements)
404
+
405
+ new(builder.machine, builder.reader, isa_elements, gs_elements, st_elements)
406
+ end
407
+
408
+ def mkconfig(definition, isa11, gs01, gs08, st01)
409
+ segment = definition.table_defs.head.header_segment_uses.head.definition
410
+
411
+ # Infer the FunctionalGroupDef based on the given `definition`
412
+ element = segment.element_uses.head.definition
413
+ version = element.class.name.split('::').slice(0..-3)
414
+ grpdefn = Object.const_get("FunctionalGroupDef".snoc(version).join('::'))
415
+
416
+ Config.new.customize do |c|
417
+ c.interchange.customize do |x|
418
+ # We can use whatever interchange version we like, it does not
419
+ # have any bearing or relationship to the given `definition`
420
+ x.register(isa11, Versions::Interchanges::FiveOhOne::InterchangeDef)
421
+ end
422
+
423
+ c.functional_group.customize do |x|
424
+ x.register(gs08, grpdefn)
425
+ end
426
+
427
+ c.transaction_set.customize do |x|
428
+ x.register(gs08, gs01, st01, definition)
429
+ end
430
+ end
431
+ end
432
+ end
433
+
434
+ end
435
+ end