Proposed Solution to Mappings
For the sake of making the argument easy to understand, I decided to represent the data as simple maps and assert facts about them based on what we know. If I'm assuming something that I'm not sure about then I marked them, so we can verify or investigate them separately.
For this to work let's forget about MedicationRequest, R3, R4, etc. All we care about is the raw data (like json) and the type after decoding it.
For example, a MedicationRequest
of STU3
version can be represented as type A
. Mixing the actual resource name with its version only
makes understanding the problem harder without any contribution to the solution.
Raw data and Profile
Here is an example of an input data:
{"a": "x",
"b": "y"}
We know that a profile p1
can change the semantic of this data but not it's type. i.e. profile p1
cannot produce:
{"a": "x",
"b": "y",
"c": "z"}
Profile p1
can add extensions ex
to any element[1]. Both of these are valid objects:
{"a": "x",
"b": "y",
"ex": ["z"]}
{"a": {"ex": ["x"]},
"b": "y"}
It's important to note that profile p1
is not an operation. It just represents the meaning of a value
and how message processor system can interpret it. If our system is receiving:
{"a": "x",
"b": "y",
"ex": ["z"]}
I can still decode the object and assign a type to it. In another word, the type of data is preserved despite its profile(s). This implies two things:
"p": "p1"
is optional because it's just a hint and does not translate to an operation.- The only way of adding more information is done via extensions.
- Profile may change the meaning of something, like
"x"
means something else, but we don't care about it because, we are only interested in the shape (i.e. type) of an object not it's meaning[2].
Encoding
Assumptions:
- We know the type of raw data before processing it[3].
In our system we will deal with raw data in form of json mostly. That means we need to decode the data to some type.
I'll use lowercase letters to represent the object value (an instance) and uppercase letter to represent its type.
Also, data is represented with the letter d
. This can be json or xml.
d₁ = {"a": "x", "b": "y"}
a:A = {a: "x", b: "y"}
d₂ = {"a": "x", "b": "y", "c": "z"}
b:B = {a: "x", b: "y", c: "z"}
We have a decoder function f
that receives data d₁
and returns a:A
.
It's possible to do f(d2)
but we lost data that is in A
but not in B
[4].
Converter vs Transformer
Our goal is to create a converter function where c(a:A) = b:B
and c(b:B) = a:A
. For simplicity's sake, from now on we
only consider converting from A
to B
. All the assumptions and methods discussed, applies to converting other way around.
Another function of interest is the transformer. The distinction between a converter and a transformer is subtle but making this distinction is utmost important.
A transformer only changes one attribute, so that the result is the same basic type, but of a different capacity or intensity (voltage for example). A converter, on the other hand makes the resulting object different in type from then initial condition, for example changing alternating current to direct current.
This is not a just a terminology definition for the argument sake. Instead, it's an exact mirror of the actual implementation. What we
call "base converter" is implemented as a method convertResource: A->B
and the method to use StructureMap
s is called
transform: A->A
[5]. This implies that we can use a converter when we want to change the type whereas transformer is
used to change the attributes of data i.e. profiles.
The problem
We've defined some terms related to the system and observed their properties. Now we can use it to define (or at least revisit) the problem.
We want to create a converter c
that converts resource of type A
to B
. We know that the attributes of A
has changed due to
existence of profile(s). In order to state the problem and also discuss possible solution(s) first we need to define
some more terms. As before, we use a:A
to describe object of type A
however, now we also make a distinction when the
object a
has extended/changed with a profile. aₚ₁:A
has the same type A
but, its attributes has extended/changed by the profile p₁
.
So we need to implement: c(aₚ₁:A) = bₚ₁:B
Implementation
We can now discuss different solutions to the above problem.
Using both converter and transformer
Assumptions:
- converter is lossless over input type
A
[6] - convert is lossless over
p₁
attributes. That's it:c(aₚ₁:A) = sₚ₁:B
;noticesₚ₁
instead ofbₚ₁
[7]
For this solution to work we need a transformer such that: t(sₚ₁:B) = bₚ₁:B
. This can be done because the profile information
is not lost during convert operation (second assumption). The FML for this solution only concerns with transforming
the attributes that p₁ is describing.
We already know that the second assumption is not true[8]. But we know we can take advantage of advisor
to make it possible.
Advisors are not just for converting extensions. We can use the ignoreExtension
method to make converter lossless around
attributes defined via p₁
.
Using just advisor
Assumptions:
- Attributes of
p₁
is carried over from typeA
toB
without any change. - Above should be true for all profiles.
This solution only works if the logic for conversion, requires only preserving p₁
and not changing the attributes. One can
observe the solution can be simplified to c(aₚ₁:A) = sₚ₁:B
. That means we only need to make converter lossless over p₁
.
In another word, we only need an advisor to signal converter not to drop/ignore extensions.
Notes
- Extensions are not the only thing that a profile can add. The FHIR profiling describes what is possible to do with profiles. The main takeaway is a profile (or a set of them), no matter how complicated, only changes the attributes of data and not it's type. There is only one exception to this and that's when a profile introduces a new resource.
- FHIR documentation makes a difference between a message processor and a system that just moves messages around. A message processor must care about the semantics of a message. An example is when a message is marked as modifier. See docs
- This assumption holds true because, in our system, the type of message is given to us via
Content-Type
header. - We can't prove this behaviour for all types (we've only confirmed it for MedicationRequest)
but whether this holds or not, is not relevant. If there is an encoder that can receive type
A
and produces typeB
then, what's the purpose of a converter? - One can say, this naming is just a coincidence and, it's very likely that it is so. However, it doesn't
change the fact that the method signature matches exactly with the definition. Transform is
A->A
and convert isA->B
. - This assumption holds true because, as we've seen with MedicationRequest, the
onBehalfOf
field is preserved during conversion. Is this true for all other resources/types? - This assumption is not true but can be made so. Keep reading.
- According to documentation,
modified: true
is an exception to this. (todo: This needs reference) and implementation to prove.