Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ specification across different FHIR versions. In particular, DateTime and Time i
include partial time (e.g. missing minutes and seconds), which is not allowed in FHIR. Therefore,
new implementations are needed.

### Profile validation

The `conformsTo()` function supports the base FHIR profiles
(`http://hl7.org/fhir/StructureDefinition/<Type>`): the input element's type is compared to the
type named by the structure. Custom profiles (e.g. US Core) would require profile validation,
Comment thread
FikriMilano marked this conversation as resolved.
Outdated
which is not implemented. Passing a custom profile URL results in an error, consistent with the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec does say this:

If the input is not a single item, the structure is empty, or the structure cannot be resolved to a valid profile, the result is empty.

so does it not mean we should return emtpy collection rather than throwing an error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me clarify, that's the current spec text, but R4 says:

If the structure cannot be resolved to a valid profile, an error is thrown 

https://hl7.org/fhir/R4/fhirpath.html#functions

and testConformsTo3 expects an execution error for conformsTo('http://trash')

<test name="testConformsTo3" inputfile="patient-example.xml"><expression invalid="execution">conformsTo('http://trash')</expression></test>

so returning empty would fail conformance.

The doc link was pointing at the versionless (current, https://hl7.org/fhir/fhirpath.html#functions) spec though, which is misleading. I can fix the R4 link.

But main point is, do we want to follow the latest spec, or refer to R4?
We might want to refer to the latest spec, since it's the up to date spec.
Or, we might pick R4 bcz our test case refers to tests-fhir-r4.xml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very good question - one thing to note is that this part of the spec is part of FHIR and not actually part of FHIRPath... and we DO have FHIR version specific implementations - we have FhirEngine.forR4 forR4B and forR5... so if we're to be really serious about this, we can actually implement different behaviors for different verions... but I'm not sure if it's worth the effort.

I don't have a super strong view here - but if we're not going to diverge between different fhir versions, it seems to make sense to implement the latest version.

specification's requirement to error when a structure cannot be resolved
(https://hl7.org/fhir/fhirpath.html#functions).

### Timezone offset in date time values

This FHIRPath implementation adopts a strict, safety-first approach to date time comparisons,
Expand Down Expand Up @@ -296,7 +305,6 @@ documented in the table below.
| `testType22` | Implementation | | | `is` with an unknown `System` type should evaluate to false, but the type resolver throws. |
| `testType23` | Implementation | | | As `testType20`. |
| `testTypeA*` | Implementation | | | Evaluating `Parameters.parameter[x].value` crashes with `NoSuchElementException`. |
| `testConformsTo*` | Implementation | | | Function `conformsTo` is not implemented. |
| `LowBoundaryDateTimeMillisecond1` | Specification/Test | | | Diverges from FHIRPath specification. See [Discussion](https://chat.fhir.org/#narrow/channel/179266-fhirpath/topic/lowBoundary.20and.20highBoundary.20with.20incomplete.20date.20time/with/611113639). |
| `HighBoundaryDateTimeMillisecond1` | Specification/Test | | As above. | As above. |
| `HighBoundaryDateTimeMillisecond3` | Specification/Test | | As above. | As above. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package dev.ohs.fhir.fhirpath.functions

import dev.ohs.fhir.fhirpath.model.FhirModelNavigator
import dev.ohs.fhir.fhirpath.toFhirPathType
import dev.ohs.fhir.fhirpath.types.FhirPathTypeResolver
import dev.ohs.fhir.fhirpath.types.FhirType

/**
* Returns the extensions with the given url on each item in the input collection.
Expand All @@ -40,3 +43,43 @@ internal fun Collection<Any>.extension(
}
.filter { fhirModelNavigator.accessProperty(it, "url") == url }
}

private const val BASE_STRUCTURE_DEFINITION_PREFIX = "http://hl7.org/fhir/StructureDefinition/"

/**
* Returns whether the single input element conforms to the profile specified by the structure
* argument.
*
* Only the base FHIR profiles (`http://hl7.org/fhir/StructureDefinition/<Type>`) are supported: the
* input's type is compared to `<Type>`. An error is thrown if the structure cannot be resolved, as
* the specification requires. Custom profiles would need profile validation, which is not
* implemented.
*
* See [specification](https://hl7.org/fhir/fhirpath.html#functions).
*/
internal fun Collection<Any>.conformsTo(
params: List<Any>,
fhirPathTypeResolver: FhirPathTypeResolver,
): Collection<Boolean> {
check(size <= 1) { "conformsTo() cannot be called on a collection with more than 1 item" }
val item = singleOrNull() ?: return emptyList()
// The structure argument can be a FHIR string (e.g. from a resource element), so it is
// converted before use like any other string parameter.
val structure =
params.singleOrNull()?.toFhirPathType(fhirPathTypeResolver) as? String
?: error("conformsTo() requires a structure argument")

if (!structure.startsWith(BASE_STRUCTURE_DEFINITION_PREFIX)) {
error("Cannot resolve structure definition: $structure")
}
// resolveFromString throws for an unknown type name, satisfying the specification's
// requirement to error when the structure cannot be resolved. It falls back to System types
// for names that are not FHIR types (e.g. `String` rather than `string`), which are not valid
// structure definitions either, so those must error as well.
val targetType =
fhirPathTypeResolver.resolveFromString(structure.removePrefix(BASE_STRUCTURE_DEFINITION_PREFIX))
if (targetType !is FhirType) {
error("Cannot resolve structure definition: $structure")
}
return listOf(fhirPathTypeResolver.resolveFromObject(item) == targetType)
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ internal fun Collection<Any>.invoke(
// FHIR-specific functions
// https://hl7.org/fhir/fhirpath.html#functions
"extension" -> this.extension(params, fhirModelNavigator)
"conformsTo" -> this.conformsTo(params, fhirPathTypeResolver)

// Utility functions
// https://hl7.org/fhirpath/N1/#utility-functions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2026 Open Health Stack Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.ohs.fhir.fhirpath

import dev.ohs.fhir.model.r4.Resource
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlinx.serialization.json.Json

private val fhirPathEngine = FhirPathEngine.forR4()

private val patient: Resource =
Json { ignoreUnknownKeys = true }
.decodeFromString("""{"resourceType": "Patient", "name": [{"family": "Chalmers"}]}""")

class ConformsToTest {

@Test
fun `element conforms to its base data type profile`() {
assertEquals(
listOf(true),
fhirPathEngine
.evaluateExpression(
"name.first().conformsTo('http://hl7.org/fhir/StructureDefinition/HumanName')",
patient,
)
.toList(),
)
assertEquals(
listOf(false),
fhirPathEngine
.evaluateExpression(
"name.first().conformsTo('http://hl7.org/fhir/StructureDefinition/Address')",
patient,
)
.toList(),
)
Comment thread
FikriMilano marked this conversation as resolved.
}

@Test
fun `unresolvable structure throws`() {
assertFailsWith<Exception> {
fhirPathEngine.evaluateExpression(
"conformsTo('http://hl7.org/fhir/StructureDefinition/NotARealType')",
patient,
)
}
}

@Test
fun `structure resolving only to a System type throws`() {
// `String` is not a FHIR structure definition (FHIR's is lowercase `string`), so it must
// error rather than fall back to the System type and return false. The input element is a
// FHIR string, so with the lowercase URL this would return true; the error is about the
// structure being unresolvable, not about the input.
assertFailsWith<Exception> {
fhirPathEngine.evaluateExpression(
"name.first().family.conformsTo('http://hl7.org/fhir/StructureDefinition/String')",
patient,
)
}
}

@Test
fun `empty input returns empty`() {
assertEquals(
emptyList(),
fhirPathEngine
.evaluateExpression(
"{}.conformsTo('http://hl7.org/fhir/StructureDefinition/Patient')",
patient,
)
.toList(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ val skippedTestGroupToReasonMap =
mapOf(
"testEscapeUnescape" to "Unimplemented",
"testVariables" to "Unimplemented",
"testConformsTo" to "Unimplemented",
"Comparable" to "Unimplemented",
"Precision" to "Unimplemented",
)
Expand Down
Loading