Current section
29 Versions
Jump to
Current section
29 Versions
Compare versions
7
files changed
+385
additions
-329
deletions
| @@ -1,213 +1,34 @@ | |
| 1 1 | # Elixir HL7 |
| 2 | + |
| 2 3 | [](https://hex.pm/packages/elixir_hl7) |
| 3 4 | [](https://hex.pm/packages/elixir_hl7) |
| 4 5 | [](https://hex.pm/packages/elixir_hl7) |
| 5 6 | |
| 6 | - An Elixir library for working with HL7 v2.x healthcare data |
| 7 | + An Elixir library for working with HL7 v2.x healthcare data. |
| 7 8 | |
| 8 9 | Elixir HL7 provides functions to parse, query and modify healthcare data that conforms to the HL7 v2.x standards. |
| 9 | - It should be able to reconstruct any HL7 Message without data loss or corruption. |
| 10 | + It should be able to reconstruct any HL7 message without data loss or corruption. |
| 10 11 | |
| 11 12 | It also provides basic support for reading HL7 file streams with configurable delimiters (MLLP included). |
| 12 13 | |
| 13 | - This library has been tested on a fairly wide variety of real-world HL7 messages to ensure correctness and flexibility. |
| 14 | + This library has been tested on a wide variety of real-world HL7 messages to ensure correctness and flexibility. |
| 15 | + |
| 16 | + Since many HL7 messages do not strictly conform to the standards specified for each version, the library does not |
| 17 | + attempt to enforce limits such as character counts or structural expectations. |
| 18 | + |
| 19 | + In fact, HL7 uses implicit hierarchies within segments (by leaving out certain separators) and to group segments |
| 20 | + (via expected patterns only known to the consuming application). |
| 21 | + |
| 22 | + The majority of the API currently resides in the eponymous `HL7` module. |
| 14 23 | |
| 15 24 | You can learn more about HL7 here: |
| 25 | + * Wikipedia's [HL7 article](https://en.wikipedia.org/wiki/Health_Level_7) |
| 16 26 | * The official HL7 website ([hl7.org](http://www.hl7.org/index.cfm)) |
| 17 | - * Wikipedia's [HL7 article](https://en.wikipedia.org/wiki/Health_Level_7) |
| 27 | + |
| 28 | + Full documentation is available at [hex.pm](https://hexdocs.pm/elixir_hl7/readme.html). |
| 18 29 | |
| 19 30 | Please [report an issue](https://github.com/HCA-Healthcare/elixir-hl7/issues) if something appears to be handled incorrectly. |
| 20 31 | |
| 21 | - > ### Note {: .warning} |
| 22 | - > |
| 23 | - > |
| 24 | - > We are building a simpler and more Elixir-friendly API for this library, centered on |
| 25 | - > the `HL7` and `HL7.Path` modules. |
| 26 | - > |
| 27 | - > The new API will lead to the deprecation of `HL7.Query` and related code. |
| 28 | - > |
| 29 | - > These will likely not be removed for some time, and their |
| 30 | - > removal will coincide with a major version release. For now, the two systems can exchange data when needed. |
| 31 | - |
| 32 | - ## Getting started |
| 33 | - |
| 34 | - Add this library to your mix.exs file: |
| 35 | - |
| 36 | - ```elixir |
| 37 | - defp deps do |
| 38 | - [{:elixir_hl7, "~> 0.9.2"}] |
| 39 | - end |
| 40 | - ``` |
| 41 | - |
| 42 | - ## Use the Tabs Below to Explore the Library |
| 43 | - |
| 44 | - <!-- tabs-open --> |
| 45 | - |
| 46 | - ### Examples |
| 47 | - |
| 48 | - The `HL7.Examples` module contains functions with sample data that you can use in unit tests or to explore the API, |
| 49 | - |
| 50 | - iex> hl7_text = HL7.Examples.wikipedia_sample_hl7() |
| 51 | - ...> to_string() |
| 52 | - ...> Enum.take(30) |
| 53 | - "ADT" |
| 54 | - |
| 55 | - ### Route |
| 56 | - |
| 57 | - HL7 messages can be minimally validated to gather header (MSH segment) information for quick routing and acknowledgements. |
| 58 | - |
| 59 | - iex> hl7_text = HL7.Examples.wikipedia_sample_hl7() |
| 60 | - ...> raw_msg = HL7.Message.raw(hl7_text) |
| 61 | - ...> raw_msg.header.message_type |
| 62 | - "ADT" |
| 63 | - |
| 64 | - See the `HL7.RawMessage` and `HL7.Header` modules for more information. |
| 65 | - |
| 66 | - ### Parse |
| 67 | - |
| 68 | - HL7 messages can be fully parsed into lists of lists and strings to provide a compact representation of the message structure. |
| 69 | - |
| 70 | - One could grab the 2nd segment in a message: |
| 71 | - |
| 72 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 73 | - ...> |> HL7.Message.to_list() |
| 74 | - ...> |> Enum.at(1) |
| 75 | - ["EVN", "", "200605290901", "", "", "", "200605290900"] |
| 76 | - |
| 77 | - Or get the field value of RXA-5.2 (finding the first RXA segment's 5th field, 1st repetition, 2nd component): |
| 78 | - |
| 79 | - iex> HL7.Examples.nist_immunization_hl7() |
| 80 | - ...> |> HL7.Message.find("RXA") |
| 81 | - ...> |> HL7.Segment.get_part(5, 1, 2) |
| 82 | - "Influenza" |
| 83 | - |
| 84 | - It's also possible to modify the data within a segment (but it is much easier to manipulate messages using the `HL7.Query` module): |
| 85 | - |
| 86 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 87 | - ...> |> HL7.Message.find("EVN") |
| 88 | - ...> |> HL7.Segment.replace_part("extra data", 5, 2, 1) |
| 89 | - [ |
| 90 | - "EVN", |
| 91 | - "", |
| 92 | - "200605290901", |
| 93 | - "", |
| 94 | - "", |
| 95 | - ["", "", ["", "extra data"]], |
| 96 | - "200605290900" |
| 97 | - ] |
| 98 | - |
| 99 | - See the `HL7.Message` and `HL7.Segment` modules for more information. |
| 100 | - |
| 101 | - ### Query |
| 102 | - |
| 103 | - Advanced manipulation and analysis of HL7 messages can be performed with the `HL7.Query` module. |
| 104 | - |
| 105 | - It supports a pipeline-friendly API modeled after jQuery and D3, allowing set-based document operations and queries. |
| 106 | - |
| 107 | - Messages can be broken into groups using a segment selector (similar to a CSS selector string) that denotes optional and repeating segments in potentially nested hierarchies. |
| 108 | - |
| 109 | - Individual segments can be decomposed using a field selector to reference specific field, repetition, component and subcomponent indices. |
| 110 | - |
| 111 | - Note that all `HL7.Query` selections implicitly retain the entire message structure such that elements can be selected, modified and then used to reconstruct a new HL7 message. |
| 112 | - |
| 113 | - The act of selecting something never modifies the content of a message. Applying other methods to a selection, such as delete, filter, append, etc., does modify the actual message content. |
| 114 | - |
| 115 | - For instance, this would select all textual diagnoses (DG1-3.2) associated with a patient visit (PV1): |
| 116 | - |
| 117 | - iex> import HL7.Query |
| 118 | - iex> HL7.Examples.nist_syndromic_hl7() |
| 119 | - ...> |> select("PV1 [{DG1}]") |
| 120 | - ...> |> select("DG1") |
| 121 | - ...> |> find_all(~p"3.2") |
| 122 | - ["Cryptosporidiosis", "Dehydration", "Diarrhea"] |
| 123 | - |
| 124 | - The statement `select("PV1 [{DG1}]")` grabs a list of segment groups containing a PV1 segment and any DG1 segments that might follow it. |
| 125 | - Thus, we would select one group containing a PV1 segment and three DG1 segments. |
| 126 | - |
| 127 | - ```elixir |
| 128 | - [ |
| 129 | - [ # 1 group of 4 segments |
| 130 | - ["PV1", "1", "(data truncated to fit)"], |
| 131 | - ["DG1", "1", "", [["0074", "Cryptosporidiosis", "I9CDX"]], "", "", "F"], |
| 132 | - ["DG1", "2", "", [["27651", "Dehydration", "I9CDX"]], "", "", "F"], |
| 133 | - ["DG1", "3", "", [["78791", "Diarrhea", "I9CDX"]], "", "", "F"] |
| 134 | - ] |
| 135 | - ] |
| 136 | - ``` |
| 137 | - |
| 138 | - Then `select("DG1")` creates three groups of individual DG1 segments by searching within the confines of the prior selection. |
| 139 | - |
| 140 | - ```elixir |
| 141 | - [ # 3 groups of 1 segment each |
| 142 | - [["DG1", "1", "", [["0074", "Cryptosporidiosis", "I9CDX"]], "", "", "F"]], |
| 143 | - [["DG1", "2", "", [["27651", "Dehydration", "I9CDX"]], "", "", "F"]], |
| 144 | - [["DG1", "3", "", [["78791", "Diarrhea", "I9CDX"]], "", "", "F"]] |
| 145 | - ] |
| 146 | - ``` |
| 147 | - |
| 148 | - Finally, `find_all(~p"3.2")` will return a flattened list of data containing the 3rd field, 1st repetition (the default), 2nd component for each |
| 149 | - selected segment. |
| 150 | - |
| 151 | - ```elixir |
| 152 | - ["Cryptosporidiosis", "Dehydration", "Diarrhea"] |
| 153 | - ``` |
| 154 | - |
| 155 | - Alternately, one could select and remove every diagnosis tied to a patient visit and then output a modified HL7 message: |
| 156 | - |
| 157 | - iex> import HL7.Query |
| 158 | - iex> HL7.Examples.nist_syndromic_hl7() |
| 159 | - ...> |> select("PV1 [{DG1}]") |
| 160 | - ...> |> select("DG1") |
| 161 | - ...> |> delete() |
| 162 | - ...> |> to_string() |
| 163 | - "MSH|^~\\&||LakeMichMC^9879874000^NPI|||201204020040||ADT^A03^ADT_A03|NIST-SS-003.32|P|2.5.1|||||||||PH_SS-NoAck^SS Sender^2.16.840.1.114222.4.10.3^ISO\rEVN||201204020030|||||LakeMichMC^9879874000^NPI\rPID|1||33333^^^^MR||^^^^^^~^^^^^^S|||F||2106-3^^CDCREC|^^^^53217^^^^55089|||||||||||2186-5^^CDCREC\rPV1|1||||||||||||||||||33333_001^^^^VN|||||||||||||||||09||||||||201204012130\rOBX|1|CWE|SS003^^PHINQUESTION||261QE0002X^Emergency Care^NUCC||||||F\rOBX|2|NM|21612-7^^LN||45|a^^UCUM|||||F\rOBX|3|CWE|8661-1^^LN||^^^^^^^^Diarrhea, stomach pain, dehydration||||||F\r" |
| 164 | - |
| 165 | - The following query extracts each Common Order (ORC) group's OBX segments and outputs a list of each order's associated vaccine types. |
| 166 | - |
| 167 | - iex> import HL7.Query |
| 168 | - iex> HL7.Examples.nist_immunization_hl7() |
| 169 | - ...> |> select("ORC [RXA] [RXR] {OBX}") |
| 170 | - ...> |> filter(fn q -> find_first(q, ~p"3.2") == "vaccine type" end) |
| 171 | - ...> |> map(fn q -> find_all(q, ~p"5.2") end) |
| 172 | - [ |
| 173 | - ["Influenza, unspecified formulation"], |
| 174 | - ["DTaP", "Polio", "Hep B, unspecified formulation"] |
| 175 | - ] |
| 176 | - |
| 177 | - |
| 178 | - See the `HL7.Query` module for more information. |
| 179 | - |
| 180 | - |
| 181 | - |
| 182 | - ## Create |
| 183 | - |
| 184 | - HL7 messages can be constructed from scratch with the `HL7.Message` module. Passing an `HL7.Header` struct to |
| 185 | - `HL7.Message.new/1` will produce a base message upon which you can add additional segments. These can be appended as list data. |
| 186 | - |
| 187 | - The final raw message can be produced by invoking the `to_string/1` protocol on either the `HL7.Query` or `HL7.Message` structs. |
| 188 | - |
| 189 | - <!-- tabs-close --> |
| 190 | - |
| 191 | - |
| 192 | - ## Files |
| 193 | - |
| 194 | - The `HL7` module contains utility functions to open file streams of HL7 message content with support for MLLP and standard `:line` storage. |
| 195 | - Other formats are somewhat supported by specifying expected prefix and suffix delimiters between messages. |
| 196 | - |
| 197 | - ## Sockets |
| 198 | - |
| 199 | - A separate library, Elixir-MLLP, exists to manage MLLP connections. MLLP is a simple protocol on top of TCP that is commonly used for sending and receiving HL7 messages. |
| 200 | - |
| 201 | - ## Status |
| 202 | - |
| 203 | - This project is approaching a v1.0 release. The API is mostly stable at this point. |
| 204 | - |
| 205 | - Also, please be aware of the details of the license (Apache 2.0). |
| 206 | - |
| 207 | - # Roadmap |
| 208 | - |
| 209 | - Extending the `HL7` base namespace to provide a simpler and more powerful API. |
| 210 | - |
| 211 32 | # License |
| 212 33 | |
| 213 | - Elixir-HL7 source code is released under Apache 2 License. Check the LICENSE file for more information. |
| 34 | + Elixir HL7 source code is released under Apache 2 License. Check the LICENSE file for more information. |
| @@ -1 +1 @@ | |
| 1 | - 0.9.3 |
| \ No newline at end of file | |
| 1 | + 0.10.0 |
| \ No newline at end of file |
| @@ -1,10 +1,10 @@ | |
| 1 1 | {<<"links">>, |
| 2 2 | [{<<"GitHub">>,<<"https://github.com/HCA-Healthcare/elixir-hl7">>}]}. |
| 3 3 | {<<"name">>,<<"elixir_hl7">>}. |
| 4 | - {<<"version">>,<<"0.9.3">>}. |
| 4 | + {<<"version">>,<<"0.10.0">>}. |
| 5 5 | {<<"description">>, |
| 6 6 | <<"An Elixir library for working with HL7 v2.x healthcare data">>}. |
| 7 | - {<<"elixir">>,<<"~> 1.14">>}. |
| 7 | + {<<"elixir">>,<<"~> 1.18">>}. |
| 8 8 | {<<"app">>,<<"elixir_hl7">>}. |
| 9 9 | {<<"files">>, |
| 10 10 | [<<"lib">>,<<"lib/hl7.ex">>,<<"lib/hl7">>,<<"lib/hl7/invalid_header.ex">>, |
| @@ -1,50 +1,109 @@ | |
| 1 1 | defmodule HL7 do |
| 2 2 | @moduledoc """ |
| 3 | - Functions to manipulate and query HL7 documents -- plus a way to handle HL7 file streams. |
| 3 | + |
| 4 | + Struct and functions for parsing and manipulating HL7 messages. |
| 5 | + |
| 6 | + This library specifically handles version 2.x of HL7 as it is by far the most prevalent format in production. |
| 7 | + |
| 8 | + Check out [HL7 on Wikipedia](https://en.wikipedia.org/wiki/Health_Level_7) for a decent overview of the format. |
| 9 | + |
| 10 | + Since HL7 messages often lack critical contextual metadata, this struct also contains a `tags` field for metadata support. |
| 11 | + |
| 12 | + Use `new/2` or `new!/2` to convert HL7 text to a parsed HL7 struct. |
| 13 | + This struct supports the `String.Chars` protocol such that `to_string/1` can be used to format the HL7 message text. |
| 14 | + |
| 15 | + To see the parsed representation, call `get_segments/1`. |
| 16 | + |
| 17 | + To query or update HL7 data, use the `sigil_p/2` macro to provide an `HL7.Path` with compile-time guarantees. |
| 18 | + For dynamic path access, use `HL7.Path.new/1` to construct paths on the fly. |
| 19 | + Note that HL7 path formats have been designed to reflect common industry usage. |
| 20 | + |
| 21 | + The `get/2`, `put/3`, and `update/4` functions are designed to query and manipulate HL7 data as an HL7 struct (containing a message), |
| 22 | + a list of segments, a single segment, a list of repetitions, or a single repetition. These should handle data as |
| 23 | + nested lists (automatically converted to 1-indexed maps), nested sparse maps (1-indexed), simple strings, or mixes of each. |
| 24 | + |
| 25 | + Use `set_segments/2` to fully replace the content an HL7 message. |
| 26 | + |
| 27 | + > ### Migrating from HL7.Message, HL7.Segment and HL7.Query {: .tip} |
| 28 | + > To migrate from the deprecated `HL7.Message` struct, use `HL7.new!/2` and `HL7.Message.new/2` to transform from one |
| 29 | + > struct to the other while preserving associated metadata tags. |
| 30 | + > |
| 31 | + > You can use `chunk_by_lead_segment/3` to generate segment groups to update code that relies on `HL7.Query` groupings. |
| 32 | + > |
| 33 | + > Any operations to otherwise query or modify HL7 data should be possible using |
| 34 | + > the `get/2`, `put/3`, `update!/3` and `update/4` functions. |
| 35 | + > |
| 36 | + > If you encounter other issues with feature parity, please open an issue! |
| 37 | + |
| 38 | + > ### String.Chars Protocol {: .tip} |
| 39 | + > You can use the `to_string()` implementation of the `String.Chars` protocol to quickly render HL7 structs as text. |
| 40 | + |
| 4 41 | """ |
| 5 42 | |
| 6 | - defstruct segments: [] |
| 43 | + defstruct segments: [], tags: %{} |
| 7 44 | |
| 8 45 | @buffer_size 32768 |
| 9 46 | @type file_type_hl7 :: :mllp | :line | nil |
| 10 47 | |
| 11 48 | @type hl7_map_data() :: %{optional(non_neg_integer) => hl7_map_data() | String.t()} |
| 12 | - @type hl7_list_data() :: String.t() | [hl7_list_data] |
| 49 | + @type hl7_list_data() :: String.t() | [hl7_list_data()] |
| 13 50 | |
| 14 51 | @type segment() :: %{ |
| 15 52 | 0 => String.t(), |
| 16 53 | optional(pos_integer) => hl7_map_data() | String.t() |
| 17 54 | } |
| 18 55 | |
| 19 | - @type t() :: %__MODULE__{segments: [segment()]} |
| 56 | + @type t() :: %__MODULE__{tags: map(), segments: [segment()]} |
| 20 57 | |
| 21 | - @type parsed_hl7_segments :: t() | [segment()] |
| 22 58 | @type parsed_hl7 :: t() | segment() | [segment()] | hl7_map_data() |
| 23 59 | |
| 24 60 | alias HL7.Path |
| 25 61 | |
| 26 62 | @doc ~S""" |
| 27 63 | The `~p` sigil encodes an HL7 path into a struct at compile-time to guarantee correctness and speed. |
| 28 | - It is designed to work with data returned by `HL7.new!/1`. |
| 64 | + It is designed to work with data returned by `HL7.new!/1`, providing a standard way to get and update |
| 65 | + HL7 message content. |
| 66 | + |
| 67 | + > ### Importing Just the Sigil {: .tip} |
| 68 | + > Use `import HL7, only: :sigils` to access the `~p` sigil without importing the other `HL7` functions. |
| 29 69 | |
| 30 70 | The full path structure in HL7 is expressed as: |
| 31 71 | |
| 32 72 | `~p"SEGMENT_NAME[SEGMENT_NUMBER]-FIELD[REPETITION].COMPONENT.SUBCOMPONENT` |
| 33 73 | |
| 34 | - `SEGMENT_NAME` -- 3 character string |
| 35 | - `SEGMENT_NUMBER` -- Positive integer in brackets, defaulting to 1. All segments of the name can be accessed with `[*]` |
| 36 | - `FIELD` -- Positive integer |
| 37 | - `REPETITION` -- Positive integer in brackets, defaulting to 1. All repetitions of the field can be accessed with `[*]` |
| 38 | - `COMPONENT` -- Positive integer |
| 39 | - `SUBCOMPONENT` -- Positive integer |
| 74 | + A trailing exclamation mark can be used in the path to the return only the leftmost text at the given level. |
| 75 | + |
| 76 | + Position Name | Valid Values |
| 77 | + ------------ | ------------- |
| 78 | + `SEGMENT_NAME` | 3 character string |
| 79 | + `SEGMENT_NUMBER` | Positive integer in square brackets, defaults to 1. All segments of `SEGMENT_NAME` can be accessed with `[*]` |
| 80 | + `FIELD` | Positive integer |
| 81 | + `REPETITION` | Positive integer in square brackets, defaults to 1. All repetitions of the `FIELD` can be accessed with `[*]` |
| 82 | + `COMPONENT` | Positive integer |
| 83 | + `SUBCOMPONENT` | Positive integer |
| 84 | + |
| 85 | + > ### 1-based Indexes {: .warning} |
| 86 | + > To match industry expectations, HL7 uses 1-based indexes. |
| 87 | + > As noted above, it also includes defaults whereby all paths refer to the 1st segment and/or 1st repetition |
| 88 | + > of any query unless explicitly specified. |
| 40 89 | |
| 41 90 | Example paths: |
| 42 91 | |
| 43 | - `~p"OBX"` refers to the 1st OBX segment in its entirety, the same as `~p"OBX[1]"`. |
| 44 | - `~p"OBX-5"` means the 1st repetition of the 5th field of the 1st OBX segment. |
| 45 | - `~p"OBX[1]-5[1]"` is equivalent, the numbers in brackets specifying the default repetition and segment number values. |
| 46 | - `~p"OBX[2]-5"` thus means 1st repetition of the 5th field of the 2nd OBX segment. |
| 47 | - `~p"OBX[*]-5"` would get the 1st repetition of the 5th field of every OBX segment, returning a list of results. |
| 92 | + HL7 Path | Description |
| 93 | + `~p"OBX"` | The 1st OBX segment in its entirety, the same as `~p"OBX[1]"`. |
| 94 | + `~p"OBX-5"` | The 1st repetition of the 5th field of the 1st OBX segment. |
| 95 | + `~p"OBX[1]-5[1]"` | Same as above. The numbers in brackets specify the default repetition and segment number values. |
| 96 | + `~p"OBX-5[*]"` | Every repetition of the 5th field of the 1st OBX segment, returning a list of results. |
| 97 | + `~p"OBX[2]-5"` | The 1st repetition of the 5th field of the 2nd OBX segment. |
| 98 | + `~p"OBX[*]-5"` | The 1st repetition of the 5th field of every OBX segment, returning a list of results. |
| 99 | + `~p"OBX[*]-5[*]"` | Every repetition of the 5th field of every OBX segment, returning a nested list of results. |
| 100 | + `~p"OBX-5.2"` | The 2nd component of the 1st repetition of the 5th field of the 1st OBX segment. |
| 101 | + `~p"OBX-5.2.3"` | The 3rd subcomponent of the 2nd component of the 1st repetition of the 5th field of the 1st OBX segment. |
| 102 | + `~p"OBX[*]-5.2.3"` | Same as above, but returned as a list with a value for each OBX segment. |
| 103 | + `~p"OBX[*]-5[*].2.3"` | Same as above, but now a nested list of results for each repetition within each segment. |
| 104 | + `~p"OBX-5!"` | The `!` will take the first text (leftmost value) at whatever level is specified. Thus, `p"OBX-5!"` is equivalent to `p"OBX-5.1.1"`. |
| 105 | + `~p"5"` | The fifth field of a segment (parsed as an ordinal map starting with 0) |
| 106 | + `~p".2"` | The second component of a repetition (parsed as an ordinal map starting with 1) |
| 48 107 | |
| 49 108 | Note that repetitions are uncommon in HL7 and the default of a 1st repetition is often just assumed. |
| 50 109 | `~p"PID-3"` is equivalent to `~p"PID-3[1]"` and is the most standard representation. |
| @@ -53,61 +112,63 @@ defmodule HL7 do | |
| 53 112 | be produced by selecting multiple segments and multiple repetitions with `~p"PID[*]-11[*]"`. |
| 54 113 | |
| 55 114 | Components and subcomponents can also be accessed with the path structures. |
| 56 | - `h"OBX-2.3.1"` would return the 1st subcomponent of the 3rd component of the 2nd field of the 1st OBX. |
| 115 | + `p"OBX-2.3.1"` would return the 1st subcomponent of the 3rd component of the 2nd field of the 1st OBX. |
| 116 | + |
| 117 | + If dealing with segments or repetitions extracted from parsed HL7, you can use partial paths that |
| 118 | + lack the segment name and/or field like `~p"5"` for the fifth field of a segment or `~p".3"` for the 3rd |
| 119 | + component of a repetition. |
| 57 120 | |
| 58 121 | Additionally, if a path might have additional data such that a string might be found at either |
| 59 | - `~p"OBX-2"` or `~p"OBX-2.1"` or even `~p"OBX-2.1.1"`, there is truncation character (the bang symbol) that |
| 122 | + `~p"OBX-2"` or `~p"OBX-2.1"` or even `~p"OBX-2.1.1"`, there is truncation character (`!`) that |
| 60 123 | will return the first element found in the HL7 text at the target specificity. Thus, `~p"OBX[*]-2!"` |
| 61 124 | would get the 1st piece of data in the 2nd field of every OBX whether it is a string or nested map. |
| 62 125 | |
| 63 | - Lastly, when accessing data from a specific repetition, the path should begin with `.`; for example, |
| 64 | - when working with the Nth OBX segment and using `HL7.get/2` to access that segment's second field, first |
| 65 | - component, use the path `~p".2.1"` |
| 126 | + > ### Nil vs Empty String {: .tip} |
| 127 | + > Paths that query beyond the content of an HL7 document, e.g. asking for the 10th field of a segment with five fields, |
| 128 | + > will return `nil` as opposed to an empty string to indicate that the data does not exist. Adding the |
| 129 | + > trailing `!` to an `HL7.Path` will force all return values to be simple strings in cases where `nil` is not desired. |
| 130 | + > Note that it also returns the leftmost text at the path level, discarding extra data as noted above. |
| 66 131 | |
| 67 132 | ## Examples |
| 68 133 | |
| 69 | - iex> import HL7 |
| 70 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 71 | - ...> |> HL7.new!() |
| 72 | - ...> |> HL7.get(~p"OBX-5") |
| 134 | + iex> import HL7, only: :sigils |
| 135 | + iex> HL7.Examples.wikipedia_sample_hl7() |> HL7.new!() |> HL7.get(~p"OBX-5") |
| 73 136 | "1.80" |
| 74 137 | |
| 75 138 | iex> import HL7 |
| 76 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 77 | - ...> |> HL7.new!() |
| 78 | - ...> |> HL7.get(~p"OBX[*]-5") |
| 139 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |> get(~p"OBX-3") |
| 140 | + %{2 => "Body Height"} |
| 141 | + |
| 142 | + iex> import HL7 |
| 143 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |> get(~p"PID-11!") |
| 144 | + "260 GOODWIN CREST DRIVE" |
| 145 | + |
| 146 | + iex> import HL7 |
| 147 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |> get(~p"OBX[*]-5") |
| 79 148 | ["1.80", "79"] |
| 80 149 | |
| 81 150 | iex> import HL7 |
| 82 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 83 | - ...> |> HL7.new!() |
| 84 | - ...> |> HL7.get(~p"OBX[*]-2!") |
| 151 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |> get(~p"OBX[*]-2!") |
| 85 152 | ["N", "NM"] |
| 86 153 | |
| 87 154 | iex> import HL7 |
| 88 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 89 | - ...> |> HL7.new!() |
| 90 | - ...> |> HL7.get(~p"PID-11[*].5") |
| 155 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |> get(~p"PID-11[*].5") |
| 91 156 | ["35209", "35200"] |
| 92 157 | |
| 93 158 | iex> import HL7 |
| 94 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 95 | - ...> |> HL7.new!() |
| 96 | - ...> |> HL7.get(~p"PID[*]-11[*].5") |
| 159 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |> get(~p"PID[*]-11[*].5") |
| 97 160 | [["35209", "35200"]] |
| 98 161 | |
| 99 162 | iex> import HL7 |
| 100 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 101 | - ...> |> HL7.new!() |
| 102 | - ...> |> HL7.get(~p"PID-11[2].1") |
| 163 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |> get(~p"PID-11[2].1") |
| 103 164 | "NICKELL’S PICKLES" |
| 104 165 | |
| 105 166 | iex> import HL7 |
| 106 167 | iex> HL7.Examples.wikipedia_sample_hl7() |
| 107 | - ...> |> HL7.new!() |
| 108 | - ...> |> HL7.get(~p"PID-11[*]") |
| 168 | + ...> |> new!() |
| 169 | + ...> |> get(~p"PID-11[*]") |
| 109 170 | ...> |> List.last() |
| 110 | - ...> |> HL7.get(~p".1") |
| 171 | + ...> |> get(~p".1") |
| 111 172 | "NICKELL’S PICKLES" |
| 112 173 | """ |
| 113 174 | defmacro sigil_p({:<<>>, _, [path]}, _modifiers) do |
| @@ -117,10 +178,23 @@ defmodule HL7 do | |
| 117 178 | end |
| 118 179 | |
| 119 180 | @doc ~S""" |
| 120 | - Creates an HL7 struct from HL7 data (accepting text, lists or the deprecated `HL7.Message` struct). |
| 181 | + Creates an HL7 struct from valid HL7 data (accepting text, lists or the deprecated `HL7.Message` struct). |
| 182 | + Raises with a `RuntimeError` if the data is not valid HL7. |
| 183 | + |
| 184 | + ## Examples |
| 185 | + |
| 186 | + iex> import HL7 |
| 187 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |
| 188 | + #HL7<with 8 segments> |
| 189 | + |
| 121 190 | """ |
| 122 | - @spec new!(hl7_list_data() | String.t() | HL7.Message.t() | [segment()]) :: t() |
| 123 | - def new!(segments) when is_list(segments) do |
| 191 | + @spec new!(list() | String.t() | HL7.Message.t(), Keyword.t()) :: t() |
| 192 | + def new!(message_content, options \\ []) do |
| 193 | + do_new!(message_content, apply_default_options(message_content, options)) |
| 194 | + end |
| 195 | + |
| 196 | + @spec do_new!(list() | String.t() | HL7.Message.t(), Keyword.t()) :: t() |
| 197 | + defp do_new!(segments, options) when is_list(segments) do |
| 124 198 | segments = |
| 125 199 | Enum.map( |
| 126 200 | segments, |
| @@ -130,25 +204,95 @@ defmodule HL7 do | |
| 130 204 | end |
| 131 205 | ) |
| 132 206 | |
| 133 | - %__MODULE__{segments: segments} |
| 207 | + %__MODULE__{segments: segments, tags: options[:tags] || %{}} |
| 134 208 | end |
| 135 209 | |
| 136 | - def new!(text) when is_binary(text) do |
| 137 | - text |> HL7.Message.to_list() |> new!() |
| 210 | + defp do_new!(text, options) when is_binary(text) do |
| 211 | + text |> HL7.Message.to_list() |> new!(options) |
| 138 212 | end |
| 139 213 | |
| 140 | - def new!(%HL7.Message{} = message) do |
| 141 | - message |> HL7.Message.to_list() |> new!() |
| 214 | + defp do_new!(%HL7.Message{} = message, options) do |
| 215 | + message |> HL7.Message.to_list() |> new!(options) |
| 142 216 | end |
| 143 217 | |
| 144 | - @spec new(String.t()) :: {:ok, t()} | {:error, HL7.InvalidMessage.t()} |
| 145 | - def new(text) when is_binary(text) do |
| 146 | - case HL7.Message.new(text) do |
| 147 | - %HL7.Message{} = message -> {:ok, message |> HL7.Message.to_list() |> new!()} |
| 148 | - invalid_message -> {:error, invalid_message} |
| 218 | + @doc ~S""" |
| 219 | + Creates an HL7 struct from valid HL7 data (accepting text, lists or the deprecated `HL7.Message` struct). |
| 220 | + Returns `{:ok, HL7.t()}` if successful, `{:error, HL7.InvalidMessage.t()}` otherwise. |
| 221 | + """ |
| 222 | + @spec new(String.t(), Keyword.t()) :: {:ok, t()} | {:error, HL7.InvalidMessage.t()} |
| 223 | + def new(text, options \\ []) when is_binary(text) do |
| 224 | + case HL7.Message.new(text, Keyword.take(options, [:copy, :validate_string]) |> Map.new()) do |
| 225 | + %HL7.Message{} = message -> |
| 226 | + {:ok, HL7.new!(message, options)} |
| 227 | + |
| 228 | + invalid_message -> |
| 229 | + {:error, invalid_message} |
| 149 230 | end |
| 150 231 | end |
| 151 232 | |
| 233 | + @doc ~S""" |
| 234 | + Puts data within an `HL7` struct, parsed segments or repetitions |
| 235 | + using an `HL7.Path` struct (see `sigil_p/2`). |
| 236 | + |
| 237 | + ## Examples |
| 238 | + |
| 239 | + Put field data as a string. |
| 240 | + |
| 241 | + iex> import HL7 |
| 242 | + iex> HL7.Examples.wikipedia_sample_hl7() |
| 243 | + ...> |> new!() |
| 244 | + ...> |> put(~p"PID-8", "F") |
| 245 | + ...> |> get(~p"PID-8") |
| 246 | + "F" |
| 247 | + |
| 248 | + |
| 249 | + Put field data as a string overwriting all repetitions. |
| 250 | + |
| 251 | + iex> import HL7 |
| 252 | + iex> HL7.Examples.wikipedia_sample_hl7() |
| 253 | + ...> |> new!() |
| 254 | + ...> |> put(~p"PID-11[*]", "SOME_ID") |
| 255 | + ...> |> get(~p"PID-11[*]") |
| 256 | + ["SOME_ID"] |
| 257 | + |
| 258 | + Put field data into a single repetition. |
| 259 | + |
| 260 | + iex> import HL7 |
| 261 | + iex> HL7.Examples.wikipedia_sample_hl7() |
| 262 | + ...> |> new!() |
| 263 | + ...> |> put(~p"PID-3[2]", ["a", "b", "c"]) |
| 264 | + ...> |> get(~p"PID-3[2].3") |
| 265 | + "c" |
| 266 | + |
| 267 | + Put component data across multiple repetitions. |
| 268 | + |
| 269 | + iex> import HL7 |
| 270 | + iex> HL7.Examples.wikipedia_sample_hl7() |
| 271 | + ...> |> new!() |
| 272 | + ...> |> put(~p"PID-11[*].3", "SOME_PLACE") |
| 273 | + ...> |> get(~p"PID-11[*].3") |
| 274 | + ["SOME_PLACE", "SOME_PLACE"] |
| 275 | + |
| 276 | + Put data in a segment using just the path to a field. |
| 277 | + |
| 278 | + iex> import HL7 |
| 279 | + iex> HL7.Examples.wikipedia_sample_hl7() |
| 280 | + ...> |> new!() |
| 281 | + ...> |> get(~p"PID") |
| 282 | + ...> |> put(~p"3", "SOME_ID") |
| 283 | + ...> |> get(~p"3") |
| 284 | + "SOME_ID" |
| 285 | + |
| 286 | + Put data across multiple segments |
| 287 | + |
| 288 | + iex> import HL7 |
| 289 | + iex> HL7.Examples.wikipedia_sample_hl7() |
| 290 | + ...> |> new!() |
| 291 | + ...> |> put(~p"OBX[*]-5", "REDACTED") |
| 292 | + ...> |> get(~p"OBX[*]-5") |
| 293 | + ["REDACTED", "REDACTED"] |
| 294 | + |
| 295 | + """ |
| 152 296 | @spec put(parsed_hl7(), Path.t(), String.t() | nil | hl7_map_data()) :: parsed_hl7() |
| 153 297 | def put(%HL7{segments: segments} = hl7, %Path{} = path, value) do |
| 154 298 | %HL7{hl7 | segments: put(segments, path, value)} |
| @@ -158,6 +302,13 @@ defmodule HL7 do | |
| 158 302 | segment_data |> do_put(path, value) |
| 159 303 | end |
| 160 304 | |
| 305 | + @doc ~S""" |
| 306 | + Updates data within an `HL7` struct, parsed segments, or repetitions |
| 307 | + using an `HL7.Path` struct (see `sigil_p/2`). |
| 308 | + """ |
| 309 | + @spec update(parsed_hl7(), Path.t(), String.t() | nil | hl7_map_data(), (hl7_map_data() -> |
| 310 | + hl7_map_data())) :: |
| 311 | + parsed_hl7() |
| 161 312 | def update(%HL7{segments: segments} = hl7, %Path{} = path, default, fun) do |
| 162 313 | %HL7{hl7 | segments: update(segments, path, default, fun)} |
| 163 314 | end |
| @@ -166,6 +317,12 @@ defmodule HL7 do | |
| 166 317 | segment_data |> do_put(path, {default, fun}) |
| 167 318 | end |
| 168 319 | |
| 320 | + @doc ~S""" |
| 321 | + Updates data within an `HL7` struct, parsed segments, or repetitions |
| 322 | + using an `HL7.Path` struct (see `sigil_p/2`). Raises a `RuntimeError` |
| 323 | + if the path is not present in the source data. |
| 324 | + """ |
| 325 | + @spec update!(parsed_hl7(), Path.t(), (hl7_map_data() -> hl7_map_data())) :: parsed_hl7() |
| 169 326 | def update!(%HL7{segments: segments} = hl7, %Path{} = path, fun) do |
| 170 327 | %HL7{hl7 | segments: update!(segments, path, fun)} |
| 171 328 | end |
| @@ -174,16 +331,55 @@ defmodule HL7 do | |
| 174 331 | segment_data |> do_put(path, {fun}) |
| 175 332 | end |
| 176 333 | |
| 334 | + @doc ~S""" |
| 335 | + Returns a list of sparse maps (with ordinal indices and strings) representing |
| 336 | + the parsed segments of an HL7 message stored in an `HL7` struct. |
| 337 | + """ |
| 177 338 | def get_segments(%HL7{segments: segments}) do |
| 178 339 | segments |
| 179 340 | end |
| 180 341 | |
| 342 | + @doc ~S""" |
| 343 | + Sets a list of sparse maps (with ordinal indices and strings) representing |
| 344 | + the parsed segments of an HL7 message to define the content of an `HL7` struct. |
| 345 | + """ |
| 181 346 | def set_segments(%HL7{} = hl7, segments) do |
| 182 347 | %HL7{hl7 | segments: segments} |
| 183 348 | end |
| 184 349 | |
| 185 350 | @doc ~S""" |
| 186 | - Labels source data (a segment map or list of segment maps) by using `HL7.Path` sigils in a labeled |
| 351 | + Returns a map of custom metadata associated with the `HL7` struct. |
| 352 | + """ |
| 353 | + def get_tags(%HL7{tags: tags}) do |
| 354 | + tags |
| 355 | + end |
| 356 | + |
| 357 | + @doc ~S""" |
| 358 | + Sets a map of custom metadata associated with the `HL7` struct. |
| 359 | + """ |
| 360 | + def set_tags(%HL7{} = hl7, tags) when is_map(tags) do |
| 361 | + %HL7{hl7 | tags: tags} |
| 362 | + end |
| 363 | + |
| 364 | + @doc ~S""" |
| 365 | + Creates a minimal map representing an empty HL7 segment that can |
| 366 | + be modified via this module. |
| 367 | + """ |
| 368 | + @spec new_segment(String.t()) :: segment() |
| 369 | + def new_segment(<<_::binary-size(3)>> = segment_name) do |
| 370 | + %{0 => segment_name} |
| 371 | + end |
| 372 | + |
| 373 | + @doc ~S""" |
| 374 | + Converts an `HL7` struct into its string representation. |
| 375 | + Convenience options will be added in the future. |
| 376 | + """ |
| 377 | + def format(%HL7{} = hl7, _options \\ []) do |
| 378 | + to_string(hl7) |
| 379 | + end |
| 380 | + |
| 381 | + @doc ~S""" |
| 382 | + Labels source data (a segment map or list of segment maps) by using `HL7.Path`s in a labeled |
| 187 383 | output template. |
| 188 384 | |
| 189 385 | One-arity functions placed as output template values will be called with the source data. |
| @@ -205,14 +401,16 @@ defmodule HL7 do | |
| 205 401 | end |
| 206 402 | |
| 207 403 | @doc ~S""" |
| 208 | - Finds data within an `HL7.t()` and from within the data that `get/2` returns (segments and repetitions) |
| 209 | - using an `HL7.Path` sigil. |
| 404 | + Finds data within an `HL7` struct, parsed segments or repetitions |
| 405 | + using an `HL7.Path` struct (see `sigil_p/2`). |
| 210 406 | |
| 211 407 | Selecting data across multiple segments or repetitions with the wildcard `[*]` pattern |
| 212 408 | will return a list of results. |
| 213 409 | |
| 214 | - Repetition data can be search using a partial path containing ony the component and/or |
| 215 | - subcomponent with the preceding period, e.g. `~p".2.3"`. |
| 410 | + Repetition data can be searched using a partial path containing ony the component and/or |
| 411 | + subcomponent with a preceding period, e.g. `~p".2.3"`. |
| 412 | + |
| 413 | + See the `sigil_p/2` docs and tests for more examples! |
| 216 414 | |
| 217 415 | ## Examples |
| 218 416 | |
| @@ -246,17 +444,16 @@ defmodule HL7 do | |
| 246 444 | ...> |> HL7.get(~p"PID-11[2].1") |
| 247 445 | "NICKELL’S PICKLES" |
| 248 446 | |
| 249 | - iex> import HL7, only: :sigils |
| 447 | + iex> import HL7 |
| 250 448 | iex> HL7.Examples.wikipedia_sample_hl7() |
| 251 | - ...> |> HL7.new!() |
| 252 | - ...> |> HL7.get(~p"PID-11[*]") |
| 253 | - ...> |> HL7.get(~p".1") |
| 449 | + ...> |> new!() |
| 450 | + ...> |> get(~p"PID") |
| 451 | + ...> |> get(~p"11[*]") |
| 452 | + ...> |> get(~p".1") |
| 254 453 | ["260 GOODWIN CREST DRIVE", "NICKELL’S PICKLES"] |
| 255 454 | |
| 256 455 | iex> import HL7 |
| 257 | - iex> HL7.Examples.wikipedia_sample_hl7() |
| 258 | - ...> |> HL7.new!() |
| 259 | - ...> |> HL7.get(~p"OBX[*]") |
| 456 | + iex> HL7.Examples.wikipedia_sample_hl7() |> new!() |> get(~p"OBX[*]") |
| 260 457 | [ |
| 261 458 | %{ |
| 262 459 | 0 => "OBX", |
| @@ -278,10 +475,7 @@ defmodule HL7 do | |
| 278 475 | } |
| 279 476 | ] |
| 280 477 | """ |
| 281 | - |
| 282 | - @spec get(parsed_hl7(), %Path{}) :: |
| 283 | - hl7_map_data() | [hl7_map_data()] | String.t() | nil |
| 284 | - |
| 478 | + @spec get(parsed_hl7(), Path.t()) :: hl7_map_data() | [hl7_map_data()] | String.t() | nil |
| 285 479 | def get(data, path) do |
| 286 480 | data |
| 287 481 | |> do_get(path) |
| @@ -303,19 +497,11 @@ defmodule HL7 do | |
| 303 497 | end |
| 304 498 | |
| 305 499 | @doc """ |
| 306 | - Converts a segment map (or lists of segments maps) or HL7 struct into a raw Elixir list. |
| 500 | + Converts an HL7 message struct into a nested list of strings. |
| 307 501 | """ |
| 308 | - @spec to_list(t() | hl7_map_data()) :: hl7_list_data() |
| 502 | + @spec to_list(t()) :: hl7_list_data() |
| 309 503 | def to_list(%HL7{segments: segments}) do |
| 310 | - to_list(segments) |
| 311 | - end |
| 312 | - |
| 313 | - def to_list(map_data) when is_list(map_data) do |
| 314 | - Enum.map(map_data, fn segment_map -> do_to_list(segment_map) end) |
| 315 | - end |
| 316 | - |
| 317 | - def to_list(map_data) when is_map(map_data) do |
| 318 | - do_to_list(map_data) |
| 504 | + Enum.map(segments, fn segment_map -> do_to_list(segment_map) end) |
| 319 505 | end |
| 320 506 | |
| 321 507 | @doc """ |
| @@ -343,11 +529,11 @@ defmodule HL7 do | |
| 343 529 | |> case do |
| 344 530 | {:ok, :line} -> |
| 345 531 | file_path |
| 346 | - |> File.stream!([], @buffer_size) |
| 532 | + |> File.stream!(:line) |
| 347 533 | |
| 348 534 | {:ok, :mllp} -> |
| 349 535 | file_path |
| 350 | - |> File.stream!([], @buffer_size) |
| 536 | + |> File.stream!(@buffer_size) |
| 351 537 | |> HL7.MLLPStream.raw_to_messages() |
| 352 538 | |
| 353 539 | {:error, reason} -> |
| @@ -381,11 +567,11 @@ defmodule HL7 do | |
| 381 567 | |> to_map(index + 1, t) |
| 382 568 | end |
| 383 569 | |
| 384 | - def do_to_list(hl7_map_data) when is_binary(hl7_map_data) do |
| 570 | + defp do_to_list(hl7_map_data) when is_binary(hl7_map_data) do |
| 385 571 | hl7_map_data |
| 386 572 | end |
| 387 573 | |
| 388 | - def do_to_list(hl7_map_data) do |
| 574 | + defp do_to_list(hl7_map_data) do |
| 389 575 | do_to_list([], hl7_map_data, get_max_index(hl7_map_data)) |
| 390 576 | end |
| 391 577 | |
| @@ -463,6 +649,10 @@ defmodule HL7 do | |
| 463 649 | data |
| 464 650 | end |
| 465 651 | |
| 652 | + defp maybe_truncate(nil, %Path{truncate: true}) do |
| 653 | + "" |
| 654 | + end |
| 655 | + |
| 466 656 | defp maybe_truncate(segment_data, %Path{truncate: true}) do |
| 467 657 | truncate(segment_data) |
| 468 658 | end |
| @@ -485,11 +675,11 @@ defmodule HL7 do | |
| 485 675 | end |
| 486 676 | |
| 487 677 | defp resolve_placement_value(_field_data = nil, {default, _fun}, _path) do |
| 488 | - default |
| 678 | + default |> lists_become_maps() |
| 489 679 | end |
| 490 680 | |
| 491 681 | defp resolve_placement_value(field_data, {_default, fun}, _path) do |
| 492 | - fun.(field_data) |
| 682 | + fun.(field_data) |> lists_become_maps() |
| 493 683 | end |
| 494 684 | |
| 495 685 | defp resolve_placement_value(_field_data = nil, {_fun}, path) do |
| @@ -497,13 +687,21 @@ defmodule HL7 do | |
| 497 687 | end |
| 498 688 | |
| 499 689 | defp resolve_placement_value(field_data, {fun}, _path) do |
| 500 | - fun.(field_data) |
| 690 | + fun.(field_data) |> lists_become_maps() |
| 501 691 | end |
| 502 692 | |
| 503 693 | defp resolve_placement_value(_field_data, value, _path) do |
| 504 | - value |
| 694 | + value |> lists_become_maps() |
| 505 695 | end |
| 506 696 | |
| 697 | + defp lists_become_maps(value) when is_list(value) do |
| 698 | + value |
| 699 | + |> Enum.with_index() |
| 700 | + |> Map.new(fn {v, i} -> {i + 1, lists_become_maps(v)} end) |
| 701 | + end |
| 702 | + |
| 703 | + defp lists_become_maps(value), do: value |
| 704 | + |
| 507 705 | defp do_get(%HL7{} = hl7, %Path{} = path) do |
| 508 706 | do_get(hl7.segments, path) |
| 509 707 | end |
| @@ -512,17 +710,19 @@ defmodule HL7 do | |
| 512 710 | [] |
| 513 711 | end |
| 514 712 | |
| 515 | - defp do_get([%{0 => _} | _] = _segments, %Path{segment: nil} = path) do |
| 516 | - raise RuntimeError, |
| 517 | - "`HL7.Path` to get data across segments must begin with a segment name, not #{inspect(path)}" |
| 713 | + # from multiple segments, return a result for each segment in a list |
| 714 | + defp do_get([%{0 => _} | _] = segments, %Path{segment: nil} = path) do |
| 715 | + Enum.map(segments, &do_get_in_segment(&1, path)) |
| 518 716 | end |
| 519 717 | |
| 718 | + # from multiple segments, filter by name and return a result for each segment in a list |
| 520 719 | defp do_get([%{0 => _} | _] = segments, %Path{segment: name, segment_number: "*"} = path) do |
| 521 720 | segments |
| 522 721 | |> Stream.filter(&(&1[0] == name)) |
| 523 722 | |> Enum.map(&do_get_in_segment(&1, path)) |
| 524 723 | end |
| 525 724 | |
| 725 | + # from multiple segments, return a result for a specific segment |
| 526 726 | defp do_get([%{0 => _} | _] = segments, %Path{segment: name, segment_number: n} = path) do |
| 527 727 | segments |
| 528 728 | |> Stream.filter(&(&1[0] == name)) |
| @@ -531,35 +731,38 @@ defmodule HL7 do | |
| 531 731 | |> do_get_in_segment(path) |
| 532 732 | end |
| 533 733 | |
| 534 | - defp do_get(nil = _segment_data, _path) do |
| 535 | - nil |
| 734 | + defp do_get(nil = _segment_data, path) do |
| 735 | + nil |> maybe_truncate(path) |
| 536 736 | end |
| 537 737 | |
| 738 | + # for a single segment |
| 538 739 | defp do_get(%{0 => _} = segment_data, path) do |
| 539 740 | do_get_in_segment(segment_data, path) |
| 540 741 | end |
| 541 742 | |
| 743 | + # for a list of repetitions (generally a repeating field), return a result for each |
| 542 744 | defp do_get(repetition_list, %{field: nil, repetition: nil} = path) |
| 543 745 | when is_list(repetition_list) do |
| 544 746 | Enum.map(repetition_list, &do_get_in_repetition(&1, path)) |
| 545 747 | end |
| 546 748 | |
| 749 | + # for a single repetition in a field |
| 547 750 | defp do_get(repetition_data, %{field: nil, repetition: nil} = path) do |
| 548 751 | do_get_in_repetition(repetition_data, path) |
| 549 752 | end |
| 550 753 | |
| 551 754 | defp do_get(_repetition_data, path) do |
| 552 755 | raise RuntimeError, |
| 553 | - "HL7.Path to directly access repetitions should be begin with `.`, not #{inspect(path)}" |
| 756 | + "HL7.Path #{inspect(path)} could not work with the given data." |
| 554 757 | end |
| 555 758 | |
| 556 | - defp do_get_in_segment(segment_data, %{field: nil, component: nil} = _path) do |
| 557 | - segment_data |
| 759 | + defp do_get_in_segment(segment_data, %{field: nil, component: nil} = path) do |
| 760 | + segment_data |> maybe_truncate(path) |
| 558 761 | end |
| 559 762 | |
| 560 763 | defp do_get_in_segment(_segment_data, %{field: nil} = path) do |
| 561 764 | raise RuntimeError, |
| 562 | - "HL7.Path to access components within segments requires field number, not #{inspect(path)}" |
| 765 | + "HL7.Path #{inspect(path)} requires field number." |
| 563 766 | end |
| 564 767 | |
| 565 768 | defp do_get_in_segment(segment_data, %{field: f} = path) do |
| @@ -577,8 +780,8 @@ defmodule HL7 do | |
| 577 780 | end) |
| 578 781 | end |
| 579 782 | |
| 580 | - defp do_get_in_field(field_data, %{repetition: "*"}) do |
| 581 | - [field_data] |
| 783 | + defp do_get_in_field(field_data, %{repetition: "*"} = path) do |
| 784 | + [field_data |> maybe_truncate(path)] |
| 582 785 | end |
| 583 786 | |
| 584 787 | defp do_get_in_field(field_data, %{repetition: r} = path) do |
| @@ -587,8 +790,8 @@ defmodule HL7 do | |
| 587 790 | |> do_get_in_repetition(path) |
| 588 791 | end |
| 589 792 | |
| 590 | - defp do_get_in_repetition(repetition_data, %{component: nil}) do |
| 591 | - repetition_data |
| 793 | + defp do_get_in_repetition(repetition_data, %{component: nil} = path) do |
| 794 | + repetition_data |> maybe_truncate(path) |
| 592 795 | end |
| 593 796 | |
| 594 797 | defp do_get_in_repetition(repetition_data, %{component: c} = path) do |
| @@ -597,15 +800,17 @@ defmodule HL7 do | |
| 597 800 | |> do_get_in_component(path) |
| 598 801 | end |
| 599 802 | |
| 600 | - defp do_get_in_component(component_data, %{subcomponent: nil}) do |
| 601 | - component_data |
| 803 | + defp do_get_in_component(component_data, %{subcomponent: nil} = path) do |
| 804 | + component_data |> maybe_truncate(path) |
| 602 805 | end |
| 603 806 | |
| 604 | - defp do_get_in_component(component_data, %{subcomponent: s}) do |
| 807 | + defp do_get_in_component(component_data, %{subcomponent: s} = path) do |
| 605 808 | component_data |
| 606 809 | |> get_value_at_index(s) |
| 810 | + |> maybe_truncate(path) |
| 607 811 | end |
| 608 812 | |
| 813 | + # put across multiple segments |
| 609 814 | defp do_put([%{0 => _} | _] = segments, %Path{segment: name, segment_number: "*"} = path, value) do |
| 610 815 | segments |
| 611 816 | |> Enum.map(fn segment -> |
| @@ -613,6 +818,7 @@ defmodule HL7 do | |
| 613 818 | end) |
| 614 819 | end |
| 615 820 | |
| 821 | + # put within a specific segment |
| 616 822 | defp do_put([%{0 => _} | _] = segments, %Path{segment: name, segment_number: n} = path, value) do |
| 617 823 | segments |
| 618 824 | |> Stream.with_index() |
| @@ -643,22 +849,34 @@ defmodule HL7 do | |
| 643 849 | |
| 644 850 | defp do_put(_repetition_data, path, _value) do |
| 645 851 | raise RuntimeError, |
| 646 | - "HL7.Path to directly update repetitions should be begin with `.`, not #{inspect(path)}" |
| 852 | + "HL7.Path #{inspect(path)} to update repetition data should be begin with `.`" |
| 853 | + end |
| 854 | + |
| 855 | + defp do_put_in_segment(segment_data, value, %{field: nil} = path) do |
| 856 | + resolve_placement_value(segment_data, value, path) |
| 647 857 | end |
| 648 858 | |
| 649 859 | defp do_put_in_segment(segment_data, value, %{field: f} = path) do |
| 650 860 | Map.put(segment_data, f, do_put_in_field(segment_data[f], value, path)) |
| 651 861 | end |
| 652 862 | |
| 653 | - # fields can contain either simple strings or maps of repetitions by ordinal index |
| 654 | - # if a map of repetitions contains only one string, it is reduced here to the string value alone |
| 655 | - defp simplify_string_fields(%{1 => value} = field_data) |
| 656 | - when map_size(field_data) == 1 and is_binary(value) do |
| 863 | + # data can contain either simple strings or maps by ordinal index |
| 864 | + # if a map contains only one string, it is reduced here to the string value alone |
| 865 | + |
| 866 | + defp simplify_string_fields(value) do |
| 867 | + do_simplify_string_fields(value, value) |
| 868 | + end |
| 869 | + |
| 870 | + defp do_simplify_string_fields(original, %{1 => value} = map) when map_size(map) == 1 do |
| 871 | + do_simplify_string_fields(original, value) |
| 872 | + end |
| 873 | + |
| 874 | + defp do_simplify_string_fields(_original, value) when is_binary(value) do |
| 657 875 | value |
| 658 876 | end |
| 659 877 | |
| 660 | - defp simplify_string_fields(field_data) do |
| 661 | - field_data |
| 878 | + defp do_simplify_string_fields(original, _value) do |
| 879 | + original |
| 662 880 | end |
| 663 881 | |
| 664 882 | defp do_put_in_field(field_data, value, %{repetition: "*", component: nil} = path) do |
| @@ -683,6 +901,7 @@ defmodule HL7 do | |
| 683 901 | |
| 684 902 | defp do_put_in_repetition(repetition_data, value, %{component: nil} = path) do |
| 685 903 | resolve_placement_value(repetition_data, value, path) |
| 904 | + |> simplify_string_fields() |
| 686 905 | end |
| 687 906 | |
| 688 907 | defp do_put_in_repetition(repetition_data, value, %{component: c} = path) do |
| @@ -692,6 +911,7 @@ defmodule HL7 do | |
| 692 911 | |
| 693 912 | defp do_put_in_component(component_data, value, %{subcomponent: nil} = path) do |
| 694 913 | resolve_placement_value(component_data, value, path) |
| 914 | + |> simplify_string_fields() |
| 695 915 | end |
| 696 916 | |
| 697 917 | defp do_put_in_component(subcomponent_data, value, %{subcomponent: s} = path) do |
| @@ -730,6 +950,15 @@ defmodule HL7 do | |
| 730 950 | defp nil_for_empty(""), do: nil |
| 731 951 | defp nil_for_empty(value), do: value |
| 732 952 | |
| 953 | + @spec apply_default_options(list() | String.t() | HL7.Message.t(), Keyword.t()) :: Keyword.t() |
| 954 | + defp apply_default_options(%HL7.Message{tag: tags} = _message_content, options) do |
| 955 | + [tags: tags, copy: options[:copy]] |
| 956 | + end |
| 957 | + |
| 958 | + defp apply_default_options(_message_content, options) do |
| 959 | + [tags: options[:tags] || %{}, copy: options[:copy]] |
| 960 | + end |
| 961 | + |
| 733 962 | @spec infer_file_type(String.t()) :: {:ok, :line} | {:ok, :mllp} | {:error, atom()} |
| 734 963 | defp infer_file_type(file_path) do |
| 735 964 | File.open(file_path, [:read]) |
| @@ -757,15 +986,9 @@ end | |
| 757 986 | |
| 758 987 | defimpl Inspect, for: HL7 do |
| 759 988 | def inspect(%HL7{segments: segments} = _hl7, _opts) do |
| 760 | - count = Enum.count(segments) |
| 761 | - names = segments |> Enum.take(5) |> Enum.map(&to_string(&1[0])) |
| 762 | - label = Enum.join(names, ", ") |
| 763 | - over = count - Enum.count(names) |
| 764 | - |
| 765 | - if over > 0 do |
| 766 | - "#HL7[" <> label <> ", +" <> to_string(over) <> "]" |
| 767 | - else |
| 768 | - "#HL7[" <> label <> "]" |
| 989 | + case Enum.count(segments) do |
| 990 | + 1 -> "#HL7<with 1 segment>" |
| 991 | + c -> "#HL7<with #{c} segments>" |
| 769 992 | end |
| 770 993 | end |
| 771 994 | end |
| @@ -157,6 +157,10 @@ defmodule HL7.Message do | |
| 157 157 | msg |
| 158 158 | end |
| 159 159 | |
| 160 | + def new(%HL7{} = msg, _options) do |
| 161 | + HL7.to_list(msg) |> new() |> Map.put(:tag, msg.tags) |
| 162 | + end |
| 163 | + |
| 160 164 | def new(<<"MSH|^~\\&", _rest::binary>> = raw_text, options) do |
| 161 165 | with {:ok, text} <- validate_text(raw_text, options) do |
| 162 166 | copy = options[:copy] == true |
| @@ -228,6 +232,10 @@ defmodule HL7.Message do | |
| 228 232 | segments |
| 229 233 | end |
| 230 234 | |
| 235 | + def to_list(%HL7.InvalidMessage{}) do |
| 236 | + raise RuntimeError, "invalid HL7 data" |
| 237 | + end |
| 238 | + |
| 231 239 | def to_list(%HL7.RawMessage{} = msg) do |
| 232 240 | msg |> HL7.Message.new() |> to_list() |
| 233 241 | end |
Loading more files…