- Introduction
- Generating hierarchical format HL7 interface documentation
- Generating flat format HL7 interface documentation
- A web service to document interfaces from inside Iguana
Introduction
This article was originally written for Iguana 5 so it contains version 5 screenshots, and may contain out of date references.
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:
- An indented report similar to an Iguana message annotation
- And a flat-style customizable implementation
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:
- Create a new shared module called “hl7doc” and copy paste in the code below.
- Add the code require(‘hl7doc’) at the top of the main module.
- Download the hl7.css file and copy it to the same folder as the report.
- 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:
- Create a new shared module called “hl7doc2” and copy paste in the code below.
- Add the code require(‘hl7doc2’) at the top of the main module.
- Download thehl7.css file and copy it to the same folder as the report.
- 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]
Note: This code was written for Iguana 5 and demonstrates how to extract code from Fossil version control. In Iguana 6 we are using Git for version control, so the code will need to be updated to reflect this.
If you need help to update this code for Iguana 6 please contact us at support@interfaceware.com.
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 (Iguana 5 doumentation), 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)…”.