tediparse 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +319 -0
  3. data/LICENSE +26 -0
  4. data/README.md +308 -0
  5. data/Rakefile +33 -0
  6. data/bin/tediparse +102 -0
  7. data/doc/Defining.md +97 -0
  8. data/doc/Generating-Grammars.md +241 -0
  9. data/doc/Generating.md +332 -0
  10. data/doc/Navigating.md +670 -0
  11. data/doc/Parsing.md +133 -0
  12. data/doc/README.md +51 -0
  13. data/doc/Serializing.md +100 -0
  14. data/doc/Tokenizing.md +136 -0
  15. data/doc/Validating.md +140 -0
  16. data/lib/ruby/array.rb +226 -0
  17. data/lib/ruby/blank.rb +52 -0
  18. data/lib/ruby/exception.rb +14 -0
  19. data/lib/ruby/hash.rb +14 -0
  20. data/lib/ruby/module.rb +60 -0
  21. data/lib/ruby/object.rb +56 -0
  22. data/lib/ruby/string.rb +89 -0
  23. data/lib/ruby/to_d.rb +82 -0
  24. data/lib/ruby/to_date.rb +26 -0
  25. data/lib/ruby/to_time.rb +21 -0
  26. data/lib/ruby/try.rb +46 -0
  27. data/lib/stupidedi/builder.rb +6 -0
  28. data/lib/stupidedi/color.rb +94 -0
  29. data/lib/stupidedi/config/code_list_config.rb +45 -0
  30. data/lib/stupidedi/config/functional_group_config.rb +72 -0
  31. data/lib/stupidedi/config/interchange_config.rb +86 -0
  32. data/lib/stupidedi/config/transaction_set_config.rb +73 -0
  33. data/lib/stupidedi/config.rb +100 -0
  34. data/lib/stupidedi/either.rb +286 -0
  35. data/lib/stupidedi/exceptions/invalid_element_error.rb +7 -0
  36. data/lib/stupidedi/exceptions/invalid_schema_error.rb +7 -0
  37. data/lib/stupidedi/exceptions/missing_grammar_error.rb +50 -0
  38. data/lib/stupidedi/exceptions/output_error.rb +7 -0
  39. data/lib/stupidedi/exceptions/parse_error.rb +7 -0
  40. data/lib/stupidedi/exceptions/stupidedi_error.rb +7 -0
  41. data/lib/stupidedi/exceptions/tokenize_error.rb +7 -0
  42. data/lib/stupidedi/exceptions/zipper_error.rb +7 -0
  43. data/lib/stupidedi/exceptions.rb +13 -0
  44. data/lib/stupidedi/inspect.rb +26 -0
  45. data/lib/stupidedi/interchanges/element_types/separator_val.rb +79 -0
  46. data/lib/stupidedi/interchanges/element_types/special_val.rb +48 -0
  47. data/lib/stupidedi/interchanges/element_types.rb +11 -0
  48. data/lib/stupidedi/interchanges.rb +21 -0
  49. data/lib/stupidedi/parser/builder_dsl.rb +292 -0
  50. data/lib/stupidedi/parser/constraint_table.rb +566 -0
  51. data/lib/stupidedi/parser/generation.rb +147 -0
  52. data/lib/stupidedi/parser/identifier_stack.rb +248 -0
  53. data/lib/stupidedi/parser/instruction.rb +112 -0
  54. data/lib/stupidedi/parser/instruction_table.rb +222 -0
  55. data/lib/stupidedi/parser/navigation.rb +786 -0
  56. data/lib/stupidedi/parser/state_machine.rb +62 -0
  57. data/lib/stupidedi/parser/states/abstract_state.rb +385 -0
  58. data/lib/stupidedi/parser/states/failure_state.rb +69 -0
  59. data/lib/stupidedi/parser/states/functional_group_state.rb +101 -0
  60. data/lib/stupidedi/parser/states/initial_state.rb +62 -0
  61. data/lib/stupidedi/parser/states/interchange_state.rb +96 -0
  62. data/lib/stupidedi/parser/states/loop_state.rb +74 -0
  63. data/lib/stupidedi/parser/states/table_state.rb +90 -0
  64. data/lib/stupidedi/parser/states/transaction_set_state.rb +120 -0
  65. data/lib/stupidedi/parser/states/transmission_state.rb +59 -0
  66. data/lib/stupidedi/parser/tokenization.rb +195 -0
  67. data/lib/stupidedi/parser.rb +32 -0
  68. data/lib/stupidedi/reader/input/abstract_input.rb +137 -0
  69. data/lib/stupidedi/reader/input/delegated_input.rb +112 -0
  70. data/lib/stupidedi/reader/input/file_input.rb +157 -0
  71. data/lib/stupidedi/reader/input.rb +31 -0
  72. data/lib/stupidedi/reader/position.rb +78 -0
  73. data/lib/stupidedi/reader/result.rb +172 -0
  74. data/lib/stupidedi/reader/segment_dict.rb +176 -0
  75. data/lib/stupidedi/reader/separators.rb +90 -0
  76. data/lib/stupidedi/reader/stream_reader.rb +173 -0
  77. data/lib/stupidedi/reader/token_reader.rb +465 -0
  78. data/lib/stupidedi/reader/tokens/component_element_tok.rb +71 -0
  79. data/lib/stupidedi/reader/tokens/composite_element_tok.rb +85 -0
  80. data/lib/stupidedi/reader/tokens/repeated_element_tok.rb +74 -0
  81. data/lib/stupidedi/reader/tokens/segment_tok.rb +74 -0
  82. data/lib/stupidedi/reader/tokens/simple_element_tok.rb +76 -0
  83. data/lib/stupidedi/reader.rb +121 -0
  84. data/lib/stupidedi/schema/abstract_def.rb +76 -0
  85. data/lib/stupidedi/schema/abstract_element_def.rb +35 -0
  86. data/lib/stupidedi/schema/abstract_element_use.rb +47 -0
  87. data/lib/stupidedi/schema/abstract_use.rb +79 -0
  88. data/lib/stupidedi/schema/code_list.rb +99 -0
  89. data/lib/stupidedi/schema/component_element_use.rb +76 -0
  90. data/lib/stupidedi/schema/composite_element_def.rb +103 -0
  91. data/lib/stupidedi/schema/composite_element_use.rb +78 -0
  92. data/lib/stupidedi/schema/element_req.rb +57 -0
  93. data/lib/stupidedi/schema/functional_group_def.rb +124 -0
  94. data/lib/stupidedi/schema/generation/definition_generator.rb +139 -0
  95. data/lib/stupidedi/schema/generation/element_generator.rb +221 -0
  96. data/lib/stupidedi/schema/generation/flat_file_reader.rb +551 -0
  97. data/lib/stupidedi/schema/generation/functional_group_generator.rb +64 -0
  98. data/lib/stupidedi/schema/generation/interchange_generator.rb +145 -0
  99. data/lib/stupidedi/schema/generation/master_loader_generator.rb +121 -0
  100. data/lib/stupidedi/schema/generation/models.rb +85 -0
  101. data/lib/stupidedi/schema/generation/module_loader_generator.rb +64 -0
  102. data/lib/stupidedi/schema/generation/registration_generator.rb +230 -0
  103. data/lib/stupidedi/schema/generation/runner.rb +161 -0
  104. data/lib/stupidedi/schema/generation/segment_generator.rb +130 -0
  105. data/lib/stupidedi/schema/generation/support.rb +78 -0
  106. data/lib/stupidedi/schema/generation/support_modules_generator.rb +126 -0
  107. data/lib/stupidedi/schema/generation/version_modules.rb +35 -0
  108. data/lib/stupidedi/schema/generation.rb +94 -0
  109. data/lib/stupidedi/schema/interchange_def.rb +103 -0
  110. data/lib/stupidedi/schema/loop_def.rb +156 -0
  111. data/lib/stupidedi/schema/repeat_count.rb +86 -0
  112. data/lib/stupidedi/schema/segment_def.rb +122 -0
  113. data/lib/stupidedi/schema/segment_req.rb +46 -0
  114. data/lib/stupidedi/schema/segment_use.rb +99 -0
  115. data/lib/stupidedi/schema/simple_element_def.rb +51 -0
  116. data/lib/stupidedi/schema/simple_element_use.rb +83 -0
  117. data/lib/stupidedi/schema/syntax_note.rb +52 -0
  118. data/lib/stupidedi/schema/table_def.rb +178 -0
  119. data/lib/stupidedi/schema/transaction_set_def.rb +125 -0
  120. data/lib/stupidedi/schema.rb +30 -0
  121. data/lib/stupidedi/sets.rb +42 -0
  122. data/lib/stupidedi/transaction_sets/builder/dsl.rb +192 -0
  123. data/lib/stupidedi/transaction_sets/builder.rb +188 -0
  124. data/lib/stupidedi/transaction_sets/common/implementations/element_reqs.rb +37 -0
  125. data/lib/stupidedi/transaction_sets/common/implementations/segment_reqs.rb +31 -0
  126. data/lib/stupidedi/transaction_sets/common/implementations.rb +11 -0
  127. data/lib/stupidedi/transaction_sets/common.rb +8 -0
  128. data/lib/stupidedi/transaction_sets/validation/ambiguity.rb +395 -0
  129. data/lib/stupidedi/transaction_sets/validation/implementation.rb +12 -0
  130. data/lib/stupidedi/transaction_sets/validation.rb +9 -0
  131. data/lib/stupidedi/transaction_sets.rb +25 -0
  132. data/lib/stupidedi/values/abstract_element_val.rb +19 -0
  133. data/lib/stupidedi/values/abstract_val.rb +133 -0
  134. data/lib/stupidedi/values/composite_element_val.rb +102 -0
  135. data/lib/stupidedi/values/functional_group_val.rb +105 -0
  136. data/lib/stupidedi/values/interchange_val.rb +102 -0
  137. data/lib/stupidedi/values/invalid_envelope_val.rb +61 -0
  138. data/lib/stupidedi/values/invalid_segment_val.rb +89 -0
  139. data/lib/stupidedi/values/loop_val.rb +73 -0
  140. data/lib/stupidedi/values/repeated_element_val.rb +113 -0
  141. data/lib/stupidedi/values/segment_val.rb +105 -0
  142. data/lib/stupidedi/values/segment_val_group.rb +20 -0
  143. data/lib/stupidedi/values/simple_element_val.rb +80 -0
  144. data/lib/stupidedi/values/table_val.rb +69 -0
  145. data/lib/stupidedi/values/transaction_set_val.rb +69 -0
  146. data/lib/stupidedi/values/transmission_val.rb +56 -0
  147. data/lib/stupidedi/values.rb +22 -0
  148. data/lib/stupidedi/version.rb +4 -0
  149. data/lib/stupidedi/versions/common/element_reqs.rb +13 -0
  150. data/lib/stupidedi/versions/common/element_types/an.rb +386 -0
  151. data/lib/stupidedi/versions/common/element_types/dt.rb +572 -0
  152. data/lib/stupidedi/versions/common/element_types/id.rb +304 -0
  153. data/lib/stupidedi/versions/common/element_types/nn.rb +312 -0
  154. data/lib/stupidedi/versions/common/element_types/operators.rb +128 -0
  155. data/lib/stupidedi/versions/common/element_types/r.rb +342 -0
  156. data/lib/stupidedi/versions/common/element_types/simple_element.rb +73 -0
  157. data/lib/stupidedi/versions/common/element_types/tm.rb +347 -0
  158. data/lib/stupidedi/versions/common/element_types.rb +29 -0
  159. data/lib/stupidedi/versions/common/segment_reqs.rb +15 -0
  160. data/lib/stupidedi/versions/common/syntax_notes.rb +172 -0
  161. data/lib/stupidedi/versions/common.rb +11 -0
  162. data/lib/stupidedi/versions.rb +25 -0
  163. data/lib/stupidedi/writer/claredi.rb +178 -0
  164. data/lib/stupidedi/writer/default.rb +119 -0
  165. data/lib/stupidedi/writer.rb +7 -0
  166. data/lib/stupidedi/zipper/abstract_cursor.rb +351 -0
  167. data/lib/stupidedi/zipper/dangling_cursor.rb +103 -0
  168. data/lib/stupidedi/zipper/edited_cursor.rb +157 -0
  169. data/lib/stupidedi/zipper/memoized_cursor.rb +133 -0
  170. data/lib/stupidedi/zipper/path.rb +132 -0
  171. data/lib/stupidedi/zipper/root_cursor.rb +120 -0
  172. data/lib/stupidedi/zipper/stack_cursor.rb +107 -0
  173. data/lib/stupidedi/zipper.rb +45 -0
  174. data/lib/stupidedi.rb +69 -0
  175. data/lib/tediparse.rb +1 -0
  176. metadata +249 -0
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`).
@@ -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.