openhealthcare/ex_hl7

Name: ex_hl7

Owner: Open Health Care

Description: HL7 Parser for Elixir

Created: 2016-02-04 15:11:55.0

Updated: 2016-02-04 15:11:55.0

Pushed: 2016-02-04 15:18:56.0

Homepage: null

Size: 90

Language: Elixir

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

HL7 Parser for Elixir

Overview

Health Level 7 (HL7) is a protocol (and an organization) designed to model and transfer health-related data electronically.

This parser has support for the HL7 version 2.x syntax. It was tested using v2.4-compliant data, but it should also work with any v2.x messages. It doesn't support the XML mappings that were created for HL7 v3.x, though.

It also has support for custom segment and composite field definitions though an easy-to-use DSL built on top of Elixir macros.

The parser was designed to make the interaction with HL7 as smooth as possible, but its use requires at least moderate knowledge of the HL7 messaging standards.

Example

This is a basic example of a pre-authorization request with referral to another provider (RQA^I08) that shows how to use the parser. For more information, please check the rest of the sections below.

odule Authorizer do
quire HL7.Composite

ias HL7.Segment.AUT
ias HL7.Segment.MSA
ias HL7.Segment.MSH
ias HL7.Segment.PID
ias HL7.Segment.PRD

ias HL7.Composite.CE
ias HL7.Composite.CM_MSH_9
ias HL7.Composite.CP
ias HL7.Composite.EI
ias HL7.Composite.MO

f authorize(req) do
message_type = HL7.segment(req, "MSH").message_type
authorize(req, message_type.id, message_type.trigger_event)
d

f authorize(req, "RQA", "I08") do
msh = HL7.segment(req, "MSH")
msa = %MSA{
        ack_code: "AA",
        message_control_id: msh.message_control_id
      }
msh = %MSH{msh |
        sending_app: msh.receiving_app,
        sending_facility: msh.receiving_facility,
        receiving_app: msh.sending_app,
        receiving_facility: msh.sending_facility,
        message_datetime: :calendar.universal_time(),
        # RPA^I08
        message_type: %CM_MSH_9{msh.message_type | id: "RPA"},
        # Kids, don't try this at home
        message_control_id: Base.encode32(:crypto.rand_bytes(5)),
        accept_ack_type: "ER",
        app_ack_type: "ER"
      }
aut = %AUT{
        plan: %CE{id: "PPO"},
        company: %CE{id: "WA02"},
        company_name: "WSIC (WA State Code)",
        effective_date: {1994, 1, 10},
        expiration_date: {1994, 05, 10},
        authorization: %EI{id: "123456789"},
        reimbursement_limit: %CP{price: %MO{quantity: 175.0, denomination: "USD"}},
        requested_treatments: 1
      }
res = HL7.replace(req, "MSH", msh)
res = HL7.insert_after(res, "MSH", msa)
HL7.insert_after(res, "PR1", 0, aut)
d

f patient(%PID{patient_name: name}) when is_map(name) do
surname = if is_map(name.family_name) do
            name.family_name.surname
          else
            "<unknown>"
          end
"Patient: #{name.given_name} #{surname}"
d
f patient(_pid) do
nil
d

f practice([dg1, pr1]) do
"""
Diagnosed with: #{dg1.description}
Treatment: #{pr1.description}
"""
d

f providers(prds), do:
providers(prds, [])

f providers([%PRD{role: role, name: name, address: address} | tail], acc)
hen is_map(role) and is_map(name) and is_map(address) do
surname = if is_map(name.family_name) do
            name.family_name.surname
          else
            "<unknown>"
          end
info = """
#{role_label(role.id)}:
  #{name.prefix} #{name.given_name} #{surname}
  #{address.street_address}
  #{address.city}, #{address.state} #{address.postal_code}
"""
providers(tail, [info | acc])
d
f providers([_prd | tail], acc) do
providers(tail, acc)
d
f providers([], acc) do
Enum.reverse(acc)
d

f role_label("RP"), do: "By"
f role_label("RT"), do: "And referred to"


rt Authorizer

=
SH|^~\\&|BLAKEMD|EWHIN|MSC|EWHIN|19940110105307||RQA^I08|BLAKEM7898|P|2.4|||NE|AL\r" <>
RD|RP|BLAKE^BEVERLY^^^DR^MD|N. 12828 NEWPORT HIGHWAY^^MEAD^WA^99021| ^^^BLAKEMD&EWHIN^^^^^BLAKE MEDICAL CENTER|BLAKEM7899\r" <>
RD|RT|WSIC||^^^MSC&EWHIN^^^^^WASHINGTON STATE INSURANCE COMPANY\r" <>
ID|||402941703^9^M10||BROWN^CARY^JOE||19600309||||||||||||402941703\r" <>
N1|1|PPO|WA02|WSIC (WA State Code)|11223 FOURTH STREET^^MEAD^WA^99021^USA|ANN MILLER|509)333-1234|987654321||||19901101||||BROWN^CARY^JOE|1|19600309|N. 12345 SOME STREET^^MEAD^WA^99021^USA|||||||||||||||||402941703||||||01|M\r" <>
G1|1|I9|569.0|RECTAL POLYP|19940106103500|0\r" <>
R1|1|C4|45378|Colonoscopy|19940110105309|00\r"

, req} = HL7.read(buf, input_format: :wire)
int authorization request data
|> HL7.segment("PID") |> patient |> IO.puts
|> HL7.paired_segments(["DG1", "PR1"]) |> practice |> IO.puts
|> Enum.filter(&(HL7.segment_id(&1) === "PRD")) |> providers |> IO.puts
eate an authorized response and print it
|> authorize |> HL7.write(output_format: :text, trim: true) |> IO.puts
Requirements

This application was developed and tested using Elixir 1.0.4 (and Erlang 17.5) but there shouldn't be any special dependency that prevents it from working with other versions.

There are no dependencies on external projects. The parser will make use of the Logger application included in Elixir to output warnings when reading or writing to fields that are not present in the corresponding segment's definition.

Installation

You can use ex_hl7 in your projects by adding it to your mix.exs dependencies:

deps do
:ex_hl7, "~> 0.1.3"},

And then listing it as part of your application's dependencies:

application do
pplications: [:ex_hl7]]

Contributing

Only a small subset of the HL7 segments and composite fields are included in the project. You can always roll your own definitions in your project, but if you feel your changes would help others, please fork the repository, add whatever you need and send a pull-request.

Encoding Rules

An HL7 message in its v2.x wire-format is actually a collection of concatenated segments, each terminated by a carriage-return (0x13) character. Each segment is a collection of fields separated by a custom separator character (| by default). Depending on the type of the field, each field can have multiple optional repetitions (separated by ~ by default), can be made out of multiple components (separated by ^ by default) where each of them can also have subcomponents (separated by & by default).

This structure maps nicely to a k-ary tree. For example, given the following segment:

OBX|1|CE|71020&IMP|1|.61^RUL^ACR~.212^Bronchopneumonia^ACR\r

We could represent it as the following subtree within a message:

ent                            OBX
                                |
ds        [1]--[2]--------[3]---+------[4]------------[5]
          /     |          |            |               \
         1     "CE"        |           "1"               |
                           |                             |
onents                    [0]                [0]--------[1]---------[2]
                           |                 /           |            \
                           |               0.61        "RUL"         "ACR"
                           |              0.212  "Bronchopneumonia"  "ACR"
omponents             [0]--+--[1]
                      /         \
                  "71020"      "IMP"

The field on sequence 5 contains two repetitions of a composite field.

Note: the indexes used for the fields are 1-based because this value is actually the sequence number assigned by HL7 to identify the field, whereas the indexes used for components and subcomponents are 0-based because this is the convention in Elixir.

The input and output of the high level functions used to read or write a message (e.g. HL7.read/2, HL7.write/2) is affected by boolean argument named trim. This value changes the input and output from the lower level functions of the parser. If set to true, some trailing optional items and separators will be omitted from the decoded or encoded message.

For example, a field that was originally read as:

504599^223344&&IIN&^~

Would be written in the following way when trim is set to true:

504599^223344&&IIN

Both representations are correct, given that HL7 allows trailing items that are empty to be omitted.

Single Value Fields

HL7 supports many types of single value (scalar, non-composite) fields. This parser maps all of them (including those that are identifiers in a table) to a few data types:

Note: there is no support for the full HL7 date/time format yet, as there is no standard way to represent times with subsecond precision and timezones in Elixir.

Composite Fields

HL7 supports many types of composite fields and not all of them are included in this project, so to simplify their use there are some macros that help you easily define new ones.

This parser exposes composite fields as structs and, given the following definition from the HL7 standard:

2.9.3 CE - coded element

<identifier (ST)> ^ <text (ST)> ^ <name of coding system (IS)> ^
<alternate identifier (ST)> ^ <alternate text (ST)> ^
<name of alternate coding system (IS)>

They can be defined in the following way:

HL7.Composite.Def

odule HL7.Composite.CE do
mposite do
component :id,                type: :string
component :text,              type: :string
component :coding_system,     type: :string
component :alt_id,            type: :string
component :alt_text,          type: :string
component :alt_coding_system, type: :string
d

This composite will be exposed as the following struct:

truct :id, :text, :coding_system, :alt_id, :alt_text, :alt_coding_system

Each component has a name represented by an atom with the following properties:

Composite fields can also be nested, and you can do it in the following way:

HL7.Composite.Def

odule HL7.Composite.CQ do
mposite do
component :quantity,          type: :integer
component :units,             type: CE
d

Segments

As with composite fields, not all HL7 segments are provided with the project, so there is also a set of macros that help define new segments.

Segments are also exposed as structs and can be defined in this way:

HL7.Segment.Def

odule HL7.Segment.OBX do
ias HL7.Composite.CE

gment "OBX" do
field :set_id,             seq:  1, type: :integer,  length:  4
field :value_type,         seq:  2, type: :string,   length: 10
field :observation_id,     seq:  3, type: CE,        length: 24
field :observation_sub_id, seq:  4, type: :string,   length: 20
field :observation_value,  seq:  5, type: CE,        length: 24
field :observation_status, seq: 11, type: :string,   length:  1
d

This segment will be exposed as the following struct:

truct :set_id, :value_type, :observation_id, :observation_sub_id,
      :observation_value, :observation_status

Each field has a name represented by an atom and has the following properties:

Note: not all of the fields need to be defined in a segment. Segments can be “sparse” and the fields can be defined in an order that is not their sequence order. This means that if a segment containing an undefined field is parsed, that field will be lost when writing/serializing the segment back to its wire-format.

Messages

A parsed HL7 message is represented as a list of segment structs, so you can use the functions from the Enum and List modules to retrieve data or modify them.

The HL7 module has several functions that can be used with messages. The examples below assume that the following HL7 message is being used:

er =
SH|^~\\&|BLAKEMD|EWHIN|MSC|EWHIN|19940110105307||RQA^I08|BLAKEM7898|P|2.4|||NE|AL\r" <>
RD|RP|BLAKE^BEVERLY^^^DR^MD|N. 12828 NEWPORT HIGHWAY^^MEAD^WA^99021| ^^^BLAKEMD&EWHIN^^^^^BLAKE MEDICAL CENTER|BLAKEM7899\r" <>
RD|RT|WSIC||^^^MSC&EWHIN^^^^^WASHINGTON STATE INSURANCE COMPANY\r" <>
ID|||402941703^9^M10||BROWN^CARY^JOE||19600309||||||||||||402941703\r" <>
N1|1|PPO|WA02|WSIC (WA State Code)|11223 FOURTH STREET^^MEAD^WA^99021^USA|ANN MILLER|509)333-1234|987654321||||19901101||||BROWN^CARY^JOE|1|19600309|N. 12345 SOME STREET^^MEAD^WA^99021^USA|||||||||||||||||402941703||||||01|M\r" <>
G1|1|I9|569.0|RECTAL POLYP|19940106103500|0\r" <>
R1|1|C4|45378|Colonoscopy|19940110105309|00\r"

You can read/parse a message from a binary in the following way:

, message} = HL7.read(buffer)

Retrieve a specific repetition of a segment:

s HL7.Segment.PRD

{role: role} = prd = HL7.segment(message, "PRD", 1)
" = HL7.segment_id(prd)
 = role.id

Insert segments:

s HL7.Segment.PR1
s HL7.Segment.AUT
s HL7.Composite.CE
s HL7.Composite.EI

= HL7.segment(message, "PR1")
= %AUT{plan: %CE{id: "PPO"}, company: %CE{id: "WA02"},
       effective_date: {1994, 1, 10},
       expiration_date: {1994, 05, 10},
       authorization: %EI{id: "123456789"}}
age = HL7.insert_before(message, "PR1", 0, [pr1, aut])
age = HL7.insert_after(message, "PR1", 1, aut)

Replace segments:

age = HL7.replace(message, "PR1", 0, %PR1{pr1 | set_id: 2})

Delete segments:

age = HL7.delete(message, "PR1", 1)
age = HL7.delete(message, "AUT", 1)

Write a message into the HL7 wire format:

f = HL7.write(message, output_format: :wire, trim: true)

Write a message as text to standard output:

uts(HL7.write(message, output_format: :text, trim: true))

This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.