stupidedi 1.3.21 → 1.3.22

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