Skip to content

Latest commit

 

History

67 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fhir-utils

Reads delimited and fixed-width record files and maps them to FHIR JSON resources. Column mapping, code translation and resource shaping are declared in a JSON data contract, so onboarding a new source extract is a configuration change, not a patch.

Build

cargo build --release                                        # target/release/fhir-utils
cargo build --release --target x86_64-unknown-linux-musl     # fully static binary
cargo test                                                   # unit, integration, end-to-end

Commands

Command

Purpose

fhir-utils validate -f <contract.json>

validate a data contract

fhir-utils convert -d <base> -o <out>

convert every input under <base>/input

fhir-utils convert -f <file> -c <config-dir> -o <out>

convert one file

Flag

Meaning

Required

-f, --file

contract file for validate, input file for convert

yes for validate

-d, --directory

base directory holding input/ and config/

one of -d or -f

-c, --config-dir

directory holding the contract

with -f, unless MAPPING_CONFIG_DIRECTORY is set

-o, --output

output directory, created when missing

yes for convert

--strict

fail on a task error, an empty group key or a row that builds nothing

no

-d and -f are mutually exclusive; -f without -c reads the contract from MAPPING_CONFIG_DIRECTORY, and -d requires both <base>/input and <base>/config. Exit code 0 on success, 1 on an invalid contract, a missing path or a row conversion failure.

Quickstart

Source extract input/patients.csv with headers that do not match the internal model:

MRN,LastName,FirstName,Sex,DOB,SSN
1234567,Smith,Ann,F,1980-01-02,123-45-6789
7654321,Jones,Bob,M,1975-11-30,

Code map config/sex.csv, two columns, header names fixed:

source_value,target_value
F,female
M,male

Contract config/data-contract.json:

{
  "general": {
    "timeZone": "America/New_York",
    "tenantId": "hospital-a",
    "streamType": "live"
  },
  "fileDefinitions": {
    "patients": {
      "fileType": "csv",
      "resourceType": "Patient",
      "groupByKey": "MRN",
      "tasks": [
        {
          "task": "rename_columns",
          "column_map": {
            "MRN": "mrn",
            "LastName": "nameLast",
            "FirstName": "nameFirst",
            "Sex": "gender",
            "DOB": "birthDate",
            "SSN": "ssn"
          }
        },
        { "task": "map_codes", "code_map": { "gender": "sex.csv" } }
      ]
    }
  }
}

Run:

fhir-utils validate -f config/data-contract.json
fhir-utils convert -f input/patients.csv -c config -o out
fhir-utils convert --strict -f input/patients.csv -c config -o out

Result:

out/1234567/1234567-Patient-patients-00001.json
out/7654321/7654321-Patient-patients-00001.json

The key patients matches the file name patients.csv, groupByKey names the source column because grouping happens before tasks, and sex.csv resolves against the configuration directory. Target column names come from the record fields below; a name outside that list travels through the pipeline but never reaches the resource.

Contract

Key

Description

Required

general.timeZone

IANA zone used to localize naive datetimes

yes

general.tenantId

tenant identifier injected into every row

yes

general.streamType

live or historical

yes

general.assigningAuthority

identifier system for PI, MR, AN, VN, RI, RXN

no

general.emptyFieldValues

values read as null

no

general.regexFilenames

match file definition keys as regular expressions

no

fileDefinitions.<key>.fileType

csv or fixed-width

no

fileDefinitions.<key>.resourceType

one of the resource keys

yes

fileDefinitions.<key>.groupByKey

source column that groups the output

yes

fileDefinitions.<key>.skiprows

zero-based line numbers dropped before the header

no

fileDefinitions.<key>.headers

column names, or names with widths

no

fileDefinitions.<key>.tasks

ordered tasks appended after the default chain

no

  • Every general key is injected into each row as a constant, including keys beyond the ones listed above, so a contract can carry its own values into tasks.

  • A key matches when it is a substring of the input file name without its extension; longest key first, then alphabetically.

  • Fixed-width files need a width form of headers; otherwise the first remaining line supplies the column names.

  • Input files matching no definition are skipped and named in the summary.

  • Repeated column names keep the last column of the row.

Resource keys

Patient, AllergyIntolerance, Condition, Encounter, Immunization, Observation, Location, Organization, Practitioner, Procedure, MedicationUse, MedicationAdministration, MedicationRequest, MedicationStatement, DocumentReference, DiagnosticReport, Unstructured, Basic.

Record fields

Task output must use these names; anything else is ignored by the converter. Every row also carries filePath and rowNum. ssn is normalized to nine digits without dashes and repeated-digit values are rejected. a → b renames a to b.

Model

Fields

Patient

patientInternalId, accountNumber, ssn, ssnSystem, patientSourceRecordId, driversLicense, driversLicenseSystem, mrn, assigningAuthority, nameFirst, nameFirstMiddle, nameMiddle, nameLast, nameFirstMiddleLast, prefix, suffix, birthDate, deceasedDateTime, deceasedBoolean, multipleBirthBoolean, multipleBirthInteger, address1, address2, city, state, postalCode, country, addressText, telecomPhone, race, raceSystem, raceText, ethnicity, ethnicitySystem, ethnicityText, gender, ageInWeeksForAgeUnder2Years, ageInMonthsForAgeUnder8Years

AllergyIntolerance

patientInternalId, accountNumber, ssn, ssnSystem, mrn, encounterInternalId, encounterNumber, resourceInternalId, assigningAuthority, allergySourceRecordId, allergyCategory, allergyType, allergyRecordedDateTime, allergyCode, allergyCodeSystem, allergyCodeText, allergyCriticality, allergyManifestationCode, allergyManifestationSystem, allergyManifestationText, allergyManifestationCodeList, allergyClinicalStatusCode, allergyVerificationStatusCode, allergyOnsetStartDateTime, allergyOnsetEndDateTime

Condition

patientInternalId, accountNumber, ssn, ssnSystem, mrn, encounterInternalId, encounterNumber, resourceInternalId, assigningAuthority, conditionSourceRecordId, encounterClaimType, conditionCategory, conditionRecordedDateTime, conditionOnsetDateTime, conditionAbatementDateTime, conditionClinicalStatus, conditionVerificationStatus, conditionDiagnosisRank, conditionDiagnosisUse, conditionCode, conditionCodeSystem, conditionCodeText, conditionSeverityCode, conditionSeveritySystem, conditionSeverityText, conditionChronicity

Encounter

patientInternalId, accountNumber, ssn, ssnSystem, mrn, encounterNumber, resourceInternalId, assigningAuthority, encounterSourceRecordId, encounterStatus, encounterClassCode/Text/System, encounterPriorityCode/Text/CodeSystem, encounterStartDateTime, encounterEndDateTime, encounterLengthValue, encounterLengthUnits, encounterReasonCode/System/Text, hospitalizationAdmitSourceCode/Text/System, hospitalizationReAdmissionCode/Text/System, hospitalizationDischargeDispositionCode/Text/System, encounterParticipantSequenceId, encounterParticipantTypeCode/Text/CodeSystem, practitionerInternalId, practitionerNPI, practitionerNameLast, practitionerNameFirst, practitionerGender, practitionerRoleText, practitionerRoleCodes, practitionerRoleCodesSystem, practitionerSpecialtyCodes, practitionerSpecialtyCodesSystem, practitionerSpecialtyText, encounterLocationSequenceId, encounterLocationPeriodStart, encounterLocationPeriodEnd, locationResourceInternalId, locationName, locationTypeCode, locationTypeText, locationTypeCodeSystem, encounterStatusHistory, encounterInsuredEntryId, encounterInsuredRank, encounterInsuredCategoryCode/System/Text, encounterClaimType, encounterDrgCode

Immunization

patientInternalId, accountNumber, ssn, ssnSystem, mrn, resourceInternalId, assigningAuthority, immunizationSourceRecordId, immunizationDoseQuantity, immunizationDoseUnit, immunizationDoseText, encounterNumber, encounterInternalId, organizationResourceInternalId, organizationName, immunizationRouteCode/System/Text, immunizationSiteCode/System/Text, immunizationStatus, immunizationStatusReasonCode/System/Text, immunizationVaccineCode/System/Display/Text, immunizationVaccineCodeList, immunizationDate, immunizationExpirationDate

Location

assigningAuthority, locationResourceInternalIdresourceInternalId, locationName, locationTypeCode, locationTypeText, locationTypeCodeSystem

MedicationUse

resourceType, patientInternalId, accountNumber, ssn, ssnSystem, mrn, medicationSourceRecordId, encounterInternalId, encounterNumber, encounterClaimType, encounterClassCode, resourceInternalId, medicationRxNumber, assigningAuthority, medicationUseStatus, medicationUseCategoryCode/System/Text, medicationUseOccuranceDateTime, medicationCode/Display/System/Text, medicationCodeList, medicationUseRouteCode/System/Text, medicationUseRouteList, medicationUseDosageText/Value/Unit, medicationValidityStart, medicationValidityEnd, medicationRefills, medicationQuantity, medicationAuthoredOn, medicationRequestIntent

Observation

patientInternalId, accountNumber, ssn, ssnSystem, mrn, encounterNumber, encounterInternalId, resourceInternalId, assigningAuthority, observationSourceRecordId, observationStatus, observationCategory, observationDateTime, practitionerNPI, practitionerInternalId, observationCode, observationCodeSystem, observationCodeText, observationCodeList, observationValue, observationValueUnits, observationValueDataType, observationRefRange, observationRefRangeLow, observationRefRangeHigh, observationRefRangeText, observationInterpretationCode/System/Display/Text

Organization

assigningAuthority, organizationResourceInternalIdresourceInternalId, organizationName

Practitioner

assigningAuthority, practitionerInternalIdresourceInternalId, practitionerNPIidentifier_practitionerNPI, practitionerNameLast, practitionerNameFirst, practitionerNameText, practitionerGender, practitionerRoleText, practitionerRoleCode, practitionerRoleCodeList, practitionerRoleCodeSystem, practitionerSpecialtyCode, practitionerSpecialtyCodeList, practitionerSpecialtyCodeSystem, practitionerSpecialtyText

Procedure

patientInternalId, accountNumber, ssn, ssnSystem, mrn, procedureSourceRecordId, encounterInternalId, encounterNumber, encounterClaimType, resourceInternalId, assigningAuthority, procedureStatus, procedurePerformedDateTime, procedureCategory, procedureCategorySystem, procedureCategoryText, procedureCode/System/Display/Text, procedureCodeList, procedureModifierList, procedureModifierSystem, procedureEncounterSequenceId, practitionerInternalId, practitionerNPI, practitionerNameLast, practitionerNameFirst, practitionerGender, practitionerRoleText, practitionerRoleCode, practitionerRoleCodeSystem, practitionerSpecialtyCode, practitionerSpecialtyCodeSystem, practitionerSpecialtyText

Basic

baseSystem, tokenList, patientInternalIdentifier, created_date, otherIdentifierList

Unstructured

resourceType, patientInternalId, accountNumber, ssn, ssnSystem, mrn, encounterNumber, resourceInternalId, encounterInternalId, assigningAuthority, resourceStatus, documentStatus, documentTypeCode, documentTypeCodeSystem, documentTypeCodeText, documentDateTime, documentAttachmentContentType, documentAttachmentContent, documentAttachmentTitle, practitionerInternalId, practitionerNPI, practitionerNameLast, practitionerNameFirst

Tasks

Default chain, in order: add_row_num, set_nan_to_none, remove_whitespace_from_columns, copy_columns into groupByKey, then add_constant per general key plus filePath and configResourceType. User tasks run after it.

Task

Parameters

add_constant

name, value as string, number, boolean or list

add_row_num

starting_index

append_list

source_columns, target_column, discard_if_duplicate

build_object_array

entry_class, target_column, entries

change_case

columns, casing as UPPER or LOWER

compare_to_date

column, target_column, compare_date, comparison, true_string, false_string

conditional_column

source_column, target_column, condition_map

conditional_column_update

source_column, target_column, condition_map

condition_column_with_prerequisite

+ prerequisite_column, prerequisite_match

convert_to_list

column, separator

copy_columns

columns, target_column, value_separator

filter_to_columns

source_column, target_columns, filters

find_not_null_value

columns, target_column

format_date

columns, date_format

join_data

secondary_data_source, join_type, join_on; optional source_type, reader_params

map_codes

code_map, whose per-column map takes a default for unlisted values

remove_whitespace_from_columns

none

rename_columns

column_map

replace_text

column_name, match, replacement, options

set_nan_to_none

none

split_column

column_name, new_column_names, delimiter or indices

split_row

columns, split_column_name, split_value_column_name

validate_value

column_name, regex, no_match_replacement

  • Tasks run in order over one chunk at a time; a failing task is logged and the chain continues.

  • List valued cells are strings joined with |.

  • condition_map and code_map take an inline object or a CSV file with source_value and target_value columns; a null or empty target clears the cell.

  • A value the code map does not list passes through unchanged, which is reported once per file and column; add a default entry to control it.

  • Unknown tasks and unknown or missing parameters are rejected when the contract loads, as is an unknown key inside a file definition or a missing fileDefinitions block.

  • A task target that neither a later task nor the resource reads is reported by validate and warned about during convert; --strict makes it fail the run. It catches a misspelled field name, which otherwise drops the value silently.

Output

  • Path: <out>/<groupByKey>/<groupByKey>-<resourceType>-<safeFileId>-<nnnnn>.json.

  • The counter runs per group key, resource type and source file.

  • JSON is pretty printed with sorted keys.

  • safeFileId keeps [A-Za-z0-9-] and maps every other character to _.

  • Group key directories keep letters, digits, spaces and dots; path separators, control characters and \ : * ? " < > | become , and an all-dot key becomes .

  • Re-running overwrites matching file names and leaves earlier files in place.

  • The summary counts files, rows read, resources written and rows that produced none.

  • A row whose record carries no usable code produces no resource; those rows are counted in the summary, listed by row number on standard error, and fatal with --strict.

  • Resource ids are generated per run unless the contract maps resourceInternalId, so a reload creates new ids; the urn:id:extID identifier and the urn:id:source-file-id extension, which carries <file>:<rowNum>, stay stable and are the keys to match on.

  • The summary goes to standard output; warnings and errors go to standard error.

  • Warnings print by default; RUST_LOG=info adds a line per file naming the definition that matched it and, once converted, its row and resource counts.

Required field defaults

FHIR requires some fields the source extract may not carry. When a column is absent the converter fills these so the output loads into a server:

Resource

Field

Default

Observation, Encounter, Procedure, DiagnosticReport

status

unknown

MedicationAdministration, MedicationRequest, MedicationStatement

status

unknown

MedicationRequest

intent

order

Immunization

status

completed

DocumentReference

status

current

AllergyIntolerance

clinicalStatus

active unless the verification status is entered-in-error

Encounter

class

data absent temp-unknown

Immunization

occurrence

occurrenceString of unknown when no date is given

Map the real column whenever the source has one; the defaults keep output valid, they do not carry clinical meaning.

Errors and strict mode

Class

Behaviour

Contract or validation error

run stops naming the key or the accepted values

Missing file definition

file skipped

Task failure

warning, pipeline continues; fatal with --strict

Task target no field reads

warning at load; fatal with --strict

Rows without a group key

warning once per file; fatal with --strict

Rows producing no resource

counted in the summary and listed; fatal with --strict

Unlisted code map value

warning once per file and column, value passes through

Input that is not UTF-8

run stops naming the file and the re-encoding command

Row conversion error

run stops with the group key and file

Configuration

Variable

Meaning

Default

CSV_BUFFER_SIZE

rows per chunk; 0 reads the whole file into memory

1000

MAPPING_CONFIG_DIRECTORY

contract directory used when -c is absent

/var/app/config

MAPPING_CONFIG_FILE_NAME

contract file name

data-contract.json

Settings are read once per process. Contracts, external file definitions, mapping files and join sources open through one pluggable opener: local paths and file:// URIs work out of the box, other schemes need a backend registered with opener::set_opener. The opener is scoped to the calling thread, so each worker thread installs its own.

About

Contract-driven conversion of delimited and fixed-width records into FHIR JSON

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Used by

Contributors

Languages