HL7v2.Profile (HL7v2 v3.10.1)

Copy Markdown View Source

Conformance profile for constraining HL7v2 message structures beyond the base HL7 spec.

A profile lets integrators express "our hospital's version of ADT_A01" in terms of segment requirements, field constraints, table bindings, cardinality, and value predicates. Profiles are evaluated alongside structural, field, and conditional validation via HL7v2.validate/2:

profile =
  HL7v2.Profile.new("MyHospital_ADT_A01", message_type: {"ADT", "A01"})
  |> HL7v2.Profile.require_segment("ROL")
  |> HL7v2.Profile.require_field("PID", 18)
  |> HL7v2.Profile.bind_table("PV1", 14, "0069")
  |> HL7v2.Profile.require_cardinality("OBX", min: 1, max: 10)

HL7v2.validate(msg, profile: profile)

Profiles are pure data — no code execution at load time. Custom business rules can be added via add_rule/3 which takes a function that receives the typed message and returns a list of error maps.

Summary

Functions

Adds a custom business rule. The rule function receives the full typed message and returns a list of error maps.

Adds a value constraint for a field. The constraint function receives the parsed field value and returns true, false, or {:error, message}.

Returns true if this profile applies to the given message type tuple. A nil message_type (wildcard) matches any type.

Binds a coded field to a specific HL7 table, overriding any base binding.

Forbids a specific field within a segment from being populated.

Forbids a segment from appearing in the message.

Returns the sorted list of forbidden segment IDs.

Creates a new empty profile.

Sets cardinality constraints on a segment.

Requires a specific component (or subcomponent) within a field to be populated.

Requires a specific field in a segment to be populated.

Requires a segment to appear at least once in the message.

Pins a field to a specific expected value.

Returns the sorted list of required segment IDs.

Types

cardinality()

@type cardinality() :: {non_neg_integer(), non_neg_integer() | :unbounded}

component_spec()

@type component_spec() :: {segment_id(), field_seq(), pos_integer(), keyword()}

custom_rule()

@type custom_rule() :: (HL7v2.TypedMessage.t() -> [error()])

error()

@type error() :: %{
  level: :error | :warning,
  location: String.t(),
  field: atom() | nil,
  message: String.t(),
  rule: atom(),
  profile: String.t()
}

field_seq()

@type field_seq() :: pos_integer()

segment_id()

@type segment_id() :: String.t()

t()

@type t() :: %HL7v2.Profile{
  cardinality_constraints: %{required(segment_id()) => cardinality()},
  custom_rules: [{atom(), custom_rule()}],
  description: String.t(),
  field_table_bindings: %{required({segment_id(), field_seq()}) => table_id()},
  forbidden_fields: MapSet.t({segment_id(), field_seq()}),
  forbidden_segments: MapSet.t(segment_id()),
  message_type: {String.t(), String.t()} | nil,
  name: String.t(),
  required_components: [component_spec()],
  required_fields: %{required({segment_id(), field_seq()}) => :required},
  required_segments: MapSet.t(segment_id()),
  required_values: %{required({segment_id(), field_seq()}) => value_spec()},
  value_constraints: %{
    required({segment_id(), field_seq()}) => (term() ->
                                                boolean() | {:error, String.t()})
  },
  version: String.t() | nil
}

table_id()

@type table_id() :: String.t() | atom()

value_spec()

@type value_spec() :: {:eq, term(), keyword()} | {:in, [term()], keyword()}

Functions

add_rule(profile, rule_name, fun)

@spec add_rule(t(), atom(), custom_rule()) :: t()

Adds a custom business rule. The rule function receives the full typed message and returns a list of error maps.

add_value_constraint(profile, segment_id, field_seq, fun)

@spec add_value_constraint(
  t(),
  segment_id(),
  field_seq(),
  (term() -> boolean() | {:error, String.t()})
) :: t()

Adds a value constraint for a field. The constraint function receives the parsed field value and returns true, false, or {:error, message}.

applies_to?(profile, msg_type)

@spec applies_to?(t(), {String.t(), String.t()} | nil) :: boolean()

Returns true if this profile applies to the given message type tuple. A nil message_type (wildcard) matches any type.

bind_table(profile, segment_id, field_seq, table_id)

@spec bind_table(t(), segment_id(), field_seq(), table_id()) :: t()

Binds a coded field to a specific HL7 table, overriding any base binding.

The field's value is validated against HL7v2.Standard.Tables at check time. table_id may be an integer (69), a zero-padded string ("0069"), or a non-padded numeric string ("69"). Non-numeric table IDs (e.g. site-local codes like "LOCAL_SEX") are silently skipped — only the HL7 standard numeric tables are currently enforced. Unknown numeric IDs also silently pass — consistent with HL7v2.Standard.Tables.validate/2.

For struct-valued fields (CE, CWE, CX, HD, and any other composite type registered in HL7v2.Profile.ComponentAccess) the binding validates the first component of the struct (the identifier / id / namespace_id). Raw string fields are validated directly. Structs of unfamiliar types silently pass. Use add_value_constraint/4 for more elaborate shapes.

Before v3.10.0, this builder stored the binding but never enforced it. Profiles that relied on the silent behavior now produce :bind_table errors at validation time. Flag as a breaking change when upgrading.

Examples

iex> HL7v2.Profile.new("p")
...> |> HL7v2.Profile.bind_table("PV1", 14, "0069")
...> |> Map.get(:field_table_bindings)
%{{"PV1", 14} => "0069"}

forbid_field(profile, segment_id, field_seq)

@spec forbid_field(t(), segment_id(), field_seq()) :: t()

Forbids a specific field within a segment from being populated.

IHE profiles routinely mark base-HL7 fields as "X" (not supported). For example, MSH-8 (Security) and EVN-1 (Event Type Code) are forbidden in IHE PAM; ORC-7 (Quantity/Timing) is forbidden in IHE LAB in favor of TQ1.

The rule fires only when the field is present with a non-blank value. A missing segment is silently ignored (use require_segment/2 if absence should also be an error).

Examples

iex> HL7v2.Profile.new("p")
...> |> HL7v2.Profile.forbid_field("MSH", 8)
...> |> Map.get(:forbidden_fields)
...> |> MapSet.to_list()
[{"MSH", 8}]

forbid_segment(profile, segment_id)

@spec forbid_segment(t(), segment_id()) :: t()

Forbids a segment from appearing in the message.

forbidden_segments?(profile)

@spec forbidden_segments?(t()) :: [segment_id()]

Returns the sorted list of forbidden segment IDs.

new(name, opts \\ [])

@spec new(
  String.t(),
  keyword()
) :: t()

Creates a new empty profile.

Options

  • :message_type — tuple {code, event}, e.g. {"ADT", "A01"}. When set, the profile only applies to messages matching this type. Nil means the profile applies to any message.
  • :version — HL7 version this profile targets (e.g. "2.5.1"). Nil means any version.
  • :description — human-readable description.

Examples

iex> profile = HL7v2.Profile.new("Minimal", message_type: {"ADT", "A01"})
iex> profile.name
"Minimal"
iex> profile.message_type
{"ADT", "A01"}

require_cardinality(profile, segment_id, opts)

@spec require_cardinality(t(), segment_id(), keyword()) :: t()

Sets cardinality constraints on a segment.

Examples

iex> HL7v2.Profile.new("p") |> HL7v2.Profile.require_cardinality("OBX", min: 1, max: 10)
...> |> Map.get(:cardinality_constraints)
%{"OBX" => {1, 10}}

iex> HL7v2.Profile.new("p") |> HL7v2.Profile.require_cardinality("NTE", min: 0, max: :unbounded)
...> |> Map.get(:cardinality_constraints)
%{"NTE" => {0, :unbounded}}

require_component(profile, segment_id, field_seq, component, opts \\ [])

@spec require_component(t(), segment_id(), field_seq(), pos_integer(), keyword()) ::
  t()

Requires a specific component (or subcomponent) within a field to be populated.

This is the declarative replacement for bespoke add_rule/3 closures that walk into composite type structs to check individual components (CX-1, CX-4, CE-3, HD.namespace_id, etc.). The rule produces targeted error messages like:

IHE requires PID-3[2].CX-4 (Assigning Authority) to be populated

Options

  • :each_repetition — when true and the target field is a repeating field (list), the rule validates every repetition (emitting one error per missing occurrence). When false (the default), only the first repetition is validated.
  • :subcomponent — 1-indexed subcomponent position within the selected component. Use this when the component is itself a composite struct (e.g. CX-4 Assigning Authority is an HD, so subcomponent: 1 targets HD.namespace_id).
  • :repetition — 1-indexed single repetition to validate (mutually exclusive with :each_repetition). Defaults to repetition 1.

The rule is silent when the segment is missing or the field is blank — compose with require_segment/2 and/or require_field/3 if absence of the containing element should also be an error.

Currently supports composite types registered in HL7v2.Profile.ComponentAccess: CX, HD, CE, CWE. Unknown types produce a clean error pointing at the gap.

Examples

# "PID-3 repetition N must carry CX-1 (ID Number)"
iex> HL7v2.Profile.new("p")
...> |> HL7v2.Profile.require_component("PID", 3, 1, each_repetition: true)
...> |> Map.get(:required_components)
[{"PID", 3, 1, [each_repetition: true]}]

# "PID-3.4.1 (HD.namespace_id) required for every repetition"
iex> HL7v2.Profile.new("p")
...> |> HL7v2.Profile.require_component("PID", 3, 4,
...>      each_repetition: true, subcomponent: 1)
...> |> Map.get(:required_components)
[{"PID", 3, 4, [each_repetition: true, subcomponent: 1]}]

require_field(profile, segment_id, field_seq)

@spec require_field(t(), segment_id(), field_seq()) :: t()

Requires a specific field in a segment to be populated.

Examples

iex> HL7v2.Profile.new("p")
...> |> HL7v2.Profile.require_field("PID", 18)
...> |> Map.get(:required_fields)
%{{"PID", 18} => :required}

require_segment(profile, segment_id)

@spec require_segment(t(), segment_id()) :: t()

Requires a segment to appear at least once in the message.

Examples

iex> HL7v2.Profile.new("p")
...> |> HL7v2.Profile.require_segment("ROL")
...> |> HL7v2.Profile.required_segments?()
["ROL"]

require_value(profile, segment_id, field_seq, expected, opts \\ [])

@spec require_value(t(), segment_id(), field_seq(), term(), keyword()) :: t()

Pins a field to a specific expected value.

Unlike add_value_constraint/4, which takes a closure, this stores the expected value as data. Profiles built with require_value/4 remain introspectable — you can serialize, diff, or audit them without executing functions.

Options

  • :accessor — a 1-arity function applied to the parsed field value before the equality check. Use this to target a struct component, e.g. accessor: & &1.identifier to pin the first component of a CE. Defaults to the identity function.

Examples

iex> HL7v2.Profile.new("p")
...> |> HL7v2.Profile.require_value("PV1", 2, "N")
...> |> Map.get(:required_values)
%{{"PV1", 2} => {:eq, "N", []}}

iex> accessor = & &1.identifier
iex> profile =
...>   HL7v2.Profile.new("p")
...>   |> HL7v2.Profile.require_value("QPD", 1, "IHE PIX Query", accessor: accessor)
iex> {:eq, "IHE PIX Query", opts} = profile.required_values[{"QPD", 1}]
iex> is_function(Keyword.fetch!(opts, :accessor), 1)
true

require_value_in(profile, segment_id, field_seq, allowed, opts \\ [])

@spec require_value_in(t(), segment_id(), field_seq(), [term()], keyword()) :: t()

Pins a field to a set of allowed values.

Like require_value/4 but accepts a list — the field's (possibly accessor-transformed) value must be a member of allowed.

Options

  • :accessor — 1-arity function applied before the membership test.

Examples

iex> HL7v2.Profile.new("p")
...> |> HL7v2.Profile.require_value_in("MSA", 1, ["AA", "AE", "AR"])
...> |> Map.get(:required_values)
%{{"MSA", 1} => {:in, ["AA", "AE", "AR"], []}}

required_segments?(profile)

@spec required_segments?(t()) :: [segment_id()]

Returns the sorted list of required segment IDs.