Documenting an HL7 interface

Introduction

We are often asked, “Can your tools help us document our interfaces?” We have tackled the problem with various approaches and found a couple of potential solutions. This section outlines some of our discussions and designs.

Note: If the development of this topic is something you are interested in, we recommend that you join our LinkedIn Group to get the latest updates.

Although Chameleon can spit out HL7 documentation in both HTML and RTF, the generated material is not particularly useful. In Iguana, the Translator’s APIs do provide a good foundation to generate documentation. It is quite easy to iterate through the all the fields of an HL7 message and look at the name and value of each field. Theoretically, we could build on this and create code that documents an interface. Here are a couple of proof-of-concepts supporting this idea:

There are different approaches to documenting an HL7 interface. For example, one good starting point is to see how other vendors go about documenting their interfaces. Have a look at how Athena Health documents their interfaces in the discussion on secure HL7 messaging.  I think they did a good job.

Having sample data that another vendor can download easily off your website is a huge boon to fully documenting an HL7 interface. As such, one valid approach to documentation could be to a) provide raw test data, then b) wait for user requests and use a fast tool like a wiki to rapidly answer them.

The advantage of this approach is that it adheres to the idea of “less is more“. It’s better to document the non-obvious things and skip documenting the obvious parts. That way, your users don’t have to wade through lengthy documentation that explains the obvious. It is far more likely that they want to read a small amount of really good documentation.

Other interesting links on this topic can be found in the Iguana Translator Modules reference section.

Generating hierarchical format HL7 interface documentation [top]

The example iterates through an HL7 message, recursively scanning each element and documenting the data (empty nodes are ignored). The report is output as an HTML file.

Scroll down for the source code.

Using the module is simple, just a single function call – with the report name, and the segment to document as parameters:

Note: do not add a file extension to the file name

The code above documents the PID segment – the report follows:

The report is quite simple: subfields are indented, Number and Name are bold, sample data is in plain text (enclosed in brackets and quotes). The formatting can be changed by modifying the CSS file.

You can create a report for a whole message, or any element by changing the call to the hl7doc.generateDoc function:

The report with Address only looks like this:

Source Code

Here is the source code for the hl7doc module.

To use it:

  1. Create a new shared module called “hl7doc” and copy paste in the code below.
  2. Add the code require(‘hl7doc’) at the top of the main module.
  3. Download the hl7.css file and copy it to the same folder as the report.
  4. Test using sample data.

Note: the CSS code assumes a default install directory for Iguana image files (C:\Program Files\iNTERFACEWARE\Iguana\web_docs\js\treeview\images). You will need to update the CSS file if they are in a different directory on your system.

Code for main:

doc = require('hl7doc')

function main(Data)
   local msg, Name = hl7.parse{vmd = 'demo.vmd', data = Data}
   doc.generateDoc(msg,[[C:\Program Files\iNTERFACEWARE\Iguana\hl7doc\doc]])            -- report for whole message
   doc.generateDoc(msg.PID,[[C:\Program Files\iNTERFACEWARE\Iguana\hl7doc\doc]])        -- report for the PID segment
   doc.generateDoc(msg.PID[11][1],[[C:\Program Files\iNTERFACEWARE\Iguana\hl7doc\doc]]) -- report for the 1st Address listed
   trace(doc)
end

Code for hl7doc module:

local hl7doc={}

-- NOTE: blanks removed from names "ID Number" -> "IDNumber"
local Comments={
   PID='Patient information segment',
   PID_IdList='List of Ids',
   PID_IdList_IdList='Id List item (possible repeats)',
   PID_IdList_IdList_IDNumber='Identification number',
   PID_IdList_IdList_AssigningAuthority='Authority that created ID Number',
   PID_IdList_IdList_AssigningAuthority_Id='ID for Authority'}

function hl7doc.generateDoc(Msg,DocFile)
   local doc=hl7doc.parseMsg(Msg)
   local R=hl7doc.makeHeader({title='HL7 Report',h1='Message Structure'},hl7doc.formatHeaderHtml)
   R=R..hl7doc.makeBody(doc,hl7doc.formatBodyHtml)
   R=R..hl7doc.makeFooter(hl7doc.formatFooterHtml)
   hl7doc.writeFile(DocFile..'.html',R)
   print(R)
end

function hl7doc.makeBody(Doc,Format,Report,Level)
   if not Level then --root
      Level = 0
      Report=''
   end
   Report = Report..Format(Doc,Level)
   Level=Level+1
   for i in hl7doc.pairsByNumKeys(Doc) do
      Report=hl7doc.makeBody(Doc[i],Format,Report,Level)
   end
   return Report
end

function hl7doc.writeFile(Name, Content)
   io.output(Name)
   io.write(Content)
   io.close()
end

function hl7doc.formatBodyHtml(Doc,Level)
   -- create indent levels
   local I=''
   for i=1,Level do
      I=I..'<ul><li>'
   end
   I=I..'<ul><li ' -- last level open then add css style
   if Doc.type=='message' then
      I=I..'class = message>\n'
   elseif Doc.type=='segment_group' then
      I=I..'class = segment_group>\n'
   elseif Doc.type=='segment_repeated' then
      I=I..'class = segment_repeated>\n'
   elseif Doc.type=='segment' then
      I=I..'class = segment>\n'
   elseif Doc.type:sub(-5,-1)=='field' then
      I=I..'class = field>\n'
   end
   -- create the message body text
   local B='<b>'..Doc.number..' '..Doc.name..':</b> '..'\n'
   if Doc.type:sub(1,7)~='segment' and Doc.type~='message' then
      B=B..'("'..Doc.value..'")\n'
   elseif Doc.type=='message' then
      B='<span class="highlight">'..B..'HL7 Message</span>\n'
   elseif Doc.type=='segment_group' then
      B='<span class="highlight">'..B..'Segment Group</span>\n'
      elseif Doc.type=='segment_repeated' then
      B='<span class="highlight">'..B..'Segment Repeated</span>\n'
   elseif Doc.type=='segment' then
      B='<span class="highlight">'..B..'Segment</span>\n'
   end
   if Comments[Doc.compname] then
      B=B..'<span class="comment">'..Comments[Doc.compname]..'</span>'
   end
   B=I..B
   -- close indent levels
   for i=0,Level do
      B=B..'</li></ul>'
   end
   B=B..'\n'
   return B
end

function hl7doc.makeHeader(Header,Format)
   return Format(Header)
 end

function hl7doc.formatHeaderHtml(Header)
   local H='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"'
   H=H..'"http://www.w3.org/TR/html4/strict.dtd">\n'
   H=H..'<link rel="stylesheet" type="text/css" '
   H=H..'href="hl7.css" />\n'
   H=H..'<html>\n'
   H=H..'<head>\n'
   H=H..'<title>'..Header.title..'</title>\n'
--   H=H..'<meta http-equiv="refresh" content="1">' -- for testing
   H=H..'</head>\n'
   H=H..'<body>\n'
   H=H..'<h1>'..Header.h1..'</h1>'
   H=H..'<h3>Only populated fields are listed</h3>'
   return H
 end

function hl7doc.makeFooter(Format)
   return Format(Header)
 end

function hl7doc.formatFooterHtml(Format)
   local F='</body>\n'..
           '</html>\n'
   return F
 end

function hl7doc.parseMsg(Msg,Doc,CompName)
   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
         hl7doc.parseMsg(Msg,Doc,Doc.compname)
      end
   else
      for i=1, #Msg do
         if hl7doc.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].value=Msg[i]:S()
            if #(Msg[i])~=0 then
               Doc[i].sub=true
               hl7doc.parseMsg(Msg[i],Doc[i],Doc[i].compname)
            else
               Doc[i].sub=false
            end
         end
      end
   end
   return Doc
end

function hl7doc.isFieldPresent(Field)
   if Field:isNull() then
      return false
   else
      return true
   end
end

function hl7doc.SegmentFilter(Segment, SegName)
   if Segment:nodeName() == SegName then
      return true
   end
   return false
end

-- iterator sorts numbers and ignores non-numeric keys
function hl7doc.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

return hl7doc

Possible modifications:

  • If you need a report in a different format than HTML you can duplicate the three format functions (formatHeaderHtml, formatBodyHtml, formatFooterHtml), and modify them to format as RTF, XML etc.
  • This code inspects a single message only – adding persistence across messages could be useful for:
  • Including optional fields in the report
  • Including multiple data samples (change the value field in the Doc table from a string to a table of strings)
  • Modify the Comments table to include optional data/features for the report, for example:

 

  • Including/excluding fields regardless of whether or not they include data
  • Flag elements to be created as tables – for example, repeating fields like Address, IdList, Name etc
  • The Comments table should probaly become a subtable in modified table
  • The icons in the report could be updated match the Iguana tree Annotations
    • Repeating fields repeating fields (IdList, Name, Adress etc) should use the (composite_repeating) icon rather than the (composite) icon
    • Other messages (Lab etc) also need icons like (folder) and (folder_repeating)
  • If you want to get really clever you could create clickable icons to expand and collapse levels

Generating flat format HL7 interface documentation [top]

The example iterates through one level of an HL7 message, documenting the data in each element (empty nodes are ignored). The report is output as an HTML file.

Scroll down for the source code.

Using the module is simple, just a single function call with 3 parameters – the report name, the segment to document, and an optional table of comments:

Note: do not add a file extension to the file name

The code above documents the PID segment – the report follows:

The report is very simple: only the first sub-level fields are shown, Numbers are highlighted, and Name and Comment have a different background colour. The formatting can be changed by modifying the CSS file.

You can create a report for a whole message, or any element by changing the call to the hl7doc.generateDoc function.

You can use multiple calls to makeDoc if you want to inlude several elements in one report:

As you can see the report contains three elements:

Source Code

Here is the source code for the hl7doc2 module.

To use it:

  1. Create a new shared module called “hl7doc2” and copy paste in the code below.
  2. Add the code require(‘hl7doc2’) at the top of the main module.
  3. Download thehl7.css file and copy it to the same folder as the report.
  4. Test using sample data.

Note: the CSS code assumes a default install directory for Iguana image files (C:\Program Files\iNTERFACEWARE\Iguana\web_docs\js\treeview\images). You will need to update the CSS file if they are in a different directory on your system.

Code snippet for main:

require("hl7doc2")

function main(Data)
   local msg, Name = hl7.parse{vmd = 'demo.vmd', data = Data}

 -- report for one segment
   local comment={
      [3]="List of Ids",
      [5]="Patient Name",
      [7]='Birth Date',
      [8]='M = male, F = female, O = other',
      [10]='Patient Race',
      [11]='Patient Address',
      [19]='Social Security Number'}
   hl7doc.generateDoc(msg.PID,[[.hl7docPID]],comment)

   -- report for multiple segments
   local commentPID={
      [3]="List of Ids",
      [5]="Patient Name",
      [7]='Birth Date',
      [8]='M = male, F = female, O = other',
      [10]='Patient Race',
      [11]='Patient Address',
      [19]='Social Security Number'}
   local R=hl7doc.makeDoc(msg.PID,commentPID)
   local commentPID_3={
      [1]='Identifier',
      [4]='Organisation that created the indentifier'}
   R=R..hl7doc.makeDoc(msg.PID[3],commentPID_3)
   local commentPID_3_1={
      [1]='First repeat'}
   R=R..hl7doc.makeDoc(msg.PID[3][1],commentPID_3_1)
   hl7doc.writeFile([[.hl7docCompDoc.html]], R)
end

Code snippet for hl7doc2 module:

hl7doc={}

function hl7doc.generateDoc(Segment,DocFile,Comment)
   local R=hl7doc.makeDoc(Segment,Comment)
   hl7doc.writeFile(DocFile..".html", R)
end

function hl7doc.makeDoc(Segment,Comment)
   local R = hl7doc.makeHeader(Segment)
   R = R..hl7doc.makeSegment(Segment,Comment)
   R = R..hl7doc.makeFooter()
   return R
end

function hl7doc.makeSegment(Segment,Comment)
   local S='<table class="hl7segment">'
   for i=1, #Segment do
      if hl7doc.isFieldPresent(Segment[i]) then
         S = S..formatRow(Segment,Comment,i)
      end   
   end
   S = S..'</table>'  
   return S
end

function formatRow(Segment, Comment, i) 
   local S = '<tr><th>'..i.. '</td><td>'..Segment[i]:nodeName()..'</td>'
   if Comment[i] then
      S = S..'<td>'..Comment[i]..'</td>'
   end
   S = S..'</tr>\n'
   return S
end

function hl7doc.isFieldPresent(Field)
   if Field:isNull() then
      return false
   else
      return true
   end
end

function hl7doc.makeHeader(Segment) 
--   local R =  '<html><head><title>Segment</title><meta http-equiv="refresh" content="1"><link rel="stylesheet" type="text/css" href="hl7.css"></head><body><h1>Segment '..Segment:nodeName()..'</h1>\n'
   local R =  '<html><head><title>Segment</title><link rel="stylesheet" type="text/css" href="hl7.css"></head><body><h1>Segment '..Segment:nodeName()..'</h1>\n'
   R = R..'<p>Only populated  fields are listed.</p>'
   return R
end

function hl7doc.writeFile(Name, Content)
   io.output(Name)
   io.write(Content)
   io.close()
end

function hl7doc.makeFooter()
   local F='</body></html>\n'
   return F
 end

function node.S(ANode)
   return tostring(ANode)
end

Possible modifications:

  • If you need a report in a different format than HTML you can duplicate the three make functions (makeHeader, makeSegment, makeFooter), and modify them to format as RTF, XML etc. Ideally you should make three format functions as in hl7doc and modify those instead.
  • This code inspects a single message only – adding persistence across messages could be useful for:
  • Including optional fields in the report
  • Modify the Comments table to include optional data/features for the report, for example:
    • Including/excluding fields regardless of whether or not they include data

A web service to document interfaces from inside Iguana [top]

So I had Kevin do an interesting proof of concept of seeing how effective we can be in generating automated documentation of interfaces in Iguana. Iguana is a very open platform that had most of the hooks that Kevin needed to get started.

The sample data that you see associated with each Translator interface is all stored in a SQLlite database and so it’s possible to access that data from Lua.

The Lua code itself for each channel is stored in fossil, and as I have shown before, it quite easy to get access to this code programatically. Kevin is able to port a channel’s code out into a sandbox, then capture the input and output by replacing the hl7.parse() and queue.push() commands, etc. Put those all together with the power of the Iguana Translator environment, and you have a spectacular toolset to help you go through and empirically analyze the actual data coming in and out of an interface.

It’s the best way to document an interface.

Kevin implemented the prototype as a web service channel (HTTP).

This type of analysis tool is interesting since it really has to employ a range of soft heuristics in order to be useful. For instance, if you look at the sample output of analyzing a PID segment, you can see the ‘Field Type’ column. Kevin’s set up a nice little system where the framework allows the definition of little rules which attempt to analyze the data. He’s set priority on the rules so a rule that identifies a field as a Phone Number takes priority over one the identifies a plain numeric field etc.

The sample value employs another heuristic of finding the longest example.

You can see the example of the output from the tool here: ifdoc.png

With a tool like this really the only model that makes sense is to make it open source and extensible. Anything less than this and a tool like this might be good during the sales process but wouldn’t be useful in the long run.

Customers should be able to extend and improve the heuristics to fit with their unique data feeds.

Another important thing is that it makes sense to be able to customize how the output is generated and where it goes to. If your organization has a wiki or share point wouldn’t it be awesome to integrate with that?

Obviously there many things we can do with this – should be easy to add a little tooltip or roll out that can give the list of codes that appear for a field type etc. We’re really hoping we’ll get some interested feedback from our user community in our forum that will give some ideas how to polish off this little project.

The Project Itself

The project is contained in the two attached zip files. The first, ifdoc_From_HTTP, as the name implies, should be loaded into a From HTTP(S) component. The second, ifdoc_To_Translator.zip should be loaded into a To Translator component. After saving Milestones for these two projects, you should be ready to go; start the channel, and click on the hyperlink provided in the From HTTP(S) component’s properties. You should be presented with a list of all your Translator projects. Selecting one will allow you to generate the inbound/outbound HL7 interface documentation for that particular project.

Note: If there are a large number of sample messages, the documentation could take a long time to generate. The web UI will keep you informed of its progress; for example, “Generating data (33 of 282 messages processed)…”.

ifdoc_From_HTTP.zip

ifdoc_To_Translator.zip