{"id":13592263,"url":"https://github.com/kputnam/stupidedi","last_synced_at":"2026-05-29T07:17:46.947Z","repository":{"id":1926109,"uuid":"2854367","full_name":"kputnam/stupidedi","owner":"kputnam","description":"Ruby API for parsing and generating ASC X12 EDI transactions","archived":false,"fork":false,"pushed_at":"2024-03-01T16:52:28.000Z","size":41887,"stargazers_count":254,"open_issues_count":29,"forks_count":138,"subscribers_count":12,"default_branch":"master","last_synced_at":"2024-05-21T23:06:48.920Z","etag":null,"topics":["edi","grammar","hc-837","hn-277","hp-835","parser","po-810","ruby","validation","x12"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kputnam.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2011-11-26T06:04:43.000Z","updated_at":"2024-06-18T18:37:10.334Z","dependencies_parsed_at":"2024-01-13T15:37:50.552Z","dependency_job_id":"b8704973-6697-4cdb-abe1-9554820a1be3","html_url":"https://github.com/kputnam/stupidedi","commit_stats":{"total_commits":952,"total_committers":39,"mean_commits":24.41025641025641,"dds":0.5399159663865547,"last_synced_commit":"9e1b9896b371078d439e72505165082b898cd00a"},"previous_names":["irobayna/stupidedi"],"tags_count":30,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kputnam%2Fstupidedi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kputnam%2Fstupidedi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kputnam%2Fstupidedi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kputnam%2Fstupidedi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kputnam","download_url":"https://codeload.github.com/kputnam/stupidedi/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247294538,"owners_count":20915340,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["edi","grammar","hc-837","hn-277","hp-835","parser","po-810","ruby","validation","x12"],"created_at":"2024-08-01T16:01:07.473Z","updated_at":"2026-05-29T07:17:46.941Z","avatar_url":"https://github.com/kputnam.png","language":"Ruby","funding_links":[],"categories":["Parsers","Libraries"],"sub_categories":["Ruby"],"readme":"# Stupidedi\n\n[![Build Status](https://github.com/kputnam/stupidedi/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/kputnam/stupidedi/actions?query=branch%3Amaster) [![GitHub version](https://badge.fury.io/rb/stupidedi.svg)](http://badge.fury.io/gh/irobayna%2Fstupidedi) [![Code Climate](https://codeclimate.com/github/irobayna/stupidedi.png)](https://codeclimate.com/github/irobayna/stupidedi) [![Inline docs](http://inch-ci.org/github/irobayna/stupidedi.png?branch=master)](http://inch-ci.org/github/irobayna/stupidedi)\n\n![Screenshot](https://raw.github.com/irobayna/stupidedi/master/doc/images/edi-pp.png)\n\n\n* [GitHub project](http://github.com/irobayna/stupidedi)\n* [Human Documentation](https://github.com/irobayna/stupidedi/tree/master/doc)\n* [API Documentation](http://rubydoc.info/github/kputnam/stupidedi/master/frames)\n\nStupidedi is a high-quality library for parsing, generating, validating,\nand manipulating ASC X12 EDI documents. Very roughly, it's jQuery for\nEDI.\n\nFor those unfamiliar with ASC X12 EDI, it is a data format used to\nencode common business documents like purchase orders, delivery\nnotices, and health care claims. It is similar to XML in some ways,\nbut precedes it by about 15 years; so if you think XML sucks, you\nwill love to hate EDI.\n\n### Credits\n\n* __Author__: [Kyle Putnam](https://github.com/kputnam)\n* __Maintainer__: [Isi Robayna](https://github.com/irobayna)\n\n## What problem does it solve?\n\nTransaction set specifications can be enormous, boring, and vague.\nTrading partners can demand strict adherence (often to their own unique\ninterpretation of the specification) of the documents you generate.\nHowever, documents they generate themselves are often non-standard\nand require flexibility to parse them.\n\nStupidedi enables you to encode these transaction set specifications\ndirectly in Ruby. From these specifications, it will generate a parser\nto read incoming messages and a DSL to generate outgoing messages. This\napproach has a huge advantage over writing a parser from scratch, which\ncan be error-prone and difficult to change.\n\nSignificant thought was put into the design of the library. Some of\nthe features are described here.\n\n### Robust tokenization and parsing\n\nDelimiters, line breaks, and out-of-band data between interchanges are\nhandled correctly. While many trading partners follow common conventions,\nit only takes one unexpected deviation, like swapping the \":\" and \"~\"\ndelimiters, to render a hand-written parser broken.\n\nStupidedi handles many edge cases that can only be anticipated by reading\ncarefully between the lines of the X12 documentation.\n\n### Instant feedback on error conditions\n\nWhen generating EDI documents, validation is performed incrementally\non each segment. This means the instant your client code violates the\nspecification, an exception is thrown with a meaningful stack trace.\nOther libraries only perform validation after the entire document has\nbeen generated, while some don't perform validation at all.\n\nStupidedi performs extensive validation and ensures well-formedness.\nSee the human readable documentation in doc/Generating.md for more\ndetails.\n\n### Encourages readable client code\n\nUnlike other libraries, generating documents doesn't involve naming\nobscure identifiers from the specification (like C001, DE522 or LOOP2000),\nfor elements of the grammar that don't actually appear in the output.\n\nLike HAML or Builder::XmlMarkup, the DSL for generating documents closely\nmatches terminology from the problem domain. You can see in the example\nbelow that code looks very similar to an EDI document. This makes it easy\nto understand, assuming a reasonable familiarity with EDI.\n\n### Efficient parsing and traversing\n\nThe parser is designed using immutable data structures, making it thread-safe\nfor runtimes that can utilize multiple cores. In some cases, immutability places\nhigher demand on garbage collection, this has been somewhat mitigated with careful\noptimization.\n\n**Note:** Further optimizations are planned for late 2021. Until then, modestly large\nfiles can consume excessive memory to the point all CPU time is spent in garbage\ncollection. In more than ten years since this library was put into production, this\nseems to rarely happen in practice (as X12 files are most often very small), but until\n  these improvements are merged, it can be difficult to work around. See [#65](https://github.com/irobayna/stupidedi/issues/65) for\n    more discussion. The current work in progress is in the [`gh-65.3`](https://github.com/irobayna/stupidedi/tree/gh-65.3) branch.\n\n![Benchmark](https://raw.github.com/irobayna/stupidedi/master/notes/benchmark/throughput.png)\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003cth\u003esegments\u003c/th\u003e\n    \u003cth\u003e1.9.3\u003c/th\u003e\n    \u003cth\u003e1.9.2\u003c/th\u003e\n    \u003cth\u003erbx-head\u003c/th\u003e\n    \u003cth\u003ejruby-1.6.6\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e1680\u003c/td\u003e\n    \u003ctd\u003e2107.90\u003c/td\u003e\n    \u003ctd\u003e2007.17\u003c/td\u003e\n    \u003ctd\u003e503.14\u003c/td\u003e\n    \u003ctd\u003e317.52\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e3360\u003c/td\u003e\n    \u003ctd\u003e2461.54\u003c/td\u003e\n    \u003ctd\u003e2420.75\u003c/td\u003e\n    \u003ctd\u003e731.71\u003c/td\u003e\n    \u003ctd\u003e477.07\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e6720\u003c/td\u003e\n    \u003ctd\u003e2677.29\u003c/td\u003e\n    \u003ctd\u003e2620.90\u003c/td\u003e\n    \u003ctd\u003e950.63\u003c/td\u003e\n    \u003ctd\u003e685.15\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e13440\u003c/td\u003e\n    \u003ctd\u003e2699.88\u003c/td\u003e\n    \u003ctd\u003e2663.50\u003c/td\u003e\n    \u003ctd\u003e1071.00\u003c/td\u003e\n    \u003ctd\u003e897.50\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e26880\u003c/td\u003e\n    \u003ctd\u003e2558.54\u003c/td\u003e\n    \u003ctd\u003e2510.51\u003c/td\u003e\n    \u003ctd\u003e1124.50\u003c/td\u003e\n    \u003ctd\u003e1112.67\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e53760\u003c/td\u003e\n    \u003ctd\u003e2254.94\u003c/td\u003e\n    \u003ctd\u003e2164.16\u003c/td\u003e\n    \u003ctd\u003e1039.81\u003c/td\u003e\n    \u003ctd\u003e1292.62\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\nThese benchmarks aren't scientific by any means. They were performed on a\nMacBook Pro, 2.2GHz Core i7 with 8GB RAM by reading the X222-HC837 fixture data\nfiles, in serial. The results show the parser runtime is O(n), linear in the\nsize of the input, but the drop in throughput at 13K+ segments is likely due to\nmemory allocation. The steady increase in throughput on JRuby and Rubinus is\nprobably attributable optimizations performed by the JIT compiler.\n\nLastly, these results should approximate the performance of document generation\nwith BuilderDsl, except BuilderdsL API should have less overhead, as it skips\nthe tokenizer. On the other hand, BuilderDsl frequently queries the call stack\nto track provenance of elements in the parse tree. In common real-world use,\ncustom application logic and database access are going to bottleneck performance,\nrather than Stupidedi.\n\n### Helps developers gain familiarity\n\n## Why not a commercial translator?\n\nCommercial EDI translators solve a different set of problems. Many focus\non translating between EDI and another data format, like XML, CSV, or a\nrelational database. This isn't particularly productive, as you still have\nto unserialize the data to do anything with it.\n\n## What doesn't it solve?\n\nIt isn't a translator. It doesn't have bells and whistles, like the\ncommercial EDI translators have, so it...\n\n* Doesn't convert to/from XML, CSV, etc\n* Doesn't transmit or receive files\n* Doesn't do encryption\n* Doesn't connect to your database\n* Doesn't transmit over a dial-up modem\n* Doesn't queue messages for delivery or receipt\n* Doesn't generate acknowledgements\n* Doesn't have a graphical interface\n\nThese features are orthogonal to the problem Stupidedi aims to solve, but they\ncan certainly be implemented with other code taking advantage of Stupidedi.\n\n## Alternative libraries\n\nStupidedi is an opinionated library, and maybe you don't agree with\nit. Here are a few alternative libraries:\n\n* https://github.com/rjackson/X12\n* https://github.com/dlabare/ruby-edi\n* https://github.com/pstuteville/x12\n* http://www.appdesign.com/x12parser/\n  * https://github.com/swalberg/x12\n  * https://github.com/mjpete3/x12\n* https://github.com/mrcsparker/x12_parser\n* http://edi4r.rubyforge.org/edi4r/\n* http://edival.sourceforge.net/\n* http://www.edidev.com/\n\n## Examples\n\nIn addition to these brief examples, see sample code in the `notes` directory\nand the human-readable Markdown documentation in `doc`.\n\n### Utilities\n\nPretty print the syntax tree\n\n    $ ./bin/edi-pp spec/fixtures/X222-HC837/1-good.txt\n    ...\n            TableVal[Table 3 - Summary](\n              SegmentVal[SE: Transaction Set Trailer](\n                Nn.value[  E96: Number of Included Segments](45),\n                AN.value[ E329: Transaction Set Control Number](0021)))),\n          SegmentVal[GE: Functional Group Trailer](\n            Nn.value[  E97: Number of Transaction Sets Included](1),\n            Nn.value[  E28: Group Control Number](1))),\n        SegmentVal[IEA: Interchange Control Trailer](\n          Nn.value[  I16: Number of Included Functional Groups](1),\n          Nn.value[  I12: Interchange Control Number](905))))\n    49 segments\n    49 segments\n    0.140 seconds\n\nPerform validation on a file\n\n    $ ./bin/edi-ed spec/fixtures/X222-HC837/1-bad.txt\n    [AK905(file spec/fixtures/X222-HC837/1-bad.txt,\n           line 16, column 4, is not an allowed value,\n           ID.value[ E479: Functional Identifier Code](XX)),\n     IK304(file spec/fixtures/X222-HC837/1-bad.txt,\n           line 33, column 1,\n           missing N4 segment, NM1),\n     IK304(file spec/fixtures/X222-HC837/1-bad.txt,\n           line 35, column 1,\n           missing N4 segment, NM1)]\n    46 segments\n    0.177 seconds\n\n### Generating, Writing\n\n#### X12 Writer\n\n```ruby\nrequire \"stupidedi\"\n\n# You can customize this to delegate to your own grammar definitions, if needed.\nconfig = Stupidedi::Config.hipaa\n\nb = Stupidedi::Parser::BuilderDsl.build(config)\n\n# These methods perform error checking: number of elements, element types, min/max\n# length requirements, conditionally required elements, valid segments, number of\n# segment occurrences, number of loop occurrences, etc.\nb.ISA \"00\", nil, \"00\", nil,\n      \"ZZ\", \"SUBMITTER ID\",\n      \"ZZ\", \"RECEIVER ID\",\n      \"990531\", \"1230\", nil, \"00501\", \"123456789\", \"1\", \"T\", nil\n\n# The API tracks the current position in the specification (e.g., the current loop,\n# table, etc) to ensure well-formedness as each segment is generated.\nb.GS \"HC\", \"SENDER ID\", \"RECEIVER ID\", \"19990531\", \"1230\", \"1\", \"X\", \"005010X222\"\n\n# The `b.default` value can be used to generate the appropriate value if it can\n# be unambigously inferred from the grammar.\nb.ST \"837\", \"1234\", b.default\n  # You can use string representations of data or standard Ruby data types, like Time.\n  b.BHT \"0019\", \"00\", \"X\"*30, \"19990531\", Time.now.utc, \"CH\"\n  b.NM1 b.default, \"1\", \"PREMIER BILLING SERVICE\", nil, nil, nil, nil, \"46\", \"12EEER000TY\"\n  b.PER \"IC\", \"JERRY THE CLOWN\", \"TE\", \"3056660000\"\n\n  b.NM1 \"40\", \"2\", \"REPRICER JONES\", nil, nil, nil, nil, \"46\", \"66783JJT\"\n    b.HL \"1\", nil, \"20\", \"1\"\n\n  b.NM1 \"85\", \"2\", \"PREMIER BILLING SERVICE\", nil, nil, nil, nil, \"XX\", \"123234560\"\n    b.N3  \"1234 SEAWAY ST\"\n    b.N4  \"MIAMI\", \"FL\", \"331111234\"\n    b.REF \"EI\", \"123667894\"\n    b.PER \"IC\", b.blank, \"TE\", \"3056661111\"\n\n  b.NM1 \"87\", \"2\"\n    b.N3 \"2345 OCEAN BLVD\"\n    b.N4 \"MIAMI\", \"FL\", \"33111\"\n\nb.HL \"2\", \"1\", \"22\", \"0\"\n  b.SBR \"S\", \"18\", nil, nil, \"12\", nil, nil, nil, \"MB\"\n\n  b.NM1 \"IL\", \"1\", \"BACON\", \"KEVIN\", nil, nil, nil, \"MI\", \"222334444\"\n    b.N3  \"236 N MAIN ST\"\n    b.N4  \"MIAMI\", \"FL\", \"33413\"\n    b.DMG \"D8\", \"19431022\", \"F\"\n\nb.machine.zipper.tap do |z|\n  # The :component, and :repetition parameters can also be specified as elements\n  # of the ISA segment, at `b.ISA(...)` above. When generating a document from\n  # scratch, :segment and :element must be specified -- if you've parsed the doc\n  # from a file, these params will default to whatever was used in the file, or\n  # you can override them here.\n  separators =\n    Stupidedi::Reader::Separators.build :segment    =\u003e \"~\\n\",\n                                        :element    =\u003e \"*\",\n                                        :component  =\u003e \":\",\n                                        :repetition =\u003e \"^\"\n\n  # You can also serialize any subtree within the document (e.g., everything inside\n  # some ST..SE transaction set, or a single loop. Here, z.root is the entire tree.\n  w = Stupidedi::Writer::Default.new(z.root, separators)\n  print w.write()\nend\n```\n\n#### HTML writer\n\n As shown above `Stupidedi::Writer::Default` will output data encoded in plain x12 format. While `Stupidedi::Writer::Claredi` will output a formatted HTML string.\n\n`Stupidedi::Writer::Claredi#write` operates on `StringIO`.\n\n```ruby\nb.machine.zipper.tap do |z|\n  w = Stupidedi::Writer::Claredi.new(z.root)\n\n  File.open('output.html', 'w') { |f| f.write w.write }\nend\n```\n\n#### Json (Hash)  Writer\n\nConverting the tree to a JSON document is intentionally not included in the library. However this still may be implemented utilizing the stupidedi API.\n\n[Here](https://github.com/irobayna/stupidedi/blob/master/notes/json_writer/json.rb) is one of the possible ways to implement this.\n\nThe shown approach allows to define custom traversing logic for the nodes with ability to change hash keys and values to whatever is needed.\n\nPlease refer to [this readme](https://github.com/irobayna/stupidedi/blob/master/notes/json_writer/json.MD) and [these nodes implementation](https://github.com/irobayna/stupidedi/blob/master/notes/json_writer/json/) for more information.\n\n### Reading, Traversing\n\n```ruby\nrequire \"stupidedi\"\n\nconfig = Stupidedi::Config.hipaa\nparser = Stupidedi::Parser.build(config)\n\ninput  = if RUBY_VERSION \u003e \"1.8\"\n           File.open(\"spec/fixtures/X221-HP835/1-good.txt\", :encoding =\u003e \"ISO-8859-1\")\n         else\n           File.open(\"spec/fixtures/X221-HP835/1-good.txt\")\n         end\n\n# Reader.build accepts IO (File), String, and DelegateInput\nparser, result = parser.read(Stupidedi::Reader.build(input))\n\n# Report fatal tokenizer failures\nif result.fatal?\n  result.explain{|reason| raise reason + \" at #{result.position.inspect}\" }\nend\n\n# Helper function: fetch an element from the current segment\ndef el(m, *ns, \u0026block)\n  if Stupidedi::Either === m\n    m.tap{|m| el(m, *ns, \u0026block) }\n  else\n    yield(*ns.map{|n| m.elementn(n).map(\u0026:value).fetch })\n  end\nend\n\n# Print some information\nparser.first\n  .flatmap{|m| m.find(:GS) }\n  .flatmap{|m| m.find(:ST) }\n  .tap do |m|\n    el(m.find(:N1, \"PR\"), 2){|e| puts \"Payer: #{e}\" }\n    el(m.find(:N1, \"PE\"), 2){|e| puts \"Payee: #{e}\" }\n  end\n  .flatmap{|m| m.find(:LX) }\n  .flatmap{|m| m.find(:CLP) }\n  .flatmap{|m| m.find(:NM1, \"QC\") }\n  .tap{|m| el(m, 3, 4){|l,f| puts \"Patient: #{l}, #{f}\" }}\n```\n\n### Testing\n\n```ruby\nrake spec\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkputnam%2Fstupidedi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkputnam%2Fstupidedi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkputnam%2Fstupidedi/lists"}