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,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
|