Packages
hl7v2
0.5.6
3.10.1
3.9.0
3.8.0
3.7.0
3.6.0
3.5.0
3.4.0
3.3.6
3.3.5
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.0
3.1.1
3.1.0
3.0.2
3.0.1
3.0.0
2.11.0
2.10.0
2.9.1
2.9.0
2.8.2
2.8.1
2.8.0
2.7.1
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.3
2.1.2
2.1.1
2.1.0
1.4.6
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.6.0
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.1.0
Pure Elixir HL7 v2.x toolkit — schema-driven parsing, typed segments, message builder, MLLP transport
Current section
Files
Jump to
Current section
Files
lib/hl7v2/segment.ex
defmodule HL7v2.Segment do
@moduledoc """
Base behaviour and helpers for typed HL7v2 segments.
Provides a `__using__` macro that generates struct, `parse/2`, and `encode/1`
from a declarative field definition list — zero boilerplate per segment.
## Defining a Segment
defmodule HL7v2.Segment.MSA do
use HL7v2.Segment,
id: "MSA",
fields: [
{1, :acknowledgment_code, HL7v2.Type.ID, :r, 1},
{2, :message_control_id, HL7v2.Type.ST, :r, 1},
{3, :text_message, HL7v2.Type.ST, :o, 1},
...
]
end
## Field Definitions
Each field is a tuple `{sequence, name, type, optionality, max_reps}`:
- `sequence` — 1-based field position per HL7 spec
- `name` — atom used as struct key
- `type` — type module (e.g. `HL7v2.Type.ST`) or `:raw` for pass-through
- `optionality` — `:r` (required), `:o` (optional), `:c` (conditional), `:b` (backwards-compat/withdrawn)
- `max_reps` — `1` for non-repeating, `:unbounded` or integer for repeating
"""
@type optionality :: :r | :o | :c | :b
@type max_reps :: pos_integer() | :unbounded
@type field_def :: {pos_integer(), atom(), module() | :raw, optionality(), max_reps()}
@callback fields() :: [field_def()]
@callback segment_id() :: binary()
@callback parse(list(), HL7v2.Separator.t()) :: struct()
@callback encode(struct()) :: list()
@compile {:inline, parse_field_value: 3, encode_field_value: 3, composite_type?: 1}
@composite_types MapSet.new([
HL7v2.Type.AUI,
HL7v2.Type.CNN,
HL7v2.Type.CP,
HL7v2.Type.CQ,
HL7v2.Type.CX,
HL7v2.Type.DLD,
HL7v2.Type.DLN,
HL7v2.Type.NDL,
HL7v2.Type.XCN,
HL7v2.Type.XPN,
HL7v2.Type.XAD,
HL7v2.Type.XTN,
HL7v2.Type.CE,
HL7v2.Type.CWE,
HL7v2.Type.EIP,
HL7v2.Type.ERL,
HL7v2.Type.FC,
HL7v2.Type.HD,
HL7v2.Type.JCC,
HL7v2.Type.MO,
HL7v2.Type.MOC,
HL7v2.Type.PL,
HL7v2.Type.PRL,
HL7v2.Type.EI,
HL7v2.Type.MSG,
HL7v2.Type.PT,
HL7v2.Type.VID,
HL7v2.Type.CNE,
HL7v2.Type.XON,
HL7v2.Type.FN,
HL7v2.Type.SAD,
HL7v2.Type.DR,
HL7v2.Type.TS,
HL7v2.Type.NR
])
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@behaviour HL7v2.Segment
@segment_id Keyword.fetch!(opts, :id)
@segment_fields Keyword.fetch!(opts, :fields)
@struct_keys Enum.map(@segment_fields, fn {_seq, name, _type, _opt, _reps} -> name end)
defstruct @struct_keys ++ [extra_fields: []]
@type t :: %__MODULE__{}
@impl HL7v2.Segment
def fields, do: @segment_fields
@impl HL7v2.Segment
def segment_id, do: @segment_id
@impl HL7v2.Segment
@spec parse(list(), HL7v2.Separator.t()) :: t()
def parse(raw_fields, separators \\ HL7v2.Separator.default()) do
HL7v2.Segment.do_parse(__MODULE__, @segment_fields, raw_fields, separators)
end
@impl HL7v2.Segment
@spec encode(t()) :: list()
def encode(%__MODULE__{} = segment) do
HL7v2.Segment.do_encode(segment, @segment_fields)
end
defoverridable parse: 1, parse: 2, encode: 1
end
end
# --- Runtime Parse/Encode Helpers ---
@doc false
@spec do_parse(module(), [field_def()], list(), HL7v2.Separator.t()) :: struct()
def do_parse(module, field_defs, raw_fields, separators \\ HL7v2.Separator.default()) do
sep = <<separators.sub_component>>
ensure_sub_component_separator(sep, fn ->
attrs =
Enum.map(field_defs, fn {seq, name, type, _opt, max_reps} ->
raw = Enum.at(raw_fields, seq - 1)
{name, parse_field_value(raw, type, max_reps)}
end)
max_seq = max_declared_seq(field_defs)
extra = Enum.drop(raw_fields, max_seq)
struct(module, [{:extra_fields, extra} | attrs])
end)
end
@doc false
@spec do_encode(struct(), [field_def()], HL7v2.Separator.t()) :: list()
def do_encode(segment, field_defs, separators \\ HL7v2.Separator.default()) do
sep = <<separators.sub_component>>
ensure_sub_component_separator(sep, fn ->
declared =
Enum.map(field_defs, fn {_seq, name, type, _opt, max_reps} ->
value = Map.get(segment, name)
encode_field_value(value, type, max_reps)
end)
extra = Map.get(segment, :extra_fields) || []
(declared ++ extra) |> trim_trailing()
end)
end
# --- Field-Level Parse ---
@doc false
@spec parse_field_value(term(), module() | :raw, max_reps()) :: term()
def parse_field_value(nil, _type, _max_reps), do: nil
def parse_field_value("", _type, _max_reps), do: nil
def parse_field_value(raw, :raw, _max_reps), do: raw
def parse_field_value(raw, type, 1) do
parse_single(raw, type)
end
def parse_field_value(raw, type, _max_reps) do
parse_repeating(raw, type)
end
defp parse_single(raw, type) do
if composite_type?(type) do
components = if is_binary(raw), do: [raw], else: raw
type.parse(components)
else
case raw do
# Primitive with extra components (e.g., "M^EXTRA" on an IS field).
# Non-conformant input — preserve the raw component list for lossless
# round-trip. The encoder emits lists as component-separated values.
[_first | rest] = list when rest != [] ->
list
[first] ->
type.parse(first || "")
value when is_binary(value) ->
type.parse(value)
_ ->
type.parse("")
end
end
end
defp parse_repeating(raw, type) when is_binary(raw) do
[parse_single(raw, type)]
end
defp parse_repeating(raw, type) when is_list(raw) do
if composite_type?(type) do
if all_lists?(raw) do
# Multiple repetitions, each a list of components
Enum.map(raw, &type.parse/1)
else
# Single composite value (components), not repeated
[type.parse(raw)]
end
else
# Repeated primitive — each element is a string value
Enum.map(raw, fn
v when is_binary(v) -> type.parse(v)
[_first | rest] = list when rest != [] -> list
[first] -> type.parse(first || "")
_ -> nil
end)
end
end
# --- Field-Level Encode ---
@doc false
@spec encode_field_value(term(), module() | :raw, max_reps()) :: term()
def encode_field_value(nil, _type, _max_reps), do: ""
def encode_field_value(value, :raw, _max_reps), do: value || ""
def encode_field_value(value, type, 1) do
encode_single(value, type)
end
def encode_field_value(values, type, _max_reps) when is_list(values) do
case values do
[] ->
""
[single] ->
# Single repetition — encode directly (no repetition wrapper needed)
encode_single(single, type)
multiple ->
# Multiple repetitions
Enum.map(multiple, fn v -> wrap_for_repetition(encode_single(v, type)) end)
end
end
# Single value in a repeating field slot — encode directly
def encode_field_value(value, type, _max_reps) do
encode_single(value, type)
end
defp encode_single(nil, _type), do: ""
# Primitive fields with extra components preserved as raw lists (non-conformant input).
# Emit as component list for the encoder to rejoin with ^.
defp encode_single(value, _type) when is_list(value), do: value
defp encode_single(value, type), do: type.encode(value)
# Ensure repeated values produce list-of-lists for the raw encoder.
# Composite types already return lists from encode. Primitive types return binaries,
# which need wrapping in a list so the encoder treats them as repetitions.
defp wrap_for_repetition(value) when is_list(value), do: value
defp wrap_for_repetition(value) when is_binary(value), do: [value]
# --- Helpers ---
@doc false
@spec composite_type?(module()) :: boolean()
def composite_type?(type), do: MapSet.member?(@composite_types, type)
defp all_lists?(list), do: Enum.all?(list, &is_list/1)
@doc false
@spec trim_trailing(list()) :: list()
def trim_trailing(list) do
list
|> Enum.reverse()
|> Enum.drop_while(&empty_field?/1)
|> Enum.reverse()
end
defp empty_field?(""), do: true
defp empty_field?(nil), do: true
defp empty_field?([]), do: true
defp empty_field?(_), do: false
# Sets the sub-component separator only if no separator context is already
# active (i.e., a caller like TypedParser.to_raw has not already set it).
# This prevents do_parse/do_encode from overriding an outer separator context.
defp ensure_sub_component_separator(sep, fun) do
if Process.get(:hl7v2_sub_component_sep) do
# Separator already set by an outer context — don't override
fun.()
else
HL7v2.Type.with_sub_component_separator(sep, fun)
end
end
@doc false
@spec max_declared_seq([field_def()]) :: non_neg_integer()
def max_declared_seq([]), do: 0
def max_declared_seq(field_defs) do
field_defs
|> Enum.map(fn {seq, _, _, _, _} -> seq end)
|> Enum.max()
end
end