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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +319 -0
- data/LICENSE +26 -0
- data/README.md +308 -0
- data/Rakefile +33 -0
- data/bin/tediparse +102 -0
- data/doc/Defining.md +97 -0
- data/doc/Generating-Grammars.md +241 -0
- data/doc/Generating.md +332 -0
- data/doc/Navigating.md +670 -0
- data/doc/Parsing.md +133 -0
- data/doc/README.md +51 -0
- data/doc/Serializing.md +100 -0
- data/doc/Tokenizing.md +136 -0
- data/doc/Validating.md +140 -0
- data/lib/ruby/array.rb +226 -0
- data/lib/ruby/blank.rb +52 -0
- data/lib/ruby/exception.rb +14 -0
- data/lib/ruby/hash.rb +14 -0
- data/lib/ruby/module.rb +60 -0
- data/lib/ruby/object.rb +56 -0
- data/lib/ruby/string.rb +89 -0
- data/lib/ruby/to_d.rb +82 -0
- data/lib/ruby/to_date.rb +26 -0
- data/lib/ruby/to_time.rb +21 -0
- data/lib/ruby/try.rb +46 -0
- data/lib/stupidedi/builder.rb +6 -0
- data/lib/stupidedi/color.rb +94 -0
- data/lib/stupidedi/config/code_list_config.rb +45 -0
- data/lib/stupidedi/config/functional_group_config.rb +72 -0
- data/lib/stupidedi/config/interchange_config.rb +86 -0
- data/lib/stupidedi/config/transaction_set_config.rb +73 -0
- data/lib/stupidedi/config.rb +100 -0
- data/lib/stupidedi/either.rb +286 -0
- data/lib/stupidedi/exceptions/invalid_element_error.rb +7 -0
- data/lib/stupidedi/exceptions/invalid_schema_error.rb +7 -0
- data/lib/stupidedi/exceptions/missing_grammar_error.rb +50 -0
- data/lib/stupidedi/exceptions/output_error.rb +7 -0
- data/lib/stupidedi/exceptions/parse_error.rb +7 -0
- data/lib/stupidedi/exceptions/stupidedi_error.rb +7 -0
- data/lib/stupidedi/exceptions/tokenize_error.rb +7 -0
- data/lib/stupidedi/exceptions/zipper_error.rb +7 -0
- data/lib/stupidedi/exceptions.rb +13 -0
- data/lib/stupidedi/inspect.rb +26 -0
- data/lib/stupidedi/interchanges/element_types/separator_val.rb +79 -0
- data/lib/stupidedi/interchanges/element_types/special_val.rb +48 -0
- data/lib/stupidedi/interchanges/element_types.rb +11 -0
- data/lib/stupidedi/interchanges.rb +21 -0
- data/lib/stupidedi/parser/builder_dsl.rb +292 -0
- data/lib/stupidedi/parser/constraint_table.rb +566 -0
- data/lib/stupidedi/parser/generation.rb +147 -0
- data/lib/stupidedi/parser/identifier_stack.rb +248 -0
- data/lib/stupidedi/parser/instruction.rb +112 -0
- data/lib/stupidedi/parser/instruction_table.rb +222 -0
- data/lib/stupidedi/parser/navigation.rb +786 -0
- data/lib/stupidedi/parser/state_machine.rb +62 -0
- data/lib/stupidedi/parser/states/abstract_state.rb +385 -0
- data/lib/stupidedi/parser/states/failure_state.rb +69 -0
- data/lib/stupidedi/parser/states/functional_group_state.rb +101 -0
- data/lib/stupidedi/parser/states/initial_state.rb +62 -0
- data/lib/stupidedi/parser/states/interchange_state.rb +96 -0
- data/lib/stupidedi/parser/states/loop_state.rb +74 -0
- data/lib/stupidedi/parser/states/table_state.rb +90 -0
- data/lib/stupidedi/parser/states/transaction_set_state.rb +120 -0
- data/lib/stupidedi/parser/states/transmission_state.rb +59 -0
- data/lib/stupidedi/parser/tokenization.rb +195 -0
- data/lib/stupidedi/parser.rb +32 -0
- data/lib/stupidedi/reader/input/abstract_input.rb +137 -0
- data/lib/stupidedi/reader/input/delegated_input.rb +112 -0
- data/lib/stupidedi/reader/input/file_input.rb +157 -0
- data/lib/stupidedi/reader/input.rb +31 -0
- data/lib/stupidedi/reader/position.rb +78 -0
- data/lib/stupidedi/reader/result.rb +172 -0
- data/lib/stupidedi/reader/segment_dict.rb +176 -0
- data/lib/stupidedi/reader/separators.rb +90 -0
- data/lib/stupidedi/reader/stream_reader.rb +173 -0
- data/lib/stupidedi/reader/token_reader.rb +465 -0
- data/lib/stupidedi/reader/tokens/component_element_tok.rb +71 -0
- data/lib/stupidedi/reader/tokens/composite_element_tok.rb +85 -0
- data/lib/stupidedi/reader/tokens/repeated_element_tok.rb +74 -0
- data/lib/stupidedi/reader/tokens/segment_tok.rb +74 -0
- data/lib/stupidedi/reader/tokens/simple_element_tok.rb +76 -0
- data/lib/stupidedi/reader.rb +121 -0
- data/lib/stupidedi/schema/abstract_def.rb +76 -0
- data/lib/stupidedi/schema/abstract_element_def.rb +35 -0
- data/lib/stupidedi/schema/abstract_element_use.rb +47 -0
- data/lib/stupidedi/schema/abstract_use.rb +79 -0
- data/lib/stupidedi/schema/code_list.rb +99 -0
- data/lib/stupidedi/schema/component_element_use.rb +76 -0
- data/lib/stupidedi/schema/composite_element_def.rb +103 -0
- data/lib/stupidedi/schema/composite_element_use.rb +78 -0
- data/lib/stupidedi/schema/element_req.rb +57 -0
- data/lib/stupidedi/schema/functional_group_def.rb +124 -0
- data/lib/stupidedi/schema/generation/definition_generator.rb +139 -0
- data/lib/stupidedi/schema/generation/element_generator.rb +221 -0
- data/lib/stupidedi/schema/generation/flat_file_reader.rb +551 -0
- data/lib/stupidedi/schema/generation/functional_group_generator.rb +64 -0
- data/lib/stupidedi/schema/generation/interchange_generator.rb +145 -0
- data/lib/stupidedi/schema/generation/master_loader_generator.rb +121 -0
- data/lib/stupidedi/schema/generation/models.rb +85 -0
- data/lib/stupidedi/schema/generation/module_loader_generator.rb +64 -0
- data/lib/stupidedi/schema/generation/registration_generator.rb +230 -0
- data/lib/stupidedi/schema/generation/runner.rb +161 -0
- data/lib/stupidedi/schema/generation/segment_generator.rb +130 -0
- data/lib/stupidedi/schema/generation/support.rb +78 -0
- data/lib/stupidedi/schema/generation/support_modules_generator.rb +126 -0
- data/lib/stupidedi/schema/generation/version_modules.rb +35 -0
- data/lib/stupidedi/schema/generation.rb +94 -0
- data/lib/stupidedi/schema/interchange_def.rb +103 -0
- data/lib/stupidedi/schema/loop_def.rb +156 -0
- data/lib/stupidedi/schema/repeat_count.rb +86 -0
- data/lib/stupidedi/schema/segment_def.rb +122 -0
- data/lib/stupidedi/schema/segment_req.rb +46 -0
- data/lib/stupidedi/schema/segment_use.rb +99 -0
- data/lib/stupidedi/schema/simple_element_def.rb +51 -0
- data/lib/stupidedi/schema/simple_element_use.rb +83 -0
- data/lib/stupidedi/schema/syntax_note.rb +52 -0
- data/lib/stupidedi/schema/table_def.rb +178 -0
- data/lib/stupidedi/schema/transaction_set_def.rb +125 -0
- data/lib/stupidedi/schema.rb +30 -0
- data/lib/stupidedi/sets.rb +42 -0
- data/lib/stupidedi/transaction_sets/builder/dsl.rb +192 -0
- data/lib/stupidedi/transaction_sets/builder.rb +188 -0
- data/lib/stupidedi/transaction_sets/common/implementations/element_reqs.rb +37 -0
- data/lib/stupidedi/transaction_sets/common/implementations/segment_reqs.rb +31 -0
- data/lib/stupidedi/transaction_sets/common/implementations.rb +11 -0
- data/lib/stupidedi/transaction_sets/common.rb +8 -0
- data/lib/stupidedi/transaction_sets/validation/ambiguity.rb +395 -0
- data/lib/stupidedi/transaction_sets/validation/implementation.rb +12 -0
- data/lib/stupidedi/transaction_sets/validation.rb +9 -0
- data/lib/stupidedi/transaction_sets.rb +25 -0
- data/lib/stupidedi/values/abstract_element_val.rb +19 -0
- data/lib/stupidedi/values/abstract_val.rb +133 -0
- data/lib/stupidedi/values/composite_element_val.rb +102 -0
- data/lib/stupidedi/values/functional_group_val.rb +105 -0
- data/lib/stupidedi/values/interchange_val.rb +102 -0
- data/lib/stupidedi/values/invalid_envelope_val.rb +61 -0
- data/lib/stupidedi/values/invalid_segment_val.rb +89 -0
- data/lib/stupidedi/values/loop_val.rb +73 -0
- data/lib/stupidedi/values/repeated_element_val.rb +113 -0
- data/lib/stupidedi/values/segment_val.rb +105 -0
- data/lib/stupidedi/values/segment_val_group.rb +20 -0
- data/lib/stupidedi/values/simple_element_val.rb +80 -0
- data/lib/stupidedi/values/table_val.rb +69 -0
- data/lib/stupidedi/values/transaction_set_val.rb +69 -0
- data/lib/stupidedi/values/transmission_val.rb +56 -0
- data/lib/stupidedi/values.rb +22 -0
- data/lib/stupidedi/version.rb +4 -0
- data/lib/stupidedi/versions/common/element_reqs.rb +13 -0
- data/lib/stupidedi/versions/common/element_types/an.rb +386 -0
- data/lib/stupidedi/versions/common/element_types/dt.rb +572 -0
- data/lib/stupidedi/versions/common/element_types/id.rb +304 -0
- data/lib/stupidedi/versions/common/element_types/nn.rb +312 -0
- data/lib/stupidedi/versions/common/element_types/operators.rb +128 -0
- data/lib/stupidedi/versions/common/element_types/r.rb +342 -0
- data/lib/stupidedi/versions/common/element_types/simple_element.rb +73 -0
- data/lib/stupidedi/versions/common/element_types/tm.rb +347 -0
- data/lib/stupidedi/versions/common/element_types.rb +29 -0
- data/lib/stupidedi/versions/common/segment_reqs.rb +15 -0
- data/lib/stupidedi/versions/common/syntax_notes.rb +172 -0
- data/lib/stupidedi/versions/common.rb +11 -0
- data/lib/stupidedi/versions.rb +25 -0
- data/lib/stupidedi/writer/claredi.rb +178 -0
- data/lib/stupidedi/writer/default.rb +119 -0
- data/lib/stupidedi/writer.rb +7 -0
- data/lib/stupidedi/zipper/abstract_cursor.rb +351 -0
- data/lib/stupidedi/zipper/dangling_cursor.rb +103 -0
- data/lib/stupidedi/zipper/edited_cursor.rb +157 -0
- data/lib/stupidedi/zipper/memoized_cursor.rb +133 -0
- data/lib/stupidedi/zipper/path.rb +132 -0
- data/lib/stupidedi/zipper/root_cursor.rb +120 -0
- data/lib/stupidedi/zipper/stack_cursor.rb +107 -0
- data/lib/stupidedi/zipper.rb +45 -0
- data/lib/stupidedi.rb +69 -0
- data/lib/tediparse.rb +1 -0
- 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
|