OpenFn / adaptors

The new home for OpenFn adaptors; re-usable connectors for the most common DPGs and DPI building blocks.
GNU General Public License v3.0
7 stars 8 forks source link

fhir: Resource builders #696

Open josephjclark opened 4 months ago

josephjclark commented 4 months ago

This is a concept to make it easy to generate FHIR resources.

Builder Functions.

We can introduce "builder" functions which make it easy to create a FHIR resource. Every FHIR resource type should have a builder (of course we can phase them in as we need to - maybe we'd try and start with the level 3 concepts).

Builders are chainable and return a plain, serializable JSON object at the end.

The builders are semantically rich and backed by constants. They should be nicely compatible with the openfn style and have sensible defaults.

For example (super rough sketch):

patient($.data.id)
  .name("joe", "clark") // add a name
  .name("joseph", "clark") // add a second name!
  .gender(MALE) // set gender with a constant

The builders should be smart. Patients should be presumed alive. Calling .deceased() with an optional date would set the right internal fields on the patient object.

Builder functions would be backed by full type definitions, so you get full code assist in Lightning. This lets you build a resource without having the fhir docs open.

Each builder has a typed entrypoint like patient(id, data), where data is just a blob of fhir JSON that will be assignged by default (if you would rather use json than the helper functions, go for it).

Implementation Notes

The builders would have to use subclasses or mixins or something so that the fhir concepts can be layered

Eg you might have a Name builder and an Address builder, and the Patient builder subclasses or mixes in both of those. So we have a lot of internal compositionality.

Opportunity #1

If we release the fhir builders as a standalone package, they could be used by any Javascript package using fhir resources. The fhir npm package is pretty generic.

Opportunity #2

A lot of this stuff is fairly rote, I think. Could we use AI to generate at least a starting point?

josephjclark commented 3 months ago

WIP Detailed pitch

I have been using AI this week to have a crack at a standalone fhir-builders package (unrelated to openfn) to understand the cost and size and feasibility of it.

My hope is that I can make a core solution - a pattern - and use AI to scale it up.

I'm going to have a crack at documenting the work here in this comment, then I can move it up into the main issue.

API

I've set up a good basic pattern for the API with strong type support.

One thing I've realised with the chaining design is that some chaining will have to be scoped.

So you can do patient().name().name(), where name will return the chain for the top level.

But some things are deeply nested, like contract.term(), where term needs to return a different chain, a scoped chain.

So as well as supporting nested chains, I wonder if we need a .out() or .back() or .root() to let you traverse back out of the chain you're in.

Do these scopes provide .json() calls? And if so does that return json for the whole thing?

Types

Bit of a sticking point here.

There is a @types/fhir package which we can build on. Hurrah!

Frustratingly though, every property is duplicated with an underscore, so the code assist experience is terrible:

Screenshot from 2024-08-02 09-33-33

I am not sure what to do about this. We may need to duplicate the types for a good UX

Versions

The version problem is abig one.

The builders need to be version aware.

My dream API would be this:

patient({}, '4') // this will create a v4 patient
  .toVersion('5') // this will auto migrate the patient to v5

Internally, we'd have to have a builder for each version. This ideally can be a re-assembly and override of the v1 )(or maybe we start at v5?). Each version also needs migration functions to the version either side, so you can migrate from v5 to v1 by recursively walking the stack.

It's just a lot of work. I don' t know if AI can scale it.

Here is a diff from v5 to v4 https://hl7.org/fhir/R5/diff.html

Documentation

The addition of all these builders is going to add a lot of noise to the docs.

Basically for each of the hundred or so Fhir Resource types, we now have a helper function (not an operation) to build it.

I don't want to document these indivdiually. I think we should expose a builder or b namespace, so you do b.patient(). And we document that namespace as a whole with examples.

The idea really is that every fhir resource has a builder function, with the same name, and full code assist when you start coding the mappings. So it shouldn't need a lot of docs.

josephjclark commented 3 months ago

One of the biggest problems these resource builders are going to have to solve is different implementation standards.

Different FHIR implementations come with different extensions and standards built-in. Each resource will have some default fields and boilerplate content, and extensions are added along with codings to the standard types (eg, residential-type on address).

A perfect builder function would handle this for you. It would set defaults and apply coding rules - all the user has to do is provide the actual value.

So the builder would be like .address({ city:'addis ababa', residential-type: 'rural' }), and it would generate JSON like this:

"address" : [
    {
      "extension" : [
        {
          "url" : "http://moh.gov.et/fhir/hiv/StructureDefinition/residential-type",
          "valueCodeableConcept" : {
            "coding" : [
              {
                "system" : "http://snomed.info/sct",
                "code" : "224804009"
              }
            ],
            "text" : "Rural"
          }
        }
      ],
      "city" : "Addis Ababa",
  ],

Maybe it would even provide content assist on the value 'rural'. 90% of the JSON there is boiler-plate, and that's EXACTLY the kind of thing the builders are designed to help.

So the concept is sound. The problem is that generic helpers aren't much use - they have to be implementation specific.

Solutions: