Packages
hl7v2
3.3.2
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/validation.ex
defmodule HL7v2.Validation do
@moduledoc """
Validates HL7v2 typed messages.
Opt-in validation that returns accumulated errors without blocking parsing.
Call `validate/1` on a `HL7v2.TypedMessage` to check message-level and
field-level rules.
## Examples
{:ok, msg} = HL7v2.parse(text, mode: :typed)
:ok = HL7v2.Validation.validate(msg)
{:error, errors} = HL7v2.Validation.validate(invalid_msg)
# errors is a list of %{level: :error | :warning, location: ..., field: ..., message: ...}
"""
alias HL7v2.MessageDefinition
alias HL7v2.TypedMessage
alias HL7v2.Validation.{MessageRules, FieldRules}
@doc """
Validates a typed message.
Returns `:ok` when no issues are found, `{:ok, warnings}` when only
warnings are present (non-fatal), or `{:error, errors}` when errors exist.
Runs three validation passes:
1. **Message rules** — MSH presence, required MSH fields
2. **Structural rules** — segment ordering, group anchors, cardinality
for all 186 official v2.5.1 message structures. Unsupported structures produce a
warning in lenient mode or an error in strict mode.
3. **Field rules** — required fields, max repetitions per segment
Each error map has:
- `:level` — `:error` or `:warning`
- `:location` — segment identifier (e.g., `"MSH"`, `"PID"`, `"message"`)
- `:field` — field name atom or `nil` for structural issues
- `:message` — human-readable description
## Options
- `:mode` — `:lenient` (default) or `:strict`. In lenient mode, ordering
and cardinality issues are warnings. In strict mode, all structural
violations are errors.
- `:validate_tables` — `true` to check coded fields against HL7-defined
tables. Defaults to `false`.
"""
@spec validate(TypedMessage.t(), keyword()) :: :ok | {:error, [map()]} | {:ok, [map()]}
def validate(%TypedMessage{} = msg, opts \\ []) do
mode = Keyword.get(opts, :mode, :lenient)
context = extract_trigger_context(msg.segments)
field_opts =
Keyword.take(opts, [:validate_tables, :mode])
|> Keyword.put(:mode, mode)
|> Keyword.put(:context, context)
all =
MessageRules.check(msg) ++
structure_errors(msg, mode) ++
Enum.flat_map(msg.segments, &FieldRules.check(&1, field_opts))
errors = Enum.filter(all, &(&1.level == :error))
warnings = Enum.filter(all, &(&1.level == :warning))
case {errors, warnings} do
{[], []} -> :ok
{[], warnings} -> {:ok, warnings}
{errors, warnings} -> {:error, errors ++ warnings}
end
end
defp structure_errors(%TypedMessage{segments: segments}, mode) do
structure_name = extract_message_structure(segments)
segment_ids = extract_segment_ids(segments)
# Prefer structural validation (order + groups) when definition exists
case HL7v2.Standard.MessageStructure.get(structure_name) do
%{} = struct_def ->
HL7v2.Validation.Structural.validate(struct_def, segment_ids, mode: mode)
nil ->
# No group-aware structure definition exists.
# In strict mode, unsupported structures are errors.
case MessageDefinition.validate_structure(structure_name, segment_ids) do
:ok ->
[]
{:error, results} ->
if mode == :strict do
Enum.map(results, fn
%{level: :warning} = r -> %{r | level: :error}
r -> r
end)
else
results
end
end
end
end
defp extract_message_structure([%HL7v2.Segment.MSH{message_type: %HL7v2.Type.MSG{} = msg} | _]) do
# Always canonicalize via message_code + trigger_event first. MSH-9.3 may
# carry a non-canonical alias (e.g., "SIU_S14" instead of "SIU_S12") that
# won't match the structure registry. Fall back to MSH-9.3 only when
# canonical resolution yields a default "CODE_EVENT" that isn't registered.
canonical = canonicalize_structure(msg.message_code, msg.trigger_event)
cond do
canonical != nil -> canonical
msg.message_structure != nil -> msg.message_structure
true -> nil
end
end
defp extract_message_structure(_), do: nil
defp canonicalize_structure(code, event) when is_binary(code) and is_binary(event) do
resolved = MessageDefinition.canonical_structure(code, event)
cond do
# Canonical resolution found a registered structure
HL7v2.Standard.MessageStructure.get(resolved) != nil ->
resolved
# Fallback: the bare message_code is itself a registered structure.
# Handles cases like ACK^A01^ACK_A01 — ACK_A01 isn't registered, but
# ACK is. Also covers ACK^A02^ACK_A02, ACK^A08^ACK_A08, etc.
HL7v2.Standard.MessageStructure.get(code) != nil ->
code
true ->
nil
end
end
defp canonicalize_structure(_code, _event), do: nil
defp extract_trigger_context([%HL7v2.Segment.MSH{message_type: %HL7v2.Type.MSG{} = msg} | _]) do
%{trigger_event: msg.trigger_event, message_code: msg.message_code}
end
defp extract_trigger_context(_), do: %{}
defp extract_segment_ids(segments) do
Enum.map(segments, fn
%HL7v2.Segment.ZXX{segment_id: id} -> id
%{__struct__: module} -> module.segment_id()
{name, _fields} when is_binary(name) -> name
end)
end
end