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,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tmpdir"
5
+
6
+ module Stupidedi
7
+ module Schema
8
+ module Generation
9
+ # Orchestrates a full-release generation run. Reads the flat files, then
10
+ # runs every generator in dependency order. Mirrors the Tediware
11
+ # +stupidedi:generate_version+ pipeline.
12
+ #
13
+ # All files are generated into a temporary staging directory first. The
14
+ # registration and master-loader generators scan that staging tree (not
15
+ # the live +out+), so their content is always consistent with the run -
16
+ # including under write: false (dry-run previews are accurate). On a
17
+ # successful write run the staging tree is copied into +out+ in one step,
18
+ # so a failure partway through never leaves a half-written tree.
19
+ class Runner
20
+ # Describes one generated file.
21
+ Result = Struct.new(:path, :relative_path, :content, :written, keyword_init: true)
22
+
23
+ def initialize(table_data:, release:, out:, namespace: "Edi", write: true, master_loader: false, logger: nil)
24
+ @table_data = table_data
25
+ @release_code = release
26
+ @out = out
27
+ @namespace = namespace
28
+ @write = write
29
+ @master_loader = master_loader
30
+ @logger = logger
31
+ end
32
+
33
+ # @return [Array<Result>] one entry per generated file
34
+ def run
35
+ Generation.validate_namespace!(@namespace)
36
+ validate_release!
37
+
38
+ release = FlatFileReader.read(@table_data, @release_code)
39
+ guard_against_empty!(release)
40
+ log "Read release #{release.code}: " \
41
+ "#{release.elements.size} elements, #{release.segments.size} segments, " \
42
+ "#{release.transaction_sets.size} transaction sets"
43
+
44
+ results = []
45
+ Dir.mktmpdir("tediparse-gen") do |staging|
46
+ @staging = staging
47
+ results = generate_all(release)
48
+ if @write
49
+ ensure_out
50
+ replace_staged_release_dirs
51
+ FileUtils.cp_r(File.join(staging, "."), @out)
52
+ end
53
+ end
54
+
55
+ log "#{@write ? 'Wrote' : 'Previewed'} #{results.size} files under #{@out}"
56
+ results
57
+ end
58
+
59
+ private
60
+
61
+ def generate_all(release)
62
+ results = []
63
+ results.concat(run_support_modules(release))
64
+ results << emit(ElementGenerator.new(release, namespace: @namespace))
65
+ results << emit(SegmentGenerator.new(release, include_control_segments: true, namespace: @namespace))
66
+ results << emit(FunctionalGroupGenerator.new(release, namespace: @namespace))
67
+ results << emit(ModuleLoaderGenerator.new(release, namespace: @namespace))
68
+ results << emit(InterchangeGenerator.new(release, namespace: @namespace))
69
+
70
+ release.transaction_sets.each do |ts|
71
+ results << emit(DefinitionGenerator.new(ts, namespace: @namespace))
72
+ end
73
+
74
+ # Registration and master loader are whole-tree artifacts. They scan
75
+ # [staging, out] so the emitted files cover the release just generated
76
+ # (in staging) UNION the releases already present under out, with
77
+ # staging shadowing out for the regenerated release. This keeps a
78
+ # multi-release output tree consistent and makes dry-run previews
79
+ # reflect the true resulting tree.
80
+ roots = [@staging, @out]
81
+ results << emit(RegistrationGenerator.new(roots: roots, namespace: @namespace))
82
+ results << emit(MasterLoaderGenerator.new(roots: roots, namespace: @namespace)) if @master_loader
83
+ results
84
+ end
85
+
86
+ def run_support_modules(release)
87
+ generator = SupportModulesGenerator.new(release, namespace: @namespace)
88
+ paths = generator.output_paths
89
+ generator.generate_all.map do |mod, content|
90
+ write_result(paths[mod], content)
91
+ end
92
+ end
93
+
94
+ def emit(generator)
95
+ write_result(generator.output_path, generator.generate)
96
+ end
97
+
98
+ # Always writes into the staging tree (so the disk-scanning generators
99
+ # see a complete, consistent picture); the staging tree is copied to
100
+ # +out+ once, at the end, only on a successful write run.
101
+ def write_result(relative_path, content)
102
+ staged_path = File.join(@staging, relative_path)
103
+ FileUtils.mkdir_p(File.dirname(staged_path))
104
+ File.write(staged_path, content)
105
+
106
+ full_path = File.join(@out, relative_path)
107
+ log(@write ? " #{full_path}" : " would write #{full_path} (#{content.lines.size} lines)")
108
+
109
+ Result.new(path: full_path, relative_path: relative_path, content: content, written: @write)
110
+ end
111
+
112
+ def validate_release!
113
+ return if VERSION_MODULES.key?(@release_code)
114
+
115
+ raise ArgumentError, "Unsupported release code: #{@release_code}. " \
116
+ "Supported: #{VERSION_MODULES.keys.join(', ')}"
117
+ end
118
+
119
+ def guard_against_empty!(release)
120
+ return unless release.segments.empty? || release.elements.empty?
121
+
122
+ raise ArgumentError,
123
+ "No X12 table data found in #{@table_data} " \
124
+ "(expected ELEDETL.TXT, SEGHEAD.TXT, SETHEAD.TXT, ... — got " \
125
+ "#{release.elements.size} elements, #{release.segments.size} segments). " \
126
+ "Check the --table-data path points at the release's table-data directory."
127
+ end
128
+
129
+ def ensure_out
130
+ FileUtils.mkdir_p(@out)
131
+ @out
132
+ end
133
+
134
+ # Remove the live-tree counterpart of every release directory the staging
135
+ # tree is about to deliver, so the regenerated release REPLACES (rather
136
+ # than merges into) what was there. Without this, a regeneration whose
137
+ # transaction-set list shrank would leave orphaned standards/*.rb behind -
138
+ # which the run itself shadows correctly, but which would later mislead a
139
+ # standalone Generation.register scan. The shared `interchanges/` dir and
140
+ # other releases are left untouched.
141
+ def replace_staged_release_dirs
142
+ Dir.glob(File.join(@staging, "*")).each do |namespace_dir|
143
+ next unless File.directory?(namespace_dir)
144
+
145
+ namespace_name = File.basename(namespace_dir)
146
+ Dir.glob(File.join(namespace_dir, "*")).each do |release_dir|
147
+ next unless File.directory?(release_dir)
148
+ next if File.basename(release_dir) == "interchanges"
149
+
150
+ FileUtils.rm_rf(File.join(@out, namespace_name, File.basename(release_dir)))
151
+ end
152
+ end
153
+ end
154
+
155
+ def log(message)
156
+ @logger&.call(message)
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Generates segment_defs.rb for a release: every segment definition with
7
+ # its ordered element uses and syntax notes. With include_control_segments:
8
+ # true it also emits ISA/IEA/GS/GE/TA1.
9
+ class SegmentGenerator
10
+ include Support
11
+
12
+ SYNTAX_NOTE_MAP = {
13
+ "paired" => "P",
14
+ "required" => "R",
15
+ "conditional" => "C",
16
+ "excluded" => "E",
17
+ "list_conditional" => "L"
18
+ }.freeze
19
+
20
+ # Interchange control segments - included when include_control_segments: true
21
+ CONTROL_SEGMENTS = %w[ISA IEA GS GE TA1].freeze
22
+
23
+ def initialize(release, include_control_segments: false, namespace: "Edi")
24
+ @release = release
25
+ @include_control_segments = include_control_segments
26
+ @namespace = namespace
27
+ end
28
+
29
+ def generate
30
+ validate_release!
31
+ build_ruby_content
32
+ end
33
+
34
+ def output_path
35
+ "#{namespace_path}/#{underscore(version_module)}/segment_defs.rb"
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :release
41
+
42
+ def release_code
43
+ release.code
44
+ end
45
+
46
+ def include_control_segments?
47
+ @include_control_segments
48
+ end
49
+
50
+ def build_ruby_content
51
+ lines = []
52
+ lines << 'require "tediparse"'
53
+ lines << "module #{namespace}"
54
+ lines << " module #{version_module}"
55
+ lines << " module SegmentDefs"
56
+ lines << " s = Stupidedi::Schema"
57
+ lines << " e = #{namespace}::#{version_module}::ElementDefs"
58
+ lines << " r = #{namespace}::#{version_module}::ElementReqs"
59
+ lines << ""
60
+ lines << build_segment_definitions
61
+ lines << " end"
62
+ lines << " end"
63
+ lines << "end"
64
+ lines.join("\n") + "\n"
65
+ end
66
+
67
+ def build_segment_definitions
68
+ segments = release.segments.sort_by(&:code)
69
+ segments = segments.reject { |seg| CONTROL_SEGMENTS.include?(seg.code) } unless include_control_segments?
70
+
71
+ segments.map { |segment| generate_segment_def(segment) }.join("\n")
72
+ end
73
+
74
+ def generate_segment_def(segment)
75
+ lines = []
76
+ lines << "#{indent(3)}#{segment.code} ||= s::SegmentDef.build(:#{segment.code},"
77
+ lines << "#{indent(4)}#{escape_string(segment.name)},"
78
+ lines << "#{indent(4)}#{escape_string(segment.purpose || '')},"
79
+
80
+ element_uses = segment.element_uses.sort_by(&:position)
81
+ syntax_notes = segment.syntax_notes.to_a
82
+
83
+ element_uses.each_with_index do |element_use, index|
84
+ is_last = index == element_uses.length - 1 && syntax_notes.empty?
85
+ lines << generate_element_use(element_use, is_last: is_last)
86
+ end
87
+
88
+ syntax_notes.each_with_index do |syntax_note, index|
89
+ is_last = index == syntax_notes.length - 1
90
+ lines << generate_syntax_note(syntax_note, is_last: is_last)
91
+ end
92
+
93
+ lines << "#{indent(3)})"
94
+ lines.join("\n")
95
+ end
96
+
97
+ def generate_element_use(element_use, is_last:)
98
+ element = element_use.element
99
+ requirement = format_requirement(element_use.requirement)
100
+ trailing_comma = is_last ? "" : ","
101
+
102
+ comment = " # Min #{element.min_length}/Max #{element.max_length}"
103
+
104
+ # Composite elements don't have the E prefix
105
+ element_ref = element.is_composite ? element.code : "E#{element.code}"
106
+
107
+ "#{indent(4)}e::#{element_ref}.simple_use(#{requirement}, #{repeat_count_expr(element_use.max_reps)})#{trailing_comma}#{comment}"
108
+ end
109
+
110
+ # nil max_reps -> unbounded (the ">1" repetition marker); an integer
111
+ # count -> bounded(n). A non-repeating element use carries 1.
112
+ def repeat_count_expr(max_reps)
113
+ max_reps.nil? ? "s::RepeatCount.unbounded" : "s::RepeatCount.bounded(#{max_reps})"
114
+ end
115
+
116
+ def generate_syntax_note(syntax_note, is_last:)
117
+ note_class = SYNTAX_NOTE_MAP.fetch(syntax_note.condition_type) do
118
+ raise ArgumentError, "Unknown condition_type: #{syntax_note.condition_type}"
119
+ end
120
+
121
+ positions = syntax_note.element_positions
122
+ positions_str = positions.join(", ")
123
+ trailing_comma = is_last ? "" : ","
124
+
125
+ "#{indent(4)}#{namespace}::#{version_module}::SyntaxNotes::#{note_class}.build(#{positions_str})#{trailing_comma}"
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Shared helpers mixed into every generator.
7
+ #
8
+ # The host class must provide a private +#release_code+ returning the
9
+ # 6-character release string (e.g. "005010") and may set +@namespace+,
10
+ # the root Ruby module for the emitted code (defaults to "Edi").
11
+ module Support
12
+ # Root module for generated constants, e.g. "Edi". Consumers override
13
+ # this to emit into their own namespace. Validated (and memoized) on
14
+ # first use so an invalid namespace fails before any code is emitted.
15
+ def namespace
16
+ @validated_namespace ||= Generation.validate_namespace!(@namespace || "Edi")
17
+ end
18
+
19
+ # Filesystem/require-path root derived from the namespace ("Edi" -> "edi").
20
+ def namespace_path
21
+ underscore(namespace)
22
+ end
23
+
24
+ private
25
+
26
+ def version_module
27
+ VERSION_MODULES.fetch(release_code)
28
+ end
29
+
30
+ def validate_release!
31
+ return if VERSION_MODULES.key?(release_code)
32
+
33
+ raise ArgumentError, "Unsupported release code: #{release_code}. " \
34
+ "Supported: #{VERSION_MODULES.keys.join(', ')}"
35
+ end
36
+
37
+ def format_requirement(requirement)
38
+ stupidedi_req = REQUIREMENT_MAP.fetch(requirement) do
39
+ raise ArgumentError, "Unknown requirement: #{requirement}"
40
+ end
41
+ "r::#{stupidedi_req}"
42
+ end
43
+
44
+ def escape_string(str)
45
+ return '""' if str.nil? || str.empty?
46
+
47
+ escaped = str.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
48
+ "\"#{escaped}\""
49
+ end
50
+
51
+ def indent(level)
52
+ " " * level
53
+ end
54
+
55
+ # Convert release code to interchange version code.
56
+ # "004010" -> "00401", "005010" -> "00501"
57
+ def interchange_version_code
58
+ release_code[0..4]
59
+ end
60
+
61
+ # Minimal ActiveSupport-free String#underscore.
62
+ # "FortyTen" -> "forty_ten", "FourOhOne" -> "four_oh_one"
63
+ def underscore(str)
64
+ str.gsub("::", "/")
65
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
66
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
67
+ .downcase
68
+ end
69
+
70
+ # Minimal ActiveSupport-free String#camelize.
71
+ # "forty_ten" -> "FortyTen", "four_oh_one" -> "FourOhOne"
72
+ def camelize(str)
73
+ str.split(%r{[_/]}).reject(&:empty?).map { |part| part[0].upcase + part[1..] }.join
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Generates the four small per-version wrapper modules that alias common
7
+ # Stupidedi types into the version-scoped namespace so the other generated
8
+ # files can use short names.
9
+ class SupportModulesGenerator
10
+ include Support
11
+
12
+ SUPPORT_MODULES = %i[element_reqs element_types segment_reqs syntax_notes].freeze
13
+
14
+ def initialize(release, namespace: "Edi")
15
+ @release = release
16
+ @namespace = namespace
17
+ end
18
+
19
+ def generate_all
20
+ validate_release!
21
+ SUPPORT_MODULES.to_h { |mod| [mod, send(:"generate_#{mod}")] }
22
+ end
23
+
24
+ def output_paths
25
+ base_path = "#{namespace_path}/#{underscore(version_module)}"
26
+ SUPPORT_MODULES.to_h { |mod| [mod, "#{base_path}/#{mod}.rb"] }
27
+ end
28
+
29
+ def generate_element_reqs
30
+ <<~RUBY
31
+ # frozen_string_literal: true
32
+
33
+ require "tediparse"
34
+
35
+ module #{namespace}
36
+ module #{version_module}
37
+ module ElementReqs
38
+ Mandatory = Stupidedi::Versions::Common::ElementReqs::Mandatory
39
+ Optional = Stupidedi::Versions::Common::ElementReqs::Optional
40
+ Relational = Stupidedi::Versions::Common::ElementReqs::Relational
41
+ end
42
+ end
43
+ end
44
+ RUBY
45
+ end
46
+
47
+ def generate_element_types
48
+ <<~RUBY
49
+ # frozen_string_literal: true
50
+
51
+ require "tediparse"
52
+
53
+ module #{namespace}
54
+ module #{version_module}
55
+ module ElementTypes
56
+ DT = Stupidedi::Versions::Common::ElementTypes::DT
57
+ DateVal = Stupidedi::Versions::Common::ElementTypes::DateVal
58
+
59
+ R = Stupidedi::Versions::Common::ElementTypes::R
60
+ FloatVal = Stupidedi::Versions::Common::ElementTypes::FloatVal
61
+
62
+ ID = Stupidedi::Versions::Common::ElementTypes::ID
63
+ IdentifierVal = Stupidedi::Versions::Common::ElementTypes::IdentifierVal
64
+
65
+ Nn = Stupidedi::Versions::Common::ElementTypes::Nn
66
+ FixnumVal = Stupidedi::Versions::Common::ElementTypes::FixnumVal
67
+
68
+ AN = Stupidedi::Versions::Common::ElementTypes::AN
69
+ StringVal = Stupidedi::Versions::Common::ElementTypes::StringVal
70
+
71
+ TM = Stupidedi::Versions::Common::ElementTypes::TM
72
+ TimeVal = Stupidedi::Versions::Common::ElementTypes::TimeVal
73
+
74
+ SimpleElementDef = Stupidedi::Versions::Common::ElementTypes::SimpleElementDef
75
+ end
76
+ end
77
+ end
78
+ RUBY
79
+ end
80
+
81
+ def generate_segment_reqs
82
+ <<~RUBY
83
+ # frozen_string_literal: true
84
+
85
+ require "tediparse"
86
+
87
+ module #{namespace}
88
+ module #{version_module}
89
+ module SegmentReqs
90
+ Mandatory = Stupidedi::Versions::Common::SegmentReqs::Mandatory
91
+ Optional = Stupidedi::Versions::Common::SegmentReqs::Optional
92
+ end
93
+ end
94
+ end
95
+ RUBY
96
+ end
97
+
98
+ def generate_syntax_notes
99
+ <<~RUBY
100
+ # frozen_string_literal: true
101
+
102
+ module #{namespace}
103
+ module #{version_module}
104
+ module SyntaxNotes
105
+ P = Stupidedi::Versions::Common::SyntaxNotes::P
106
+ R = Stupidedi::Versions::Common::SyntaxNotes::R
107
+ E = Stupidedi::Versions::Common::SyntaxNotes::E
108
+ C = Stupidedi::Versions::Common::SyntaxNotes::C
109
+ L = Stupidedi::Versions::Common::SyntaxNotes::L
110
+ end
111
+ end
112
+ end
113
+ RUBY
114
+ end
115
+
116
+ private
117
+
118
+ attr_reader :release
119
+
120
+ def release_code
121
+ release.code
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stupidedi
4
+ module Schema
5
+ module Generation
6
+ # Maps 6-digit ASC X12 release codes to the Ruby module names used by the
7
+ # generated grammar (e.g. "005010" => "FiftyTen"). This is the canonical
8
+ # list of releases the generator supports.
9
+ VERSION_MODULES = {
10
+ "003060" => "ThirtySixty",
11
+ "004010" => "FortyTen",
12
+ "004060" => "FortySixty",
13
+ "005010" => "FiftyTen",
14
+ "006010" => "SixtyTen",
15
+ "007010" => "SeventyTen",
16
+ "008010" => "EightyTen"
17
+ }.freeze
18
+
19
+ # Maps X12 requirement designators to Stupidedi ElementReq names.
20
+ # Note: Conditional -> Relational and NotUsed -> Optional are deliberate.
21
+ REQUIREMENT_MAP = {
22
+ "Mandatory" => "Mandatory",
23
+ "Optional" => "Optional",
24
+ "Conditional" => "Relational",
25
+ "NotUsed" => "Optional"
26
+ }.freeze
27
+
28
+ # A valid output namespace is a single Ruby constant name (e.g. "Edi").
29
+ # Nested namespaces ("Acme::Grammars") are not supported: the generated
30
+ # code uses `module <namespace>` directly, which only works for a single
31
+ # top-level constant.
32
+ NAMESPACE_FORMAT = /\A[A-Z]\w*\z/
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generation/version_modules"
4
+
5
+ module Stupidedi
6
+ module Schema
7
+ # Grammar generation: turns ASC X12 Table Data flat files into Stupidedi
8
+ # grammar definition source files.
9
+ #
10
+ # The gem ships the generation *machine* only. The X12 Table Data it reads is
11
+ # licensed X12 IP and is supplied by the consumer; the generated grammar
12
+ # files are a derivative of that IP and belong to the consumer. Nothing here
13
+ # bundles X12 content.
14
+ #
15
+ # Stupidedi::Schema::Generation.run(
16
+ # table_data: "vendor/x12/table_data/005010",
17
+ # release: "005010",
18
+ # out: "lib"
19
+ # )
20
+ module Generation
21
+ autoload :Support, "stupidedi/schema/generation/support"
22
+ autoload :Models, "stupidedi/schema/generation/models"
23
+ autoload :FlatFileReader, "stupidedi/schema/generation/flat_file_reader"
24
+ autoload :SupportModulesGenerator, "stupidedi/schema/generation/support_modules_generator"
25
+ autoload :ElementGenerator, "stupidedi/schema/generation/element_generator"
26
+ autoload :SegmentGenerator, "stupidedi/schema/generation/segment_generator"
27
+ autoload :DefinitionGenerator, "stupidedi/schema/generation/definition_generator"
28
+ autoload :FunctionalGroupGenerator, "stupidedi/schema/generation/functional_group_generator"
29
+ autoload :InterchangeGenerator, "stupidedi/schema/generation/interchange_generator"
30
+ autoload :ModuleLoaderGenerator, "stupidedi/schema/generation/module_loader_generator"
31
+ autoload :RegistrationGenerator, "stupidedi/schema/generation/registration_generator"
32
+ autoload :MasterLoaderGenerator, "stupidedi/schema/generation/master_loader_generator"
33
+ autoload :Runner, "stupidedi/schema/generation/runner"
34
+
35
+ # Validates the output namespace is a single Ruby constant name and
36
+ # returns it. Raises ArgumentError otherwise. See NAMESPACE_FORMAT.
37
+ def self.validate_namespace!(namespace)
38
+ return namespace if namespace.to_s =~ NAMESPACE_FORMAT
39
+
40
+ raise ArgumentError,
41
+ %(namespace must be a single Ruby constant name like "Edi" ) +
42
+ %(got #{namespace.inspect}; nested namespaces ("::") are not supported)
43
+ end
44
+
45
+ # Generate the full grammar for one release. See Runner for options.
46
+ # master_loader: true also emits a single-require entry file (e.g. edi.rb).
47
+ def self.run(table_data:, release:, out:, namespace: "Edi", write: true, master_loader: false, logger: nil)
48
+ Runner.new(
49
+ table_data: table_data,
50
+ release: release,
51
+ out: out,
52
+ namespace: namespace,
53
+ write: write,
54
+ master_loader: master_loader,
55
+ logger: logger
56
+ ).run
57
+ end
58
+
59
+ # Rebuild the whole-tree aggregation artifacts by scanning the live output
60
+ # tree: stupidedi_registration.rb always, and the master loader too when one
61
+ # is present (so the rebuilt registration's constants are actually required;
62
+ # otherwise require "edi" would resolve to a NameError). Useful after adding
63
+ # or removing releases by other means; `run` already keeps these current for
64
+ # the releases it generates. Returns the paths (re)generated.
65
+ def self.register(out:, namespace: "Edi", write: true, logger: nil)
66
+ validate_namespace!(namespace)
67
+
68
+ paths = [write_artifact(RegistrationGenerator.new(roots: [out], namespace: namespace), out, write, logger)]
69
+
70
+ master_loader = MasterLoaderGenerator.new(roots: [out], namespace: namespace)
71
+ if File.exist?(File.join(out, master_loader.output_path))
72
+ paths << write_artifact(master_loader, out, write, logger)
73
+ end
74
+
75
+ paths
76
+ end
77
+
78
+ def self.write_artifact(generator, out, write, logger)
79
+ content = generator.generate
80
+ path = File.join(out, generator.output_path)
81
+
82
+ if write
83
+ require "fileutils"
84
+ FileUtils.mkdir_p(File.dirname(path))
85
+ File.write(path, content)
86
+ logger&.call("wrote #{path}")
87
+ end
88
+
89
+ path
90
+ end
91
+ private_class_method :write_artifact
92
+ end
93
+ end
94
+ end