tediparse 2.0.0

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 (176) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +319 -0
  3. data/LICENSE +26 -0
  4. data/README.md +308 -0
  5. data/Rakefile +33 -0
  6. data/bin/tediparse +102 -0
  7. data/doc/Defining.md +97 -0
  8. data/doc/Generating-Grammars.md +241 -0
  9. data/doc/Generating.md +332 -0
  10. data/doc/Navigating.md +670 -0
  11. data/doc/Parsing.md +133 -0
  12. data/doc/README.md +51 -0
  13. data/doc/Serializing.md +100 -0
  14. data/doc/Tokenizing.md +136 -0
  15. data/doc/Validating.md +140 -0
  16. data/lib/ruby/array.rb +226 -0
  17. data/lib/ruby/blank.rb +52 -0
  18. data/lib/ruby/exception.rb +14 -0
  19. data/lib/ruby/hash.rb +14 -0
  20. data/lib/ruby/module.rb +60 -0
  21. data/lib/ruby/object.rb +56 -0
  22. data/lib/ruby/string.rb +89 -0
  23. data/lib/ruby/to_d.rb +82 -0
  24. data/lib/ruby/to_date.rb +26 -0
  25. data/lib/ruby/to_time.rb +21 -0
  26. data/lib/ruby/try.rb +46 -0
  27. data/lib/stupidedi/builder.rb +6 -0
  28. data/lib/stupidedi/color.rb +94 -0
  29. data/lib/stupidedi/config/code_list_config.rb +45 -0
  30. data/lib/stupidedi/config/functional_group_config.rb +72 -0
  31. data/lib/stupidedi/config/interchange_config.rb +86 -0
  32. data/lib/stupidedi/config/transaction_set_config.rb +73 -0
  33. data/lib/stupidedi/config.rb +100 -0
  34. data/lib/stupidedi/either.rb +286 -0
  35. data/lib/stupidedi/exceptions/invalid_element_error.rb +7 -0
  36. data/lib/stupidedi/exceptions/invalid_schema_error.rb +7 -0
  37. data/lib/stupidedi/exceptions/missing_grammar_error.rb +50 -0
  38. data/lib/stupidedi/exceptions/output_error.rb +7 -0
  39. data/lib/stupidedi/exceptions/parse_error.rb +7 -0
  40. data/lib/stupidedi/exceptions/stupidedi_error.rb +7 -0
  41. data/lib/stupidedi/exceptions/tokenize_error.rb +7 -0
  42. data/lib/stupidedi/exceptions/zipper_error.rb +7 -0
  43. data/lib/stupidedi/exceptions.rb +13 -0
  44. data/lib/stupidedi/inspect.rb +26 -0
  45. data/lib/stupidedi/interchanges/element_types/separator_val.rb +79 -0
  46. data/lib/stupidedi/interchanges/element_types/special_val.rb +48 -0
  47. data/lib/stupidedi/interchanges/element_types.rb +11 -0
  48. data/lib/stupidedi/interchanges.rb +21 -0
  49. data/lib/stupidedi/parser/builder_dsl.rb +292 -0
  50. data/lib/stupidedi/parser/constraint_table.rb +566 -0
  51. data/lib/stupidedi/parser/generation.rb +147 -0
  52. data/lib/stupidedi/parser/identifier_stack.rb +248 -0
  53. data/lib/stupidedi/parser/instruction.rb +112 -0
  54. data/lib/stupidedi/parser/instruction_table.rb +222 -0
  55. data/lib/stupidedi/parser/navigation.rb +786 -0
  56. data/lib/stupidedi/parser/state_machine.rb +62 -0
  57. data/lib/stupidedi/parser/states/abstract_state.rb +385 -0
  58. data/lib/stupidedi/parser/states/failure_state.rb +69 -0
  59. data/lib/stupidedi/parser/states/functional_group_state.rb +101 -0
  60. data/lib/stupidedi/parser/states/initial_state.rb +62 -0
  61. data/lib/stupidedi/parser/states/interchange_state.rb +96 -0
  62. data/lib/stupidedi/parser/states/loop_state.rb +74 -0
  63. data/lib/stupidedi/parser/states/table_state.rb +90 -0
  64. data/lib/stupidedi/parser/states/transaction_set_state.rb +120 -0
  65. data/lib/stupidedi/parser/states/transmission_state.rb +59 -0
  66. data/lib/stupidedi/parser/tokenization.rb +195 -0
  67. data/lib/stupidedi/parser.rb +32 -0
  68. data/lib/stupidedi/reader/input/abstract_input.rb +137 -0
  69. data/lib/stupidedi/reader/input/delegated_input.rb +112 -0
  70. data/lib/stupidedi/reader/input/file_input.rb +157 -0
  71. data/lib/stupidedi/reader/input.rb +31 -0
  72. data/lib/stupidedi/reader/position.rb +78 -0
  73. data/lib/stupidedi/reader/result.rb +172 -0
  74. data/lib/stupidedi/reader/segment_dict.rb +176 -0
  75. data/lib/stupidedi/reader/separators.rb +90 -0
  76. data/lib/stupidedi/reader/stream_reader.rb +173 -0
  77. data/lib/stupidedi/reader/token_reader.rb +465 -0
  78. data/lib/stupidedi/reader/tokens/component_element_tok.rb +71 -0
  79. data/lib/stupidedi/reader/tokens/composite_element_tok.rb +85 -0
  80. data/lib/stupidedi/reader/tokens/repeated_element_tok.rb +74 -0
  81. data/lib/stupidedi/reader/tokens/segment_tok.rb +74 -0
  82. data/lib/stupidedi/reader/tokens/simple_element_tok.rb +76 -0
  83. data/lib/stupidedi/reader.rb +121 -0
  84. data/lib/stupidedi/schema/abstract_def.rb +76 -0
  85. data/lib/stupidedi/schema/abstract_element_def.rb +35 -0
  86. data/lib/stupidedi/schema/abstract_element_use.rb +47 -0
  87. data/lib/stupidedi/schema/abstract_use.rb +79 -0
  88. data/lib/stupidedi/schema/code_list.rb +99 -0
  89. data/lib/stupidedi/schema/component_element_use.rb +76 -0
  90. data/lib/stupidedi/schema/composite_element_def.rb +103 -0
  91. data/lib/stupidedi/schema/composite_element_use.rb +78 -0
  92. data/lib/stupidedi/schema/element_req.rb +57 -0
  93. data/lib/stupidedi/schema/functional_group_def.rb +124 -0
  94. data/lib/stupidedi/schema/generation/definition_generator.rb +139 -0
  95. data/lib/stupidedi/schema/generation/element_generator.rb +221 -0
  96. data/lib/stupidedi/schema/generation/flat_file_reader.rb +551 -0
  97. data/lib/stupidedi/schema/generation/functional_group_generator.rb +64 -0
  98. data/lib/stupidedi/schema/generation/interchange_generator.rb +145 -0
  99. data/lib/stupidedi/schema/generation/master_loader_generator.rb +121 -0
  100. data/lib/stupidedi/schema/generation/models.rb +85 -0
  101. data/lib/stupidedi/schema/generation/module_loader_generator.rb +64 -0
  102. data/lib/stupidedi/schema/generation/registration_generator.rb +230 -0
  103. data/lib/stupidedi/schema/generation/runner.rb +161 -0
  104. data/lib/stupidedi/schema/generation/segment_generator.rb +130 -0
  105. data/lib/stupidedi/schema/generation/support.rb +78 -0
  106. data/lib/stupidedi/schema/generation/support_modules_generator.rb +126 -0
  107. data/lib/stupidedi/schema/generation/version_modules.rb +35 -0
  108. data/lib/stupidedi/schema/generation.rb +94 -0
  109. data/lib/stupidedi/schema/interchange_def.rb +103 -0
  110. data/lib/stupidedi/schema/loop_def.rb +156 -0
  111. data/lib/stupidedi/schema/repeat_count.rb +86 -0
  112. data/lib/stupidedi/schema/segment_def.rb +122 -0
  113. data/lib/stupidedi/schema/segment_req.rb +46 -0
  114. data/lib/stupidedi/schema/segment_use.rb +99 -0
  115. data/lib/stupidedi/schema/simple_element_def.rb +51 -0
  116. data/lib/stupidedi/schema/simple_element_use.rb +83 -0
  117. data/lib/stupidedi/schema/syntax_note.rb +52 -0
  118. data/lib/stupidedi/schema/table_def.rb +178 -0
  119. data/lib/stupidedi/schema/transaction_set_def.rb +125 -0
  120. data/lib/stupidedi/schema.rb +30 -0
  121. data/lib/stupidedi/sets.rb +42 -0
  122. data/lib/stupidedi/transaction_sets/builder/dsl.rb +192 -0
  123. data/lib/stupidedi/transaction_sets/builder.rb +188 -0
  124. data/lib/stupidedi/transaction_sets/common/implementations/element_reqs.rb +37 -0
  125. data/lib/stupidedi/transaction_sets/common/implementations/segment_reqs.rb +31 -0
  126. data/lib/stupidedi/transaction_sets/common/implementations.rb +11 -0
  127. data/lib/stupidedi/transaction_sets/common.rb +8 -0
  128. data/lib/stupidedi/transaction_sets/validation/ambiguity.rb +395 -0
  129. data/lib/stupidedi/transaction_sets/validation/implementation.rb +12 -0
  130. data/lib/stupidedi/transaction_sets/validation.rb +9 -0
  131. data/lib/stupidedi/transaction_sets.rb +25 -0
  132. data/lib/stupidedi/values/abstract_element_val.rb +19 -0
  133. data/lib/stupidedi/values/abstract_val.rb +133 -0
  134. data/lib/stupidedi/values/composite_element_val.rb +102 -0
  135. data/lib/stupidedi/values/functional_group_val.rb +105 -0
  136. data/lib/stupidedi/values/interchange_val.rb +102 -0
  137. data/lib/stupidedi/values/invalid_envelope_val.rb +61 -0
  138. data/lib/stupidedi/values/invalid_segment_val.rb +89 -0
  139. data/lib/stupidedi/values/loop_val.rb +73 -0
  140. data/lib/stupidedi/values/repeated_element_val.rb +113 -0
  141. data/lib/stupidedi/values/segment_val.rb +105 -0
  142. data/lib/stupidedi/values/segment_val_group.rb +20 -0
  143. data/lib/stupidedi/values/simple_element_val.rb +80 -0
  144. data/lib/stupidedi/values/table_val.rb +69 -0
  145. data/lib/stupidedi/values/transaction_set_val.rb +69 -0
  146. data/lib/stupidedi/values/transmission_val.rb +56 -0
  147. data/lib/stupidedi/values.rb +22 -0
  148. data/lib/stupidedi/version.rb +4 -0
  149. data/lib/stupidedi/versions/common/element_reqs.rb +13 -0
  150. data/lib/stupidedi/versions/common/element_types/an.rb +386 -0
  151. data/lib/stupidedi/versions/common/element_types/dt.rb +572 -0
  152. data/lib/stupidedi/versions/common/element_types/id.rb +304 -0
  153. data/lib/stupidedi/versions/common/element_types/nn.rb +312 -0
  154. data/lib/stupidedi/versions/common/element_types/operators.rb +128 -0
  155. data/lib/stupidedi/versions/common/element_types/r.rb +342 -0
  156. data/lib/stupidedi/versions/common/element_types/simple_element.rb +73 -0
  157. data/lib/stupidedi/versions/common/element_types/tm.rb +347 -0
  158. data/lib/stupidedi/versions/common/element_types.rb +29 -0
  159. data/lib/stupidedi/versions/common/segment_reqs.rb +15 -0
  160. data/lib/stupidedi/versions/common/syntax_notes.rb +172 -0
  161. data/lib/stupidedi/versions/common.rb +11 -0
  162. data/lib/stupidedi/versions.rb +25 -0
  163. data/lib/stupidedi/writer/claredi.rb +178 -0
  164. data/lib/stupidedi/writer/default.rb +119 -0
  165. data/lib/stupidedi/writer.rb +7 -0
  166. data/lib/stupidedi/zipper/abstract_cursor.rb +351 -0
  167. data/lib/stupidedi/zipper/dangling_cursor.rb +103 -0
  168. data/lib/stupidedi/zipper/edited_cursor.rb +157 -0
  169. data/lib/stupidedi/zipper/memoized_cursor.rb +133 -0
  170. data/lib/stupidedi/zipper/path.rb +132 -0
  171. data/lib/stupidedi/zipper/root_cursor.rb +120 -0
  172. data/lib/stupidedi/zipper/stack_cursor.rb +107 -0
  173. data/lib/stupidedi/zipper.rb +45 -0
  174. data/lib/stupidedi.rb +69 -0
  175. data/lib/tediparse.rb +1 -0
  176. metadata +249 -0
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Generates an interchange (ISA/IEA envelope) file. Detects the component
7
+ # separator from ISA element positions and whether the release supports a
8
+ # repetition separator (I65 in ISA11), and emits replace_separators.
9
+ class InterchangeGenerator
10
+ include Support
11
+
12
+ WORDS = {
13
+ "0" => "Oh", "1" => "One", "2" => "Two", "3" => "Three", "4" => "Four",
14
+ "5" => "Five", "6" => "Six", "7" => "Seven", "8" => "Eight", "9" => "Nine"
15
+ }.freeze
16
+
17
+ def initialize(release, namespace: "Edi")
18
+ @release = release
19
+ @namespace = namespace
20
+ end
21
+
22
+ def generate
23
+ validate_release!
24
+ build_ruby_content
25
+ end
26
+
27
+ def output_path
28
+ "#{namespace_path}/interchanges/#{interchange_file_name}.rb"
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :release
34
+
35
+ def release_code
36
+ release.code
37
+ end
38
+
39
+ # Convert "00401" -> "FourOhOne", "00406" -> "FourOhSix", "00501" -> "FiveOhOne"
40
+ # Ignore leading zeros, then convert remaining digits to words.
41
+ def interchange_module_name
42
+ significant = interchange_version_code.sub(/^0+/, "")
43
+ significant.chars.map { |d| WORDS[d] }.join
44
+ end
45
+
46
+ # Convert "FourOhOne" -> "four_oh_one"
47
+ def interchange_file_name
48
+ underscore(interchange_module_name)
49
+ end
50
+
51
+ def isa_segment
52
+ @isa_segment ||= release.segments.find { |s| s.code == "ISA" } ||
53
+ raise(ArgumentError, "Release #{release_code} has no ISA segment")
54
+ end
55
+
56
+ def has_repetition_separator?
57
+ position_11_use = isa_segment.element_uses.find { |eu| eu.position == 11 }
58
+ return false unless position_11_use
59
+
60
+ position_11_use.element.code == "I65"
61
+ end
62
+
63
+ def separators_method
64
+ if has_repetition_separator?
65
+ "Stupidedi::Reader::Separators.new(isa.element(16).to_s, isa.element(11).to_s, nil, nil)"
66
+ else
67
+ "Stupidedi::Reader::Separators.new(isa.element(16).to_s, nil, nil, nil)"
68
+ end
69
+ end
70
+
71
+ def replace_separators_method
72
+ isa_element_count = isa_segment.element_uses.size
73
+ rewrite_repetition = has_repetition_separator?
74
+
75
+ element_refs = (1..isa_element_count).map do |i|
76
+ suffix =
77
+ if i == isa_element_count
78
+ # Last element (ISA16) carries the component separator.
79
+ ".copy(:value => separators.component)]"
80
+ elsif rewrite_repetition && i == 11
81
+ # ISA11 carries the repetition separator on releases that use one
82
+ # (I65), matching the reference spec/support/synthetic envelope.
83
+ ".copy(:value => separators.repetition),"
84
+ else
85
+ ","
86
+ end
87
+
88
+ " isa.element(#{i})#{suffix}"
89
+ end
90
+
91
+ <<~RUBY.strip
92
+ # @return [SegmentVal]
93
+ def replace_separators(isa, separators)
94
+ isa.copy \\
95
+ :separators => separators,
96
+ :children =>
97
+ [#{element_refs.first.strip}
98
+ #{element_refs[1..].join("\n")}
99
+ end
100
+ RUBY
101
+ end
102
+
103
+ def build_ruby_content
104
+ <<~RUBY
105
+ # frozen_string_literal: true
106
+ require "tediparse"
107
+
108
+ module #{namespace}
109
+ module Interchanges
110
+ module #{interchange_module_name}
111
+ s = Stupidedi::Schema
112
+ r = #{namespace}::#{version_module}::ElementReqs
113
+ sd = #{namespace}::#{version_module}::SegmentDefs
114
+
115
+ InterchangeDef = Class.new(s::InterchangeDef) do
116
+ # @return [Values::InterchangeVal]
117
+ def empty(separators)
118
+ Stupidedi::Values::InterchangeVal.new(self, [], separators)
119
+ end
120
+
121
+ # @return [Module]
122
+ def segment_dict
123
+ #{namespace}::#{version_module}::SegmentDefs
124
+ end
125
+
126
+ # @return [Reader::Separators]
127
+ def separators(isa)
128
+ #{separators_method}
129
+ end
130
+
131
+ #{replace_separators_method}
132
+ end.new "#{interchange_version_code}",
133
+ [ sd::ISA.use(1, r::Mandatory, s::RepeatCount.bounded(1)),
134
+ sd::TA1.use(4, r::Optional, s::RepeatCount.unbounded) ],
135
+ [ sd::IEA.use(5, r::Mandatory, s::RepeatCount.bounded(1)) ]
136
+
137
+ end
138
+ end
139
+ end
140
+ RUBY
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Generates an optional master loader (e.g. edi.rb) that requires the whole
7
+ # generated tree in dependency order, so a non-Rails consumer can pull in
8
+ # the entire grammar with a single `require "edi"`.
9
+ #
10
+ # Like the registration generator it is a whole-tree artifact: it scans an
11
+ # ordered list of roots and unions the releases found (earlier roots shadow
12
+ # later ones per release), so a run can scan [staging, out] and require the
13
+ # freshly generated release together with releases already under out.
14
+ #
15
+ # Not emitted by default - it adds a file the Rails-autoloaded layout does
16
+ # not need. Pass master_loader: true (or --master-loader) to include it.
17
+ class MasterLoaderGenerator
18
+ include Support
19
+
20
+ # @param roots [String, Array<String>] one or more base directories.
21
+ def initialize(roots:, namespace: "Edi")
22
+ @roots = Array(roots)
23
+ @namespace = namespace
24
+ end
25
+
26
+ def generate
27
+ build_ruby_content
28
+ end
29
+
30
+ def output_path
31
+ "#{namespace_path}.rb"
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :roots
37
+
38
+ def namespace_roots
39
+ roots.map { |r| File.join(r, namespace_path) }
40
+ end
41
+
42
+ # [[dir_name, version_dir_path], ...] sorted by dir name, each release
43
+ # once, earlier roots shadowing later ones.
44
+ def version_dirs
45
+ seen = {}
46
+ dirs = []
47
+ namespace_roots.each do |base|
48
+ Dir.glob(File.join(base, "*")).each do |dir|
49
+ next unless File.directory?(dir)
50
+
51
+ dir_name = File.basename(dir)
52
+ next unless VERSION_MODULES.value?(camelize(dir_name))
53
+ next if seen[dir_name]
54
+
55
+ seen[dir_name] = true
56
+ dirs << [dir_name, dir]
57
+ end
58
+ end
59
+ dirs.sort_by(&:first)
60
+ end
61
+
62
+ def interchange_basenames
63
+ names = {}
64
+ namespace_roots.each do |base|
65
+ Dir.glob(File.join(base, "interchanges", "*.rb")).each do |path|
66
+ names[File.basename(path, ".rb")] ||= true
67
+ end
68
+ end
69
+ names.keys.sort
70
+ end
71
+
72
+ def registration_present?
73
+ namespace_roots.any? { |base| File.exist?(File.join(base, "stupidedi_registration.rb")) }
74
+ end
75
+
76
+ def require_lines
77
+ lines = []
78
+
79
+ # Version loaders first - they set up the per-version autoloads that the
80
+ # interchange and standards files (and the registration) depend on.
81
+ version_dirs.each do |dir_name, _|
82
+ lines << require_line("#{namespace_path}/#{dir_name}")
83
+ end
84
+
85
+ # Interchange envelopes.
86
+ interchange_basenames.each do |basename|
87
+ lines << require_line("#{namespace_path}/interchanges/#{basename}")
88
+ end
89
+
90
+ # Transaction-set standards.
91
+ version_dirs.each do |dir_name, dir|
92
+ Dir.glob(File.join(dir, "standards", "*.rb")).sort.each do |file|
93
+ lines << require_line("#{namespace_path}/#{dir_name}/standards/#{File.basename(file, '.rb')}")
94
+ end
95
+ end
96
+
97
+ # Registration last.
98
+ lines << require_line("#{namespace_path}/stupidedi_registration") if registration_present?
99
+
100
+ lines
101
+ end
102
+
103
+ def require_line(path)
104
+ %(require "#{path}")
105
+ end
106
+
107
+ def build_ruby_content
108
+ <<~RUBY
109
+ # frozen_string_literal: true
110
+
111
+ # Auto-generated by Stupidedi::Schema::Generation::MasterLoaderGenerator
112
+ # Requires the full generated grammar tree in dependency order.
113
+ require "tediparse"
114
+
115
+ #{require_lines.join("\n")}
116
+ RUBY
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Plain-Ruby value objects mirroring the shape of the X12 standard data.
7
+ #
8
+ # These are the seam between the input reader (Layer A) and the grammar
9
+ # generators (Layer B): the FlatFileReader populates them, the generators
10
+ # consume them. They carry no database or persistence concerns. Any source
11
+ # that builds objects responding to these methods can drive the generators.
12
+ #
13
+ # The reader always populates every field (collections default to []), so
14
+ # the structs need no custom initializers — keeping them compatible across
15
+ # the supported Ruby range (2.6+).
16
+ module Models
17
+ # A whole X12 release: its elements (simple + composite), its segments,
18
+ # and its transaction sets.
19
+ Release = Struct.new(:code, :elements, :segments, :transaction_sets, keyword_init: true)
20
+
21
+ # A data element or composite (distinguished by #is_composite).
22
+ # Simple elements carry x12_type/min_length/max_length/digits and, for
23
+ # ID elements, element_codes. Composites carry component_uses.
24
+ Element = Struct.new(
25
+ :code, :name, :description, :x12_type, :is_composite,
26
+ :min_length, :max_length, :digits, :element_codes, :component_uses,
27
+ keyword_init: true
28
+ )
29
+
30
+ # A single code-list entry for an ID element.
31
+ ElementCode = Struct.new(:code, :name, :paragraph, :partition, keyword_init: true)
32
+
33
+ # A component element used within a composite definition.
34
+ ComponentUse = Struct.new(:position, :requirement, :element, keyword_init: true)
35
+
36
+ # A segment definition with its ordered element uses and syntax notes.
37
+ Segment = Struct.new(:code, :name, :purpose, :element_uses, :syntax_notes, keyword_init: true)
38
+
39
+ # An element referenced from a specific position within a segment.
40
+ # #max_reps is the element's repetition count: an integer for a bounded
41
+ # repeat, or nil for unbounded (">1"). A non-repeating element use is 1.
42
+ ElementUse = Struct.new(:position, :requirement, :element, :max_reps, keyword_init: true)
43
+
44
+ # A segment syntax rule (P/R/C/E/L) over a set of element positions.
45
+ SyntaxNote = Struct.new(:condition_type, :element_positions, :description, keyword_init: true)
46
+
47
+ # A transaction set: its functional group id, code, name, owning release,
48
+ # and ordered table definitions.
49
+ TransactionSet = Struct.new(:code, :func_group, :name, :release, :table_definitions, keyword_init: true)
50
+
51
+ # A heading/detail/summary table. #area is the lowercase enum name
52
+ # ("heading"/"detail"/"summary"); #name is the display name. #children is
53
+ # a mixed, ordered list of SegmentUse and LoopDefinition.
54
+ TableDefinition = Struct.new(:area, :name, :position, :children, keyword_init: true) do
55
+ def ordered_children
56
+ # Index tiebreaker keeps ordering deterministic if two children ever
57
+ # share a position (Ruby's sort_by is not stable).
58
+ children.each_with_index.sort_by { |child, i| [child.position, i] }.map(&:first)
59
+ end
60
+ end
61
+
62
+ # A loop within a table or another loop. #children nests recursively.
63
+ # #max_reps is nil for unbounded.
64
+ LoopDefinition = Struct.new(:identifier, :max_reps, :position, :children, keyword_init: true) do
65
+ def ordered_children
66
+ children.each_with_index.sort_by { |child, i| [child.position, i] }.map(&:first)
67
+ end
68
+
69
+ def segment_use?
70
+ false
71
+ end
72
+ end
73
+
74
+ # A segment referenced from a specific position within a table or loop.
75
+ # #x12_sequence is the (string) sequence number from the standard, which
76
+ # takes precedence over #position when present.
77
+ SegmentUse = Struct.new(:segment, :x12_sequence, :position, :requirement, :max_reps, keyword_init: true) do
78
+ def segment_use?
79
+ true
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Generates the top-level per-version loader file (e.g. forty_ten.rb) with
7
+ # autoload statements in dependency order.
8
+ class ModuleLoaderGenerator
9
+ include Support
10
+
11
+ # Autoload order matters - dependencies must be loaded first
12
+ AUTOLOAD_MODULES = %w[
13
+ ElementReqs
14
+ ElementTypes
15
+ SegmentReqs
16
+ SyntaxNotes
17
+ ElementDefs
18
+ SegmentDefs
19
+ FunctionalGroupDef
20
+ ].freeze
21
+
22
+ def initialize(release, namespace: "Edi")
23
+ @release = release
24
+ @namespace = namespace
25
+ end
26
+
27
+ def generate
28
+ validate_release!
29
+ build_ruby_content
30
+ end
31
+
32
+ def output_path
33
+ "#{namespace_path}/#{underscore(version_module)}.rb"
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :release
39
+
40
+ def release_code
41
+ release.code
42
+ end
43
+
44
+ def build_ruby_content
45
+ base_path = "#{namespace_path}/#{underscore(version_module)}"
46
+ autoloads = AUTOLOAD_MODULES.map do |mod|
47
+ file_name = underscore(mod)
48
+ padding = " " * (18 - mod.length)
49
+ " autoload :#{mod},#{padding}\"#{base_path}/#{file_name}\""
50
+ end
51
+
52
+ <<~RUBY
53
+ # frozen_string_literal: true
54
+ module #{namespace}
55
+ module #{version_module}
56
+ #{autoloads.join("\n")}
57
+ end
58
+ end
59
+ RUBY
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Generates stupidedi_registration.rb. Does not read X12 data - instead it
7
+ # scans already-generated files and emits a register(config) method plus
8
+ # INTERCHANGE_VERSIONS / FUNCTIONAL_GROUP_VERSIONS constants.
9
+ #
10
+ # The registration is a *whole-tree* artifact: it must list every release
11
+ # present in the output tree, not just one. It therefore scans an ordered
12
+ # list of roots and unions the releases it finds. Earlier roots shadow
13
+ # later ones per release, so a run can scan [staging, out] and aggregate
14
+ # the freshly generated release (in staging) with the releases already
15
+ # committed under out, without listing either twice.
16
+ class RegistrationGenerator
17
+ include Support
18
+
19
+ WORDS_TO_DIGITS = {
20
+ "Oh" => "0", "One" => "1", "Two" => "2", "Three" => "3", "Four" => "4",
21
+ "Five" => "5", "Six" => "6", "Seven" => "7", "Eight" => "8", "Nine" => "9"
22
+ }.freeze
23
+
24
+ # @param roots [String, Array<String>] one or more base directories (each
25
+ # containing <namespace_path>/...). Earlier roots win per release.
26
+ def initialize(roots:, namespace: "Edi")
27
+ @roots = Array(roots)
28
+ @namespace = namespace
29
+ @registrations = {
30
+ interchanges: {},
31
+ functional_groups: {},
32
+ transaction_sets: {}
33
+ }
34
+ end
35
+
36
+ def generate
37
+ scan_existing_definitions
38
+ build_ruby_content
39
+ end
40
+
41
+ def output_path
42
+ "#{namespace_path}/stupidedi_registration.rb"
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :registrations, :roots
48
+
49
+ def namespace_roots
50
+ roots.map { |r| File.join(r, namespace_path) }
51
+ end
52
+
53
+ # Yields each release's version directory once, with earlier roots
54
+ # shadowing later ones for the same release.
55
+ def each_version_dir
56
+ seen = {}
57
+ namespace_roots.each do |base|
58
+ Dir.glob(File.join(base, "*")).each do |dir|
59
+ next unless File.directory?(dir)
60
+
61
+ dir_name = File.basename(dir)
62
+ version_module = camelize(dir_name)
63
+ next unless VERSION_MODULES.value?(version_module)
64
+ next if seen[dir_name]
65
+
66
+ seen[dir_name] = true
67
+ yield dir, version_module, module_to_release_code(version_module)
68
+ end
69
+ end
70
+ end
71
+
72
+ def scan_existing_definitions
73
+ scan_transaction_sets
74
+ scan_functional_groups
75
+ scan_interchanges
76
+ end
77
+
78
+ def scan_transaction_sets
79
+ each_version_dir do |version_dir, version_module, release_code|
80
+ Dir.glob(File.join(version_dir, "standards", "*.rb")).each do |path|
81
+ match = File.basename(path).match(/\A([A-Z]{2})(\d{3})\.rb\z/)
82
+ next unless match
83
+
84
+ func_group = match[1]
85
+ ts_code = match[2]
86
+ # A "TS" prefix encodes a nil/empty functional group (see
87
+ # DefinitionGenerator#constant_name).
88
+ constant_name = "#{namespace}::#{version_module}::Standards::#{func_group}#{ts_code}"
89
+ reg_func_group = func_group == "TS" ? nil : func_group
90
+
91
+ (registrations[:transaction_sets][release_code] ||= []) << {
92
+ func_group: reg_func_group,
93
+ ts_code: ts_code,
94
+ constant: constant_name
95
+ }
96
+ end
97
+ end
98
+
99
+ # Sort transaction sets within each release (handle nil func_group)
100
+ registrations[:transaction_sets].each_value do |ts_list|
101
+ ts_list.sort_by! { |ts| [ts[:func_group] || "ZZ", ts[:ts_code]] }
102
+ end
103
+ end
104
+
105
+ def scan_functional_groups
106
+ each_version_dir do |version_dir, version_module, release_code|
107
+ next unless File.exist?(File.join(version_dir, "functional_group_def.rb"))
108
+
109
+ registrations[:functional_groups][release_code] =
110
+ "#{namespace}::#{version_module}::FunctionalGroupDef"
111
+ end
112
+ end
113
+
114
+ def scan_interchanges
115
+ namespace_roots.each do |base|
116
+ Dir.glob(File.join(base, "interchanges", "*.rb")).each do |path|
117
+ module_name = camelize(File.basename(path, ".rb"))
118
+ interchange_code = interchange_module_to_code(module_name)
119
+
120
+ # ||= so an earlier root (e.g. staging) wins on collision.
121
+ registrations[:interchanges][interchange_code] ||=
122
+ "#{namespace}::Interchanges::#{module_name}::InterchangeDef"
123
+ end
124
+ end
125
+ end
126
+
127
+ def module_to_release_code(version_module)
128
+ VERSION_MODULES.key(version_module)
129
+ end
130
+
131
+ # Convert "FourOhOne" -> "00401", "FiveOhOne" -> "00501"
132
+ def interchange_module_to_code(module_name)
133
+ result = module_name.dup
134
+ WORDS_TO_DIGITS.each do |word, digit|
135
+ result = result.gsub(word, digit)
136
+ end
137
+ # Pad to 5 digits with leading zeros (interchange codes are 5 chars)
138
+ result.rjust(5, "0")
139
+ end
140
+
141
+ def build_ruby_content
142
+ lines = []
143
+ lines << "# frozen_string_literal: true"
144
+ lines << ""
145
+ lines << "# Auto-generated by Stupidedi::Schema::Generation::RegistrationGenerator"
146
+ lines << "# DO NOT EDIT MANUALLY"
147
+ lines << ""
148
+ lines << "module #{namespace}"
149
+ lines << " module StupidediRegistration"
150
+ lines << build_version_constants
151
+ lines << ""
152
+ lines << " def self.register(config)"
153
+ lines << " register_interchanges(config)"
154
+ lines << " register_functional_groups(config)"
155
+ lines << " register_transaction_sets(config)"
156
+ lines << " end"
157
+ lines << ""
158
+ lines << build_interchange_registration
159
+ lines << ""
160
+ lines << build_functional_group_registration
161
+ lines << ""
162
+ lines << build_transaction_set_registration
163
+ lines << " end"
164
+ lines << "end"
165
+ lines.join("\n") + "\n"
166
+ end
167
+
168
+ def build_version_constants
169
+ interchange_codes = registrations[:interchanges].keys.sort
170
+ functional_group_codes = registrations[:functional_groups].keys.sort
171
+
172
+ lines = []
173
+ lines << " INTERCHANGE_VERSIONS = %w[#{interchange_codes.join(' ')}].freeze"
174
+ lines << " FUNCTIONAL_GROUP_VERSIONS = %w[#{functional_group_codes.join(' ')}].freeze"
175
+ lines.join("\n")
176
+ end
177
+
178
+ def build_interchange_registration
179
+ lines = []
180
+ lines << " def self.register_interchanges(config)"
181
+ lines << " config.interchange.customize do |x|"
182
+
183
+ registrations[:interchanges].keys.sort.each do |code|
184
+ constant = registrations[:interchanges][code]
185
+ lines << " x.register(\"#{code}\") { #{constant} }"
186
+ end
187
+
188
+ lines << " end"
189
+ lines << " end"
190
+ lines.join("\n")
191
+ end
192
+
193
+ def build_functional_group_registration
194
+ lines = []
195
+ lines << " def self.register_functional_groups(config)"
196
+ lines << " config.functional_group.customize do |x|"
197
+
198
+ registrations[:functional_groups].keys.sort.each do |code|
199
+ constant = registrations[:functional_groups][code]
200
+ lines << " x.register(\"#{code}\") { #{constant} }"
201
+ end
202
+
203
+ lines << " end"
204
+ lines << " end"
205
+ lines.join("\n")
206
+ end
207
+
208
+ def build_transaction_set_registration
209
+ lines = []
210
+ lines << " def self.register_transaction_sets(config)"
211
+ lines << " config.transaction_set.customize do |x|"
212
+
213
+ registrations[:transaction_sets].keys.sort.each do |release_code|
214
+ ts_list = registrations[:transaction_sets][release_code]
215
+ ts_list.each do |ts|
216
+ # Skip transaction sets without a functional group - they can't be registered
217
+ next if ts[:func_group].nil?
218
+
219
+ lines << " x.register(\"#{release_code}\", \"#{ts[:func_group]}\", \"#{ts[:ts_code]}\") { #{ts[:constant]} }"
220
+ end
221
+ end
222
+
223
+ lines << " end"
224
+ lines << " end"
225
+ lines.join("\n")
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end