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.
Pins a field to a set of allowed values.
Returns the sorted list of required segment IDs.
Types
@type cardinality() :: {non_neg_integer(), non_neg_integer() | :unbounded}
@type component_spec() :: {segment_id(), field_seq(), pos_integer(), keyword()}
@type custom_rule() :: (HL7v2.TypedMessage.t() -> [error()])
@type field_seq() :: pos_integer()
@type segment_id() :: String.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 }
Functions
@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.
@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}.
Returns true if this profile applies to the given message type tuple. A nil message_type (wildcard) matches any type.
@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_tableerrors 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"}
@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}]
@spec forbid_segment(t(), segment_id()) :: t()
Forbids a segment from appearing in the message.
@spec forbidden_segments?(t()) :: [segment_id()]
Returns the sorted list of forbidden segment IDs.
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"}
@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}}
@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 populatedOptions
:each_repetition— whentrueand the target field is a repeating field (list), the rule validates every repetition (emitting one error per missing occurrence). Whenfalse(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, sosubcomponent: 1targets 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]}]
@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}
@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"]
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.identifierto 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
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"], []}}
@spec required_segments?(t()) :: [segment_id()]
Returns the sorted list of required segment IDs.