Skip to content

FHIR Client API Design Ideas

Rob Scanlon edited this page May 12, 2016 · 10 revisions

FHIR Client API Design

Following References

A lot of our current code centers around the idea of fetching data about a particular patient. Our FHIR-request wrapping code has encapsulated this nicely already, and as we're looking at using fhir_client, we're realizing it doesn't support most of this.

For example, what if we want to get all observations related to a patient from the past week? Currently, with fhir_client, we might have to do something like this:

patient_response = client.read(FHIR::Patient, patient_id)
patient = patient_response.resource

observation_response = client.search(FHIR::Observation, search: {
  parameters: {
    patient: patient.id,
    date: ">#{1.week.ago.to_date}"
  }
})
observations = observation_response.resource.entry.map(&:resource)

However, following the pattern of libraries like ActiveRecord, it would be nice to be able to do something like this:

patient = client.find(FHIR::Patient, patient_id)
observations = patient.observations(date: ">#{1.week.ago.to_date}")

Where the FHIR::Patient instance could remember the client it was fetched from, and could also be aware of associations and use that client to fetch them.

Or -- we could take the ActiveRecord analogy further, and do something like this:

FHIR::Base.client = client

patient = FHIR::Patient.find(patient_id)
observations = patient.observations(date: ">#{1.week.ago.to_date}")
conditions = patient.conditions

Alternatively, maybe auto-defining instance methods on FHIR::Patient would be tricky, and it would make more sense to do something like:

patient = FHIR::Patient.read(client, patient_id)
observations = FHIR::Observation.search(client, patient: patient, date: { gt: 1.week.ago })
conditions = FHIR::Condition.search(client, patient: patient)

To make this work in a very basic way, we could do:

module FHIR::Utilities
  module ClassMethods
    def error_for(bad_response)
      # figure out what errors to raise based on the response
    end

    def read(client, id)
      reply = client.read(self, id)
      reply.resource || raise error_for(reply.response)
    end
    
    def search(client, params)
      # potentially do some modification of params based on search param introspection
      reply = client.search(self, search: { parameters: params })
      raise error_for(reply.response) unless reply.resource
      reply.resource.entry.map(&:resource)
    end
  end

  def self.included(base)
    super
    base.extend(ClassMethods)
  end
end

###Comments from Rob

We like your idea of using ActiveRecord-style interface. In order for it to be useful to our Crucible test system, we would need access to the actual response as well. In your examples, would it make sense to expose it as an attribute, like patient.response, observations.response, etc? Or maybe we should store it in the client, like in client.last_response? What do you think make sense?

For searches, we may need to keep the return type as Bundle (instead of an array), because it is possible for bundles to be paginated. Would it be possible to have Bundle be enumerable, but have the next pages be fetched in a lazy fashion? See https://www.hl7.org/fhir/http.html#paging

Doing something like observations = patient.observations(..) might be a bit difficult for us right now, because I’m not sure if we have easy access to that relationship (that observations support a search named “patient” that searches over the Patient resource) when we are generating resources. We’d have to look into that. I think we can start by doing observation = FHIR::Observation.search(…), since we’d want to support that method of searching anyhow, and then look into figuring out how to automatically add supported search methods directly to the instances later.

I like not having to pass client in with each call as well, as it gets pretty redundant… though FHIR::Base.client = client isn’t particularly intuitive. I’m not sure if we have a much better solution though, maybe have it be a ‘configure’ method that accepts a client (among potentially other things)?