Skip to content

Latest commit

 

History

History
510 lines (423 loc) · 29 KB

design-notes.md

File metadata and controls

510 lines (423 loc) · 29 KB

Project Design Notes

General References

FHIR Specifications

HAPI FHIR

Reference Links

Package Links

hapifhir/hapi-fhir

GitHub: hapifhir/hapi-fhir

hapifhir/org.hl7.fhir.core

GitHub: hapifhir/org.hl7.fhir.core

TypeScript Runtime Data Validator References

Zod Library (Selected)

superstruct Library

io-ts Library

JavaScript Object Serialization/Deserialization

FHIR Guidance

  • FHIR JSON representation of primitive elements

    • Representation

      • FHIR elements with primitive datatypes are represented in two parts:
        • A JSON property with the name of the element, which has a JSON type of number, boolean, or string
        • a JSON property with _ prepended to the name of the element, which, if present, contains the value's id and/or extensions
      • The FHIR types integer, unsignedInt, positiveInt and decimal are represented as a JSON number, the FHIR type boolean as a JSON boolean, and all other types (including integer64) are represented as a JSON string which has the same content as that specified for the relevant datatype. Whitespace is always significant (i.e. no leading and trailing spaces for non-strings).
    • Repeating Primitives (x..*)

      • In the case where the primitive element may repeat, it is represented in two arrays. JSON null values are used to fill out both arrays so that the id and/or extension are aligned with the matching value in the first array.

          "code": [ "au", "nz" ],
          "_code": [
            null,
            {
              "extension" : [ {
                "url" : "http://hl7.org/fhir/StructureDefinition/display",
                "valueString" : "New Zealand a.k.a Kiwiland"
              }]
            }
          ]

Libraries

Date/DateTime Handling

FHIR Date/Time Primitives

NOTE: Where a timezone (+zz:zz) is specified, UTC (Z) may also be specified

  • date
    • YYYY
    • YYYY-MM
    • YYYY-MM-DD
  • dateTime
    • YYYY
    • YYYY-MM
    • YYYY-MM-DD
    • YYYY-MM-DDThh:mm:ss+zz:zz / YYYY-MM-DDThh:mm:ssZ
    • YYYY-MM-DDThh:mm:ss.sss+zz:zz / YYYY-MM-DDThh:mm:ss.sssZ
  • instant
    • YYYY-MM-DDThh:mm:ss.sss+zz:zz / YYYY-MM-DDThh:mm:ss.sssZ

Luxon

  • References

  • Notes

    • Immutable, chainable, unambiguous API
    • Native time zone and Intl support (no locale or tz files)
    • Months in Luxon are 1-indexed instead of 0-indexed like in Moment and the native Date type
    • Luxon has both a Duration type and an Interval type
    • Luxon parsers are very strict
    • Luxon uses getters instead of accessor methods, so dateTime.year instead of dateTime.year()
    • Luxon centralizes its "setters", like dateTime.set({year: 2016, month: 4}) instead of dateTime.year(2016).month(4)

Luxon Parsing (ISO 8601)

  • All supported FHIR formats are directly parsable by Luzon
    • const dt: DateTime = DateTime.fromISO("2016");
    • const dt: DateTime = DateTime.fromISO("2016-05");
    • const dt: DateTime = DateTime.fromISO("2016-05-25");
    • const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15Z");
    • const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15-04.00");
    • const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15.123Z");
    • const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15.123-04.00");
  • DateTime.fromISO() will default to the system's local timezone unless an offset is included in the dateTime string or a timezone option is provided to override the default:
    • const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15Z");
    • const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15-04.00");
    • const dt: DateTime = DateTime.fromISO("2016-05-25", { zone: "utc" });
    • const dt: DateTime = DateTime.fromISO("2016-05-25", { zone: "America/New_York" });
  • DateTime.now().toISO() will default to the system's local date/time and timezone in ISO format
  • DateTime.utc().toISO() will default to UTC and timezone in ISO format

Luxon Formatting (ISO 8601)

  • to ISO
    • dt.toISO(); //=> '2017-04-20T11:32:00.000-04:00'
    • dt.toISO({ suppressMilliseconds: true }); //=> '2017-04-20T11:32:00-04:00'
    • dt.toISODate(); //=> '2017-04-20'
  • to Format
    • dt.toFormat("yyyy"); //=> '2017'
    • dt.toFormat("yyyy-MM"); //=> '2017-04'

Design Decisions

Auto Create DataType Elements on getXxxxElement

  • NOTE: HAPI FHIR has been designed to "auto create" data type elements rather than to return null. See class header content in Configuration.java
  • Therefore, we will follow suit and not return undefined for all getXxxxElement methods.

TypeScript Runtime Data Validator for Primitives

Based on past experience with FHIR data model generators for TypeScript, I wanted to define at least FHIR primitive data types using TypeScript runtime data validators. I was somewhat familiar with io-ts and wanted to gain a better understanding of that library. In my investigation, I ran across A TypeScript Runtime Data Validators Comparison. The article defines a set of desired design goals a runtime data validator library should satisfy. Subsequent articles provided reviews of six (6) popular runtime data validator libraries that documented how well the libraries satisfied the design goals.

Based on these reviews and my subsequent "deep dive", I decided to use the Zod library.

JavaScript Object Serialization/Deserialization Approach

After researching this subject in many articles on the web and after investigating the various serialization/deserialization libraries (see above), I came to the conclusion that a "generic" approach to serialization/deserialization was overkill in this use case. While complex and primitive data type models are handcrafted, the ultimate goal is to use a code generator to create all FHIR resource data models for each version of FHIR. From experience, creating class templates for a code generator is a straight-forward process. Trying to generalize the serialization/deserialization functionality adds unnecessary complexity to the templates and the generated code.

My goal for the serialization/deserialization functionality was to access the functionality from the FHIR resource data model classes - not from a separate "tool"/"parser" as is the case in the HAPI FHIR project. To that end, I decided to provide a public toJSON() instance method on each FHIR resource data model class to perform the serialization. This allows consumers of these data model classes to manipulate the data models instances as needed and then when ready, execute the toJSON() method of the instance to render the FHIR compatible JSON data object. For the deserialization process, I decided to provide a public static parse(sourceJson: JSON.Object) method that can consume the provided JSON data object and return a fully populated FHIR resource data model instance.

Unit Testing Strategy

Unit testing best practices recommend testing the public interfaces and not the abstract or private interfaces. While I agree with this recommendation, I have chosen to ignore this recommendation for needs of this proof-of-concept project. The primary goal of this project is to create a set of hand-crafted data models that can be used to create templates from which a production-level project can be created to generate FHIR data models using FHIR StructureDefinitions. Therefore, the hand-crafted data models must be fully documented with significant unit testing to verify all functionality to be generated. The abstract core FHIR models will be created with a minimal set of functionality to define a minimal viable product. As FHIR models are hand-crafted for data types and resources, additional functionality will be added to meet design needs. The desire is to write all unit tests as these data models evolve into a final state.

Therefore for convenience, it will be easier to test the abstract classes rather than replicating tests across all hand-crafted derived classes.

Code Documentation

Background

Most Node-based projects make use of JSDoc for documenting the code base and optionally generating project documentation. For projects using TypeScript, Microsoft provides the JSDoc Reference. This combination works for many cases. In my case, this was not a satisfactory approach. My biggest issue is the inability to provide documentation for TypeScript type definitions. Generating the project documentation using JSDoc and TypeScript felt like a kluge.

As an alternative, Microsoft provides TSDoc as a replacement for JSDoc in TypeScript projects. "TSDoc is a proposal to standardize the doc comments used in TypeScript code, so that different tools can extract content without getting confused by each other's markup. Microsoft also provides a library package that provides an open source reference implementation of a parser. Using this library is an easy way to ensure that your tool is 100% compatible with the standard."

I decided to investigate TSDoc for my use in this project. I noticed the TSDoc documentation mentions various tools that interact with the JSDoc/TSDoc notations and tags. One that caught my eye is TypeDoc. It generates project documentation based on JSDoc/TSDoc notations and tags. Its home page is very brief. It provides a 2-step process to use TypeDoc out of the box:

# Install
npm install --save-dev typedoc

# Execute typedoc on your project
npx typedoc src/index.ts

Following the above steps, I was pleasantly surprised by the quality of the automatic generation of the project's documentation. The current state of the project had very limited JSDoc notations, but TypeDoc generated very good documentation based on the actual TypeScript code. Where JSDoc notation existed, TypeDoc parsed that content and added it to the generated documentation. I was immediately sold on TypeDoc!

The TypeDoc ecosystem includes plugins for various uses. I was thrilled when I discovered a plugin for Zod (described above). TypeDoc provides extensive configuration, but in my case, I only needed to included five (5) options!

Therefore, I am using TypeDoc to generate project documentation!

WARNINGS for Code Generator
  • We will need to check header descriptive content for relative links (e.g., [Location](location.html#)) to the FHIR specification to change them to absolute links (e.g., [Location](https://hl7.org/fhir/location.html#)).

  • We will need to check header descriptive content for whitespace characters (e.g., \t, \n, etc.) replacing them with a space:

    /**
     * Replace line breaks (i.e., '\n' and '\r') with a single space (i.e., ' ').
     * Replace with a space character rather than an empty string to prevent combining
     * text where the only separating whitespace is '\n' or '\r' (observed in various
     * StructureDefinition descriptions).
     *
     * @param str - source string value
     * @returns new string value with each line break replaced with a space character
     */
    export const stripLineBreaks = (str: string | undefined | null): string => {
      const tempValue = str || '';
      const regex = /[\r\n]+/g;
      return tempValue.replace(regex, ' ');
    };

Class Header Template

/**
 * <StructureDefinition.type> Class
 *
 * @remarks
 * <StructureDefinition.description>
 *
 * <StructureDefinition.purpose>
 *
 * **FHIR Specification**
 * - **Short:** <StructureDefinition.snapshot.element[0]?.short>
 * - **Definition:** <StructureDefinition.snapshot.element[0]?.definition>
 * - **Comment:** <StructureDefinition.snapshot.element[0]?.comment>
 * - **Requirements:** <StructureDefinition.snapshot.element[0]?.requirements>
 * - **FHIR Version:** <StructureDefinition.fhirVersion>
 *
 * @privateRemarks
 * Loosely based on HAPI FHIR org.hl7.fhir-core.r4.model.<StructureDefinition.type>
 *
 * @category Resource Models | Datatypes: Complex
 * @see [FHIR <StructureDefinition.type>](<StructureDefinition.url>)
 */

Component (BackboneElement) Class Header Template

The <StructureDefinition.snapshot.element[i].path> will be reformatted to <ReformattedPathComponent> as follows:

  • The path value will be PascalCase
  • The . separator will be removed
  • 'Component' will be appended to the reformatted path
/**
 * <ReformattedPathComponent> Subclass for `<StructureDefinition.snapshot.element[i].path>`
 *
 * @remarks
 * **FHIR Specification**
 * - **Short:** <StructureDefinition.snapshot.element[i]?.short>
 * - **Definition:** <StructureDefinition.snapshot.element[i]?.definition>
 * - **Comment:** <StructureDefinition.snapshot.element[i]?.comment>
 * - **Requirements:** <StructureDefinition.snapshot.element[i]?.requirements>
 *
 * @category Resource Models
 * @see [FHIR <StructureDefinition.type>](<StructureDefinition.url>)
 */

Field Header Template

/**
 * <StructureDefinition.snapshot.element[i].path> Element
 *
 * @remarks
 * **FHIR Specification**
 * - **Short:** <StructureDefinition.snapshot.element[i]?.short>
 * - **Definition:** <StructureDefinition.snapshot.element[i]?.definition>
 * - **Comment:** <StructureDefinition.snapshot.element[i]?.comment>
 * - **Requirements:** <StructureDefinition.snapshot.element[i]?.requirements>
 * - **FHIR Type:** `<StructureDefinition.snapshot.element[i].type.code[0]>`
 *   - _TargetProfiles_: [ <StructureDefinition.snapshot.element[i].type.code[0].taretProfile[?]> ]
 * - **Cardinality:** <StructureDefinition.snapshot.element[i].min>..<StructureDefinition.snapshot.element[i].max>
 * - **isModifier:** <StructureDefinition.snapshot.element[i].isModifier>
 * - **isModifierReason:** <StructureDefinition.snapshot.element[i].isModifierReason?>
 * - **isSummary:** <StructureDefinition.snapshot.element[i].isSummary>
 */

Polymorphic Field Header Template

/**
 * <StructureDefinition.snapshot.element[i].path> Element
 *
 * @remarks
 * **FHIR Specification**
 * - **Short:** <StructureDefinition.snapshot.element[i]?.short>
 * - **Definition:** <StructureDefinition.snapshot.element[i]?.definition>
 * - **Comment:** <StructureDefinition.snapshot.element[i]?.comment>
 * - **Requirements:** <StructureDefinition.snapshot.element[i]?.requirements>
 * - **FHIR Types:**
 *  - `<StructureDefinition.snapshot.element[i].type.code[j]>`
 *    - _TargetProfiles_: [ <StructureDefinition.snapshot.element[i].type.code[0].taretProfile[?]> ]
 * - **Cardinality:** <StructureDefinition.snapshot.element[i].min>..<StructureDefinition.snapshot.element[i].max>
 * - **isModifier:** <StructureDefinition.snapshot.element[i].isModifier>
 * - **isModifierReason:** <StructureDefinition.snapshot.element[i].isModifierReason?>
 * - **isSummary:** <StructureDefinition.snapshot.element[i].isSummary>
 */

TypeScript Circular References

References:

The FHIR specification defines data types and resources (data models) based on the FHIR Type Framework. Ultimately, the specification defines these data structures for exchanging healthcare data using JSON and/or XML. By design, circular references in the specification cannot be prevented but are not a problem in the actual data structures. Examples of circular references in the FHIR specification include:

  • The FHIR Reference data type includes the Identifier data type while the Identifier data type includes the Reference data type.
  • All data types inherit from the FHIR Element that includes a property for FHIR Extensions while the FHIR Extension references almost all FHIR data types.

When designing data structures using TypeScript, these circular references cannot be avoided and must be resolved. Various strategies exist to resolve circular references such as:

  • Use relative imports inside of the same module
  • Move common code into a separate file to be imported
  • Move code from one module to another
  • Last resort: Combine files with circular dependencies into one file

Unfortunately, due to the FHIR specification, the "last resort" above has been occasionally used in this project. When this has been done, a file/module header block documents these cases.