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
data/Rakefile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
abspath = Pathname.new(File.dirname(__FILE__)).expand_path
|
|
4
|
+
relpath = abspath.relative_path_from(Pathname.pwd)
|
|
5
|
+
|
|
6
|
+
task :default => :spec
|
|
7
|
+
|
|
8
|
+
require "rspec/core/rake_task"
|
|
9
|
+
RSpec::Core::RakeTask.new do |t|
|
|
10
|
+
t.verbose = false
|
|
11
|
+
t.rspec_opts = "-w -rspec_helper"
|
|
12
|
+
|
|
13
|
+
if ENV.include?("CI")
|
|
14
|
+
t.rspec_opts += " --format progress"
|
|
15
|
+
else
|
|
16
|
+
t.rspec_opts += " --format documentation"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Note options are loaded from .yardopts
|
|
21
|
+
require "yard"
|
|
22
|
+
YARD::Rake::YardocTask.new(:yard => :clobber_yard)
|
|
23
|
+
task :clobber_yard do
|
|
24
|
+
# Call FileUtils directly rather than Rake's DSL shim: on Ruby 3.x the
|
|
25
|
+
# rake-12.3 FileUtilsExt wrapper forwards its verbose/noop flags as a
|
|
26
|
+
# positional argument, which the keyword-only FileUtils.rm_rf rejects.
|
|
27
|
+
FileUtils.rm_rf "#{relpath}/build/generated/doc"
|
|
28
|
+
FileUtils.mkdir_p "#{relpath}/build/generated/doc/images"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
task :console do
|
|
32
|
+
exec(*%w(irb -I lib -r stupidedi))
|
|
33
|
+
end
|
data/bin/tediparse
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
7
|
+
require "stupidedi"
|
|
8
|
+
|
|
9
|
+
options = {
|
|
10
|
+
namespace: "Edi",
|
|
11
|
+
out: ".",
|
|
12
|
+
write: true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
parser = OptionParser.new do |opts|
|
|
16
|
+
opts.banner = <<~BANNER
|
|
17
|
+
tediparse - generate Stupidedi grammar definitions from ASC X12 Table Data.
|
|
18
|
+
|
|
19
|
+
The X12 Table Data is licensed X12 IP that you supply; the generated grammar
|
|
20
|
+
files are a derivative you own. This tool ships no X12 content.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
tediparse generate --release CODE --table-data DIR [--out DIR] [options]
|
|
24
|
+
tediparse register --out DIR [--namespace NAME]
|
|
25
|
+
|
|
26
|
+
`register` rebuilds the whole-tree artifacts (stupidedi_registration.rb, and
|
|
27
|
+
the master loader if present) by scanning an existing generated tree.
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
BANNER
|
|
31
|
+
|
|
32
|
+
opts.on("-r", "--release CODE", "X12 release code (e.g. 005010)") { |v| options[:release] = v }
|
|
33
|
+
opts.on("-t", "--table-data DIR", "Directory of ASC X12 Table Data .TXT files") { |v| options[:table_data] = v }
|
|
34
|
+
opts.on("-o", "--out DIR", "Output directory (default: current dir)") { |v| options[:out] = v }
|
|
35
|
+
opts.on("-n", "--namespace NAME", "Root Ruby module for generated code (default: Edi)") { |v| options[:namespace] = v }
|
|
36
|
+
opts.on("--master-loader", "Also emit a single-require entry file (e.g. edi.rb)") { options[:master_loader] = true }
|
|
37
|
+
opts.on("--dry-run", "Preview only; write nothing") { options[:write] = false }
|
|
38
|
+
opts.on("-h", "--help", "Show this help") do
|
|
39
|
+
puts opts
|
|
40
|
+
exit
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
command = ARGV.shift
|
|
45
|
+
parser.parse!(ARGV)
|
|
46
|
+
|
|
47
|
+
case command
|
|
48
|
+
when "generate"
|
|
49
|
+
missing = %i[release table_data].reject { |k| options[k] }
|
|
50
|
+
unless missing.empty?
|
|
51
|
+
warn "Missing required option(s): #{missing.map { |k| "--#{k.to_s.tr('_', '-')}" }.join(', ')}"
|
|
52
|
+
warn parser.help
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless Dir.exist?(options[:table_data])
|
|
57
|
+
warn "Table data directory not found: #{options[:table_data]}"
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
results = Stupidedi::Schema::Generation.run(
|
|
63
|
+
table_data: options[:table_data],
|
|
64
|
+
release: options[:release],
|
|
65
|
+
out: options[:out],
|
|
66
|
+
namespace: options[:namespace],
|
|
67
|
+
write: options[:write],
|
|
68
|
+
master_loader: options.fetch(:master_loader, false),
|
|
69
|
+
logger: ->(msg) { puts msg }
|
|
70
|
+
)
|
|
71
|
+
rescue ArgumentError => e
|
|
72
|
+
warn "Error: #{e.message}"
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts "Done: #{results.size} files (#{options[:write] ? 'written' : 'preview'})."
|
|
77
|
+
when "register"
|
|
78
|
+
unless options[:out] && Dir.exist?(options[:out])
|
|
79
|
+
warn "register requires --out DIR pointing at an existing generated tree"
|
|
80
|
+
exit 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
paths = Stupidedi::Schema::Generation.register(
|
|
85
|
+
out: options[:out],
|
|
86
|
+
namespace: options[:namespace],
|
|
87
|
+
write: options[:write],
|
|
88
|
+
logger: ->(msg) { puts msg }
|
|
89
|
+
)
|
|
90
|
+
rescue ArgumentError => e
|
|
91
|
+
warn "Error: #{e.message}"
|
|
92
|
+
exit 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
puts "Done: rebuilt #{paths.size} file(s)#{options[:write] ? '' : ' (preview)'}."
|
|
96
|
+
when nil, "help", "-h", "--help"
|
|
97
|
+
puts parser.help
|
|
98
|
+
else
|
|
99
|
+
warn "Unknown command: #{command}"
|
|
100
|
+
warn parser.help
|
|
101
|
+
exit 1
|
|
102
|
+
end
|
data/doc/Defining.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
> This page covers transcribing a grammar **by hand**. If you license the ASC
|
|
2
|
+
> X12 Table Data flat files, tediparse can generate the whole definition tree
|
|
3
|
+
> for a release instead — see
|
|
4
|
+
> [Generating-Grammars.md](Generating-Grammars.md). Hand-authoring as below is
|
|
5
|
+
> still the right approach for a small custom grammar or for understanding what
|
|
6
|
+
> the generator emits.
|
|
7
|
+
|
|
8
|
+
Be sure you start with the right documentation, which must be purchased from [ASC
|
|
9
|
+
X12](http://x12.org/). Documentation from a trading partner might be usable in a
|
|
10
|
+
pinch, but is often missing details or has details relevant only to that
|
|
11
|
+
partner. If you are unsure, the cover page for the document should look like
|
|
12
|
+
this:
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## Tables
|
|
17
|
+
|
|
18
|
+
The overall structure of the grammar is given in section 2.2 of the
|
|
19
|
+
documentation (be sure it is marked `IMPLEMENTATION` and not `STANDARD`). The
|
|
20
|
+
top-level tables can be written as `TableDef.header`, `TableDef.detail`, and
|
|
21
|
+
`TableDef.summary`.
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
> Note for some reason, some grammars show the `SE` segment at the end of a
|
|
26
|
+
> detail table, rather than in its own summary table. The `SE` segment is the
|
|
27
|
+
> end of the document, so when you code up the grammar, you should create a
|
|
28
|
+
> `TableDef.summary` to contain the `SE` segment (see 837P). This is because
|
|
29
|
+
> a detail table can be repeated, while a summary table cannot, and the `SE`
|
|
30
|
+
> segment should only occur once in a document.
|
|
31
|
+
|
|
32
|
+
## Loops
|
|
33
|
+
|
|
34
|
+
Loops are groups of segments which have a particular starting segment that
|
|
35
|
+
indicates the start of the group, followed by other segments. Loops may be
|
|
36
|
+
nested, and each loop may be repeated a specified number of times. You can
|
|
37
|
+
translate each loop in the documentation to a `LoopDef.build(name, repeat_count,
|
|
38
|
+
*children)` where children are either segments or child loops.
|
|
39
|
+
|
|
40
|
+
In the example above, the `"2000C PATIENT HIERARCHY"` loop has a repeat count
|
|
41
|
+
of `RepeatCount.unbounded`, child segments `HL` and `PAT`, and many child loops
|
|
42
|
+
beginning with `"2000CA PATIENT NAME"`. Since `HL` is the first segment in the
|
|
43
|
+
loop, when the parser reads an `HL` in the right context, it will then create
|
|
44
|
+
the corresponding loop in the parse tree.
|
|
45
|
+
|
|
46
|
+
## Segments
|
|
47
|
+
|
|
48
|
+
Segments are sort of like a `struct` in the C programming language, where the
|
|
49
|
+
fields are called elements. Unlike structs, each time a segment occurs in the
|
|
50
|
+
grammar, it can be given a different name, different flags to indicate which
|
|
51
|
+
fields are required or optional, different set of allowed values in each field,
|
|
52
|
+
and a different number of allowed repeats.
|
|
53
|
+
|
|
54
|
+
One of the most commonly used segments is `NM1`, which is generally some kind of
|
|
55
|
+
name. In the above example, you an `NM1 Patient Name`, but there are many others
|
|
56
|
+
in the grammar for different people.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
The important parts of the image above have probably already been transcribed in
|
|
61
|
+
an existing `SegmentDef` (unless you find a segment that hasn't been defined), which
|
|
62
|
+
are the same across all grammars (all the X12 attributes). The "loop", "usage", and
|
|
63
|
+
"repeat count" were described earlier, but are again shown here.
|
|
64
|
+
|
|
65
|
+
Segments can be described using `BuilderDsl.Segment(offset, id, name,
|
|
66
|
+
usage, repeat_count, *elements)`. The first argument comes from the `Pos. #`
|
|
67
|
+
column in the table diagram above. For instance, the `NM1 Patient Name` segment
|
|
68
|
+
has an offset of 150. The usage argument is denoted in a few places: on the
|
|
69
|
+
above diagrams, it is the labeled `Usage`; this indicates if the segment is
|
|
70
|
+
required or optional, or if its presence depends on other conditions.
|
|
71
|
+
|
|
72
|
+
## Elements
|
|
73
|
+
|
|
74
|
+
Elements are (apart from composite elements), some kind of atomic data type,
|
|
75
|
+
like a string, number, date, etc. Each place a segment is specified in the
|
|
76
|
+
grammar, its element properties are also specified. Beware that each place that
|
|
77
|
+
a segment appears can have different details about its elements, per-occurence.
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
This diagram indicates some of the properties of each element, in this
|
|
82
|
+
particular instance of the `NM1` segemnt. The bold border around an element
|
|
83
|
+
indicates that it is `Required`, while strike-through text indicates a
|
|
84
|
+
`NotUsed`. If you encounter a segment that hasn't already been defined, some of
|
|
85
|
+
the other properties needed to define the segment are also included on the
|
|
86
|
+
diagram, but we'll ignore those for now.
|
|
87
|
+
|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
The third section of each element detail gives a name to each element,
|
|
91
|
+
usually in the gray section labeled `IMPLEMENTATION NAME`, and also specifies
|
|
92
|
+
its usage, allowed values, and minimum and maximum lengths. The usage indicator
|
|
93
|
+
is in the first column, name and allowed values in the fourth. Minimum and
|
|
94
|
+
maximum length, and number of decimal places are given in the last column, but
|
|
95
|
+
these rarely need to be specified as they are usually the same as the default
|
|
96
|
+
declared in the `ElementDef`.
|
|
97
|
+
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Generating Grammars from X12 Table Data
|
|
2
|
+
=======================================
|
|
3
|
+
|
|
4
|
+
> **"Generating" means two different things in this project.** This page is
|
|
5
|
+
> about *generating the grammar* — turning the ASC X12 standard into the Ruby
|
|
6
|
+
> definition files the engine walks. [Generating.md](Generating.md) is about
|
|
7
|
+
> *generating X12 documents* — emitting EDI with the writer once you already
|
|
8
|
+
> have a grammar. If you want to produce an 837 or a 204, you want the other
|
|
9
|
+
> page.
|
|
10
|
+
|
|
11
|
+
There are two ways to get a grammar that the engine can use:
|
|
12
|
+
|
|
13
|
+
1. **Author it by hand** — transcribe a purchased implementation guide into
|
|
14
|
+
`SegmentDef` / `LoopDef` / `TransactionSetDef` values. See
|
|
15
|
+
[Defining.md](Defining.md). This is fine for a handful of segments, but a
|
|
16
|
+
full transaction set is hundreds of definitions.
|
|
17
|
+
2. **Generate it from ASC X12 Table Data** — feed the official flat-file
|
|
18
|
+
distribution to the generator and it emits the whole definition tree for a
|
|
19
|
+
release. This page covers that path.
|
|
20
|
+
|
|
21
|
+
Both produce the same thing: Ruby source that builds `Stupidedi::Schema`
|
|
22
|
+
objects, registered against a `Stupidedi::Config`. Generation is just
|
|
23
|
+
automation over the hand-authoring you would otherwise do.
|
|
24
|
+
|
|
25
|
+
The IP boundary
|
|
26
|
+
---------------
|
|
27
|
+
|
|
28
|
+
**The gem ships the generation *machine* only.** The X12 Table Data it reads
|
|
29
|
+
is licensed X12 IP that you supply, and the grammar files it writes are a
|
|
30
|
+
derivative of that IP that belong to you. Neither the input nor the output
|
|
31
|
+
ships with tediparse.
|
|
32
|
+
|
|
33
|
+
The committed fixture under `spec/support/generation/table_data/` is a small
|
|
34
|
+
*synthetic* grammar in the same flat-file format — fabricated structure used
|
|
35
|
+
to test the generator — not real X12 content.
|
|
36
|
+
|
|
37
|
+
Input: the ASC X12 Table Data distribution
|
|
38
|
+
-------------------------------------------
|
|
39
|
+
|
|
40
|
+
To obtain the Table Data you need an **ASC X12 license**; with one in hand you
|
|
41
|
+
can download the distribution from <https://ecommerce.x12.org/downloads>.
|
|
42
|
+
|
|
43
|
+
The Table Data is a set of CSV-shaped `.TXT` files, one header/detail pair per
|
|
44
|
+
kind of definition, plus a free-form file:
|
|
45
|
+
|
|
46
|
+
| File | Holds |
|
|
47
|
+
| -------------- | -------------------------------------------------- |
|
|
48
|
+
| `ELEHEAD` / `ELEDETL` | simple data elements (type, lengths, code lists) |
|
|
49
|
+
| `COMHEAD` / `COMDETL` | composite elements and their component uses |
|
|
50
|
+
| `SEGHEAD` / `SEGDETL` | segments and their ordered element uses |
|
|
51
|
+
| `SETHEAD` / `SETDETL` | transaction sets and their table/loop/segment structure |
|
|
52
|
+
| `FREEFORM` | longer prose (segment/element notes) |
|
|
53
|
+
|
|
54
|
+
Point `--table-data` at the directory holding these files for one release.
|
|
55
|
+
|
|
56
|
+
The CLI
|
|
57
|
+
-------
|
|
58
|
+
|
|
59
|
+
The gem ships one executable, `bin/tediparse`:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# Generate the full grammar tree for a release into ./lib
|
|
63
|
+
tediparse generate --release 005010 \
|
|
64
|
+
--table-data vendor/x12/table_data/005010 \
|
|
65
|
+
--out lib
|
|
66
|
+
|
|
67
|
+
# Preview without writing anything
|
|
68
|
+
tediparse generate --release 005010 \
|
|
69
|
+
--table-data vendor/x12/table_data/005010 \
|
|
70
|
+
--out lib --dry-run
|
|
71
|
+
|
|
72
|
+
# Emit into a custom root module (default is Edi)
|
|
73
|
+
tediparse generate --release 005010 \
|
|
74
|
+
--table-data vendor/x12/table_data/005010 \
|
|
75
|
+
--out lib --namespace Acme
|
|
76
|
+
|
|
77
|
+
# Also emit a single-require entry file (for non-autoloader setups)
|
|
78
|
+
tediparse generate --release 005010 \
|
|
79
|
+
--table-data vendor/x12/table_data/005010 \
|
|
80
|
+
--out lib --master-loader
|
|
81
|
+
|
|
82
|
+
# Rebuild the whole-tree aggregation files for an existing tree
|
|
83
|
+
tediparse register --out lib
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Supported releases: `003060`, `004010`, `004060`, `005010`, `006010`,
|
|
87
|
+
`007010`, `008010`. An unsupported release code is rejected before anything
|
|
88
|
+
is read.
|
|
89
|
+
|
|
90
|
+
From Ruby
|
|
91
|
+
---------
|
|
92
|
+
|
|
93
|
+
The CLI is a thin wrapper over a facade you can call directly — useful from a
|
|
94
|
+
Rake task:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
Stupidedi::Schema::Generation.run(
|
|
98
|
+
table_data: "vendor/x12/table_data/005010",
|
|
99
|
+
release: "005010",
|
|
100
|
+
out: "lib",
|
|
101
|
+
namespace: "Edi", # root module for the emitted code (default: "Edi")
|
|
102
|
+
master_loader: false, # also emit a single-require entry file?
|
|
103
|
+
write: true, # false = dry run (returns results, writes nothing)
|
|
104
|
+
logger: ->(msg) { puts msg }
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Rebuild only the whole-tree artifacts (registration + master loader if present)
|
|
108
|
+
Stupidedi::Schema::Generation.register(out: "lib")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`run` returns one `Result` per file (`path`, `relative_path`, `content`,
|
|
112
|
+
`written`), so a dry run gives you the full generated source to diff or
|
|
113
|
+
inspect without touching disk. The whole release is staged in a tempdir first
|
|
114
|
+
and copied into `out` in one step only on success, so a failure partway
|
|
115
|
+
through never leaves a half-written tree.
|
|
116
|
+
|
|
117
|
+
The output tree
|
|
118
|
+
---------------
|
|
119
|
+
|
|
120
|
+
For `--release 005010 --namespace Edi --out lib`, the generator writes
|
|
121
|
+
(`005010` → the `FiftyTen` version module → the `fifty_ten` path):
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
lib/
|
|
125
|
+
edi/
|
|
126
|
+
fifty_ten/
|
|
127
|
+
element_reqs.rb # aliases Mandatory/Optional/Relational
|
|
128
|
+
element_types.rb # aliases the element type primitives
|
|
129
|
+
segment_reqs.rb # aliases the segment requirement enums
|
|
130
|
+
syntax_notes.rb # aliases the P/R/C/E/L syntax-note builders
|
|
131
|
+
element_defs.rb # every simple + composite element for the release
|
|
132
|
+
segment_defs.rb # every segment, incl. ISA/IEA/GS/GE/TA1
|
|
133
|
+
functional_group_def.rb # the GS/GE envelope
|
|
134
|
+
standards/
|
|
135
|
+
<transaction_set>.rb # one file per transaction set
|
|
136
|
+
fifty_ten.rb # version loader: requires this version's files
|
|
137
|
+
interchanges/
|
|
138
|
+
five_oh_one.rb # the ISA/IEA envelope (named by ISA version code)
|
|
139
|
+
stupidedi_registration.rb # wires every release in the tree onto a Config
|
|
140
|
+
edi.rb # master loader — only with --master-loader
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- The four small `*_reqs` / `*_types` / `syntax_notes` modules just alias
|
|
144
|
+
common `Stupidedi` types into the version namespace so the bulky files can
|
|
145
|
+
use short names.
|
|
146
|
+
- `element_defs.rb` and `segment_defs.rb` are the bulk of the grammar.
|
|
147
|
+
- `standards/*.rb` are the transaction-set definitions — these are what you
|
|
148
|
+
register per `(version, functional id, transaction code)`.
|
|
149
|
+
- `interchanges/` is **shared across releases** in the tree and named by the
|
|
150
|
+
ISA12 version code (`00501` → `five_oh_one`), not by the release module.
|
|
151
|
+
- `stupidedi_registration.rb` is the entry point that builds a populated
|
|
152
|
+
`Config` for everything in the tree (see "Using the grammar" below).
|
|
153
|
+
|
|
154
|
+
Loading the generated code
|
|
155
|
+
---------------------------
|
|
156
|
+
|
|
157
|
+
By default the tree is meant to be picked up by your application's autoloader
|
|
158
|
+
(Rails / Zeitwerk): the file/constant layout follows the `edi/fifty_ten/...`
|
|
159
|
+
convention, so `Edi::FiftyTen::SegmentDefs` resolves on demand.
|
|
160
|
+
|
|
161
|
+
If you are **not** using an autoloader, pass `--master-loader` (or
|
|
162
|
+
`master_loader: true`). That emits a single `edi.rb` (named after the
|
|
163
|
+
namespace) which `require`s the whole tree in dependency order, so a plain
|
|
164
|
+
`require "edi"` loads everything.
|
|
165
|
+
|
|
166
|
+
Multiple releases in one tree
|
|
167
|
+
-----------------------------
|
|
168
|
+
|
|
169
|
+
You can keep several releases side by side under one `out`. Generating an
|
|
170
|
+
additional release **preserves** the ones already present:
|
|
171
|
+
`stupidedi_registration.rb` (and the master loader, if any) are whole-tree
|
|
172
|
+
artifacts — every run rebuilds them to cover every release found in `out`, not
|
|
173
|
+
just the one you just generated. The regenerated release's own directory is
|
|
174
|
+
replaced wholesale (so a shrunk transaction-set list doesn't leave orphans),
|
|
175
|
+
while other releases and the shared `interchanges/` directory are left alone.
|
|
176
|
+
|
|
177
|
+
If you add or remove a release by other means and need to refresh just the
|
|
178
|
+
aggregation files, run `tediparse register --out lib` (or
|
|
179
|
+
`Stupidedi::Schema::Generation.register(out: "lib")`).
|
|
180
|
+
|
|
181
|
+
Using the generated grammar
|
|
182
|
+
----------------------------
|
|
183
|
+
|
|
184
|
+
The generated `stupidedi_registration.rb` does the wiring for you: it returns
|
|
185
|
+
a `Stupidedi::Config` with every release's interchange, functional group, and
|
|
186
|
+
transaction sets registered. Load the tree, get the config, and hand it to the
|
|
187
|
+
parser or writer exactly as you would a hand-authored one:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
require "edi" # or rely on your autoloader
|
|
191
|
+
|
|
192
|
+
config = Edi.config # populated Stupidedi::Config from stupidedi_registration.rb
|
|
193
|
+
|
|
194
|
+
parser = Stupidedi::Parser.build(config)
|
|
195
|
+
# ... parse, navigate (see Navigating.md)
|
|
196
|
+
|
|
197
|
+
b = Stupidedi::Parser::BuilderDsl.build(config)
|
|
198
|
+
# ... generate documents (see Generating.md)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
From here the generated grammar is indistinguishable from one you wrote by
|
|
202
|
+
hand. Continue with [Generating.md](Generating.md) to emit documents and
|
|
203
|
+
[Navigating.md](Navigating.md) to read and traverse them.
|
|
204
|
+
|
|
205
|
+
How it works
|
|
206
|
+
------------
|
|
207
|
+
|
|
208
|
+
Generation is two layers with a plain-struct seam between them
|
|
209
|
+
(`lib/stupidedi/schema/generation/`):
|
|
210
|
+
|
|
211
|
+
- **Layer A — `flat_file_reader.rb`** parses the `.TXT` distribution into the
|
|
212
|
+
plain value objects in `models.rb` (`Release`, `Element`, `Segment`,
|
|
213
|
+
`TransactionSet`, `LoopDefinition`, …). It carries no database or Ruby-source
|
|
214
|
+
concerns — any source that builds objects with the same shape can drive the
|
|
215
|
+
next layer.
|
|
216
|
+
- **Layer B — the generators** (`element_generator.rb`, `segment_generator.rb`,
|
|
217
|
+
`definition_generator.rb`, `functional_group_generator.rb`,
|
|
218
|
+
`interchange_generator.rb`, `module_loader_generator.rb`,
|
|
219
|
+
`registration_generator.rb`, `support_modules_generator.rb`) consume those
|
|
220
|
+
structs and emit the Ruby source above. `runner.rb` orchestrates a full
|
|
221
|
+
release; `bin/tediparse` is the CLI over it.
|
|
222
|
+
|
|
223
|
+
`spec/lib/stupidedi/schema/generation_spec.rb` round-trips the synthetic
|
|
224
|
+
fixture through the reader, the generators, and a child-process load that
|
|
225
|
+
builds real engine objects — so it's also a worked example of what each layer
|
|
226
|
+
produces.
|
|
227
|
+
|
|
228
|
+
Fidelity notes
|
|
229
|
+
--------------
|
|
230
|
+
|
|
231
|
+
A few details the generator carries through from the standard that are easy to
|
|
232
|
+
miss:
|
|
233
|
+
|
|
234
|
+
- **Element repeat counts** come from the SEGDETL repetition column: a numeric
|
|
235
|
+
count becomes `RepeatCount.bounded(n)`, the unbounded marker (`>1`) becomes
|
|
236
|
+
`RepeatCount.unbounded`, and a non-repeating use is `bounded(1)`.
|
|
237
|
+
- **The repetition separator** (ISA11 / element I65) is re-stamped in the
|
|
238
|
+
generated interchange's `replace_separators` on releases that use one, so the
|
|
239
|
+
writer round-trips it.
|
|
240
|
+
- **Higher-precision numeric types** (`N3`, `N5`, `N7`, `N8`, `N9`, …) all map
|
|
241
|
+
to the engine's `Nn`, which accepts arbitrary implied-decimal precision.
|