defmodule HL7.Query do
require Logger
alias HL7.Path
@moduledoc """
Queries and modifies HL7 messages using Field and Segment Grammar Notations with a pipeline-friendly API and set-based
operations.
Similar to libraries such as jQuery and D3, `HL7.Query` is designed around the concept of selecting and sub-selecting
elements (segments or segment groups) in an HL7 message. The full message context is retained in the `HL7.Query`
struct so that messages can be modified piecemeal and then reconstructed as strings.
In general, use `HL7.Query.select/2` with a segment selector (similar to CSS selectors) to select lists of segment groups.
The segment selector is written as a string of ordered segment names. Curly braces surround repeating elements.
Square brackets enclose optional elements. These can be nested to create selectors that can select complex groups of segments or validate
entire HL7 message layouts.
For example, an ORU_R01 HL7 message's Order Group selector could be written as:
`\"[ORC] OBR {[NTE]} {[OBX {[NTE]}]}\"`.
Note that this would look for OBRs, optionally preceded by an ORC, possibly followed by one or more NTEs, maybe followed
again by one or more OBRs with their own optional NTE sets.
To reference data within segments, there is a field selector format that can access fields, repetitions, components
and sub-components across one or more segments. All indices start at one, and the repetition index defaults to one
unless specified within brackets after the field number.
Field Selector | Description
------------ | -------------
`\"PID-11[2].4\"` | PID segments, 11th field, 2nd repetition, 4th component
`\"OBX-2.2.1\"` | OBX segments, 2nd field, 2nd component, 1st sub-component
`\"1\"` | All segments, 1st field
`\"3[4].2\"` | All segments, 3rd field, 4th repetition, 2nd component
"""
# todo add wildcard field grammar, e.g. PID-11[*] to grab all field repetitions
@type t :: %HL7.Query{selections: list()}
@type raw_hl7 :: String.t() | HL7.RawMessage.t()
@type parsed_hl7 :: [list()] | HL7.Message.t()
@type content_hl7 :: raw_hl7() | parsed_hl7()
@type parsed_or_query_hl7 :: parsed_hl7() | HL7.Query.t()
@type content_or_query_hl7 :: content_hl7() | HL7.Query.t()
defstruct selections: [], invalid_message: nil, part: nil
@deprecated "use HL7.Query.sigil_p/2 instead"
defmacro sigil_g({:<<>>, _, [term]}, _modifiers) do
term
|> Path.new()
|> Macro.escape()
end
@doc """
Checks the format of an hl7 path at compile time and returns an Path
"""
defmacro sigil_p({:<<>>, _, [term]}, _modifiers) do
term
|> Path.new()
|> Macro.escape()
end
@doc """
Creates an `HL7.Query` struct that selects an entire HL7 Message. This step is implicitly carried out by most other
functions in this module. As it involves parsing an HL7 message, one should
cache this result if invoked repeatedly.
"""
@spec new(content_or_query_hl7()) :: HL7.Query.t()
def new(%HL7.Message{} = msg) do
msg
|> HL7.Message.to_list()
|> HL7.Query.new()
end
def new(%HL7.InvalidMessage{} = msg) do
%HL7.Query{invalid_message: msg}
end
def new(msg) when is_list(msg) do
full_selection = %HL7.Selection{segments: msg, complete: true, valid: true}
%HL7.Query{selections: [full_selection]}
end
def new(msg) when is_binary(msg) do
msg
|> HL7.Message.new()
|> HL7.Query.new()
end
def new(%HL7.Query{} = query) do
query
end
@doc ~S"""
Selects or sub-selects segment groups in an HL7 message using a segment selector or filter function.
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> select("OBX")
...> |> select(fn q -> find_first(q, ~p"1") != "1" end)
...> |> delete()
...> |> root()
...> |> get_segment_names()
["MSH", "PID", "ORC", "RXA", "RXR", "OBX", "ORC", "RXA", "ORC", "RXA", "RXR", "OBX"]
"""
@spec select(content_or_query_hl7(), binary()) :: HL7.Query.t()
def select(msg, segment_selector)
when is_list(msg) and (is_binary(segment_selector) or is_function(segment_selector)) do
HL7.Query.new(msg) |> perform_select(segment_selector)
end
def select(msg, segment_selector)
when is_binary(msg) and (is_binary(segment_selector) or is_function(segment_selector)) do
HL7.Query.new(msg) |> perform_select(segment_selector)
end
def select(%HL7.Message{} = msg, segment_selector)
when is_binary(segment_selector) or is_function(segment_selector) do
HL7.Query.new(msg) |> perform_select(segment_selector)
end
def select(%HL7.Query{} = query, segment_selector)
when is_binary(segment_selector) or is_function(segment_selector) do
query |> perform_select(segment_selector)
end
@doc """
Returns the current selection count.
"""
@spec count(HL7.Query.t()) :: non_neg_integer()
def count(%HL7.Query{selections: selections}) do
selections
|> Enum.filter(fn m -> m.valid end)
|> Enum.count()
end
@doc ~S"""
Filters (deletes) segments within selections by whitelisting one or more segment types.
Accepts either a segment name, list of acceptable names, or a function that takes an `HL7.Query`
(containing one sub-selected segment at a time) and returns a boolean filter value.
## Examples
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> filter(["MSH", "PID", "ORC"])
...> |> root()
...> |> get_segment_names()
["MSH", "PID", "ORC", "ORC", "ORC"]
"""
@spec filter(content_or_query_hl7(), (HL7.Query.t() -> as_boolean(term))) ::
HL7.Query.t()
def filter(%HL7.Query{selections: selections} = query, func) when is_function(func) do
filtered_segment_selections =
selections
|> Enum.map(fn m ->
filtered_segments =
case m.valid do
true ->
Enum.filter(
m.segments,
fn segment ->
q = %HL7.Query{selections: [%HL7.Selection{m | segments: [segment]}]}
func.(q)
end
)
false ->
m.segments
end
%HL7.Selection{m | segments: filtered_segments}
end)
%HL7.Query{query | selections: filtered_segment_selections}
end
def filter(content_hl7, fun) when is_function(fun) do
query = HL7.Query.new(content_hl7)
HL7.Query.filter(query, fun)
end
@spec filter(content_or_query_hl7(), binary()) :: HL7.Query.t()
def filter(%HL7.Query{} = query, tag) when is_binary(tag) do
HL7.Query.filter(query, [tag])
end
def filter(content_hl7, tag) when is_binary(tag) do
query = HL7.Query.new(content_hl7)
HL7.Query.filter(query, [tag])
end
@spec filter(content_or_query_hl7(), [binary()]) :: HL7.Query.t()
def filter(%HL7.Query{selections: selections} = query, tags) when is_list(tags) do
filtered_segment_selections =
selections
|> Enum.map(fn m ->
filtered_segments =
Enum.filter(m.segments, fn [<<t::binary-size(3)>> | _] -> t in tags end)
%HL7.Selection{m | segments: filtered_segments}
end)
%HL7.Query{query | selections: filtered_segment_selections}
end
def filter(content_hl7, tags) when is_list(tags) do
query = HL7.Query.new(content_hl7)
HL7.Query.filter(query, tags)
end
@doc ~S"""
Rejects (deletes) segments within selections by blacklisting one or more segment types.
Accepts either a segment name, list of acceptable names, or a function that takes an `HL7.Query`
(containing one sub-selected segment at a time) and returns a boolean reject value.
## Examples
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> reject(["OBX", "RXA", "RXR"])
...> |> root()
...> |> get_segment_names()
["MSH", "PID", "ORC", "ORC", "ORC"]
"""
@spec reject(HL7.Query.t(), binary()) :: HL7.Query.t()
def reject(%HL7.Query{} = query, tag) when is_binary(tag) do
HL7.Query.reject(query, [tag])
end
def reject(content_hl7, tag) when is_binary(tag) do
query = HL7.Query.new(content_hl7)
HL7.Query.reject(query, tag)
end
@spec reject(content_or_query_hl7(), [binary()]) :: HL7.Query.t()
def reject(%HL7.Query{selections: selections} = query, tags) when is_list(tags) do
filtered_segment_selections =
selections
|> Enum.map(fn m ->
filtered_segments =
Enum.reject(m.segments, fn [<<t::binary-size(3)>> | _] -> t in tags end)
%HL7.Selection{m | segments: filtered_segments}
end)
%HL7.Query{query | selections: filtered_segment_selections}
end
def reject(content_hl7, tags) when is_list(tags) do
query = HL7.Query.new(content_hl7)
HL7.Query.reject(query, tags)
end
@spec reject(content_or_query_hl7, (HL7.Query.t() -> as_boolean(term))) ::
HL7.Query.t()
def reject(%HL7.Query{selections: selections} = query, func) when is_function(func) do
rejected_segment_selections =
selections
|> Enum.map(fn m ->
rejected_segments =
case m.valid do
true ->
Enum.reject(
m.segments,
fn segment ->
q = %HL7.Query{selections: [%HL7.Selection{m | segments: [segment]}]}
func.(q)
end
)
false ->
m.segments
end
%HL7.Selection{m | segments: rejected_segments}
end)
%HL7.Query{query | selections: rejected_segment_selections}
end
def reject(content_hl7, fun) when is_function(fun) do
query = HL7.Query.new(content_hl7)
HL7.Query.reject(query, fun)
end
@doc """
Rejects (deletes) Z-segments in all selections.
"""
def reject_z_segments(%HL7.Query{} = query) do
func = fn q ->
get_segments(q)
|> Enum.at(0)
|> case do
[<<segment_name::binary-size(3)>> | _] -> String.at(segment_name, 0) == "Z"
_ -> false
end
end
reject(query, func)
end
@doc ~S"""
Associates data with each selection. This data remains accessible to
child selections.
Each selection stores a map of data. The supplied `fun` should
accept an `HL7.Query` and return a map of key-values to be merged into the
existing map of data.
Selections automatically include the index of the current selection, i.e., `%{index: 5}`.
For HL7 numbering, the index values begin at 1.
## Examples
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> select("ORC RXA RXR {[OBX]}")
...> |> data(fn q -> %{order_num: find_first(q, ~p"ORC-3.1")} end)
...> |> select("OBX")
...> |> update(~p"6", fn q -> get_datum(q, :order_num) end)
...> |> root()
...> |> find_first(~p"OBX-6")
"IZ-783278"
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> select("ORC RXA RXR {[OBX]}")
...> |> data(fn q -> %{group_num: get_index(q)} end)
...> |> select("OBX")
...> |> update(~p"6", fn q -> get_datum(q, :group_num) end)
...> |> root()
...> |> find_all(~p"OBX-6")
[1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
"""
@spec data(HL7.Query.t(), (HL7.Query.t() -> map())) :: HL7.Query.t()
def data(%HL7.Query{selections: selections} = query, func)
when is_function(func) do
associated_selections = associate_selections(selections, func, [])
%HL7.Query{query | selections: associated_selections}
end
@doc """
Returns a list containing an associated data map for each selection in the given `HL7.Query`.
"""
@spec get_data(HL7.Query.t()) :: [map()]
def get_data(%HL7.Query{selections: selections}) do
selections
|> Enum.filter(fn m -> m.valid end)
|> Enum.map(fn m -> m.data end)
end
@doc """
Returns the associated key-value of the first (or only) selection for the given `HL7.Query`.
"""
@spec get_datum(HL7.Query.t(), any(), any()) :: any()
def get_datum(%HL7.Query{} = query, key, default \\ nil) do
query
|> get_data()
|> case do
[] -> default
[datum | _] -> Map.get(datum, key, default)
end
end
@doc ~S"""
Returns the selection index of the first (or only) selection for the given `HL7.Query`.
## Examples
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> select("ORC RXA RXR {[OBX]}")
...> |> map(fn q -> get_index(q) end)
[1, 2]
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> select("ORC RXA RXR {[OBX]}")
...> |> select("OBX")
...> |> map(fn q -> get_index(q) end)
[1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> select("OBX")
...> |> map(fn q -> get_index(q) end)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
"""
@spec get_index(HL7.Query.t()) :: non_neg_integer()
def get_index(%HL7.Query{} = query) do
query |> get_datum(:index)
end
@doc """
Updates the set numbers in each segment's first field to be their respective selection indices.
"""
@spec number_set_ids(HL7.Query.t()) :: HL7.Query.t()
def number_set_ids(%HL7.Query{} = query) do
update(query, Path.new("1"), fn q -> get_index(q) |> Integer.to_string() end)
end
@deprecated "use HL7.Query.update/2 instead"
@spec replace_parts(
content_or_query_hl7(),
String.t(),
function() | String.t() | list()
) ::
HL7.Query.t()
def replace_parts(query, grammar_string, func_or_value) when is_binary(grammar_string) do
field_path = Path.new(grammar_string)
update(query, field_path, func_or_value)
end
@doc ~S"""
Updates segment parts of all selected segments, iterating through each selection.
A replacement function accepts an `HL7.Query` containing one selection with its
`part` property set to the value found using the `field_selector`.
## Examples
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> update(~p"OBX-3.2", fn q -> "TEST: " <> q.part end)
...> |> find_all(~p"OBX-3.2")
...> |> List.first()
"TEST: Vaccine funding program eligibility category"
iex> import HL7.Query
iex> HL7.Examples.wikipedia_sample_hl7()
...> |> select("PID")
...> |> update(~p"5.2", "UNKNOWN")
...> |> find_first(~p"PID-5.2")
"UNKNOWN"
"""
@doc since: "0.8.0"
@spec update(
content_or_query_hl7(),
Path.t(),
(t() -> String.t()) | String.t() | list()
) ::
HL7.Query.t()
def update(
%HL7.Query{selections: selections} = query,
%Path{} = field_path,
func_or_value
) do
selection_transform = get_selection_transform(func_or_value, field_path)
replaced_selections = replace_parts_in_selections(selections, selection_transform, [])
%HL7.Query{query | selections: replaced_selections}
end
def update(hl7_content, indices, func_or_value) do
query = new(hl7_content)
update(query, indices, func_or_value)
end
@doc """
Replaces all segments in each selection. The given `fun` should
accept an `HL7.Query` (referencing a single selection) and return a list of replacement segments
(in parsed list format).
"""
@spec replace(HL7.Query.t(), (HL7.Query.t() -> [HL7.Segment.segment_hl7()])) :: HL7.Query.t()
def replace(%HL7.Query{selections: selections} = query, fun) when is_function(fun) do
replaced_selections = replace_selections(selections, fun, [])
%HL7.Query{query | selections: replaced_selections}
end
@doc """
Prepends a segment or list of segments to the segments in each of the current selections.
"""
@spec prepend(HL7.Query.t(), list()) :: HL7.Query.t()
def prepend(
%HL7.Query{selections: selections} = query,
[<<_::binary-size(3)>> | _] = segment_data
) do
prepended_segment_selections =
selections
|> Enum.map(fn m ->
prepended_segments = [segment_data | m.segments]
%HL7.Selection{m | segments: prepended_segments}
end)
%HL7.Query{query | selections: prepended_segment_selections}
end
def prepend(%HL7.Query{selections: selections} = query, segment_list)
when is_list(segment_list) do
prepended_segment_selections =
selections
|> Enum.map(fn m ->
prepended_segments = segment_list ++ m.segments
%HL7.Selection{m | segments: prepended_segments}
end)
%HL7.Query{query | selections: prepended_segment_selections}
end
@doc """
Appends a segment or segments to the currently _selected_ segments in each selection.
"""
@spec append(HL7.Query.t(), list()) :: HL7.Query.t()
def append(%HL7.Query{selections: selections} = query, [<<_::binary-size(3)>> | _] = segments) do
appended_segment_selections =
selections
|> Enum.map(fn m ->
appended_segments = [segments | m.segments |> Enum.reverse()] |> Enum.reverse()
%HL7.Selection{m | segments: appended_segments}
end)
%HL7.Query{query | selections: appended_segment_selections}
end
def append(%HL7.Query{selections: selections} = query, segments) when is_list(segments) do
appended_segment_selections =
selections
|> Enum.map(fn m ->
appended_segments = m.segments ++ segments
%HL7.Selection{m | segments: appended_segments}
end)
%HL7.Query{query | selections: appended_segment_selections}
end
@doc ~S"""
Maps across each selection with a `fun` that accepts an `HL7.Query`
(handling one selection at a time) and returns a results list.
## Examples
iex> import HL7.Query
iex> HL7.Examples.nist_immunization_hl7()
...> |> select("RXA")
...> |> map(fn q -> find_first(q, ~p"5.2") end)
["Influenza", "PCV 13", "DTaP-Hep B-IPV"]
"""
@spec map(HL7.Query.t(), function()) :: list()
def map(%HL7.Query{selections: selections}, fun) when is_function(fun) do
selections
|> Enum.filter(fn s -> s.valid end)
|> Enum.map(fn s ->
query = %HL7.Query{selections: [s]}
fun.(query)
end)
end
@doc """
Deletes all selections in the query document.
"""
@spec delete(HL7.Query.t()) :: HL7.Query.t()
def delete(%HL7.Query{selections: selections} = query) do
deleted_segment_selections =
selections
|> Enum.map(fn m -> %HL7.Selection{m | segments: []} end)
%HL7.Query{query | selections: deleted_segment_selections}
end
@doc """
Returns a list containing a list of _selected_ segments for each selection.
"""
@spec get_segment_groups(HL7.Query.t()) :: [list()]
def get_segment_groups(%HL7.Query{selections: selections}) do
selections
|> Enum.map(fn m -> m.segments end)
|> Enum.reject(fn s -> s == [] end)
end
@doc """
Returns a flattened list of _selected_ segments across all selections.
"""
@spec get_segments(HL7.Query.t()) :: [list()]
def get_segments(%HL7.Query{selections: selections}) do
selections
|> Enum.reduce([], fn m, acc ->
Enum.reduce(m.segments, acc, fn s, s_acc -> [s | s_acc] end)
end)
|> Enum.reject(fn s -> s == [] end)
|> Enum.reverse()
end
@doc """
Returns a flattened list of segment names across all selections.
"""
@spec get_segment_names(content_or_query_hl7()) :: [String.t()]
def get_segment_names(%HL7.Query{} = query) do
find_all(query, Path.new("0"))
end
def get_segment_names(content_hl7) do
query = HL7.Query.new(content_hl7)
get_segment_names(query)
end
@deprecated "use HL7.Query.find_all/2 instead"
@spec get_parts(content_or_query_hl7(), String.t()) :: list()
def get_parts(query, grammar_string) when is_binary(grammar_string) do
field_path = Path.new(grammar_string)
find_all(query, field_path)
end
@doc """
Returns a flattened list of segment parts from _selected_ segments across all selections using
the given `field_selector`.
`PID-3[2].1.2` PID segments, field 3, repetition 2, component 1, subcomponent 2
`OBX-5` OBX segments, field 5
`2.3` All segments, field 2, component 3
"""
@doc since: "0.8.0"
@spec find_all(content_or_query_hl7(), Path.t()) :: list()
def find_all(%HL7.Query{invalid_message: nil} = query, %Path{} = field_path) do
case field_path.data do
{segment_name, numeric_indices} ->
query
|> HL7.Query.get_segments()
|> Enum.filter(fn [name | _] -> name == segment_name end)
|> Enum.map(fn segment ->
segment |> HL7.Segment.get_part_by_indices(numeric_indices)
end)
numeric_indices ->
query
|> HL7.Query.get_segments()
|> Enum.map(fn segment ->
segment |> HL7.Segment.get_part_by_indices(numeric_indices)
end)
end
end
def find_all(content_or_query, field_path) do
content_or_query |> HL7.Query.new() |> find_all(field_path)
end
@deprecated "use HL7.Query.find_first/2 instead"
@spec get_part(content_or_query_hl7(), String.t()) :: nil | iodata()
def get_part(query, grammar_string)
when is_binary(grammar_string) do
field_path = Path.new(grammar_string)
find_first(query, field_path)
end
@doc """
Returns a segment part from the first _selected_ segment (of the given name, if specified)
across all selections using the given `field_selector`.
`PID-3[2].1.2` PID segments, field 3, repetition 2, component 1, subcomponent 2
`OBX-5` OBX segments, field 5
`2.3` All segments, field 2, component 3
"""
@doc since: "0.8.0"
@spec find_first(content_or_query_hl7(), Path.t()) :: nil | iodata()
def find_first(%HL7.Query{} = query, %Path{} = field_path) do
case field_path.data do
{segment_name, numeric_indices} ->
query
|> HL7.Query.get_segments()
|> HL7.Message.find(segment_name)
|> HL7.Segment.get_part_by_indices(numeric_indices)
numeric_indices ->
query
|> HL7.Query.get_segments()
|> List.first()
|> HL7.Segment.get_part_by_indices(numeric_indices)
end
end
def find_first(msg, field_path) do
msg
|> HL7.Query.new()
|> find_first(field_path)
end
@doc """
Outputs an ANSI representation of an `HL7.Query` with corresponding selection indices on the left.
"""
def to_console(%HL7.Query{selections: selections}) do
import IO.ANSI
selections
|> Enum.map(fn m ->
prefix =
m.prefix |> Enum.map(fn segment -> default_color() <> " " <> inspect(segment) end)
segments =
m.segments
|> Enum.map(fn segment ->
magenta() <>
(Map.get(m.data, :index) |> to_string() |> String.pad_leading(3)) <>
": " <> green() <> inspect(segment)
end)
suffix = m.suffix |> Enum.map(fn segment -> default_color() <> inspect(segment) end)
[prefix, segments, suffix]
end)
|> List.flatten()
|> Enum.join("\n")
|> IO.puts()
end
@doc """
Converts an `HL7.Query` into an `HL7.Message`.
"""
@spec to_message(HL7.Query.t()) :: HL7.Message.t()
def to_message(%HL7.Query{} = query) do
extract_lists_for_message(query) |> HL7.Message.new()
end
@doc """
Selects the entire an `HL7.Query` into an `HL7.Message`.
"""
@spec root(HL7.Query.t()) :: HL7.Query.t()
def root(%HL7.Query{} = query) do
extract_lists_for_message(query) |> HL7.Query.new()
end
defimpl String.Chars, for: HL7.Query do
require Logger
@spec to_string(HL7.Query.t()) :: String.t()
def to_string(%HL7.Query{} = q) do
HL7.Query.to_message(q) |> Kernel.to_string()
end
end
###############################
### private functions ###
###############################
defp get_selections_within_a_selection(selection, grammar) do
selection.segments
|> build_selections(grammar, [])
|> List.update_at(0, fn m -> %HL7.Selection{m | prefix: selection.prefix ++ m.prefix} end)
|> List.update_at(-1, fn m -> %HL7.Selection{m | suffix: selection.suffix ++ m.suffix} end)
|> Enum.map(fn m -> %HL7.Selection{m | valid: m.segments != [], data: selection.data} end)
|> index_selection_data()
end
defp index_selection_data(selections) do
index_selection_data(selections, 1, [])
end
defp index_selection_data([selection | tail], index, result) do
%HL7.Selection{data: data, valid: valid} = selection
new_data = %{data | index: index}
new_selection = %HL7.Selection{selection | data: new_data}
new_index = if valid, do: index + 1, else: index
index_selection_data(tail, new_index, [new_selection | result])
end
defp index_selection_data([], _, result) do
result |> Enum.reverse()
end
defp build_selections([], _grammar, result) do
result |> Enum.reverse()
end
defp build_selections(source, grammar, result) do
selection = extract_first_selection(source, grammar)
case selection do
%HL7.Selection{complete: true, suffix: suffix} ->
selection_minus_leftovers = %HL7.Selection{selection | suffix: []}
build_selections(suffix, grammar, [selection_minus_leftovers | result])
%HL7.Selection{complete: false} ->
case result do
[] ->
[%HL7.Selection{prefix: source, broken: true}]
[last_selection | tail] ->
[%HL7.Selection{last_selection | suffix: source} | tail]
|> Enum.reverse()
end
end
end
defp extract_first_selection(source, grammar) do
head_selection = selection_from_head(grammar, %HL7.Selection{suffix: source})
%HL7.Selection{
head_selection
| prefix: Enum.reverse(head_selection.prefix),
segments: Enum.reverse(head_selection.segments)
}
end
defp selection_from_head(_grammar, %HL7.Selection{suffix: []} = selection) do
selection
end
defp selection_from_head(grammar, %HL7.Selection{} = selection) do
head_selection = follow_grammar(grammar, selection)
%HL7.Selection{complete: complete, prefix: prefix, suffix: suffix, broken: broken} =
head_selection
case complete && !broken do
true ->
head_selection
false ->
[segment | remaining_segments] = suffix
selection_from_head(grammar, %HL7.Selection{
head_selection
| prefix: [segment | prefix],
complete: false,
broken: false,
suffix: remaining_segments
})
end
end
defp follow_grammar(grammar, %HL7.Selection{suffix: []} = selection) when is_binary(grammar) do
%HL7.Selection{selection | complete: false, broken: true}
end
defp follow_grammar(<<"!", segment_type::binary-size(3)>> = _grammar, selection) do
source = selection.suffix
case segment_type != next_segment_type(source) do
true ->
[segment | remaining_segments] = source
%HL7.Selection{
selection
| complete: true,
fed: true,
segments: [segment | selection.segments],
suffix: remaining_segments
}
false ->
%HL7.Selection{selection | fed: false, broken: true}
end
end
defp follow_grammar(grammar, selection) when is_binary(grammar) do
source = selection.suffix
case grammar == next_segment_type(source) do
true ->
[segment | remaining_segments] = source
%HL7.Selection{
selection
| complete: true,
fed: true,
segments: [segment | selection.segments],
suffix: remaining_segments
}
false ->
%HL7.Selection{selection | fed: false, broken: true}
end
end
defp follow_grammar(
%HL7.SegmentGrammar{repeating: true} = grammar,
selection
) do
grammar_once = %HL7.SegmentGrammar{grammar | repeating: false}
attempt_selection = %HL7.Selection{selection | complete: false}
found_once = follow_grammar(grammar_once, attempt_selection)
case found_once.complete && !found_once.broken do
true ->
collect_copies(grammar, found_once)
false ->
%HL7.Selection{selection | complete: false}
end
end
defp follow_grammar(%HL7.SegmentGrammar{optional: optional} = grammar, selection) do
attempt_selection = %HL7.Selection{selection | fed: false, complete: false, broken: false}
children_selection =
Enum.reduce_while(grammar.children, attempt_selection, fn child_grammar,
current_selection ->
child_selection = follow_grammar(child_grammar, current_selection)
case child_selection.complete && !child_selection.broken do
true ->
{:cont, child_selection}
false ->
case optional do
true ->
{:cont, current_selection}
false ->
{:halt, %HL7.Selection{selection | complete: false, broken: true}}
end
end
end)
%HL7.Selection{children_selection | complete: !children_selection.broken}
end
defp collect_copies(%HL7.SegmentGrammar{} = grammar, selection) do
grammar_once = %HL7.SegmentGrammar{grammar | repeating: false, optional: false}
attempt_selection = %HL7.Selection{selection | complete: false}
found_once = follow_grammar(grammar_once, attempt_selection)
case found_once.fed && found_once.complete && !found_once.broken do
true ->
collect_copies(grammar, found_once)
false ->
selection
end
end
defp next_segment_type([[segment_type | _segment_data] | _tail_segments] = _source) do
segment_type
end
defp replace_selections([%HL7.Selection{valid: false} = selection | tail], function, result) do
replace_selections(tail, function, [selection | result])
end
defp replace_selections([%HL7.Selection{} = selection | tail], func, result) do
query = %HL7.Query{selections: [selection]}
replaced_segments = func.(query)
new_selection = %HL7.Selection{selection | segments: replaced_segments}
replace_selections(tail, func, [new_selection | result])
end
defp replace_selections([], _, result) do
result |> Enum.reverse()
end
defp replace_parts_in_selections(
[%HL7.Selection{valid: false} = selection | tail],
selection_transform,
result
) do
replace_parts_in_selections(tail, selection_transform, [selection | result])
end
defp replace_parts_in_selections(
[%HL7.Selection{} = selection | tail],
selection_transform,
result
) do
query = %HL7.Query{selections: [selection]}
replaced_segments = selection_transform.(query)
new_selection = %HL7.Selection{selection | segments: replaced_segments}
replace_parts_in_selections(tail, selection_transform, [new_selection | result])
end
defp replace_parts_in_selections([], _, result) do
result |> Enum.reverse()
end
defp associate_selections([%HL7.Selection{valid: false} = selection | tail], func, result) do
associate_selections(tail, func, [selection | result])
end
defp associate_selections([selection | tail], func, result) do
%HL7.Selection{data: data} = selection
query = %HL7.Query{selections: [selection]}
assignments = func.(query)
new_data =
Map.merge(data, assignments, fn
:index, v1, _v2 ->
Logger.warn("HL7 data :index cannot be overwritten (used for selection position).")
v1
_, _, v2 ->
v2
end)
new_selection = %HL7.Selection{selection | data: new_data}
associate_selections(tail, func, [new_selection | result])
end
defp associate_selections([], _, result) do
result |> Enum.reverse()
end
@spec extract_lists_for_message(HL7.Query.t()) :: [list()]
defp extract_lists_for_message(%HL7.Query{selections: selections}) do
# performant version of [prefix ++ segments ++ suffix]
selections
|> Enum.reduce([], fn m, acc ->
with_prefixes = Enum.reduce(m.prefix, acc, fn s, s_acc -> [s | s_acc] end)
with_segments = Enum.reduce(m.segments, with_prefixes, fn s, s_acc -> [s | s_acc] end)
Enum.reduce(m.suffix, with_segments, fn s, s_acc -> [s | s_acc] end)
end)
|> Enum.reject(fn s -> s == [] end)
|> Enum.reverse()
end
defp get_selection_transform(transform, indices) when is_function(transform) do
fn query ->
field_transform = fn current_value ->
query_with_part = %HL7.Query{query | part: current_value}
transform.(query_with_part)
end
segments = query.selections |> Enum.at(0) |> Map.get(:segments)
HL7.Message.update_segments(segments, indices, field_transform)
end
end
defp get_selection_transform(transform_value, indices) do
fn query ->
field_transform = fn _current_value -> transform_value end
segments = query.selections |> Enum.at(0) |> Map.get(:segments)
HL7.Message.update_segments(segments, indices, field_transform)
end
end
defp deselect_selection(%HL7.Selection{valid: false} = m) do
m
end
defp deselect_selection(%HL7.Selection{valid: true, segments: segments, suffix: suffix} = m) do
%HL7.Selection{m | segments: [], suffix: segments ++ suffix}
end
@spec perform_select(HL7.Query.t(), String.t() | (HL7.Query.t() -> as_boolean(term))) ::
HL7.Query.t()
defp perform_select(%HL7.Query{selections: selections, invalid_message: nil}, segment_selector)
when is_binary(segment_selector) do
grammar = HL7.SegmentGrammar.new(segment_selector)
sub_selections =
selections
|> Enum.map(&get_selections_within_a_selection(&1, grammar))
|> List.flatten()
%HL7.Query{selections: sub_selections}
end
defp perform_select(%HL7.Query{selections: selections, invalid_message: nil}, segment_selector)
when is_function(segment_selector) do
modified_selections =
selections
|> Enum.map(fn m ->
q = %HL7.Query{selections: [m]}
if !segment_selector.(q), do: deselect_selection(m), else: m
end)
%HL7.Query{selections: modified_selections}
end
defp perform_select(invalid_message_in_query, _segment_selector) do
invalid_message_in_query
end
end