HL7 Conformance
This article was originally written for Iguana 5 and contains version 5 screenshots, and may contain out of date references.
Using the validate.lua module.
This is an area which is becoming more and more important as more data is exchanged between different health organizations. i.e. HIE (Health Information Exchange). If your organization can offer interfaces to your trading partners that are very rich and informative in telling what is wrong with the format of HL7 they are feeding you, then it can save an awful lot of time in creating interfaces. The programmer on the other side can see immediately from the responses coming back from your interfaces what they need to fix, rather than requiring time consuming manual processes with unreadable specifications and endless conference calls.
The Translator is a great tool for doing detailed validation of HL7 messages once you realize the best way to leverage it.
It helps if you think about message validation from the perspective of the overall goals:
- The application should catch all the errors.
- It is nice if you can list the problems and describe them in nice language that describe to a counter party how they can fix their messages.
A good model for doing conformance testing is:
- Make a reusable shared module that have the conformance logic within it.
- In that module define a function which takes the message you wish to check and
- Have it return a list of clear reasons why the message doesn’t conform.
Structuring things in this manner has the following advantages:
- The code can be made very clean and easy to understand.
- Very complex conformance rules can be tested for – i.e. looking at multiple fields in a message.
- The function can be used to generate informative ACKs which NTE note segments with the data, or to generate good error messages or do a true/false pass/fail type logic for accepting or rejecting messages.
This following screen shot shows a nice example of a conformance module that follows these principles (the full source is given at the end of this page):
From the above it’s clear to see how maintainable the code is – the validation messages are all in clean understandable English which would clearly communicate to the user what the issue is. Because of the use of small helper functions to define the rules, it’s simple to read the code and edit the rules. In the above example we write out the error message explicitly. In some cases though you might be able to use this helper function which uses the grammar information of the HL7 node tree.
If we click on the ErrList table annotation we can see a nice readable list of errors:
With the validation errors in this structure it becomes easy to use this information. For instance we could invoke this code inside of LLP listener Translator instance to make a nice NACK (see custom ACKS).
Here’s the ACK that is generated:
MSH|^~&|Main HIS|St. Micheals|AcmeMed|Lab|20110908122935||ACK||P| MSA||9B38584D9903051F0D2B52CC0148965775D2D23FE4C51BE060B33B6ED27DA820| NTE|||PID 17.1 religion code not present.| NTE|||PID.8, patient sex had value 'F' instead of acceptable values ['Female','Male']|
Source Code
You will need to add the conformance_ack.vmd to your project.
Get the latest version of the validate.lua module from our repository.
Helper function which uses the grammar information of the HL7 node tree
This helper function makes use of the grammar information of the HL7 node tree. This may in some cases make it easier to maintain conformance code:
As you can see from the usage this helper function doesn’t require a description of the field being checked to be passed in. Instead we pass in the address co-ordinates in a Lua table in an array format. It works quite well for PID.2 and PID.17.1 but as you can see for PID.5.1.1.3 the error message isn’t that nice. That’s partly why in my first cut of this type of logic I preferred to just explicitly put in the error message.
There are compromises possible – like not having the code make the textual description and instead just have it generate the PID.5.1.1.3 notation.
One could take it further and look at nodeType() to check if a node is in fact a repeating field and alter the validation function depending on whether we are going down into a repeating field and so on. It’s not clear to me that the added benefit will be worth the effort. Here is the code for the RequiredFieldMeta function:
local function RequiredFieldMeta(Errs, N, Address) local T = N for i=1, #Address do T = T[Address[i]] trace(T) end if T:isNull() then local Msg = N:nodeName() local Long = '' T = N for i=1, #Address do T = T[Address[i]] Msg = Msg..'.'..Address[i] Long = Long..' '..T:nodeName() end Msg = Msg.." - "..Long..' not present.' Errs:add(Msg) return Errs[#Errs] end end
Generate database tables for an HL7 interface [top]
Using the genDb.lua module (code at the bottom of the page).
Note: This code is only a proof-of-concept only so we did not put it in the code repository.
This article shows how to generate a create table script from an HL7 message.
The code parses an HL7 message and generates a table for each Segment. The SQL for creating the tables is saved in a text file with a .sql extension. The database structure produced is far from ideal, and needs to be tweaked. I suggest adding a table that flags elements as tables, and maps them to (better) table names.
Using the module is simple, just a single function call – with the file name, and the parsed HL7 message as parameters:
Note: do not add a file extension to the file name
Source Code
Here is the source code for the genDb module. To use it:
- Create a new shared module called “genDb ” and copy paste in the code below.
- Add the code require(‘genDb ‘) at the top of the main module.
- Test using sample data.
Code for main:
require "genDb" function main(Data) local msg, Name = hl7.parse{vmd = 'demo.vmd', data = Data} local SQL = genDb.genDbScript(msg,[[C:\Program Files\iNTERFACEWARE\Iguana\hl7doc\CreateDb]]) print(SQL) end
Code for genDb module:
genDb={} TblInfo={} function genDb.genDbScript(Msg,File) local T=genDb.parseMsg(Msg) local S='-- Generated table creation script\n' S=genDb.makeDbScript(T,S,0) S=S:sub(1,-4)..'\n);' genDb.writeFile(File..'.sql',S) return S end function genDb.makeDbScript(Tree,Script,Level,TblName) if Level==0 then if Tree.type~='segment_group'and Tree.type~='segment_repeating' and Tree.type~='message' then TblName=Tree.name:gsub(' ','') TblName=genDb.delSymbols(TblName) TblInfo[TblName]={} --Script=Script..'\nDROP TABLE IF EXISTS ' --Script=Script..TblName..';\n\t' Script=Script..'\nCREATE TABLE ' Script=Script..TblName..'\n(\n\t' end else if Tree.type=='segment'then -- WARNING - condition depends on string format if Script:sub(-1,-1)=='\t' then Script=Script:sub(1,-4)..'\n);\n\n' end TblName=Tree.name:gsub(' ','') TblName=genDb.delSymbols(TblName) TblInfo[TblName]={} --Script=Script..'\nDROP TABLE IF EXISTS ' --Script=Script..TblName..';\n\t' Script=Script..'\nCREATE TABLE ' Script=Script..TblName..'\n(\n\t' else if TblName then local F=Tree.name:gsub(' ','') F=genDb.delSymbols(F) if TblInfo[TblName][F]then F=Tree.parentname..'_'..F end Script=Script..F..' VARCHAR(255),\n\t' TblInfo[TblName][F]=true end end end Level=Level+1 for i in genDb.pairsByNumKeys(Tree) do Script=genDb.makeDbScript(Tree[i],Script,Level,TblName) end return Script end function genDb.writeFile(Name, Content) io.output(Name) io.write(Content) io.close() end function genDb.parseMsg(Msg,Doc,CompName,ParentName) if Doc == nil then -- root node Doc ={} Doc.type=Msg:nodeType() Doc.number=1 Doc.name=Msg:nodeName() Doc.compname=Msg:nodeName():gsub(' ','') Doc.value=Msg:S() Doc.sub=true if #Msg~=0 then genDb.parseMsg(Msg,Doc,Doc.compname,Doc.name:gsub(' ','')) end else for i=1, #Msg do if genDb.isFieldPresent(Msg[i]) then Doc[i]={} Doc[i].type=Msg[i]:nodeType() Doc[i].number=i Doc[i].name=Msg[i]:nodeName() Doc[i].compname=CompName..'_'..Msg[i]:nodeName():gsub(' ','') Doc[i].parentname=ParentName Doc[i].value=Msg[i]:S() if #(Msg[i])~=0 then Doc[i].sub=true genDb.parseMsg(Msg[i],Doc[i],Doc[i].compname,Doc[i].name:gsub(' ','')) else Doc[i].sub=false end end end end return Doc end function genDb.isFieldPresent(Field) if Field:isNull() then return false else return true end end function genDb.SegmentFilter(Segment, SegName) if Segment:nodeName() == SegName then return true end return false end function genDb.delSymbols(String) -- brackets and escape used for chars that have special meaning to gsub() function local C={'-','/','~','\\','@','#',[[\$]],[[\%]],[[\^]],'&','*',[[\(]],[[\)]],'|','=','+','?','\t'} for i,v in ipairs(C) do String=String:gsub(C[i],'_') end return String end -- iterator sorts numbers and ignores non-numeric keys function genDb.pairsByNumKeys(t, f) local a = {} for n,v in pairs(t) do if tonumber(n) then table.insert(a, n) end end table.sort(a, f) local i = 0 -- iterator variable local iter = function () -- iterator function i = i + 1 if a[i] == nil then return nil else return a[i], t[a[i]] end end return iter end function node.S(ANode) return tostring(ANode) end
Possible modifications:
- Add a table with extra information to tweak the database structure
- Mark elements to be generated as tables
- Map elements to be generated as tables to (better) table names
- The current code creates a SQL script for a single HL7 message only, you may want to tweak the code to parse multiple messages. This would enable it to generate fields for optional data (not found in all messages)
- Automatically import data
- Generate code to map HL7 data to the generated table structure, and save to file.
- Use the generated mappings as the basis for another channel to import the data
- If possible use
dofile()
to to directly execute the generated mapping code - Connect to the database and run the table create script automatically
- NOTE: Drop table statements were in included in the script (for testing) and then commented out