# `HL7v2.Profile`
[🔗](https://github.com/Balneario-de-Cofrentes/hl7v2/blob/v3.10.1/lib/hl7v2/profile.ex#L1)

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.

# `cardinality`

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

# `component_spec`

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

# `custom_rule`

```elixir
@type custom_rule() :: (HL7v2.TypedMessage.t() -&gt; [error()])
```

# `error`

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

# `field_seq`

```elixir
@type field_seq() :: pos_integer()
```

# `segment_id`

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

# `t`

```elixir
@type t() :: %HL7v2.Profile{
  cardinality_constraints: %{required(segment_id()) =&gt; cardinality()},
  custom_rules: [{atom(), custom_rule()}],
  description: String.t(),
  field_table_bindings: %{required({segment_id(), field_seq()}) =&gt; 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()}) =&gt; :required},
  required_segments: MapSet.t(segment_id()),
  required_values: %{required({segment_id(), field_seq()}) =&gt; value_spec()},
  value_constraints: %{
    required({segment_id(), field_seq()}) =&gt; (term() -&gt;
                                                boolean() | {:error, String.t()})
  },
  version: String.t() | nil
}
```

# `table_id`

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

# `value_spec`

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

# `add_rule`

```elixir
@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`

```elixir
@spec add_value_constraint(
  t(),
  segment_id(),
  field_seq(),
  (term() -&gt; 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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Forbids a segment from appearing in the message.

# `forbidden_segments?`

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

Returns the sorted list of forbidden segment IDs.

# `new`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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?`

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

Returns the sorted list of required segment IDs.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
