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/doc/Parsing.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Parsing X12
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
Parsing turns the token stream from the [reader](Tokenizing.md) into a typed
|
|
5
|
+
**parse tree** — a tree of interchange, functional-group, transaction-set,
|
|
6
|
+
table, loop, segment, and element values — by walking it against a grammar you
|
|
7
|
+
have registered. Where [tokenizing](Tokenizing.md) is purely lexical, parsing is
|
|
8
|
+
grammar-aware: it knows which segments may occur where, which loops they open,
|
|
9
|
+
and which elements are composite or repeating.
|
|
10
|
+
|
|
11
|
+
This page covers getting from input to a tree. Once you have the tree, see
|
|
12
|
+
[Navigating](Navigating.md) to traverse it and [Serializing](Serializing.md) to
|
|
13
|
+
write it back out.
|
|
14
|
+
|
|
15
|
+
You bring the grammar
|
|
16
|
+
---------------------
|
|
17
|
+
|
|
18
|
+
Tediparse ships the engine, not the grammars. Before parsing real documents you
|
|
19
|
+
must register your own grammar against a `Stupidedi::Config` — see
|
|
20
|
+
[Defining](Defining.md) (authoring by hand) or
|
|
21
|
+
[Generating-Grammars](Generating-Grammars.md) (generating from ASC X12 Table
|
|
22
|
+
Data). The examples below use `Synthetic.config`, the worked grammar in
|
|
23
|
+
`spec/support/synthetic/`; substitute your own populated config (e.g.
|
|
24
|
+
`MyApp::EDI.config`).
|
|
25
|
+
|
|
26
|
+
Building a parser and reading input
|
|
27
|
+
-----------------------------------
|
|
28
|
+
|
|
29
|
+
`Stupidedi::Parser.build(config)` returns a `Parser::StateMachine`. Feed it a
|
|
30
|
+
[reader](Tokenizing.md) with `#read`, which returns a **`[machine, result]`
|
|
31
|
+
pair**: the updated state machine (now holding the parse tree) and a
|
|
32
|
+
`Reader::Result` describing how the read ended.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "tediparse"
|
|
36
|
+
|
|
37
|
+
config = Synthetic.config # your populated Stupidedi::Config
|
|
38
|
+
parser = Stupidedi::Parser.build(config)
|
|
39
|
+
|
|
40
|
+
input = File.read("path/to/your.edi") # or File.open(..., :encoding => "ISO-8859-1")
|
|
41
|
+
machine, result = parser.read(Stupidedi::Reader.build(input))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Both the parser and the input are values: `read` does not mutate `parser`, it
|
|
45
|
+
returns a new machine. The original is still usable, which is what makes the
|
|
46
|
+
[navigation](Navigating.md) combinators safe to chain.
|
|
47
|
+
|
|
48
|
+
Checking the result
|
|
49
|
+
--------------------
|
|
50
|
+
|
|
51
|
+
The second element of the pair is a `Reader::Result`. Ask it whether the read
|
|
52
|
+
ended fatally, and use `explain` to surface the reason with its position:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
if result.fatal?
|
|
56
|
+
result.explain { |reason| raise "#{reason} at #{result.position.inspect}" }
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`result.position` (offset / line / column) points into the input, so errors are
|
|
61
|
+
locatable. A non-fatal result is the normal end-of-input case.
|
|
62
|
+
|
|
63
|
+
What "fatal" does and doesn't mean
|
|
64
|
+
----------------------------------
|
|
65
|
+
|
|
66
|
+
A non-fatal result does **not** mean the document was fully valid — it means
|
|
67
|
+
tokenizing and the overall walk completed. Individual segments that could not be
|
|
68
|
+
placed in the grammar are recorded *in the tree* as `InvalidSegmentVal` /
|
|
69
|
+
`InvalidEnvelopeVal` nodes rather than aborting the parse. Walk the tree to find
|
|
70
|
+
them; see [Validating](Validating.md) for how to collect and interpret these.
|
|
71
|
+
|
|
72
|
+
Missing grammar
|
|
73
|
+
---------------
|
|
74
|
+
|
|
75
|
+
If you parse a document whose interchange, functional-group, or transaction-set
|
|
76
|
+
identifiers are not registered on the config, the parser does **not** raise — it
|
|
77
|
+
produces a `FailureState` whose reason is
|
|
78
|
+
`Stupidedi::Exceptions::MissingGrammarError::DEFAULT_MESSAGE` (a pointer to
|
|
79
|
+
register a grammar). That surfaces as an `InvalidSegmentVal` in the tree at the
|
|
80
|
+
envelope segment that couldn't be resolved. So an empty or partial config yields
|
|
81
|
+
locatable failures, not a stack trace. (Reaching for a removed per-era constant
|
|
82
|
+
like `Stupidedi::Versions::FiftyTen` in *code*, by contrast, raises
|
|
83
|
+
`MissingGrammarError` directly.)
|
|
84
|
+
|
|
85
|
+
Immutability and non-determinism
|
|
86
|
+
--------------------------------
|
|
87
|
+
|
|
88
|
+
The state machine is immutable and may track **more than one parse hypothesis at
|
|
89
|
+
once**. Internally `machine.active` is a list of cursors — when the grammar is
|
|
90
|
+
momentarily ambiguous (a segment that could open more than one loop, say), the
|
|
91
|
+
parser keeps every live interpretation rather than guessing. `machine` answers
|
|
92
|
+
`#deterministic?` with `false` while more than one hypothesis is live.
|
|
93
|
+
|
|
94
|
+
`read` accepts a `:nondeterminism` option capping how many simultaneous
|
|
95
|
+
hypotheses are tolerated (**default `1`**). Exceeding the cap ends the read with
|
|
96
|
+
a fatal result ("too much non-determinism"):
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# Allow up to 8 concurrent parse hypotheses before giving up
|
|
100
|
+
machine, result = parser.read(Stupidedi::Reader.build(input), :nondeterminism => 8)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Most well-formed documents stay deterministic; raise the limit only for grammars
|
|
104
|
+
that are genuinely ambiguous at points the input later disambiguates. See
|
|
105
|
+
[Navigating → Non-determinism](Navigating.md) for how traversal behaves while
|
|
106
|
+
multiple trees are live, and [Validating](Validating.md) for catching ambiguity
|
|
107
|
+
in a *grammar definition* up front.
|
|
108
|
+
|
|
109
|
+
Getting at the tree
|
|
110
|
+
-------------------
|
|
111
|
+
|
|
112
|
+
The parse tree hangs off the machine. `machine.zipper` returns an
|
|
113
|
+
[`Either`](Navigating.md) wrapping a cursor at the root of the value tree;
|
|
114
|
+
`machine.zipper.fetch.root` gives you the root node (e.g. for the
|
|
115
|
+
[writer](Serializing.md)). In practice you navigate with the higher-level
|
|
116
|
+
combinators rather than touching the zipper directly:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
machine.first
|
|
120
|
+
.flatmap { |m| m.find(:GS) }
|
|
121
|
+
.flatmap { |m| m.find(:ST) }
|
|
122
|
+
.tap { |m| puts m.segment.fetch.node.id }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
From here, traversal is its own topic — continue with
|
|
126
|
+
[Navigating the Parse Tree](Navigating.md).
|
|
127
|
+
|
|
128
|
+
Where to go next
|
|
129
|
+
----------------
|
|
130
|
+
|
|
131
|
+
- [Navigating](Navigating.md) — find, iterate, and read values from the tree.
|
|
132
|
+
- [Validating](Validating.md) — interpret invalid nodes and check grammars.
|
|
133
|
+
- [Serializing](Serializing.md) — write a tree back to X12 text.
|
data/doc/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Tediparse Documentation
|
|
2
|
+
========================
|
|
3
|
+
|
|
4
|
+
Tediparse is a library for **parsing, generating, and validating** ASC X12 EDI
|
|
5
|
+
documents. It ships the engine — the schema vocabulary, the immutable
|
|
6
|
+
parser/state machine, the reader/writer, and the tree cursor — but **not** any
|
|
7
|
+
X12 transaction-set grammars: that material is X12 IP you supply or generate.
|
|
8
|
+
See the [project README](../README.md) for scope, licensing, and installation.
|
|
9
|
+
|
|
10
|
+
These pages are the human documentation. They run roughly in the order you'd
|
|
11
|
+
use the library: define or generate a grammar, then read, traverse, validate,
|
|
12
|
+
and write documents against it. The runnable worked example every page refers to
|
|
13
|
+
is the synthetic grammar under `spec/support/synthetic/` (`Synthetic.config`).
|
|
14
|
+
|
|
15
|
+
Authoring a grammar
|
|
16
|
+
-------------------
|
|
17
|
+
|
|
18
|
+
You bring the grammar; these cover the two ways to get one.
|
|
19
|
+
|
|
20
|
+
- [Defining](Defining.md) — transcribe a grammar by hand from a purchased X12
|
|
21
|
+
implementation guide (`TableDef`, `LoopDef`, `SegmentDef`, elements).
|
|
22
|
+
- [Generating Grammars](Generating-Grammars.md) — generate the whole definition
|
|
23
|
+
tree for a release from licensed ASC X12 Table Data flat files.
|
|
24
|
+
|
|
25
|
+
Generating documents
|
|
26
|
+
--------------------
|
|
27
|
+
|
|
28
|
+
- [Generating](Generating.md) — build well-formed X12 documents with the
|
|
29
|
+
`BuilderDsl` writer DSL, with validation on every segment.
|
|
30
|
+
|
|
31
|
+
Reading & parsing
|
|
32
|
+
-----------------
|
|
33
|
+
|
|
34
|
+
- [Tokenizing](Tokenizing.md) — the lexical layer: bytes → segment tokens,
|
|
35
|
+
separators, and positions (no grammar required).
|
|
36
|
+
- [Parsing](Parsing.md) — tokens → a typed parse tree, against a registered
|
|
37
|
+
grammar; results, immutability, and non-determinism.
|
|
38
|
+
- [Navigating](Navigating.md) — traverse and read values from the parse tree
|
|
39
|
+
(`first`/`find`/`next`/`parent`, the `Either` combinators, and more).
|
|
40
|
+
|
|
41
|
+
Validating
|
|
42
|
+
----------
|
|
43
|
+
|
|
44
|
+
- [Validating](Validating.md) — the three moments validation happens: while
|
|
45
|
+
generating, while parsing, and when auditing a grammar for ambiguity.
|
|
46
|
+
|
|
47
|
+
Serializing
|
|
48
|
+
-----------
|
|
49
|
+
|
|
50
|
+
- [Serializing](Serializing.md) — write a parse tree back to X12 text
|
|
51
|
+
(`Writer::Default`) or HTML (`Writer::Claredi`).
|
data/doc/Serializing.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Serializing X12
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
Serializing is the inverse of [parsing](Parsing.md): taking a parse tree and
|
|
5
|
+
writing it back out. Once you have a tree — whether you
|
|
6
|
+
[generated](Generating.md) it with the builder DSL or [parsed](Parsing.md) it
|
|
7
|
+
from input — you serialize it with one of the writers under
|
|
8
|
+
`Stupidedi::Writer`:
|
|
9
|
+
|
|
10
|
+
- **`Writer::Default`** writes X12 text using a set of [separators](Tokenizing.md).
|
|
11
|
+
- **`Writer::Claredi`** writes a formatted HTML rendering of the tree.
|
|
12
|
+
|
|
13
|
+
Getting a tree to serialize
|
|
14
|
+
---------------------------
|
|
15
|
+
|
|
16
|
+
Both writers take the **root node** of the value tree. You reach it through the
|
|
17
|
+
state machine's zipper, `machine.zipper.fetch.root`, in either of the two ways a
|
|
18
|
+
tree comes to exist:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# (a) After generating with the builder DSL
|
|
22
|
+
b = Stupidedi::Parser::BuilderDsl.build(config)
|
|
23
|
+
b.ISA(...); b.GS(...); b.ST(...); # ... ; b.SE(...); b.GE(...); b.IEA(...)
|
|
24
|
+
root = b.machine.zipper.fetch.root
|
|
25
|
+
|
|
26
|
+
# (b) After parsing input
|
|
27
|
+
machine, _result = Stupidedi::Parser.build(config).read(Stupidedi::Reader.build(input))
|
|
28
|
+
root = machine.zipper.fetch.root
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`zipper` returns an [`Either`](Navigating.md); `.fetch` unwraps it (or raises if
|
|
32
|
+
the tree is empty) and `.root` rewinds to the top of the tree.
|
|
33
|
+
|
|
34
|
+
Writing X12 text
|
|
35
|
+
----------------
|
|
36
|
+
|
|
37
|
+
`Writer::Default.new(root, separators)` writes the tree as X12, using the
|
|
38
|
+
separators you pass for the element, component, repetition, and segment
|
|
39
|
+
delimiters. `#write` returns the serialized `String`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
separators = Stupidedi::Reader::Separators.new(":", "^", "*", "~")
|
|
43
|
+
# comp rep elem seg
|
|
44
|
+
|
|
45
|
+
x12 = Stupidedi::Writer::Default.new(root, separators).write
|
|
46
|
+
puts x12
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You can serialize with **different separators than the input used** — the writer
|
|
50
|
+
re-stamps the envelope's `ISA11`/`ISA16` to match the separators you request, so
|
|
51
|
+
the output is internally consistent. Round-tripping with the *same* separators
|
|
52
|
+
reproduces the original document (the synthetic smoke tests in
|
|
53
|
+
`spec/lib/stupidedi/synthetic/smoke_spec.rb` assert this byte-for-byte).
|
|
54
|
+
|
|
55
|
+
The role of separators
|
|
56
|
+
----------------------
|
|
57
|
+
|
|
58
|
+
`Stupidedi::Reader::Separators` (the same class the [reader](Tokenizing.md)
|
|
59
|
+
discovers from `ISA`) drives every delimiter the writer emits. A few rules the
|
|
60
|
+
writer enforces:
|
|
61
|
+
|
|
62
|
+
- The segment terminator and element separator must be non-blank when writing an
|
|
63
|
+
interchange — a blank one raises `Stupidedi::Exceptions::OutputError`
|
|
64
|
+
(`"separators.segment cannot be blank"`).
|
|
65
|
+
- A separator character must not collide with the data: if a value contains a
|
|
66
|
+
character you've chosen as a delimiter, the writer raises `OutputError`
|
|
67
|
+
(`'characters "~" occur as data'`) rather than emit an unparseable document.
|
|
68
|
+
|
|
69
|
+
`Separators.default` (`(":", "^", "*", "~")`) is a convenient starting point.
|
|
70
|
+
|
|
71
|
+
Writing HTML
|
|
72
|
+
------------
|
|
73
|
+
|
|
74
|
+
`Writer::Claredi` renders the tree as an HTML document — useful for inspection
|
|
75
|
+
or display. It takes just the root node (no separators) and `#write` returns the
|
|
76
|
+
output (an `IO`/`StringIO` you can read or write to a file):
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
File.open("output.html", "w") do |f|
|
|
80
|
+
f.write Stupidedi::Writer::Claredi.new(root).write.string
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Serializing a subtree
|
|
85
|
+
---------------------
|
|
86
|
+
|
|
87
|
+
The writers don't require the interchange root — you can serialize any subtree.
|
|
88
|
+
Navigate to the node you want, take its zipper, and write that:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
st = b.machine.first.flatmap { |m| m.sequence(:GS, :ST) }.fetch.zipper.fetch
|
|
92
|
+
fragment = Stupidedi::Writer::Default.new(st, separators).write
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Where to go next
|
|
96
|
+
----------------
|
|
97
|
+
|
|
98
|
+
- [Generating](Generating.md) — build a tree to serialize.
|
|
99
|
+
- [Parsing](Parsing.md) — parse input into a tree to round-trip.
|
|
100
|
+
- [Navigating](Navigating.md) — locate the subtree you want to write.
|
data/doc/Tokenizing.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Tokenizing X12
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Tokenizing is the first stage of reading an X12 document: turning a stream of
|
|
5
|
+
bytes into a sequence of **segment tokens**, before any grammar is consulted.
|
|
6
|
+
The reader knows about X12's *lexical* structure — segments, elements,
|
|
7
|
+
components, repetitions, and the four delimiters — but nothing about what a
|
|
8
|
+
particular transaction set means. That separation is deliberate: you can
|
|
9
|
+
tokenize a document with no grammar registered at all, and the same tokenizer
|
|
10
|
+
feeds the grammar-aware [parser](Parsing.md).
|
|
11
|
+
|
|
12
|
+
The reader lives under `Stupidedi::Reader`. To go from tokens to a typed parse
|
|
13
|
+
tree, see [Parsing](Parsing.md); this page covers the lexical layer only.
|
|
14
|
+
|
|
15
|
+
Building a reader
|
|
16
|
+
-----------------
|
|
17
|
+
|
|
18
|
+
`Stupidedi::Reader.build(input)` returns a tokenizer. `input` may be a `String`,
|
|
19
|
+
an `IO` (e.g. an open `File`), or any `Stupidedi::Reader::Input`:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "tediparse"
|
|
23
|
+
|
|
24
|
+
input = File.open("path/to/your.edi", :encoding => "ISO-8859-1")
|
|
25
|
+
reader = Stupidedi::Reader.build(input) # => Reader::StreamReader
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
X12 is an ISO-8859-1 (Latin-1) format, not UTF-8 — open files with that
|
|
29
|
+
encoding to avoid corrupting extended characters.
|
|
30
|
+
|
|
31
|
+
Two phases: StreamReader and TokenReader
|
|
32
|
+
----------------------------------------
|
|
33
|
+
|
|
34
|
+
Tokenizing happens in two phases, because the delimiters are not known until
|
|
35
|
+
the `ISA` segment has been read:
|
|
36
|
+
|
|
37
|
+
- **`StreamReader`** is what `Reader.build` returns. It skips any out-of-band
|
|
38
|
+
data (whitespace, line breaks, or other content between interchanges) until
|
|
39
|
+
it finds a literal `ISA`, then tokenizes that fixed-width segment. The `ISA`
|
|
40
|
+
segment is special: its layout is fixed by the standard, so the reader can
|
|
41
|
+
parse it byte-for-byte even before it knows the delimiters.
|
|
42
|
+
- **`TokenReader`** takes over once `ISA` has been read. It is delimiter-aware
|
|
43
|
+
and tokenizes every following segment (`GS`, `ST`, …) using the separators
|
|
44
|
+
discovered from `ISA`.
|
|
45
|
+
|
|
46
|
+
You rarely name these classes directly — you call `read_segment` and thread the
|
|
47
|
+
reader it hands back, and the transition from `StreamReader` to `TokenReader`
|
|
48
|
+
happens for you.
|
|
49
|
+
|
|
50
|
+
Reading segments
|
|
51
|
+
----------------
|
|
52
|
+
|
|
53
|
+
A reader has no `each`; you pull one segment at a time with `read_segment`,
|
|
54
|
+
which returns an [`Either`](Navigating.md) wrapping the token **and the next
|
|
55
|
+
reader**. Thread that reader through `Either#flatmap` to consume the stream,
|
|
56
|
+
stopping when the `Either` becomes a failure (end of input):
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
reader = Stupidedi::Reader.build(input)
|
|
60
|
+
|
|
61
|
+
drain = lambda do |r|
|
|
62
|
+
r.read_segment.flatmap do |segment_tok, next_reader|
|
|
63
|
+
puts segment_tok.id # => :ISA, :GS, :ST, ...
|
|
64
|
+
drain.call(next_reader) # recurse on the new reader
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
drain.call(reader)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Each success yields a `Reader::SegmentTok` plus the next reader; the previous
|
|
72
|
+
reader is untouched (the readers are immutable, like the rest of the library).
|
|
73
|
+
The parser's own read loop is exactly this pattern — see
|
|
74
|
+
`lib/stupidedi/parser/generation.rb` for the production version.
|
|
75
|
+
|
|
76
|
+
Tokens
|
|
77
|
+
------
|
|
78
|
+
|
|
79
|
+
- **`SegmentTok`** — one segment. `#id` is the segment identifier as a `Symbol`
|
|
80
|
+
(`:ISA`, `:ST`, …), `#element_toks` is the ordered list of element tokens,
|
|
81
|
+
and `#position` is where it started in the input (see below).
|
|
82
|
+
- **`SimpleElementTok`** — one ordinary element; `#value` is its string value.
|
|
83
|
+
- **`CompositeElementTok`**, **`ComponentElementTok`**, **`RepeatedElementTok`**
|
|
84
|
+
— composites, their components, and repeated elements, when the grammar (or
|
|
85
|
+
the raw delimiters) calls for them.
|
|
86
|
+
|
|
87
|
+
Without a grammar, the `TokenReader` treats every element as simple — it has no
|
|
88
|
+
way to know which elements are composite or repeating. Supply a grammar through
|
|
89
|
+
the [parser](Parsing.md) to get composite/repeated structure.
|
|
90
|
+
|
|
91
|
+
Separators
|
|
92
|
+
----------
|
|
93
|
+
|
|
94
|
+
`Stupidedi::Reader::Separators` carries the four X12 delimiters, each available
|
|
95
|
+
as an accessor:
|
|
96
|
+
|
|
97
|
+
| Accessor | X12 role | Typical character |
|
|
98
|
+
| ------------- | ------------------------ | ----------------- |
|
|
99
|
+
| `#element` | separates elements | `*` |
|
|
100
|
+
| `#component` | separates components | `:` |
|
|
101
|
+
| `#repetition` | separates repetitions | `^` |
|
|
102
|
+
| `#segment` | terminates a segment | `~` |
|
|
103
|
+
|
|
104
|
+
The delimiters are **discovered from the document**, not assumed — handling an
|
|
105
|
+
unexpected choice (say, swapping `:` and `~`) is one of the things a hand-rolled
|
|
106
|
+
parser usually gets wrong. They become known in stages:
|
|
107
|
+
|
|
108
|
+
1. While reading `ISA`, `StreamReader` takes the character immediately after
|
|
109
|
+
`ISA` as the **element** separator, and the character after the 16th element
|
|
110
|
+
as the **segment** terminator.
|
|
111
|
+
2. The **component** and **repetition** separators are version-dependent and are
|
|
112
|
+
resolved only when the parser enters the interchange envelope and the
|
|
113
|
+
grammar's `InterchangeDef#separators(isa)` reads them out of `ISA16` and
|
|
114
|
+
`ISA11`.
|
|
115
|
+
|
|
116
|
+
So **standalone tokenizing (no grammar) gives you the element and segment
|
|
117
|
+
delimiters**; full component/repetition resolution needs the envelope grammar,
|
|
118
|
+
i.e. the [parser](Parsing.md). You can also construct separators explicitly —
|
|
119
|
+
`Separators.default` is `(":", "^", "*", "~")` — which the [writer](Serializing.md)
|
|
120
|
+
uses to serialize a tree back to X12.
|
|
121
|
+
|
|
122
|
+
Positions
|
|
123
|
+
---------
|
|
124
|
+
|
|
125
|
+
Every token carries a `Stupidedi::Reader::Position` (`#position`) recording
|
|
126
|
+
where it began: `#offset`, `#line`, `#column`, and `#path`. Positions are what
|
|
127
|
+
make error messages actionable — when the [parser](Parsing.md) reports a
|
|
128
|
+
failure, `result.position` points at the exact spot in the input.
|
|
129
|
+
|
|
130
|
+
Where to go next
|
|
131
|
+
----------------
|
|
132
|
+
|
|
133
|
+
- [Parsing](Parsing.md) — feed the tokens to a grammar and build a typed parse
|
|
134
|
+
tree.
|
|
135
|
+
- [Navigating](Navigating.md) — traverse the tree the parser builds.
|
|
136
|
+
- [Serializing](Serializing.md) — turn a tree back into X12 text.
|
data/doc/Validating.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Validating X12
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Validation in tediparse happens at three distinct moments, and it helps to keep
|
|
5
|
+
them separate because they report problems in different ways:
|
|
6
|
+
|
|
7
|
+
1. **Generating** a document — the writer DSL validates each segment as you
|
|
8
|
+
build it and *raises* on the first violation.
|
|
9
|
+
2. **Parsing** a document — segments that don't fit the grammar become
|
|
10
|
+
*invalid-value nodes in the tree* rather than exceptions, so one bad segment
|
|
11
|
+
doesn't abort the read.
|
|
12
|
+
3. **Auditing a grammar** — the `Ambiguity` analyzer statically checks a
|
|
13
|
+
transaction-set definition for places the parser couldn't deterministically
|
|
14
|
+
resolve, and raises before you ship the grammar.
|
|
15
|
+
|
|
16
|
+
This page covers all three. The examples use `Synthetic.config` and the
|
|
17
|
+
synthetic grammar in `spec/support/synthetic/`; substitute your own grammar.
|
|
18
|
+
|
|
19
|
+
Validation while generating
|
|
20
|
+
---------------------------
|
|
21
|
+
|
|
22
|
+
`Stupidedi::Parser::BuilderDsl` validates incrementally. Built with
|
|
23
|
+
`strict = true` (the default), it checks each segment the instant you add it and
|
|
24
|
+
raises `Stupidedi::Exceptions::ParseError` the moment your code violates the
|
|
25
|
+
specification — with a stack trace pointing at the offending call, not after the
|
|
26
|
+
whole document is assembled.
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
b = Stupidedi::Parser::BuilderDsl.build(config) # strict: true by default
|
|
30
|
+
|
|
31
|
+
b.ISA(...)
|
|
32
|
+
b.GS(...)
|
|
33
|
+
b.ST("100", "0001")
|
|
34
|
+
b.HH("Synthetic heading text")
|
|
35
|
+
# b.IT("P:item-1", "3", "125.5") # would raise ParseError: required segment
|
|
36
|
+
# missing, value not allowed, too long, etc.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
What `critique` checks as each node completes
|
|
40
|
+
(`lib/stupidedi/parser/builder_dsl.rb`):
|
|
41
|
+
|
|
42
|
+
- **Element type** — the value is valid for its element type (numeric, date,
|
|
43
|
+
time, …).
|
|
44
|
+
- **Requirement** — a required element/segment isn't blank; a forbidden
|
|
45
|
+
("not used") one isn't present.
|
|
46
|
+
- **Code lists** — an `ID` element's value is one of its allowed values.
|
|
47
|
+
- **Length** — the value isn't shorter or longer than the element's min/max.
|
|
48
|
+
- **Syntax notes** — paired/conditional/exclusion rules (e.g. the `P` paired
|
|
49
|
+
note) are satisfied on composites and segments.
|
|
50
|
+
- **Occurrence bounds** — a segment or loop occurs at least the required number
|
|
51
|
+
of times and no more than its repeat count allows.
|
|
52
|
+
|
|
53
|
+
Passing `strict = false` to `BuilderDsl.build(config, false)` skips these checks
|
|
54
|
+
— useful when you deliberately need to emit a non-conforming document for a
|
|
55
|
+
trading partner that demands one.
|
|
56
|
+
|
|
57
|
+
Validation while parsing
|
|
58
|
+
------------------------
|
|
59
|
+
|
|
60
|
+
When *reading* (see [Parsing](Parsing.md)), the parser is forgiving by design: a
|
|
61
|
+
segment that can't be placed in the grammar — or an envelope whose grammar isn't
|
|
62
|
+
registered — becomes an `InvalidSegmentVal` / `InvalidEnvelopeVal` node in the
|
|
63
|
+
tree instead of raising. `result.fatal?` stays `false`; the document parses, and
|
|
64
|
+
you inspect the tree to find what didn't fit.
|
|
65
|
+
|
|
66
|
+
Walk the value tree and collect the invalid nodes:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
machine, result = Stupidedi::Parser.build(config).read(Stupidedi::Reader.build(input))
|
|
70
|
+
|
|
71
|
+
def collect_failures(machine)
|
|
72
|
+
failures = []
|
|
73
|
+
walk = lambda do |z|
|
|
74
|
+
node = z.node
|
|
75
|
+
failures << node if node.is_a?(Stupidedi::Values::InvalidSegmentVal) ||
|
|
76
|
+
node.is_a?(Stupidedi::Values::InvalidEnvelopeVal)
|
|
77
|
+
z.children.each { |c| walk.call(c) } if z.respond_to?(:children) && !z.leaf?
|
|
78
|
+
end
|
|
79
|
+
walk.call(machine.zipper.fetch.root)
|
|
80
|
+
failures
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
failures = collect_failures(machine)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
An `InvalidSegmentVal` carries the reason it couldn't be placed and the original
|
|
87
|
+
segment token (with its [position](Tokenizing.md)). A transaction set whose
|
|
88
|
+
`ST01` code isn't registered, for example, yields an `InvalidSegmentVal` at the
|
|
89
|
+
`ST` segment carrying the missing-grammar message (see
|
|
90
|
+
[Parsing → Missing grammar](Parsing.md)). This is the pattern the smoke tests in
|
|
91
|
+
`spec/lib/stupidedi/synthetic/smoke_spec.rb` use.
|
|
92
|
+
|
|
93
|
+
Auditing a grammar for ambiguity
|
|
94
|
+
--------------------------------
|
|
95
|
+
|
|
96
|
+
The third kind of validation is about the *grammar itself*, not a document. A
|
|
97
|
+
grammar is ambiguous when the parser, reading a given segment, can't
|
|
98
|
+
deterministically decide which slot it fills — for instance two sibling loops
|
|
99
|
+
that open on the same segment with overlapping allowed values on the
|
|
100
|
+
discriminating element. Ambiguity is what forces the parser into the
|
|
101
|
+
multiple-hypothesis state described in [Parsing](Parsing.md); catching it at the
|
|
102
|
+
definition stage is far better than discovering it at runtime.
|
|
103
|
+
|
|
104
|
+
`Stupidedi::TransactionSets::Validation::Ambiguity` audits a `TransactionSetDef`
|
|
105
|
+
and raises `Stupidedi::Exceptions::InvalidSchemaError` when it finds an
|
|
106
|
+
unresolvable choice:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
Stupidedi::TransactionSets::Validation::Ambiguity.build(
|
|
110
|
+
transaction_set_def, # the TransactionSetDef to audit
|
|
111
|
+
functional_group_def, # the GS/GE envelope it lives in
|
|
112
|
+
interchange_def # the ISA/IEA envelope
|
|
113
|
+
).audit
|
|
114
|
+
# raises InvalidSchemaError if the definition is ambiguous
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The synthetic suite exercises this against a deliberately ambiguous grammar —
|
|
118
|
+
`spec/support/synthetic/ambiguous_demo.rb` defines two `LO` loops at the same
|
|
119
|
+
position that both accept the discriminator value `"P"`, and
|
|
120
|
+
`spec/lib/stupidedi/synthetic/ambiguity_spec.rb` asserts the audit raises an
|
|
121
|
+
`InvalidSchemaError` whose message names the overlapping `ValueBased` choice:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
Stupidedi::TransactionSets::Validation::Ambiguity.build(
|
|
125
|
+
Synthetic::AmbiguousDemo,
|
|
126
|
+
Synthetic::FunctionalGroupDef,
|
|
127
|
+
Synthetic::InterchangeDef
|
|
128
|
+
).audit
|
|
129
|
+
#=> raises InvalidSchemaError: ... overlapping ... ValueBased ...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Run the audit over your transaction-set definitions in your own test suite to
|
|
133
|
+
catch ambiguity before it reaches a parser.
|
|
134
|
+
|
|
135
|
+
Where to go next
|
|
136
|
+
----------------
|
|
137
|
+
|
|
138
|
+
- [Generating](Generating.md) — the full writer DSL these checks guard.
|
|
139
|
+
- [Parsing](Parsing.md) — how invalid nodes get into the tree.
|
|
140
|
+
- [Navigating](Navigating.md) — traversing a tree that may contain invalid nodes.
|