I'm on a V2-to-FHIR journey writing some code to support translation of V2 messages to FHIR.
Along the way, one of the challenges I have is getting the testing framework set up so that it's easy to write assertions about conversions. HL7 Messages are full of dense text, which makes it hard to read, and even harder to sprinkle them through with assertions, so that:
- The assertions about the FHIR translation are close to the HL7 Message text for which they are made (making it easy to verify that the right assertions are present).
- The text is loadable from external resources, rather than written in code.
- The assertions are written in a language that's easy to get parsing tools for, and
- The assertion language works well with FHIR.
So, the last two are what leads to FHIRpath, which is obvious when you think about it.
FHIRPath is already in use for validating FHIR Resources, so here we go, just validating a bundle of resources.
The HAPI FHIRPath toolkit isn't quite as extensible as I'd look without digging into some of the guts (which I did some time back for
FluentQuery), so about all I can do is write my own resource resolver to provide some customizations, even though I VERY MUCH WANT the ability to add constants and my own functions without having to rewrite some of the native R4 FhirPath code. But, realistically, I just need to replace a few leading expressions to deal with my worst cases.
As a user, you want to write MessageHeader, or Patient, or Observation[3], or maybe even Bundle at the start of your tests. For example: OperationOutcome.count() = 2. But instead, you'd have to write something like %context.entry.resource.ofType(OperationOutcome).count() = 2. Or in the case of the uniquitous Observation %context.entry.resource.ofType(Observation)[3] instead of just Observation[3]. It's about 30 characters added to each test you need to write, and just ONE message conversion probably needs about 500 assertions to fully validate it. I'm at PID on my first example, and I've got about 100 assertions, and haven't hit the meat of the message yet. That would be 3000 characters, or at my typing speed, about 8.5 minutes of extra typing. I figure I can save about 5 seconds per assertion, and I've written 100 in one day. According to
Randal Monroe, I can invest 10 days into making that time savings happen. Since it took me about an hour, I have 9 days of time to invest in something else (or I could just get back to work).
The next bit is about making HL7 messages easier to split into smaller pieces. For now, a segment is small enough. Segments are detectable by this regular expression pattern: /^[A-Z]{2}[A-Z0-9]/. Any line that begins with that is a segment. Great, now we're writing a parser (really a stream filter).
Now what? Well, I need a way to identify assertions. Let's use the @ character to introduce an assertion. Anything followed by @ at the start of a line is an assertion, and it can be followed by a FHIRPath and an optional comment. I suppose we could use # for comments, but FHIRpath already recognizes // comments, so I'll go with that. Except that I have urls in my messages, and I don't want to have https:// urls split up after //. So, I'll make the rule that the comment delimiter is two // followed by at least one space character.
Now, I'll borrow from some older programming languages and say that a terminal \ at the end of a line signals continuation of on the next line. And it would be nice if \ plus some whitespace was the same as ending the line with a \, so I'll throw that in. And finally, let's ignore leading and trailing whitespace on a line, because whitespace messes up messyages (yes, HL7 messages in text are messy).
Here's an example of how this might look in a message file:
MSH|^~\&|\
TEST|MOCK|\
DEST|DAPP|\
20240618083101-0400||\
RSP^K11^RSP_K11|\
RESULT-01|\
P|2.5.1|||\
ER|AL|\
||||Z32^CDCPHINVS
@MessageHeader.count() = 1 // It has one message header
@MessageHeader.source.name = "TEST"
@MessageHeader.source.endpoint = "MOCK"
@MessageHeader.destination.name = "DEST"
@MessageHeader.destination.endpoint = "DAPP"
This is about where I'm at now. Even better would be:
MSH|^~\&|\
@MessageHeader.count() = 1 // It has one message header
TEST|MOCK|\
@MessageHeader.source.name = "TEST"
@MessageHeader.source.endpoint = "MOCK"
DEST|DAPP|\
@MessageHeader.destination.name = "DEST"
@MessageHeader.destination.endpoint = "DAPP"
20240618083101-0400||\
RSP^K11^RSP_K11|\
RESULT-01|\
P|2.5.1|||\
ER|AL|\
||||Z32^CDCPHINVS
I'll get to that stage soon enough. I still have 9 more days to burn on automation. You can see though, how FHIRPath makes it easy to write the tests within your sample messages.