defmodule HL7v2 do @moduledoc """ Pure Elixir HL7 v2.x toolkit. Schema-driven parsing, typed segment structs, programmatic message building, validation, and integrated MLLP transport. ## Parsing # Raw mode — canonical round-trip, delimiter-based {:ok, raw} = HL7v2.parse(text) # Typed mode — segments become structs {:ok, msg} = HL7v2.parse(text, mode: :typed) ## Building msg = HL7v2.new("ADT", "A01", sending_application: "PHAOS") |> HL7v2.Message.add_segment(%HL7v2.Segment.PID{...}) text = HL7v2.encode(msg) ## Acknowledgments {:ok, typed} = HL7v2.parse(wire, mode: :typed) msh = hd(typed.segments) {ack_msh, msa} = HL7v2.ack(msh) ## MLLP Transport {:ok, _} = HL7v2.MLLP.Listener.start_link(port: 2575, handler: MyHandler) """ alias HL7v2.{Parser, Telemetry} @doc """ Parses an HL7v2 message from a binary string. ## Options - `:mode` — `:raw` (default) or `:typed` - `:validate` — `true` to validate after parsing (default `false`) - `:copy` — `true` to copy all parsed binaries (prevents GC reference to original message binary). Use when storing parsed messages long-term. Default `false`. ## Examples {:ok, msg} = HL7v2.parse("MSH|^~\\\\&|...") {:ok, msg} = HL7v2.parse(text, mode: :typed, validate: true) {:ok, msg} = HL7v2.parse(text, copy: true) """ @spec parse(binary(), keyword()) :: {:ok, term()} | {:ok, term(), [map()]} | {:error, term()} def parse(text, opts \\ []) def parse(text, _opts) when not is_binary(text), do: {:error, :invalid_input} def parse(text, opts) do mode = Keyword.get(opts, :mode, :raw) validate? = Keyword.get(opts, :validate, false) Telemetry.span(:parse, %{mode: mode}, fn -> with {:ok, msg} <- Parser.parse(text, opts) do if validate? and mode == :typed do validate_opts = Keyword.take(opts, [:mode, :validate_tables]) case HL7v2.Validation.validate(msg, validate_opts) do :ok -> {:ok, msg} {:ok, warnings} -> {:ok, msg, warnings} {:error, _} = err -> err end else {:ok, msg} end end end) end @doc """ Encodes an HL7v2 message to wire format. Accepts: - `%HL7v2.RawMessage{}` — encodes directly - `%HL7v2.Message{}` — converts to raw via `Message.encode/1` - `%HL7v2.TypedMessage{}` — converts to raw via `TypedParser.to_raw/1`, then encodes ## Examples wire = HL7v2.encode(raw_message) wire = HL7v2.encode(builder_message) wire = HL7v2.encode(typed_message) """ @spec encode(HL7v2.RawMessage.t() | HL7v2.Message.t() | HL7v2.TypedMessage.t()) :: binary() def encode(%HL7v2.RawMessage{} = message) do Telemetry.span(:encode, %{type: :raw}, fn -> HL7v2.Encoder.encode(message) end) end def encode(%HL7v2.Message{} = message) do Telemetry.span(:encode, %{type: :message}, fn -> HL7v2.Message.encode(message) end) end def encode(%HL7v2.TypedMessage{} = message) do Telemetry.span(:encode, %{type: :typed}, fn -> message |> HL7v2.TypedParser.to_raw() |> HL7v2.Encoder.encode() end) end @doc """ Validates an HL7v2 typed message. Returns `:ok` when all validation rules pass, or `{:error, errors}` with a list of error/warning maps. Requires a `HL7v2.TypedMessage` -- raw messages return `{:error, :not_a_typed_message}`. ## Options - `:mode` -- `:lenient` (default) or `:strict` - `:validate_tables` -- `true` to check coded fields against HL7 tables (default `false`) ## Examples {:ok, msg} = HL7v2.parse(text, mode: :typed) :ok = HL7v2.validate(msg) # With table validation {:error, errors} = HL7v2.validate(msg, validate_tables: true) """ @spec validate(term(), keyword()) :: :ok | {:ok, [map()]} | {:error, [map()] | :not_a_typed_message} def validate(message, opts \\ []) def validate(%HL7v2.TypedMessage{} = message, opts) do HL7v2.Validation.validate(message, opts) end def validate(_message, _opts) do {:error, :not_a_typed_message} end @doc """ Builds a new HL7v2 message. Shortcut for `HL7v2.Message.new/3`. ## Options * `:sending_application` — string or `%HD{}` * `:sending_facility` — string or `%HD{}` * `:receiving_application` — string or `%HD{}` * `:receiving_facility` — string or `%HD{}` * `:message_control_id` — string (default: auto-generated) * `:processing_id` — string (default: `"P"`) * `:version_id` — string (default: `"2.5.1"`) ## Examples msg = HL7v2.new("ADT", "A01", sending_application: "PHAOS") """ @spec new(binary(), binary(), keyword()) :: HL7v2.Message.t() def new(code, event, opts \\ []) do HL7v2.Message.new(code, event, opts) end @doc "Gets a value from a typed message using a path string like `\"PID-5\"` or a `%HL7v2.Path{}` struct." @spec get(HL7v2.TypedMessage.t(), binary() | HL7v2.Path.t()) :: term() defdelegate get(msg, path), to: HL7v2.Access @doc "Gets a value from a typed message with a default." @spec get(HL7v2.TypedMessage.t(), binary() | HL7v2.Path.t(), term()) :: term() defdelegate get(msg, path, default), to: HL7v2.Access @doc """ Converts a `%HL7v2.RawMessage{}` to a `%HL7v2.TypedMessage{}`. Shortcut for `HL7v2.TypedParser.convert/1`. ## Examples {:ok, raw} = HL7v2.parse(wire) {:ok, typed} = HL7v2.type(raw) """ @spec type(HL7v2.RawMessage.t()) :: {:ok, HL7v2.TypedMessage.t()} | {:error, term()} def type(%HL7v2.RawMessage{} = raw) do HL7v2.TypedParser.convert(raw) end @doc "Fetches a value, returning `{:ok, value}` or `{:error, reason}`." @spec fetch(HL7v2.TypedMessage.t(), binary() | HL7v2.Path.t()) :: {:ok, term()} | {:error, atom()} defdelegate fetch(msg, path), to: HL7v2.Access @doc """ Builds an ACK (Application Accept) for the given MSH segment. Shortcut for `HL7v2.Ack.accept/2`. ## Options * `:text` — optional text message for MSA-3 * `:message_control_id` — override the generated ACK message control ID ## Examples {ack_msh, msa} = HL7v2.ack(original_msh) """ @spec ack(HL7v2.Segment.MSH.t(), keyword()) :: {HL7v2.Segment.MSH.t(), HL7v2.Segment.MSA.t()} def ack(msh, opts \\ []) do HL7v2.Ack.accept(msh, opts) end end