Conformance Profiles

Copy Markdown View Source

Conformance profiles let you validate HL7 v2 messages against organization-specific constraints beyond the base HL7 spec.

What is a profile?

A profile is a set of rules that extend the base HL7 schema:

  • Required segments — this message type MUST contain segment X
  • Forbidden segments — this message type MUST NOT contain segment Y
  • Required fields — beyond what the base spec requires (e.g. PID-18 is optional in the spec but required in your integration)
  • Table bindings — override which HL7 table a coded field must be in
  • Cardinality — this message must have between N and M occurrences of segment X
  • Value constraints — arbitrary predicates on field values
  • Custom rules — multi-segment business logic

Building a profile

Profiles are built via the functional HL7v2.Profile DSL:

profile =
  HL7v2.Profile.new("Hospital_ADT_A01", message_type: {"ADT", "A01"})
  |> HL7v2.Profile.require_segment("NK1")
  |> HL7v2.Profile.require_field("PID", 18)
  |> HL7v2.Profile.require_cardinality("OBX", min: 1, max: 10)
  |> HL7v2.Profile.bind_table("PV1", 14, "0069")
  |> HL7v2.Profile.require_value_in("PV1", 2, ["I", "O", "E"])
BuilderPurpose
require_segment/2segment must appear at least once
forbid_segment/2segment must not appear
require_field/3field must be populated when its segment is present
forbid_field/3field must be blank/absent when its segment is present
require_value/5field must equal a specific value (with optional :accessor)
require_value_in/5field must be in a specific allowed-value list
require_component/5composite field must have a specific component (or subcomponent) populated — supports :each_repetition, :subcomponent, :repetition
bind_table/4field value must be in a specific HL7 table (uses HL7v2.Standard.Tables)
require_cardinality/3segment must appear within a {min, max} range

All builders above store the constraint as data — no closures, no opaque function references. That means your profiles are:

  • Introspectable: inspect(profile) shows a full data tree
  • Serializable: the profile can be round-tripped through JSON/YAML/etc. (minus the optional :accessor function)
  • Diffable: two profiles can be compared structurally
  • Reusable: a profile built by one module can be composed, extended, or loaded by another without executing code

Custom rule escape hatches

For cases the declarative DSL can't express (custom predicates, multi-field invariants, cross-segment rules), two escape hatches remain:

  • add_value_constraint/4 — a 1-arity closure that receives the field value and returns true, false, or {:error, reason}.
  • add_rule/3 — a 1-arity closure that receives the whole typed message and returns a list of error maps.

Both still run as part of HL7v2.Validation.ProfileRules.check/2 and are fully integrated with the evaluation pipeline. Use them sparingly — the declarative DSL is preferred because closures are opaque to tooling.

Value pinning examples

# Simple equality pin
profile
|> HL7v2.Profile.require_value("PV1", 2, "N")

# Allowed-values list
profile
|> HL7v2.Profile.require_value_in("MSA", 1, ["AA", "AE", "AR"])

# Struct-component pin via :accessor
# (QPD-1 is a CE — validate only its identifier component)
profile
|> HL7v2.Profile.require_value("QPD", 1, "IHE PIX Query",
     accessor: & &1.identifier)

Component targeting examples

# "Every PID-3 repetition must carry CX-1 (ID Number)"
profile
|> HL7v2.Profile.require_component("PID", 3, 1,
     each_repetition: true)

# "Every PID-3 repetition must carry CX-4.1 (HD namespace_id)"
profile
|> HL7v2.Profile.require_component("PID", 3, 4,
     each_repetition: true, subcomponent: 1)

require_component currently supports the composite types registered in HL7v2.Profile.ComponentAccess: CX, HD, CE, CWE. Additional types are a one-line addition — open an issue or PR.

Validating with a profile

Pass the profile via the :profile option to HL7v2.validate/2:

case HL7v2.validate(msg, profile: profile) do
  :ok ->
    IO.puts("valid against profile")

  {:error, errors} ->
    for %{rule: rule, location: loc, message: msg} <- errors do
      IO.puts("[\#{rule}] \#{loc}: \#{msg}")
    end
end

You can also pass a list of profiles:

HL7v2.validate(msg, profile: [profile_1, profile_2, profile_3])

Profiles with a specific :message_type only apply to matching messages — a profile keyed to {"ADT", "A01"} is silently skipped for an ORU_R01 message.

Example profiles

The library ships with example profiles in HL7v2.Profiles.Examples:

  • hospital_adt_a01/0 — strict hospital ADT profile
  • ihe_lab_oru_r01/0 — IHE-style lab results profile

Copy and customize these for your use case.

Profile error shape

Profile violations follow the standard validation error shape plus two profile-specific keys:

%{
  level: :error,              # or :warning
  location: "PID",            # segment ID
  field: :patient_account_number,  # field name (if applicable)
  message: "profile requires PID-18 to be populated",
  rule: :require_field,       # which rule type fired
  profile: "Hospital_ADT_A01" # which profile produced this error
}

This makes it easy to trace violations back to a specific profile when multiple profiles are active.

Custom business rules

For complex multi-segment rules, use add_rule/3:

profile
|> HL7v2.Profile.add_rule(:guarantor_required_for_inpatient, fn msg ->
  has_inpatient_pv1? = ...  # your logic
  has_gt1? = ...

  if has_inpatient_pv1? and not has_gt1? do
    [%{
      level: :error,
      location: "GT1",
      field: nil,
      message: "guarantor required for inpatient visits"
    }]
  else
    []
  end
end)

Custom rule functions receive the full %HL7v2.TypedMessage{} and return a list of error maps. Exceptions inside custom rules are caught and treated as silent — they don't crash validation.