README.md

# fhir_ex

An Elixir library for working with [FHIR R5](https://www.hl7.org/fhir/R5/) resources. Provides typed structs, JSON serialization, and field-level validation for the resources and data types used in **lab exam ordering** and **patient admissions** workflows.

## Features

- Typed Elixir structs for all FHIR R5 data types and resources
- JSON encoding (`struct → JSON`) and decoding (`JSON → struct`) via [Jason](https://github.com/michalmuskala/jason)
- Nil fields omitted from JSON output; `resourceType` injected automatically
- Polymorphic `[x]` fields (e.g. `value[x]`, `occurrence[x]`) represented as tagged tuples
- Validation returning `{:ok, struct}` or `{:validation_error, [errors]}` with JSON-path error locations
- Recursive nested validation — errors from deep inside a struct carry their full path

## Installation

Add `fhir_ex` to your dependencies:

```elixir
def deps do
  [
    {:fhir_ex, "~> 0.1.0"}
  ]
end
```

## Quick start

### Lab order (ServiceRequest)

```elixir
alias FhirEx.Resources.ServiceRequest
alias FhirEx.Types.{CodeableConcept, Coding, Identifier, Reference, Annotation}

order = %ServiceRequest{
  id: "order-001",
  identifier: [
    %Identifier{system: "http://hospital.org/orders", value: "ORD-2024-0042"}
  ],
  status: "active",
  intent: "order",
  priority: "routine",
  code: %CodeableConcept{
    coding: [%Coding{system: "http://loinc.org", code: "58410-2", display: "CBC panel"}],
    text: "Complete Blood Count"
  },
  subject: %Reference{reference: "Patient/patient-001"},
  encounter: %Reference{reference: "Encounter/enc-001"},
  requester: %Reference{reference: "Practitioner/pract-001"},
  authored_on: "2024-01-15T08:00:00Z",
  occurrence: {:date_time, "2024-01-15T10:00:00Z"},
  note: [%Annotation{text: "Patient must fast for 8 hours before blood draw."}]
}

json = FhirEx.JSON.encode!(order)
```

<details>
<summary>JSON output</summary>

```json
{
  "resourceType": "ServiceRequest",
  "id": "order-001",
  "status": "active",
  "intent": "order",
  "priority": "routine",
  "authoredOn": "2024-01-15T08:00:00Z",
  "occurrenceDateTime": "2024-01-15T10:00:00Z",
  "code": {
    "coding": [{"system": "http://loinc.org", "code": "58410-2", "display": "CBC panel"}],
    "text": "Complete Blood Count"
  },
  "subject": {"reference": "Patient/patient-001"},
  "encounter": {"reference": "Encounter/enc-001"},
  "requester": {"reference": "Practitioner/pract-001"},
  "identifier": [{"system": "http://hospital.org/orders", "value": "ORD-2024-0042"}],
  "note": [{"text": "Patient must fast for 8 hours before blood draw."}]
}
```

</details>

### Lab result (Observation)

```elixir
alias FhirEx.Resources.Observation
alias FhirEx.Types.{CodeableConcept, Coding, Reference, Quantity}

observation = %Observation{
  id: "obs-hemoglobin",
  status: "final",
  category: [
    %CodeableConcept{
      coding: [%Coding{
        system: "http://terminology.hl7.org/CodeSystem/observation-category",
        code: "laboratory"
      }]
    }
  ],
  code: %CodeableConcept{
    coding: [%Coding{system: "http://loinc.org", code: "718-7", display: "Hemoglobin"}],
    text: "Hemoglobin"
  },
  subject: %Reference{reference: "Patient/patient-001"},
  encounter: %Reference{reference: "Encounter/enc-001"},
  effective: {:date_time, "2024-01-15T09:30:00Z"},
  issued: "2024-01-15T10:00:00Z",
  value: {:quantity, %Quantity{value: 14.5, unit: "g/dL", system: "http://unitsofmeasure.org", code: "g/dL"}},
  reference_range: [
    %{
      low:  %Quantity{value: 12.0, unit: "g/dL", system: "http://unitsofmeasure.org", code: "g/dL"},
      high: %Quantity{value: 17.5, unit: "g/dL", system: "http://unitsofmeasure.org", code: "g/dL"},
      text: "12.0–17.5 g/dL"
    }
  ]
}
```

### Patient admission (Encounter)

```elixir
alias FhirEx.Resources.Encounter
alias FhirEx.Types.{CodeableConcept, Coding, Reference, Period}

encounter = %Encounter{
  id: "enc-001",
  status: "in-progress",
  class: [
    %CodeableConcept{
      coding: [%Coding{
        system: "http://terminology.hl7.org/CodeSystem/v3-ActCode",
        code: "IMP",
        display: "inpatient encounter"
      }]
    }
  ],
  subject: %Reference{reference: "Patient/patient-001"},
  service_provider: %Reference{reference: "Organization/org-001"},
  actual_period: %Period{start: "2024-01-15T08:00:00Z"},
  admission: %{
    admit_source: %CodeableConcept{
      coding: [%Coding{
        system: "http://terminology.hl7.org/CodeSystem/admit-source",
        code: "emd",
        display: "From accident/emergency department"
      }]
    }
  }
}
```

## JSON

### Encoding

```elixir
json = FhirEx.JSON.encode!(resource)
```

- `resourceType` is injected automatically for resources
- `nil` fields are stripped from the output
- Struct field names are converted to FHIR camelCase keys (`birth_date` → `"birthDate"`)

### Decoding

```elixir
resource = FhirEx.JSON.decode!(json_string, FhirEx.Resources.Patient)
```

Pass the target module as the second argument. Nested structs are reconstructed automatically.

### Round-trip

```elixir
patient
|> FhirEx.JSON.encode!()
|> FhirEx.JSON.decode!(Patient)
```

## Polymorphic fields

FHIR uses `[x]` to indicate a field that can hold one of several types (e.g. `value[x]`, `occurrence[x]`). In this library they are represented as **tagged tuples** in Elixir and serialised to the correct FHIR JSON key.

| Elixir (internal) | FHIR JSON key |
|---|---|
| `{:date_time, "2024-01-15T10:00:00Z"}` | `"occurrenceDateTime"` |
| `{:period, %Period{...}}` | `"occurrencePeriod"` |
| `{:quantity, %Quantity{...}}` | `"valueQuantity"` |
| `{:codeable_concept, %CodeableConcept{...}}` | `"valueCodeableConcept"` |
| `{:string, "Positive"}` | `"valueString"` |
| `{:boolean, true}` | `"valueBoolean"` |
| `{:range, %Range{...}}` | `"valueRange"` |

Resources that use polymorphic fields:

- `Observation` — `value[x]`, `effective[x]`
- `ServiceRequest` — `occurrence[x]`, `quantity[x]`, `asNeeded[x]`
- `Specimen` — `collected[x]`, `fastingStatus[x]`
- `DiagnosticReport` — `effective[x]`

## Validation

```elixir
alias FhirEx.Validation
alias FhirEx.Validation.Error

case Validation.validate(resource) do
  {:ok, resource} ->
    # proceed

  {:validation_error, errors} ->
    IO.puts(Error.format_all(errors))
end
```

`validate!/1` raises `ArgumentError` instead of returning the tuple:

```elixir
Validation.validate!(resource)
```

### Error structure

Each error carries a JSON-pointer style `path` list and a human-readable `message`:

```elixir
%Error{path: ["name", "1", "use"], message: "must be one of: usual | official | ..."}
```

Format helpers:

```elixir
Error.format(error)       #=> "name.1.use: must be one of: usual | official | ..."
Error.format_all(errors)  #=> newline-separated string of all errors
```

Nested struct errors are validated recursively and their paths are prefixed with the parent field and list index, so you always know exactly where the problem is.

### What is validated

| Type / Resource | Rules |
|---|---|
| `Coding` | `code` has no whitespace; `system` is a non-empty URI |
| `CodeableConcept` | at least one of `coding` or `text` present; nested codings valid |
| `Identifier` | `use` is a valid code; `system` is a non-empty URI |
| `HumanName` | `use` is a valid code; at least one name part present |
| `Address` | `use` and `type` are valid codes |
| `ContactPoint` | `system`/`use` are valid codes; `value` required when `system` is set |
| `Period` | `start`/`end` are valid FHIR dateTimes; `start` ≤ `end` |
| `Quantity` | `comparator` is a valid code; `system` required when `code` is set |
| `Range` | both quantities valid; `low.system` matches `high.system` |
| `Reference` | at least one of `reference`/`identifier`/`display`; reference string format |
| `Annotation` | `text` required; `authorReference` and `authorString` are mutually exclusive |
| `Extension` | `url` required; at most one `value[x]`; `value[x]` and nested extensions mutually exclusive |
| `Meta` | `versionId` matches FHIR id format; `lastUpdated` is a valid instant |
| `Narrative` | `status`/`div` required; `div` must be an XHTML `<div>` element |
| `Patient` | `gender`, `birthDate` format; `deceased*` and `multipleBirth*` mutual exclusivity |
| `Encounter` | `status` required and valid |
| `ServiceRequest` | `status`, `intent`, `subject` required; `priority` valid if set |
| `Observation` | `status`, `code` required; `value[x]` type validated; component `code` required |
| `Specimen` | `status` valid if set; `receivedTime` is a valid instant |
| `DiagnosticReport` | `status`, `code` required |

## Resources

| Module | FHIR resource | Primary use |
|---|---|---|
| `FhirEx.Resources.Patient` | [Patient](https://www.hl7.org/fhir/R5/patient.html) | Demographics and identifiers |
| `FhirEx.Resources.Practitioner` | [Practitioner](https://www.hl7.org/fhir/R5/practitioner.html) | Ordering clinician, result interpreter |
| `FhirEx.Resources.Organization` | [Organization](https://www.hl7.org/fhir/R5/organization.html) | Hospital, laboratory, clinic |
| `FhirEx.Resources.Encounter` | [Encounter](https://www.hl7.org/fhir/R5/encounter.html) | Admissions and visits |
| `FhirEx.Resources.ServiceRequest` | [ServiceRequest](https://www.hl7.org/fhir/R5/servicerequest.html) | Lab exam orders |
| `FhirEx.Resources.Observation` | [Observation](https://www.hl7.org/fhir/R5/observation.html) | Lab results and measurements |
| `FhirEx.Resources.Specimen` | [Specimen](https://www.hl7.org/fhir/R5/specimen.html) | Biological samples |
| `FhirEx.Resources.DiagnosticReport` | [DiagnosticReport](https://www.hl7.org/fhir/R5/diagnosticreport.html) | Report bundling observations |

## Data types

| Module | FHIR type | Notes |
|---|---|---|
| `FhirEx.Types.Primitives` | — | `@type` aliases for all 18 FHIR R5 primitives |
| `FhirEx.Types.Coding` | [Coding](https://www.hl7.org/fhir/R5/datatypes.html#Coding) | system + code + display |
| `FhirEx.Types.CodeableConcept` | [CodeableConcept](https://www.hl7.org/fhir/R5/datatypes.html#CodeableConcept) | `[Coding]` + text |
| `FhirEx.Types.Identifier` | [Identifier](https://www.hl7.org/fhir/R5/datatypes.html#Identifier) | MRN, NPI, accession number |
| `FhirEx.Types.Reference` | [Reference](https://www.hl7.org/fhir/R5/references.html) | Relative, absolute, logical, or contained |
| `FhirEx.Types.HumanName` | [HumanName](https://www.hl7.org/fhir/R5/datatypes.html#HumanName) | |
| `FhirEx.Types.Address` | [Address](https://www.hl7.org/fhir/R5/datatypes.html#Address) | |
| `FhirEx.Types.ContactPoint` | [ContactPoint](https://www.hl7.org/fhir/R5/datatypes.html#ContactPoint) | Phone, email, etc. |
| `FhirEx.Types.Period` | [Period](https://www.hl7.org/fhir/R5/datatypes.html#Period) | Start/end datetime range |
| `FhirEx.Types.Quantity` | [Quantity](https://www.hl7.org/fhir/R5/datatypes.html#Quantity) | Measured amount with UCUM unit |
| `FhirEx.Types.Range` | [Range](https://www.hl7.org/fhir/R5/datatypes.html#Range) | Low/high Quantity pair |
| `FhirEx.Types.Ratio` | [Ratio](https://www.hl7.org/fhir/R5/datatypes.html#Ratio) | Numerator/denominator (INR, titers) |
| `FhirEx.Types.Annotation` | [Annotation](https://www.hl7.org/fhir/R5/datatypes.html#Annotation) | Text note with author + time |
| `FhirEx.Types.Extension` | [Extension](https://www.hl7.org/fhir/R5/extensibility.html) | FHIR extensibility mechanism |
| `FhirEx.Types.Meta` | [Meta](https://www.hl7.org/fhir/R5/resource.html#Meta) | Version, profile, tags |
| `FhirEx.Types.Narrative` | [Narrative](https://www.hl7.org/fhir/R5/narrative.html) | XHTML human-readable summary |

## FHIR R5 notes

This library targets **FHIR R5**. Key differences from R4 that are reflected in the structs:

- `Encounter.class` is now `[CodeableConcept]` (was a single `Coding` in R4)
- `Encounter.hospitalization` has been renamed to `Encounter.admission`
- `Encounter.location.form` replaces `location.physicalType`
- `ServiceRequest.patientInstruction` is now a structured array
- `Observation` component validation requires `code` on each component

## Development

```sh
mix deps.get
mix test
mix docs   # generate HTML documentation
```

The test suite has 180 tests covering struct construction, JSON round-trips, nil field omission, all polymorphic field variants, validation rules, and nested error path propagation.

## License

MIT