Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -172,6 +172,15 @@ that trigger conversion include equality (`=`), comparison (`<`, `<=`, `>`, `>=`
`contains`), and set functions (`union`, `distinct`, `intersect`, `exclude`, `subsetOf`,
`supersetOf`).

### 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. Other profiles, such as those defined in implementation guides (e.g.
the US Core Patient profile), would require profile validation, which is not implemented. Passing
such a profile URL results in an error, consistent with the specification's requirement to error
when a structure cannot be resolved (https://hl7.org/fhir/R4/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 @@ -299,7 +308,6 @@ documented in the table below.
| `testPolymorphicsB` | Test | | | Test case expects output in lenient mode for invalid property navigation. |
| `testType22` | Implementation | | | `is` with an unknown `System` type should evaluate to false, but the type resolver throws. |
| `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. Other profiles, such as those defined in implementation guides, would
* need profile validation, which is not implemented.
*
* See [specification](https://hl7.org/fhir/R4/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 @@ -150,6 +150,7 @@ internal fun Collection<Any>.invoke(

// FHIR-specific functions
// https://hl7.org/fhir/fhirpath.html#functions
"conformsTo" -> this.conformsTo(params, fhirPathTypeResolver)
"extension" -> this.extension(params, fhirModelNavigator)

else -> error("Function '$functionName' is not implemented.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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(),
)
}

@Test
fun `element does not conform to a different data type profile`() {
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 @@ private val fhirPathEngineStrict = FhirPathEngine.forR4(strictMode = true)
val skippedTestGroupToReasonMap =
mapOf(
"testEscapeUnescape" to "Unimplemented",
"testConformsTo" to "Unimplemented",
"Comparable" to "Unimplemented",
)

Expand Down
Loading