From e29f6e5997be448adfd077a02469c21a1ebe5c23 Mon Sep 17 00:00:00 2001 From: Copybara <copybara@example.com> Date: Mon, 6 May 2024 15:22:05 -0400 Subject: [PATCH] Project import generated by Copybara. GitOrigin-RevId: b8bebf065c6b2212c17d7d4dbfb8e5427b9e96d7 --- .github/CODEOWNERS | 3 + .github/workflows/test.yaml | 24 + README.md | 99 + fhirpath/.gitignore | 4 + fhirpath/compopts/compopts.go | 55 + fhirpath/doc.go | 9 + fhirpath/evalopts/evalopts.go | 74 + fhirpath/fhirpath.go | 118 + fhirpath/fhirpath_expression_test.go | 292 ++ fhirpath/fhirpath_test.go | 1413 ++++++ fhirpath/fhirpathtest/fhirpathtest.go | 51 + .../fhirpathtest/fhirpathtest_example_test.go | 47 + fhirpath/fhirpathtest/fhirpathtest_test.go | 61 + fhirpath/internal/compile/compile.go | 46 + fhirpath/internal/compile/doc.go | 5 + fhirpath/internal/expr/arithmetic.go | 197 + fhirpath/internal/expr/booleans.go | 47 + fhirpath/internal/expr/context.go | 47 + fhirpath/internal/expr/doc.go | 5 + fhirpath/internal/expr/expressions.go | 779 ++++ fhirpath/internal/expr/expressions_test.go | 1465 ++++++ fhirpath/internal/expr/exprtest/doc.go | 5 + fhirpath/internal/expr/exprtest/doubles.go | 37 + fhirpath/internal/expr/operators.go | 29 + fhirpath/internal/funcs/doc.go | 6 + fhirpath/internal/funcs/function.go | 94 + fhirpath/internal/funcs/function_table.go | 23 + .../internal/funcs/function_table_test.go | 49 + fhirpath/internal/funcs/function_test.go | 187 + fhirpath/internal/funcs/impl/conversion.go | 646 +++ .../internal/funcs/impl/conversion_test.go | 2112 +++++++++ fhirpath/internal/funcs/impl/errors.go | 9 + fhirpath/internal/funcs/impl/existence.go | 106 + .../internal/funcs/impl/existence_test.go | 494 ++ fhirpath/internal/funcs/impl/filtering.go | 35 + .../internal/funcs/impl/filtering_test.go | 114 + fhirpath/internal/funcs/impl/math.go | 367 ++ fhirpath/internal/funcs/impl/math_test.go | 1059 +++++ fhirpath/internal/funcs/impl/not.go | 25 + fhirpath/internal/funcs/impl/not_test.go | 54 + fhirpath/internal/funcs/impl/r4.go | 42 + fhirpath/internal/funcs/impl/r4_test.go | 120 + fhirpath/internal/funcs/impl/strings.go | 435 ++ fhirpath/internal/funcs/impl/strings_test.go | 1148 +++++ fhirpath/internal/funcs/impl/subsetting.go | 238 + .../internal/funcs/impl/subsetting_test.go | 677 +++ fhirpath/internal/funcs/impl/utility.go | 24 + fhirpath/internal/funcs/impl/utility_test.go | 50 + fhirpath/internal/funcs/table.go | 392 ++ fhirpath/internal/grammar/fhirpath.g4 | 179 + fhirpath/internal/grammar/fhirpath_lexer.go | 410 ++ fhirpath/internal/grammar/fhirpath_parser.go | 4109 +++++++++++++++++ fhirpath/internal/grammar/fhirpath_visitor.go | 135 + fhirpath/internal/grammar/generate.go | 3 + fhirpath/internal/grammar/generate.sh | 12 + fhirpath/internal/opts/opts.go | 66 + fhirpath/internal/parser/doc.go | 6 + fhirpath/internal/parser/error_handling.go | 22 + fhirpath/internal/parser/transforms.go | 12 + fhirpath/internal/parser/visitor.go | 515 +++ fhirpath/internal/reflection/consts.go | 7 + fhirpath/internal/reflection/doc.go | 5 + fhirpath/internal/reflection/elements.go | 38 + .../internal/reflection/type_specifier.go | 138 + .../reflection/type_specifier_test.go | 308 ++ fhirpath/options.go | 32 + fhirpath/patch/doc.go | 7 + fhirpath/patch/patch.go | 597 +++ fhirpath/patch/patch_test.go | 824 ++++ fhirpath/system/cmp.go | 81 + fhirpath/system/collection.go | 239 + fhirpath/system/collection_test.go | 320 ++ fhirpath/system/consts.go | 26 + fhirpath/system/date.go | 269 ++ fhirpath/system/date_test.go | 437 ++ fhirpath/system/date_time.go | 289 ++ fhirpath/system/date_time_test.go | 474 ++ fhirpath/system/doc.go | 8 + fhirpath/system/layouts.go | 86 + fhirpath/system/primitives.go | 303 ++ fhirpath/system/primitives_test.go | 402 ++ fhirpath/system/quantity.go | 211 + fhirpath/system/quantity_test.go | 117 + fhirpath/system/time.go | 192 + fhirpath/system/time_test.go | 383 ++ fhirpath/system/types.go | 166 + fhirpath/system/types_test.go | 281 ++ go.mod | 28 + go.sum | 715 +++ gotchas.md | 25 + internal/bundle/bundle.go | 79 + internal/bundle/bundle_entry.go | 285 ++ internal/bundle/bundle_entry_example_test.go | 94 + internal/bundle/bundle_entry_test.go | 477 ++ internal/bundle/bundle_option.go | 51 + internal/bundle/bundle_test.go | 216 + internal/bundle/identity.go | 27 + internal/bundle/identity_test.go | 48 + internal/bundleopt/bundleopt.go | 25 + .../containedresource/contained_resource.go | 186 + .../contained_resource_example_test.go | 88 + .../contained_resource_test.go | 291 ++ internal/element/canonical/canonical.go | 151 + internal/element/canonical/canonical_test.go | 221 + .../codeableconcept/codeableconcept.go | 17 + internal/element/coding/coding.go | 20 + internal/element/element.go | 6 + internal/element/etag/etag.go | 25 + internal/element/etag/etag_test.go | 58 + internal/element/extension/extension.go | 232 + .../extension/extension_example_test.go | 42 + internal/element/extension/extension_test.go | 446 ++ internal/element/extract.go | 208 + internal/element/extract_test.go | 315 ++ internal/element/identifier/docs.go | 7 + internal/element/identifier/identifier.go | 174 + .../identifier/identifier_example_test.go | 52 + .../element/identifier/identifier_test.go | 329 ++ internal/element/identifier/opts.go | 103 + internal/element/meta/meta.go | 95 + internal/element/meta/meta_example_test.go | 25 + internal/element/meta/meta_test.go | 233 + internal/element/reference/identity.go | 131 + internal/element/reference/identity_test.go | 306 ++ internal/element/reference/literal.go | 346 ++ internal/element/reference/literal_test.go | 361 ++ internal/element/reference/reference.go | 184 + internal/element/reference/reference_test.go | 455 ++ internal/fhir/constraints.go | 129 + internal/fhir/doc.go | 13 + internal/fhir/duration.go | 110 + internal/fhir/duration_test.go | 161 + internal/fhir/elements_general.go | 307 ++ internal/fhir/elements_general_test.go | 45 + internal/fhir/elements_metadata.go | 19 + internal/fhir/elements_primitive.go | 343 ++ internal/fhir/elements_primitive_test.go | 390 ++ internal/fhir/elements_special.go | 26 + internal/fhir/elements_special_test.go | 28 + internal/fhir/encoding.go | 26 + internal/fhir/encoding_test.go | 33 + internal/fhir/iface.go | 146 + internal/fhir/iface_test.go | 483 ++ internal/fhir/protofields.go | 13 + internal/fhir/time.go | 358 ++ internal/fhir/time_test.go | 352 ++ internal/fhirconv/doc.go | 5 + internal/fhirconv/integer.go | 112 + internal/fhirconv/integer_test.go | 182 + internal/fhirconv/string.go | 158 + internal/fhirconv/string_test.go | 358 ++ internal/fhirconv/time.go | 138 + internal/fhirconv/time_test.go | 290 ++ internal/fhirtest/doc.go | 12 + internal/fhirtest/elements.go | 46 + internal/fhirtest/meta.go | 65 + internal/fhirtest/random.go | 69 + internal/fhirtest/resources.go | 298 ++ internal/fhirtest/resources_example_test.go | 46 + internal/fhirtest/resources_test.go | 326 ++ internal/narrow/narrow.go | 176 + internal/narrow/narrow_example_test.go | 268 ++ internal/narrow/narrow_test.go | 703 +++ internal/protofields/descriptor.go | 12 + internal/protofields/dummies.go | 364 ++ internal/protofields/fields.go | 212 + internal/protofields/fields_test.go | 61 + internal/protofields/strcase.go | 22 + internal/protofields/update.go | 61 + internal/protofields/update_test.go | 138 + internal/resource/canonical_identity.go | 58 + internal/resource/canonical_identity_test.go | 88 + internal/resource/consts.go | 441 ++ internal/resource/identity.go | 187 + internal/resource/identity_test.go | 137 + internal/resource/options.go | 31 + internal/resource/options_test.go | 82 + internal/resource/patient/patient.go | 172 + internal/resource/patient/patient_test.go | 172 + internal/resource/resource.go | 244 + internal/resource/resource_example_test.go | 32 + internal/resource/resource_test.go | 390 ++ internal/resource/type.go | 86 + internal/resource/type_test.go | 127 + internal/resourceopt/resourceopt.go | 83 + internal/slices/slices.go | 219 + internal/slices/slices_example_test.go | 220 + internal/slices/slices_test.go | 707 +++ internal/stablerand/doc.go | 18 + internal/stablerand/rand.go | 120 + internal/units/doc.go | 5 + internal/units/time.go | 88 + 192 files changed, 43115 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/test.yaml create mode 100644 README.md create mode 100644 fhirpath/.gitignore create mode 100644 fhirpath/compopts/compopts.go create mode 100644 fhirpath/doc.go create mode 100644 fhirpath/evalopts/evalopts.go create mode 100644 fhirpath/fhirpath.go create mode 100644 fhirpath/fhirpath_expression_test.go create mode 100644 fhirpath/fhirpath_test.go create mode 100644 fhirpath/fhirpathtest/fhirpathtest.go create mode 100644 fhirpath/fhirpathtest/fhirpathtest_example_test.go create mode 100644 fhirpath/fhirpathtest/fhirpathtest_test.go create mode 100644 fhirpath/internal/compile/compile.go create mode 100644 fhirpath/internal/compile/doc.go create mode 100644 fhirpath/internal/expr/arithmetic.go create mode 100644 fhirpath/internal/expr/booleans.go create mode 100644 fhirpath/internal/expr/context.go create mode 100644 fhirpath/internal/expr/doc.go create mode 100644 fhirpath/internal/expr/expressions.go create mode 100644 fhirpath/internal/expr/expressions_test.go create mode 100644 fhirpath/internal/expr/exprtest/doc.go create mode 100644 fhirpath/internal/expr/exprtest/doubles.go create mode 100644 fhirpath/internal/expr/operators.go create mode 100644 fhirpath/internal/funcs/doc.go create mode 100644 fhirpath/internal/funcs/function.go create mode 100644 fhirpath/internal/funcs/function_table.go create mode 100644 fhirpath/internal/funcs/function_table_test.go create mode 100644 fhirpath/internal/funcs/function_test.go create mode 100644 fhirpath/internal/funcs/impl/conversion.go create mode 100644 fhirpath/internal/funcs/impl/conversion_test.go create mode 100644 fhirpath/internal/funcs/impl/errors.go create mode 100644 fhirpath/internal/funcs/impl/existence.go create mode 100644 fhirpath/internal/funcs/impl/existence_test.go create mode 100644 fhirpath/internal/funcs/impl/filtering.go create mode 100644 fhirpath/internal/funcs/impl/filtering_test.go create mode 100644 fhirpath/internal/funcs/impl/math.go create mode 100644 fhirpath/internal/funcs/impl/math_test.go create mode 100644 fhirpath/internal/funcs/impl/not.go create mode 100644 fhirpath/internal/funcs/impl/not_test.go create mode 100644 fhirpath/internal/funcs/impl/r4.go create mode 100644 fhirpath/internal/funcs/impl/r4_test.go create mode 100644 fhirpath/internal/funcs/impl/strings.go create mode 100644 fhirpath/internal/funcs/impl/strings_test.go create mode 100644 fhirpath/internal/funcs/impl/subsetting.go create mode 100644 fhirpath/internal/funcs/impl/subsetting_test.go create mode 100644 fhirpath/internal/funcs/impl/utility.go create mode 100644 fhirpath/internal/funcs/impl/utility_test.go create mode 100644 fhirpath/internal/funcs/table.go create mode 100644 fhirpath/internal/grammar/fhirpath.g4 create mode 100644 fhirpath/internal/grammar/fhirpath_lexer.go create mode 100644 fhirpath/internal/grammar/fhirpath_parser.go create mode 100644 fhirpath/internal/grammar/fhirpath_visitor.go create mode 100644 fhirpath/internal/grammar/generate.go create mode 100755 fhirpath/internal/grammar/generate.sh create mode 100644 fhirpath/internal/opts/opts.go create mode 100644 fhirpath/internal/parser/doc.go create mode 100644 fhirpath/internal/parser/error_handling.go create mode 100644 fhirpath/internal/parser/transforms.go create mode 100644 fhirpath/internal/parser/visitor.go create mode 100644 fhirpath/internal/reflection/consts.go create mode 100644 fhirpath/internal/reflection/doc.go create mode 100644 fhirpath/internal/reflection/elements.go create mode 100644 fhirpath/internal/reflection/type_specifier.go create mode 100644 fhirpath/internal/reflection/type_specifier_test.go create mode 100644 fhirpath/options.go create mode 100644 fhirpath/patch/doc.go create mode 100644 fhirpath/patch/patch.go create mode 100644 fhirpath/patch/patch_test.go create mode 100644 fhirpath/system/cmp.go create mode 100644 fhirpath/system/collection.go create mode 100644 fhirpath/system/collection_test.go create mode 100644 fhirpath/system/consts.go create mode 100644 fhirpath/system/date.go create mode 100644 fhirpath/system/date_test.go create mode 100644 fhirpath/system/date_time.go create mode 100644 fhirpath/system/date_time_test.go create mode 100644 fhirpath/system/doc.go create mode 100644 fhirpath/system/layouts.go create mode 100644 fhirpath/system/primitives.go create mode 100644 fhirpath/system/primitives_test.go create mode 100644 fhirpath/system/quantity.go create mode 100644 fhirpath/system/quantity_test.go create mode 100644 fhirpath/system/time.go create mode 100644 fhirpath/system/time_test.go create mode 100644 fhirpath/system/types.go create mode 100644 fhirpath/system/types_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gotchas.md create mode 100644 internal/bundle/bundle.go create mode 100644 internal/bundle/bundle_entry.go create mode 100644 internal/bundle/bundle_entry_example_test.go create mode 100644 internal/bundle/bundle_entry_test.go create mode 100644 internal/bundle/bundle_option.go create mode 100644 internal/bundle/bundle_test.go create mode 100644 internal/bundle/identity.go create mode 100644 internal/bundle/identity_test.go create mode 100644 internal/bundleopt/bundleopt.go create mode 100644 internal/containedresource/contained_resource.go create mode 100644 internal/containedresource/contained_resource_example_test.go create mode 100644 internal/containedresource/contained_resource_test.go create mode 100644 internal/element/canonical/canonical.go create mode 100644 internal/element/canonical/canonical_test.go create mode 100644 internal/element/codeableconcept/codeableconcept.go create mode 100644 internal/element/coding/coding.go create mode 100644 internal/element/element.go create mode 100644 internal/element/etag/etag.go create mode 100644 internal/element/etag/etag_test.go create mode 100644 internal/element/extension/extension.go create mode 100644 internal/element/extension/extension_example_test.go create mode 100644 internal/element/extension/extension_test.go create mode 100644 internal/element/extract.go create mode 100644 internal/element/extract_test.go create mode 100644 internal/element/identifier/docs.go create mode 100644 internal/element/identifier/identifier.go create mode 100644 internal/element/identifier/identifier_example_test.go create mode 100644 internal/element/identifier/identifier_test.go create mode 100644 internal/element/identifier/opts.go create mode 100644 internal/element/meta/meta.go create mode 100644 internal/element/meta/meta_example_test.go create mode 100644 internal/element/meta/meta_test.go create mode 100644 internal/element/reference/identity.go create mode 100644 internal/element/reference/identity_test.go create mode 100644 internal/element/reference/literal.go create mode 100644 internal/element/reference/literal_test.go create mode 100644 internal/element/reference/reference.go create mode 100644 internal/element/reference/reference_test.go create mode 100644 internal/fhir/constraints.go create mode 100644 internal/fhir/doc.go create mode 100644 internal/fhir/duration.go create mode 100644 internal/fhir/duration_test.go create mode 100644 internal/fhir/elements_general.go create mode 100644 internal/fhir/elements_general_test.go create mode 100644 internal/fhir/elements_metadata.go create mode 100644 internal/fhir/elements_primitive.go create mode 100644 internal/fhir/elements_primitive_test.go create mode 100644 internal/fhir/elements_special.go create mode 100644 internal/fhir/elements_special_test.go create mode 100644 internal/fhir/encoding.go create mode 100644 internal/fhir/encoding_test.go create mode 100644 internal/fhir/iface.go create mode 100644 internal/fhir/iface_test.go create mode 100644 internal/fhir/protofields.go create mode 100644 internal/fhir/time.go create mode 100644 internal/fhir/time_test.go create mode 100644 internal/fhirconv/doc.go create mode 100644 internal/fhirconv/integer.go create mode 100644 internal/fhirconv/integer_test.go create mode 100644 internal/fhirconv/string.go create mode 100644 internal/fhirconv/string_test.go create mode 100644 internal/fhirconv/time.go create mode 100644 internal/fhirconv/time_test.go create mode 100644 internal/fhirtest/doc.go create mode 100644 internal/fhirtest/elements.go create mode 100644 internal/fhirtest/meta.go create mode 100644 internal/fhirtest/random.go create mode 100644 internal/fhirtest/resources.go create mode 100644 internal/fhirtest/resources_example_test.go create mode 100644 internal/fhirtest/resources_test.go create mode 100644 internal/narrow/narrow.go create mode 100644 internal/narrow/narrow_example_test.go create mode 100644 internal/narrow/narrow_test.go create mode 100644 internal/protofields/descriptor.go create mode 100644 internal/protofields/dummies.go create mode 100644 internal/protofields/fields.go create mode 100644 internal/protofields/fields_test.go create mode 100644 internal/protofields/strcase.go create mode 100644 internal/protofields/update.go create mode 100644 internal/protofields/update_test.go create mode 100644 internal/resource/canonical_identity.go create mode 100644 internal/resource/canonical_identity_test.go create mode 100644 internal/resource/consts.go create mode 100644 internal/resource/identity.go create mode 100644 internal/resource/identity_test.go create mode 100644 internal/resource/options.go create mode 100644 internal/resource/options_test.go create mode 100644 internal/resource/patient/patient.go create mode 100644 internal/resource/patient/patient_test.go create mode 100644 internal/resource/resource.go create mode 100644 internal/resource/resource_example_test.go create mode 100644 internal/resource/resource_test.go create mode 100644 internal/resource/type.go create mode 100644 internal/resource/type_test.go create mode 100644 internal/resourceopt/resourceopt.go create mode 100644 internal/slices/slices.go create mode 100644 internal/slices/slices_example_test.go create mode 100644 internal/slices/slices_test.go create mode 100644 internal/stablerand/doc.go create mode 100644 internal/stablerand/rand.go create mode 100644 internal/units/doc.go create mode 100644 internal/units/time.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7a3abc2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Note: For syntax, see <https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax> + +* @verily-src/fhirpathgo-eng-reviewers diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..1a1a2b4 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,24 @@ +name: Build and Test +on: + pull_request: + branches: + - main + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + # Use the central Go version defined in go.mod to make it easier + # to perform upgrades. + go-version-file: go.mod + - name: Vet + run: go vet -v -unreachable=false ./... + - name: Build + run: go build -v ./... + - name: Test + run: go test ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..1db2431 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# FHIRPath + +This package contains a Go implementation of the [FHIRPath][fhirpath] specification, implemented directly with +the [google/fhir][google-fhir] proto definitions. + +This package aims to be compliant with both: + +- the [N1 Normative Release](http://hl7.org/fhirpath/N1/) specification, and +- the [R4 specifications](http://hl7.org/fhir/R4/fhirpath.html). + +## Import + +```go +import "github.com/verily-src/fhirpath-go/fhirpath" +``` + +## Usage + +A FHIRPath must be compiled before running it against a resource using the `Compile` method like so: + +```go +expression, err := fhirpath.Compile("Patient.name.given") +if err != nil { + panic("error while compiling FHIRPath") +} +``` + +The compilation result can then be run against a resource: + +```go +inputResources := []fhir.Resource{somePatient, someMedication} + +result, err := expression.Evaluate(inputResources) +if err != nil { + panic("error while running FHIRPath against resource") +} +``` + +As defined in the FHIRPath specification, the output of evaluation is a **Collection**. So, the +result of Evaluate is of type `[]any`. As such, the result must be unpacked and cast to the desired +type for further processing. + +### CompileOptions and EvaluateOptions + +Options are provided for optional modification of compilation and evaluation. There is currently +support for: + +- adding custom functions during Compile time +- adding custom external constant variables + +#### To add a custom function + +The constraints on the custom function are as follows: + +- First argument must be `system.Collection` +- Arguments that follow must be either a fhir proto type or primitive system type + +```go +customFn := func (input system.Collection, args ...any) (system.Collection error) { + fmt.Print("called custom fn") + return input, nil +} +expression, err := fhirpath.Compile("print()", WithFunction("print", customFn)) +``` + +#### To add external constants + +The constraints on external constants are as follows: + +- Must be a fhir proto type, primitive system type, or `system.Collection` +- If you pass in a collection, contained elements must be fhir proto or system type. + +```go +customVar := system.String("custom variable") +result, err := expression.Evaluate([]fhir.Resource{someResource}, WithConstant("var", customVar)) +``` + +### System Types + +The FHIRPath [spec](http://hl7.org/fhirpath/N1/#literals) defines the following custom System types: + +- Boolean +- String +- Integer +- Decimal +- Quantity +- Date +- Time +- DateTime + +FHIR Protos get implicitly converted to the above types according to this +[chart](http://hl7.org/fhir/R4/fhirpath.html#types), when used in some FHIRPath expressions. + +### Things to be aware of + +FHIRPath is not the most intuitive language, and there are some quirks. See [gotchas](gotchas.md). + +[fhirpath]: http://hl7.org/fhirpath/ +[google-fhir]: https://github.com/google/fhir diff --git a/fhirpath/.gitignore b/fhirpath/.gitignore new file mode 100644 index 0000000..0396a49 --- /dev/null +++ b/fhirpath/.gitignore @@ -0,0 +1,4 @@ +*antlr*.jar +*.interp +*.tokens +*fhirpath_base_visitor.go diff --git a/fhirpath/compopts/compopts.go b/fhirpath/compopts/compopts.go new file mode 100644 index 0000000..9b04db1 --- /dev/null +++ b/fhirpath/compopts/compopts.go @@ -0,0 +1,55 @@ +/* +Package compopts provides CompileOption values for FHIRPath. + +This package exists to isolate the options away from the core FHIRPath logic, +since this will simplify discovery of compile-specific options. +*/ +package compopts + +import ( + "errors" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" + "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" +) + +var ErrMultipleTransforms = errors.New("multiple transforms provided") + +// AddFunction creates a CompileOption that will register a custom FHIRPath +// function that can be called during evaluation with the given name. +// +// If the function already exists, then compilation will return an error. +func AddFunction(name string, fn any) opts.CompileOption { + return opts.Transform(func(cfg *opts.CompileConfig) error { + return cfg.Table.Register(name, fn) + }) +} + +// Transform creates a CompileOption that will set a transform +// to be called on each expression returned by the Visitor. +// +// If there is already a Transform set, then compilation will return an error. +func Transform(v parser.VisitorTransform) opts.CompileOption { + return opts.Transform(func(cfg *opts.CompileConfig) error { + if cfg.Transform != nil { + return ErrMultipleTransforms + } + cfg.Transform = v + return nil + }) +} + +// Permissive is an option that enables deprecated behavior in FHIRPath field +// navigation. This can be used as a temporary fix for FHIRpaths that have never +// been valid FHIRPaths, but have worked up until this point. +// +// This option is marked Deprecated so that it nags users until the paths can +// be resolved. +// +// Deprecated: Please update FHIRPaths whenever possible. +func Permissive() opts.CompileOption { + return opts.Transform(func(cfg *opts.CompileConfig) error { + cfg.Permissive = true + return nil + }) +} diff --git a/fhirpath/doc.go b/fhirpath/doc.go new file mode 100644 index 0000000..4e1405d --- /dev/null +++ b/fhirpath/doc.go @@ -0,0 +1,9 @@ +/* +Package fhirpath implements the FHIRPath specification. + +More documentation about the FHIRPath specification can be found on HL7 +websites: +* https://www.hl7.org/fhir/fhirpath.html +* http://hl7.org/fhirpath/N1/ +*/ +package fhirpath diff --git a/fhirpath/evalopts/evalopts.go b/fhirpath/evalopts/evalopts.go new file mode 100644 index 0000000..eff566a --- /dev/null +++ b/fhirpath/evalopts/evalopts.go @@ -0,0 +1,74 @@ +/* +Package evalopts provides EvaluateOption values for FHIRPath. + +This package exists to isolate the options away from the core FHIRPath logic, +since this will simplify discovery of evaluation-specific options. +*/ +package evalopts + +import ( + "errors" + "fmt" + "time" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +var ( + ErrUnsupportedType = errors.New("external constant type not supported") + ErrExistingConstant = errors.New("constant already exists") +) + +// OverrideTime returns an EvaluateOption that can be used to override the time +// that will be used in FHIRPath expressions. +func OverrideTime(t time.Time) opts.EvaluateOption { + return opts.Transform(func(cfg *opts.EvaluateConfig) error { + cfg.Context.Now = t + return nil + }) +} + +// EnvVariable returns an EvaluateOption that sets FHIRPath environment variables +// (e.g. %action). +// +// The input must be one of: +// - A FHIRPath System type, +// - A FHIR Element or Resource type, or +// - A FHIRPath Collection, containing the above types. +// +// If an EnvVariable is specified that already exists in the expression, then +// evaluation will yield an ErrExistingConstant error. If an EnvVariable is +// contains a type that is not one of the above valid types, then evaluation +// will yield an ErrUnsupportedType error. +func EnvVariable(name string, value any) opts.EvaluateOption { + return opts.Transform(func(cfg *opts.EvaluateConfig) error { + if err := validateType(value); err != nil { + return err + } + if _, ok := cfg.Context.ExternalConstants[name]; !ok { + cfg.Context.ExternalConstants[name] = value + return nil + } + return fmt.Errorf("%w: %s", ErrExistingConstant, name) + }) +} + +// validateType validates that the input type is a supported +// fhir proto or System type. If a system.Collection is passed in, +// recursively checks each element. +func validateType(input any) error { + var err error + switch v := input.(type) { + case fhir.Base, system.Any: + break + case system.Collection: + for _, elem := range v { + err = errors.Join(err, validateType(elem)) + } + default: + err = fmt.Errorf("%w: %T", ErrUnsupportedType, input) + } + return err +} diff --git a/fhirpath/fhirpath.go b/fhirpath/fhirpath.go new file mode 100644 index 0000000..395de89 --- /dev/null +++ b/fhirpath/fhirpath.go @@ -0,0 +1,118 @@ +package fhirpath + +import ( + "errors" + + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/evalopts" + "github.com/verily-src/fhirpath-go/fhirpath/internal/compile" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" + "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +var ( + ErrInvalidField = expr.ErrInvalidField + ErrUnsupportedType = evalopts.ErrUnsupportedType + ErrExistingConstant = evalopts.ErrExistingConstant +) + +// Expression is the FHIRPath expression that will be compiled from a FHIRPath string +type Expression struct { + expression expr.Expression + path string +} + +// Compile parses and compiles the FHIRPath expression down to a single +// Expression object. +// +// If there are any syntax or semantic errors, this will return an +// error indicating the compilation failure reason. +func Compile(expr string, options ...CompileOption) (*Expression, error) { + config, err := compile.PopulateConfig(options...) + if err != nil { + return nil, err + } + + tree, err := compile.Tree(expr) + if err != nil { + return nil, err + } + + visitor := &parser.FHIRPathVisitor{ + Functions: config.Table, + Permissive: config.Permissive, + } + vr, ok := visitor.Visit(tree).(*parser.VisitResult) + if !ok { + return nil, errors.New("input expression currently unsupported") + } + + if vr.Error != nil { + return nil, vr.Error + } + return &Expression{ + expression: vr.Result, + path: expr, + }, nil +} + +// String returns the string representation of this FHIRPath expression. +// This is just the input that initially produced the FHIRPath value. +func (e *Expression) String() string { + return e.path +} + +// MustCompile compiles the FHIRpath expression input, and returns the +// compiled expression. If any compilation error occurs, this function +// will panic. +func MustCompile(expr string, opts ...CompileOption) *Expression { + result, err := Compile(expr, opts...) + if err != nil { + panic(err) + } + return result +} + +// Evaluate the expression, returning either a collection of elements, or error +func (e *Expression) Evaluate(input []fhir.Resource, options ...EvaluateOption) (system.Collection, error) { + config := &opts.EvaluateConfig{ + Context: expr.InitializeContext(slices.MustConvert[any](input)), + } + config, err := opts.ApplyOptions(config, options...) + if err != nil { + return nil, err + } + + collection := slices.MustConvert[any](input) + return e.expression.Evaluate(config.Context, collection) +} + +// EvaluateAsString evaluates the expression, returning a string or error +func (e *Expression) EvaluateAsString(input []fhir.Resource, options ...EvaluateOption) (string, error) { + got, err := e.Evaluate(input, options...) + if err != nil { + return "", err + } + return got.ToString() +} + +// EvaluateAsBool evaluates the expression, returning either a boolean or error +func (e *Expression) EvaluateAsBool(input []fhir.Resource, options ...EvaluateOption) (bool, error) { + got, err := e.Evaluate(input, options...) + if err != nil { + return false, err + } + return got.ToBool() +} + +// EvaluateAsInt32 evaluates the expression, returning either an int32 or error +func (e *Expression) EvaluateAsInt32(input []fhir.Resource, options ...EvaluateOption) (int32, error) { + got, err := e.Evaluate(input, options...) + if err != nil { + return 0, err + } + return got.ToInt32() +} diff --git a/fhirpath/fhirpath_expression_test.go b/fhirpath/fhirpath_expression_test.go new file mode 100644 index 0000000..daed640 --- /dev/null +++ b/fhirpath/fhirpath_expression_test.go @@ -0,0 +1,292 @@ +package fhirpath_test + +import ( + "errors" + "math" + "testing" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath" + "github.com/verily-src/fhirpath-go/fhirpath/fhirpathtest" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestExpressionString(t *testing.T) { + const want = "Patient.name" + expr, err := fhirpath.Compile(want) + if err != nil { + t.Fatalf("Expression.String(): got unexpected err: %v", err) + } + + got := expr.String() + + if got != want { + t.Errorf("Expression.String(): got %v, want %v", got, want) + } +} + +func TestExpressionEvaluateAsBool_EvaluationError_ReturnsError(t *testing.T) { + want := errors.New("some error") + path := fhirpathtest.Error(want) + + _, err := path.EvaluateAsBool(nil) + + if got, want := err, want; !errors.Is(got, want) { + t.Errorf("EvaluateAsBool: want err %v, got %v", want, got) + } +} + +func TestExpressionEvaluateAsBool_NonConvertibleResult_ReturnsError(t *testing.T) { + testCases := []struct { + name string + input system.Collection + }{ + { + name: "Collection of more than 1", + input: system.Collection{system.String("1"), system.String("2")}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := fhirpathtest.ReturnCollection(tc.input) + + _, err := path.EvaluateAsBool(nil) + + if err == nil { + t.Fatalf("EvaluateAsBool: Expected error") + } + }) + } +} + +func TestExpressionEvaluateAsBoo_EmptyCollection_ReturnsFalse(t *testing.T) { + testCases := []struct { + name string + input system.Collection + }{ + { + name: "Empty Collection", + input: system.Collection{}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := fhirpathtest.ReturnCollection(tc.input) + + result, err := path.EvaluateAsBool(nil) + + if err != nil { + t.Fatalf("EvaluateAsBool: Expected no error") + } + + if result { + t.Fatalf("EvaluateAsBool: Expected false") + } + }) + } +} + +func TestExpressionEvaluateAsBool_ConvertibleResult_ReturnsBool(t *testing.T) { + testCases := []struct { + name string + input any + want bool + }{ + { + name: "system boolean", + input: system.Boolean(true), + want: true, + }, { + name: "FHIR boolean", + input: fhir.Boolean(true), + want: true, + }, { + name: "Random singleton type", + input: system.String("Hello world"), + want: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := fhirpathtest.Return(tc.input) + + got, err := path.EvaluateAsBool(nil) + if err != nil { + t.Fatalf("EvaluateAsBool: Unexpected error %v", err) + } + + if got != tc.want { + t.Errorf("EvaluateAsBool: want %v, got %v", tc.want, got) + } + }) + } +} + +func TestExpressionEvaluateAsString_EvaluationError_ReturnsError(t *testing.T) { + want := errors.New("some error") + path := fhirpathtest.Error(want) + + _, err := path.EvaluateAsString(nil) + + if got, want := err, want; !errors.Is(got, want) { + t.Errorf("EvaluateAsString: want err %v, got %v", want, got) + } +} + +func TestExpressionEvaluateAsString_NonConvertibleResult_ReturnsError(t *testing.T) { + testCases := []struct { + name string + input system.Collection + }{ + { + name: "Empty Collection", + input: system.Collection{}, + }, { + name: "Collection of more than 1", + input: system.Collection{system.String("1"), system.String("2")}, + }, { + name: "Invalid type", + input: system.Collection{fhir.Integer(42)}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := fhirpathtest.ReturnCollection(tc.input) + + _, err := path.EvaluateAsString(nil) + + if err == nil { + t.Fatalf("EvaluateAsString: Expected error") + } + }) + } +} + +func TestExpressionEvaluateAsString_ConvertibleResult_ReturnsString(t *testing.T) { + const str = "hello world" + testCases := []struct { + name string + input any + want string + }{ + { + name: "system String", + input: system.String(str), + want: str, + }, { + name: "FHIR String", + input: fhir.String(str), + want: str, + }, { + name: "FHIR Code", + input: fhir.Code(str), + want: str, + }, { + name: "FHIR ID", + input: fhir.ID(str), + want: str, + }, { + name: "FHIR Markdown", + input: fhir.Markdown(str), + want: str, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := fhirpathtest.Return(tc.input) + + got, err := path.EvaluateAsString(nil) + if err != nil { + t.Fatalf("EvaluateAsString: Unexpected error %v", err) + } + + if got != tc.want { + t.Errorf("EvaluateAsString: want %v, got %v", tc.want, got) + } + }) + } +} + +func TestExpressionEvaluateAsInt32_EvaluationError_ReturnsError(t *testing.T) { + want := errors.New("some error") + path := fhirpathtest.Error(want) + + _, err := path.EvaluateAsInt32(nil) + + if got, want := err, want; !errors.Is(got, want) { + t.Errorf("EvaluateAsInt32: want err %v, got %v", want, got) + } +} + +func TestExpressionEvaluateAsInt32_NonConvertibleResult_ReturnsError(t *testing.T) { + testCases := []struct { + name string + input system.Collection + }{ + { + name: "Empty Collection", + input: system.Collection{}, + }, { + name: "Collection of more than 1", + input: system.Collection{system.Integer(1), system.Integer(2)}, + }, { + name: "Integer not representable", + input: system.Collection{fhir.UnsignedInt(math.MaxUint32)}, + }, { + name: "Invalid Type", + input: system.Collection{fhir.String("hello world")}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := fhirpathtest.ReturnCollection(tc.input) + + _, err := path.EvaluateAsInt32(nil) + + if err == nil { + t.Fatalf("EvaluateAsInt32: Expected error") + } + }) + } +} + +func TestExpressionEvaluateAsInt32_ConvertibleResult_ReturnsInt32(t *testing.T) { + const val = 42 + testCases := []struct { + name string + input any + want int32 + }{ + { + name: "system Integer", + input: system.Integer(val), + want: val, + }, { + name: "FHIR Integer", + input: fhir.Integer(val), + want: val, + }, { + name: "FHIR PositiveInt", + input: fhir.PositiveInt(val), + want: val, + }, { + name: "FHIR ID", + input: fhir.UnsignedInt(val), + want: val, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := fhirpathtest.Return(tc.input) + + got, err := path.EvaluateAsInt32(nil) + if err != nil { + t.Fatalf("EvaluateAsInt32: Unexpected error %v", err) + } + + if got != tc.want { + t.Errorf("EvaluateAsInt32: want %v, got %v", tc.want, got) + } + }) + } +} diff --git a/fhirpath/fhirpath_test.go b/fhirpath/fhirpath_test.go new file mode 100644 index 0000000..9734b3e --- /dev/null +++ b/fhirpath/fhirpath_test.go @@ -0,0 +1,1413 @@ +package fhirpath_test + +import ( + "errors" + "testing" + "time" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + drpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/document_reference_go_proto" + epb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/encounter_go_proto" + lpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/list_go_proto" + mrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_request_go_proto" + opb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + prpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/practitioner_go_proto" + tpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/task_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element/extension" + "github.com/verily-src/fhirpath-go/internal/element/reference" + "github.com/verily-src/fhirpath-go/internal/fhirconv" + "github.com/verily-src/fhirpath-go/fhirpath" + "github.com/verily-src/fhirpath-go/fhirpath/compopts" + "github.com/verily-src/fhirpath-go/fhirpath/evalopts" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/testing/protocmp" +) + +type evaluateTestCase struct { + name string + inputPath string + inputCollection []fhir.Resource + wantCollection system.Collection + compileOptions []fhirpath.CompileOption + evaluateOptions []fhirpath.EvaluateOption +} + +var patientChu = &ppb.Patient{ + Id: fhir.ID("123"), + Active: fhir.Boolean(true), + Gender: &ppb.Patient_GenderCode{ + Value: cpb.AdministrativeGenderCode_FEMALE, + }, + BirthDate: fhir.MustParseDate("2000-03-22"), + Telecom: []*dtpb.ContactPoint{ + { + System: &dtpb.ContactPoint_SystemCode{Value: cpb.ContactPointSystemCode_PHONE}, + }, + }, + Name: []*dtpb.HumanName{ + { + Use: &dtpb.HumanName_UseCode{ + Value: cpb.NameUseCode_NICKNAME, + }, + Given: []*dtpb.String{fhir.String("Senpai")}, + Family: fhir.String("Chu"), + }, + { + Use: &dtpb.HumanName_UseCode{ + Value: cpb.NameUseCode_OFFICIAL, + }, + Given: []*dtpb.String{fhir.String("Kang")}, + Family: fhir.String("Chu"), + }, + }, + Contact: []*ppb.Patient_Contact{ + { + Name: &dtpb.HumanName{ + Given: []*dtpb.String{fhir.String("Senpai")}, + Family: fhir.String("Rodusek"), + }, + }, + }, +} +var fooExtension, _ = extension.FromElement("foourl", fhir.String("foovalue")) +var barExtension, _ = extension.FromElement("barurl", fhir.String("barvalue")) +var nameVoldemort = &dtpb.HumanName{ + Given: []*dtpb.String{ + fhir.String("Lord"), + }, + Family: fhir.String("Voldemort"), +} +var patientVoldemort = &ppb.Patient{ + Id: fhir.ID("123"), + Active: fhir.Boolean(true), + Gender: &ppb.Patient_GenderCode{ + Value: cpb.AdministrativeGenderCode_FEMALE, + }, + Deceased: &ppb.Patient_DeceasedX{ + Choice: &ppb.Patient_DeceasedX_Boolean{ + Boolean: fhir.Boolean(true), + }, + }, + MultipleBirth: &ppb.Patient_MultipleBirthX{ + Choice: &ppb.Patient_MultipleBirthX_Integer{ + Integer: fhir.Integer(int32(2)), + }, + }, + Meta: &dtpb.Meta{ + Tag: []*dtpb.Coding{ + { + Code: fhir.Code("#blessed"), + }, + }, + }, + Name: []*dtpb.HumanName{nameVoldemort}, + Extension: []*dtpb.Extension{ + fooExtension, + barExtension, + }, +} +var docRef = &drpb.DocumentReference{ + Status: &drpb.DocumentReference_StatusCode{ + Value: cpb.DocumentReferenceStatusCode_CURRENT, + }, + Content: []*drpb.DocumentReference_Content{ + { + Attachment: &dtpb.Attachment{ + ContentType: &dtpb.Attachment_ContentTypeCode{ + Value: "image", + }, + Url: fhir.URL("http://image"), + Title: fhir.String("title"), + }, + }, + }, +} +var questionnaireRef, _ = reference.Typed("Questionnaire", "1234") +var obsWithRef = &opb.Observation{ + Meta: &dtpb.Meta{ + Extension: []*dtpb.Extension{ + { + Url: fhir.URI("https://extension"), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_Reference{ + Reference: questionnaireRef, + }, + }, + }, + }, + }, + DerivedFrom: []*dtpb.Reference{ + questionnaireRef, + }, +} +var listWithNilRef = &lpb.List{ + Entry: []*lpb.List_Entry{ + {Item: &dtpb.Reference{Type: fhir.URI("Location")}}, + }, +} + +func testEvaluate(t *testing.T, testCases []evaluateTestCase) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + compiledExpression, err := fhirpath.Compile(tc.inputPath, tc.compileOptions...) + if err != nil { + t.Fatalf("Compiling \"%s\" returned unexpected error: %v", tc.inputPath, err) + } + + got, err := compiledExpression.Evaluate(tc.inputCollection, tc.evaluateOptions...) + + if err != nil { + t.Fatalf("Evaluating \"%s\" returned unexpected error: %v", tc.inputPath, err) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("Evaluating \"%s\" returned unexpected diff (-want, +got)\n%s", tc.inputPath, diff) + } + }) + } +} + +func TestEvaluate_PathSelection_ReturnsError(t *testing.T) { + end := system.MustParseDateTime("@2016-01-01T12:22:33Z") + task := makeTaskWithEndTime(end) + + testCases := []struct { + name string + path string + input fhir.Resource + wantErr error + }{ + { + name: "Invalid value_us field on DateTime", + path: "(Task.input.value as DataRequirement).dateFilter[0].value.end.value_us", + input: task, + wantErr: fhirpath.ErrInvalidField, + }, { + name: "Invalid timezone field on DateTime", + path: "(Task.input.value as DataRequirement).dateFilter[0].value.end.timezone", + input: task, + wantErr: fhirpath.ErrInvalidField, + }, { + name: "Field is not in correct casing, but exists", + path: "Patient.multiple_birth", + input: patientVoldemort, + wantErr: fhirpath.ErrInvalidField, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sut, err := fhirpath.Compile(tc.path) + if err != nil { + t.Fatalf("fhirpath.Compile(%v): unexpected err: %v", tc.name, err) + } + + _, err = sut.Evaluate([]fhir.Resource{tc.input}) + + if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("fhirpath.Compile(%v): want err '%v', got '%v'", tc.name, got, want) + } + }) + } +} + +func TestEvaluate_PathSelection_ReturnsResult(t *testing.T) { + practitioner := &prpb.Practitioner{ + Name: []*dtpb.HumanName{nameVoldemort}, + } + end := system.MustParseDateTime("@2016-01-01T12:22:33Z") + task := makeTaskWithEndTime(end) + + testCases := []evaluateTestCase{ + { + name: "Patient.name.given returns given name", + inputPath: "Patient.name.given", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{fhir.String("Lord")}, + }, + { + name: "Patient.name returns name", + inputPath: "Patient.name", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{nameVoldemort}, + }, + { + name: "Extension with resource type returns extensions", + inputPath: "Patient.extension", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{fooExtension, barExtension}, + }, + { + name: "Extension without resource type returns extensions", + inputPath: "extension", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{fooExtension, barExtension}, + }, + { + name: "Patient.name returns empty on non-patient resource", + inputPath: "Patient.name", + inputCollection: []fhir.Resource{practitioner}, + wantCollection: system.Collection{}, + }, + { + name: "Accessing code field returns code", + inputPath: "Patient.gender", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{patientVoldemort.Gender}, + }, + { + name: "converts value field of primitive to System primitive", + inputPath: "Patient.name.given.value", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{system.String("Lord")}, + }, + { + name: "returns empty on non-existent field", + inputPath: "Patient.language", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{}, + }, + { + name: "returns value from a field with the _value suffix", + inputPath: "Encounter.class", + inputCollection: []fhir.Resource{&epb.Encounter{ClassValue: fhir.Coding("class-system", "class-code")}}, + wantCollection: system.Collection{fhir.Coding("class-system", "class-code")}, + }, + { + name: "value as Quantity returns fhir Quantity datatype", + inputPath: "Observation.value as Quantity", + inputCollection: []fhir.Resource{ + &opb.Observation{ + Value: &opb.Observation_ValueX{ + Choice: &opb.Observation_ValueX_Quantity{ + Quantity: &dtpb.Quantity{ + Value: fhir.Decimal(float64(22.2)), + }, + }, + }, + }, + }, + wantCollection: []any{ + &dtpb.Quantity{ + Value: fhir.Decimal(float64(22.2)), + }, + }, + }, + { + name: "Quantity with addition returns system.Quantity", + inputPath: "Observation.value as Quantity + 2", + inputCollection: []fhir.Resource{ + &opb.Observation{ + Value: &opb.Observation_ValueX{ + Choice: &opb.Observation_ValueX_Quantity{ + Quantity: &dtpb.Quantity{ + Value: fhir.Decimal(float64(22.2)), + }, + }, + }, + }, + }, + wantCollection: []any{system.MustParseQuantity("24.2", "")}, + }, + { + name: "reference field returns Type/ID", + inputPath: "Observation.derivedFrom[0].reference", + inputCollection: []fhir.Resource{obsWithRef}, + wantCollection: system.Collection{fhir.String("Questionnaire/1234")}, + }, + { + name: "reference extension field returns Type/ID", + inputPath: "Observation.meta.extension('https://extension').value.reference", + inputCollection: []fhir.Resource{obsWithRef}, + wantCollection: system.Collection{fhir.String("Questionnaire/1234")}, + }, + { + name: "nil reference does not panic", + inputPath: "List.entry.item.where(type = 'Location').reference", + inputCollection: []fhir.Resource{listWithNilRef}, + wantCollection: system.Collection{}, + }, + { + name: "Valid access of time field", + inputPath: "(Task.input.value as DataRequirement).dateFilter[0].value.end.value", + inputCollection: []fhir.Resource{task}, + wantCollection: system.Collection{system.String(fhirconv.DateTimeToString(end.ToProtoDateTime()))}, + }, + } + testEvaluate(t, testCases) +} + +func makeTaskWithEndTime(end system.DateTime) *tpb.Task { + start := system.MustParseDateTime("@2016-01-01T12:00:00Z") + task := &tpb.Task{ + Input: []*tpb.Task_Parameter{ + { + Value: &tpb.Task_Parameter_ValueX{ + Choice: &tpb.Task_Parameter_ValueX_DataRequirement{ + DataRequirement: &dtpb.DataRequirement{ + DateFilter: []*dtpb.DataRequirement_DateFilter{ + { + Value: &dtpb.DataRequirement_DateFilter_ValueX{ + Choice: &dtpb.DataRequirement_DateFilter_ValueX_Period{ + Period: &dtpb.Period{ + Start: start.ToProtoDateTime(), + End: end.ToProtoDateTime(), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + return task +} + +func TestEvaluate_LegacyPathSelection_ReturnsResult(t *testing.T) { + compileOptions := []fhirpath.CompileOption{compopts.Permissive()} + end := system.MustParseDateTime("@2016-01-01T12:22:33Z") + task := makeTaskWithEndTime(end) + + testCases := []evaluateTestCase{ + { + name: "Legacy: Evaluates ValueX fields and value_us fields", + inputPath: "(Task.input.value as DataRequirement).dateFilter[0].value.period.end.value_us", + inputCollection: []fhir.Resource{task}, + wantCollection: system.Collection{end}, + compileOptions: compileOptions, + }, + } + testEvaluate(t, testCases) +} + +func TestEvaluate_Literal_ReturnsLiteral(t *testing.T) { + decimal := system.Decimal(decimal.NewFromFloat(1.450)) + date, _ := system.ParseDate("2023-05-30") + time, _ := system.ParseTime("08:30:55.999") + dateTime, _ := system.ParseDateTime("2023-06-14T13:48:55.555Z") + quantity, _ := system.ParseQuantity("20", "years") + + testCases := []evaluateTestCase{ + { + name: "null literal returns empty collection", + inputPath: "{}", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{}, + }, + { + name: "boolean literal returns Boolean", + inputPath: "true", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "string literal returns escaped string", + inputPath: "'string test\\ 1\\''", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.String("string test 1'")}, + }, + { + name: "integer literal returns Integer", + inputPath: "23", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Integer(23)}, + }, + { + name: "decimal literal returns Decimal", + inputPath: "1.450", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{decimal}, + }, + { + name: "date literal returns Date", + inputPath: "@2023-05-30", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{date}, + }, + { + name: "time literal returns Time", + inputPath: "@T08:30:55.999", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{time}, + }, + { + name: "dateTime literal returns DateTime", + inputPath: "@2023-06-14T13:48:55.555Z", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{dateTime}, + }, + { + name: "quantity literal returns Quantity", + inputPath: "20 years", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{quantity}, + }, + } + + testEvaluate(t, testCases) +} + +func TestEvaluate_ThisInvocation_Evaluates(t *testing.T) { + testCases := []evaluateTestCase{ + { + name: "returns nickname with where()", + inputPath: "Patient.name.given.where($this = 'Senpai')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{dtpb.String{Value: "Senpai"}}, + }, + } + + testEvaluate(t, testCases) +} + +func TestEvaluate_Index_ReturnsIndex(t *testing.T) { + nameOne := &dtpb.HumanName{ + Given: []*dtpb.String{ + fhir.String("Kobe"), + fhir.String("Bean"), + }, + Family: fhir.String("Bryant"), + } + nameTwo := &dtpb.HumanName{ + Given: []*dtpb.String{ + fhir.String("The"), + }, + Family: fhir.String("Goat"), + } + patient := &ppb.Patient{ + Name: []*dtpb.HumanName{ + nameOne, + nameTwo, + }, + } + + testCases := []evaluateTestCase{ + { + name: "first index returns result", + inputPath: "Patient.name[0]", + inputCollection: []fhir.Resource{patient}, + wantCollection: system.Collection{nameOne}, + }, + { + name: "second index returns result", + inputPath: "Patient.name[1]", + inputCollection: []fhir.Resource{patient}, + wantCollection: system.Collection{nameTwo}, + }, + { + name: "indexing name.given", + inputPath: "Patient.name.given[2]", + inputCollection: []fhir.Resource{patient}, + wantCollection: system.Collection{fhir.String("The")}, + }, + { + name: "indexing multiple times", + inputPath: "Patient.name[0].given[1]", + inputCollection: []fhir.Resource{patient}, + wantCollection: system.Collection{fhir.String("Bean")}, + }, + { + name: "out of bounds index returns empty", + inputPath: "Patient.name.given[5]", + inputCollection: []fhir.Resource{patient}, + wantCollection: system.Collection{}, + }, + { + name: "empty collection index returns empty", + inputPath: "Patient.name.given[{}]", + inputCollection: []fhir.Resource{patient}, + wantCollection: system.Collection{}, + }, + } + + testEvaluate(t, testCases) +} + +func TestEvaluateEquality_ReturnsBoolean(t *testing.T) { + request := &mrpb.MedicationRequest{ + Intent: &mrpb.MedicationRequest_IntentCode{Value: cpb.MedicationRequestIntentCode_FILLER_ORDER}, + } + + testCases := []evaluateTestCase{ + { + name: "querying active field", + inputPath: "Patient.active = true", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "two contrary conditions on 2 resources with an OR, first one true", + inputPath: "Patient.active = true or Observation.status = 'final'", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "two contrary conditions on 2 resources with an OR, second one true", + inputPath: "Observation.status = 'final' or Patient.active = true", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "inverse of active field", + inputPath: "Patient.active != true", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "querying given name", + inputPath: "Patient.name[0].given = 'Senpai'", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "equality of complex types", + inputPath: "Patient.name[0].given = Patient.contact.name.given", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "complex types not equal", + inputPath: "Patient.name.family != Patient.contact.name.family", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing non-equal fields", + inputPath: "Patient.name.family = Patient.contact.name.family", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "comparing non-existent field", + inputPath: "Patient.maritalStatus = false", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{}, + }, + { + name: "comparing dates", + inputPath: "Patient.birthDate = @2000-03-22", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "compare date with dateTime", + inputPath: "@2012-12-31 = @2012-12-31T", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "compare dateTime with date", + inputPath: "@2012-12-31T = @2012-12-31", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing non-equal dates", + inputPath: "@2000-01-02 != @2000-01-01", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing mismatched date precision", + inputPath: "@2000-01 = @2000-01-03", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{}, + }, + { + name: "comparing mismatched date precision that isn't equal", + inputPath: "@2000-02 = @2000-01-03", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "respects timezones for DateTime comparison", + inputPath: "@2000-02-01T12:30:00Z = @2000-02-01T13:30:00+01:00", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing gender code", + inputPath: "Patient.gender = 'female'", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing code with non-enum value", + inputPath: "DocumentReference.content[0].attachment.contentType = 'image'", + inputCollection: []fhir.Resource{docRef}, + wantCollection: []any{system.Boolean(true)}, + }, + { + name: "comparing name use code", + inputPath: "Patient.name[0].use = 'nickname'", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing telecom system code", + inputPath: "telecom.system = 'phone'", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing incorrect telecom code", + inputPath: "telecom.system = 'carrier pigeon'", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "mismatched case on code", + inputPath: "Patient.name.use = 'NICKNAME'", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "comparing multi-word code", + inputPath: "MedicationRequest.intent = 'filler-order'", + inputCollection: []fhir.Resource{request}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "mismatched case for multi-word code", + inputPath: "MedicationRequest.intent = 'fillerOrder'", + inputCollection: []fhir.Resource{request}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "comparing decimal to integer", + inputPath: "1 = 1.000", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing decimal to quantity", + inputPath: "24.3 = 24.3 'kg'", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "comparing integer to quantity", + inputPath: "2 = 2.0 'lbs'", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + } + + testEvaluate(t, testCases) +} + +func TestParenthesizedExpression_MaintainsPrecedence(t *testing.T) { + patient := &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{ + fhir.String("Alex"), + fhir.String("Jon"), + fhir.String("Matt"), + fhir.String("Heming"), + }, + }, + }, + } + testCases := []evaluateTestCase{ + { + name: "evaluates parenthesized equality first", + inputPath: "true = (false = false)", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "evaluates parenthesized expressions in order", + inputPath: "true = ('Alex' = (name.given[0]))", + inputCollection: []fhir.Resource{patient}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + } + + testEvaluate(t, testCases) +} + +func TestFunctionInvocation_Evaluates(t *testing.T) { + testTime := time.Now() + testDateTime, _ := system.DateTimeFromProto(fhir.DateTime(testTime)) + testCases := []evaluateTestCase{ + { + name: "returns nickname with where()", + inputPath: "Patient.name.where(use = 'nickname')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{patientChu.Name[0]}, + }, + { + name: "returns official name with where()", + inputPath: "Patient.name.where(use = 'official')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{patientChu.Name[1]}, + }, + { + name: "returns true with exists()", + inputPath: "Patient.name.exists(use = 'official')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false with exists()", + inputPath: "Patient.name.exists(use = 'random-use')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true with exists() with BooleanExpression", + inputPath: "Patient.name.exists(use = 'official' and given = 'Kang')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns true with exists() with BooleanExpression", + inputPath: "Patient.name.exists(use = 'random-use' or given = 'Kang')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns true with where() and exists()", + inputPath: "Patient.name.where(use = 'official').exists()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "chaining exists() is fine when the first exists() evaluates to true", + inputPath: "Patient.name.where(use = 'official').exists().exists().exists()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "chaining exists() when the first exists() evaluates to false gives correct but ambiguous result", + inputPath: "Patient.name.where(use = 'random').exists().exists()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "chaining empty() gives correct but ambiguous result", + inputPath: "Patient.name.where(use = 'random').empty().empty()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "returns false with where() and empty()", + inputPath: "Patient.name.where(use = 'official').empty()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true with where() and empty()", + inputPath: "Patient.name.where(use = 'random').empty()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns empty if no elements match where condition", + inputPath: "Patient.name.where(family = 'Suresh')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{}, + }, + { + name: "evaluates timeOfDay() based on context, not dependent on latent factors", + inputPath: "timeOfDay().delay() = timeOfDay()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + compileOptions: []fhirpath.CompileOption{compopts.AddFunction("delay", func(in system.Collection) (system.Collection, error) { + time.Sleep(time.Second * 2) + return in, nil + })}, + }, + { + name: "evaluates now() based on context, not dependent on latent factors", + inputPath: "now().delay() = now()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + compileOptions: []fhirpath.CompileOption{compopts.AddFunction("delay", func(in system.Collection) (system.Collection, error) { + time.Sleep(time.Second * 2) + return in, nil + })}, + }, + { + name: "evaluates today() based on context, not dependent on latent factors", + inputPath: "today().delay() = today()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + compileOptions: []fhirpath.CompileOption{compopts.AddFunction("delay", func(in system.Collection) (system.Collection, error) { + time.Sleep(time.Second * 2) + return in, nil + })}, + }, + { + name: "evaluates now() using overridden time", + inputPath: "now()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{testDateTime}, + evaluateOptions: []fhirpath.EvaluateOption{evalopts.OverrideTime(testTime)}, + }, + { + name: "evaluate with custom function 'patient()'", + inputPath: "patient() = Patient", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + compileOptions: []fhirpath.CompileOption{compopts.AddFunction("patient", func(system.Collection) (system.Collection, error) { + return system.Collection{patientChu}, nil + })}, + }, + { + name: "evaluate with custom function startsWith()", + inputPath: "Patient.name[0].family.startsWith('Ch')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "evaluate with custom function endsWith()", + inputPath: "Patient.name[0].family.endsWith('hu')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "evaluate with custom function length()", + inputPath: "Patient.name[0].family.length()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Integer(3)}, + }, + { + name: "evaluate with custom function upper()", + inputPath: "Patient.name[0].given.upper()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.String("SENPAI")}, + }, + { + name: "evaluate with custom function lower()", + inputPath: "Patient.name[0].family.lower()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.String("chu")}, + }, + { + name: "evaluate with custom function contains()", + inputPath: "Patient.name[0].given.contains('pai')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "evaluate with custom function toChars()", + inputPath: "Patient.name[0].family.toChars()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{ + system.String('C'), + system.String('h'), + system.String('u'), + }, + }, + { + name: "evaluate with custom function substring()", + inputPath: "Patient.name[0].given.substring(1, 4)", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.String("enpa")}, + }, + { + name: "evaluate with custom function indexOf()", + inputPath: "Patient.name[0].given.indexOf('pa')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Integer(3)}, + }, + { + name: "evaluate with custom function matches()", + inputPath: "Patient.name[0].family.matches('^[A-Za-z]*$')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "evaluate with custom function replace()", + inputPath: "Patient.name[0].given.replace('Senpai', 'Oppa')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.String("Oppa")}, + }, + { + name: "evaluate with custom function replaceMatches()", + inputPath: "Patient.name[0].family.replaceMatches('[A-Z]', 'zzz')", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.String("zzzhu")}, + }, + } + + testEvaluate(t, testCases) +} + +func TestTypeExpression_Evaluates(t *testing.T) { + testCases := []evaluateTestCase{ + { + name: "returns true for resource type check", + inputPath: "Patient is Patient", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns true for resource subtype relationship", + inputPath: "Patient is FHIR.Resource", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns true for primitive type check", + inputPath: "Patient.deceased is boolean", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false for primitive type case mismatch", + inputPath: "Patient.deceased is Boolean", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true for system type check", + inputPath: "1 is Integer", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "propagates empty collection", + inputPath: "{} is Boolean", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{}, + }, + { + name: "passes through as expression", + inputPath: "Patient as Patient", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{patientChu}, + }, + { + name: "passes through as expression for subtype relationship", + inputPath: "Patient.name.use[0] as FHIR.Element", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{patientChu.Name[0].Use}, + }, + { + name: "returns empty if as expression is not correct type", + inputPath: "Patient.name.family[0] as HumanName", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{}, + }, + { + name: "unwraps polymorphic type with as expression", + inputPath: "Patient.deceased as boolean", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{fhir.Boolean(true)}, + }, + { + name: "passes through system type with as expression", + inputPath: "@2000-12-05 as Date", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.MustParseDate("2000-12-05")}, + }, + } + + testEvaluate(t, testCases) +} + +func TestBooleanExpression_Evaluates(t *testing.T) { + testCases := []evaluateTestCase{ + { + name: "evaluates and correctly", + inputPath: "true and false", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "evaluates boolean correctly with protos", + inputPath: "Patient.active and Patient.deceased", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "evaluates or correctly", + inputPath: "true or false", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "propogates empty collections correctly", + inputPath: "false or {}", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{}, + }, + { + name: "evaluates xor correctly", + inputPath: "true xor true", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "evaluates implies correctly", + inputPath: "false implies false", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "not function inverts input", + inputPath: "Patient.active.not()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + } + + testEvaluate(t, testCases) +} + +func TestComparisonExpression_ReturnsBool(t *testing.T) { + testCases := []evaluateTestCase{ + { + name: "compares strings", + inputPath: "'abc' > 'ABC'", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "compares integer with decimal", + inputPath: "4 <= 4.0", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "compares quantities of the same precision", + inputPath: "3.2 'kg' > 9.7 'kg'", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "returns empty for quantities of different precision", + inputPath: "99.9 'cm' < 1 'm'", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{}, + }, + { + name: "compares dates correctly", + inputPath: "@2018-03-01 >= @2018-03-01", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns empty for mismatched time precision", + inputPath: "@T08:30 > @T08:30:00", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{}, + }, + { + name: "correctly compares times", + inputPath: "@T10:29:59.999 < @T10:30:00", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "validate the age of an individual", + inputPath: "Patient.birthDate + 23 'years' <= today()", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + } + + testEvaluate(t, testCases) +} + +func TestArithmetic_ReturnsResult(t *testing.T) { + testCases := []evaluateTestCase{ + { + name: "adds dates with quantity", + inputPath: "@2012-12-12 + 12 days", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.MustParseDate("2012-12-24")}, + }, + { + name: "concatenates strings", + inputPath: "'hello ' & 'world'", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.String("hello world")}, + }, + { + name: "subtracts integer from quantity", + inputPath: "8 'kg' - 4", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.MustParseQuantity("4", "kg")}, + }, + { + name: "multiplies values together", + inputPath: "8 * 4.2", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Decimal(decimal.NewFromFloat(33.6))}, + }, + { + name: "divides values", + inputPath: "8 / 2.5", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Decimal(decimal.NewFromFloat(3.2))}, + }, + { + name: "performs floor division", + inputPath: "29 div 10", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Integer(2)}, + }, + { + name: "performs modulo operation", + inputPath: "100 mod 11", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Integer(1)}, + }, + } + + testEvaluate(t, testCases) +} + +func TestCompile_ReturnsError(t *testing.T) { + testCases := []struct { + name string + inputPath string + compileOptions []fhirpath.CompileOption + }{ + { + name: "mismatched parentheses", + inputPath: "Patient.name.where(use = official", + }, + { + name: "invalid character", + inputPath: "Patient.*name", + }, + { + name: "invalid expression (misspelling)", + inputPath: "Patient.name aand Patient.name", + }, + { + name: "invalid expression (non-existent operator)", + inputPath: "Patient.name nor Patient.name", + }, + { + name: "invalid character (lexer error)", + inputPath: "Patient^", + }, + { + name: "non-existent function", + inputPath: "Patient.notAFunc()", + }, + { + name: "expanding function table with bad function", + inputPath: "Patient.badFn()", + compileOptions: []fhirpath.CompileOption{compopts.AddFunction("badFn", func() {})}, + }, + { + name: "attempting to override existing function", + inputPath: "Patient.where()", + compileOptions: []fhirpath.CompileOption{compopts.AddFunction("where", func(system.Collection) (system.Collection, error) { return nil, nil })}, + }, + { + name: "evaluating function with mismatched arity", + inputPath: "Patient.name.where(use = 'official', use = 'nickname')", + }, + { + name: "evaluating function with invalid arguments", + inputPath: "Patient.name.where(invalid $ expr)", + }, + { + name: "resolving invalid type specifier", + inputPath: "1 is System.Patient", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := fhirpath.Compile(tc.inputPath, tc.compileOptions...); err == nil { + t.Errorf("Compiling \"%s\" doesn't raise error when expected to", tc.inputPath) + } + }) + } +} + +func TestEvaluate_ReturnsError(t *testing.T) { + alwaysFails := func(system.Collection) (system.Collection, error) { + return nil, errors.New("some error") + } + testCases := []struct { + name string + inputPath string + inputCollection []fhir.Resource + compileOptions []fhirpath.CompileOption + evaluateOptions []fhirpath.EvaluateOption + }{ + { + name: "non-integer index returns error", + inputPath: "Patient.name['not a number']", + inputCollection: []fhir.Resource{patientChu}, + }, + { + name: "evaluating failing function propagates error", + inputPath: "alwaysFails()", + inputCollection: []fhir.Resource{}, + compileOptions: []fhirpath.CompileOption{compopts.AddFunction("alwaysFails", alwaysFails)}, + }, + { + name: "evaluating is expression on non-singleton collection", + inputPath: "Patient.name is string", + inputCollection: []fhir.Resource{patientChu}, + }, + { + name: "comparing unsupported types", + inputPath: "true > 0", + inputCollection: []fhir.Resource{}, + }, + { + name: "arithmetic on unsupported types", + inputPath: "1 + true", + inputCollection: []fhir.Resource{}, + }, + { + name: "misspelled identifier raises error", + inputPath: "Patient.nam.given", + inputCollection: []fhir.Resource{patientVoldemort}, + }, + { + name: "overriding existing constant", + inputPath: "'valid fhirpath'", + inputCollection: []fhir.Resource{}, + evaluateOptions: []fhirpath.EvaluateOption{ + evalopts.EnvVariable("context", system.String("context")), + }, + }, + { + name: "adding unsupported type as constant", + inputPath: "%var", + inputCollection: []fhir.Resource{}, + evaluateOptions: []fhirpath.EvaluateOption{ + evalopts.EnvVariable("var", 1), + }, + }, + { + name: "adding unsupported type within collection as constant", + inputPath: "%collection", + inputCollection: []fhir.Resource{}, + evaluateOptions: []fhirpath.EvaluateOption{ + evalopts.EnvVariable("collection", system.Collection{system.Integer(1), 1}), + }, + }, + { + name: "negating unsupported type", + inputPath: "-'string'", + inputCollection: []fhir.Resource{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expression, err := fhirpath.Compile(tc.inputPath, tc.compileOptions...) + if err != nil { + t.Fatalf("compiling \"%s\" raised unexpected error: %v", tc.inputPath, err) + } + if _, err = expression.Evaluate(tc.inputCollection, tc.evaluateOptions...); err == nil { + t.Errorf("Evaluating expression \"%s\" doesn't raise error when expected to", tc.inputPath) + } + }) + } +} + +func TestExternalConstantExpression_ReturnsConstant(t *testing.T) { + testCases := []evaluateTestCase{ + { + name: "system type constant", + inputPath: "%var", + inputCollection: []fhir.Resource{}, + evaluateOptions: []fhirpath.EvaluateOption{ + evalopts.EnvVariable("var", system.String("hello")), + }, + wantCollection: system.Collection{system.String("hello")}, + }, + { + name: "proto type constant", + inputPath: "%patient", + inputCollection: []fhir.Resource{}, + evaluateOptions: []fhirpath.EvaluateOption{ + evalopts.EnvVariable("patient", patientChu), + }, + wantCollection: system.Collection{patientChu}, + }, + { + name: "collection constant containing system and proto types", + inputPath: "%collection", + inputCollection: []fhir.Resource{}, + evaluateOptions: []fhirpath.EvaluateOption{ + evalopts.EnvVariable("collection", system.Collection{system.String("hello"), patientChu}), + }, + wantCollection: system.Collection{system.String("hello"), patientChu}, + }, + { + name: "returns input as %context variable", + inputPath: "%context", + inputCollection: []fhir.Resource{patientChu}, + wantCollection: system.Collection{patientChu}, + }, + { + name: "returns ucum url as %ucum", + inputPath: "%ucum", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.String("http://unitsofmeasure.org")}, + }, + } + + testEvaluate(t, testCases) +} + +func TestPolarityExpression(t *testing.T) { + testCases := []evaluateTestCase{ + { + name: "negates integer", + inputPath: "-1", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Integer(-1)}, + }, + { + name: "does nothing when using '+'", + inputPath: "+2.45", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Decimal(decimal.NewFromFloat(2.45))}, + }, + { + name: "negates field from proto", + inputPath: "-(Patient.multipleBirth as integer)", + inputCollection: []fhir.Resource{patientVoldemort}, + wantCollection: system.Collection{system.Integer(-2)}, + }, + { + name: "performs arithmetic correctly with negatives", + inputPath: "-1 - (-2)", + inputCollection: []fhir.Resource{}, + wantCollection: system.Collection{system.Integer(1)}, + }, + } + + testEvaluate(t, testCases) +} + +func TestMustCompile_CompileError_Panics(t *testing.T) { + defer func() { _ = recover() }() + + fhirpath.MustCompile("Patient.name.where(use = official") + + t.Errorf("MustCompile: Expected panic") +} + +func TestMustCompile_ValidExpression_ReturnsExpression(t *testing.T) { + result := fhirpath.MustCompile("Patient.name") + + if result == nil { + t.Errorf("MustCompile: Expected result") + } +} diff --git a/fhirpath/fhirpathtest/fhirpathtest.go b/fhirpath/fhirpathtest/fhirpathtest.go new file mode 100644 index 0000000..f9dc733 --- /dev/null +++ b/fhirpath/fhirpathtest/fhirpathtest.go @@ -0,0 +1,51 @@ +/* +Package fhirpathtest provides an easy way to generate test-doubles within +FHIRPath. +*/ +package fhirpathtest + +import ( + "github.com/verily-src/fhirpath-go/fhirpath" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// Return creates a FHIRPath expression that will always return the given +// values. +func Return(args ...any) *fhirpath.Expression { + return ReturnCollection(system.Collection(args)) +} + +// ReturnCollection creates a FHIRPath expression that will always return the +// given input collection. +func ReturnCollection(collection system.Collection) *fhirpath.Expression { + return fhirpath.MustCompile("return()", + fhirpath.WithFunction("return", func(system.Collection) (system.Collection, error) { + return collection, nil + }), + ) +} + +// Error creates a FHIRPath expression that will always return the specified error. +func Error(err error) *fhirpath.Expression { + return fhirpath.MustCompile("return()", + fhirpath.WithFunction("return", func(system.Collection) (system.Collection, error) { + return nil, err + }), + ) +} + +var ( + // Empty is a FHIRPath expression that returns an empty collection when + // evaluated. + Empty = Return(system.Collection{}) + + // True is a FHIRPath expression that returns a collection containing a single + // system boolean of 'true'. This is useful for testing expected boolean + // logic in paths. + True = Return(system.Collection{system.Boolean(true)}) + + // False is a FHIRPath expression that returns a collection containing a single + // system boolean of 'false'. This is useful for testing expected boolean + // logic in paths. + False = Return(system.Collection{system.Boolean(false)}) +) diff --git a/fhirpath/fhirpathtest/fhirpathtest_example_test.go b/fhirpath/fhirpathtest/fhirpathtest_example_test.go new file mode 100644 index 0000000..2c0ea04 --- /dev/null +++ b/fhirpath/fhirpathtest/fhirpathtest_example_test.go @@ -0,0 +1,47 @@ +package fhirpathtest_test + +import ( + "errors" + "fmt" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/fhirpathtest" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func ExampleError() { + want := errors.New("example error") + expr := fhirpathtest.Error(want) + + _, err := expr.Evaluate([]fhir.Resource{}) + if errors.Is(err, want) { + fmt.Printf("err = '%v'", want) + } + + // Output: err = 'example error' +} + +func ExampleReturn() { + want := system.Boolean(true) + expr := fhirpathtest.Return(want) + + got, err := expr.Evaluate([]fhir.Resource{}) + if err != nil { + panic(err) + } + + fmt.Printf("got = %v", bool(got[0].(system.Boolean))) + // Output: got = true +} +func ExampleReturnCollection() { + want := system.Collection{system.Boolean(true)} + expr := fhirpathtest.ReturnCollection(want) + + got, err := expr.Evaluate([]fhir.Resource{}) + if err != nil { + panic(err) + } + + fmt.Printf("got = %v", bool(got[0].(system.Boolean))) + // Output: got = true +} diff --git a/fhirpath/fhirpathtest/fhirpathtest_test.go b/fhirpath/fhirpathtest/fhirpathtest_test.go new file mode 100644 index 0000000..1ea6443 --- /dev/null +++ b/fhirpath/fhirpathtest/fhirpathtest_test.go @@ -0,0 +1,61 @@ +package fhirpathtest_test + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" + "github.com/verily-src/fhirpath-go/fhirpath/fhirpathtest" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestError_Evaluates_ReturnsErr(t *testing.T) { + wantErr := errors.New("test error") + expr := fhirpathtest.Error(wantErr) + + _, err := expr.Evaluate([]fhir.Resource{}) + + if got, want := err, wantErr; !errors.Is(got, want) { + t.Errorf("Error: want err %v, got %v", want, got) + } +} + +func TestReturn_Evaluates_ReturnsCollectionOfEntries(t *testing.T) { + res := fhirtest.NewResource(t, resource.Patient) + want := system.Collection{res} + expr := fhirpathtest.Return(res) + + got, err := expr.Evaluate([]fhir.Resource{}) + if err != nil { + t.Fatalf("Return: unexpected err: %v", err) + } + + if len(got) != len(want) { + t.Fatalf("Return: mismatched size; want %v, got %v", len(got), len(want)) + } + if !cmp.Equal(got[0], want[0], protocmp.Transform()) { + t.Errorf("Return: want %v, got %v", want, got) + } +} + +func TestReturnCollection_Evaluates_ReturnsCollectionOfEntries(t *testing.T) { + res := fhirtest.NewResource(t, resource.Patient) + want := system.Collection{res} + expr := fhirpathtest.ReturnCollection(want) + + got, err := expr.Evaluate([]fhir.Resource{}) + if err != nil { + t.Fatalf("Return: unexpected err: %v", err) + } + + if len(got) != len(want) { + t.Fatalf("ReturnCollection: mismatched size; want %v, got %v", len(got), len(want)) + } + if !cmp.Equal(got[0], want[0], protocmp.Transform()) { + t.Errorf("ReturnCollection: want %v, got %v", want, got) + } +} diff --git a/fhirpath/internal/compile/compile.go b/fhirpath/internal/compile/compile.go new file mode 100644 index 0000000..510e78e --- /dev/null +++ b/fhirpath/internal/compile/compile.go @@ -0,0 +1,46 @@ +package compile + +import ( + "github.com/antlr4-go/antlr/v4" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" + "github.com/verily-src/fhirpath-go/fhirpath/internal/grammar" + "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" + "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" +) + +// PopulateConfig creates a CompileConfig and prepopulates it with +// a function table and any provided options. +func PopulateConfig(options ...opts.CompileOption) (*opts.CompileConfig, error) { + config := &opts.CompileConfig{ + Table: funcs.Clone(), + } + config, err := opts.ApplyOptions(config, options...) + if err != nil { + return nil, err + } + return config, err +} + +// Tree creates an ANTLR parsing context from the provided FHIRPath string. +func Tree(expr string) (grammar.IProgContext, error) { + inputStream := antlr.NewInputStream(expr) + errorListener := &parser.FHIRPathErrorListener{} + + // Lex the input stream + lexer := grammar.NewfhirpathLexer(inputStream) + lexer.RemoveErrorListeners() + lexer.AddErrorListener(errorListener) + tokens := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) + + // Parse the tokens + p := grammar.NewfhirpathParser(tokens) + p.RemoveErrorListeners() + p.AddErrorListener(errorListener) + tree := p.Prog() + + if err := errorListener.Error(); err != nil { + return nil, err + } + + return tree, nil +} diff --git a/fhirpath/internal/compile/doc.go b/fhirpath/internal/compile/doc.go new file mode 100644 index 0000000..51a9394 --- /dev/null +++ b/fhirpath/internal/compile/doc.go @@ -0,0 +1,5 @@ +/* +Package compile provides helpers for compiling +FHIRPath strings into FHIRPath expressions. +*/ +package compile diff --git a/fhirpath/internal/expr/arithmetic.go b/fhirpath/internal/expr/arithmetic.go new file mode 100644 index 0000000..a753f89 --- /dev/null +++ b/fhirpath/internal/expr/arithmetic.go @@ -0,0 +1,197 @@ +package expr + +import ( + "fmt" + + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// EvaluateAdd takes in two system types, and calls the appropriate Add method. +func EvaluateAdd(lhs, rhs system.Any) (system.Any, error) { + switch left := lhs.(type) { + case system.String: + if right, ok := rhs.(system.String); ok { + return left.Add(right), nil + } + return nil, typeMismatch(Add, lhs, rhs) + case system.Integer: + if right, ok := rhs.(system.Integer); ok { + return left.Add(right) + } + return nil, typeMismatch(Add, lhs, rhs) + case system.Decimal: + if right, ok := rhs.(system.Decimal); ok { + return left.Add(right), nil + } + return nil, typeMismatch(Add, lhs, rhs) + case system.Time: + if right, ok := rhs.(system.Quantity); ok { + return left.Add(right) + } + return nil, typeMismatch(Add, lhs, rhs) + case system.Date: + if right, ok := rhs.(system.Quantity); ok { + return left.Add(right) + } + return nil, typeMismatch(Add, lhs, rhs) + case system.DateTime: + if right, ok := rhs.(system.Quantity); ok { + return left.Add(right) + } + return nil, typeMismatch(Add, lhs, rhs) + case system.Quantity: + if right, ok := rhs.(system.Quantity); ok { + return left.Add(right) + } + return nil, typeMismatch(Add, lhs, rhs) + default: + return nil, typeMismatch(Add, lhs, rhs) + } +} + +// EvaluateSub takes in two system types, and calls the appropriate Sub method. +func EvaluateSub(lhs, rhs system.Any) (system.Any, error) { + switch left := lhs.(type) { + case system.Integer: + if right, ok := rhs.(system.Integer); ok { + return left.Sub(right) + } + return nil, typeMismatch(Sub, lhs, rhs) + case system.Decimal: + if right, ok := rhs.(system.Decimal); ok { + return left.Sub(right), nil + } + return nil, typeMismatch(Sub, lhs, rhs) + case system.Time: + if right, ok := rhs.(system.Quantity); ok { + return left.Sub(right) + } + return nil, typeMismatch(Sub, lhs, rhs) + case system.Date: + if right, ok := rhs.(system.Quantity); ok { + return left.Sub(right) + } + return nil, typeMismatch(Sub, lhs, rhs) + case system.DateTime: + if right, ok := rhs.(system.Quantity); ok { + return left.Sub(right) + } + return nil, typeMismatch(Sub, lhs, rhs) + case system.Quantity: + if right, ok := rhs.(system.Quantity); ok { + return left.Sub(right) + } + return nil, typeMismatch(Sub, lhs, rhs) + default: + return nil, typeMismatch(Sub, lhs, rhs) + } +} + +// EvaluateMul takes in two system types, and calls the appropriate Mul method. +func EvaluateMul(lhs, rhs system.Any) (system.Any, error) { + switch left := lhs.(type) { + case system.Integer: + if right, ok := rhs.(system.Integer); ok { + return left.Mul(right) + } + if _, ok := rhs.(system.Quantity); ok { + return nil, fmt.Errorf("%w: PHP-7340", ErrToBeImplemented) + } + return nil, typeMismatch(Mul, lhs, rhs) + case system.Decimal: + if right, ok := rhs.(system.Decimal); ok { + return left.Mul(right), nil + } + if _, ok := rhs.(system.Quantity); ok { + return nil, fmt.Errorf("%w: PHP-7340", ErrToBeImplemented) + } + return nil, typeMismatch(Mul, lhs, rhs) + case system.Quantity: + return nil, fmt.Errorf("%w: PHP-7171", ErrToBeImplemented) + default: + return nil, typeMismatch(Mul, lhs, rhs) + } +} + +// EvaluateDiv takes in two system types, and calls the appropriate Div method. +func EvaluateDiv(lhs, rhs system.Any) (system.Any, error) { + switch left := lhs.(type) { + case system.Integer: + if right, ok := rhs.(system.Integer); ok { + return left.Div(right), nil + } + if _, ok := rhs.(system.Quantity); ok { + return nil, fmt.Errorf("%w: PHP-7340", ErrToBeImplemented) + } + return nil, typeMismatch(Div, lhs, rhs) + case system.Decimal: + if right, ok := rhs.(system.Decimal); ok { + return left.Div(right), nil + } + if _, ok := rhs.(system.Quantity); ok { + return nil, fmt.Errorf("%w: PHP-7340", ErrToBeImplemented) + } + return nil, typeMismatch(Div, lhs, rhs) + case system.Quantity: + return nil, fmt.Errorf("%w: PHP-7171", ErrToBeImplemented) + default: + return nil, typeMismatch(Div, lhs, rhs) + } +} + +// EvaluateFloorDiv takes in two system types, and calls the appropriate FloorDiv method. +func EvaluateFloorDiv(lhs, rhs system.Any) (system.Any, error) { + switch left := lhs.(type) { + case system.Integer: + if right, ok := rhs.(system.Integer); ok { + return left.FloorDiv(right), nil + } + if _, ok := rhs.(system.Quantity); ok { + return nil, fmt.Errorf("%w: PHP-7340", ErrToBeImplemented) + } + return nil, typeMismatch(FloorDiv, lhs, rhs) + case system.Decimal: + if right, ok := rhs.(system.Decimal); ok { + return left.FloorDiv(right) + } + if _, ok := rhs.(system.Quantity); ok { + return nil, fmt.Errorf("%w: PHP-7340", ErrToBeImplemented) + } + return nil, typeMismatch(FloorDiv, lhs, rhs) + case system.Quantity: + return nil, fmt.Errorf("%w: PHP-7171", ErrToBeImplemented) + default: + return nil, typeMismatch(FloorDiv, lhs, rhs) + } +} + +// EvaluateMod takes in two system types, and calls the appropriate Mod method. +func EvaluateMod(lhs, rhs system.Any) (system.Any, error) { + switch left := lhs.(type) { + case system.Integer: + if right, ok := rhs.(system.Integer); ok { + return left.Mod(right), nil + } + if _, ok := rhs.(system.Quantity); ok { + return nil, fmt.Errorf("%w: PHP-7340", ErrToBeImplemented) + } + return nil, typeMismatch(Mod, lhs, rhs) + case system.Decimal: + if right, ok := rhs.(system.Decimal); ok { + return left.Mod(right), nil + } + if _, ok := rhs.(system.Quantity); ok { + return nil, fmt.Errorf("%w: PHP-7340", ErrToBeImplemented) + } + return nil, typeMismatch(Mod, lhs, rhs) + case system.Quantity: + return nil, fmt.Errorf("%w: PHP-7171", ErrToBeImplemented) + default: + return nil, typeMismatch(Mod, lhs, rhs) + } +} + +// typeMismatch generates an unsupported operation error. +func typeMismatch(op Operator, lhs, rhs system.Any) error { + return fmt.Errorf("%w: %T %s %T", system.ErrTypeMismatch, lhs, op, rhs) +} diff --git a/fhirpath/internal/expr/booleans.go b/fhirpath/internal/expr/booleans.go new file mode 100644 index 0000000..98d44b6 --- /dev/null +++ b/fhirpath/internal/expr/booleans.go @@ -0,0 +1,47 @@ +package expr + +import "github.com/verily-src/fhirpath-go/fhirpath/system" + +func evaluateAnd(left []system.Boolean, right []system.Boolean) system.Collection { + if len(left) > 0 && len(right) > 0 { + result := system.Boolean(left[0] && right[0]) + return system.Collection{result} + } + // returns false if either boolean is false, regardless of whether or not the other is empty. + if (len(left) == 1 && !left[0]) || (len(right) == 1 && !right[0]) { + return system.Collection{system.Boolean(false)} + } + return system.Collection{} +} + +func evaluateOr(left []system.Boolean, right []system.Boolean) system.Collection { + if len(left) > 0 && len(right) > 0 { + result := system.Boolean(left[0] || right[0]) + return system.Collection{result} + } + // returns false if either boolean is true, regardless of whether or not the other is empty. + if (len(left) == 1 && left[0]) || (len(right) == 1 && right[0]) { + return system.Collection{system.Boolean(true)} + } + return system.Collection{} +} + +func evaluateXor(left []system.Boolean, right []system.Boolean) system.Collection { + if len(left) > 0 && len(right) > 0 { + result := system.Boolean(left[0] != right[0]) + return system.Collection{result} + } + return system.Collection{} +} + +func evaluateImplies(left []system.Boolean, right []system.Boolean) system.Collection { + if len(left) > 0 && len(right) > 0 { + result := system.Boolean(!left[0] || right[0]) + return system.Collection{result} + } + // returns true if left is false, or if right is true, regardless of whether or not the other is empty. + if (len(left) > 0 && !left[0]) || (len(right) > 0 && right[0]) { + return system.Collection{system.Boolean(true)} + } + return system.Collection{} +} diff --git a/fhirpath/internal/expr/context.go b/fhirpath/internal/expr/context.go new file mode 100644 index 0000000..b65d9da --- /dev/null +++ b/fhirpath/internal/expr/context.go @@ -0,0 +1,47 @@ +package expr + +import ( + "time" + + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// Context holds the global time and external constant +// variable map, to enable deterministic evaluation. +type Context struct { + Now time.Time + ExternalConstants map[string]any + + // LastResult is required for implementing most FHIRPatch operations, since + // a reference to the node before the one being (inserted, replaced, moved) is + // necessary in order to alter the containing object. + LastResult system.Collection + + // BeforeLastResult is necessary for implementing FHIRPatch delete due to an + // edge-case, where deleting a specific element from a list requires a pointer + // to the container that holds the list. In a path like `Patient.name.given[0]`, + // the 'LastResult' will be the unwrapped list from 'given', but we need the + // 'name' element that contains the 'given' list in order to alter the list. + BeforeLastResult system.Collection +} + +// Clone copies this Context object to produce a new instance. +func (c *Context) Clone() *Context { + return &Context{ + Now: c.Now, + ExternalConstants: c.ExternalConstants, + LastResult: c.LastResult, + } +} + +// InitializeContext returns a base context, initialized with current time and initial +// constant variables set. +func InitializeContext(input system.Collection) *Context { + return &Context{ + Now: time.Now().Local().UTC(), + ExternalConstants: map[string]any{ + "context": input, + "ucum": system.String("http://unitsofmeasure.org"), + }, + } +} diff --git a/fhirpath/internal/expr/doc.go b/fhirpath/internal/expr/doc.go new file mode 100644 index 0000000..1123e43 --- /dev/null +++ b/fhirpath/internal/expr/doc.go @@ -0,0 +1,5 @@ +/* +Package expr contains all the expression types +and related logic for FHIRPath expressions. +*/ +package expr diff --git a/fhirpath/internal/expr/expressions.go b/fhirpath/internal/expr/expressions.go new file mode 100644 index 0000000..e25b9b7 --- /dev/null +++ b/fhirpath/internal/expr/expressions.go @@ -0,0 +1,779 @@ +package expr + +import ( + "errors" + "fmt" + "strings" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/iancoleman/strcase" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/containedresource" + "github.com/verily-src/fhirpath-go/internal/fhirconv" + "github.com/verily-src/fhirpath-go/fhirpath/internal/reflection" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "github.com/verily-src/fhirpath-go/internal/protofields" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +var ( + ErrNotSingleton = errors.New("collection is not a singleton") + ErrInvalidType = errors.New("collection evaluates to incorrect type") + ErrInvalidOperator = errors.New("received invalid operator") + ErrToBeImplemented = errors.New("expression not yet implemented") + ErrInvalidField = errors.New("invalid field") + ErrConstantNotFound = errors.New("external constant not found") +) + +// Expression is the abstraction for all FHIRPath expressions, +// ie. taking in some input collection and outputting some collection. +type Expression interface { + Evaluate(*Context, system.Collection) (system.Collection, error) +} + +// ExpressionSequence abstracts the flow of evaluation for +// compound expressions. It consists of a sequence of expressions +// whose outputs flow into the inputs of the next expression. +type ExpressionSequence struct { + Expressions []Expression +} + +// Evaluate iterates through the ExpressionSequence, feeding the output of +// an evaluation to the next Expression. +func (s *ExpressionSequence) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + output := input + + for _, expr := range s.Expressions { + result, err := expr.Evaluate(ctx, output) + // raise error as soon as one is encountered + if err != nil { + return nil, err + } + output = result + } + return output, nil +} + +var _ Expression = (*ExpressionSequence)(nil) + +// IdentityExpression encapsulates the top-level expression, ie. the +// first step in the chain of evaluation. A no-op expression that +// returns itself. +type IdentityExpression struct{} + +// Evaluate returns the input collection, without raising +// an error. +func (*IdentityExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + return input, nil +} + +var _ Expression = (*IdentityExpression)(nil) + +// FieldExpression is the expression that accesses the specified +// FieldName in the input collection. +type FieldExpression struct { + FieldName string + Permissive bool +} + +// Evaluate filters the input collections by those that contain +// the FieldName string, and returns the result. +func (e *FieldExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + output := system.Collection{} + + for _, item := range input { + message, ok := item.(proto.Message) + if !ok { + if e.Permissive { + continue + } + return nil, e.errField(item) + } + + // Date, Time, DateTime, and Instant have "fake" fields 'value_us', 'timezone', + // and 'precision'. This checks to ensure that such fields aren't being accessed, + // since they aren't actually real and don't exist in the FHIR spec. + if !e.isEvaluable(message) { + return nil, e.errField(message) + } + + // unwrap if a ContainedResource + if contained, ok := message.(*bcrpb.ContainedResource); ok { + message = containedresource.Unwrap(contained) + } + + // Get desired field + fieldName := strcase.ToSnake(e.FieldName) + reflect := message.ProtoReflect() + field := reflect.Descriptor().Fields().ByName(protoreflect.Name(fieldName)) + + // extract field and append to output, flattening + // if the field is a list. Raises error if field doesn't exist + if field == nil { + // If the field is a reference, we need to combine the type and + // ID fields to create a usable reference, e.g. Type/ID. Since + // ID is a oneof (e.g. questionnaire_id), we need to determine + // which it is to find the appropriate field. + if fieldName == "reference" { + if reference, ok := message.(*dtpb.Reference); ok { + refString := e.unwrapReference(reference) + if refString != nil { + output = append(output, refString) + } + continue + } + } + + // Attempting to get a "value" field from a Date, DateTime, Time, or Instant + // needs to convert the value to a System String type. + // The FHIR Protos model time datatypes using a "value_us" field, which + // is normalized here, since the FHIR spec models these types as strings + // with a "value" field. + if fieldName == "value" { + switch v := message.(type) { + case *dtpb.Date: + output = append(output, system.String(fhirconv.DateToString(v))) + continue + case *dtpb.DateTime: + output = append(output, system.String(fhirconv.DateTimeToString(v))) + continue + case *dtpb.Time: + output = append(output, system.String(fhirconv.TimeToString(v))) + continue + case *dtpb.Instant: + output = append(output, system.String(fhirconv.InstantToString(v))) + continue + } + } + + // Try again with "_value" added because sometimes Google protos do that + // for primitives like: + // Observation.ValueX.String --> Observation_ValueX_StringValue + fieldName = fieldName + "_value" + field = reflect.Descriptor().Fields().ByName(protoreflect.Name(fieldName)) + if field == nil { + return nil, fmt.Errorf("%w: %s not a field on %T", ErrInvalidField, fieldName, message) + } + } + + // If the field is not a message, it is a primitive (enum or go native type). + // So, it can be cast to a system type. Otherwise, a field is being accessed that + // shouldn't be accessed, so the error is returned. + if field.Kind() != protoreflect.MessageKind { + primitive, err := system.From(message) + if err != nil { + return nil, err + } + output = append(output, primitive) + continue + } + + unwrap := e.unwrapOneof + if e.Permissive { + unwrap = func(obj proto.Message) proto.Message { return obj } + } + + if !field.IsList() { + message := reflect.Get(field).Message() + if !message.IsValid() { + continue + } + output = append(output, unwrap(message.Interface())) + continue + } + content := reflect.Get(field).List() + for i := 0; i < content.Len(); i++ { // flatten out list + result := content.Get(i).Message().Interface() + output = append(output, unwrap(result)) + } + } + return output, nil +} + +var nonEvaluableFields = []string{ + "valueUs", "precision", "timezone", +} + +func (e *FieldExpression) isEvaluable(msg proto.Message) bool { + if e.Permissive { + return true + } + + // Prevent snake_case fields, since all FHIRPath fields need to be in + // camelCase. + if strcase.ToLowerCamel(e.FieldName) != e.FieldName { + return false + } + + // Prevent manually accessing idiosynchratic fields from google/fhir like + // value_us, precision, and time_zone + switch msg.(type) { + case *dtpb.Time, *dtpb.Date, *dtpb.DateTime, *dtpb.Instant: + return !slices.Includes(nonEvaluableFields, e.FieldName) + } + + return true +} + +func (e *FieldExpression) errField(object any) error { + return fmt.Errorf("%w: %s not a field on %T", ErrInvalidField, e.FieldName, object) +} + +func (e *FieldExpression) unwrapReference(ref *dtpb.Reference) *dtpb.String { + if ref.GetReference() == nil { + return nil + } + rv := ref.ProtoReflect() + switch ref := ref.GetReference().(type) { + case *dtpb.Reference_Uri: + return fhir.String(ref.Uri.GetValue()) + case *dtpb.Reference_Fragment: + return fhir.String("#" + ref.Fragment.GetValue()) + default: + descriptor := rv.Descriptor() + oneof := descriptor.Oneofs().ByName("reference") + field := rv.WhichOneof(oneof) + if field == nil { + return nil + } + refid := rv.Get(field).Message().Interface().(*dtpb.ReferenceId) + fieldName, ok := strings.CutSuffix(string(field.Name()), "_id") + if !ok { + return nil + } + fieldName = strcase.ToCamel(fieldName) + if history := refid.GetHistory(); history != nil { + return fhir.String(fmt.Sprintf("%v/%v/_history/%v", fieldName, refid.GetValue(), history.GetValue())) + } + return fhir.String(fmt.Sprintf("%v/%v", fieldName, refid.GetValue())) + } +} + +func (e *FieldExpression) unwrapOneof(obj proto.Message) proto.Message { + message := obj.ProtoReflect() + descriptor := message.Descriptor() + if name := string(descriptor.Name()); !(strings.HasSuffix(name, "ValueX") || name == "ContainedResource") { + return obj + } + oneofsNum := descriptor.Oneofs().Len() + if oneofsNum != 1 { + return obj + } + + oneof := descriptor.Oneofs().Get(0) + field := message.WhichOneof(oneof) + if oneof == nil || field == nil { + return obj + } + if msg := message.Get(field).Message(); msg != nil { + return msg.Interface() + } + return obj +} + +var _ Expression = (*FieldExpression)(nil) + +// TypeExpression contains the FHIR Type identifier string, +// to be able to filter the items in the input collection that have the +// given type. +type TypeExpression struct { + Type string +} + +// Evaluate filters the messages in the input that are identified by the Type +// defined in the expression. +func (e *TypeExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + output := system.Collection{} + + for _, item := range input { + message, ok := item.(proto.Message) + if !ok { + continue + } + + // find message name, add to collection only if it matches + pReflect := message.ProtoReflect() + name := pReflect.Descriptor().Name() + + if string(name) != e.Type { + continue + } + + output = append(output, message) + } + return output, nil +} + +var _ Expression = (*TypeExpression)(nil) + +// LiteralExpression abstracts FHIRPath system types, that +// are returned from parsing literals. +type LiteralExpression struct { + Literal system.Any +} + +// Evaluate returns the contained literal, without +// raising an error. Returns an empty collection if the +// literal is nil, representing a Null literal. +func (e *LiteralExpression) Evaluate(*Context, system.Collection) (system.Collection, error) { + if e.Literal != nil { + return system.Collection{e.Literal}, nil + } + return system.Collection{}, nil +} + +var _ Expression = (*LiteralExpression)(nil) + +// IndexExpression allows accessing of an input system.Collection's index. +// Contains an expression, that when evaluated, should return an integer +// that represents the index. +type IndexExpression struct { + Index Expression +} + +// Evaluate indexes the input system.Collection and returns the +// item located at the given index. If the index is negative +// or out of bounds, returns an empty collection. Raises an error if the +// contained expression does not evaluate to an expression, or raises an error +// itself. +func (e *IndexExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + indexResult, err := e.Index.Evaluate(ctx, input) + if err != nil { + return nil, err + } + length := len(indexResult) + if length == 0 { + return system.Collection{}, nil + } + if length > 1 { + return nil, fmt.Errorf("%w: contains %v elements", ErrNotSingleton, length) + } + value, err := system.From(indexResult[0]) + if err != nil { + return nil, err + } + index, ok := value.(system.Integer) + if !ok { + return nil, fmt.Errorf("%w: want Integer but got %T", ErrInvalidType, index) + } + if int(index) >= len(input) || int(index) < 0 { + return system.Collection{}, nil + } + return system.Collection{input[int(index)]}, nil +} + +var _ Expression = (*IndexExpression)(nil) + +// EqualityExpression allows checking equality of the two contained +// subexpressions. The two expressions should return comparable values +// when evaluated. +type EqualityExpression struct { + Left Expression + Right Expression + Not bool +} + +// Evaluate evaluates the two subexpressions, and returns true if their +// contents are equal, using the functionality of system.Collection.Equal. If either +// collection is empty, returns an empty collection. +func (e *EqualityExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + leftResult, err := e.Left.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + rightResult, err := e.Right.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + if len(leftResult) == 0 || len(rightResult) == 0 { + return system.Collection{}, nil + } + + result, ok := leftResult.TryEqual(rightResult) + if !ok { + return system.Collection{}, nil + } + if err != nil { + return nil, err + } + if e.Not { + result = !result + } + return system.Collection{system.Boolean(result)}, nil +} + +var _ Expression = (*EqualityExpression)(nil) + +// FunctionExpression enables evaluation of Function Invocation expressions. +// It holds the function and function arguments. +type FunctionExpression struct { + Fn func(*Context, system.Collection, ...Expression) (system.Collection, error) + Args []Expression +} + +// Evaluate evaluates the function with respect to its arguments. Returns the result +// of the function, or an error if raised. +func (e *FunctionExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + return e.Fn(ctx.Clone(), input, e.Args...) +} + +var _ Expression = (*FunctionExpression)(nil) + +// IsExpression enables evaluation of an "is" type expression. +type IsExpression struct { + Expr Expression + Type reflection.TypeSpecifier +} + +// Evaluate evaluates the contained expression with respect to singleton evaluation +// of collections, and determines whether or not it is the given type. +func (e *IsExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + result, err := e.Expr.Evaluate(ctx, input) + if err != nil { + return nil, err + } + length := len(result) + if length == 0 { + return system.Collection{}, nil + } + if length > 1 { + return nil, fmt.Errorf("%w: contains %v elements", ErrNotSingleton, length) + } + typeSpecifier, err := reflection.TypeOf(result[0]) + if err != nil { + return nil, err + } + return system.Collection{typeSpecifier.Is(e.Type)}, nil +} + +var _ Expression = (*IsExpression)(nil) + +// AsExpression enables evaluation of an "as" type expression. +type AsExpression struct { + Expr Expression + Type reflection.TypeSpecifier +} + +// Evaluate evaluates the contained expression with respect to singleton evaluation +// of collections, returns the singleton if it is of the given type. Returns empty otherwise. +func (e *AsExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + result, err := e.Expr.Evaluate(ctx, input) + if err != nil { + return nil, err + } + length := len(result) + if length == 0 { + return system.Collection{}, nil + } + if length > 1 { + return nil, fmt.Errorf("%w: contains %v elements", ErrNotSingleton, length) + } + typeSpecifier, err := reflection.TypeOf(result[0]) + if err != nil { + return nil, err + } + if !typeSpecifier.Is(e.Type) { + return system.Collection{}, nil + } + // attempt to unwrap polymorphic types + message, ok := result[0].(fhir.Base) + if !ok { + return result, nil + } + if oneOf := protofields.UnwrapOneofField(message, "choice"); oneOf != nil { + return system.Collection{oneOf}, nil + } + return result, nil +} + +var _ Expression = (*AsExpression)(nil) + +// BooleanExpression enables evaluation of boolean expressions, +// including "and", "or", "xor", and "implies". +type BooleanExpression struct { + Left Expression + Right Expression + Op Operator +} + +// Evaluate evaluates the subexpressions with respect to singleton evaluation of +// collections, and performs the respective Boolean operation. +func (e *BooleanExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + leftResult, err := e.Left.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + rightResult, err := e.Right.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + + leftBool, err := leftResult.ToSingletonBoolean() + if err != nil { + return nil, err + } + rightBool, err := rightResult.ToSingletonBoolean() + if err != nil { + return nil, err + } + + switch e.Op { + case And: + return evaluateAnd(leftBool, rightBool), nil + case Or: + return evaluateOr(leftBool, rightBool), nil + case Xor: + return evaluateXor(leftBool, rightBool), nil + case Implies: + return evaluateImplies(leftBool, rightBool), nil + default: + return nil, fmt.Errorf("%w: %s", ErrInvalidOperator, e.Op) + } +} + +var _ Expression = (*BooleanExpression)(nil) + +type ComparisonExpression struct { + Left Expression + Right Expression + Op Operator +} + +// Evaluate evaluates the subexpressions with respect to singleton evaluation of collections, +// and performs the respective comparison operation. +func (e *ComparisonExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + leftResult, err := e.Left.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + rightResult, err := e.Right.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + + if len(leftResult) == 0 || len(rightResult) == 0 { + return system.Collection{}, nil + } + if len(leftResult) != 1 || len(rightResult) != 1 { + return nil, fmt.Errorf("%w: left contains %v elements, right contains %v elements", ErrNotSingleton, len(leftResult), len(rightResult)) + } + + leftPrimitive, err := system.From(leftResult[0]) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidType, err) + } + rightPrimitive, err := system.From(rightResult[0]) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidType, err) + } + + // Implicitly convert types + leftPrimitive = system.Normalize(leftPrimitive, rightPrimitive) + rightPrimitive = system.Normalize(rightPrimitive, leftPrimitive) + + // Calculate both less than and greater than + lessThan, err := leftPrimitive.Less(rightPrimitive) + if errors.Is(err, system.ErrMismatchedPrecision) || errors.Is(err, system.ErrMismatchedUnit) { + return system.Collection{}, nil + } + if err != nil { + return nil, err + } + greaterThan, err := rightPrimitive.Less(leftPrimitive) + if errors.Is(err, system.ErrMismatchedPrecision) { + return system.Collection{}, nil + } + if err != nil { + return nil, err + } + + switch e.Op { + case Lt: + return system.Collection{lessThan}, nil + case Gt: + return system.Collection{greaterThan}, nil // (a > b) = (b < a) + case Lte: + return system.Collection{!greaterThan}, nil // (a <= b) = !(a > b) + case Gte: + return system.Collection{!lessThan}, nil // (a >= b) = !(a < b) + default: + return nil, fmt.Errorf("%w: %s", ErrInvalidOperator, e.Op) + } +} + +var _ Expression = (*ComparisonExpression)(nil) + +// ArithmeticExpression enables mathematical arithmetic operations. +// Includes '+', '-', '*', "/", "div", and 'mod'. +type ArithmeticExpression struct { + Left Expression + Right Expression + Op func(system.Any, system.Any) (system.Any, error) +} + +// Evaluate evaluates the two subexpressions, with respect to singleton evaluation of collections, +// and performs the respective additive operation. +func (e *ArithmeticExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + leftResult, err := e.Left.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + rightResult, err := e.Right.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + + if len(leftResult) == 0 || len(rightResult) == 0 { + return system.Collection{}, nil + } + if len(leftResult) != 1 || len(rightResult) != 1 { + return nil, fmt.Errorf("%w: left contains %v elements, right contains %v elements", ErrNotSingleton, len(leftResult), len(rightResult)) + } + + // Cast contents to system types. Addition and subtraction is not supported for protos. + leftPrimitive, err := system.From(leftResult[0]) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidType, err) + } + rightPrimitive, err := system.From(rightResult[0]) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidType, err) + } + + // Implicitly convert types + leftPrimitive = system.Normalize(leftPrimitive, rightPrimitive) + rightPrimitive = system.Normalize(rightPrimitive, leftPrimitive) + + result, err := e.Op(leftPrimitive, rightPrimitive) + if errors.Is(err, system.ErrIntOverflow) { + return system.Collection{}, nil // "Operations that cause arithmetic overflow or underflow will result in empty ( { } )". + } + if err != nil { + return nil, err + } + return system.Collection{result}, nil +} + +var _ Expression = (*ArithmeticExpression)(nil) + +// ConcatExpression enables the evaluation of a string concatenation expression. +type ConcatExpression struct { + Left Expression + Right Expression +} + +// Evaluate evaluates the two subexpressions with respect to singleton evaluation of +// collections, and attempts to concatenate the two strings. Returns an error if the expressions +// don't resolve to strings. This differs from string addition when either collection is empty. Rather +// than returning empty, it will treat the empty collection as an empty string. +func (e *ConcatExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + leftResult, err := e.Left.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + rightResult, err := e.Right.Evaluate(ctx.Clone(), input) + if err != nil { + return nil, err + } + + // Convert empty collection to empty string + if len(leftResult) == 0 { + leftResult = append(leftResult, system.String("")) + } + if len(rightResult) == 0 { + rightResult = append(rightResult, system.String("")) + } + + if len(leftResult) > 1 || len(rightResult) > 1 { + return nil, fmt.Errorf("%w: left contains %v elements, right contains %v elements", ErrNotSingleton, len(leftResult), len(rightResult)) + } + + // Cast contents to system types + leftPrimitive, err := system.From(leftResult[0]) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidType, err) + } + rightPrimitive, err := system.From(rightResult[0]) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidType, err) + } + + leftStr, ok := leftPrimitive.(system.String) + if !ok { + return nil, fmt.Errorf("%w: expected a string", ErrInvalidType) + } + rightStr, ok := rightPrimitive.(system.String) + if !ok { + return nil, fmt.Errorf("%w: expected a string", ErrInvalidType) + } + + return system.Collection{leftStr + rightStr}, nil +} + +var _ Expression = (*ConcatExpression)(nil) + +// ExternalConstantExpression enables evaluation of external constants. +type ExternalConstantExpression struct { + Identifier string +} + +// Evaluate retrieves the constant from the map located in the Context. Returns an error if the +// constant is not present. +func (e *ExternalConstantExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + constant, ok := ctx.ExternalConstants[e.Identifier] + if !ok { + return nil, fmt.Errorf("%w: %s", ErrConstantNotFound, e.Identifier) + } + if collection, ok := constant.(system.Collection); ok { + return collection, nil + } + return system.Collection{constant}, nil +} + +var _ Expression = (*ExternalConstantExpression)(nil) + +// NegationExpression enables negation of number values (Integer, Decimal, Quantity). +type NegationExpression struct { + Expr Expression +} + +// Evaluate negates the contained expression, that is evaluated with respect to singleton evaluation. +// If the contained value is not a number, returns an error. +func (e *NegationExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) { + result, err := e.Expr.Evaluate(ctx, input) + if err != nil { + return nil, err + } + + length := len(result) + if length == 0 { + return system.Collection{}, nil + } + if length != 1 { + return nil, fmt.Errorf("%w: can't negate a collection", ErrNotSingleton) + } + + primitive, err := system.From(result[0]) + if err != nil { + return nil, fmt.Errorf("%w: can't negate complex type %T", ErrInvalidType, result[0]) + } + + // handle negation of value + switch v := primitive.(type) { + case system.Integer: + return system.Collection{system.Integer(-1) * v}, nil + case system.Decimal: + negative := system.Decimal(decimal.NewFromInt(-1)) + return system.Collection{v.Mul(negative)}, nil + case system.Quantity: + return system.Collection{v.Negate()}, nil + default: + return nil, fmt.Errorf("%w: can't negate %T", ErrInvalidType, primitive) + } +} + +var _ Expression = (*NegationExpression)(nil) diff --git a/fhirpath/internal/expr/expressions_test.go b/fhirpath/internal/expr/expressions_test.go new file mode 100644 index 0000000..2e4eaf0 --- /dev/null +++ b/fhirpath/internal/expr/expressions_test.go @@ -0,0 +1,1465 @@ +package expr_test + +import ( + "errors" + "math" + "testing" + "time" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_go_proto" + epb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/encounter_go_proto" + mrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/bundle" + "github.com/verily-src/fhirpath-go/internal/element/extension" + "github.com/verily-src/fhirpath-go/internal/fhirconv" + "github.com/verily-src/fhirpath-go/fhirpath" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" + "github.com/verily-src/fhirpath-go/fhirpath/internal/reflection" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/testing/protocmp" +) + +var ( + errMock = errors.New("some error") +) + +func TestIdentityExpression_Input_EqualsOutput(t *testing.T) { + identity := &expr.IdentityExpression{} + testCases := []struct { + name string + data system.Collection + }{ + { + name: "Empty set", + data: system.Collection{}, + }, + { + name: "Mixed type set", + data: system.Collection{fhir.String("test"), fhir.Integer(1), fhir.Integer(2)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + out, err := identity.Evaluate(&expr.Context{}, tc.data) + + if err != nil { + t.Fatalf("Input: %s error when not expected, err: %v", tc.name, err) + } + if got, want := out, tc.data; !cmp.Equal(got, want, protocmp.Transform()) { + t.Errorf("Input: %s, got: %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestFieldExpression_Gets_DesiredField(t *testing.T) { + patientID := "123" + patientFirstHumanName := &dtpb.HumanName{ + Given: []*dtpb.String{ + fhir.String("IU"), + fhir.String("Amanda"), + }, + } + patientBirthDay := &dtpb.Date{ + ValueUs: time.Now().UnixMicro(), + Precision: dtpb.Date_DAY, + } + fullName := &dtpb.HumanName{ + Given: []*dtpb.String{ + fhir.String("Julius"), + }, + Family: fhir.String("Caesar"), + } + patientContactPoint := []*dtpb.ContactPoint{ + { + System: &dtpb.ContactPoint_SystemCode{ + Value: cpb.ContactPointSystemCode_PHONE, + }, + Value: fhir.String("123-456-7890"), + Rank: fhir.PositiveInt(1), + }, + { + System: &dtpb.ContactPoint_SystemCode{ + Value: cpb.ContactPointSystemCode_EMAIL, + }, + Value: fhir.String("example@gmail.com"), + Rank: fhir.PositiveInt(2), + }, + } + + containedPatient := &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{ + Patient: &ppb.Patient{ + Id: fhir.ID(patientID), + Active: fhir.Boolean(true), + Gender: &ppb.Patient_GenderCode{ + Value: cpb.AdministrativeGenderCode_FEMALE, + }, + Deceased: &ppb.Patient_DeceasedX{ + Choice: &ppb.Patient_DeceasedX_Boolean{ + Boolean: fhir.Boolean(true), + }, + }, + MultipleBirth: &ppb.Patient_MultipleBirthX{ + Choice: &ppb.Patient_MultipleBirthX_Integer{ + Integer: fhir.Integer(int32(2)), + }, + }, + Meta: &dtpb.Meta{ + Tag: []*dtpb.Coding{ + { + Code: fhir.Code("#blessed"), + }, + }, + }, + Name: []*dtpb.HumanName{ + patientFirstHumanName, + fullName, + }, + Telecom: patientContactPoint, + }, + }, + } + patientMissingName := &ppb.Patient{ + Id: fhir.ID(patientID), + Active: fhir.Boolean(true), + Telecom: patientContactPoint, + BirthDate: patientBirthDay, + } + patientWithOneName := &ppb.Patient{ + Id: fhir.ID(patientID), + Name: []*dtpb.HumanName{fullName}, + } + + testCases := []struct { + name string + fieldExp *expr.FieldExpression + input system.Collection + wantCollection system.Collection + wantErr error + }{ + { + name: "contained resource has collection in field", + fieldExp: &expr.FieldExpression{FieldName: "name"}, + input: system.Collection{containedPatient}, + wantCollection: system.Collection{patientFirstHumanName, fullName}, + }, + { + name: "resource has empty list field", + fieldExp: &expr.FieldExpression{FieldName: "name"}, + input: system.Collection{patientMissingName}, + wantCollection: system.Collection{}, + }, + { + name: "resource has empty non-list field", + fieldExp: &expr.FieldExpression{FieldName: "family"}, + input: system.Collection{patientFirstHumanName}, + wantCollection: system.Collection{}, + }, + { + name: "field is appended with the suffix `value`", + fieldExp: &expr.FieldExpression{FieldName: "class"}, + input: system.Collection{&epb.Encounter{ClassValue: fhir.Coding("class-system", "class-code")}}, + wantCollection: system.Collection{fhir.Coding("class-system", "class-code")}, + }, + { + name: "accessing non existent field", + fieldExp: &expr.FieldExpression{FieldName: "version"}, + input: system.Collection{containedPatient}, + wantErr: expr.ErrInvalidField, + }, + { + name: "resource has singleton in field", + fieldExp: &expr.FieldExpression{FieldName: "name"}, + input: system.Collection{patientWithOneName}, + wantCollection: system.Collection{fullName}, + }, + { + name: "accessing non-list field", + fieldExp: &expr.FieldExpression{FieldName: "id"}, + input: system.Collection{containedPatient}, + wantCollection: system.Collection{fhir.ID(patientID)}, + }, + { + name: "accessing family in HumanName element", + fieldExp: &expr.FieldExpression{FieldName: "family"}, + input: system.Collection{fullName}, + wantCollection: system.Collection{fhir.String("Caesar")}, + }, + { + name: "accessing fields from multiple resources", + fieldExp: &expr.FieldExpression{FieldName: "name"}, + input: system.Collection{containedPatient, patientWithOneName}, + wantCollection: system.Collection{patientFirstHumanName, fullName, fullName}, + }, + { + name: "accessing field of 2 words", + fieldExp: &expr.FieldExpression{FieldName: "birthDate"}, + input: system.Collection{patientMissingName}, + wantCollection: system.Collection{patientBirthDay}, + }, + { + name: "(Legacy) input contains non-resource items", + fieldExp: &expr.FieldExpression{FieldName: "birthDate", Permissive: true}, + input: system.Collection{patientMissingName, "hello"}, + wantCollection: system.Collection{patientBirthDay}, + }, + { + name: "input contains non-resource items", + fieldExp: &expr.FieldExpression{FieldName: "birthDate"}, + input: system.Collection{patientMissingName, "hello"}, + wantErr: fhirpath.ErrInvalidField, + }, + { + name: "accessing value field of primitive returns System primitive", + fieldExp: &expr.FieldExpression{FieldName: "value"}, + input: system.Collection{fullName.Family}, + wantCollection: system.Collection{system.String("Caesar")}, + }, + { + name: "accessing value of code returns System primitive", + fieldExp: &expr.FieldExpression{FieldName: "value"}, + input: system.Collection{patientContactPoint[0].System}, + wantCollection: system.Collection{system.String("phone")}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.fieldExp.Evaluate(&expr.Context{}, tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("FieldExpression.Evaluate(%s) raised unexpected error: got %v, want %v", tc.input, err, tc.wantErr) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("FieldExpression.Evaluate(%s) returned unexpected diff (-want, +got):\n%s", tc.input, diff) + } + }) + } +} + +func TestFieldExpression_ValidInput_GetsField(t *testing.T) { + tm := time.Now() + device1 := &device_go_proto.Device{ + Id: fhir.RandomID(), + } + patient1 := &patient_go_proto.Patient{ + Id: fhir.RandomID(), + } + entries := []*bundle_and_contained_resource_go_proto.Bundle_Entry{ + bundle.NewPostEntry(device1), bundle.NewPostEntry(patient1), + } + testCases := []struct { + name string + input system.Collection + field string + want system.Collection + }{ + { + name: "Value field from primitive element", + input: system.Collection{fhir.Integer(32)}, + field: "value", + want: system.Collection{system.Integer(32)}, + }, { + name: "Value field from valueX type", + input: system.Collection{extension.New("url", fhir.Boolean(true))}, + field: "value", + want: system.Collection{fhir.Boolean(true)}, + }, { + name: "Repeated field returns collection of repeated elements", + input: system.Collection{bundle.NewCollection( + bundle.WithEntries(entries...), + )}, + field: "entry", + want: system.Collection( + slices.MustConvert[any](entries), + ), + }, { + name: "Field on empty input returns empty", + input: nil, + field: "value", + want: system.Collection{}, + }, { + name: "Bundle entry contained resource", + input: system.Collection{bundle.NewPostEntry(device1)}, + field: "resource", + want: system.Collection{device1}, + }, { + name: "Reference using URI", + input: system.Collection{&dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: fhir.String("https://some-url.com"), + }, + }}, + field: "reference", + want: system.Collection{fhir.String("https://some-url.com")}, + }, { + name: "Reference Patient without history ID", + input: system.Collection{&dtpb.Reference{ + Reference: &dtpb.Reference_PatientId{ + PatientId: &dtpb.ReferenceId{ + Value: "12345", + }, + }, + }}, + field: "reference", + want: system.Collection{fhir.String("Patient/12345")}, + }, { + name: "Reference using fragment", + input: system.Collection{&dtpb.Reference{ + Reference: &dtpb.Reference_Fragment{ + Fragment: fhir.String("p0"), + }, + }}, + field: "reference", + want: system.Collection{fhir.String("#p0")}, + }, { + name: "Time using value_us", + input: system.Collection{fhir.Time(tm)}, + field: "value", + want: system.Collection{system.String(fhirconv.TimeToString(fhir.Time(tm)))}, + }, { + name: "Date using value_us", + input: system.Collection{fhir.Date(tm)}, + field: "value", + want: system.Collection{system.String(fhirconv.DateToString(fhir.Date(tm)))}, + }, { + name: "DateTime using value_us", + input: system.Collection{fhir.DateTime(tm)}, + field: "value", + want: system.Collection{system.String(fhirconv.DateTimeToString(fhir.DateTime(tm)))}, + }, { + name: "Instant using value_us", + input: system.Collection{fhir.Instant(tm)}, + field: "value", + want: system.Collection{system.String(fhirconv.InstantToString(fhir.Instant(tm)))}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sut := &expr.FieldExpression{ + FieldName: tc.field, + } + + got, err := sut.Evaluate(expr.InitializeContext(tc.input), tc.input) + if err != nil { + t.Fatalf("FieldExpression.Evaluate(%v): got unexpected err %v", tc.name, err) + } + + if diff := cmp.Diff(got, tc.want, protocmp.Transform()); diff != "" { + t.Errorf("FieldExpression.Evaluate(%v): (-got,+want):\n%v", tc.name, diff) + } + }) + } +} + +func TestTypeExpression_Filters_DesiredType(t *testing.T) { + medicationRequest := &mrpb.MedicationRequest{ + Reported: &mrpb.MedicationRequest_ReportedX{ + Choice: &mrpb.MedicationRequest_ReportedX_Boolean{ + Boolean: fhir.Boolean(true), + }, + }, + } + patient := &ppb.Patient{ + Id: fhir.ID("123"), + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{ + fhir.String("Julius"), + }, + Family: fhir.String("Caesar"), + }, + }, + } + + testCases := []struct { + name string + typeExp *expr.TypeExpression + input system.Collection + wantCollection system.Collection + shouldError bool + }{ + { + name: "input contains only resource", + typeExp: &expr.TypeExpression{Type: "Patient"}, + input: system.Collection{patient}, + wantCollection: system.Collection{patient}, + }, + { + name: "input contains more than desired resource", + typeExp: &expr.TypeExpression{Type: "Patient"}, + input: system.Collection{patient, medicationRequest}, + wantCollection: system.Collection{patient}, + }, + { + name: "input doesn't contain desired resource", + typeExp: &expr.TypeExpression{Type: "MedicationRequest"}, + input: system.Collection{patient}, + wantCollection: system.Collection{}, + }, + { + name: "desired type has 2 words", + typeExp: &expr.TypeExpression{Type: "MedicationRequest"}, + input: system.Collection{medicationRequest, patient}, + wantCollection: system.Collection{medicationRequest}, + }, + { + name: "input collection has non-resource types with desired name", + typeExp: &expr.TypeExpression{Type: "Patient"}, + input: system.Collection{"Patient", struct{ Patient string }{Patient: "Peter Griffin"}}, + wantCollection: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.typeExp.Evaluate(&expr.Context{}, tc.input) + + if err != nil { + t.Fatalf("TypeExpression.Evaluate(%s) got unexpected error: %s", tc.input, err) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("TypeExpression.Evaluate(%s) returned unexpected diff (-want, +got):\n%s", tc.input, diff) + } + }) + } +} + +func TestExpressionSequence_EvaluatesInSequence(t *testing.T) { + mock := &exprtest.MockExpression{ + Eval: func(ctx *expr.Context, input system.Collection) (system.Collection, error) { + wasCalled := input[0].(int) + return system.Collection{wasCalled + 1}, nil + }, + } + + sequence := &expr.ExpressionSequence{[]expr.Expression{mock, mock, mock, mock}} + result, err := sequence.Evaluate(&expr.Context{}, system.Collection{0}) + + if err != nil { + t.Fatalf("ExpressionSequence.Evaluate raised unexpected error: %v", err) + } + if got, want := result[0].(int), len(sequence.Expressions); got != want { + t.Errorf("ExpressionSequence.Evaluate incorrectly accumulated values, got: %v, want: %v", got, want) + } +} + +func TestExpressionSequence_EvaluateRaisesError(t *testing.T) { + sequence := &expr.ExpressionSequence{[]expr.Expression{exprtest.Return(), exprtest.Return(), exprtest.Error(errMock), exprtest.Return()}} + + _, err := sequence.Evaluate(&expr.Context{}, system.Collection{}) + + if err == nil { + t.Fatal("ExpressionSequence.Evaluate didn't raise error when expected") + } +} + +func TestLiteralExpression_EvaluateReturnsLiteral(t *testing.T) { + testCases := []struct { + name string + literalExp *expr.LiteralExpression + inputCollection system.Collection + wantCollection system.Collection + }{ + { + name: "string literal expression without input", + literalExp: &expr.LiteralExpression{system.String("string")}, + inputCollection: system.Collection{"some input"}, + wantCollection: system.Collection{system.String("string")}, + }, + { + name: "null literal expression", + literalExp: &expr.LiteralExpression{}, + inputCollection: system.Collection{}, + wantCollection: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.literalExp.Evaluate(&expr.Context{}, tc.inputCollection) + + if err != nil { + t.Fatalf("LiteralExpression.Evaluate raised unexpected error %v", err) + } + if diff := cmp.Diff(tc.wantCollection, got); diff != "" { + t.Errorf("LiteralExpression.Evaluate returned unexpected diff: (-want, +got):\n%s", diff) + } + }) + } +} + +func TestIndexExpression_ReturnsIndex(t *testing.T) { + givenName := []*dtpb.String{ + fhir.String("He"), + fhir.String("Who"), + fhir.String("Must"), + fhir.String("Not"), + fhir.String("Be"), + } + familyName := fhir.String("Named") + voldemort := []*dtpb.HumanName{ + { + Given: givenName, + Family: familyName, + }, + { + Given: []*dtpb.String{ + fhir.String("Tom"), + fhir.String("Marvolo"), + }, + Family: fhir.String("Riddle"), + }, + } + + testCases := []struct { + name string + indexExpr *expr.IndexExpression + inputCollection system.Collection + wantCollection system.Collection + }{ + { + name: "indexing a proto string array", + indexExpr: &expr.IndexExpression{&expr.LiteralExpression{system.Integer(1)}}, + inputCollection: slices.MustConvert[any](givenName), + wantCollection: system.Collection{fhir.String("Who")}, + }, + { + name: "indexing a proto HumanName", + indexExpr: &expr.IndexExpression{&expr.LiteralExpression{system.Integer(0)}}, + inputCollection: slices.MustConvert[any](voldemort), + wantCollection: system.Collection{voldemort[0]}, + }, + { + name: "index out of bounds", + indexExpr: &expr.IndexExpression{&expr.LiteralExpression{system.Integer(5)}}, + inputCollection: slices.MustConvert[any](givenName), + wantCollection: system.Collection{}, + }, + { + name: "negative index", + indexExpr: &expr.IndexExpression{&expr.LiteralExpression{system.Integer(-1)}}, + inputCollection: slices.MustConvert[any](givenName), + wantCollection: system.Collection{}, + }, + { + name: "empty input collection", + indexExpr: &expr.IndexExpression{&expr.LiteralExpression{system.Integer(0)}}, + inputCollection: system.Collection{}, + wantCollection: system.Collection{}, + }, + { + name: "index that evaluates to empty returns empty", + indexExpr: &expr.IndexExpression{exprtest.Return()}, + inputCollection: slices.MustConvert[any](givenName), + wantCollection: system.Collection{}, + }, + } + + for _, tc := range testCases { + got, err := tc.indexExpr.Evaluate(&expr.Context{}, tc.inputCollection) + + if err != nil { + t.Fatalf("IndexExpression.Evaluate raised unexpected error %v", err) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("IndexExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff) + } + } +} + +func TestIndexExpression_EvaluateRaisesError(t *testing.T) { + testCases := []struct { + name string + indexExpr *expr.IndexExpression + inputCollection system.Collection + }{ + { + name: "evaluate index raises an error", + indexExpr: &expr.IndexExpression{Index: exprtest.Error(errMock)}, + inputCollection: system.Collection{}, + }, + { + name: "index expression doesn't evaluate to a system type", + indexExpr: &expr.IndexExpression{Index: exprtest.Return(1)}, + inputCollection: system.Collection{}, + }, + { + name: "index doesn't evaluate to an integer", + indexExpr: &expr.IndexExpression{Index: exprtest.Return("not an integer")}, + inputCollection: system.Collection{}, + }, + { + name: "index expression evaluates to multiple entries", + indexExpr: &expr.IndexExpression{Index: exprtest.Return(system.Integer(1), system.Integer(2))}, + inputCollection: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.indexExpr.Evaluate(&expr.Context{}, tc.inputCollection) + + if err == nil { + t.Fatalf("IndexExpression.Evaluate didn't return error when expected") + } + }) + } +} + +func TestEqualityExpression_ReturnsResult(t *testing.T) { + testCases := []struct { + name string + inputCollection system.Collection + equalityExpr *expr.EqualityExpression + wantCollection system.Collection + }{ + { + name: "one empty collection", + inputCollection: system.Collection{}, + equalityExpr: &expr.EqualityExpression{exprtest.Return(), exprtest.Return("one"), false}, + wantCollection: system.Collection{}, + }, + { + name: "comparing with != operator", + inputCollection: system.Collection{}, + equalityExpr: &expr.EqualityExpression{exprtest.Return(system.String("abc")), exprtest.Return(system.String("abcd")), true}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.equalityExpr.Evaluate(&expr.Context{}, tc.inputCollection) + + if err != nil { + t.Fatalf("EqualityExpression.Evaluate raised unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantCollection, got); diff != "" { + t.Errorf("EqualityExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestEqualityExpression_RaisesError(t *testing.T) { + testCases := []struct { + name string + equalityExpr *expr.EqualityExpression + }{ + { + name: "subexpression one errors", + equalityExpr: &expr.EqualityExpression{exprtest.Error(errMock), exprtest.Return(system.Boolean(true)), false}, + }, + { + name: "subexpression two errors", + equalityExpr: &expr.EqualityExpression{exprtest.Return(system.Boolean(true)), exprtest.Error(errMock), false}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.equalityExpr.Evaluate(&expr.Context{}, system.Collection{}) + + if err == nil { + t.Fatalf("EqualityExpression.Evaluate didn't propagate error when it should have") + } + }) + } +} + +func TestIsExpression_ReturnsResult(t *testing.T) { + testCases := []struct { + name string + expr *expr.IsExpression + wantCollection system.Collection + }{ + { + name: "returns boolean", + expr: &expr.IsExpression{exprtest.Return(fhir.String("str")), reflection.MustCreateTypeSpecifier("FHIR", "string")}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns empty collection", + expr: &expr.IsExpression{exprtest.Return(), reflection.MustCreateTypeSpecifier("FHIR", "string")}, + wantCollection: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.expr.Evaluate(&expr.Context{}, []any{}) + + if err != nil { + t.Fatalf("IsExpression.Evaluate returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("IsExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestIsExpression_ReturnsError(t *testing.T) { + testCases := []struct { + name string + expr *expr.IsExpression + }{ + { + name: "subexpression errors", + expr: &expr.IsExpression{exprtest.Error(errors.New("some error")), reflection.TypeSpecifier{}}, + }, + { + name: "subexpression evaluates to non-singleton", + expr: &expr.IsExpression{exprtest.Return(system.Boolean(true), system.Boolean(true)), reflection.TypeSpecifier{}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.expr.Evaluate(&expr.Context{}, []any{}) + + if err == nil { + t.Fatalf("IsExpression.Evaluate doesn't return error when expected") + } + }) + } +} + +func TestAsExpression_ReturnsResult(t *testing.T) { + deceased := &ppb.Patient_DeceasedX{ + Choice: &ppb.Patient_DeceasedX_Boolean{ + Boolean: fhir.Boolean(true), + }, + } + testCases := []struct { + name string + expr *expr.AsExpression + wantCollection system.Collection + }{ + { + name: "input is of specified type (returns input)", + expr: &expr.AsExpression{exprtest.Return(fhir.Code("#blessed")), reflection.MustCreateTypeSpecifier("FHIR", "code")}, + wantCollection: system.Collection{fhir.Code("#blessed")}, + }, + { + name: "input is not of specified type (returns empty)", + expr: &expr.AsExpression{exprtest.Return(fhir.Integer(12)), reflection.MustCreateTypeSpecifier("FHIR", "string")}, + wantCollection: system.Collection{}, + }, + { + name: "input is empty collection (returns empty)", + expr: &expr.AsExpression{exprtest.Return(), reflection.MustCreateTypeSpecifier("FHIR", "string")}, + wantCollection: system.Collection{}, + }, + { + name: "input is a polymorphic oneOf type", + expr: &expr.AsExpression{exprtest.Return(deceased), reflection.MustCreateTypeSpecifier("FHIR", "boolean")}, + wantCollection: system.Collection{fhir.Boolean(true)}, + }, + { + name: "input is a system type", + expr: &expr.AsExpression{exprtest.Return(system.Boolean(true)), reflection.MustCreateTypeSpecifier("System", "Boolean")}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.expr.Evaluate(&expr.Context{}, []any{}) + + if err != nil { + t.Fatalf("AsExpression.Evaluate returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("AsExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestAsExpression_ReturnsError(t *testing.T) { + testCases := []struct { + name string + expr *expr.AsExpression + }{ + { + name: "subexpression errors", + expr: &expr.AsExpression{exprtest.Error(errors.New("some error")), reflection.TypeSpecifier{}}, + }, + { + name: "subexpression evaluates to non-singleton", + expr: &expr.AsExpression{exprtest.Return(system.Boolean(true), system.Boolean(true)), reflection.TypeSpecifier{}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.expr.Evaluate(&expr.Context{}, []any{}) + + if err == nil { + t.Fatalf("AsExpression.Evaluate doesn't return error when expected") + } + }) + } +} + +func TestBooleanExpression_ReturnsResult(t *testing.T) { + testCases := []struct { + name string + expr *expr.BooleanExpression + wantCollection system.Collection + }{ + { + name: "[and]both expressions return true", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(system.Boolean(true)), expr.And}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[and]both expressions return booleans (one false)", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(system.Boolean(false)), expr.And}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "[and]both expressions return empty", + expr: &expr.BooleanExpression{exprtest.Return(), exprtest.Return(), expr.And}, + wantCollection: system.Collection{}, + }, + { + name: "[and]one expression is false while other is empty", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(false)), exprtest.Return(), expr.And}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "[and]one expression is true while other is empty", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(), expr.And}, + wantCollection: system.Collection{}, + }, + { + name: "[and]singleton evaluation of collections (expressions are not booleans)", + expr: &expr.BooleanExpression{exprtest.Return(system.String("hi")), exprtest.Return(system.String("hello")), expr.And}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[or]both expressions return true", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(system.Boolean(true)), expr.Or}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[or]both expressions return booleans (one true, one false)", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(system.Boolean(false)), expr.Or}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[or]both expressions return empty", + expr: &expr.BooleanExpression{exprtest.Return(), exprtest.Return(), expr.Or}, + wantCollection: system.Collection{}, + }, + { + name: "[or]one expression is true while other is empty", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(), expr.Or}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[or]one expression is false while other is empty", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(false)), exprtest.Return(), expr.Or}, + wantCollection: system.Collection{}, + }, + { + name: "[or]correctly compares proto booleans", + expr: &expr.BooleanExpression{exprtest.Return(fhir.Boolean(true)), exprtest.Return(fhir.Boolean(false)), expr.Or}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[or]singleton evaluation of collections (expressions are not booleans)", + expr: &expr.BooleanExpression{exprtest.Return(system.String("hi")), exprtest.Return(), expr.Or}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[xor]both expressions are equal booleans", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(system.Boolean(true)), expr.Xor}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "[xor] both expressions are inequal booleans", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(false)), exprtest.Return(system.Boolean(true)), expr.Xor}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[xor]one collection is empty", + expr: &expr.BooleanExpression{exprtest.Return(), exprtest.Return(system.Boolean(true)), expr.Xor}, + wantCollection: system.Collection{}, + }, + { + name: "[implies]false implies false", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(false)), exprtest.Return(system.Boolean(false)), expr.Implies}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[implies]false implies true", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(false)), exprtest.Return(system.Boolean(true)), expr.Implies}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[implies]false implies empty", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(false)), exprtest.Return(), expr.Implies}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[implies]true implies false", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(system.Boolean(false)), expr.Implies}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "[implies]true implies true", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(system.Boolean(true)), expr.Implies}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[implies]empty implies true", + expr: &expr.BooleanExpression{exprtest.Return(), exprtest.Return(system.Boolean(true)), expr.Implies}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[implies]returns empty", + expr: &expr.BooleanExpression{exprtest.Return(true), exprtest.Return(), expr.Implies}, + wantCollection: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.expr.Evaluate(&expr.Context{}, system.Collection{}) + + if err != nil { + t.Fatalf("BooleanExpression.Evaluate returned unexpected error: %v", err) + } + if !cmp.Equal(got, tc.wantCollection) { + t.Errorf("BooleanExpression.Evaluate returned unexpected result: got %v, want %v", got, tc.wantCollection) + } + }) + } +} + +func TestComparisonExpression_ReturnsResult(t *testing.T) { + qty, _ := system.ParseQuantity("23.3", "kg") + testErr := errors.New("some error") + testCases := []struct { + name string + expr *expr.ComparisonExpression + wantCollection system.Collection + wantErr error + }{ + { + name: "[Lt] evaluates string comparison", + expr: &expr.ComparisonExpression{ + exprtest.Return(system.String("abc")), + exprtest.Return(system.String("def")), + expr.Lt, + }, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns empty if either collection is empty", + expr: &expr.ComparisonExpression{ + exprtest.Return(), + exprtest.Return(system.String("def")), + expr.Lt, + }, + wantCollection: system.Collection{}, + }, + { + name: "[Gt] correctly compares Date values", + expr: &expr.ComparisonExpression{ + exprtest.Return(system.MustParseDate("2023-07-22")), + exprtest.Return(system.MustParseDate("2023-07-21")), + expr.Gt, + }, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[Gte] correctly compares Time value with different precision", + expr: &expr.ComparisonExpression{ + exprtest.Return(system.MustParseTime("08:31")), + exprtest.Return(system.MustParseTime("08:30:30")), + expr.Gte, + }, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[Lte] correctly compares quantity value", + expr: &expr.ComparisonExpression{ + exprtest.Return(qty), + exprtest.Return(qty), + expr.Lte, + }, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "[Gte] returns empty for mismatched precision on DateTime", + expr: &expr.ComparisonExpression{ + exprtest.Return(system.MustParseDateTime("2020-12-20T08:30")), + exprtest.Return(system.MustParseDateTime("2020-12-20T08:30:05")), + expr.Gte, + }, + wantCollection: system.Collection{}, + }, + { + name: "[Lt] correctly compares a Date with a DateTime", + expr: &expr.ComparisonExpression{ + exprtest.Return(system.MustParseDate("2020-12-20")), + exprtest.Return(system.MustParseDateTime("2020-12-21T")), + expr.Lt, + }, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "returns error for comparison of invalid types", + expr: &expr.ComparisonExpression{ + exprtest.Return(system.String("100")), + exprtest.Return(system.Integer(100)), + expr.Gte, + }, + wantErr: system.ErrTypeMismatch, + }, + { + name: "Propogates error from one of the expressions", + expr: &expr.ComparisonExpression{ + exprtest.Return(), + exprtest.Error(testErr), + expr.Lt, + }, + wantErr: testErr, + }, + { + name: "returns error if either collection is not a singleton", + expr: &expr.ComparisonExpression{ + exprtest.Return(system.String("abc"), system.String("abc")), + exprtest.Return(system.String("hi")), + expr.Lt, + }, + wantErr: expr.ErrNotSingleton, + }, + { + name: "returns error if a non-system type is returned", + expr: &expr.ComparisonExpression{ + exprtest.Return(123), + exprtest.Return(system.Integer(234)), + expr.Lt, + }, + wantErr: system.ErrCantBeCast, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.expr.Evaluate(&expr.Context{}, system.Collection{}) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("ComparisonExpression.Evaluate returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if !cmp.Equal(got, tc.wantCollection) { + t.Errorf("ComparisonExpression.Evaluate returned unexpected result: got %v, want %v", got, tc.wantCollection) + } + }) + } +} + +func TestAndExpression_ReturnsError(t *testing.T) { + testCases := []struct { + name string + expr *expr.BooleanExpression + }{ + { + name: "subexpression errors", + expr: &expr.BooleanExpression{exprtest.Error(errMock), exprtest.Return(system.Boolean(true)), expr.And}, + }, + { + name: "expression returns non-singleton", + expr: &expr.BooleanExpression{exprtest.Return(system.Boolean(true)), exprtest.Return(system.Boolean(true), system.Boolean(true)), expr.And}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.expr.Evaluate(&expr.Context{}, system.Collection{}) + + if err == nil { + t.Fatalf("AndExpression.Evaluate didn't return error when expected") + } + }) + } +} + +func TestArithmeticExpression_ReturnsResult(t *testing.T) { + testCases := []struct { + name string + expr *expr.ArithmeticExpression + want system.Collection + wantErr error + }{ + { + name: "adds two system types", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.String("hello ")), + Right: exprtest.Return(system.String("world")), + Op: expr.EvaluateAdd, + }, + want: system.Collection{system.String("hello world")}, + }, + { + name: "adds two proto types", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(fhir.MustParseDate("2020-02-12")), + Right: exprtest.Return(fhir.UCUMQuantity(1, "day")), + Op: expr.EvaluateAdd, + }, + want: system.Collection{system.MustParseDate("2020-02-13")}, + }, + { + name: "adds a integer with a decimal", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(25)), + Right: exprtest.Return(system.Decimal(decimal.NewFromFloat(1.24))), + Op: expr.EvaluateAdd, + }, + want: system.Collection{system.Decimal(decimal.NewFromFloat(26.24))}, + }, + { + name: "subtracts quantity from date", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.MustParseDate("2020-12")), + Right: exprtest.Return(system.MustParseQuantity("35", "days")), + Op: expr.EvaluateSub, + }, + want: system.Collection{system.MustParseDate("2020-11")}, + }, + { + name: "returns empty if either collection is empty", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(), + Right: exprtest.Return(system.String("hell0")), + Op: expr.EvaluateAdd, + }, + want: system.Collection{}, + }, + { + name: "returns error if either collection has multiple elements", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(1), system.Integer(2)), + Right: exprtest.Return(system.Integer(2)), + Op: expr.EvaluateAdd, + }, + wantErr: expr.ErrNotSingleton, + }, + { + name: "returns error if types can not be added", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(2)), + Right: exprtest.Return(system.Boolean(true)), + Op: expr.EvaluateAdd, + }, + wantErr: system.ErrTypeMismatch, + }, + { + name: "returns empty if integer addition overflows", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(math.MaxInt32)), + Right: exprtest.Return(system.Integer(1)), + Op: expr.EvaluateAdd, + }, + want: system.Collection{}, + }, + { + name: "returns empty if integer subtraction overflows", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(math.MinInt32)), + Right: exprtest.Return(system.Integer(1)), + Op: expr.EvaluateSub, + }, + want: system.Collection{}, + }, + { + name: "multiplies decimals together", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Decimal(decimal.NewFromFloat(0.25))), + Right: exprtest.Return(system.Decimal(decimal.NewFromFloat(0.25))), + Op: expr.EvaluateMul, + }, + want: system.Collection{system.Decimal(decimal.NewFromFloat(0.0625))}, + }, + { + name: "implicitly converts integer to decimal", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Decimal(decimal.NewFromFloat(1.2))), + Right: exprtest.Return(system.Integer(2)), + Op: expr.EvaluateMul, + }, + want: system.Collection{system.Decimal(decimal.NewFromFloat(2.4))}, + }, + { + name: "multiplies integers together", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(12)), + Right: exprtest.Return(system.Integer(12)), + Op: expr.EvaluateMul, + }, + want: system.Collection{system.Integer(144)}, + }, + { + name: "returns empty if multiplication causes integer overflow", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(math.MaxInt32)), + Right: exprtest.Return(system.Integer(2)), + Op: expr.EvaluateMul, + }, + want: system.Collection{}, + }, + { + name: "returns error on a type mismatch", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(2)), + Right: exprtest.Return(system.String("a")), + Op: expr.EvaluateMul, + }, + wantErr: system.ErrTypeMismatch, + }, + { + name: "returns decimal on integer division", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(5)), + Right: exprtest.Return(system.Integer(2)), + Op: expr.EvaluateDiv, + }, + want: system.Collection{system.Decimal(decimal.NewFromFloat(2.5))}, + }, + { + name: "performs floor division", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(5)), + Right: exprtest.Return(system.Integer(2)), + Op: expr.EvaluateFloorDiv, + }, + want: system.Collection{system.Integer(2)}, + }, + { + name: "performs mod between decimals", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Decimal(decimal.NewFromFloat(5.5))), + Right: exprtest.Return(system.Decimal(decimal.NewFromFloat(0.7))), + Op: expr.EvaluateMod, + }, + want: system.Collection{system.Decimal(decimal.NewFromFloat(0.6))}, + }, + { + name: "performs mod between integers", + expr: &expr.ArithmeticExpression{ + Left: exprtest.Return(system.Integer(19)), + Right: exprtest.Return(system.Integer(9)), + Op: expr.EvaluateMod, + }, + want: system.Collection{system.Integer(1)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.expr.Evaluate(&expr.Context{}, []any{}) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("ArithmeticExpression.Evaluate returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("ArithmeticExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestConcatExpression_AddsStrings(t *testing.T) { + testCases := []struct { + name string + expr *expr.ConcatExpression + want system.Collection + wantErr error + }{ + { + name: "concatenates two strings", + expr: &expr.ConcatExpression{ + Left: exprtest.Return(system.String("hello ")), + Right: exprtest.Return(system.String("world")), + }, + want: system.Collection{system.String("hello world")}, + }, + { + name: "concatenates fhir string with system string", + expr: &expr.ConcatExpression{ + Left: exprtest.Return(fhir.String("abc")), + Right: exprtest.Return(system.String("def")), + }, + want: system.Collection{system.String("abcdef")}, + }, + { + name: "if left collection is empty, returns the other string", + expr: &expr.ConcatExpression{ + Left: exprtest.Return(system.String("Hello")), + Right: exprtest.Return(), + }, + want: system.Collection{system.String("Hello")}, + }, + { + name: "if right collection is empty, returns the other string", + expr: &expr.ConcatExpression{ + Left: exprtest.Return(), + Right: exprtest.Return(system.String("Hello")), + }, + want: system.Collection{system.String("Hello")}, + }, + { + name: "returns an error if collection has multiple elements", + expr: &expr.ConcatExpression{ + Left: exprtest.Return(system.String("1"), system.String("2")), + Right: exprtest.Return(system.String("3")), + }, + wantErr: expr.ErrNotSingleton, + }, + { + name: "returns an error if a string is not returned", + expr: &expr.ConcatExpression{ + Left: exprtest.Return(system.Integer(1)), + Right: exprtest.Return(system.String("3")), + }, + wantErr: expr.ErrInvalidType, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.expr.Evaluate(&expr.Context{}, system.Collection{}) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("ConcatExpression.Evaluate returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("ConcatExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestExternalConstantExpression(t *testing.T) { + testCases := []struct { + name string + expr *expr.ExternalConstantExpression + context *expr.Context + want system.Collection + wantErr error + }{ + { + name: "returns constant", + expr: &expr.ExternalConstantExpression{Identifier: "value"}, + context: &expr.Context{ + ExternalConstants: map[string]any{"value": system.String("some string")}, + }, + want: system.Collection{system.String("some string")}, + }, + { + name: "returns error if constant doesn't exist", + expr: &expr.ExternalConstantExpression{Identifier: "value"}, + context: &expr.Context{ + ExternalConstants: map[string]any{}, + }, + wantErr: expr.ErrConstantNotFound, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.expr.Evaluate(tc.context, system.Collection{}) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("ExternalConstantExpression.Evaluate returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("ExternalConstantExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestNegationExpression(t *testing.T) { + testCases := []struct { + name string + expr *expr.NegationExpression + want system.Collection + wantErr error + }{ + { + name: "negates integer", + expr: &expr.NegationExpression{Expr: exprtest.Return(system.Integer(4))}, + want: system.Collection{system.Integer(-4)}, + }, + { + name: "negates decimal", + expr: &expr.NegationExpression{Expr: exprtest.Return(system.Decimal(decimal.NewFromFloat(1.5)))}, + want: system.Collection{system.Decimal(decimal.NewFromFloat(-1.5))}, + }, + { + name: "negates quantity", + expr: &expr.NegationExpression{Expr: exprtest.Return(system.MustParseQuantity("2.5", "kg"))}, + want: system.Collection{system.MustParseQuantity("-2.5", "kg")}, + }, + { + name: "negates proto integer", + expr: &expr.NegationExpression{Expr: exprtest.Return(fhir.Integer(-1))}, + want: system.Collection{system.Integer(1)}, + }, + { + name: "raises error on negating a collection", + expr: &expr.NegationExpression{Expr: exprtest.Return(system.Integer(1), system.Integer(2))}, + wantErr: expr.ErrNotSingleton, + }, + { + name: "raises error if a non-number type is negated", + expr: &expr.NegationExpression{Expr: exprtest.Return(system.String("1"))}, + wantErr: expr.ErrInvalidType, + }, + { + name: "raises error if complex type is negated", + expr: &expr.NegationExpression{Expr: exprtest.Return(fhir.Ratio(20, 10))}, + wantErr: expr.ErrInvalidType, + }, + { + name: "passes through empty collection", + expr: &expr.NegationExpression{Expr: exprtest.Return()}, + want: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.expr.Evaluate(&expr.Context{}, system.Collection{}) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("NegationExpression.Evaluate returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("NegationExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} diff --git a/fhirpath/internal/expr/exprtest/doc.go b/fhirpath/internal/expr/exprtest/doc.go new file mode 100644 index 0000000..8594ccf --- /dev/null +++ b/fhirpath/internal/expr/exprtest/doc.go @@ -0,0 +1,5 @@ +/* +Package exprtest provides some useful test dummies to make it +easier to mock expressions to return a desired result. +*/ +package exprtest diff --git a/fhirpath/internal/expr/exprtest/doubles.go b/fhirpath/internal/expr/exprtest/doubles.go new file mode 100644 index 0000000..90bdedc --- /dev/null +++ b/fhirpath/internal/expr/exprtest/doubles.go @@ -0,0 +1,37 @@ +package exprtest + +import ( + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// MockExpression is a test double expression that calls +// the contained function when evaluated. +type MockExpression struct { + Eval func(*expr.Context, system.Collection) (system.Collection, error) +} + +// Evaluate calls the contained Eval function. +func (e *MockExpression) Evaluate(ctx *expr.Context, input system.Collection) (system.Collection, error) { + return e.Eval(ctx, input) +} + +// Error creates a MockExpression that returns the provided +// error when evaluated. +func Error(input error) *MockExpression { + return &MockExpression{ + func(*expr.Context, system.Collection) (system.Collection, error) { + return nil, input + }, + } +} + +// Return creates a MockExpression that returns the provided +// inputs when evaluated. +func Return(out ...any) *MockExpression { + return &MockExpression{ + func(*expr.Context, system.Collection) (system.Collection, error) { + return out, nil + }, + } +} diff --git a/fhirpath/internal/expr/operators.go b/fhirpath/internal/expr/operators.go new file mode 100644 index 0000000..f3ef506 --- /dev/null +++ b/fhirpath/internal/expr/operators.go @@ -0,0 +1,29 @@ +package expr + +// Operator constants. +const ( + Equals = "=" + NotEquals = "!=" + Equivalence = "~" + Inequivalence = "!~" + Is = "is" + As = "as" + And = "and" + Or = "or" + Xor = "xor" + Implies = "implies" + Lt = "<" + Gt = ">" + Lte = "<=" + Gte = ">=" + Add = "+" + Sub = "-" + Concat = "&" + Mul = "*" + Div = "/" + FloorDiv = "div" + Mod = "mod" +) + +// Operator represents a valid expression operator. +type Operator string diff --git a/fhirpath/internal/funcs/doc.go b/fhirpath/internal/funcs/doc.go new file mode 100644 index 0000000..3d78d71 --- /dev/null +++ b/fhirpath/internal/funcs/doc.go @@ -0,0 +1,6 @@ +/* +Package funcs provides the implementations for all +base FHIRPath functions. Provides a wrapper, and function table +for compilation. +*/ +package funcs diff --git a/fhirpath/internal/funcs/function.go b/fhirpath/internal/funcs/function.go new file mode 100644 index 0000000..6ae174a --- /dev/null +++ b/fhirpath/internal/funcs/function.go @@ -0,0 +1,94 @@ +package funcs + +import ( + "errors" + "fmt" + "reflect" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +var ( + errNotFunc = errors.New("value is not a function") + errMissingArgs = errors.New("missing arguments") + errInvalidParams = errors.New("invalid input parameters") + errInvalidReturn = errors.New("invalid function return signature") +) + +var notImplemented = Function{Func: unimplemented} + +// FHIRPathFunc is the common abstraction for all function types +// supported by FHIRPath. +type FHIRPathFunc func(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) + +// Function contains the FHIRPathFunction, along with metadata +type Function struct { + Func FHIRPathFunc + MinArity int + MaxArity int + IsTypeFunction bool +} + +// ToFunction takes in a function with any arguments and attempts to +// convert it to a functions.Function type. If the conversion is successful, +// the new function will assert the argument expressions resolve to the original +// argument types. +func ToFunction(fn any) (Function, error) { + rv := reflect.ValueOf(fn) + if err := validateFunc(rv); err != nil { + return Function{}, fmt.Errorf("constructing FHIRPathFunction: %w", err) + } + + arity := rv.Type().NumIn() - 1 // 'True' arity, as the first argument is the input Collection. + fhirpathFunc := func(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + if len(args) != arity { + return nil, fmt.Errorf("%w: function expects %v arguments, received %v", impl.ErrWrongArity, arity, len(args)) + } + // ensure the arguments match the function signature. + funcArgs := []reflect.Value{reflect.ValueOf(input)} + for i, exp := range args { + result, err := exp.Evaluate(ctx, input) + if err != nil { + return nil, err + } + if len(result) != 1 { + return nil, fmt.Errorf("%w: doesn't return singleton", impl.ErrInvalidReturnType) + } + if expectedType, gotType := rv.Type().In(i+1), reflect.TypeOf(result[0]); !gotType.AssignableTo(expectedType) { + return nil, fmt.Errorf("%w: got type '%s' when type '%s' was expected", impl.ErrInvalidReturnType, gotType.String(), expectedType.Name()) + } + funcArgs = append(funcArgs, reflect.ValueOf(result[0])) + } + output := rv.Call(funcArgs) + if err, ok := output[1].Interface().(error); ok { + return output[0].Interface().(system.Collection), err + } + return output[0].Interface().(system.Collection), nil + } + return Function{fhirpathFunc, arity, arity, false}, nil +} + +// validateFunc verifies that the input reflect value represents a +// valid FHIRPath function. If not, it returns an error. +func validateFunc(rv reflect.Value) error { + if rv.Kind() != reflect.Func { + return errNotFunc + } + errs := []error{} + if rv.Type().NumIn() < 1 { + errs = append(errs, errMissingArgs) + } else if rv.Type().In(0) != reflect.TypeOf(system.Collection{}) { + errs = append(errs, errInvalidParams) + } + if rv.Type().NumOut() != 2 || rv.Type().Out(0) != reflect.TypeOf(system.Collection{}) || rv.Type().Out(1).Name() != "error" { + errs = append(errs, errInvalidReturn) + } + return errors.Join(errs...) +} + +// unimplemented is a no-op placeholder function that satisfies the FHIRPathFunction contract +func unimplemented(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + return nil, fmt.Errorf("function not yet implemented") +} diff --git a/fhirpath/internal/funcs/function_table.go b/fhirpath/internal/funcs/function_table.go new file mode 100644 index 0000000..e4de603 --- /dev/null +++ b/fhirpath/internal/funcs/function_table.go @@ -0,0 +1,23 @@ +package funcs + +import ( + "fmt" +) + +// FunctionTable is the data structure used to store +// valid FHIRPath functions, and maps their case-sensitive +// names. +type FunctionTable map[string]Function + +// Register attempts to add a given function to the FunctionTable t. +func (t FunctionTable) Register(name string, fn any) error { + if _, ok := t[name]; ok { + return fmt.Errorf("function '%s' already exists in default table", name) + } + fhirpathFunc, err := ToFunction(fn) + if err != nil { + return err + } + t[name] = fhirpathFunc + return nil +} diff --git a/fhirpath/internal/funcs/function_table_test.go b/fhirpath/internal/funcs/function_table_test.go new file mode 100644 index 0000000..24fee66 --- /dev/null +++ b/fhirpath/internal/funcs/function_table_test.go @@ -0,0 +1,49 @@ +package funcs_test + +import ( + "testing" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestRegister_RaisesError(t *testing.T) { + testCases := []struct { + name string + funcName string + fn any + }{ + { + name: "raises error when trying to override existing function", + funcName: "where", + fn: func(system.Collection) (system.Collection, error) { return nil, nil }, + }, + { + name: "raises error when adding invalid function", + funcName: "someFn", + fn: func() {}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + table := funcs.Clone() + + if err := table.Register(tc.funcName, tc.fn); err == nil { + t.Fatalf("FunctionTable.Register(%s) doesn't raise error when expected", tc.funcName) + } + }) + } +} + +func TestRegister_AddsToMap(t *testing.T) { + table := funcs.Clone() + fn := func(system.Collection) (system.Collection, error) { return nil, nil } + + if err := table.Register("someFn", fn); err != nil { + t.Fatalf("FunctionTable.Register raised unexpected error: %v", err) + } + if _, ok := table["someFn"]; !ok { + t.Errorf("FunctionTable.Register did not successfully add function to map") + } +} diff --git a/fhirpath/internal/funcs/function_test.go b/fhirpath/internal/funcs/function_test.go new file mode 100644 index 0000000..b6bd8d3 --- /dev/null +++ b/fhirpath/internal/funcs/function_test.go @@ -0,0 +1,187 @@ +package funcs_test + +import ( + "errors" + "reflect" + "testing" + + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestToFunction_EvaluatesCorrectly(t *testing.T) { + patient := &ppb.Patient{ + Id: fhir.ID("1234"), + } + fns := map[string]any{ + "take": func(input system.Collection, num system.Integer) (system.Collection, error) { + result := system.Collection{} + for i := 0; i < int(num); i++ { + if i >= len(input) { + continue + } + result = append(result, input[i]) + } + return result, nil + }, + "findResource": func(input system.Collection, resource fhir.Resource) (system.Collection, error) { + for i, elem := range input { + if reflect.DeepEqual(elem, resource) { + return system.Collection{system.Integer(i)}, nil + } + } + return system.Collection{}, nil + }, + } + testCases := []struct { + name string + fn any + args []expr.Expression + input system.Collection + want system.Collection + }{ + { + name: "test custom take function", + fn: fns["take"], + args: []expr.Expression{&expr.LiteralExpression{Literal: system.Integer(2)}}, + input: system.Collection{"1", "2", "3"}, + want: system.Collection{"1", "2"}, + }, + { + name: "findResource returns index of desired resource", + fn: fns["findResource"], + args: []expr.Expression{exprtest.Return(patient)}, + input: system.Collection{system.Boolean(true), system.Boolean(false), patient}, + want: system.Collection{system.Integer(2)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotFunc, err := funcs.ToFunction(tc.fn) + if err != nil { + t.Fatalf("ToFunction(%T) raised unexpected invalid signature error: %v", tc.fn, err) + } + gotCollection, err := gotFunc.Func(&expr.Context{}, tc.input, tc.args...) + if err != nil { + t.Fatalf("Evaluating function generated by ToFunction raised unexpected error: %v", err) + } + if diff := cmp.Diff(tc.want, gotCollection); diff != "" { + t.Errorf("Evaluating function generated by ToFunction returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToFunction_RaisesSignatureError(t *testing.T) { + testCases := []struct { + name string + fn any + }{ + { + name: "not a function", + fn: 4, + }, + { + name: "no arguments", + fn: func() {}, + }, + { + name: "doesn't contain an input collection as first argument", + fn: func(num system.Integer) {}, + }, + { + name: "only returns one input", + fn: func(in system.Collection) system.Collection { return system.Collection{} }, + }, + { + name: "doesn't return a collection", + fn: func(in system.Collection) (int, error) { return 1, nil }, + }, + { + name: "doesn't return an error", + fn: func(in system.Collection) (system.Collection, bool) { return system.Collection{}, false }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := funcs.ToFunction(tc.fn) + if err == nil { + t.Fatalf("ToFunction(%T) didn't raise error when expected to on function signature mismatch", tc.fn) + } + }) + } +} + +func TestToFunction_RaisesEvaluationError(t *testing.T) { + testCases := []struct { + name string + fn any + args []expr.Expression + input system.Collection + }{ + { + name: "function arity doesn't match number of arguments", + fn: func(in system.Collection) (system.Collection, error) { return system.Collection{}, nil }, + args: []expr.Expression{&expr.LiteralExpression{Literal: system.Boolean(true)}}, + input: system.Collection{}, + }, + { + name: "argument expression raises error", + fn: func(in system.Collection, num system.Integer) (system.Collection, error) { + return system.Collection{}, nil + }, + args: []expr.Expression{exprtest.Error(errors.New("mock error"))}, + input: system.Collection{}, + }, + { + name: "argument expression doesn't evaluate to singleton", + fn: func(in system.Collection, num system.Integer) (system.Collection, error) { + return system.Collection{}, nil + }, + args: []expr.Expression{exprtest.Return(1, 2)}, + input: system.Collection{}, + }, + { + name: "argument expression evaluates to different type", + fn: func(in system.Collection, num system.Integer) (system.Collection, error) { + return system.Collection{}, nil + }, + args: []expr.Expression{&expr.LiteralExpression{Literal: system.Boolean(false)}}, + input: system.Collection{}, + }, + { + name: "second argument expression evaluates to wrong type", + fn: func(in system.Collection, num system.Integer, num2 system.Integer) (system.Collection, error) { + return system.Collection{}, nil + }, + args: []expr.Expression{&expr.LiteralExpression{Literal: system.Integer(3)}, &expr.LiteralExpression{Literal: system.Boolean(true)}}, + input: system.Collection{}, + }, + { + name: "function returns error", + fn: func(in system.Collection) (system.Collection, error) { return nil, errors.New("some error") }, + args: []expr.Expression{}, + input: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotFunc, err := funcs.ToFunction(tc.fn) + if err != nil { + t.Fatalf("ToFunction(%T) raised unexpected invalid signature error: %v", tc.fn, err) + } + _, err = gotFunc.Func(&expr.Context{}, tc.input, tc.args...) + if err == nil { + t.Fatalf("ToFunction() did not raise evaluation error when calling generated function") + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/conversion.go b/fhirpath/internal/funcs/impl/conversion.go new file mode 100644 index 0000000..047fa1f --- /dev/null +++ b/fhirpath/internal/funcs/impl/conversion.go @@ -0,0 +1,646 @@ +package impl + +import ( + "errors" + "fmt" + "math" + "regexp" + "strconv" + "strings" + "time" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// DefaultQuantityUnit is defined by the following FHIRPath rules: +// the item is an Integer, or Decimal, where the resulting quantity will have the default unit ('1') +// the item is a Boolean, where true results in the quantity 1.0 '1', and false results in the quantity 0.0 '1' +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#toquantityunit-string-quantity +const DefaultQuantityUnit = "1" + +// Based on the FHIRPath Quantity string validation regexp defined here: +// https://hl7.org/fhirpath/N1/#convertstoquantityunit-string-boolean +const fhirQuantityRegexp = `^(?P<value>(\+|-)?\d+(\.\d+)?)\s*('(?P<unit>[^']+)'|(?P<time>[a-zA-Z]+))?$` + +var regex = regexp.MustCompile(fhirQuantityRegexp) + +// ConvertsToBoolean checks if the input can be converted to a Boolean +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstoboolean-boolean +func ConvertsToBoolean(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Conversion validation + result, err := ToBoolean(ctx, input, args...) + if result.IsEmpty() || err != nil { + return system.Collection{system.Boolean(false)}, nil + } + return system.Collection{system.Boolean(true)}, nil +} + +// ConvertsToDate checks if the input can be converted to a Date +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstodate-boolean +func ConvertsToDate(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Conversion validation + result, err := ToDate(ctx, input, args...) + if result.IsEmpty() || err != nil { + return system.Collection{system.Boolean(false)}, nil + } + return system.Collection{system.Boolean(true)}, nil +} + +// ConvertsToDateTime checks if the input can be converted to a Time +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstodatetime-boolean +func ConvertsToDateTime(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Conversion validation + result, err := ToDateTime(ctx, input, args...) + if result.IsEmpty() || err != nil { + return system.Collection{system.Boolean(false)}, nil + } + return system.Collection{system.Boolean(true)}, nil +} + +// ConvertsToDecimal checks if the input can be converted to a Decimal +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstodecimal-boolean +func ConvertsToDecimal(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Conversion validation + result, err := ToDecimal(ctx, input, args...) + if result.IsEmpty() || err != nil { + return system.Collection{system.Boolean(false)}, nil + } + return system.Collection{system.Boolean(true)}, nil +} + +// ConvertsToInteger checks if the input can be converted to an Integer +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstointeger-boolean +func ConvertsToInteger(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Conversion validation + result, err := ToInteger(ctx, input, args...) + if result.IsEmpty() || err != nil { + return system.Collection{system.Boolean(false)}, nil + } + return system.Collection{system.Boolean(true)}, nil +} + +// ConvertsToQuantity checks if the input can be converted to a Quantity +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstoquantityunit-string-boolean +func ConvertsToQuantity(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) > 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1 or 0", ErrWrongArity, len(args)) + } + // Conversion validation + result, err := ToQuantity(ctx, input, args...) + if result.IsEmpty() || err != nil { + return system.Collection{system.Boolean(false)}, nil + } + return system.Collection{system.Boolean(true)}, nil +} + +// ConvertsToString checks if the input can be converted to a String +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstostring-string +func ConvertsToString(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Conversion validation + result, err := ToString(ctx, input, args...) + if result.IsEmpty() || err != nil { + return system.Collection{system.Boolean(false)}, nil + } + if boolean, _ := result.ToBool(); boolean == false { + return system.Collection{system.Boolean(false)}, nil + } + return system.Collection{system.Boolean(true)}, nil +} + +// ConvertsToTime checks if the input can be converted to a Time +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstotime-boolean +func ConvertsToTime(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Conversion validation + result, err := ToTime(ctx, input, args...) + if result.IsEmpty() || err != nil { + return system.Collection{system.Boolean(false)}, nil + } + return system.Collection{system.Boolean(true)}, nil +} + +// ToBoolean converts the input to a Boolean +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#toboolean-boolean +func ToBoolean(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input reading + value, err := system.From(input[0]) + if err != nil { + return nil, err + } + // Input conversion + switch value := value.(type) { + case system.Decimal: + result, err := system.ParseBoolean(value.String()) + if err != nil { + return system.Collection{}, nil + } + return system.Collection{result}, nil + case system.Integer, system.String: + str := fmt.Sprintf("%v", value) + result, err := system.ParseBoolean(str) + if err != nil { + return system.Collection{}, nil + } + return system.Collection{result}, nil + case system.Boolean: + return system.Collection{value}, nil + } + return system.Collection{}, nil +} + +// ToDate converts the input to a Date +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#todate-date +func ToDate(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input reading + value, err := system.From(input[0]) + if err != nil { + return system.Collection{}, nil + } + // Input conversion + switch value := value.(type) { + case system.Date: + return system.Collection{value}, nil + case system.DateTime: + dt := value.String() + result := system.MustParseDate(dt[:10]) + return system.Collection{result}, nil + case system.String: + result, err := system.ParseDate(string(value)) + if err != nil { + return system.Collection{}, nil + } + return system.Collection{result}, nil + } + return system.Collection{}, nil +} + +// ToDateTime converts the input to a Date +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#todatetime-datetime +func ToDateTime(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validations + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Reading input + value, err := system.From(input[0]) + if err != nil { + return system.Collection{}, nil + } + // Input conversion + switch value := value.(type) { + case system.Date: + return system.Collection{value.ToDateTime()}, nil + case system.DateTime: + return system.Collection{value}, nil + case system.String: + result, err := system.ParseDateTime(string(value)) + if err != nil { + return system.Collection{}, nil + } + return system.Collection{result}, nil + } + return system.Collection{}, nil +} + +// ToDecimal converts the input to a Decimal +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#todecimal-decimal +func ToDecimal(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input reading + value, err := system.From(input[0]) + if err != nil { + return nil, err + } + // Input conversion + switch value.(type) { + case system.Decimal: + return system.Collection{value}, nil + case system.Integer: + str := fmt.Sprintf("%v", value) + result, err := system.ParseDecimal(str) + if err != nil { + return system.Collection{}, nil + } + return system.Collection{result}, nil + case system.String: + str := fmt.Sprintf("%s", value) + result, err := system.ParseDecimal(str) + if err != nil { + return system.Collection{}, nil + } + return system.Collection{result}, nil + case system.Boolean: + if value.(system.Boolean) { + return system.Collection{system.MustParseDecimal("1.0")}, nil + } + return system.Collection{system.MustParseDecimal("0.0")}, nil + } + return system.Collection{}, nil +} + +// ToInteger converts the input to an Integer +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#tointeger-integer +func ToInteger(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input reading + value, err := system.From(input[0]) + if err != nil { + return nil, err + } + // Input conversion + switch value.(type) { + case system.Integer: + return system.Collection{value}, nil + case system.String: + str := fmt.Sprintf("%s", value) + result, err := system.ParseInteger(str) + if err != nil { + return nil, err + } + return system.Collection{result}, nil + case system.Boolean: + if value.(system.Boolean) { + return system.Collection{system.Integer(1)}, nil + } + return system.Collection{system.Integer(0)}, nil + } + return system.Collection{}, nil +} + +// ToQuantity converts the input to a Quantity +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#toquantityunit-string-quantity +func ToQuantity(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) > 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1 or 0", ErrWrongArity, len(args)) + } + argStr := "" + if len(args) == 1 { + argValue, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + argStr, err = argValue.ToString() + if err != nil { + return nil, err + } + } + // Input reading + value, err := system.From(input[0]) + if err != nil { + return nil, err + } + // Input conversion + switch value := value.(type) { + case system.Integer: + matches := regex.FindStringSubmatch(fmt.Sprintf("%v %v", value, argStr)) + if matches == nil { + return system.Collection{}, nil + } + if argStr != "" { + unit := regex.SubexpIndex("unit") + t := regex.SubexpIndex("time") + if matches[unit] != "" { + result := system.MustParseQuantity(fmt.Sprintf("%v", value), matches[unit]) + return system.Collection{result}, nil + } + if matches[t] != "" { + result := system.MustParseQuantity(fmt.Sprintf("%v", value), matches[t]) + return system.Collection{result}, nil + } + } + result := system.MustParseQuantity(fmt.Sprintf("%v", value), DefaultQuantityUnit) + return system.Collection{result}, nil + case system.Decimal: + str := value.String() + result, err := system.ParseQuantity(string(str), DefaultQuantityUnit) + if err != nil { + return nil, err + } + return system.Collection{result}, nil + case system.Quantity: + return system.Collection{value}, nil + case system.String: + matches := regex.FindStringSubmatch(string(value)) + if matches == nil { + return system.Collection{}, nil + } + if argStr != "" { + if !isValidUnitConversion(argStr) { + return nil, fmt.Errorf("invalid unit of time: %v", input) + } + conversion, err := convertDuration(string(value), argStr) + if err != nil { + return nil, err + } + res := strings.SplitN(conversion, " ", 2) + result := system.MustParseQuantity(res[0], res[1]) + return system.Collection{result}, nil + } + res := strings.SplitN(string(value), " ", 2) + unit := strings.Trim(res[1], "'") + result := system.MustParseQuantity(res[0], unit) + return system.Collection{result}, nil + case system.Boolean: + if value { + result := system.MustParseQuantity("1.0", DefaultQuantityUnit) + return system.Collection{result}, nil + } + result := system.MustParseQuantity("0.0", DefaultQuantityUnit) + return system.Collection{result}, nil + } + return system.Collection{}, nil +} + +// ToString converts the input to a String +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#tostring-string +func ToString(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input reading + value, err := system.From(input[0]) + if err != nil { + return system.Collection{system.Boolean(false)}, nil + } + // Input conversion + switch value := value.(type) { + case system.String: + return system.Collection{value}, nil + case system.Integer: + return system.Collection{system.String(fmt.Sprintf("%v", value))}, nil + case system.Decimal: + return system.Collection{system.String(value.String())}, nil + case system.Quantity: + return system.Collection{system.String(value.String())}, nil + case system.Date: + return system.Collection{system.String(value.String())}, nil + case system.Time: + return system.Collection{system.String(value.String())}, nil + case system.DateTime: + return system.Collection{system.String(value.String())}, nil + case system.Boolean: + if value { + return system.Collection{system.String("true")}, nil + } + return system.Collection{system.String("false")}, nil + } + return system.Collection{system.Boolean(false)}, nil +} + +// ToTime converts the input to a Time +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#totime-time +func ToTime(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Argument validation + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input reading + value, err := system.From(input[0]) + if err != nil { + return system.Collection{}, nil + } + // Input conversion + switch value := value.(type) { + case system.Time: + return system.Collection{value}, nil + case system.String: + result, err := system.ParseTime(fmt.Sprintf("%v", value)) + if err != nil { + return system.Collection{}, nil + } + return system.Collection{result}, nil + } + return system.Collection{}, nil +} + +func isValidUnitConversion(outputFormat string) bool { + validFormats := map[string]bool{ + "years": true, + "months": true, + "days": true, + "hours": true, + "minutes": true, + "seconds": true, + } + + return validFormats[outputFormat] +} + +func convertDuration(input string, outputFormat string) (string, error) { + duration, err := parseHumanDuration(input) + if err != nil { + return "", err + } + + var convertedValue float64 + switch outputFormat { + case "years": + convertedValue = duration.Hours() / (24 * 365) + case "months": + convertedValue = duration.Hours() / (24 * 30) + case "days": + convertedValue = duration.Hours() / 24 + case "hours": + convertedValue = duration.Hours() + case "minutes": + convertedValue = duration.Minutes() + case "seconds": + convertedValue = duration.Seconds() + } + + if outputFormat == "years" { + convertedValue = math.Ceil(convertedValue / 12.0) + } + + return fmt.Sprintf("%.0f %s", convertedValue, outputFormat), nil +} + +func parseHumanDuration(input string) (time.Duration, error) { + re := regexp.MustCompile(`(\d+)\s*(\w+)`) + matches := re.FindAllStringSubmatch(input, -1) + totalSeconds := int64(0) + + for _, match := range matches { + value, err := strconv.ParseInt(match[1], 10, 64) + if err != nil { + return 0, err + } + + unit := strings.ToLower(match[2]) + switch unit { + case "second", "seconds": + totalSeconds += value + case "minute", "minutes": + totalSeconds += value * 60 + case "hour", "hours": + totalSeconds += value * 3600 + case "day", "days": + totalSeconds += value * 86400 + case "month", "months": + totalSeconds += value * 30 * 86400 // Assuming one month is 30 days + case "year", "years": + totalSeconds += value * 365 * 86400 // Assuming one year is 365 days + default: + return 0, fmt.Errorf("invalid unit: %s", unit) + } + } + + return time.Duration(totalSeconds) * time.Second, nil +} diff --git a/fhirpath/internal/funcs/impl/conversion_test.go b/fhirpath/internal/funcs/impl/conversion_test.go new file mode 100644 index 0000000..c083727 --- /dev/null +++ b/fhirpath/internal/funcs/impl/conversion_test.go @@ -0,0 +1,2112 @@ +package impl_test + +import ( + "testing" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" + "google.golang.org/protobuf/testing/protocmp" + + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestConvertsToBoolean(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("T"), + system.String("True")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("false")}, + args: []expr.Expression{ + exprtest.Return(system.String("200")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns false if input is not convertible", + input: system.Collection{system.String("404 Kg")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.Decimal '0.0'", + input: system.Collection{system.MustParseDecimal("0.0")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Decimal '1.0'", + input: system.Collection{system.MustParseDecimal("1.0")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Decimal '3.5'", + input: system.Collection{system.MustParseDecimal("3.5")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.Integer '0'", + input: system.Collection{system.Integer(0)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Integer '1'", + input: system.Collection{system.Integer(1)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Integer '3'", + input: system.Collection{system.Integer(3)}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String '2.0'", + input: system.Collection{system.String("2.0")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String '1.0'", + input: system.Collection{system.String("1.0")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'y'", + input: system.Collection{system.String("y")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'yes'", + input: system.Collection{system.String("yes")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'Y'", + input: system.Collection{system.String("Y")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'YES'", + input: system.Collection{system.String("YES")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '0.0'", + input: system.Collection{system.String("0.0")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'f'", + input: system.Collection{system.String("f")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'false'", + input: system.Collection{system.String("false")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'F'", + input: system.Collection{system.String("F")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'FALSE'", + input: system.Collection{system.String("FALSE")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'n", + input: system.Collection{system.String("n")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'no'", + input: system.Collection{system.String("no")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'N", + input: system.Collection{system.String("N")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'NO'", + input: system.Collection{system.String("NO")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'true'", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'false'", + input: system.Collection{system.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.Integer '1'", + input: system.Collection{fhir.Integer(1)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.PositiveInt '1'", + input: system.Collection{fhir.PositiveInt(1)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.UnsignedInt '1'", + input: system.Collection{fhir.UnsignedInt(1)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ConvertsToBoolean(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ConvertsToBoolean() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ConvertsToBoolean() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestConvertsToDate(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("2001-09-11"), + system.String("2011-05-02")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("false")}, + args: []expr.Expression{ + exprtest.Return(system.String("minutes")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns false if input is not convertible to system.Date", + input: system.Collection{system.MustParseQuantity("75", "Kg")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns true for a system.Date", + input: system.Collection{system.MustParseDate("1993-08-13")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a partial system.Date", + input: system.Collection{system.MustParseDate("1993-08")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.DateTime", + input: system.Collection{system.MustParseDateTime("1993-08-13T14:20:00")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a convertible system.String", + input: system.Collection{system.String("1993-08-13")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false for a non convertible system.String", + input: system.Collection{system.String("93.08.13")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns false for a ppb.Patient", + input: system.Collection{&ppb.Patient{}}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ConvertsToDate(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ConvertsToDate() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ConvertsToDate() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestConvertsToDateTime(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("2001-09-11"), + system.String("2011-05-02")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("2001-09-11")}, + args: []expr.Expression{ + exprtest.Return(system.String("minutes")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns empty if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns false if input is not convertible to system.DateTime", + input: system.Collection{system.MustParseQuantity("75", "Kg")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns true for a system.DateTime", + input: system.Collection{system.MustParseDateTime("1993-08-13T14:20:00")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a partial system.DateTime", + input: system.Collection{system.MustParseDateTime("2012-01-01T10:00")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.Date", + input: system.Collection{system.MustParseDate("2006-01-02")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a convertible system.String", + input: system.Collection{system.String("1993-08-13T14:20:00")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns empty for a non convertible system.String", + input: system.Collection{system.String("93.08.13")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns empty for a ppb.Patient", + input: system.Collection{&ppb.Patient{}}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ConvertsToDateTime(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ConvertsToDateTime() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ConvertsToDateTime() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestConvertsToDecimal(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("101"), + system.String("102")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("100")}, + args: []expr.Expression{ + exprtest.Return(system.String("200")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.Decimal 'true'", + input: system.Collection{system.MustParseDecimal("13.5")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Decimal 'false'", + input: system.Collection{system.MustParseDecimal("-13.5")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Integer '13'", + input: system.Collection{system.Integer(13)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input system.Integer '-13'", + input: system.Collection{system.Integer(-13)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input system.String '+13.5'", + input: system.Collection{system.String("+13.5")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '-13.5'", + input: system.Collection{system.String("-13.5")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '3.1416 cm'", + input: system.Collection{system.String("3.1416 cm")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String '12.99'", + input: system.Collection{system.String(" 12.99 ")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is true system.Boolean 'true'", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'false'", + input: system.Collection{system.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.Integer '10'", + input: system.Collection{fhir.Integer(10)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.PositiveInt '11'", + input: system.Collection{fhir.PositiveInt(11)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.UnsignedInt '12'", + input: system.Collection{fhir.UnsignedInt(12)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.Boolean 'true'", + input: system.Collection{fhir.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ConvertsToDecimal(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ConvertsToDecimal() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ConvertsToDecimal() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestConvertsToInteger(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("101"), + system.String("102")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("100")}, + args: []expr.Expression{ + exprtest.Return(system.String("200")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.Integer '13'", + input: system.Collection{system.Integer(13)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Integer '-13'", + input: system.Collection{system.Integer(-13)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '13'", + input: system.Collection{system.String("13")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '+13'", + input: system.Collection{system.String("+13")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '404 kg'", + input: system.Collection{system.String("404 Kg")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String ' 12 '", + input: system.Collection{system.String(" 12 ")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String '-13'", + input: system.Collection{system.String("-13")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'true'", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'false'", + input: system.Collection{system.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.Integer '10'", + input: system.Collection{fhir.Integer(10)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.PositiveInt '11'", + input: system.Collection{fhir.PositiveInt(11)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.UnsignedInt '12'", + input: system.Collection{fhir.UnsignedInt(12)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.Boolean 'true'", + input: system.Collection{fhir.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ConvertsToInteger(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ConvertsToInteger() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ConvertsToInteger() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestConvertsToQuantity(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("10 'km/hr'"), + system.String("10 'mi/hr'")}, + want: nil, + wantErr: true, + }, + { + name: "returns false if input is not convertible to a system.Quantity", + input: system.Collection{system.String("10 km / hr")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "errors if args length is more than 1", + input: system.Collection{system.String("2 days")}, + args: []expr.Expression{ + exprtest.Return(system.String("hours")), + exprtest.Return(system.String("minutes")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.Integer '13'", + input: system.Collection{system.Integer(13)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Decimal '13.5", + input: system.Collection{system.MustParseDecimal("13.5")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Quantity '13.5 lbs'", + input: system.Collection{system.MustParseQuantity("13.5", "lbs")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '100 days'", + input: system.Collection{system.String("100 days")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '100 km'", + input: system.Collection{system.String("100 km")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '100 km/h'", + input: system.Collection{system.String("100 km/h")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String '100 'km/h''", + input: system.Collection{system.String("100 'km/h'")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '100 'km per hour''", + input: system.Collection{system.String("100 'km per hour'")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '100 km per hour'", + input: system.Collection{system.String("100 km per hour")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String '100 km'", + input: system.Collection{system.String("100 km")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '100 km/h'", + input: system.Collection{system.String("100 km/h")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String '10 'km per hr''", + input: system.Collection{system.String("10 'km per hr'")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '2 years' with arg 'months'", + input: system.Collection{system.String("2 years")}, + args: []expr.Expression{ + exprtest.Return(system.String("months")), + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '2 months' with arg 'days'", + input: system.Collection{system.String("2 months")}, + args: []expr.Expression{ + exprtest.Return(system.String("days")), + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '2 days' with arg 'hours'", + input: system.Collection{system.String("2 days")}, + args: []expr.Expression{ + exprtest.Return(system.String("hours")), + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '2 hours' with arg 'minutes'", + input: system.Collection{system.String("2 hours")}, + args: []expr.Expression{ + exprtest.Return(system.String("minutes")), + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '2 minutes' with arg 'seconds'", + input: system.Collection{system.String("2 minutes")}, + args: []expr.Expression{ + exprtest.Return(system.String("seconds")), + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '100 'km''", + input: system.Collection{system.String("100 'km'")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Integer '100' with arg ''km''", + input: system.Collection{system.Integer(100)}, + args: []expr.Expression{ + exprtest.Return(system.String("'km'")), + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Integer '100' with arg 'days''", + input: system.Collection{system.Integer(100)}, + args: []expr.Expression{ + exprtest.Return(system.String("days")), + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'true'", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'false'", + input: system.Collection{system.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.Integer '10'", + input: system.Collection{fhir.Integer(10)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.PositiveInt '11'", + input: system.Collection{fhir.PositiveInt(11)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.UnsignedInt '12'", + input: system.Collection{fhir.UnsignedInt(12)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ConvertsToQuantity(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ConvertsToQuantity() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ConvertsToQuantity() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestConvertsToString(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a singleton", + input: system.Collection{ + system.String("101"), + system.String("102"), + }, + want: nil, + wantErr: true, + }, + { + name: "returns empty for and empty input collection", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns true for a system.String", + input: system.Collection{system.String("100")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a fhir.String", + input: system.Collection{fhir.String("100")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.Integer", + input: system.Collection{system.Integer(100)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a fhir.Integer", + input: system.Collection{fhir.Integer(100)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a fhir.PositiveInt", + input: system.Collection{fhir.PositiveInt(11)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a fhir.UnsignedInt", + input: system.Collection{fhir.UnsignedInt(12)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.Decimal", + input: system.Collection{system.MustParseDecimal("100.999")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.Date", + input: system.Collection{system.MustParseDate("1993-08-13")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.Time", + input: system.Collection{system.MustParseTime("14:01:45.0000001")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.DateTime", + input: system.Collection{system.MustParseDateTime("1993-08-13T14:01:45.0000001")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.Boolean", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a fhir.Boolean", + input: system.Collection{fhir.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a system.Quantity", + input: system.Collection{system.MustParseQuantity("75", "kg")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false for a ppb.Patient", + input: system.Collection{&ppb.Patient{}}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ConvertsToString(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ConvertsToString() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ConvertsToString() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestConvertsToTime(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("2001-09-11"), + system.String("2011-05-02")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("2001-09-11")}, + args: []expr.Expression{ + exprtest.Return(system.String("minutes")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns empty if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns an empty if input is not convertible to system.Time", + input: system.Collection{system.MustParseQuantity("75", "Kg")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns true for a systemTime", + input: system.Collection{system.MustParseTime("16:20:59")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a partial system.Time", + input: system.Collection{system.MustParseTime("16:20")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for a convertible system.String", + input: system.Collection{system.String("12:59:59")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false for a non convertible system.String", + input: system.Collection{system.String("12/59/99")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns empty for a ppb.Patient", + input: system.Collection{&ppb.Patient{}}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ConvertsToTime(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ConvertsToTime() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ConvertsToTime() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToBoolean(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("T"), + system.String("True")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("false")}, + args: []expr.Expression{ + exprtest.Return(system.String("200")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns an empty collection if input is not convertible", + input: system.Collection{system.String("404 Kg")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.Decimal '0.0'", + input: system.Collection{system.MustParseDecimal("0.0")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.Decimal '1.0'", + input: system.Collection{system.MustParseDecimal("1.0")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Decimal '3.5'", + input: system.Collection{system.MustParseDecimal("3.5")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.Integer '0'", + input: system.Collection{system.Integer(0)}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.Integer '1'", + input: system.Collection{system.Integer(1)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Integer '3'", + input: system.Collection{system.Integer(3)}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.String '2.0'", + input: system.Collection{system.String("2.0")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.String '1.0'", + input: system.Collection{system.String("1.0")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'y'", + input: system.Collection{system.String("y")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'yes'", + input: system.Collection{system.String("yes")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'Y'", + input: system.Collection{system.String("Y")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String 'YES'", + input: system.Collection{system.String("YES")}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.String '0.0'", + input: system.Collection{system.String("0.0")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String 'f'", + input: system.Collection{system.String("f")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String 'false'", + input: system.Collection{system.String("false")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String 'F'", + input: system.Collection{system.String("F")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String 'FALSE'", + input: system.Collection{system.String("FALSE")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String 'n", + input: system.Collection{system.String("n")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String 'no'", + input: system.Collection{system.String("no")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String 'N", + input: system.Collection{system.String("N")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.String 'NO'", + input: system.Collection{system.String("NO")}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'true'", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'false", + input: system.Collection{system.Boolean(false)}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "input is fhir.Integer '1'", + input: system.Collection{fhir.Integer(1)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.PositiveInt '1'", + input: system.Collection{fhir.PositiveInt(1)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "input is fhir.UnsignedInt '1'", + input: system.Collection{fhir.UnsignedInt(1)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToBoolean(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ToBoolean() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ToBoolean() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToDate(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("2001-09-11"), + system.String("2011-05-02")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("2001-09-11")}, + args: []expr.Expression{ + exprtest.Return(system.String("minutes")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns empty if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns an empty if input is not convertible to system.Date", + input: system.Collection{system.MustParseQuantity("75", "Kg")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns system.Date for a system.Date", + input: system.Collection{system.MustParseDate("1993-08-13")}, + want: system.Collection{system.MustParseDate("1993-08-13")}, + wantErr: false, + }, + { + name: "returns system.Date for a partial system.Date", + input: system.Collection{system.MustParseDate("1993-08")}, + want: system.Collection{system.MustParseDate("1993-08")}, + wantErr: false, + }, + { + name: "returns system.Date for a system.DateTime", + input: system.Collection{system.MustParseDateTime("1993-08-13T14:20:00")}, + want: system.Collection{system.MustParseDate("1993-08-13")}, + wantErr: false, + }, + { + name: "returns system.Date for a convertible system.String", + input: system.Collection{system.String("1993-08-13")}, + want: system.Collection{system.MustParseDate("1993-08-13")}, + wantErr: false, + }, + { + name: "returns empty for a non convertible system.String", + input: system.Collection{system.String("93.08.13")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for a ppb.Patient", + input: system.Collection{&ppb.Patient{}}, + want: system.Collection{}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToDate(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ToDate() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ToDate() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToDateTime(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("2001-09-11"), + system.String("2011-05-02")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("2001-09-11")}, + args: []expr.Expression{ + exprtest.Return(system.String("minutes")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns empty if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns an empty if input is not convertible to system.DateTime", + input: system.Collection{system.MustParseQuantity("75", "Kg")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns system.DateTime for a system.DateTime", + input: system.Collection{system.MustParseDateTime("1993-08-13T14:20:00")}, + want: system.Collection{system.MustParseDateTime("1993-08-13T14:20:00")}, + wantErr: false, + }, + { + name: "returns system.DateTime for a partial system.DateTime", + input: system.Collection{system.MustParseDateTime("2012-01-01T10:00")}, + want: system.Collection{system.MustParseDateTime("2012-01-01T10:00")}, + wantErr: false, + }, + { + name: "returns system.DateTime for a system.Date", + input: system.Collection{system.MustParseDate("2006-01-02")}, + want: system.Collection{system.MustParseDate("2006-01-02").ToDateTime()}, + wantErr: false, + }, + { + name: "returns system.DateTime for a convertible system.String", + input: system.Collection{system.String("1993-08-13T14:20:00")}, + want: system.Collection{system.MustParseDateTime("1993-08-13T14:20:00")}, + wantErr: false, + }, + { + name: "returns empty for a non convertible system.String", + input: system.Collection{system.String("93.08.13")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for a ppb.Patient", + input: system.Collection{&ppb.Patient{}}, + want: system.Collection{}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToDateTime(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ToDateTime() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ToDateTime() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToDecimal(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("101"), + system.String("102")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("100")}, + args: []expr.Expression{ + exprtest.Return(system.String("200")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns and empty collection if input is not convertible to system.Decimal", + input: system.Collection{system.MustParseQuantity("500", "kg")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.Decimal '13'", + input: system.Collection{system.MustParseDecimal("13")}, + want: system.Collection{system.MustParseDecimal("13")}, + wantErr: false, + }, + { + name: "input is system.Integer '13'", + input: system.Collection{system.Integer(13)}, + want: system.Collection{system.MustParseDecimal("13")}, + wantErr: false, + }, + { + name: "input is system.String '13'", + input: system.Collection{system.String("13")}, + want: system.Collection{system.MustParseDecimal("13")}, + wantErr: false, + }, + { + name: "input is system.Boolean 'true'", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.MustParseDecimal("1")}, + wantErr: false, + }, + { + name: "input is system.Boolean 'false'", + input: system.Collection{system.Boolean(false)}, + want: system.Collection{system.MustParseDecimal("0")}, + wantErr: false, + }, + { + name: "input is fhir.Integer '0'", + input: system.Collection{fhir.Integer(0)}, + want: system.Collection{system.MustParseDecimal("0")}, + wantErr: false, + }, + { + name: "input is fhir.PositiveInt '1'", + input: system.Collection{fhir.PositiveInt(1)}, + want: system.Collection{system.MustParseDecimal("1")}, + wantErr: false, + }, + { + name: "input is fhir.UnsignedInt '2'", + input: system.Collection{fhir.UnsignedInt(2)}, + want: system.Collection{system.MustParseDecimal("2")}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToDecimal(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ToDecimal() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ToDecimal() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToInteger(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("101"), + system.String("102")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not convertible to system.Integer", + input: system.Collection{system.String("404 Kg")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("100")}, + args: []expr.Expression{ + exprtest.Return(system.String("200")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.Integer '13'", + input: system.Collection{system.Integer(13)}, + want: system.Collection{system.Integer(13)}, + wantErr: false, + }, + { + name: "input is system.String '13'", + input: system.Collection{system.String("13")}, + want: system.Collection{system.Integer(13)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'true'", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.Integer(1)}, + wantErr: false, + }, + { + name: "input is system.Boolean 'false'", + input: system.Collection{system.Boolean(false)}, + want: system.Collection{system.Integer(0)}, + wantErr: false, + }, + { + name: "input is fhir.Integer '10'", + input: system.Collection{fhir.Integer(10)}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "input is fhir.PositiveInt '11'", + input: system.Collection{fhir.PositiveInt(11)}, + want: system.Collection{system.Integer(11)}, + wantErr: false, + }, + { + name: "input is fhir.UnsignedInt '12'", + input: system.Collection{fhir.UnsignedInt(12)}, + want: system.Collection{system.Integer(12)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToInteger(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ToInteger() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ToInteger() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToQuantity(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("10 'km/hr'"), + system.String("10 'mi/hr'")}, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is not convertible to a system.Quantity", + input: system.Collection{system.String("10 km / hr")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "errors if args length is more than 1", + input: system.Collection{system.String("2 days")}, + args: []expr.Expression{ + exprtest.Return(system.String("hours")), + exprtest.Return(system.String("minutes")), + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args is not a valid unit of time", + input: system.Collection{system.String("100 years")}, + args: []expr.Expression{ + exprtest.Return(system.String("decades")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.Integer '13'", + input: system.Collection{system.Integer(13)}, + want: system.Collection{system.MustParseQuantity("13", "1")}, + wantErr: false, + }, + { + name: "input is system.Decimal '13.5", + input: system.Collection{system.MustParseDecimal("13.5")}, + want: system.Collection{system.MustParseQuantity("13.5", "1")}, + wantErr: false, + }, + { + name: "input is system.Quantity '13.5 lbs'", + input: system.Collection{system.MustParseQuantity("13.5", "lbs")}, + want: system.Collection{system.MustParseQuantity("13.5", "lbs")}, + wantErr: false, + }, + { + name: "input is system.String '100 days'", + input: system.Collection{system.String("100 days")}, + want: system.Collection{system.MustParseQuantity("100", "days")}, + wantErr: false, + }, + { + name: "input is system.String '100 km'", + input: system.Collection{system.String("100 km")}, + want: system.Collection{system.MustParseQuantity("100", "km")}, + wantErr: false, + }, + { + name: "input is system.String '100 km/h'", + input: system.Collection{system.String("100 km/h")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.String '100 'km/h''", + input: system.Collection{system.String("100 'km/h'")}, + want: system.Collection{system.MustParseQuantity("100", "km/h")}, + wantErr: false, + }, + { + name: "input is system.String '100 'km per hour''", + input: system.Collection{system.String("100 'km per hour'")}, + want: system.Collection{system.MustParseQuantity("100", "km per hour")}, + wantErr: false, + }, + { + name: "input is system.String '100 km per hour'", + input: system.Collection{system.String("100 km per hour")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.String '100 km'", + input: system.Collection{system.String("100 km")}, + want: system.Collection{system.MustParseQuantity("100", " km")}, + wantErr: false, + }, + { + name: "input is system.String '100 km/h'", + input: system.Collection{system.String("100 km/h")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "input is system.String '10 'km per hr''", + input: system.Collection{system.String("10 'km per hr'")}, + want: system.Collection{system.MustParseQuantity("10", "km per hr")}, + wantErr: false, + }, + { + name: "input is system.String '2 years' with arg 'months'", + input: system.Collection{system.String("2 years")}, + args: []expr.Expression{ + exprtest.Return(system.String("months")), + }, + want: system.Collection{system.MustParseQuantity("24", "months")}, + wantErr: false, + }, + { + name: "input is system.String '2 months' with arg 'days'", + input: system.Collection{system.String("2 months")}, + args: []expr.Expression{ + exprtest.Return(system.String("days")), + }, + want: system.Collection{system.MustParseQuantity("60", "days")}, + wantErr: false, + }, + { + name: "input is system.String '2 days' with arg 'hours'", + input: system.Collection{system.String("2 days")}, + args: []expr.Expression{ + exprtest.Return(system.String("hours")), + }, + want: system.Collection{system.MustParseQuantity("48", "hours")}, + wantErr: false, + }, + { + name: "input is system.String '2 hours' with arg 'minutes'", + input: system.Collection{system.String("2 hours")}, + args: []expr.Expression{ + exprtest.Return(system.String("minutes")), + }, + want: system.Collection{system.MustParseQuantity("120", "minutes")}, + wantErr: false, + }, + { + name: "input is system.String '2 minutes' with arg 'seconds'", + input: system.Collection{system.String("2 minutes")}, + args: []expr.Expression{ + exprtest.Return(system.String("seconds")), + }, + want: system.Collection{system.MustParseQuantity("120", "seconds")}, + wantErr: false, + }, + { + name: "input is system.String '100 'km''", + input: system.Collection{system.String("100 'km'")}, + want: system.Collection{system.MustParseQuantity("100", "km")}, + wantErr: false, + }, + { + name: "input is system.Integer '100' with arg ''km''", + input: system.Collection{system.Integer(100)}, + args: []expr.Expression{ + exprtest.Return(system.String("'km'")), + }, + want: system.Collection{system.MustParseQuantity("100", "km")}, + wantErr: false, + }, + { + name: "input is system.Integer '100' with arg 'days''", + input: system.Collection{system.Integer(100)}, + args: []expr.Expression{ + exprtest.Return(system.String("days")), + }, + want: system.Collection{system.MustParseQuantity("100", "days")}, + wantErr: false, + }, + { + name: "input is system.Boolean 'true'", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.MustParseQuantity("1.0", "1")}, + wantErr: false, + }, + { + name: "input is system.Boolean 'false'", + input: system.Collection{system.Boolean(false)}, + want: system.Collection{system.MustParseQuantity("0.0", "1")}, + wantErr: false, + }, + { + name: "input is fhir.Integer '10'", + input: system.Collection{fhir.Integer(10)}, + want: system.Collection{system.MustParseQuantity("10", "1")}, + wantErr: false, + }, + { + name: "input is fhir.PositiveInt '11'", + input: system.Collection{fhir.PositiveInt(11)}, + want: system.Collection{system.MustParseQuantity("11", "1")}, + wantErr: false, + }, + { + name: "input is fhir.UnsignedInt '12'", + input: system.Collection{fhir.UnsignedInt(12)}, + want: system.Collection{system.MustParseQuantity("12", "1")}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToQuantity(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ToQuantity() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ToQuantity() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToString(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a singleton", + input: system.Collection{ + system.String("101"), + system.String("102"), + }, + want: nil, + wantErr: true, + }, + { + name: "returns empty for and empty input collection", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns a system.String for a system.String", + input: system.Collection{system.String("100")}, + want: system.Collection{system.String("100")}, + wantErr: false, + }, + { + name: "returns a system.String for a fhir.String", + input: system.Collection{fhir.String("100")}, + want: system.Collection{system.String("100")}, + wantErr: false, + }, + { + name: "returns a system.String for a system.Integer", + input: system.Collection{system.Integer(100)}, + want: system.Collection{system.String("100")}, + wantErr: false, + }, + { + name: "returns system.String for a fhir.Integer", + input: system.Collection{fhir.Integer(100)}, + want: system.Collection{system.String("100")}, + wantErr: false, + }, + { + name: "returns system.String for a fhir.PositiveInt", + input: system.Collection{fhir.PositiveInt(11)}, + want: system.Collection{system.String("11")}, + wantErr: false, + }, + { + name: "returns system.String for a fhir.UnsignedInt", + input: system.Collection{fhir.UnsignedInt(12)}, + want: system.Collection{system.String("12")}, + wantErr: false, + }, + { + name: "returns system.String for a system.Decimal", + input: system.Collection{system.MustParseDecimal("100.999")}, + want: system.Collection{system.String("100.999")}, + wantErr: false, + }, + { + name: "returns system.String for a system.Date", + input: system.Collection{system.MustParseDate("1993-08-13")}, + want: system.Collection{system.String("1993-08-13")}, + wantErr: false, + }, + { + name: "returns system.String for a system.Time", + input: system.Collection{system.MustParseTime("14:01:45")}, + want: system.Collection{system.String("14:01:45")}, + wantErr: false, + }, + { + name: "returns system.String for a system.DateTime", + input: system.Collection{system.MustParseDateTime("1993-08-13T14:01:45")}, + want: system.Collection{system.String("1993-08-13T14:01:45")}, + wantErr: false, + }, + { + name: "returns system.String for a system.Boolean", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.String("true")}, + wantErr: false, + }, + { + name: "returns system.String for a fhir.Boolean", + input: system.Collection{fhir.Boolean(true)}, + want: system.Collection{system.String("true")}, + wantErr: false, + }, + { + name: "returns system.String for a system.Quantity", + input: system.Collection{system.MustParseQuantity("75", "kg")}, + want: system.Collection{system.String("75 kg")}, + wantErr: false, + }, + { + name: "returns false for a ppb.Patient", + input: system.Collection{&ppb.Patient{}}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToString(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ToString() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ToString() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestToTime(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.String("2001-09-11"), + system.String("2011-05-02")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.String("2001-09-11")}, + args: []expr.Expression{ + exprtest.Return(system.String("minutes")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns empty if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns an empty if input is not convertible to system.Time", + input: system.Collection{system.MustParseQuantity("75", "Kg")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns system.Time for a systemTime", + input: system.Collection{system.MustParseTime("16:20:59")}, + want: system.Collection{system.MustParseTime("16:20:59")}, + wantErr: false, + }, + { + name: "returns system.Time for a partial system.Time", + input: system.Collection{system.MustParseTime("16:20")}, + want: system.Collection{system.MustParseTime("16:20")}, + wantErr: false, + }, + { + name: "returns system.Time for a convertible system.String", + input: system.Collection{system.String("12:59:59")}, + want: system.Collection{system.MustParseTime("12:59:59")}, + wantErr: false, + }, + { + name: "returns empty for a non convertible system.String", + input: system.Collection{system.String("12/59/99")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for a ppb.Patient", + input: system.Collection{&ppb.Patient{}}, + want: system.Collection{}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToTime(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("ToTime() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ToTime() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/errors.go b/fhirpath/internal/funcs/impl/errors.go new file mode 100644 index 0000000..f4089b4 --- /dev/null +++ b/fhirpath/internal/funcs/impl/errors.go @@ -0,0 +1,9 @@ +package impl + +import "errors" + +// Error constants +var ( + ErrWrongArity = errors.New("incorrect function arity") + ErrInvalidReturnType = errors.New("invalid return type") +) diff --git a/fhirpath/internal/funcs/impl/existence.go b/fhirpath/internal/funcs/impl/existence.go new file mode 100644 index 0000000..cc282bd --- /dev/null +++ b/fhirpath/internal/funcs/impl/existence.go @@ -0,0 +1,106 @@ +package impl + +import ( + "fmt" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// AllTrue Takes a collection of Boolean values and returns true if all the items are true. +// If any items are false, the result is false. If the input is empty, the result is true. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#alltrue-boolean +func AllTrue(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{system.Boolean(true)}, nil + } + for _, v := range input { + value, _ := system.From(v) + if value == system.Boolean(false) { + return system.Collection{system.Boolean(false)}, nil + } + } + return system.Collection{system.Boolean(true)}, nil +} + +// AnyTrue takes a collection of Boolean values and returns true if any of the items are true. +// If all the items are false, or if the input is empty ({ }), the result is false. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#anytrue-boolean +func AnyTrue(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{system.Boolean(false)}, nil + } + for _, v := range input { + value, _ := system.From(v) + if value == system.Boolean(true) { + return system.Collection{system.Boolean(true)}, nil + } + } + return system.Collection{system.Boolean(false)}, nil +} + +// AllFalse takes a collection of Boolean values and returns true if all the items are false. +// If any items are true, the result is false. If the input is empty, the result is true. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#allfalse-boolean +func AllFalse(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{system.Boolean(true)}, nil + } + for _, v := range input { + value, _ := system.From(v) + if value == system.Boolean(true) { + return system.Collection{system.Boolean(false)}, nil + } + } + return system.Collection{system.Boolean(true)}, nil +} + +// AnyFalse takes a collection of Boolean values and returns true if any of the items are false. +// If all the items are true, or if the input is empty ({ }), the result is false. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#anyfalse-boolean +func AnyFalse(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{system.Boolean(false)}, nil + } + for _, v := range input { + value, _ := system.From(v) + if value == system.Boolean(false) { + return system.Collection{system.Boolean(true)}, nil + } + } + return system.Collection{system.Boolean(false)}, nil +} + +// Count returns the integer count of the number of items in the input collection. +// returns 0 when the input collection is empty. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#convertstodate-boolean +func Count(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + return system.Collection{system.Integer(len(input))}, nil +} + +// Exists evaluates the expression args[0] on each input item, returns whether +// there exists at least one item that cause the expression to evaluate to true. +// http://hl7.org/fhirpath/N1/#existscriteria-expression-boolean +func Exists(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + if len(args) == 0 { + return system.Collection{system.Boolean(len(input) > 0)}, nil + } + whereOutput, err := Where(ctx, input, args...) + if err != nil { + return nil, fmt.Errorf("calling Where(): %w", err) + } + return system.Collection{system.Boolean(len(whereOutput) > 0)}, nil +} + +// Empty evaluates the expression args[0] on each input item, returns whether +// none of the items causes the expression to evaluate to true. +// http://hl7.org/fhirpath/N1/#empty-boolean +func Empty(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + if len(args) == 0 { + return system.Collection{system.Boolean(len(input) == 0)}, nil + } + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) +} diff --git a/fhirpath/internal/funcs/impl/existence_test.go b/fhirpath/internal/funcs/impl/existence_test.go new file mode 100644 index 0000000..a313cdd --- /dev/null +++ b/fhirpath/internal/funcs/impl/existence_test.go @@ -0,0 +1,494 @@ +package impl_test + +import ( + "errors" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/testing/protocmp" +) + +var coding = []*dtpb.Coding{ + fhir.Coding("loinc-system", "loinc-code"), + fhir.Coding("loinc-system", "generic-code"), + fhir.Coding("snomed-system", "snomed-code"), + fhir.Coding("snomed-system", "snomed-code"), + fhir.Coding("icd10-system", "icd10-code"), + {}, +} + +func TestAllTrue(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns true if input is empty", + input: system.Collection{}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false if input contains a false value", + input: system.Collection{ + system.Boolean(true), + system.Boolean(true), + system.Boolean(false)}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns true if input contains only true values", + input: system.Collection{system.Boolean(true), + system.Boolean(true), + system.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true if input (fhir.Boolean) contains only true values", + input: system.Collection{fhir.Boolean(true), + fhir.Boolean(true), + fhir.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.AllTrue(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("AllTrue() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("AllTrue() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestAnyTrue(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns false if input is empty", + input: system.Collection{}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns true if input contains a true value", + input: system.Collection{ + system.Boolean(false), + system.Boolean(false), + system.Boolean(true)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false if input contains only false values", + input: system.Collection{system.Boolean(false), + system.Boolean(false), + system.Boolean(false)}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns false if input (fhir.Boolean) contains only false values", + input: system.Collection{fhir.Boolean(false), + fhir.Boolean(false), + fhir.Boolean(false)}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.AnyTrue(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("AnyTrue() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("AnyTrue() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestAllFalse(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns true if input is empty", + input: system.Collection{}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false if input contains a true value", + input: system.Collection{ + system.Boolean(false), + system.Boolean(true), + system.Boolean(false)}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns true if input contains only false values", + input: system.Collection{system.Boolean(false), + system.Boolean(false), + system.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true if input (fhir.Boolean) contains only false values", + input: system.Collection{fhir.Boolean(false), + fhir.Boolean(false), + fhir.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.AllFalse(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("AllFalse() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("AllFalse() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestAnyFalse(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns false if input is empty", + input: system.Collection{}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "returns true if input contains a false value", + input: system.Collection{ + system.Boolean(true), + system.Boolean(true), + system.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true if input contains only false values", + input: system.Collection{system.Boolean(false), + system.Boolean(false), + system.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true if input (fhir.Boolean) contains only false values", + input: system.Collection{fhir.Boolean(false), + fhir.Boolean(false), + fhir.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false if input is not boolean", + input: system.Collection{fhir.Integer(5), + fhir.Integer(6)}, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.AnyFalse(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("AnyFalse() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("AnyFalse() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestCount(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns 0 if input is empty", + input: system.Collection{}, + want: system.Collection{system.Integer(0)}, + wantErr: false, + }, + { + name: "input 1 if input length is 1 ", + input: system.Collection{system.Integer(1)}, + want: system.Collection{system.Integer(1)}, + wantErr: false, + }, + { + name: "input 5 if input length is 5 ", + input: system.Collection{ + system.Integer(2), + system.Integer(4), + system.Integer(6), + system.Integer(8), + system.Integer(10)}, + want: system.Collection{system.Integer(5)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Count(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Count() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Count() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestExists_Evaluates(t *testing.T) { + testCases := []struct { + name string + inputCollection system.Collection + inputArgs []expr.Expression + wantCollection system.Collection + }{ + { + name: "exists loinc system + loinc code", + inputCollection: slices.MustConvert[any](coding), + inputArgs: []expr.Expression{&expr.BooleanExpression{ + Left: &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "system"}, + Right: &expr.LiteralExpression{Literal: system.String("loinc-system")}, + }, + Right: &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "code"}, + Right: &expr.LiteralExpression{Literal: system.String("loinc-code")}, + }, + Op: expr.And, + }}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "exists loinc system", + inputCollection: slices.MustConvert[any](coding), + inputArgs: []expr.Expression{&expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "system"}, + Right: &expr.LiteralExpression{Literal: system.String("loinc-system")}, + }}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "exists icd10 system + icd10 code", + inputCollection: slices.MustConvert[any](coding), + inputArgs: []expr.Expression{&expr.BooleanExpression{ + Left: &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "system"}, + Right: &expr.LiteralExpression{Literal: system.String("icd10-system")}, + }, + Right: &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "code"}, + Right: &expr.LiteralExpression{Literal: system.String("icd10-code")}, + }, + Op: expr.And, + }}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "exists snomed system + snomed code", + inputCollection: slices.MustConvert[any](coding), + inputArgs: []expr.Expression{&expr.BooleanExpression{ + Left: &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "system"}, + Right: &expr.LiteralExpression{Literal: system.String("snomed-system")}, + }, + Right: &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "code"}, + Right: &expr.LiteralExpression{Literal: system.String("snomed-code")}, + }, + Op: expr.And, + }}, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "does not exist snomed system + loinc code", + inputCollection: slices.MustConvert[any](coding), + inputArgs: []expr.Expression{&expr.BooleanExpression{ + Left: &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "system"}, + Right: &expr.LiteralExpression{Literal: system.String("snomed-system")}, + }, + Right: &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "code"}, + Right: &expr.LiteralExpression{Literal: system.String("loic-code")}, + }, + Op: expr.And, + }}, + wantCollection: system.Collection{system.Boolean(false)}, + }, + { + name: "non empty inputs with empty args", + inputCollection: slices.MustConvert[any](coding), + inputArgs: nil, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "empty inputs with empty args", + inputCollection: system.Collection{}, + inputArgs: nil, + wantCollection: system.Collection{system.Boolean(false)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Exists(&expr.Context{}, tc.inputCollection, tc.inputArgs...) + if err != nil { + t.Fatalf("Exists function returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("Exists function returned unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + +func TestExists_RaisesError(t *testing.T) { + testCases := []struct { + name string + inputArgs []expr.Expression + inputCollection system.Collection + }{ + { + name: "multiple arguments", + inputArgs: []expr.Expression{exprtest.Return(1), exprtest.Return(1)}, + inputCollection: slices.MustConvert[any](coding), + }, + { + name: "argument expression raises error", + inputArgs: []expr.Expression{exprtest.Error(errors.New("some error"))}, + inputCollection: slices.MustConvert[any](coding), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := impl.Exists(&expr.Context{}, tc.inputCollection, tc.inputArgs...); err == nil { + t.Fatalf("evaluating Exists function didn't return error when expected") + } + }) + } +} + +func TestEmpty_Evaluates(t *testing.T) { + testCases := []struct { + name string + inputCollection system.Collection + inputArgs []expr.Expression + wantCollection system.Collection + }{ + { + name: "empty inputs with empty args", + inputCollection: system.Collection{}, + inputArgs: nil, + wantCollection: system.Collection{system.Boolean(true)}, + }, + { + name: "non empty inputs", + inputCollection: slices.MustConvert[any](coding), + inputArgs: nil, + wantCollection: system.Collection{system.Boolean(false)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Empty(&expr.Context{}, tc.inputCollection, tc.inputArgs...) + if err != nil { + t.Fatalf("Empty function returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("Empty function returned unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + +func TestEmpty_RaisesError(t *testing.T) { + testCases := []struct { + name string + inputArgs []expr.Expression + inputCollection system.Collection + }{ + { + name: "multiple arguments", + inputArgs: []expr.Expression{exprtest.Return(1)}, + inputCollection: slices.MustConvert[any](coding), + }, + { + name: "argument expression raises error", + inputArgs: []expr.Expression{exprtest.Error(errors.New("some error"))}, + inputCollection: slices.MustConvert[any](coding), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := impl.Empty(&expr.Context{}, tc.inputCollection, tc.inputArgs...); err == nil { + t.Fatalf("evaluating Empty function didn't return error when expected") + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/filtering.go b/fhirpath/internal/funcs/impl/filtering.go new file mode 100644 index 0000000..7a293d4 --- /dev/null +++ b/fhirpath/internal/funcs/impl/filtering.go @@ -0,0 +1,35 @@ +package impl + +import ( + "fmt" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// Where evaluates the expression args[0] on each input item, collects the items that cause +// the expression to evaluate to true. +func Where(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + if len(args) != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) + } + e := args[0] + result := system.Collection{} + for _, item := range input { + output, err := e.Evaluate(ctx, system.Collection{item}) + if err != nil { + return nil, err + } + if len(output) == 0 { + continue + } + pass, err := output.ToSingletonBoolean() + if err != nil { + return nil, fmt.Errorf("evaluating where condition as boolean resulted in an error: %w", err) + } + if pass[0] { + result = append(result, item) + } + } + return result, nil +} diff --git a/fhirpath/internal/funcs/impl/filtering_test.go b/fhirpath/internal/funcs/impl/filtering_test.go new file mode 100644 index 0000000..2030555 --- /dev/null +++ b/fhirpath/internal/funcs/impl/filtering_test.go @@ -0,0 +1,114 @@ +package impl_test + +import ( + "errors" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/testing/protocmp" +) + +var contact = []*dtpb.ContactDetail{ + { + Name: fhir.String("Vick"), + Id: fhir.String("123"), + }, + { + Name: fhir.String("Vick"), + Id: fhir.String("234"), + }, + { + Name: fhir.String("Matt"), + Id: fhir.String("123"), + }, +} + +func TestWhere_Evaluates(t *testing.T) { + nameEquality := &expr.EqualityExpression{ + Left: &expr.FieldExpression{FieldName: "name"}, + Right: &expr.LiteralExpression{Literal: system.String("Vick")}, + } + + testCases := []struct { + name string + inputCollection system.Collection + inputArgs []expr.Expression + wantCollection system.Collection + }{ + { + name: "filters those that pass name query", + inputCollection: slices.MustConvert[any](contact), + inputArgs: []expr.Expression{nameEquality}, + wantCollection: slices.MustConvert[any](contact[0:2]), + }, + { + name: "passes through when expression evaluates to singleton", + inputCollection: slices.MustConvert[any](contact), + inputArgs: []expr.Expression{exprtest.Return(system.String("1"))}, + wantCollection: slices.MustConvert[any](contact), + }, + { + name: "passes through when expression evaluates to proto boolean true", + inputCollection: slices.MustConvert[any](contact), + inputArgs: []expr.Expression{exprtest.Return(fhir.Boolean(true))}, + wantCollection: slices.MustConvert[any](contact), + }, + { + name: "filters out when expression evaluates to empty", + inputCollection: slices.MustConvert[any](contact), + inputArgs: []expr.Expression{exprtest.Return()}, + wantCollection: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Where(&expr.Context{}, tc.inputCollection, tc.inputArgs...) + if err != nil { + t.Fatalf("Where function returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { + t.Errorf("Where function returned unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + +func TestWhere_RaisesError(t *testing.T) { + testCases := []struct { + name string + inputArgs []expr.Expression + inputCollection system.Collection + }{ + { + name: "multiple arguments", + inputArgs: []expr.Expression{exprtest.Return(1), exprtest.Return(1)}, + inputCollection: slices.MustConvert[any](contact), + }, + { + name: "argument expression raises error", + inputArgs: []expr.Expression{exprtest.Error(errors.New("some error"))}, + inputCollection: slices.MustConvert[any](contact), + }, + { + name: "argument expression returns multiple items", + inputArgs: []expr.Expression{exprtest.Return(1, 2)}, + inputCollection: slices.MustConvert[any](contact), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := impl.Where(&expr.Context{}, tc.inputCollection, tc.inputArgs...); err == nil { + t.Fatalf("evaluating Where function didn't return error when expected") + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/math.go b/fhirpath/internal/funcs/impl/math.go new file mode 100644 index 0000000..49fbb53 --- /dev/null +++ b/fhirpath/internal/funcs/impl/math.go @@ -0,0 +1,367 @@ +package impl + +import ( + "errors" + "fmt" + "math" + "strconv" + "strings" + + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +var ( + ErrInvalidInput = errors.New("invalid input") +) + +// Abs returns the absolute value of the input. +// When taking the absolute value of a quantity, the unit is unchanged. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#abs-integer-decimal-quantity +func Abs(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + // Argument validations + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + + switch input[0].(type) { + case system.Integer: + // Input type conversion to int32 + number, err := input.ToInt32() + if err != nil { + return nil, err + } + // Absolution number + res := math.Abs(float64(number)) + return system.Collection{system.Integer(res)}, nil + case system.Decimal: + // Input type conversion to float64 + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + // Absolution number + res := math.Abs(number) + result := decimal.NewFromFloat(res) + return system.Collection{system.Decimal(result)}, nil + case system.Quantity: + quantity := strings.Split(input[0].(system.Quantity).String(), " ") + // Input type conversion + f, err := strconv.ParseFloat(quantity[0], 64) + if err != nil { + return nil, err + } + // Absolution number + res := math.Abs(f) + return system.Collection{system.MustParseQuantity(fmt.Sprintf("%f", res), quantity[1])}, nil + } + return nil, errors.New("input is not a number") +} + +// Ceiling returns the first integer greater than or equal to the input. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#ceiling-integer +func Ceiling(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + // Argument validations + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input type conversion to float64 + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + // Ceiling number + result := math.Ceil(number) + return system.Collection{system.Integer(result)}, nil +} + +// Exp returns e raised to the power of the input. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#exp-decimal +func Exp(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + // Argument validations + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Reading input + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + // Exp number + res := math.Pow(math.E, number) + result := system.MustParseDecimal(fmt.Sprintf("%v", res)) + return system.Collection{result}, nil +} + +// Floor returns the first integer less than or equal to the input. +// FHIRPath docs here: https://hl7.org/fhirpath/n1/#floor-integer +func Floor(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + // Argument validations + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input type conversion to float64 + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + // Flooring number + result := math.Floor(number) + return system.Collection{system.Integer(result)}, nil +} + +// Ln returns the natural logarithm of the input number. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#ln-decimal +func Ln(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + // Argument validations + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input type conversion to float64 + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + res := math.Log(number) + // Validating NaN case + if math.IsNaN(res) { + return system.Collection{}, nil + } + // Type conversion to system.Decimal + result := decimal.NewFromFloat(res) + return system.Collection{system.Decimal(result)}, nil +} + +// Log returns the logarithm base of the input number. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#logbase-decimal-decimal +func Log(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + // Input type conversion to float64 + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + // Validating args + if len(args) != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) + } + argValues, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + base, err := argValues.ToFloat64() + if err != nil { + return nil, err + } + // Log number to base + res := logToBase(number, base) + // Validating NaN case + if math.IsNaN(res) { + return system.Collection{}, nil + } + // Type conversion to system.Decimal + result := decimal.NewFromFloat(res) + return system.Collection{system.Decimal(result)}, nil +} + +// Power returns a number to the exponent power. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#powerexponent-integer-decimal-integer-decimal +func Power(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validating input + if input.IsEmpty() { + return system.Collection{}, nil + } + // Validating args + if len(args) != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) + } + argValues, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + // Validating integers case + _, ok := input[0].(system.Integer) + _, ok2 := argValues[0].(system.Integer) + if ok && ok2 { + // Input type conversion to int32 + number, err := input.ToInt32() + if err != nil { + return nil, err + } + // Input type conversion to int32 + exp, err := argValues.ToInt32() + if err != nil { + return nil, err + } + // Powering ints + res := powInt32(number, exp) + return system.Collection{system.Integer(res)}, nil + } + // Input type conversion to float64 + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + // Input type conversion to float64 + exp, err := argValues.ToFloat64() + if err != nil { + return nil, err + } + // Powering number + res := math.Pow(number, exp) + // Validating NaN case + if math.IsNaN(res) { + return system.Collection{}, nil + } + // Type conversion to system.Decimal + result := decimal.NewFromFloat(res) + return system.Collection{system.Decimal(result)}, nil +} + +// Round rounds the decimal to the nearest whole number using a traditional round (i.e. 0.5 or higher will round to 1). +// If specified, the precision argument determines the decimal place at which the rounding will occur. +// If not specified, the rounding will default to 0 decimal places. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#roundprecision-integer-decimal +func Round(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validating input + if input.IsEmpty() { + return system.Collection{}, nil + } + if !input.IsSingleton() { + return nil, errors.New("invalid input, is not a singleton") + } + // Validating args + if len(args) > 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0 or 1 arguments", ErrWrongArity, len(args)) + } + precision := int32(0) + if len(args) == 1 { + argValues, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + // Arg type conversion to int32 + precision, err = argValues.ToInt32() + if err != nil { + return nil, err + } + if precision < 0 { + return nil, errors.New("precision must be greater or equal than 0") + } + } + value, err := system.From(input[0]) + if err != nil { + return nil, err + } + // Rounding number + switch value.(type) { + case system.Decimal: + res, _ := input[0].(system.Decimal) + result := res.Round(precision) + return system.Collection{result}, nil + case system.Integer: + number, err := input.ToInt32() + if err != nil { + return nil, err + } + res := system.MustParseDecimal(fmt.Sprintf("%d", number)) + result := res.Round(precision) + return system.Collection{result}, nil + } + return nil, errors.New("input is not a number") +} + +// Sqrt returns the square root of the input number as a Decimal. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#sqrt-decimal +func Sqrt(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + // Argument validations + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input type conversion to float64 + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + // Validate negative input + if number < 0 { + return nil, fmt.Errorf("%w: unable to sqrt negative value", ErrInvalidInput) + } + // Ceiling number + value := math.Sqrt(number) + result := decimal.NewFromFloat(value) + return system.Collection{system.Decimal(result)}, nil +} + +// Truncate returns the integer portion of the input. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#truncate-integer +func Truncate(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validations + if input.IsEmpty() { + return system.Collection{}, nil + } + // Argument validations + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + // Input type conversion to float64 + number, err := input.ToFloat64() + if err != nil { + return nil, err + } + // Ceiling number + result := math.Trunc(number) + return system.Collection{system.Integer(result)}, nil +} + +func logToBase(number, base float64) float64 { + if number <= 0 || base <= 1 { + return math.NaN() // Return NaN for invalid inputs + } + + return math.Log(number) / math.Log(base) +} + +// powInt32 returns the powering of a number to a given exponential. +func powInt32(base, exp int32) int32 { + if exp == 0 { + return 1 + } + if exp < 0 { + return 0 + } + + result := base + for i := int32(2); i <= exp; i++ { + result *= base + } + return result +} diff --git a/fhirpath/internal/funcs/impl/math_test.go b/fhirpath/internal/funcs/impl/math_test.go new file mode 100644 index 0000000..7ebfaf9 --- /dev/null +++ b/fhirpath/internal/funcs/impl/math_test.go @@ -0,0 +1,1059 @@ +package impl_test + +import ( + "testing" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" + "google.golang.org/protobuf/testing/protocmp" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestAbs(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.MustParseDecimal("10.8")}, + args: []expr.Expression{ + exprtest.Return(system.String("kg")), + exprtest.Return(system.String("lb")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "abs a positive Decimal number", + input: system.Collection{system.MustParseDecimal("10.5")}, + want: system.Collection{system.MustParseDecimal("10.5")}, + wantErr: false, + }, + { + name: "abs a negative Decimal number", + input: system.Collection{system.MustParseDecimal("-10.5")}, + want: system.Collection{system.MustParseDecimal("10.5")}, + wantErr: false, + }, + { + name: "abs a positive Integer number", + input: system.Collection{system.Integer(11)}, + want: system.Collection{system.Integer(11)}, + wantErr: false, + }, + { + name: "abs a negative Integer number", + input: system.Collection{system.Integer(-11)}, + want: system.Collection{system.Integer(11)}, + wantErr: false, + }, + { + name: "abs a positive Quantity number", + input: system.Collection{system.MustParseQuantity("10.5", "kg")}, + want: system.Collection{system.MustParseQuantity("10.5", "kg")}, + wantErr: false, + }, + { + name: "abs a negative Quantity number", + input: system.Collection{system.MustParseQuantity("-10.5", "kg")}, + want: system.Collection{system.MustParseQuantity("10.5", "kg")}, + wantErr: false, + }, + { + name: "abs zero number", + input: system.Collection{system.Integer(0)}, + want: system.Collection{system.Integer(0)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Abs(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Abs() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Abs() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestCeiling(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.MustParseDecimal("10")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("20")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "ceiling a positive float number", + input: system.Collection{system.MustParseDecimal("10.5")}, + want: system.Collection{system.Integer(11)}, + wantErr: false, + }, + { + name: "ceiling a positive float number with zero decimal", + input: system.Collection{system.MustParseDecimal("10.0")}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "ceiling a negative float number", + input: system.Collection{system.MustParseDecimal("-10.5")}, + want: system.Collection{system.Integer(-10)}, + wantErr: false, + }, + { + name: "ceiling a negative float number with zero decimal", + input: system.Collection{system.MustParseDecimal("-10.0")}, + want: system.Collection{system.Integer(-10)}, + wantErr: false, + }, + { + name: "ceiling an Integer number", + input: system.Collection{fhir.Integer(10)}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "ceiling a negative Integer number", + input: system.Collection{fhir.Integer(-10)}, + want: system.Collection{system.Integer(-10)}, + wantErr: false, + }, + { + name: "ceiling a PositiveInt number", + input: system.Collection{fhir.PositiveInt(100)}, + want: system.Collection{system.Integer(100)}, + wantErr: false, + }, + { + name: "ceiling an UnsignedInt number", + input: system.Collection{fhir.UnsignedInt(1000)}, + want: system.Collection{system.Integer(1000)}, + wantErr: false, + }, + { + name: "ceiling zero number", + input: system.Collection{fhir.Integer(0)}, + want: system.Collection{system.Integer(0)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Ceiling(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Ceiling() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Ceiling() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestExp(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.Integer(1), + system.Integer(2)}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.Integer(1)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns a system.Decimal if input is system.Integer", + input: system.Collection{system.Integer(0)}, + want: system.Collection{system.MustParseDecimal("1")}, + wantErr: false, + }, + { + name: "returns a system.Decimal if input is system.Decimal", + input: system.Collection{system.MustParseDecimal("0")}, + want: system.Collection{system.MustParseDecimal("1")}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Exp(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Exp() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Exp() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestFloor(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.MustParseDecimal("10")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("20")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "floors a positive float number", + input: system.Collection{system.MustParseDecimal("10.5")}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "floors a positive float number with zero decimal", + input: system.Collection{system.MustParseDecimal("10.0")}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "floors a negative float number", + input: system.Collection{system.MustParseDecimal("-10.5")}, + want: system.Collection{system.Integer(-11)}, + wantErr: false, + }, + { + name: "floors a negative float number with zero decimal", + input: system.Collection{system.MustParseDecimal("-10.0")}, + want: system.Collection{system.Integer(-10)}, + wantErr: false, + }, + { + name: "floors an Integer number", + input: system.Collection{fhir.Integer(10)}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "floors a negative Integer number", + input: system.Collection{fhir.Integer(-10)}, + want: system.Collection{system.Integer(-10)}, + wantErr: false, + }, + { + name: "floors a PositiveInt number", + input: system.Collection{fhir.PositiveInt(100)}, + want: system.Collection{system.Integer(100)}, + wantErr: false, + }, + { + name: "floors an UnsignedInt number", + input: system.Collection{fhir.UnsignedInt(1000)}, + want: system.Collection{system.Integer(1000)}, + wantErr: false, + }, + { + name: "floors zero number", + input: system.Collection{fhir.Integer(0)}, + want: system.Collection{system.Integer(0)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Floor(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Floor() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Floor() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestLn(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.MustParseDecimal("10")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("20")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns an empty collection if result is NaN", + input: system.Collection{system.MustParseDecimal("-1.0")}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "lns a positive number", + input: system.Collection{system.MustParseDecimal("2")}, + want: system.Collection{system.MustParseDecimal("0.6931471805599453")}, + wantErr: false, + }, + { + name: "lns a positive float number", + input: system.Collection{system.MustParseDecimal("0.5")}, + want: system.Collection{system.MustParseDecimal("-0.6931471805599453")}, + wantErr: false, + }, + { + name: "lns an PositiveInt number", + input: system.Collection{fhir.PositiveInt(16)}, + want: system.Collection{system.MustParseDecimal("2.772588722239781")}, + wantErr: false, + }, + { + name: "lns an UnsignedInt number", + input: system.Collection{fhir.UnsignedInt(16)}, + want: system.Collection{system.MustParseDecimal("2.772588722239781")}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Ln(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Ln() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Ln() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestLog(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns an empty collection if result is NaN", + input: system.Collection{system.MustParseDecimal("-1.0")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("0.5")), + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "logs a number with base 2", + input: system.Collection{system.MustParseDecimal("16")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2")), + }, + want: system.Collection{system.MustParseDecimal("4")}, + wantErr: false, + }, + { + name: "logs a number with base 10", + input: system.Collection{system.MustParseDecimal("100")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("10")), + }, + want: system.Collection{system.MustParseDecimal("2")}, + wantErr: false, + }, + { + name: "logs a PositiveInt number with base 2", + input: system.Collection{fhir.PositiveInt(16)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2")), + }, + want: system.Collection{system.MustParseDecimal("4")}, + wantErr: false, + }, + { + name: "logs an UnsignedInt number with base 2", + input: system.Collection{fhir.UnsignedInt(16)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2")), + }, + want: system.Collection{system.MustParseDecimal("4")}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Log(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Log() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Log() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestPower(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns an empty collection if result is NaN", + input: system.Collection{system.MustParseDecimal("-1.0")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("0.5")), + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "powers a positive float number to a positive float arg", + input: system.Collection{system.MustParseDecimal("2.5")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2")), + }, + want: system.Collection{system.MustParseDecimal("6.25")}, + wantErr: false, + }, + { + name: "powers a positive float number to a negative float arg", + input: system.Collection{system.MustParseDecimal("2.5")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("-2")), + }, + want: system.Collection{system.MustParseDecimal("0.16")}, + wantErr: false, + }, + { + name: "powers a positive float number to a positive int arg", + input: system.Collection{system.MustParseDecimal("2.5")}, + args: []expr.Expression{ + exprtest.Return(system.Integer(2)), + }, + want: system.Collection{system.MustParseDecimal("6.25")}, + wantErr: false, + }, + { + name: "powers a positive float number to a negative int arg", + input: system.Collection{system.MustParseDecimal("2.5")}, + args: []expr.Expression{ + exprtest.Return(system.Integer(-2)), + }, + want: system.Collection{system.MustParseDecimal("0.16")}, + wantErr: false, + }, + { + name: "powers an integer float number to a positive float arg", + input: system.Collection{system.Integer(4)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2.5")), + }, + want: system.Collection{system.MustParseDecimal("32")}, + wantErr: false, + }, + { + name: "powers an integer float number to a negative float arg", + input: system.Collection{system.Integer(4)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("-2.5")), + }, + want: system.Collection{system.MustParseDecimal("0.03125")}, + wantErr: false, + }, + { + name: "powers an integer number to a negative int arg", + input: system.Collection{system.Integer(4)}, + args: []expr.Expression{ + exprtest.Return(system.Integer(-2)), + }, + want: system.Collection{system.Integer(0)}, + wantErr: false, + }, + { + name: "powers a PositiveInt number", + input: system.Collection{fhir.PositiveInt(10)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2.0")), + }, + want: system.Collection{system.MustParseDecimal("100")}, + wantErr: false, + }, + { + name: "powers an UnsignedInt number", + input: system.Collection{fhir.UnsignedInt(20)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2.0")), + }, + want: system.Collection{system.MustParseDecimal("400")}, + wantErr: false, + }, + { + name: "powers zero number to positive float exp", + input: system.Collection{fhir.Integer(0)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("10.5")), + }, + want: system.Collection{system.MustParseDecimal("0")}, + wantErr: false, + }, + { + name: "powers a positive integer number to zero exp", + input: system.Collection{fhir.Integer(10)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("0")), + }, + want: system.Collection{system.MustParseDecimal("1")}, + wantErr: false, + }, + { + name: "powers zero number to zero exp", + input: system.Collection{fhir.Integer(0)}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("0")), + }, + want: system.Collection{system.MustParseDecimal("1")}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Power(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Power() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Power() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestRound(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args len is greater than 1", + input: system.Collection{system.MustParseDecimal("3.141592653589793")}, + args: []expr.Expression{ + exprtest.Return(system.Integer(1)), + exprtest.Return(system.Integer(2)), + }, + want: nil, + wantErr: true, + }, + { + name: "errors if arg is not an Integer", + input: system.Collection{system.MustParseDecimal("3.141592653589793")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("2.2")), + }, + want: nil, + wantErr: true, + }, + { + name: "errors if precision arg is negative", + input: system.Collection{system.MustParseDecimal("3.141592653589793")}, + args: []expr.Expression{ + exprtest.Return(system.Integer(-2)), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "rounds a positive Decimal", + input: system.Collection{system.MustParseDecimal("3.141592653589793")}, + args: []expr.Expression{ + exprtest.Return(system.Integer(4)), + }, + want: system.Collection{system.MustParseDecimal("3.1416")}, + wantErr: false, + }, + { + name: "rounds a positive Decimal with no precision arg", + input: system.Collection{system.MustParseDecimal("3.141592653589793")}, + want: system.Collection{system.MustParseDecimal("3")}, + wantErr: false, + }, + { + name: "rounds a negative Decimal", + input: system.Collection{system.MustParseDecimal("-3.141592653589793")}, + args: []expr.Expression{ + exprtest.Return(system.Integer(4)), + }, + want: system.Collection{system.MustParseDecimal("-3.1416")}, + wantErr: false, + }, + { + name: "rounds a positive Integer", + input: system.Collection{system.Integer(3)}, + want: system.Collection{system.MustParseDecimal("3")}, + wantErr: false, + }, + { + name: "rounds a positive Integer with precision arg", + input: system.Collection{system.Integer(3)}, + args: []expr.Expression{ + exprtest.Return(system.Integer(2)), + }, + want: system.Collection{system.MustParseDecimal("3")}, + wantErr: false, + }, + { + name: "rounds a negative Integer", + input: system.Collection{system.Integer(-7)}, + want: system.Collection{system.MustParseDecimal("-7")}, + wantErr: false, + }, + { + name: "rounds a fhir Integer", + input: system.Collection{fhir.Integer(20)}, + want: system.Collection{system.MustParseDecimal("20")}, + wantErr: false, + }, + { + name: "rounds a fhir PositiveInt", + input: system.Collection{fhir.PositiveInt(30)}, + want: system.Collection{system.MustParseDecimal("30")}, + wantErr: false, + }, + { + name: "rounds a fhir UnsignedInt", + input: system.Collection{fhir.UnsignedInt(10)}, + want: system.Collection{system.MustParseDecimal("10")}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Round(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Round() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Round() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestSqrt(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "errors if input is negative", + input: system.Collection{system.MustParseDecimal("-16.0")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.MustParseDecimal("10")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("20")), + }, + want: nil, + wantErr: true, + }, + { + name: "sqrt a positive float number", + input: system.Collection{system.MustParseDecimal("16.5")}, + want: system.Collection{system.MustParseDecimal("4.06201920231798")}, + wantErr: false, + }, + { + name: "sqrt a positive float number with zero decimal", + input: system.Collection{system.MustParseDecimal("81.0")}, + want: system.Collection{system.MustParseDecimal("9.0")}, + wantErr: false, + }, + { + name: "sqrt an Integer number", + input: system.Collection{fhir.Integer(400)}, + want: system.Collection{system.MustParseDecimal("20.0")}, + wantErr: false, + }, + { + name: "sqrt a PositiveInt number", + input: system.Collection{fhir.PositiveInt(100)}, + want: system.Collection{system.MustParseDecimal("10.0")}, + wantErr: false, + }, + { + name: "sqrt an UnsignedInt number", + input: system.Collection{fhir.UnsignedInt(9)}, + want: system.Collection{system.MustParseDecimal("3.0")}, + wantErr: false, + }, + { + name: "sqrt zero number", + input: system.Collection{fhir.Integer(0)}, + want: system.Collection{system.MustParseDecimal("0.0")}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Sqrt(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Sqrt() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Sqrt() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestTruncate(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if input is not a number", + input: system.Collection{system.String("1.2")}, + want: nil, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{ + system.MustParseDecimal("10.1"), + system.MustParseDecimal("10.5")}, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is more than 0", + input: system.Collection{system.MustParseDecimal("10")}, + args: []expr.Expression{ + exprtest.Return(system.MustParseDecimal("20")), + }, + want: nil, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "truncates a positive float number", + input: system.Collection{system.MustParseDecimal("10.12345")}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "truncates a positive float number with zero decimal", + input: system.Collection{system.MustParseDecimal("10.00")}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "truncates a negative float number", + input: system.Collection{system.MustParseDecimal("-10.12345")}, + want: system.Collection{system.Integer(-10)}, + wantErr: false, + }, + { + name: "truncates a negative float number with zero decimal", + input: system.Collection{system.MustParseDecimal("-10.000")}, + want: system.Collection{system.Integer(-10)}, + wantErr: false, + }, + { + name: "truncates an Integer number", + input: system.Collection{fhir.Integer(10)}, + want: system.Collection{system.Integer(10)}, + wantErr: false, + }, + { + name: "truncates a negative Integer number", + input: system.Collection{fhir.Integer(-10)}, + want: system.Collection{system.Integer(-10)}, + wantErr: false, + }, + { + name: "truncates a PositiveInt number", + input: system.Collection{fhir.PositiveInt(100)}, + want: system.Collection{system.Integer(100)}, + wantErr: false, + }, + { + name: "truncates an UnsignedInt number", + input: system.Collection{fhir.UnsignedInt(1000)}, + want: system.Collection{system.Integer(1000)}, + wantErr: false, + }, + { + name: "truncates zero number", + input: system.Collection{fhir.Integer(0)}, + want: system.Collection{system.Integer(0)}, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Truncate(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Truncate() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Truncate() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/not.go b/fhirpath/internal/funcs/impl/not.go new file mode 100644 index 0000000..2ed5eae --- /dev/null +++ b/fhirpath/internal/funcs/impl/not.go @@ -0,0 +1,25 @@ +package impl + +import ( + "fmt" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// Not returns the boolean inverse of the singleton input collection. +func Not(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + if length := len(args); length != 0 { + return nil, fmt.Errorf("%w, received %v arguments, expected 0", ErrWrongArity, length) + } + + boolean, err := input.ToSingletonBoolean() + if err != nil { + return nil, err + } + if len(boolean) == 0 { + return system.Collection{}, nil + } + result := system.Boolean(!boolean[0]) + return system.Collection{result}, nil +} diff --git a/fhirpath/internal/funcs/impl/not_test.go b/fhirpath/internal/funcs/impl/not_test.go new file mode 100644 index 0000000..adf90a9 --- /dev/null +++ b/fhirpath/internal/funcs/impl/not_test.go @@ -0,0 +1,54 @@ +package impl_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestNot_InvertsBoolean(t *testing.T) { + testCases := []struct { + name string + input system.Collection + want system.Collection + wantErr bool + }{ + { + name: "inverts system boolean", + input: system.Collection{system.Boolean(true)}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "inverts proto boolean", + input: system.Collection{fhir.Boolean(false)}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "receives non-singleton collection", + input: system.Collection{system.Boolean(true), system.Boolean(false)}, + wantErr: true, + }, + { + name: "passes through empty collection", + input: system.Collection{}, + want: system.Collection{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Not(&expr.Context{}, tc.input) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("Not function got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("Not function returned unexpected result: got: %v, want %v", got, tc.want) + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/r4.go b/fhirpath/internal/funcs/impl/r4.go new file mode 100644 index 0000000..112ad09 --- /dev/null +++ b/fhirpath/internal/funcs/impl/r4.go @@ -0,0 +1,42 @@ +package impl + +import ( + "fmt" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// Extension is syntactic sugar over `extension.where(url = ...)`, and is +// specific to the R4 extensions for FHIRPath (as oppose to being part of the +// N1 normative spec). +// +// For more details, see https://hl7.org/fhir/R4/fhirpath.html#functions +func Extension(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + if len(args) != 1 { + return nil, fmt.Errorf("%w: expected 1 argument", ErrWrongArity) + } + arg, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + str, err := arg.ToString() + if err != nil { + return nil, err + } + + var result system.Collection + for _, entry := range input { + entry, ok := entry.(fhir.Extendable) + if !ok { + continue + } + for _, ext := range entry.GetExtension() { + if url := ext.GetUrl(); url != nil && url.Value == str { + result = append(result, ext) + } + } + } + return result, nil +} diff --git a/fhirpath/internal/funcs/impl/r4_test.go b/fhirpath/internal/funcs/impl/r4_test.go new file mode 100644 index 0000000..02b1a70 --- /dev/null +++ b/fhirpath/internal/funcs/impl/r4_test.go @@ -0,0 +1,120 @@ +package impl_test + +import ( + "errors" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element/extension" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestExtension_ValidInput(t *testing.T) { + extURL := "1234" + patient := fhirtest.NewResourceOf[*ppb.Patient](t) + ext := extension.New(extURL, fhir.String("some value")) + patient.Extension = append(patient.Extension, &dtpb.Extension{}) + patient.Extension = append(patient.Extension, ext, ext) + testCases := []struct { + name string + input system.Collection + arg string + want system.Collection + }{ + { + name: "empty input result in empty output", + input: system.Collection{}, + arg: "some-url", + want: nil, + }, + { + name: "entries have extensions not matched by url", + input: system.Collection{ + fhirtest.NewResourceOf[*ppb.Patient](t), + }, + arg: "some-url", + want: nil, + }, + { + name: "input does not have extension field", + input: system.Collection{ + system.String("hello world"), + }, + arg: "some-url", + want: nil, + }, + { + name: "entries have extensions matched by url", + input: system.Collection{patient}, + arg: extURL, + want: system.Collection{ext, ext}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Extension(&expr.Context{}, tc.input, exprtest.Return(system.String(tc.arg))) + if err != nil { + t.Fatalf("Extension function returned unexpected error: %v", err) + } + + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Extension function returned unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + +func TestExtension_InvalidInput_RaisesError(t *testing.T) { + testErr := errors.New("test error") + testCases := []struct { + name string + input system.Collection + args []expr.Expression + wantErr error + }{ + { + name: "too many arguments", + input: system.Collection{}, + args: []expr.Expression{ + exprtest.Return(system.String("")), + exprtest.Return(system.String("")), + }, + wantErr: impl.ErrWrongArity, + }, { + name: "too few arguments", + input: system.Collection{}, + args: []expr.Expression{}, + wantErr: impl.ErrWrongArity, + }, { + name: "invalid argument type", + input: system.Collection{}, + args: []expr.Expression{exprtest.Return(system.Integer(42))}, + wantErr: cmpopts.AnyError, + }, { + name: "argument errors", + input: system.Collection{}, + args: []expr.Expression{exprtest.Error(testErr)}, + wantErr: testErr, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := impl.Extension(&expr.Context{}, tc.input, tc.args...) + + if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Fatalf("Extension(%v): got err %v, want err %v", tc.name, got, want) + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/strings.go b/fhirpath/internal/funcs/impl/strings.go new file mode 100644 index 0000000..30e3480 --- /dev/null +++ b/fhirpath/internal/funcs/impl/strings.go @@ -0,0 +1,435 @@ +package impl + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +var ErrInvalidRegex = errors.New("invalid regex") + +// StartsWith returns true if the input string starts with the given prefix. +func StartsWith(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + // Validate single string argument + if length := len(args); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + output, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(output); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + prefix, err := output.ToString() + if err != nil { + return nil, err + } + + result := system.Boolean(strings.HasPrefix(string(fullString), string(prefix))) + return system.Collection{result}, nil +} + +// EndsWith returns true if the input string ends with the given prefix. +func EndsWith(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + // Validate single string argument + if length := len(args); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + output, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(output); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + suffix, err := output.ToString() + if err != nil { + return nil, err + } + + result := system.Boolean(strings.HasSuffix(string(fullString), string(suffix))) + return system.Collection{result}, nil +} + +// Length returns the length of the input string. +func Length(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + if length := len(args); length != 0 { + return nil, fmt.Errorf("%w, received %v arguments, expected 0", ErrWrongArity, length) + } + + result := system.Integer(len(fullString)) + return system.Collection{result}, nil +} + +// Upper returns the input string with all characters converted to upper case. +func Upper(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + if length := len(args); length != 0 { + return nil, fmt.Errorf("%w, received %v arguments, expected 0", ErrWrongArity, length) + } + + result := system.String(strings.ToUpper(fullString)) + return system.Collection{result}, nil +} + +// Lower returns the input string with all characters converted to lower case. +func Lower(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + if length := len(args); length != 0 { + return nil, fmt.Errorf("%w, received %v arguments, expected 0", ErrWrongArity, length) + } + + result := system.String(strings.ToLower(fullString)) + return system.Collection{result}, nil +} + +// Contains returns true if the input string contains the given substring. +func Contains(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + // Validate single string argument + if length := len(args); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + output, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(output); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + substring, err := output.ToString() + if err != nil { + return nil, err + } + + result := system.Boolean(strings.Contains(string(fullString), string(substring))) + return system.Collection{result}, nil +} + +// ToChars returns the list of characters in the input string. +func ToChars(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + if length := len(args); length != 0 { + return nil, fmt.Errorf("%w, received %v arguments, expected 0", ErrWrongArity, length) + } + + result := system.Collection{} + for _, char := range strings.Split(fullString, "") { + result = append(result, system.String(char)) + } + return result, nil +} + +// Substring returns the part of the string starting at position start (zero-based). +// If length is given, will return at most length number of characters from the input string. +func Substring(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + argLength := len(args) + if argLength < 1 || argLength > 2 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1 or 2", ErrWrongArity, argLength) + } + + // Validate 1st integer argument (start) + startOutput, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(startOutput); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + start, err := startOutput.ToInt32() + if err != nil { + return nil, err + } + if int(start) >= len(fullString) { + return system.Collection{}, nil + } + + // Validate optional 2nd integer argument (length). + var substringLength int32 = -1 + if argLength == 2 { + lengthOutput, err := args[1].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(lengthOutput); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + substringLength, err = lengthOutput.ToInt32() + if err != nil { + return nil, err + } + } + + var result system.String + if substringLength > -1 && int(start+substringLength) < len(fullString) { + // Substring will not go out of bounds + result = system.String(fullString[start : start+substringLength]) + } else { + result = system.String(fullString[start:]) + } + return system.Collection{result}, nil +} + +// IndexOf returns the 0-based index of the first position in which the +// substring is found in the input string, or -1 if it is not found. +func IndexOf(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + // Validate single string argument + if length := len(args); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + output, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(output); length == 0 { + // Return empty for empty argument + return system.Collection{}, nil + } else if length > 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + substring, err := output.ToString() + if err != nil { + return nil, err + } + + result := system.Integer(strings.Index(fullString, substring)) + return system.Collection{result}, nil +} + +// Matches returns true when the value matches the given regular expression. +func Matches(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + // Validate single string argument + if length := len(args); length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + output, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(output); length == 0 { + return system.Collection{}, nil + } else if length != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + regexString, err := output.ToString() + if err != nil { + return nil, err + } + re, err := regexp.Compile(regexString) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrInvalidRegex, regexString) + } + + result := system.Boolean(re.Match([]byte(fullString))) + return system.Collection{result}, nil +} + +// Replace returns the input string with all instances of pattern replaced with substitution. +func Replace(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + if length := len(args); length != 2 { + return nil, fmt.Errorf("%w: received %v arguments, expected 2", ErrWrongArity, length) + } + + // Validate 1st string argument (pattern) + patternOutput, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(patternOutput); length == 0 { + // Empty arg + return system.Collection{}, nil + } else if length > 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + pattern, err := patternOutput.ToString() + if err != nil { + return nil, err + } + + // Validate 2nd string argument (substitution) + subOutput, err := args[1].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(subOutput); length == 0 { + // Empty arg + return system.Collection{}, nil + } else if length > 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + substitution, err := subOutput.ToString() + if err != nil { + return nil, err + } + + result := system.String(strings.ReplaceAll(fullString, pattern, substitution)) + return system.Collection{result}, nil +} + +// ReplaceMatches matches the input using the regular expression in +// regex and replaces each match with the substitution string. The +// substitution may refer to identified match groups in the regular expression. +func ReplaceMatches(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate single string input + if length := len(input); length > 1 { + return nil, fmt.Errorf("%w: input has length %v, expected 1", ErrWrongArity, length) + } else if length == 0 { + return system.Collection{}, nil + } + fullString, err := input.ToString() + if err != nil { + return nil, err + } + + if length := len(args); length != 2 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + + // Validate 1st string argument (regex) + regexOutput, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(regexOutput); length == 0 { + return system.Collection{}, nil + } else if length > 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + regexString, err := regexOutput.ToString() + if err != nil { + return nil, err + } + re, err := regexp.Compile(regexString) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrInvalidRegex, regexString) + } + + // Validate 2nd string argument (substitution) + subOutput, err := args[1].Evaluate(ctx, input) + if err != nil { + return nil, err + } else if length := len(subOutput); length == 0 { + return system.Collection{}, nil + } else if length > 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, length) + } + substitution, err := subOutput.ToString() + if err != nil { + return nil, err + } + + result := system.String(re.ReplaceAllString(fullString, substitution)) + return system.Collection{result}, nil +} diff --git a/fhirpath/internal/funcs/impl/strings_test.go b/fhirpath/internal/funcs/impl/strings_test.go new file mode 100644 index 0000000..7fc06bf --- /dev/null +++ b/fhirpath/internal/funcs/impl/strings_test.go @@ -0,0 +1,1148 @@ +package impl_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestStartsWith(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns true for empty prefix", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("Lee")}, + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false for no match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is not 1", + input: system.Collection{fullString}, + args: []expr.Expression{}, + want: nil, + wantErr: true, + }, + { + name: "errors if arg is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(516)}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.StartsWith(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("StartsWith got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("StartsWith returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestEndsWith(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns true for empty suffix", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("Jieun")}, + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false for no match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is not 1", + input: system.Collection{fullString}, + args: []expr.Expression{}, + want: nil, + wantErr: true, + }, + { + name: "errors if arg is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(516)}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.EndsWith(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("EndsWith got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("EndsWith returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestLength(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns length of string", + input: system.Collection{fullString}, + want: system.Collection{system.Integer(9)}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + want: nil, + wantErr: true, + }, + { + name: "errors if args is not empty", + input: system.Collection{system.String("IU")}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("516")}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Length(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("Length got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("Length returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestUpper(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns upper string", + input: system.Collection{fullString}, + want: system.Collection{system.String("LEE JIEUN")}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + want: nil, + wantErr: true, + }, + { + name: "errors if args is not empty", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Upper(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("Upper got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("Upper returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestLower(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns lower string", + input: system.Collection{fullString}, + want: system.Collection{system.String("lee jieun")}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + want: nil, + wantErr: true, + }, + { + name: "errors if args is not empty", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Lower(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("Lower got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("Lower returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestContains(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns true for empty substring", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("Jie")}, + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false for no match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is not 1", + input: system.Collection{fullString}, + args: []expr.Expression{}, + want: nil, + wantErr: true, + }, + { + name: "errors if arg is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(516)}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Contains(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("Contains got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("Contains returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestToChars(t *testing.T) { + fullString := system.String("IU") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns chars of string", + input: system.Collection{fullString}, + want: system.Collection{system.String("I"), system.String("U")}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + want: nil, + wantErr: true, + }, + { + name: "errors if args is not empty", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(516)}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ToChars(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("ToChars got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("ToChars returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestSubstring(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(0)}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty is start is bigger than input string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(50)}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns substring with no length", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(4)}, + }, + want: system.Collection{system.String("Jieun")}, + wantErr: false, + }, + { + name: "returns substring with length", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(4)}, + &expr.LiteralExpression{Literal: system.Integer(2)}, + }, + want: system.Collection{system.String("Ji")}, + wantErr: false, + }, + { + name: "returns remaining chars if length overflows", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(4)}, + &expr.LiteralExpression{Literal: system.Integer(50)}, + }, + want: system.Collection{system.String("Jieun")}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(4)}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(4)}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is not 1 or 2", + input: system.Collection{fullString}, + args: []expr.Expression{}, + want: nil, + wantErr: true, + }, + { + name: "errors if arg 1 is not an integer", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if arg 2 is not an integer", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(1)}, + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Substring(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("Substring got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("Substring returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestIndexOf(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for empty arg", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns 0 for empty substring", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{system.Integer(0)}, + wantErr: false, + }, + { + name: "returns proper index for match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("Jieun")}, + }, + want: system.Collection{system.Integer(4)}, + wantErr: false, + }, + { + name: "returns -1 for no match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: system.Collection{system.Integer(-1)}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is not 1", + input: system.Collection{fullString}, + args: []expr.Expression{}, + want: nil, + wantErr: true, + }, + { + name: "errors if arg is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(516)}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.IndexOf(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("IndexOf got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("IndexOf returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestMatches(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for empty regex arg", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns true for empty regex", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns true for match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("^Lee[A-Za-z ]*$")}, + }, + want: system.Collection{system.Boolean(true)}, + wantErr: false, + }, + { + name: "returns false for no match", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("^Lee[0-9]*$")}, + }, + want: system.Collection{system.Boolean(false)}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is not 1", + input: system.Collection{fullString}, + args: []expr.Expression{}, + want: nil, + wantErr: true, + }, + { + name: "errors if arg is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(516)}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if regex is invalid", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("^[$")}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Matches(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("Matches got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("Matches returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestReplace(t *testing.T) { + fullString := system.String("Lee Jieun") + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for empty pattern", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{}, + &expr.LiteralExpression{Literal: system.String("Jieun")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for empty substitution", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("Jieun")}, + &expr.LiteralExpression{}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns replaced string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("Jieun")}, + &expr.LiteralExpression{Literal: system.String("Uaena")}, + }, + want: system.Collection{system.String("Lee Uaena")}, + wantErr: false, + }, + { + name: "returns original string if both args are empty string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{fullString}, + wantErr: false, + }, + { + name: "removes pattern if substitution is empty string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("e")}, + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{system.String("L Jiun")}, + wantErr: false, + }, + { + name: "surrounds all characters with subtitution if pattern is empty string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + &expr.LiteralExpression{Literal: system.String("!")}, + }, + want: system.Collection{system.String("!L!e!e! !J!i!e!u!n!")}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is not 2", + input: system.Collection{fullString}, + args: []expr.Expression{}, + want: nil, + wantErr: true, + }, + { + name: "errors if arg 1 is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(516)}, + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if arg 2 is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + &expr.LiteralExpression{Literal: system.Integer(516)}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Replace(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("Replace got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("Replace returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestReplaceMatches(t *testing.T) { + fullString := system.String("Woo Young Woo") + dateRegex := `\b(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<year>\d{2,4})\b` + + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns empty for empty input", + input: system.Collection{}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("")}, + &expr.LiteralExpression{Literal: system.String("")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for empty pattern arg", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{}, + &expr.LiteralExpression{Literal: system.String(" to the ")}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns empty for empty substitution arg", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String(`[ ]`)}, + &expr.LiteralExpression{}, + }, + want: system.Collection{}, + wantErr: false, + }, + { + name: "returns replaced string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String(`[ ]`)}, + &expr.LiteralExpression{Literal: system.String(" to the ")}, + }, + want: system.Collection{system.String("Woo to the Young to the Woo")}, + wantErr: false, + }, + // This test case comes directly from the FHIRPath spec. + // https://build.fhir.org/ig/HL7/FHIRPath/#replacematchesregex-string-substitution-string-string + { + name: "returns replaced string with named patterns", + input: system.Collection{system.String("5/16/1993")}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String(dateRegex)}, + &expr.LiteralExpression{Literal: system.String("${day}/${month}/${year}")}, + }, + want: system.Collection{system.String("16/5/1993")}, + wantErr: false, + }, + { + name: "errors if input length is more than 1", + input: system.Collection{fullString, fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if input is not a string", + input: system.Collection{system.Integer(516)}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if args length is not 2", + input: system.Collection{fullString}, + args: []expr.Expression{}, + want: nil, + wantErr: true, + }, + { + name: "errors if arg 1 is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.Integer(516)}, + &expr.LiteralExpression{Literal: system.String("IU")}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if arg 2 is not a string", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("IU")}, + &expr.LiteralExpression{Literal: system.Integer(516)}, + }, + want: nil, + wantErr: true, + }, + { + name: "errors if regex is invalid", + input: system.Collection{fullString}, + args: []expr.Expression{ + &expr.LiteralExpression{Literal: system.String("^[$")}, + }, + want: nil, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.ReplaceMatches(&expr.Context{}, tc.input, tc.args...) + + if gotErr := err != nil; tc.wantErr != gotErr { + t.Fatalf("ReplaceMatches got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) + } + if !cmp.Equal(tc.want, got) { + t.Errorf("ReplaceMatches returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/subsetting.go b/fhirpath/internal/funcs/impl/subsetting.go new file mode 100644 index 0000000..d469c24 --- /dev/null +++ b/fhirpath/internal/funcs/impl/subsetting.go @@ -0,0 +1,238 @@ +package impl + +import ( + "fmt" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/proto" +) + +// First Returns a collection containing only the first item in the input collection. +// This function is equivalent to item[0], so it will return an empty collection if the input collection has no items. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#first-collection +func First(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + return system.Collection{input[0]}, nil +} + +// Last Returns a collection containing only the last item in the input collection. +// Will return an empty collection if the input collection has no items. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#last-collection +func Last(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + return system.Collection{input[len(input)-1]}, nil +} + +// Tail Returns a collection containing all but the first item in the input collection. +// Will return an empty collection if the input collection has no items, or only one item. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#tail-collection +func Tail(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + return input[1:], nil +} + +// Skip Returns a collection containing all but the first num items in the input collection. +// Will return an empty collection if there are no items remaining after the indicated number of items have been skipped, +// or if the input collection is empty. +// If num is less than or equal to zero, the input collection is simply returned. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#skipnum-integer-collection +func Skip(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + // Args validation + if len(args) != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) + } + argValues, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + skip, err := argValues.ToInt32() + if err != nil { + return nil, err + } + if skip <= 0 { + return input, nil + } + if skip >= int32(len(input)) { + return system.Collection{}, nil + } + return input[skip:], nil +} + +// Take Returns a collection containing the first num items in the input collection, +// or less if there are less than num items. If num is less than or equal to 0, +// or if the input collection is empty ({ }), take returns an empty collection. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#takenum-integer-collection +func Take(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + // Args validation + if len(args) != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) + } + argValues, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + take, err := argValues.ToInt32() + if err != nil { + return nil, err + } + if take <= 0 { + return system.Collection{}, nil + } + if take >= int32(len(input)) { + return input, nil + } + return input[:take], nil +} + +// Intersect Returns the set of elements that are in both collections. +// Duplicate items will be eliminated by this function. +// Order of items is not guaranteed to be preserved in the result of this function. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#intersectother-collection-collection +func Intersect(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + // Args validation + if len(args) != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) + } + argValues, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + var result system.Collection + for _, i := range input { + for _, c := range argValues { + if checkEquality(i, c) { + v, _ := system.From(c) + result = append(result, v) + } + } + } + if len(result) == 0 { + return system.Collection{}, nil + } + return removeDuplicates(result), nil +} + +// Exclude returns the set of elements that are not in the other collection. +// Duplicate items will not be eliminated by this function, and order will be preserved. +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#excludeother-collection-collection +func Exclude(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Input validation + if input.IsEmpty() { + return system.Collection{}, nil + } + // Args validation + if len(args) != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) + } + argValues, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + var result system.Collection + for _, val := range input { + if !argValues.Contains(val) { + result = append(result, val) + } + } + for _, arg := range argValues { + if !input.Contains(arg) { + result = append(result, arg) + } + } + return result, nil +} + +// Distinct returns the set of elements that are distinct and unique from the +// input by applying equality-operation tests. +// +// If the input collection is empty ({}), the result is empty. +// Note that the order of elements in the input collection is not guaranteed to +// be preserved in the result. +// +// See the spec for this function for more details: +// https://hl7.org/fhirpath/N1/#distinct-collection +func Distinct(_ *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + if len(args) != 0 { + return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args)) + } + var result system.Collection + for _, v := range input { + if result.Contains(v) { + continue + } + result = append(result, v) + } + return result, nil +} + +// IsDistinct queries whether the input collection is a set of fully distinct +// and unique values. This is effectively short-hand for calling: +// +// v.count() = v.distinct().count() +// +// See the spec for this function for more details: +// https://hl7.org/fhirpath/N1/#isdistinct-boolean +func IsDistinct(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + got, err := Distinct(ctx, input, args...) + if err != nil { + return nil, err + } + return system.Collection{system.Boolean(len(got) == len(input))}, nil +} + +func removeDuplicates(collection system.Collection) system.Collection { + seen := make(map[any]bool) + var result system.Collection + for _, val := range collection { + if _, ok := seen[val]; !ok { + seen[val] = true + result = append(result, val) + } + } + return result +} + +func checkEquality(lhs, rhs any) bool { + return checkSystemEquality(lhs, rhs) || checkProtoEquality(lhs, rhs) +} + +func checkSystemEquality(lhs, rhs any) bool { + l, lerr := system.From(lhs) + r, rerr := system.From(rhs) + if lerr == nil && rerr == nil { + got, ok := system.TryEqual(l, r) + return got && ok + } + return false +} + +func checkProtoEquality(lhs, rhs any) bool { + l, lok := lhs.(proto.Message) + r, rok := rhs.(proto.Message) + if lok && rok { + return proto.Equal(l, r) + } + return false +} diff --git a/fhirpath/internal/funcs/impl/subsetting_test.go b/fhirpath/internal/funcs/impl/subsetting_test.go new file mode 100644 index 0000000..1cee610 --- /dev/null +++ b/fhirpath/internal/funcs/impl/subsetting_test.go @@ -0,0 +1,677 @@ +package impl_test + +import ( + "testing" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" + + "google.golang.org/protobuf/testing/protocmp" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestFirst(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns and empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + }, + { + name: "returns first collection element", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + want: system.Collection{system.Integer(1)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.First(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("First() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("First() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestLast(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns and empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + }, + { + name: "returns last collection element", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + want: system.Collection{system.Integer(5)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Last(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Last() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Last() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestTail(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "returns and empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + }, + { + name: "returns collection tail", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + want: system.Collection{ + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Tail(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Tail() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Tail() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestSkip(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if arg is not provided", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + }, + { + name: "returns an empty collection if input arg is greater than or equal to input length", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + args: []expr.Expression{ + exprtest.Return(system.Integer(5)), + }, + want: system.Collection{}, + }, + { + name: "returns the same input collection if arg is <= 0", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + args: []expr.Expression{ + exprtest.Return(system.Integer(-2)), + }, + want: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + }, + { + name: "returns the skipped input collection", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + args: []expr.Expression{ + exprtest.Return(system.Integer(3)), + }, + want: system.Collection{ + system.Integer(4), + system.Integer(5)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Skip(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Skip() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Skip() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestTake(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if arg is not provided", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + }, + { + name: "returns an empty collection if arg is lower than or equal to 0 ", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + args: []expr.Expression{ + exprtest.Return(system.Integer(0)), + }, + want: system.Collection{}, + }, + { + name: "returns the same collection if arg is greater than or equal to input length", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + args: []expr.Expression{ + exprtest.Return(system.Integer(7)), + }, + want: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + }, + { + name: "returns the taken input collection", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + args: []expr.Expression{ + exprtest.Return(system.Integer(3)), + }, + want: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Take(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Take() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Take() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestIntersect(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if arg is not provided", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + }, + { + name: "returns the intersection of both collections", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5), + system.Integer(6)}, + args: []expr.Expression{ + exprtest.Return( + system.Integer(3), + system.Integer(6), + system.Integer(9), + ), + }, + want: system.Collection{ + system.Integer(3), + system.Integer(6)}, + }, + { + name: "returns the intersection of both collections ignoring duplicates", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(2), + system.Integer(3)}, + args: []expr.Expression{ + exprtest.Return( + system.Integer(2), + system.Integer(2), + system.Integer(4), + ), + }, + want: system.Collection{ + system.Integer(2), + }, + }, + { + name: "returns the intersection of both collections with fhir.Integer types", + input: system.Collection{ + fhir.Integer(1), + fhir.Integer(2), + fhir.Integer(3), + fhir.Integer(4)}, + args: []expr.Expression{ + exprtest.Return( + fhir.Integer(2), + fhir.Integer(4), + ), + }, + want: system.Collection{ + system.Integer(2), + system.Integer(4), + }, + }, + { + name: "returns an empty collection if there is no intersection", + input: system.Collection{ + system.Integer(1), + system.Integer(2)}, + args: []expr.Expression{ + exprtest.Return( + system.Integer(3), + system.Integer(4), + ), + }, + want: system.Collection{}, + }, + { + name: "returns intersection of normalized values", + input: system.Collection{ + system.Integer(1), + fhir.Integer(1)}, + args: []expr.Expression{ + exprtest.Return( + fhir.Integer(1), + system.Integer(1), + ), + }, + want: system.Collection{ + system.Integer(1), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Intersect(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Intersect() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Intersect() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestExclude(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + wantErr bool + }{ + { + name: "errors if arg is not provided", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5)}, + wantErr: true, + }, + { + name: "returns an empty collection if input is empty", + input: system.Collection{}, + want: system.Collection{}, + }, + { + name: "returns the exclude of both collections", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4)}, + args: []expr.Expression{ + exprtest.Return( + system.Integer(3), + system.Integer(4), + system.Integer(5), + system.Integer(6), + ), + }, + want: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(5), + system.Integer(6)}, + }, + { + name: "returns the exclude of both collections with fhir.Integer types", + input: system.Collection{ + fhir.Integer(1), + fhir.Integer(2), + fhir.Integer(3), + fhir.Integer(4)}, + args: []expr.Expression{ + exprtest.Return( + fhir.Integer(2), + fhir.Integer(4), + ), + }, + want: system.Collection{ + fhir.Integer(1), + fhir.Integer(3), + }, + }, + { + name: "returns an empty collection if there is no exclude", + input: system.Collection{ + system.Integer(1), + system.Integer(2)}, + args: []expr.Expression{ + exprtest.Return( + system.Integer(1), + system.Integer(2), + ), + }, + }, + { + name: "returns exclude of normalized values", + input: system.Collection{ + system.Integer(1), + fhir.Integer(2), + }, + args: []expr.Expression{ + exprtest.Return( + fhir.Integer(2), + system.Integer(3), + ), + }, + want: system.Collection{ + system.Integer(1), + system.Integer(3), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.Exclude(&expr.Context{}, tc.input, tc.args...) + if (err != nil) != tc.wantErr { + t.Errorf("Exclude() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Exclude(%v) returned unexpected diff (-want, +got)\n%s", tc.name, diff) + } + }) + } +} + +func TestDistinct(t *testing.T) { + testCases := []struct { + name string + input system.Collection + want system.Collection + }{ + { + name: "Empty input returns empty output", + }, + { + name: "Inputs are distinct", + input: system.Collection{system.String("Hello"), system.Integer(1), fhir.Integer(2)}, + want: system.Collection{system.String("Hello"), system.Integer(1), fhir.Integer(2)}, + }, + { + name: "Inputs contain exact duplicate", + input: system.Collection{system.String("Hello"), system.Integer(1), system.Integer(1)}, + want: system.Collection{system.String("Hello"), system.Integer(1)}, + }, { + name: "Inputs contain system-convertible duplicates", + input: system.Collection{system.String("Hello"), system.Integer(1), fhir.Integer(1)}, + want: system.Collection{system.String("Hello"), system.Integer(1)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := expr.Context{} + got, err := impl.Distinct(&ctx, tc.input) + if err != nil { + t.Fatalf("Distinct(%v): got unexpected err: %v", tc.name, err) + } + + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Distinct(%v) returned unexpected diff (-want, +got)\n%s", tc.name, diff) + } + }) + } +} + +func TestDistinct_BadInputs(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + wantErr error + }{ + { + name: "Function called with nonzero arguments", + input: system.Collection{}, + args: []expr.Expression{exprtest.Return()}, + wantErr: cmpopts.AnyError, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := expr.Context{} + _, err := impl.Distinct(&ctx, tc.input, tc.args...) + + if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("Distinct(%v): got err %v, want err%v", tc.name, got, want) + } + }) + } +} + +func TestIsDistinct(t *testing.T) { + testCases := []struct { + name string + input system.Collection + want bool + }{ + { + name: "Empty input is distinct", + want: true, + }, + { + name: "Inputs are distinct", + input: system.Collection{system.String("Hello"), system.Integer(1), fhir.Integer(2)}, + want: true, + }, + { + name: "Inputs contain exact duplicate", + input: system.Collection{system.String("Hello"), system.Integer(1), system.Integer(1)}, + want: false, + }, { + name: "Inputs contain system-convertible duplicates", + input: system.Collection{system.String("Hello"), system.Integer(1), fhir.Integer(1)}, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := expr.Context{} + collection, err := impl.IsDistinct(&ctx, tc.input) + if err != nil { + t.Fatalf("IsDistinct(%v): got unexpected err: %v", tc.name, err) + } + got, err := collection.ToBool() + if err != nil { + t.Fatalf("IsDistinct(%v): got unexpected err: %v", tc.name, err) + } + + if got != tc.want { + t.Errorf("IsDistinct(%v): got %v, want %v", tc.name, got, tc.want) + } + }) + } +} + +func TestIsDistinct_BadInputs(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + wantErr error + }{ + { + name: "Function called with nonzero arguments", + input: system.Collection{}, + args: []expr.Expression{exprtest.Return()}, + wantErr: cmpopts.AnyError, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := expr.Context{} + _, err := impl.IsDistinct(&ctx, tc.input, tc.args...) + + if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("IsDistinct(%v): got err %v, want err%v", tc.name, got, want) + } + }) + } +} diff --git a/fhirpath/internal/funcs/impl/utility.go b/fhirpath/internal/funcs/impl/utility.go new file mode 100644 index 0000000..97dfef8 --- /dev/null +++ b/fhirpath/internal/funcs/impl/utility.go @@ -0,0 +1,24 @@ +package impl + +import ( + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +// TimeOfDay returns the current time as a system.Time object. +func TimeOfDay(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + timeString := ctx.Now.Format("15:04:05.000") + return system.Collection{system.MustParseTime(timeString)}, nil +} + +// Today returns the current date as a system.Date object. +func Today(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + dateString := ctx.Now.Format("2006-01-02") + return system.Collection{system.MustParseDate(dateString)}, nil +} + +// Now returns the current time as a system.DateTime object. +func Now(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + dateTimeString := ctx.Now.Format("2006-01-02T15:04:05.000Z07:00") + return system.Collection{system.MustParseDateTime(dateTimeString)}, nil +} diff --git a/fhirpath/internal/funcs/impl/utility_test.go b/fhirpath/internal/funcs/impl/utility_test.go new file mode 100644 index 0000000..b3faef2 --- /dev/null +++ b/fhirpath/internal/funcs/impl/utility_test.go @@ -0,0 +1,50 @@ +package impl_test + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestTimeOfDay(t *testing.T) { + ctx := &expr.Context{Now: time.Date(0, time.January, 1, 19, 30, 5, 1000000, time.UTC)} + wantCollection := system.Collection{system.MustParseTime("19:30:05.001")} + + got, err := impl.TimeOfDay(ctx, []any{}) + if err != nil { + t.Fatalf("impl.TimeOfDay() returned unexpected error: %v", err) + } + if !cmp.Equal(got, wantCollection) { + t.Errorf("impl.TimeOfDay() returned unexpected result: got %v, want %v", got, wantCollection) + } +} + +func TestToday(t *testing.T) { + ctx := &expr.Context{Now: time.Date(2010, time.February, 12, 0, 0, 0, 0, time.UTC)} + wantCollection := system.Collection{system.MustParseDate("2010-02-12")} + + got, err := impl.Today(ctx, []any{}) + if err != nil { + t.Fatalf("impl.Today() returned unexpected error: %v", err) + } + if !cmp.Equal(got, wantCollection) { + t.Errorf("impl.Today() returned unexpected result: got %v, want %v", got, wantCollection) + } +} + +func TestNow(t *testing.T) { + ctx := &expr.Context{Now: time.Date(2010, time.February, 12, 12, 30, 34, 2000000, time.UTC)} + wantCollection := system.Collection{system.MustParseDateTime("2010-02-12T12:30:34.002Z")} + + got, err := impl.Now(ctx, []any{}) + if err != nil { + t.Fatalf("impl.Now() returned unexpected error: %v", err) + } + if !cmp.Equal(got, wantCollection) { + t.Errorf("impl.Now() returned unexpected result: got %v, want %v", got, wantCollection) + } +} diff --git a/fhirpath/internal/funcs/table.go b/fhirpath/internal/funcs/table.go new file mode 100644 index 0000000..e410cc5 --- /dev/null +++ b/fhirpath/internal/funcs/table.go @@ -0,0 +1,392 @@ +package funcs + +import "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + +// BaseTable holds the default mapping of all +// FHIRPath functions. Unimplemented functions return an +// unimplemented error. +var baseTable = FunctionTable{ + "empty": Function{ + impl.Empty, + 0, + 0, + false, + }, + "exists": Function{ + impl.Exists, + 0, + 1, + false, + }, + "extension": Function{ + impl.Extension, + 1, + 1, + false, + }, + "all": notImplemented, + "allTrue": Function{ + impl.AllTrue, + 0, + 0, + false, + }, + "anyTrue": Function{ + impl.AnyTrue, + 0, + 0, + false, + }, + "allFalse": Function{ + impl.AllFalse, + 0, + 0, + false, + }, + "anyFalse": Function{ + impl.AnyFalse, + 0, + 0, + false, + }, + "subsetOf": notImplemented, + "supersetOf": notImplemented, + "count": Function{ + impl.Count, + 0, + 0, + false, + }, + "distinct": Function{ + impl.Distinct, + 0, + 0, + false, + }, + "isDistinct": Function{ + impl.IsDistinct, + 0, + 0, + false, + }, + "where": Function{ + impl.Where, + 1, + 1, + false, + }, + "select": notImplemented, + "repeat": notImplemented, + "ofType": notImplemented, + "single": notImplemented, + "first": Function{ + impl.First, + 0, + 0, + false, + }, + "last": Function{ + impl.Last, + 0, + 0, + false, + }, + "tail": Function{ + impl.Tail, + 0, + 0, + false, + }, + "skip": Function{ + impl.Skip, + 1, + 1, + false, + }, + "take": Function{ + impl.Take, + 1, + 1, + false, + }, + "intersect": Function{ + impl.Intersect, + 1, + 1, + false, + }, + "exclude": Function{ + impl.Exclude, + 1, + 1, + false, + }, + "union": notImplemented, + "combine": notImplemented, + "iif": notImplemented, + "toBoolean": Function{ + impl.ToBoolean, + 0, + 0, + false, + }, + "convertsToBoolean": Function{ + impl.ConvertsToBoolean, + 0, + 0, + false, + }, + "toInteger": Function{ + impl.ToInteger, + 0, + 0, + false, + }, + "convertsToInteger": Function{ + impl.ConvertsToInteger, + 0, + 0, + false, + }, + "toDate": Function{ + impl.ToDate, + 0, + 0, + false, + }, + "convertsToDate": Function{ + impl.ConvertsToDate, + 0, + 0, + false, + }, + "toDateTime": Function{ + impl.ToDateTime, + 0, + 0, + false, + }, + "convertToDateTime": Function{ + impl.ConvertsToDateTime, + 0, + 0, + false, + }, + "toDecimal": Function{ + impl.ToDecimal, + 0, + 0, + false, + }, + "convertsToDecimal": Function{ + impl.ConvertsToDecimal, + 0, + 0, + false, + }, + "toQuantity": Function{ + impl.ToInteger, + 0, + 1, + false, + }, + "convertsToQuantity": Function{ + impl.ConvertsToQuantity, + 0, + 1, + false, + }, + "toString": Function{ + impl.ToString, + 0, + 0, + false, + }, + "convertsToString": Function{ + impl.ConvertsToString, + 0, + 0, + false, + }, + "toTime": Function{ + impl.ToTime, + 0, + 0, + false, + }, + "convertsToTime": Function{ + impl.ConvertsToTime, + 0, + 0, + false, + }, + "indexOf": Function{ + impl.IndexOf, + 1, + 1, + false, + }, + "substring": Function{ + impl.Substring, + 1, + 2, + false, + }, + "startsWith": Function{ + impl.StartsWith, + 1, + 1, + false, + }, + "endsWith": Function{ + impl.EndsWith, + 1, + 1, + false, + }, + "contains": Function{ + impl.Contains, + 1, + 1, + false, + }, + "upper": Function{ + impl.Upper, + 0, + 0, + false, + }, + "lower": Function{ + impl.Lower, + 0, + 0, + false, + }, + "replace": Function{ + impl.Replace, + 2, + 2, + false, + }, + "matches": Function{ + impl.Matches, + 1, + 1, + false, + }, + "replaceMatches": Function{ + impl.ReplaceMatches, + 2, + 2, + false, + }, + "length": Function{ + impl.Length, + 0, + 0, + false, + }, + "toChars": Function{ + impl.ToChars, + 0, + 0, + false, + }, + "abs": Function{ + impl.Abs, + 0, + 0, + false, + }, + "ceiling": Function{ + impl.Ceiling, + 0, + 0, + false, + }, + "exp": Function{ + impl.Exp, + 0, + 0, + false, + }, + "floor": Function{ + impl.Floor, + 0, + 0, + false, + }, + "ln": Function{ + impl.Ln, + 0, + 0, + false, + }, + "log": Function{ + impl.Log, + 0, + 0, + false, + }, + "power": Function{ + impl.Power, + 0, + 0, + false, + }, + "round": Function{ + impl.Round, + 0, + 0, + false, + }, + "sqrt": Function{ + impl.Sqrt, + 0, + 0, + false, + }, + "truncate": Function{ + impl.Truncate, + 0, + 0, + false, + }, + "children": notImplemented, + "descendants": notImplemented, + "trace": notImplemented, + "now": Function{ + impl.Now, + 0, + 0, + false, + }, + "timeOfDay": Function{ + impl.TimeOfDay, + 0, + 0, + false, + }, + "today": Function{ + impl.Today, + 0, + 0, + false, + }, + "not": Function{ + impl.Not, + 0, + 0, + false, + }, +} + +// Clone returns a deep copy of the base +// function table. +func Clone() FunctionTable { + table := make(FunctionTable) // TODO: Optimize (PHP-6173) + for k, v := range baseTable { + table[k] = v + } + return table +} diff --git a/fhirpath/internal/grammar/fhirpath.g4 b/fhirpath/internal/grammar/fhirpath.g4 new file mode 100644 index 0000000..b6c993d --- /dev/null +++ b/fhirpath/internal/grammar/fhirpath.g4 @@ -0,0 +1,179 @@ +grammar fhirpath; + +// Grammar rules +// [FHIRPath](http://hl7.org/fhirpath/N1) Normative Release + +//prog: line (line)*; +//line: ID ( '(' expr ')') ':' expr '\r'? '\n'; + +prog + : expression EOF + ; + +expression + : term #termExpression + | expression '.' invocation #invocationExpression + | expression '[' expression ']' #indexerExpression + | ('+' | '-') expression #polarityExpression + | expression ('*' | '/' | 'div' | 'mod') expression #multiplicativeExpression + | expression ('+' | '-' | '&') expression #additiveExpression + | expression ('is' | 'as') typeSpecifier #typeExpression + | expression '|' expression #unionExpression + | expression ('<=' | '<' | '>' | '>=') expression #inequalityExpression + | expression ('=' | '~' | '!=' | '!~') expression #equalityExpression + | expression ('in' | 'contains') expression #membershipExpression + | expression 'and' expression #andExpression + | expression ('or' | 'xor') expression #orExpression + | expression 'implies' expression #impliesExpression + //| (IDENTIFIER)? '=>' expression #lambdaExpression + ; + +term + : invocation #invocationTerm + | literal #literalTerm + | externalConstant #externalConstantTerm + | '(' expression ')' #parenthesizedTerm + ; + +literal + : '{' '}' #nullLiteral + | ('true' | 'false') #booleanLiteral + | STRING #stringLiteral + | NUMBER #numberLiteral + | DATE #dateLiteral + | DATETIME #dateTimeLiteral + | TIME #timeLiteral + | quantity #quantityLiteral + ; + +externalConstant + : '%' ( identifier | STRING ) + ; + +invocation // Terms that can be used after the function/member invocation '.' + : identifier #memberInvocation + | function #functionInvocation + | '$this' #thisInvocation + | '$index' #indexInvocation + | '$total' #totalInvocation + ; + +function + : identifier '(' paramList? ')' + ; + +paramList + : expression (',' expression)* + ; + +quantity + : NUMBER unit? + ; + +unit + : dateTimePrecision + | pluralDateTimePrecision + | STRING // UCUM syntax for units of measure + ; + +dateTimePrecision + : 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond' + ; + +pluralDateTimePrecision + : 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds' + ; + +typeSpecifier + : qualifiedIdentifier + ; + +qualifiedIdentifier + : identifier ('.' identifier)* + ; + +identifier + : IDENTIFIER + | DELIMITEDIDENTIFIER + | 'as' + | 'contains' + | 'in' + | 'is' + ; + + +/**************************************************************** + Lexical rules +*****************************************************************/ + +/* +NOTE: The goal of these rules in the grammar is to provide a date +token to the parser. As such it is not attempting to validate that +the date is a correct date, that task is for the parser or interpreter. +*/ + +DATE + : '@' DATEFORMAT + ; + +DATETIME + : '@' DATEFORMAT 'T' (TIMEFORMAT TIMEZONEOFFSETFORMAT?)? + ; + +TIME + : '@' 'T' TIMEFORMAT + ; + +fragment DATEFORMAT + : [0-9][0-9][0-9][0-9] ('-'[0-9][0-9] ('-'[0-9][0-9])?)? + ; + +fragment TIMEFORMAT + : [0-9][0-9] (':'[0-9][0-9] (':'[0-9][0-9] ('.'[0-9]+)?)?)? + ; + +fragment TIMEZONEOFFSETFORMAT + : ('Z' | ('+' | '-') [0-9][0-9]':'[0-9][0-9]) + ; + +IDENTIFIER + : ([A-Za-z] | '_')([A-Za-z0-9] | '_')* // Added _ to support CQL (FHIR could constrain it out) + ; + +DELIMITEDIDENTIFIER + : '`' (ESC | .)*? '`' + ; + +STRING + : '\'' (ESC | .)*? '\'' + ; + +// Also allows leading zeroes now (just like CQL and XSD) +NUMBER + : [0-9]+('.' [0-9]+)? + ; + +// Pipe whitespace to the HIDDEN channel to support retrieving source text through the parser. +WS + : [ \r\n\t]+ -> channel(HIDDEN) + ; + +COMMENT + : '/*' .*? '*/' -> channel(HIDDEN) + ; + +LINE_COMMENT + : '//' ~[\r\n]* -> channel(HIDDEN) + ; + +fragment ESC + : '\\' ([`'\\/fnrt] | UNICODE) // allow \`, \', \\, \/, \f, etc. and \uXXX + ; + +fragment UNICODE + : 'u' HEX HEX HEX HEX + ; + +fragment HEX + : [0-9a-fA-F] + ; \ No newline at end of file diff --git a/fhirpath/internal/grammar/fhirpath_lexer.go b/fhirpath/internal/grammar/fhirpath_lexer.go new file mode 100644 index 0000000..8d80b18 --- /dev/null +++ b/fhirpath/internal/grammar/fhirpath_lexer.go @@ -0,0 +1,410 @@ +// Code generated from fhirpath.g4 by ANTLR 4.13.0. DO NOT EDIT. + +package grammar + +import ( + "fmt" + "github.com/antlr4-go/antlr/v4" + "sync" + "unicode" +) + +// Suppress unused import error +var _ = fmt.Printf +var _ = sync.Once{} +var _ = unicode.IsLetter + +type fhirpathLexer struct { + *antlr.BaseLexer + channelNames []string + modeNames []string + // TODO: EOF string +} + +var FhirpathLexerLexerStaticData struct { + once sync.Once + serializedATN []int32 + ChannelNames []string + ModeNames []string + LiteralNames []string + SymbolicNames []string + RuleNames []string + PredictionContextCache *antlr.PredictionContextCache + atn *antlr.ATN + decisionToDFA []*antlr.DFA +} + +func fhirpathlexerLexerInit() { + staticData := &FhirpathLexerLexerStaticData + staticData.ChannelNames = []string{ + "DEFAULT_TOKEN_CHANNEL", "HIDDEN", + } + staticData.ModeNames = []string{ + "DEFAULT_MODE", + } + staticData.LiteralNames = []string{ + "", "'.'", "'['", "']'", "'+'", "'-'", "'*'", "'/'", "'div'", "'mod'", + "'&'", "'is'", "'as'", "'|'", "'<='", "'<'", "'>'", "'>='", "'='", "'~'", + "'!='", "'!~'", "'in'", "'contains'", "'and'", "'or'", "'xor'", "'implies'", + "'('", "')'", "'{'", "'}'", "'true'", "'false'", "'%'", "'$this'", "'$index'", + "'$total'", "','", "'year'", "'month'", "'week'", "'day'", "'hour'", + "'minute'", "'second'", "'millisecond'", "'years'", "'months'", "'weeks'", + "'days'", "'hours'", "'minutes'", "'seconds'", "'milliseconds'", + } + staticData.SymbolicNames = []string{ + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "DATE", "DATETIME", "TIME", "IDENTIFIER", "DELIMITEDIDENTIFIER", + "STRING", "NUMBER", "WS", "COMMENT", "LINE_COMMENT", + } + staticData.RuleNames = []string{ + "T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", "T__7", "T__8", + "T__9", "T__10", "T__11", "T__12", "T__13", "T__14", "T__15", "T__16", + "T__17", "T__18", "T__19", "T__20", "T__21", "T__22", "T__23", "T__24", + "T__25", "T__26", "T__27", "T__28", "T__29", "T__30", "T__31", "T__32", + "T__33", "T__34", "T__35", "T__36", "T__37", "T__38", "T__39", "T__40", + "T__41", "T__42", "T__43", "T__44", "T__45", "T__46", "T__47", "T__48", + "T__49", "T__50", "T__51", "T__52", "T__53", "DATE", "DATETIME", "TIME", + "DATEFORMAT", "TIMEFORMAT", "TIMEZONEOFFSETFORMAT", "IDENTIFIER", "DELIMITEDIDENTIFIER", + "STRING", "NUMBER", "WS", "COMMENT", "LINE_COMMENT", "ESC", "UNICODE", + "HEX", + } + staticData.PredictionContextCache = antlr.NewPredictionContextCache() + staticData.serializedATN = []int32{ + 4, 0, 64, 523, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, + 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, + 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, + 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, + 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, + 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, + 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, + 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, + 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, + 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, + 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, + 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, + 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, + 2, 68, 7, 68, 2, 69, 7, 69, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, + 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, + 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, + 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 15, 1, 15, 1, 16, 1, 16, + 1, 16, 1, 17, 1, 17, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, + 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, + 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 25, 1, + 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, + 1, 27, 1, 27, 1, 28, 1, 28, 1, 29, 1, 29, 1, 30, 1, 30, 1, 31, 1, 31, 1, + 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, + 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, + 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 37, + 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, + 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, + 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, + 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, + 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, + 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, + 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, + 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, + 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 52, 1, 52, 1, 52, 1, 52, 1, + 52, 1, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, + 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 55, 1, + 55, 1, 55, 1, 55, 1, 55, 3, 55, 386, 8, 55, 3, 55, 388, 8, 55, 1, 56, 1, + 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, + 1, 57, 1, 57, 3, 57, 404, 8, 57, 3, 57, 406, 8, 57, 1, 58, 1, 58, 1, 58, + 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 4, 58, 418, 8, 58, 11, + 58, 12, 58, 419, 3, 58, 422, 8, 58, 3, 58, 424, 8, 58, 3, 58, 426, 8, 58, + 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 3, 59, 435, 8, 59, 1, + 60, 3, 60, 438, 8, 60, 1, 60, 5, 60, 441, 8, 60, 10, 60, 12, 60, 444, 9, + 60, 1, 61, 1, 61, 1, 61, 5, 61, 449, 8, 61, 10, 61, 12, 61, 452, 9, 61, + 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 5, 62, 459, 8, 62, 10, 62, 12, 62, 462, + 9, 62, 1, 62, 1, 62, 1, 63, 4, 63, 467, 8, 63, 11, 63, 12, 63, 468, 1, + 63, 1, 63, 4, 63, 473, 8, 63, 11, 63, 12, 63, 474, 3, 63, 477, 8, 63, 1, + 64, 4, 64, 480, 8, 64, 11, 64, 12, 64, 481, 1, 64, 1, 64, 1, 65, 1, 65, + 1, 65, 1, 65, 5, 65, 490, 8, 65, 10, 65, 12, 65, 493, 9, 65, 1, 65, 1, + 65, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 1, 66, 5, 66, 504, 8, 66, + 10, 66, 12, 66, 507, 9, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 3, 67, 514, + 8, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 69, 1, 69, 3, 450, + 460, 491, 0, 70, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, + 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, + 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, + 27, 55, 28, 57, 29, 59, 30, 61, 31, 63, 32, 65, 33, 67, 34, 69, 35, 71, + 36, 73, 37, 75, 38, 77, 39, 79, 40, 81, 41, 83, 42, 85, 43, 87, 44, 89, + 45, 91, 46, 93, 47, 95, 48, 97, 49, 99, 50, 101, 51, 103, 52, 105, 53, + 107, 54, 109, 55, 111, 56, 113, 57, 115, 0, 117, 0, 119, 0, 121, 58, 123, + 59, 125, 60, 127, 61, 129, 62, 131, 63, 133, 64, 135, 0, 137, 0, 139, 0, + 1, 0, 8, 1, 0, 48, 57, 2, 0, 43, 43, 45, 45, 3, 0, 65, 90, 95, 95, 97, + 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, + 2, 0, 10, 10, 13, 13, 8, 0, 39, 39, 47, 47, 92, 92, 96, 96, 102, 102, 110, + 110, 114, 114, 116, 116, 3, 0, 48, 57, 65, 70, 97, 102, 537, 0, 1, 1, 0, + 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, + 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, + 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, + 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, + 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, + 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, + 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, + 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, + 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0, 0, 71, + 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, + 79, 1, 0, 0, 0, 0, 81, 1, 0, 0, 0, 0, 83, 1, 0, 0, 0, 0, 85, 1, 0, 0, 0, + 0, 87, 1, 0, 0, 0, 0, 89, 1, 0, 0, 0, 0, 91, 1, 0, 0, 0, 0, 93, 1, 0, 0, + 0, 0, 95, 1, 0, 0, 0, 0, 97, 1, 0, 0, 0, 0, 99, 1, 0, 0, 0, 0, 101, 1, + 0, 0, 0, 0, 103, 1, 0, 0, 0, 0, 105, 1, 0, 0, 0, 0, 107, 1, 0, 0, 0, 0, + 109, 1, 0, 0, 0, 0, 111, 1, 0, 0, 0, 0, 113, 1, 0, 0, 0, 0, 121, 1, 0, + 0, 0, 0, 123, 1, 0, 0, 0, 0, 125, 1, 0, 0, 0, 0, 127, 1, 0, 0, 0, 0, 129, + 1, 0, 0, 0, 0, 131, 1, 0, 0, 0, 0, 133, 1, 0, 0, 0, 1, 141, 1, 0, 0, 0, + 3, 143, 1, 0, 0, 0, 5, 145, 1, 0, 0, 0, 7, 147, 1, 0, 0, 0, 9, 149, 1, + 0, 0, 0, 11, 151, 1, 0, 0, 0, 13, 153, 1, 0, 0, 0, 15, 155, 1, 0, 0, 0, + 17, 159, 1, 0, 0, 0, 19, 163, 1, 0, 0, 0, 21, 165, 1, 0, 0, 0, 23, 168, + 1, 0, 0, 0, 25, 171, 1, 0, 0, 0, 27, 173, 1, 0, 0, 0, 29, 176, 1, 0, 0, + 0, 31, 178, 1, 0, 0, 0, 33, 180, 1, 0, 0, 0, 35, 183, 1, 0, 0, 0, 37, 185, + 1, 0, 0, 0, 39, 187, 1, 0, 0, 0, 41, 190, 1, 0, 0, 0, 43, 193, 1, 0, 0, + 0, 45, 196, 1, 0, 0, 0, 47, 205, 1, 0, 0, 0, 49, 209, 1, 0, 0, 0, 51, 212, + 1, 0, 0, 0, 53, 216, 1, 0, 0, 0, 55, 224, 1, 0, 0, 0, 57, 226, 1, 0, 0, + 0, 59, 228, 1, 0, 0, 0, 61, 230, 1, 0, 0, 0, 63, 232, 1, 0, 0, 0, 65, 237, + 1, 0, 0, 0, 67, 243, 1, 0, 0, 0, 69, 245, 1, 0, 0, 0, 71, 251, 1, 0, 0, + 0, 73, 258, 1, 0, 0, 0, 75, 265, 1, 0, 0, 0, 77, 267, 1, 0, 0, 0, 79, 272, + 1, 0, 0, 0, 81, 278, 1, 0, 0, 0, 83, 283, 1, 0, 0, 0, 85, 287, 1, 0, 0, + 0, 87, 292, 1, 0, 0, 0, 89, 299, 1, 0, 0, 0, 91, 306, 1, 0, 0, 0, 93, 318, + 1, 0, 0, 0, 95, 324, 1, 0, 0, 0, 97, 331, 1, 0, 0, 0, 99, 337, 1, 0, 0, + 0, 101, 342, 1, 0, 0, 0, 103, 348, 1, 0, 0, 0, 105, 356, 1, 0, 0, 0, 107, + 364, 1, 0, 0, 0, 109, 377, 1, 0, 0, 0, 111, 380, 1, 0, 0, 0, 113, 389, + 1, 0, 0, 0, 115, 393, 1, 0, 0, 0, 117, 407, 1, 0, 0, 0, 119, 434, 1, 0, + 0, 0, 121, 437, 1, 0, 0, 0, 123, 445, 1, 0, 0, 0, 125, 455, 1, 0, 0, 0, + 127, 466, 1, 0, 0, 0, 129, 479, 1, 0, 0, 0, 131, 485, 1, 0, 0, 0, 133, + 499, 1, 0, 0, 0, 135, 510, 1, 0, 0, 0, 137, 515, 1, 0, 0, 0, 139, 521, + 1, 0, 0, 0, 141, 142, 5, 46, 0, 0, 142, 2, 1, 0, 0, 0, 143, 144, 5, 91, + 0, 0, 144, 4, 1, 0, 0, 0, 145, 146, 5, 93, 0, 0, 146, 6, 1, 0, 0, 0, 147, + 148, 5, 43, 0, 0, 148, 8, 1, 0, 0, 0, 149, 150, 5, 45, 0, 0, 150, 10, 1, + 0, 0, 0, 151, 152, 5, 42, 0, 0, 152, 12, 1, 0, 0, 0, 153, 154, 5, 47, 0, + 0, 154, 14, 1, 0, 0, 0, 155, 156, 5, 100, 0, 0, 156, 157, 5, 105, 0, 0, + 157, 158, 5, 118, 0, 0, 158, 16, 1, 0, 0, 0, 159, 160, 5, 109, 0, 0, 160, + 161, 5, 111, 0, 0, 161, 162, 5, 100, 0, 0, 162, 18, 1, 0, 0, 0, 163, 164, + 5, 38, 0, 0, 164, 20, 1, 0, 0, 0, 165, 166, 5, 105, 0, 0, 166, 167, 5, + 115, 0, 0, 167, 22, 1, 0, 0, 0, 168, 169, 5, 97, 0, 0, 169, 170, 5, 115, + 0, 0, 170, 24, 1, 0, 0, 0, 171, 172, 5, 124, 0, 0, 172, 26, 1, 0, 0, 0, + 173, 174, 5, 60, 0, 0, 174, 175, 5, 61, 0, 0, 175, 28, 1, 0, 0, 0, 176, + 177, 5, 60, 0, 0, 177, 30, 1, 0, 0, 0, 178, 179, 5, 62, 0, 0, 179, 32, + 1, 0, 0, 0, 180, 181, 5, 62, 0, 0, 181, 182, 5, 61, 0, 0, 182, 34, 1, 0, + 0, 0, 183, 184, 5, 61, 0, 0, 184, 36, 1, 0, 0, 0, 185, 186, 5, 126, 0, + 0, 186, 38, 1, 0, 0, 0, 187, 188, 5, 33, 0, 0, 188, 189, 5, 61, 0, 0, 189, + 40, 1, 0, 0, 0, 190, 191, 5, 33, 0, 0, 191, 192, 5, 126, 0, 0, 192, 42, + 1, 0, 0, 0, 193, 194, 5, 105, 0, 0, 194, 195, 5, 110, 0, 0, 195, 44, 1, + 0, 0, 0, 196, 197, 5, 99, 0, 0, 197, 198, 5, 111, 0, 0, 198, 199, 5, 110, + 0, 0, 199, 200, 5, 116, 0, 0, 200, 201, 5, 97, 0, 0, 201, 202, 5, 105, + 0, 0, 202, 203, 5, 110, 0, 0, 203, 204, 5, 115, 0, 0, 204, 46, 1, 0, 0, + 0, 205, 206, 5, 97, 0, 0, 206, 207, 5, 110, 0, 0, 207, 208, 5, 100, 0, + 0, 208, 48, 1, 0, 0, 0, 209, 210, 5, 111, 0, 0, 210, 211, 5, 114, 0, 0, + 211, 50, 1, 0, 0, 0, 212, 213, 5, 120, 0, 0, 213, 214, 5, 111, 0, 0, 214, + 215, 5, 114, 0, 0, 215, 52, 1, 0, 0, 0, 216, 217, 5, 105, 0, 0, 217, 218, + 5, 109, 0, 0, 218, 219, 5, 112, 0, 0, 219, 220, 5, 108, 0, 0, 220, 221, + 5, 105, 0, 0, 221, 222, 5, 101, 0, 0, 222, 223, 5, 115, 0, 0, 223, 54, + 1, 0, 0, 0, 224, 225, 5, 40, 0, 0, 225, 56, 1, 0, 0, 0, 226, 227, 5, 41, + 0, 0, 227, 58, 1, 0, 0, 0, 228, 229, 5, 123, 0, 0, 229, 60, 1, 0, 0, 0, + 230, 231, 5, 125, 0, 0, 231, 62, 1, 0, 0, 0, 232, 233, 5, 116, 0, 0, 233, + 234, 5, 114, 0, 0, 234, 235, 5, 117, 0, 0, 235, 236, 5, 101, 0, 0, 236, + 64, 1, 0, 0, 0, 237, 238, 5, 102, 0, 0, 238, 239, 5, 97, 0, 0, 239, 240, + 5, 108, 0, 0, 240, 241, 5, 115, 0, 0, 241, 242, 5, 101, 0, 0, 242, 66, + 1, 0, 0, 0, 243, 244, 5, 37, 0, 0, 244, 68, 1, 0, 0, 0, 245, 246, 5, 36, + 0, 0, 246, 247, 5, 116, 0, 0, 247, 248, 5, 104, 0, 0, 248, 249, 5, 105, + 0, 0, 249, 250, 5, 115, 0, 0, 250, 70, 1, 0, 0, 0, 251, 252, 5, 36, 0, + 0, 252, 253, 5, 105, 0, 0, 253, 254, 5, 110, 0, 0, 254, 255, 5, 100, 0, + 0, 255, 256, 5, 101, 0, 0, 256, 257, 5, 120, 0, 0, 257, 72, 1, 0, 0, 0, + 258, 259, 5, 36, 0, 0, 259, 260, 5, 116, 0, 0, 260, 261, 5, 111, 0, 0, + 261, 262, 5, 116, 0, 0, 262, 263, 5, 97, 0, 0, 263, 264, 5, 108, 0, 0, + 264, 74, 1, 0, 0, 0, 265, 266, 5, 44, 0, 0, 266, 76, 1, 0, 0, 0, 267, 268, + 5, 121, 0, 0, 268, 269, 5, 101, 0, 0, 269, 270, 5, 97, 0, 0, 270, 271, + 5, 114, 0, 0, 271, 78, 1, 0, 0, 0, 272, 273, 5, 109, 0, 0, 273, 274, 5, + 111, 0, 0, 274, 275, 5, 110, 0, 0, 275, 276, 5, 116, 0, 0, 276, 277, 5, + 104, 0, 0, 277, 80, 1, 0, 0, 0, 278, 279, 5, 119, 0, 0, 279, 280, 5, 101, + 0, 0, 280, 281, 5, 101, 0, 0, 281, 282, 5, 107, 0, 0, 282, 82, 1, 0, 0, + 0, 283, 284, 5, 100, 0, 0, 284, 285, 5, 97, 0, 0, 285, 286, 5, 121, 0, + 0, 286, 84, 1, 0, 0, 0, 287, 288, 5, 104, 0, 0, 288, 289, 5, 111, 0, 0, + 289, 290, 5, 117, 0, 0, 290, 291, 5, 114, 0, 0, 291, 86, 1, 0, 0, 0, 292, + 293, 5, 109, 0, 0, 293, 294, 5, 105, 0, 0, 294, 295, 5, 110, 0, 0, 295, + 296, 5, 117, 0, 0, 296, 297, 5, 116, 0, 0, 297, 298, 5, 101, 0, 0, 298, + 88, 1, 0, 0, 0, 299, 300, 5, 115, 0, 0, 300, 301, 5, 101, 0, 0, 301, 302, + 5, 99, 0, 0, 302, 303, 5, 111, 0, 0, 303, 304, 5, 110, 0, 0, 304, 305, + 5, 100, 0, 0, 305, 90, 1, 0, 0, 0, 306, 307, 5, 109, 0, 0, 307, 308, 5, + 105, 0, 0, 308, 309, 5, 108, 0, 0, 309, 310, 5, 108, 0, 0, 310, 311, 5, + 105, 0, 0, 311, 312, 5, 115, 0, 0, 312, 313, 5, 101, 0, 0, 313, 314, 5, + 99, 0, 0, 314, 315, 5, 111, 0, 0, 315, 316, 5, 110, 0, 0, 316, 317, 5, + 100, 0, 0, 317, 92, 1, 0, 0, 0, 318, 319, 5, 121, 0, 0, 319, 320, 5, 101, + 0, 0, 320, 321, 5, 97, 0, 0, 321, 322, 5, 114, 0, 0, 322, 323, 5, 115, + 0, 0, 323, 94, 1, 0, 0, 0, 324, 325, 5, 109, 0, 0, 325, 326, 5, 111, 0, + 0, 326, 327, 5, 110, 0, 0, 327, 328, 5, 116, 0, 0, 328, 329, 5, 104, 0, + 0, 329, 330, 5, 115, 0, 0, 330, 96, 1, 0, 0, 0, 331, 332, 5, 119, 0, 0, + 332, 333, 5, 101, 0, 0, 333, 334, 5, 101, 0, 0, 334, 335, 5, 107, 0, 0, + 335, 336, 5, 115, 0, 0, 336, 98, 1, 0, 0, 0, 337, 338, 5, 100, 0, 0, 338, + 339, 5, 97, 0, 0, 339, 340, 5, 121, 0, 0, 340, 341, 5, 115, 0, 0, 341, + 100, 1, 0, 0, 0, 342, 343, 5, 104, 0, 0, 343, 344, 5, 111, 0, 0, 344, 345, + 5, 117, 0, 0, 345, 346, 5, 114, 0, 0, 346, 347, 5, 115, 0, 0, 347, 102, + 1, 0, 0, 0, 348, 349, 5, 109, 0, 0, 349, 350, 5, 105, 0, 0, 350, 351, 5, + 110, 0, 0, 351, 352, 5, 117, 0, 0, 352, 353, 5, 116, 0, 0, 353, 354, 5, + 101, 0, 0, 354, 355, 5, 115, 0, 0, 355, 104, 1, 0, 0, 0, 356, 357, 5, 115, + 0, 0, 357, 358, 5, 101, 0, 0, 358, 359, 5, 99, 0, 0, 359, 360, 5, 111, + 0, 0, 360, 361, 5, 110, 0, 0, 361, 362, 5, 100, 0, 0, 362, 363, 5, 115, + 0, 0, 363, 106, 1, 0, 0, 0, 364, 365, 5, 109, 0, 0, 365, 366, 5, 105, 0, + 0, 366, 367, 5, 108, 0, 0, 367, 368, 5, 108, 0, 0, 368, 369, 5, 105, 0, + 0, 369, 370, 5, 115, 0, 0, 370, 371, 5, 101, 0, 0, 371, 372, 5, 99, 0, + 0, 372, 373, 5, 111, 0, 0, 373, 374, 5, 110, 0, 0, 374, 375, 5, 100, 0, + 0, 375, 376, 5, 115, 0, 0, 376, 108, 1, 0, 0, 0, 377, 378, 5, 64, 0, 0, + 378, 379, 3, 115, 57, 0, 379, 110, 1, 0, 0, 0, 380, 381, 5, 64, 0, 0, 381, + 382, 3, 115, 57, 0, 382, 387, 5, 84, 0, 0, 383, 385, 3, 117, 58, 0, 384, + 386, 3, 119, 59, 0, 385, 384, 1, 0, 0, 0, 385, 386, 1, 0, 0, 0, 386, 388, + 1, 0, 0, 0, 387, 383, 1, 0, 0, 0, 387, 388, 1, 0, 0, 0, 388, 112, 1, 0, + 0, 0, 389, 390, 5, 64, 0, 0, 390, 391, 5, 84, 0, 0, 391, 392, 3, 117, 58, + 0, 392, 114, 1, 0, 0, 0, 393, 394, 7, 0, 0, 0, 394, 395, 7, 0, 0, 0, 395, + 396, 7, 0, 0, 0, 396, 405, 7, 0, 0, 0, 397, 398, 5, 45, 0, 0, 398, 399, + 7, 0, 0, 0, 399, 403, 7, 0, 0, 0, 400, 401, 5, 45, 0, 0, 401, 402, 7, 0, + 0, 0, 402, 404, 7, 0, 0, 0, 403, 400, 1, 0, 0, 0, 403, 404, 1, 0, 0, 0, + 404, 406, 1, 0, 0, 0, 405, 397, 1, 0, 0, 0, 405, 406, 1, 0, 0, 0, 406, + 116, 1, 0, 0, 0, 407, 408, 7, 0, 0, 0, 408, 425, 7, 0, 0, 0, 409, 410, + 5, 58, 0, 0, 410, 411, 7, 0, 0, 0, 411, 423, 7, 0, 0, 0, 412, 413, 5, 58, + 0, 0, 413, 414, 7, 0, 0, 0, 414, 421, 7, 0, 0, 0, 415, 417, 5, 46, 0, 0, + 416, 418, 7, 0, 0, 0, 417, 416, 1, 0, 0, 0, 418, 419, 1, 0, 0, 0, 419, + 417, 1, 0, 0, 0, 419, 420, 1, 0, 0, 0, 420, 422, 1, 0, 0, 0, 421, 415, + 1, 0, 0, 0, 421, 422, 1, 0, 0, 0, 422, 424, 1, 0, 0, 0, 423, 412, 1, 0, + 0, 0, 423, 424, 1, 0, 0, 0, 424, 426, 1, 0, 0, 0, 425, 409, 1, 0, 0, 0, + 425, 426, 1, 0, 0, 0, 426, 118, 1, 0, 0, 0, 427, 435, 5, 90, 0, 0, 428, + 429, 7, 1, 0, 0, 429, 430, 7, 0, 0, 0, 430, 431, 7, 0, 0, 0, 431, 432, + 5, 58, 0, 0, 432, 433, 7, 0, 0, 0, 433, 435, 7, 0, 0, 0, 434, 427, 1, 0, + 0, 0, 434, 428, 1, 0, 0, 0, 435, 120, 1, 0, 0, 0, 436, 438, 7, 2, 0, 0, + 437, 436, 1, 0, 0, 0, 438, 442, 1, 0, 0, 0, 439, 441, 7, 3, 0, 0, 440, + 439, 1, 0, 0, 0, 441, 444, 1, 0, 0, 0, 442, 440, 1, 0, 0, 0, 442, 443, + 1, 0, 0, 0, 443, 122, 1, 0, 0, 0, 444, 442, 1, 0, 0, 0, 445, 450, 5, 96, + 0, 0, 446, 449, 3, 135, 67, 0, 447, 449, 9, 0, 0, 0, 448, 446, 1, 0, 0, + 0, 448, 447, 1, 0, 0, 0, 449, 452, 1, 0, 0, 0, 450, 451, 1, 0, 0, 0, 450, + 448, 1, 0, 0, 0, 451, 453, 1, 0, 0, 0, 452, 450, 1, 0, 0, 0, 453, 454, + 5, 96, 0, 0, 454, 124, 1, 0, 0, 0, 455, 460, 5, 39, 0, 0, 456, 459, 3, + 135, 67, 0, 457, 459, 9, 0, 0, 0, 458, 456, 1, 0, 0, 0, 458, 457, 1, 0, + 0, 0, 459, 462, 1, 0, 0, 0, 460, 461, 1, 0, 0, 0, 460, 458, 1, 0, 0, 0, + 461, 463, 1, 0, 0, 0, 462, 460, 1, 0, 0, 0, 463, 464, 5, 39, 0, 0, 464, + 126, 1, 0, 0, 0, 465, 467, 7, 0, 0, 0, 466, 465, 1, 0, 0, 0, 467, 468, + 1, 0, 0, 0, 468, 466, 1, 0, 0, 0, 468, 469, 1, 0, 0, 0, 469, 476, 1, 0, + 0, 0, 470, 472, 5, 46, 0, 0, 471, 473, 7, 0, 0, 0, 472, 471, 1, 0, 0, 0, + 473, 474, 1, 0, 0, 0, 474, 472, 1, 0, 0, 0, 474, 475, 1, 0, 0, 0, 475, + 477, 1, 0, 0, 0, 476, 470, 1, 0, 0, 0, 476, 477, 1, 0, 0, 0, 477, 128, + 1, 0, 0, 0, 478, 480, 7, 4, 0, 0, 479, 478, 1, 0, 0, 0, 480, 481, 1, 0, + 0, 0, 481, 479, 1, 0, 0, 0, 481, 482, 1, 0, 0, 0, 482, 483, 1, 0, 0, 0, + 483, 484, 6, 64, 0, 0, 484, 130, 1, 0, 0, 0, 485, 486, 5, 47, 0, 0, 486, + 487, 5, 42, 0, 0, 487, 491, 1, 0, 0, 0, 488, 490, 9, 0, 0, 0, 489, 488, + 1, 0, 0, 0, 490, 493, 1, 0, 0, 0, 491, 492, 1, 0, 0, 0, 491, 489, 1, 0, + 0, 0, 492, 494, 1, 0, 0, 0, 493, 491, 1, 0, 0, 0, 494, 495, 5, 42, 0, 0, + 495, 496, 5, 47, 0, 0, 496, 497, 1, 0, 0, 0, 497, 498, 6, 65, 0, 0, 498, + 132, 1, 0, 0, 0, 499, 500, 5, 47, 0, 0, 500, 501, 5, 47, 0, 0, 501, 505, + 1, 0, 0, 0, 502, 504, 8, 5, 0, 0, 503, 502, 1, 0, 0, 0, 504, 507, 1, 0, + 0, 0, 505, 503, 1, 0, 0, 0, 505, 506, 1, 0, 0, 0, 506, 508, 1, 0, 0, 0, + 507, 505, 1, 0, 0, 0, 508, 509, 6, 66, 0, 0, 509, 134, 1, 0, 0, 0, 510, + 513, 5, 92, 0, 0, 511, 514, 7, 6, 0, 0, 512, 514, 3, 137, 68, 0, 513, 511, + 1, 0, 0, 0, 513, 512, 1, 0, 0, 0, 514, 136, 1, 0, 0, 0, 515, 516, 5, 117, + 0, 0, 516, 517, 3, 139, 69, 0, 517, 518, 3, 139, 69, 0, 518, 519, 3, 139, + 69, 0, 519, 520, 3, 139, 69, 0, 520, 138, 1, 0, 0, 0, 521, 522, 7, 7, 0, + 0, 522, 140, 1, 0, 0, 0, 24, 0, 385, 387, 403, 405, 419, 421, 423, 425, + 434, 437, 440, 442, 448, 450, 458, 460, 468, 474, 476, 481, 491, 505, 513, + 1, 0, 1, 0, + } + deserializer := antlr.NewATNDeserializer(nil) + staticData.atn = deserializer.Deserialize(staticData.serializedATN) + atn := staticData.atn + staticData.decisionToDFA = make([]*antlr.DFA, len(atn.DecisionToState)) + decisionToDFA := staticData.decisionToDFA + for index, state := range atn.DecisionToState { + decisionToDFA[index] = antlr.NewDFA(state, index) + } +} + +// fhirpathLexerInit initializes any static state used to implement fhirpathLexer. By default the +// static state used to implement the lexer is lazily initialized during the first call to +// NewfhirpathLexer(). You can call this function if you wish to initialize the static state ahead +// of time. +func FhirpathLexerInit() { + staticData := &FhirpathLexerLexerStaticData + staticData.once.Do(fhirpathlexerLexerInit) +} + +// NewfhirpathLexer produces a new lexer instance for the optional input antlr.CharStream. +func NewfhirpathLexer(input antlr.CharStream) *fhirpathLexer { + FhirpathLexerInit() + l := new(fhirpathLexer) + l.BaseLexer = antlr.NewBaseLexer(input) + staticData := &FhirpathLexerLexerStaticData + l.Interpreter = antlr.NewLexerATNSimulator(l, staticData.atn, staticData.decisionToDFA, staticData.PredictionContextCache) + l.channelNames = staticData.ChannelNames + l.modeNames = staticData.ModeNames + l.RuleNames = staticData.RuleNames + l.LiteralNames = staticData.LiteralNames + l.SymbolicNames = staticData.SymbolicNames + l.GrammarFileName = "fhirpath.g4" + // TODO: l.EOF = antlr.TokenEOF + + return l +} + +// fhirpathLexer tokens. +const ( + fhirpathLexerT__0 = 1 + fhirpathLexerT__1 = 2 + fhirpathLexerT__2 = 3 + fhirpathLexerT__3 = 4 + fhirpathLexerT__4 = 5 + fhirpathLexerT__5 = 6 + fhirpathLexerT__6 = 7 + fhirpathLexerT__7 = 8 + fhirpathLexerT__8 = 9 + fhirpathLexerT__9 = 10 + fhirpathLexerT__10 = 11 + fhirpathLexerT__11 = 12 + fhirpathLexerT__12 = 13 + fhirpathLexerT__13 = 14 + fhirpathLexerT__14 = 15 + fhirpathLexerT__15 = 16 + fhirpathLexerT__16 = 17 + fhirpathLexerT__17 = 18 + fhirpathLexerT__18 = 19 + fhirpathLexerT__19 = 20 + fhirpathLexerT__20 = 21 + fhirpathLexerT__21 = 22 + fhirpathLexerT__22 = 23 + fhirpathLexerT__23 = 24 + fhirpathLexerT__24 = 25 + fhirpathLexerT__25 = 26 + fhirpathLexerT__26 = 27 + fhirpathLexerT__27 = 28 + fhirpathLexerT__28 = 29 + fhirpathLexerT__29 = 30 + fhirpathLexerT__30 = 31 + fhirpathLexerT__31 = 32 + fhirpathLexerT__32 = 33 + fhirpathLexerT__33 = 34 + fhirpathLexerT__34 = 35 + fhirpathLexerT__35 = 36 + fhirpathLexerT__36 = 37 + fhirpathLexerT__37 = 38 + fhirpathLexerT__38 = 39 + fhirpathLexerT__39 = 40 + fhirpathLexerT__40 = 41 + fhirpathLexerT__41 = 42 + fhirpathLexerT__42 = 43 + fhirpathLexerT__43 = 44 + fhirpathLexerT__44 = 45 + fhirpathLexerT__45 = 46 + fhirpathLexerT__46 = 47 + fhirpathLexerT__47 = 48 + fhirpathLexerT__48 = 49 + fhirpathLexerT__49 = 50 + fhirpathLexerT__50 = 51 + fhirpathLexerT__51 = 52 + fhirpathLexerT__52 = 53 + fhirpathLexerT__53 = 54 + fhirpathLexerDATE = 55 + fhirpathLexerDATETIME = 56 + fhirpathLexerTIME = 57 + fhirpathLexerIDENTIFIER = 58 + fhirpathLexerDELIMITEDIDENTIFIER = 59 + fhirpathLexerSTRING = 60 + fhirpathLexerNUMBER = 61 + fhirpathLexerWS = 62 + fhirpathLexerCOMMENT = 63 + fhirpathLexerLINE_COMMENT = 64 +) diff --git a/fhirpath/internal/grammar/fhirpath_parser.go b/fhirpath/internal/grammar/fhirpath_parser.go new file mode 100644 index 0000000..22a2909 --- /dev/null +++ b/fhirpath/internal/grammar/fhirpath_parser.go @@ -0,0 +1,4109 @@ +// Code generated from fhirpath.g4 by ANTLR 4.13.0. DO NOT EDIT. + +package grammar // fhirpath +import ( + "fmt" + "strconv" + "sync" + + "github.com/antlr4-go/antlr/v4" +) + +// Suppress unused import errors +var _ = fmt.Printf +var _ = strconv.Itoa +var _ = sync.Once{} + +type fhirpathParser struct { + *antlr.BaseParser +} + +var FhirpathParserStaticData struct { + once sync.Once + serializedATN []int32 + LiteralNames []string + SymbolicNames []string + RuleNames []string + PredictionContextCache *antlr.PredictionContextCache + atn *antlr.ATN + decisionToDFA []*antlr.DFA +} + +func fhirpathParserInit() { + staticData := &FhirpathParserStaticData + staticData.LiteralNames = []string{ + "", "'.'", "'['", "']'", "'+'", "'-'", "'*'", "'/'", "'div'", "'mod'", + "'&'", "'is'", "'as'", "'|'", "'<='", "'<'", "'>'", "'>='", "'='", "'~'", + "'!='", "'!~'", "'in'", "'contains'", "'and'", "'or'", "'xor'", "'implies'", + "'('", "')'", "'{'", "'}'", "'true'", "'false'", "'%'", "'$this'", "'$index'", + "'$total'", "','", "'year'", "'month'", "'week'", "'day'", "'hour'", + "'minute'", "'second'", "'millisecond'", "'years'", "'months'", "'weeks'", + "'days'", "'hours'", "'minutes'", "'seconds'", "'milliseconds'", + } + staticData.SymbolicNames = []string{ + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "DATE", "DATETIME", "TIME", "IDENTIFIER", "DELIMITEDIDENTIFIER", + "STRING", "NUMBER", "WS", "COMMENT", "LINE_COMMENT", + } + staticData.RuleNames = []string{ + "prog", "expression", "term", "literal", "externalConstant", "invocation", + "function", "paramList", "quantity", "unit", "dateTimePrecision", "pluralDateTimePrecision", + "typeSpecifier", "qualifiedIdentifier", "identifier", + } + staticData.PredictionContextCache = antlr.NewPredictionContextCache() + staticData.serializedATN = []int32{ + 4, 1, 64, 155, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, + 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, + 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 1, 0, 1, 0, + 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 38, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 78, 8, 1, + 10, 1, 12, 1, 81, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, + 90, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 101, + 8, 3, 1, 4, 1, 4, 1, 4, 3, 4, 106, 8, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, + 3, 5, 113, 8, 5, 1, 6, 1, 6, 1, 6, 3, 6, 118, 8, 6, 1, 6, 1, 6, 1, 7, 1, + 7, 1, 7, 5, 7, 125, 8, 7, 10, 7, 12, 7, 128, 9, 7, 1, 8, 1, 8, 3, 8, 132, + 8, 8, 1, 9, 1, 9, 1, 9, 3, 9, 137, 8, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, + 12, 1, 12, 1, 13, 1, 13, 1, 13, 5, 13, 148, 8, 13, 10, 13, 12, 13, 151, + 9, 13, 1, 14, 1, 14, 1, 14, 0, 1, 2, 15, 0, 2, 4, 6, 8, 10, 12, 14, 16, + 18, 20, 22, 24, 26, 28, 0, 12, 1, 0, 4, 5, 1, 0, 6, 9, 2, 0, 4, 5, 10, + 10, 1, 0, 14, 17, 1, 0, 18, 21, 1, 0, 22, 23, 1, 0, 25, 26, 1, 0, 11, 12, + 1, 0, 32, 33, 1, 0, 39, 46, 1, 0, 47, 54, 3, 0, 11, 12, 22, 23, 58, 59, + 173, 0, 30, 1, 0, 0, 0, 2, 37, 1, 0, 0, 0, 4, 89, 1, 0, 0, 0, 6, 100, 1, + 0, 0, 0, 8, 102, 1, 0, 0, 0, 10, 112, 1, 0, 0, 0, 12, 114, 1, 0, 0, 0, + 14, 121, 1, 0, 0, 0, 16, 129, 1, 0, 0, 0, 18, 136, 1, 0, 0, 0, 20, 138, + 1, 0, 0, 0, 22, 140, 1, 0, 0, 0, 24, 142, 1, 0, 0, 0, 26, 144, 1, 0, 0, + 0, 28, 152, 1, 0, 0, 0, 30, 31, 3, 2, 1, 0, 31, 32, 5, 0, 0, 1, 32, 1, + 1, 0, 0, 0, 33, 34, 6, 1, -1, 0, 34, 38, 3, 4, 2, 0, 35, 36, 7, 0, 0, 0, + 36, 38, 3, 2, 1, 11, 37, 33, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 38, 79, 1, + 0, 0, 0, 39, 40, 10, 10, 0, 0, 40, 41, 7, 1, 0, 0, 41, 78, 3, 2, 1, 11, + 42, 43, 10, 9, 0, 0, 43, 44, 7, 2, 0, 0, 44, 78, 3, 2, 1, 10, 45, 46, 10, + 7, 0, 0, 46, 47, 5, 13, 0, 0, 47, 78, 3, 2, 1, 8, 48, 49, 10, 6, 0, 0, + 49, 50, 7, 3, 0, 0, 50, 78, 3, 2, 1, 7, 51, 52, 10, 5, 0, 0, 52, 53, 7, + 4, 0, 0, 53, 78, 3, 2, 1, 6, 54, 55, 10, 4, 0, 0, 55, 56, 7, 5, 0, 0, 56, + 78, 3, 2, 1, 5, 57, 58, 10, 3, 0, 0, 58, 59, 5, 24, 0, 0, 59, 78, 3, 2, + 1, 4, 60, 61, 10, 2, 0, 0, 61, 62, 7, 6, 0, 0, 62, 78, 3, 2, 1, 3, 63, + 64, 10, 1, 0, 0, 64, 65, 5, 27, 0, 0, 65, 78, 3, 2, 1, 2, 66, 67, 10, 13, + 0, 0, 67, 68, 5, 1, 0, 0, 68, 78, 3, 10, 5, 0, 69, 70, 10, 12, 0, 0, 70, + 71, 5, 2, 0, 0, 71, 72, 3, 2, 1, 0, 72, 73, 5, 3, 0, 0, 73, 78, 1, 0, 0, + 0, 74, 75, 10, 8, 0, 0, 75, 76, 7, 7, 0, 0, 76, 78, 3, 24, 12, 0, 77, 39, + 1, 0, 0, 0, 77, 42, 1, 0, 0, 0, 77, 45, 1, 0, 0, 0, 77, 48, 1, 0, 0, 0, + 77, 51, 1, 0, 0, 0, 77, 54, 1, 0, 0, 0, 77, 57, 1, 0, 0, 0, 77, 60, 1, + 0, 0, 0, 77, 63, 1, 0, 0, 0, 77, 66, 1, 0, 0, 0, 77, 69, 1, 0, 0, 0, 77, + 74, 1, 0, 0, 0, 78, 81, 1, 0, 0, 0, 79, 77, 1, 0, 0, 0, 79, 80, 1, 0, 0, + 0, 80, 3, 1, 0, 0, 0, 81, 79, 1, 0, 0, 0, 82, 90, 3, 10, 5, 0, 83, 90, + 3, 6, 3, 0, 84, 90, 3, 8, 4, 0, 85, 86, 5, 28, 0, 0, 86, 87, 3, 2, 1, 0, + 87, 88, 5, 29, 0, 0, 88, 90, 1, 0, 0, 0, 89, 82, 1, 0, 0, 0, 89, 83, 1, + 0, 0, 0, 89, 84, 1, 0, 0, 0, 89, 85, 1, 0, 0, 0, 90, 5, 1, 0, 0, 0, 91, + 92, 5, 30, 0, 0, 92, 101, 5, 31, 0, 0, 93, 101, 7, 8, 0, 0, 94, 101, 5, + 60, 0, 0, 95, 101, 5, 61, 0, 0, 96, 101, 5, 55, 0, 0, 97, 101, 5, 56, 0, + 0, 98, 101, 5, 57, 0, 0, 99, 101, 3, 16, 8, 0, 100, 91, 1, 0, 0, 0, 100, + 93, 1, 0, 0, 0, 100, 94, 1, 0, 0, 0, 100, 95, 1, 0, 0, 0, 100, 96, 1, 0, + 0, 0, 100, 97, 1, 0, 0, 0, 100, 98, 1, 0, 0, 0, 100, 99, 1, 0, 0, 0, 101, + 7, 1, 0, 0, 0, 102, 105, 5, 34, 0, 0, 103, 106, 3, 28, 14, 0, 104, 106, + 5, 60, 0, 0, 105, 103, 1, 0, 0, 0, 105, 104, 1, 0, 0, 0, 106, 9, 1, 0, + 0, 0, 107, 113, 3, 28, 14, 0, 108, 113, 3, 12, 6, 0, 109, 113, 5, 35, 0, + 0, 110, 113, 5, 36, 0, 0, 111, 113, 5, 37, 0, 0, 112, 107, 1, 0, 0, 0, + 112, 108, 1, 0, 0, 0, 112, 109, 1, 0, 0, 0, 112, 110, 1, 0, 0, 0, 112, + 111, 1, 0, 0, 0, 113, 11, 1, 0, 0, 0, 114, 115, 3, 28, 14, 0, 115, 117, + 5, 28, 0, 0, 116, 118, 3, 14, 7, 0, 117, 116, 1, 0, 0, 0, 117, 118, 1, + 0, 0, 0, 118, 119, 1, 0, 0, 0, 119, 120, 5, 29, 0, 0, 120, 13, 1, 0, 0, + 0, 121, 126, 3, 2, 1, 0, 122, 123, 5, 38, 0, 0, 123, 125, 3, 2, 1, 0, 124, + 122, 1, 0, 0, 0, 125, 128, 1, 0, 0, 0, 126, 124, 1, 0, 0, 0, 126, 127, + 1, 0, 0, 0, 127, 15, 1, 0, 0, 0, 128, 126, 1, 0, 0, 0, 129, 131, 5, 61, + 0, 0, 130, 132, 3, 18, 9, 0, 131, 130, 1, 0, 0, 0, 131, 132, 1, 0, 0, 0, + 132, 17, 1, 0, 0, 0, 133, 137, 3, 20, 10, 0, 134, 137, 3, 22, 11, 0, 135, + 137, 5, 60, 0, 0, 136, 133, 1, 0, 0, 0, 136, 134, 1, 0, 0, 0, 136, 135, + 1, 0, 0, 0, 137, 19, 1, 0, 0, 0, 138, 139, 7, 9, 0, 0, 139, 21, 1, 0, 0, + 0, 140, 141, 7, 10, 0, 0, 141, 23, 1, 0, 0, 0, 142, 143, 3, 26, 13, 0, + 143, 25, 1, 0, 0, 0, 144, 149, 3, 28, 14, 0, 145, 146, 5, 1, 0, 0, 146, + 148, 3, 28, 14, 0, 147, 145, 1, 0, 0, 0, 148, 151, 1, 0, 0, 0, 149, 147, + 1, 0, 0, 0, 149, 150, 1, 0, 0, 0, 150, 27, 1, 0, 0, 0, 151, 149, 1, 0, + 0, 0, 152, 153, 7, 11, 0, 0, 153, 29, 1, 0, 0, 0, 12, 37, 77, 79, 89, 100, + 105, 112, 117, 126, 131, 136, 149, + } + deserializer := antlr.NewATNDeserializer(nil) + staticData.atn = deserializer.Deserialize(staticData.serializedATN) + atn := staticData.atn + staticData.decisionToDFA = make([]*antlr.DFA, len(atn.DecisionToState)) + decisionToDFA := staticData.decisionToDFA + for index, state := range atn.DecisionToState { + decisionToDFA[index] = antlr.NewDFA(state, index) + } +} + +// fhirpathParserInit initializes any static state used to implement fhirpathParser. By default the +// static state used to implement the parser is lazily initialized during the first call to +// NewfhirpathParser(). You can call this function if you wish to initialize the static state ahead +// of time. +func FhirpathParserInit() { + staticData := &FhirpathParserStaticData + staticData.once.Do(fhirpathParserInit) +} + +// NewfhirpathParser produces a new parser instance for the optional input antlr.TokenStream. +func NewfhirpathParser(input antlr.TokenStream) *fhirpathParser { + FhirpathParserInit() + this := new(fhirpathParser) + this.BaseParser = antlr.NewBaseParser(input) + staticData := &FhirpathParserStaticData + this.Interpreter = antlr.NewParserATNSimulator(this, staticData.atn, staticData.decisionToDFA, staticData.PredictionContextCache) + this.RuleNames = staticData.RuleNames + this.LiteralNames = staticData.LiteralNames + this.SymbolicNames = staticData.SymbolicNames + this.GrammarFileName = "fhirpath.g4" + + return this +} + +// fhirpathParser tokens. +const ( + fhirpathParserEOF = antlr.TokenEOF + fhirpathParserT__0 = 1 + fhirpathParserT__1 = 2 + fhirpathParserT__2 = 3 + fhirpathParserT__3 = 4 + fhirpathParserT__4 = 5 + fhirpathParserT__5 = 6 + fhirpathParserT__6 = 7 + fhirpathParserT__7 = 8 + fhirpathParserT__8 = 9 + fhirpathParserT__9 = 10 + fhirpathParserT__10 = 11 + fhirpathParserT__11 = 12 + fhirpathParserT__12 = 13 + fhirpathParserT__13 = 14 + fhirpathParserT__14 = 15 + fhirpathParserT__15 = 16 + fhirpathParserT__16 = 17 + fhirpathParserT__17 = 18 + fhirpathParserT__18 = 19 + fhirpathParserT__19 = 20 + fhirpathParserT__20 = 21 + fhirpathParserT__21 = 22 + fhirpathParserT__22 = 23 + fhirpathParserT__23 = 24 + fhirpathParserT__24 = 25 + fhirpathParserT__25 = 26 + fhirpathParserT__26 = 27 + fhirpathParserT__27 = 28 + fhirpathParserT__28 = 29 + fhirpathParserT__29 = 30 + fhirpathParserT__30 = 31 + fhirpathParserT__31 = 32 + fhirpathParserT__32 = 33 + fhirpathParserT__33 = 34 + fhirpathParserT__34 = 35 + fhirpathParserT__35 = 36 + fhirpathParserT__36 = 37 + fhirpathParserT__37 = 38 + fhirpathParserT__38 = 39 + fhirpathParserT__39 = 40 + fhirpathParserT__40 = 41 + fhirpathParserT__41 = 42 + fhirpathParserT__42 = 43 + fhirpathParserT__43 = 44 + fhirpathParserT__44 = 45 + fhirpathParserT__45 = 46 + fhirpathParserT__46 = 47 + fhirpathParserT__47 = 48 + fhirpathParserT__48 = 49 + fhirpathParserT__49 = 50 + fhirpathParserT__50 = 51 + fhirpathParserT__51 = 52 + fhirpathParserT__52 = 53 + fhirpathParserT__53 = 54 + fhirpathParserDATE = 55 + fhirpathParserDATETIME = 56 + fhirpathParserTIME = 57 + fhirpathParserIDENTIFIER = 58 + fhirpathParserDELIMITEDIDENTIFIER = 59 + fhirpathParserSTRING = 60 + fhirpathParserNUMBER = 61 + fhirpathParserWS = 62 + fhirpathParserCOMMENT = 63 + fhirpathParserLINE_COMMENT = 64 +) + +// fhirpathParser rules. +const ( + fhirpathParserRULE_prog = 0 + fhirpathParserRULE_expression = 1 + fhirpathParserRULE_term = 2 + fhirpathParserRULE_literal = 3 + fhirpathParserRULE_externalConstant = 4 + fhirpathParserRULE_invocation = 5 + fhirpathParserRULE_function = 6 + fhirpathParserRULE_paramList = 7 + fhirpathParserRULE_quantity = 8 + fhirpathParserRULE_unit = 9 + fhirpathParserRULE_dateTimePrecision = 10 + fhirpathParserRULE_pluralDateTimePrecision = 11 + fhirpathParserRULE_typeSpecifier = 12 + fhirpathParserRULE_qualifiedIdentifier = 13 + fhirpathParserRULE_identifier = 14 +) + +// IProgContext is an interface to support dynamic dispatch. +type IProgContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + Expression() IExpressionContext + EOF() antlr.TerminalNode + + // IsProgContext differentiates from other interfaces. + IsProgContext() +} + +type ProgContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyProgContext() *ProgContext { + var p = new(ProgContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_prog + return p +} + +func InitEmptyProgContext(p *ProgContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_prog +} + +func (*ProgContext) IsProgContext() {} + +func NewProgContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *ProgContext { + var p = new(ProgContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_prog + + return p +} + +func (s *ProgContext) GetParser() antlr.Parser { return s.parser } + +func (s *ProgContext) Expression() IExpressionContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *ProgContext) EOF() antlr.TerminalNode { + return s.GetToken(fhirpathParserEOF, 0) +} + +func (s *ProgContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *ProgContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *ProgContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitProg(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Prog() (localctx IProgContext) { + localctx = NewProgContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 0, fhirpathParserRULE_prog) + p.EnterOuterAlt(localctx, 1) + { + p.SetState(30) + p.expression(0) + } + { + p.SetState(31) + p.Match(fhirpathParserEOF) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IExpressionContext is an interface to support dynamic dispatch. +type IExpressionContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + // IsExpressionContext differentiates from other interfaces. + IsExpressionContext() +} + +type ExpressionContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyExpressionContext() *ExpressionContext { + var p = new(ExpressionContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_expression + return p +} + +func InitEmptyExpressionContext(p *ExpressionContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_expression +} + +func (*ExpressionContext) IsExpressionContext() {} + +func NewExpressionContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *ExpressionContext { + var p = new(ExpressionContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_expression + + return p +} + +func (s *ExpressionContext) GetParser() antlr.Parser { return s.parser } + +func (s *ExpressionContext) CopyAll(ctx *ExpressionContext) { + s.CopyFrom(&ctx.BaseParserRuleContext) +} + +func (s *ExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *ExpressionContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +type IndexerExpressionContext struct { + ExpressionContext +} + +func NewIndexerExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *IndexerExpressionContext { + var p = new(IndexerExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *IndexerExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *IndexerExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *IndexerExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *IndexerExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitIndexerExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type PolarityExpressionContext struct { + ExpressionContext +} + +func NewPolarityExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *PolarityExpressionContext { + var p = new(PolarityExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *PolarityExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *PolarityExpressionContext) Expression() IExpressionContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *PolarityExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitPolarityExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type AdditiveExpressionContext struct { + ExpressionContext +} + +func NewAdditiveExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *AdditiveExpressionContext { + var p = new(AdditiveExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *AdditiveExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *AdditiveExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *AdditiveExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *AdditiveExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitAdditiveExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type MultiplicativeExpressionContext struct { + ExpressionContext +} + +func NewMultiplicativeExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *MultiplicativeExpressionContext { + var p = new(MultiplicativeExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *MultiplicativeExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *MultiplicativeExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *MultiplicativeExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *MultiplicativeExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitMultiplicativeExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type UnionExpressionContext struct { + ExpressionContext +} + +func NewUnionExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *UnionExpressionContext { + var p = new(UnionExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *UnionExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *UnionExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *UnionExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *UnionExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitUnionExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type OrExpressionContext struct { + ExpressionContext +} + +func NewOrExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *OrExpressionContext { + var p = new(OrExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *OrExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *OrExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *OrExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *OrExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitOrExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type AndExpressionContext struct { + ExpressionContext +} + +func NewAndExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *AndExpressionContext { + var p = new(AndExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *AndExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *AndExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *AndExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *AndExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitAndExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type MembershipExpressionContext struct { + ExpressionContext +} + +func NewMembershipExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *MembershipExpressionContext { + var p = new(MembershipExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *MembershipExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *MembershipExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *MembershipExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *MembershipExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitMembershipExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type InequalityExpressionContext struct { + ExpressionContext +} + +func NewInequalityExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *InequalityExpressionContext { + var p = new(InequalityExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *InequalityExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *InequalityExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *InequalityExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *InequalityExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitInequalityExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type InvocationExpressionContext struct { + ExpressionContext +} + +func NewInvocationExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *InvocationExpressionContext { + var p = new(InvocationExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *InvocationExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *InvocationExpressionContext) Expression() IExpressionContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *InvocationExpressionContext) Invocation() IInvocationContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IInvocationContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IInvocationContext) +} + +func (s *InvocationExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitInvocationExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type EqualityExpressionContext struct { + ExpressionContext +} + +func NewEqualityExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *EqualityExpressionContext { + var p = new(EqualityExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *EqualityExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *EqualityExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *EqualityExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *EqualityExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitEqualityExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type ImpliesExpressionContext struct { + ExpressionContext +} + +func NewImpliesExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *ImpliesExpressionContext { + var p = new(ImpliesExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *ImpliesExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *ImpliesExpressionContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *ImpliesExpressionContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *ImpliesExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitImpliesExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type TermExpressionContext struct { + ExpressionContext +} + +func NewTermExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *TermExpressionContext { + var p = new(TermExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *TermExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *TermExpressionContext) Term() ITermContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(ITermContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(ITermContext) +} + +func (s *TermExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitTermExpression(s) + + default: + return t.VisitChildren(s) + } +} + +type TypeExpressionContext struct { + ExpressionContext +} + +func NewTypeExpressionContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *TypeExpressionContext { + var p = new(TypeExpressionContext) + + InitEmptyExpressionContext(&p.ExpressionContext) + p.parser = parser + p.CopyAll(ctx.(*ExpressionContext)) + + return p +} + +func (s *TypeExpressionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *TypeExpressionContext) Expression() IExpressionContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *TypeExpressionContext) TypeSpecifier() ITypeSpecifierContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(ITypeSpecifierContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(ITypeSpecifierContext) +} + +func (s *TypeExpressionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitTypeExpression(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Expression() (localctx IExpressionContext) { + return p.expression(0) +} + +func (p *fhirpathParser) expression(_p int) (localctx IExpressionContext) { + var _parentctx antlr.ParserRuleContext = p.GetParserRuleContext() + + _parentState := p.GetState() + localctx = NewExpressionContext(p, p.GetParserRuleContext(), _parentState) + var _prevctx IExpressionContext = localctx + var _ antlr.ParserRuleContext = _prevctx // TODO: To prevent unused variable warning. + _startState := 2 + p.EnterRecursionRule(localctx, 2, fhirpathParserRULE_expression, _p) + var _la int + + var _alt int + + p.EnterOuterAlt(localctx, 1) + p.SetState(37) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + + switch p.GetTokenStream().LA(1) { + case fhirpathParserT__10, fhirpathParserT__11, fhirpathParserT__21, fhirpathParserT__22, fhirpathParserT__27, fhirpathParserT__29, fhirpathParserT__31, fhirpathParserT__32, fhirpathParserT__33, fhirpathParserT__34, fhirpathParserT__35, fhirpathParserT__36, fhirpathParserDATE, fhirpathParserDATETIME, fhirpathParserTIME, fhirpathParserIDENTIFIER, fhirpathParserDELIMITEDIDENTIFIER, fhirpathParserSTRING, fhirpathParserNUMBER: + localctx = NewTermExpressionContext(p, localctx) + p.SetParserRuleContext(localctx) + _prevctx = localctx + + { + p.SetState(34) + p.Term() + } + + case fhirpathParserT__3, fhirpathParserT__4: + localctx = NewPolarityExpressionContext(p, localctx) + p.SetParserRuleContext(localctx) + _prevctx = localctx + { + p.SetState(35) + _la = p.GetTokenStream().LA(1) + + if !(_la == fhirpathParserT__3 || _la == fhirpathParserT__4) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + { + p.SetState(36) + p.expression(11) + } + + default: + p.SetError(antlr.NewNoViableAltException(p, nil, nil, nil, nil, nil)) + goto errorExit + } + p.GetParserRuleContext().SetStop(p.GetTokenStream().LT(-1)) + p.SetState(79) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + _alt = p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 2, p.GetParserRuleContext()) + if p.HasError() { + goto errorExit + } + for _alt != 2 && _alt != antlr.ATNInvalidAltNumber { + if _alt == 1 { + if p.GetParseListeners() != nil { + p.TriggerExitRuleEvent() + } + _prevctx = localctx + p.SetState(77) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + + switch p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 1, p.GetParserRuleContext()) { + case 1: + localctx = NewMultiplicativeExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(39) + + if !(p.Precpred(p.GetParserRuleContext(), 10)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 10)", "")) + goto errorExit + } + { + p.SetState(40) + _la = p.GetTokenStream().LA(1) + + if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&960) != 0) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + { + p.SetState(41) + p.expression(11) + } + + case 2: + localctx = NewAdditiveExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(42) + + if !(p.Precpred(p.GetParserRuleContext(), 9)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 9)", "")) + goto errorExit + } + { + p.SetState(43) + _la = p.GetTokenStream().LA(1) + + if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&1072) != 0) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + { + p.SetState(44) + p.expression(10) + } + + case 3: + localctx = NewUnionExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(45) + + if !(p.Precpred(p.GetParserRuleContext(), 7)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 7)", "")) + goto errorExit + } + { + p.SetState(46) + p.Match(fhirpathParserT__12) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(47) + p.expression(8) + } + + case 4: + localctx = NewInequalityExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(48) + + if !(p.Precpred(p.GetParserRuleContext(), 6)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 6)", "")) + goto errorExit + } + { + p.SetState(49) + _la = p.GetTokenStream().LA(1) + + if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&245760) != 0) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + { + p.SetState(50) + p.expression(7) + } + + case 5: + localctx = NewEqualityExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(51) + + if !(p.Precpred(p.GetParserRuleContext(), 5)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 5)", "")) + goto errorExit + } + { + p.SetState(52) + _la = p.GetTokenStream().LA(1) + + if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&3932160) != 0) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + { + p.SetState(53) + p.expression(6) + } + + case 6: + localctx = NewMembershipExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(54) + + if !(p.Precpred(p.GetParserRuleContext(), 4)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 4)", "")) + goto errorExit + } + { + p.SetState(55) + _la = p.GetTokenStream().LA(1) + + if !(_la == fhirpathParserT__21 || _la == fhirpathParserT__22) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + { + p.SetState(56) + p.expression(5) + } + + case 7: + localctx = NewAndExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(57) + + if !(p.Precpred(p.GetParserRuleContext(), 3)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 3)", "")) + goto errorExit + } + { + p.SetState(58) + p.Match(fhirpathParserT__23) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(59) + p.expression(4) + } + + case 8: + localctx = NewOrExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(60) + + if !(p.Precpred(p.GetParserRuleContext(), 2)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 2)", "")) + goto errorExit + } + { + p.SetState(61) + _la = p.GetTokenStream().LA(1) + + if !(_la == fhirpathParserT__24 || _la == fhirpathParserT__25) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + { + p.SetState(62) + p.expression(3) + } + + case 9: + localctx = NewImpliesExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(63) + + if !(p.Precpred(p.GetParserRuleContext(), 1)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 1)", "")) + goto errorExit + } + { + p.SetState(64) + p.Match(fhirpathParserT__26) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(65) + p.expression(2) + } + + case 10: + localctx = NewInvocationExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(66) + + if !(p.Precpred(p.GetParserRuleContext(), 13)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 13)", "")) + goto errorExit + } + { + p.SetState(67) + p.Match(fhirpathParserT__0) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(68) + p.Invocation() + } + + case 11: + localctx = NewIndexerExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(69) + + if !(p.Precpred(p.GetParserRuleContext(), 12)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 12)", "")) + goto errorExit + } + { + p.SetState(70) + p.Match(fhirpathParserT__1) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(71) + p.expression(0) + } + { + p.SetState(72) + p.Match(fhirpathParserT__2) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 12: + localctx = NewTypeExpressionContext(p, NewExpressionContext(p, _parentctx, _parentState)) + p.PushNewRecursionContext(localctx, _startState, fhirpathParserRULE_expression) + p.SetState(74) + + if !(p.Precpred(p.GetParserRuleContext(), 8)) { + p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 8)", "")) + goto errorExit + } + { + p.SetState(75) + _la = p.GetTokenStream().LA(1) + + if !(_la == fhirpathParserT__10 || _la == fhirpathParserT__11) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + { + p.SetState(76) + p.TypeSpecifier() + } + + case antlr.ATNInvalidAltNumber: + goto errorExit + } + + } + p.SetState(81) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + _alt = p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 2, p.GetParserRuleContext()) + if p.HasError() { + goto errorExit + } + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.UnrollRecursionContexts(_parentctx) + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// ITermContext is an interface to support dynamic dispatch. +type ITermContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + // IsTermContext differentiates from other interfaces. + IsTermContext() +} + +type TermContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyTermContext() *TermContext { + var p = new(TermContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_term + return p +} + +func InitEmptyTermContext(p *TermContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_term +} + +func (*TermContext) IsTermContext() {} + +func NewTermContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *TermContext { + var p = new(TermContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_term + + return p +} + +func (s *TermContext) GetParser() antlr.Parser { return s.parser } + +func (s *TermContext) CopyAll(ctx *TermContext) { + s.CopyFrom(&ctx.BaseParserRuleContext) +} + +func (s *TermContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *TermContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +type ExternalConstantTermContext struct { + TermContext +} + +func NewExternalConstantTermContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *ExternalConstantTermContext { + var p = new(ExternalConstantTermContext) + + InitEmptyTermContext(&p.TermContext) + p.parser = parser + p.CopyAll(ctx.(*TermContext)) + + return p +} + +func (s *ExternalConstantTermContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *ExternalConstantTermContext) ExternalConstant() IExternalConstantContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExternalConstantContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IExternalConstantContext) +} + +func (s *ExternalConstantTermContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitExternalConstantTerm(s) + + default: + return t.VisitChildren(s) + } +} + +type LiteralTermContext struct { + TermContext +} + +func NewLiteralTermContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *LiteralTermContext { + var p = new(LiteralTermContext) + + InitEmptyTermContext(&p.TermContext) + p.parser = parser + p.CopyAll(ctx.(*TermContext)) + + return p +} + +func (s *LiteralTermContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *LiteralTermContext) Literal() ILiteralContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(ILiteralContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(ILiteralContext) +} + +func (s *LiteralTermContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitLiteralTerm(s) + + default: + return t.VisitChildren(s) + } +} + +type ParenthesizedTermContext struct { + TermContext +} + +func NewParenthesizedTermContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *ParenthesizedTermContext { + var p = new(ParenthesizedTermContext) + + InitEmptyTermContext(&p.TermContext) + p.parser = parser + p.CopyAll(ctx.(*TermContext)) + + return p +} + +func (s *ParenthesizedTermContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *ParenthesizedTermContext) Expression() IExpressionContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *ParenthesizedTermContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitParenthesizedTerm(s) + + default: + return t.VisitChildren(s) + } +} + +type InvocationTermContext struct { + TermContext +} + +func NewInvocationTermContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *InvocationTermContext { + var p = new(InvocationTermContext) + + InitEmptyTermContext(&p.TermContext) + p.parser = parser + p.CopyAll(ctx.(*TermContext)) + + return p +} + +func (s *InvocationTermContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *InvocationTermContext) Invocation() IInvocationContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IInvocationContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IInvocationContext) +} + +func (s *InvocationTermContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitInvocationTerm(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Term() (localctx ITermContext) { + localctx = NewTermContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 4, fhirpathParserRULE_term) + p.SetState(89) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + + switch p.GetTokenStream().LA(1) { + case fhirpathParserT__10, fhirpathParserT__11, fhirpathParserT__21, fhirpathParserT__22, fhirpathParserT__34, fhirpathParserT__35, fhirpathParserT__36, fhirpathParserIDENTIFIER, fhirpathParserDELIMITEDIDENTIFIER: + localctx = NewInvocationTermContext(p, localctx) + p.EnterOuterAlt(localctx, 1) + { + p.SetState(82) + p.Invocation() + } + + case fhirpathParserT__29, fhirpathParserT__31, fhirpathParserT__32, fhirpathParserDATE, fhirpathParserDATETIME, fhirpathParserTIME, fhirpathParserSTRING, fhirpathParserNUMBER: + localctx = NewLiteralTermContext(p, localctx) + p.EnterOuterAlt(localctx, 2) + { + p.SetState(83) + p.Literal() + } + + case fhirpathParserT__33: + localctx = NewExternalConstantTermContext(p, localctx) + p.EnterOuterAlt(localctx, 3) + { + p.SetState(84) + p.ExternalConstant() + } + + case fhirpathParserT__27: + localctx = NewParenthesizedTermContext(p, localctx) + p.EnterOuterAlt(localctx, 4) + { + p.SetState(85) + p.Match(fhirpathParserT__27) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(86) + p.expression(0) + } + { + p.SetState(87) + p.Match(fhirpathParserT__28) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + default: + p.SetError(antlr.NewNoViableAltException(p, nil, nil, nil, nil, nil)) + goto errorExit + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// ILiteralContext is an interface to support dynamic dispatch. +type ILiteralContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + // IsLiteralContext differentiates from other interfaces. + IsLiteralContext() +} + +type LiteralContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyLiteralContext() *LiteralContext { + var p = new(LiteralContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_literal + return p +} + +func InitEmptyLiteralContext(p *LiteralContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_literal +} + +func (*LiteralContext) IsLiteralContext() {} + +func NewLiteralContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *LiteralContext { + var p = new(LiteralContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_literal + + return p +} + +func (s *LiteralContext) GetParser() antlr.Parser { return s.parser } + +func (s *LiteralContext) CopyAll(ctx *LiteralContext) { + s.CopyFrom(&ctx.BaseParserRuleContext) +} + +func (s *LiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *LiteralContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +type TimeLiteralContext struct { + LiteralContext +} + +func NewTimeLiteralContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *TimeLiteralContext { + var p = new(TimeLiteralContext) + + InitEmptyLiteralContext(&p.LiteralContext) + p.parser = parser + p.CopyAll(ctx.(*LiteralContext)) + + return p +} + +func (s *TimeLiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *TimeLiteralContext) TIME() antlr.TerminalNode { + return s.GetToken(fhirpathParserTIME, 0) +} + +func (s *TimeLiteralContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitTimeLiteral(s) + + default: + return t.VisitChildren(s) + } +} + +type NullLiteralContext struct { + LiteralContext +} + +func NewNullLiteralContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *NullLiteralContext { + var p = new(NullLiteralContext) + + InitEmptyLiteralContext(&p.LiteralContext) + p.parser = parser + p.CopyAll(ctx.(*LiteralContext)) + + return p +} + +func (s *NullLiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *NullLiteralContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitNullLiteral(s) + + default: + return t.VisitChildren(s) + } +} + +type DateTimeLiteralContext struct { + LiteralContext +} + +func NewDateTimeLiteralContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *DateTimeLiteralContext { + var p = new(DateTimeLiteralContext) + + InitEmptyLiteralContext(&p.LiteralContext) + p.parser = parser + p.CopyAll(ctx.(*LiteralContext)) + + return p +} + +func (s *DateTimeLiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *DateTimeLiteralContext) DATETIME() antlr.TerminalNode { + return s.GetToken(fhirpathParserDATETIME, 0) +} + +func (s *DateTimeLiteralContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitDateTimeLiteral(s) + + default: + return t.VisitChildren(s) + } +} + +type StringLiteralContext struct { + LiteralContext +} + +func NewStringLiteralContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *StringLiteralContext { + var p = new(StringLiteralContext) + + InitEmptyLiteralContext(&p.LiteralContext) + p.parser = parser + p.CopyAll(ctx.(*LiteralContext)) + + return p +} + +func (s *StringLiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *StringLiteralContext) STRING() antlr.TerminalNode { + return s.GetToken(fhirpathParserSTRING, 0) +} + +func (s *StringLiteralContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitStringLiteral(s) + + default: + return t.VisitChildren(s) + } +} + +type DateLiteralContext struct { + LiteralContext +} + +func NewDateLiteralContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *DateLiteralContext { + var p = new(DateLiteralContext) + + InitEmptyLiteralContext(&p.LiteralContext) + p.parser = parser + p.CopyAll(ctx.(*LiteralContext)) + + return p +} + +func (s *DateLiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *DateLiteralContext) DATE() antlr.TerminalNode { + return s.GetToken(fhirpathParserDATE, 0) +} + +func (s *DateLiteralContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitDateLiteral(s) + + default: + return t.VisitChildren(s) + } +} + +type BooleanLiteralContext struct { + LiteralContext +} + +func NewBooleanLiteralContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *BooleanLiteralContext { + var p = new(BooleanLiteralContext) + + InitEmptyLiteralContext(&p.LiteralContext) + p.parser = parser + p.CopyAll(ctx.(*LiteralContext)) + + return p +} + +func (s *BooleanLiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *BooleanLiteralContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitBooleanLiteral(s) + + default: + return t.VisitChildren(s) + } +} + +type NumberLiteralContext struct { + LiteralContext +} + +func NewNumberLiteralContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *NumberLiteralContext { + var p = new(NumberLiteralContext) + + InitEmptyLiteralContext(&p.LiteralContext) + p.parser = parser + p.CopyAll(ctx.(*LiteralContext)) + + return p +} + +func (s *NumberLiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *NumberLiteralContext) NUMBER() antlr.TerminalNode { + return s.GetToken(fhirpathParserNUMBER, 0) +} + +func (s *NumberLiteralContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitNumberLiteral(s) + + default: + return t.VisitChildren(s) + } +} + +type QuantityLiteralContext struct { + LiteralContext +} + +func NewQuantityLiteralContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *QuantityLiteralContext { + var p = new(QuantityLiteralContext) + + InitEmptyLiteralContext(&p.LiteralContext) + p.parser = parser + p.CopyAll(ctx.(*LiteralContext)) + + return p +} + +func (s *QuantityLiteralContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *QuantityLiteralContext) Quantity() IQuantityContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IQuantityContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IQuantityContext) +} + +func (s *QuantityLiteralContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitQuantityLiteral(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Literal() (localctx ILiteralContext) { + localctx = NewLiteralContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 6, fhirpathParserRULE_literal) + var _la int + + p.SetState(100) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + + switch p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 4, p.GetParserRuleContext()) { + case 1: + localctx = NewNullLiteralContext(p, localctx) + p.EnterOuterAlt(localctx, 1) + { + p.SetState(91) + p.Match(fhirpathParserT__29) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(92) + p.Match(fhirpathParserT__30) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 2: + localctx = NewBooleanLiteralContext(p, localctx) + p.EnterOuterAlt(localctx, 2) + { + p.SetState(93) + _la = p.GetTokenStream().LA(1) + + if !(_la == fhirpathParserT__31 || _la == fhirpathParserT__32) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + + case 3: + localctx = NewStringLiteralContext(p, localctx) + p.EnterOuterAlt(localctx, 3) + { + p.SetState(94) + p.Match(fhirpathParserSTRING) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 4: + localctx = NewNumberLiteralContext(p, localctx) + p.EnterOuterAlt(localctx, 4) + { + p.SetState(95) + p.Match(fhirpathParserNUMBER) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 5: + localctx = NewDateLiteralContext(p, localctx) + p.EnterOuterAlt(localctx, 5) + { + p.SetState(96) + p.Match(fhirpathParserDATE) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 6: + localctx = NewDateTimeLiteralContext(p, localctx) + p.EnterOuterAlt(localctx, 6) + { + p.SetState(97) + p.Match(fhirpathParserDATETIME) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 7: + localctx = NewTimeLiteralContext(p, localctx) + p.EnterOuterAlt(localctx, 7) + { + p.SetState(98) + p.Match(fhirpathParserTIME) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 8: + localctx = NewQuantityLiteralContext(p, localctx) + p.EnterOuterAlt(localctx, 8) + { + p.SetState(99) + p.Quantity() + } + + case antlr.ATNInvalidAltNumber: + goto errorExit + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IExternalConstantContext is an interface to support dynamic dispatch. +type IExternalConstantContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + Identifier() IIdentifierContext + STRING() antlr.TerminalNode + + // IsExternalConstantContext differentiates from other interfaces. + IsExternalConstantContext() +} + +type ExternalConstantContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyExternalConstantContext() *ExternalConstantContext { + var p = new(ExternalConstantContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_externalConstant + return p +} + +func InitEmptyExternalConstantContext(p *ExternalConstantContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_externalConstant +} + +func (*ExternalConstantContext) IsExternalConstantContext() {} + +func NewExternalConstantContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *ExternalConstantContext { + var p = new(ExternalConstantContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_externalConstant + + return p +} + +func (s *ExternalConstantContext) GetParser() antlr.Parser { return s.parser } + +func (s *ExternalConstantContext) Identifier() IIdentifierContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IIdentifierContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IIdentifierContext) +} + +func (s *ExternalConstantContext) STRING() antlr.TerminalNode { + return s.GetToken(fhirpathParserSTRING, 0) +} + +func (s *ExternalConstantContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *ExternalConstantContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *ExternalConstantContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitExternalConstant(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) ExternalConstant() (localctx IExternalConstantContext) { + localctx = NewExternalConstantContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 8, fhirpathParserRULE_externalConstant) + p.EnterOuterAlt(localctx, 1) + { + p.SetState(102) + p.Match(fhirpathParserT__33) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + p.SetState(105) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + + switch p.GetTokenStream().LA(1) { + case fhirpathParserT__10, fhirpathParserT__11, fhirpathParserT__21, fhirpathParserT__22, fhirpathParserIDENTIFIER, fhirpathParserDELIMITEDIDENTIFIER: + { + p.SetState(103) + p.Identifier() + } + + case fhirpathParserSTRING: + { + p.SetState(104) + p.Match(fhirpathParserSTRING) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + default: + p.SetError(antlr.NewNoViableAltException(p, nil, nil, nil, nil, nil)) + goto errorExit + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IInvocationContext is an interface to support dynamic dispatch. +type IInvocationContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + // IsInvocationContext differentiates from other interfaces. + IsInvocationContext() +} + +type InvocationContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyInvocationContext() *InvocationContext { + var p = new(InvocationContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_invocation + return p +} + +func InitEmptyInvocationContext(p *InvocationContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_invocation +} + +func (*InvocationContext) IsInvocationContext() {} + +func NewInvocationContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *InvocationContext { + var p = new(InvocationContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_invocation + + return p +} + +func (s *InvocationContext) GetParser() antlr.Parser { return s.parser } + +func (s *InvocationContext) CopyAll(ctx *InvocationContext) { + s.CopyFrom(&ctx.BaseParserRuleContext) +} + +func (s *InvocationContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *InvocationContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +type TotalInvocationContext struct { + InvocationContext +} + +func NewTotalInvocationContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *TotalInvocationContext { + var p = new(TotalInvocationContext) + + InitEmptyInvocationContext(&p.InvocationContext) + p.parser = parser + p.CopyAll(ctx.(*InvocationContext)) + + return p +} + +func (s *TotalInvocationContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *TotalInvocationContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitTotalInvocation(s) + + default: + return t.VisitChildren(s) + } +} + +type ThisInvocationContext struct { + InvocationContext +} + +func NewThisInvocationContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *ThisInvocationContext { + var p = new(ThisInvocationContext) + + InitEmptyInvocationContext(&p.InvocationContext) + p.parser = parser + p.CopyAll(ctx.(*InvocationContext)) + + return p +} + +func (s *ThisInvocationContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *ThisInvocationContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitThisInvocation(s) + + default: + return t.VisitChildren(s) + } +} + +type IndexInvocationContext struct { + InvocationContext +} + +func NewIndexInvocationContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *IndexInvocationContext { + var p = new(IndexInvocationContext) + + InitEmptyInvocationContext(&p.InvocationContext) + p.parser = parser + p.CopyAll(ctx.(*InvocationContext)) + + return p +} + +func (s *IndexInvocationContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *IndexInvocationContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitIndexInvocation(s) + + default: + return t.VisitChildren(s) + } +} + +type FunctionInvocationContext struct { + InvocationContext +} + +func NewFunctionInvocationContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *FunctionInvocationContext { + var p = new(FunctionInvocationContext) + + InitEmptyInvocationContext(&p.InvocationContext) + p.parser = parser + p.CopyAll(ctx.(*InvocationContext)) + + return p +} + +func (s *FunctionInvocationContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *FunctionInvocationContext) Function() IFunctionContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IFunctionContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IFunctionContext) +} + +func (s *FunctionInvocationContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitFunctionInvocation(s) + + default: + return t.VisitChildren(s) + } +} + +type MemberInvocationContext struct { + InvocationContext +} + +func NewMemberInvocationContext(parser antlr.Parser, ctx antlr.ParserRuleContext) *MemberInvocationContext { + var p = new(MemberInvocationContext) + + InitEmptyInvocationContext(&p.InvocationContext) + p.parser = parser + p.CopyAll(ctx.(*InvocationContext)) + + return p +} + +func (s *MemberInvocationContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *MemberInvocationContext) Identifier() IIdentifierContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IIdentifierContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IIdentifierContext) +} + +func (s *MemberInvocationContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitMemberInvocation(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Invocation() (localctx IInvocationContext) { + localctx = NewInvocationContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 10, fhirpathParserRULE_invocation) + p.SetState(112) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + + switch p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 6, p.GetParserRuleContext()) { + case 1: + localctx = NewMemberInvocationContext(p, localctx) + p.EnterOuterAlt(localctx, 1) + { + p.SetState(107) + p.Identifier() + } + + case 2: + localctx = NewFunctionInvocationContext(p, localctx) + p.EnterOuterAlt(localctx, 2) + { + p.SetState(108) + p.Function() + } + + case 3: + localctx = NewThisInvocationContext(p, localctx) + p.EnterOuterAlt(localctx, 3) + { + p.SetState(109) + p.Match(fhirpathParserT__34) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 4: + localctx = NewIndexInvocationContext(p, localctx) + p.EnterOuterAlt(localctx, 4) + { + p.SetState(110) + p.Match(fhirpathParserT__35) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case 5: + localctx = NewTotalInvocationContext(p, localctx) + p.EnterOuterAlt(localctx, 5) + { + p.SetState(111) + p.Match(fhirpathParserT__36) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + case antlr.ATNInvalidAltNumber: + goto errorExit + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IFunctionContext is an interface to support dynamic dispatch. +type IFunctionContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + Identifier() IIdentifierContext + ParamList() IParamListContext + + // IsFunctionContext differentiates from other interfaces. + IsFunctionContext() +} + +type FunctionContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyFunctionContext() *FunctionContext { + var p = new(FunctionContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_function + return p +} + +func InitEmptyFunctionContext(p *FunctionContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_function +} + +func (*FunctionContext) IsFunctionContext() {} + +func NewFunctionContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *FunctionContext { + var p = new(FunctionContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_function + + return p +} + +func (s *FunctionContext) GetParser() antlr.Parser { return s.parser } + +func (s *FunctionContext) Identifier() IIdentifierContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IIdentifierContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IIdentifierContext) +} + +func (s *FunctionContext) ParamList() IParamListContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IParamListContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IParamListContext) +} + +func (s *FunctionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *FunctionContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *FunctionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitFunction(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Function() (localctx IFunctionContext) { + localctx = NewFunctionContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 12, fhirpathParserRULE_function) + var _la int + + p.EnterOuterAlt(localctx, 1) + { + p.SetState(114) + p.Identifier() + } + { + p.SetState(115) + p.Match(fhirpathParserT__27) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + p.SetState(117) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + _la = p.GetTokenStream().LA(1) + + if (int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&4575657493346129968) != 0 { + { + p.SetState(116) + p.ParamList() + } + + } + { + p.SetState(119) + p.Match(fhirpathParserT__28) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IParamListContext is an interface to support dynamic dispatch. +type IParamListContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + AllExpression() []IExpressionContext + Expression(i int) IExpressionContext + + // IsParamListContext differentiates from other interfaces. + IsParamListContext() +} + +type ParamListContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyParamListContext() *ParamListContext { + var p = new(ParamListContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_paramList + return p +} + +func InitEmptyParamListContext(p *ParamListContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_paramList +} + +func (*ParamListContext) IsParamListContext() {} + +func NewParamListContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *ParamListContext { + var p = new(ParamListContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_paramList + + return p +} + +func (s *ParamListContext) GetParser() antlr.Parser { return s.parser } + +func (s *ParamListContext) AllExpression() []IExpressionContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IExpressionContext); ok { + len++ + } + } + + tst := make([]IExpressionContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IExpressionContext); ok { + tst[i] = t.(IExpressionContext) + i++ + } + } + + return tst +} + +func (s *ParamListContext) Expression(i int) IExpressionContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IExpressionContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IExpressionContext) +} + +func (s *ParamListContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *ParamListContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *ParamListContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitParamList(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) ParamList() (localctx IParamListContext) { + localctx = NewParamListContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 14, fhirpathParserRULE_paramList) + var _la int + + p.EnterOuterAlt(localctx, 1) + { + p.SetState(121) + p.expression(0) + } + p.SetState(126) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + _la = p.GetTokenStream().LA(1) + + for _la == fhirpathParserT__37 { + { + p.SetState(122) + p.Match(fhirpathParserT__37) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(123) + p.expression(0) + } + + p.SetState(128) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + _la = p.GetTokenStream().LA(1) + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IQuantityContext is an interface to support dynamic dispatch. +type IQuantityContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + NUMBER() antlr.TerminalNode + Unit() IUnitContext + + // IsQuantityContext differentiates from other interfaces. + IsQuantityContext() +} + +type QuantityContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyQuantityContext() *QuantityContext { + var p = new(QuantityContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_quantity + return p +} + +func InitEmptyQuantityContext(p *QuantityContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_quantity +} + +func (*QuantityContext) IsQuantityContext() {} + +func NewQuantityContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *QuantityContext { + var p = new(QuantityContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_quantity + + return p +} + +func (s *QuantityContext) GetParser() antlr.Parser { return s.parser } + +func (s *QuantityContext) NUMBER() antlr.TerminalNode { + return s.GetToken(fhirpathParserNUMBER, 0) +} + +func (s *QuantityContext) Unit() IUnitContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IUnitContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IUnitContext) +} + +func (s *QuantityContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *QuantityContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *QuantityContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitQuantity(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Quantity() (localctx IQuantityContext) { + localctx = NewQuantityContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 16, fhirpathParserRULE_quantity) + p.EnterOuterAlt(localctx, 1) + { + p.SetState(129) + p.Match(fhirpathParserNUMBER) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + p.SetState(131) + p.GetErrorHandler().Sync(p) + + if p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 9, p.GetParserRuleContext()) == 1 { + { + p.SetState(130) + p.Unit() + } + + } else if p.HasError() { // JIM + goto errorExit + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IUnitContext is an interface to support dynamic dispatch. +type IUnitContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + DateTimePrecision() IDateTimePrecisionContext + PluralDateTimePrecision() IPluralDateTimePrecisionContext + STRING() antlr.TerminalNode + + // IsUnitContext differentiates from other interfaces. + IsUnitContext() +} + +type UnitContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyUnitContext() *UnitContext { + var p = new(UnitContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_unit + return p +} + +func InitEmptyUnitContext(p *UnitContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_unit +} + +func (*UnitContext) IsUnitContext() {} + +func NewUnitContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *UnitContext { + var p = new(UnitContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_unit + + return p +} + +func (s *UnitContext) GetParser() antlr.Parser { return s.parser } + +func (s *UnitContext) DateTimePrecision() IDateTimePrecisionContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IDateTimePrecisionContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IDateTimePrecisionContext) +} + +func (s *UnitContext) PluralDateTimePrecision() IPluralDateTimePrecisionContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IPluralDateTimePrecisionContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IPluralDateTimePrecisionContext) +} + +func (s *UnitContext) STRING() antlr.TerminalNode { + return s.GetToken(fhirpathParserSTRING, 0) +} + +func (s *UnitContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *UnitContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *UnitContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitUnit(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Unit() (localctx IUnitContext) { + localctx = NewUnitContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 18, fhirpathParserRULE_unit) + p.SetState(136) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + + switch p.GetTokenStream().LA(1) { + case fhirpathParserT__38, fhirpathParserT__39, fhirpathParserT__40, fhirpathParserT__41, fhirpathParserT__42, fhirpathParserT__43, fhirpathParserT__44, fhirpathParserT__45: + p.EnterOuterAlt(localctx, 1) + { + p.SetState(133) + p.DateTimePrecision() + } + + case fhirpathParserT__46, fhirpathParserT__47, fhirpathParserT__48, fhirpathParserT__49, fhirpathParserT__50, fhirpathParserT__51, fhirpathParserT__52, fhirpathParserT__53: + p.EnterOuterAlt(localctx, 2) + { + p.SetState(134) + p.PluralDateTimePrecision() + } + + case fhirpathParserSTRING: + p.EnterOuterAlt(localctx, 3) + { + p.SetState(135) + p.Match(fhirpathParserSTRING) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + + default: + p.SetError(antlr.NewNoViableAltException(p, nil, nil, nil, nil, nil)) + goto errorExit + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IDateTimePrecisionContext is an interface to support dynamic dispatch. +type IDateTimePrecisionContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + // IsDateTimePrecisionContext differentiates from other interfaces. + IsDateTimePrecisionContext() +} + +type DateTimePrecisionContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyDateTimePrecisionContext() *DateTimePrecisionContext { + var p = new(DateTimePrecisionContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_dateTimePrecision + return p +} + +func InitEmptyDateTimePrecisionContext(p *DateTimePrecisionContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_dateTimePrecision +} + +func (*DateTimePrecisionContext) IsDateTimePrecisionContext() {} + +func NewDateTimePrecisionContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *DateTimePrecisionContext { + var p = new(DateTimePrecisionContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_dateTimePrecision + + return p +} + +func (s *DateTimePrecisionContext) GetParser() antlr.Parser { return s.parser } +func (s *DateTimePrecisionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *DateTimePrecisionContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *DateTimePrecisionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitDateTimePrecision(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) DateTimePrecision() (localctx IDateTimePrecisionContext) { + localctx = NewDateTimePrecisionContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 20, fhirpathParserRULE_dateTimePrecision) + var _la int + + p.EnterOuterAlt(localctx, 1) + { + p.SetState(138) + _la = p.GetTokenStream().LA(1) + + if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&140187732541440) != 0) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IPluralDateTimePrecisionContext is an interface to support dynamic dispatch. +type IPluralDateTimePrecisionContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + // IsPluralDateTimePrecisionContext differentiates from other interfaces. + IsPluralDateTimePrecisionContext() +} + +type PluralDateTimePrecisionContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyPluralDateTimePrecisionContext() *PluralDateTimePrecisionContext { + var p = new(PluralDateTimePrecisionContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_pluralDateTimePrecision + return p +} + +func InitEmptyPluralDateTimePrecisionContext(p *PluralDateTimePrecisionContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_pluralDateTimePrecision +} + +func (*PluralDateTimePrecisionContext) IsPluralDateTimePrecisionContext() {} + +func NewPluralDateTimePrecisionContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *PluralDateTimePrecisionContext { + var p = new(PluralDateTimePrecisionContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_pluralDateTimePrecision + + return p +} + +func (s *PluralDateTimePrecisionContext) GetParser() antlr.Parser { return s.parser } +func (s *PluralDateTimePrecisionContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *PluralDateTimePrecisionContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *PluralDateTimePrecisionContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitPluralDateTimePrecision(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) PluralDateTimePrecision() (localctx IPluralDateTimePrecisionContext) { + localctx = NewPluralDateTimePrecisionContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 22, fhirpathParserRULE_pluralDateTimePrecision) + var _la int + + p.EnterOuterAlt(localctx, 1) + { + p.SetState(140) + _la = p.GetTokenStream().LA(1) + + if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&35888059530608640) != 0) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// ITypeSpecifierContext is an interface to support dynamic dispatch. +type ITypeSpecifierContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + QualifiedIdentifier() IQualifiedIdentifierContext + + // IsTypeSpecifierContext differentiates from other interfaces. + IsTypeSpecifierContext() +} + +type TypeSpecifierContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyTypeSpecifierContext() *TypeSpecifierContext { + var p = new(TypeSpecifierContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_typeSpecifier + return p +} + +func InitEmptyTypeSpecifierContext(p *TypeSpecifierContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_typeSpecifier +} + +func (*TypeSpecifierContext) IsTypeSpecifierContext() {} + +func NewTypeSpecifierContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *TypeSpecifierContext { + var p = new(TypeSpecifierContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_typeSpecifier + + return p +} + +func (s *TypeSpecifierContext) GetParser() antlr.Parser { return s.parser } + +func (s *TypeSpecifierContext) QualifiedIdentifier() IQualifiedIdentifierContext { + var t antlr.RuleContext + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IQualifiedIdentifierContext); ok { + t = ctx.(antlr.RuleContext) + break + } + } + + if t == nil { + return nil + } + + return t.(IQualifiedIdentifierContext) +} + +func (s *TypeSpecifierContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *TypeSpecifierContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *TypeSpecifierContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitTypeSpecifier(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) TypeSpecifier() (localctx ITypeSpecifierContext) { + localctx = NewTypeSpecifierContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 24, fhirpathParserRULE_typeSpecifier) + p.EnterOuterAlt(localctx, 1) + { + p.SetState(142) + p.QualifiedIdentifier() + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IQualifiedIdentifierContext is an interface to support dynamic dispatch. +type IQualifiedIdentifierContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + AllIdentifier() []IIdentifierContext + Identifier(i int) IIdentifierContext + + // IsQualifiedIdentifierContext differentiates from other interfaces. + IsQualifiedIdentifierContext() +} + +type QualifiedIdentifierContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyQualifiedIdentifierContext() *QualifiedIdentifierContext { + var p = new(QualifiedIdentifierContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_qualifiedIdentifier + return p +} + +func InitEmptyQualifiedIdentifierContext(p *QualifiedIdentifierContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_qualifiedIdentifier +} + +func (*QualifiedIdentifierContext) IsQualifiedIdentifierContext() {} + +func NewQualifiedIdentifierContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *QualifiedIdentifierContext { + var p = new(QualifiedIdentifierContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_qualifiedIdentifier + + return p +} + +func (s *QualifiedIdentifierContext) GetParser() antlr.Parser { return s.parser } + +func (s *QualifiedIdentifierContext) AllIdentifier() []IIdentifierContext { + children := s.GetChildren() + len := 0 + for _, ctx := range children { + if _, ok := ctx.(IIdentifierContext); ok { + len++ + } + } + + tst := make([]IIdentifierContext, len) + i := 0 + for _, ctx := range children { + if t, ok := ctx.(IIdentifierContext); ok { + tst[i] = t.(IIdentifierContext) + i++ + } + } + + return tst +} + +func (s *QualifiedIdentifierContext) Identifier(i int) IIdentifierContext { + var t antlr.RuleContext + j := 0 + for _, ctx := range s.GetChildren() { + if _, ok := ctx.(IIdentifierContext); ok { + if j == i { + t = ctx.(antlr.RuleContext) + break + } + j++ + } + } + + if t == nil { + return nil + } + + return t.(IIdentifierContext) +} + +func (s *QualifiedIdentifierContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *QualifiedIdentifierContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *QualifiedIdentifierContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitQualifiedIdentifier(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) QualifiedIdentifier() (localctx IQualifiedIdentifierContext) { + localctx = NewQualifiedIdentifierContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 26, fhirpathParserRULE_qualifiedIdentifier) + var _alt int + + p.EnterOuterAlt(localctx, 1) + { + p.SetState(144) + p.Identifier() + } + p.SetState(149) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + _alt = p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 11, p.GetParserRuleContext()) + if p.HasError() { + goto errorExit + } + for _alt != 2 && _alt != antlr.ATNInvalidAltNumber { + if _alt == 1 { + { + p.SetState(145) + p.Match(fhirpathParserT__0) + if p.HasError() { + // Recognition error - abort rule + goto errorExit + } + } + { + p.SetState(146) + p.Identifier() + } + + } + p.SetState(151) + p.GetErrorHandler().Sync(p) + if p.HasError() { + goto errorExit + } + _alt = p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 11, p.GetParserRuleContext()) + if p.HasError() { + goto errorExit + } + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +// IIdentifierContext is an interface to support dynamic dispatch. +type IIdentifierContext interface { + antlr.ParserRuleContext + + // GetParser returns the parser. + GetParser() antlr.Parser + + // Getter signatures + IDENTIFIER() antlr.TerminalNode + DELIMITEDIDENTIFIER() antlr.TerminalNode + + // IsIdentifierContext differentiates from other interfaces. + IsIdentifierContext() +} + +type IdentifierContext struct { + antlr.BaseParserRuleContext + parser antlr.Parser +} + +func NewEmptyIdentifierContext() *IdentifierContext { + var p = new(IdentifierContext) + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_identifier + return p +} + +func InitEmptyIdentifierContext(p *IdentifierContext) { + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) + p.RuleIndex = fhirpathParserRULE_identifier +} + +func (*IdentifierContext) IsIdentifierContext() {} + +func NewIdentifierContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *IdentifierContext { + var p = new(IdentifierContext) + + antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) + + p.parser = parser + p.RuleIndex = fhirpathParserRULE_identifier + + return p +} + +func (s *IdentifierContext) GetParser() antlr.Parser { return s.parser } + +func (s *IdentifierContext) IDENTIFIER() antlr.TerminalNode { + return s.GetToken(fhirpathParserIDENTIFIER, 0) +} + +func (s *IdentifierContext) DELIMITEDIDENTIFIER() antlr.TerminalNode { + return s.GetToken(fhirpathParserDELIMITEDIDENTIFIER, 0) +} + +func (s *IdentifierContext) GetRuleContext() antlr.RuleContext { + return s +} + +func (s *IdentifierContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { + return antlr.TreesStringTree(s, ruleNames, recog) +} + +func (s *IdentifierContext) Accept(visitor antlr.ParseTreeVisitor) interface{} { + switch t := visitor.(type) { + case fhirpathVisitor: + return t.VisitIdentifier(s) + + default: + return t.VisitChildren(s) + } +} + +func (p *fhirpathParser) Identifier() (localctx IIdentifierContext) { + localctx = NewIdentifierContext(p, p.GetParserRuleContext(), p.GetState()) + p.EnterRule(localctx, 28, fhirpathParserRULE_identifier) + var _la int + + p.EnterOuterAlt(localctx, 1) + { + p.SetState(152) + _la = p.GetTokenStream().LA(1) + + if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&864691128467724288) != 0) { + p.GetErrorHandler().RecoverInline(p) + } else { + p.GetErrorHandler().ReportMatch(p) + p.Consume() + } + } + +errorExit: + if p.HasError() { + v := p.GetError() + localctx.SetException(v) + p.GetErrorHandler().ReportError(p, v) + p.GetErrorHandler().Recover(p, v) + p.SetError(nil) + } + p.ExitRule() + return localctx + goto errorExit // Trick to prevent compiler error if the label is not used +} + +func (p *fhirpathParser) Sempred(localctx antlr.RuleContext, ruleIndex, predIndex int) bool { + switch ruleIndex { + case 1: + var t *ExpressionContext = nil + if localctx != nil { + t = localctx.(*ExpressionContext) + } + return p.Expression_Sempred(t, predIndex) + + default: + panic("No predicate with index: " + fmt.Sprint(ruleIndex)) + } +} + +func (p *fhirpathParser) Expression_Sempred(localctx antlr.RuleContext, predIndex int) bool { + switch predIndex { + case 0: + return p.Precpred(p.GetParserRuleContext(), 10) + + case 1: + return p.Precpred(p.GetParserRuleContext(), 9) + + case 2: + return p.Precpred(p.GetParserRuleContext(), 7) + + case 3: + return p.Precpred(p.GetParserRuleContext(), 6) + + case 4: + return p.Precpred(p.GetParserRuleContext(), 5) + + case 5: + return p.Precpred(p.GetParserRuleContext(), 4) + + case 6: + return p.Precpred(p.GetParserRuleContext(), 3) + + case 7: + return p.Precpred(p.GetParserRuleContext(), 2) + + case 8: + return p.Precpred(p.GetParserRuleContext(), 1) + + case 9: + return p.Precpred(p.GetParserRuleContext(), 13) + + case 10: + return p.Precpred(p.GetParserRuleContext(), 12) + + case 11: + return p.Precpred(p.GetParserRuleContext(), 8) + + default: + panic("No predicate with index: " + fmt.Sprint(predIndex)) + } +} diff --git a/fhirpath/internal/grammar/fhirpath_visitor.go b/fhirpath/internal/grammar/fhirpath_visitor.go new file mode 100644 index 0000000..c5e4772 --- /dev/null +++ b/fhirpath/internal/grammar/fhirpath_visitor.go @@ -0,0 +1,135 @@ +// Code generated from fhirpath.g4 by ANTLR 4.13.0. DO NOT EDIT. + +package grammar // fhirpath +import "github.com/antlr4-go/antlr/v4" + +// A complete Visitor for a parse tree produced by fhirpathParser. +type fhirpathVisitor interface { + antlr.ParseTreeVisitor + + // Visit a parse tree produced by fhirpathParser#prog. + VisitProg(ctx *ProgContext) interface{} + + // Visit a parse tree produced by fhirpathParser#indexerExpression. + VisitIndexerExpression(ctx *IndexerExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#polarityExpression. + VisitPolarityExpression(ctx *PolarityExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#additiveExpression. + VisitAdditiveExpression(ctx *AdditiveExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#multiplicativeExpression. + VisitMultiplicativeExpression(ctx *MultiplicativeExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#unionExpression. + VisitUnionExpression(ctx *UnionExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#orExpression. + VisitOrExpression(ctx *OrExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#andExpression. + VisitAndExpression(ctx *AndExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#membershipExpression. + VisitMembershipExpression(ctx *MembershipExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#inequalityExpression. + VisitInequalityExpression(ctx *InequalityExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#invocationExpression. + VisitInvocationExpression(ctx *InvocationExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#equalityExpression. + VisitEqualityExpression(ctx *EqualityExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#impliesExpression. + VisitImpliesExpression(ctx *ImpliesExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#termExpression. + VisitTermExpression(ctx *TermExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#typeExpression. + VisitTypeExpression(ctx *TypeExpressionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#invocationTerm. + VisitInvocationTerm(ctx *InvocationTermContext) interface{} + + // Visit a parse tree produced by fhirpathParser#literalTerm. + VisitLiteralTerm(ctx *LiteralTermContext) interface{} + + // Visit a parse tree produced by fhirpathParser#externalConstantTerm. + VisitExternalConstantTerm(ctx *ExternalConstantTermContext) interface{} + + // Visit a parse tree produced by fhirpathParser#parenthesizedTerm. + VisitParenthesizedTerm(ctx *ParenthesizedTermContext) interface{} + + // Visit a parse tree produced by fhirpathParser#nullLiteral. + VisitNullLiteral(ctx *NullLiteralContext) interface{} + + // Visit a parse tree produced by fhirpathParser#booleanLiteral. + VisitBooleanLiteral(ctx *BooleanLiteralContext) interface{} + + // Visit a parse tree produced by fhirpathParser#stringLiteral. + VisitStringLiteral(ctx *StringLiteralContext) interface{} + + // Visit a parse tree produced by fhirpathParser#numberLiteral. + VisitNumberLiteral(ctx *NumberLiteralContext) interface{} + + // Visit a parse tree produced by fhirpathParser#dateLiteral. + VisitDateLiteral(ctx *DateLiteralContext) interface{} + + // Visit a parse tree produced by fhirpathParser#dateTimeLiteral. + VisitDateTimeLiteral(ctx *DateTimeLiteralContext) interface{} + + // Visit a parse tree produced by fhirpathParser#timeLiteral. + VisitTimeLiteral(ctx *TimeLiteralContext) interface{} + + // Visit a parse tree produced by fhirpathParser#quantityLiteral. + VisitQuantityLiteral(ctx *QuantityLiteralContext) interface{} + + // Visit a parse tree produced by fhirpathParser#externalConstant. + VisitExternalConstant(ctx *ExternalConstantContext) interface{} + + // Visit a parse tree produced by fhirpathParser#memberInvocation. + VisitMemberInvocation(ctx *MemberInvocationContext) interface{} + + // Visit a parse tree produced by fhirpathParser#functionInvocation. + VisitFunctionInvocation(ctx *FunctionInvocationContext) interface{} + + // Visit a parse tree produced by fhirpathParser#thisInvocation. + VisitThisInvocation(ctx *ThisInvocationContext) interface{} + + // Visit a parse tree produced by fhirpathParser#indexInvocation. + VisitIndexInvocation(ctx *IndexInvocationContext) interface{} + + // Visit a parse tree produced by fhirpathParser#totalInvocation. + VisitTotalInvocation(ctx *TotalInvocationContext) interface{} + + // Visit a parse tree produced by fhirpathParser#function. + VisitFunction(ctx *FunctionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#paramList. + VisitParamList(ctx *ParamListContext) interface{} + + // Visit a parse tree produced by fhirpathParser#quantity. + VisitQuantity(ctx *QuantityContext) interface{} + + // Visit a parse tree produced by fhirpathParser#unit. + VisitUnit(ctx *UnitContext) interface{} + + // Visit a parse tree produced by fhirpathParser#dateTimePrecision. + VisitDateTimePrecision(ctx *DateTimePrecisionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#pluralDateTimePrecision. + VisitPluralDateTimePrecision(ctx *PluralDateTimePrecisionContext) interface{} + + // Visit a parse tree produced by fhirpathParser#typeSpecifier. + VisitTypeSpecifier(ctx *TypeSpecifierContext) interface{} + + // Visit a parse tree produced by fhirpathParser#qualifiedIdentifier. + VisitQualifiedIdentifier(ctx *QualifiedIdentifierContext) interface{} + + // Visit a parse tree produced by fhirpathParser#identifier. + VisitIdentifier(ctx *IdentifierContext) interface{} +} diff --git a/fhirpath/internal/grammar/generate.go b/fhirpath/internal/grammar/generate.go new file mode 100644 index 0000000..94a4467 --- /dev/null +++ b/fhirpath/internal/grammar/generate.go @@ -0,0 +1,3 @@ +package grammar + +//go:generate ./generate.sh diff --git a/fhirpath/internal/grammar/generate.sh b/fhirpath/internal/grammar/generate.sh new file mode 100755 index 0000000..08a7c80 --- /dev/null +++ b/fhirpath/internal/grammar/generate.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +shopt -s expand_aliases + +antlr_jar=antlr-4.13.0-complete.jar + +if [ ! -f ${antlr_jar} ]; then + wget "https://www.antlr.org/download/${antlr_jar}" +fi + +alias antlr4="java -Xmx500M -cp './$antlr_jar:\$CLASSPATH' org.antlr.v4.Tool" +antlr4 -Dlanguage=Go -no-listener -visitor -package grammar *.g4 diff --git a/fhirpath/internal/opts/opts.go b/fhirpath/internal/opts/opts.go new file mode 100644 index 0000000..5a39f05 --- /dev/null +++ b/fhirpath/internal/opts/opts.go @@ -0,0 +1,66 @@ +/* +Package opts is an internal package that exists for setting configuration +settings for FHIRPath. This is an internal package so that only parts of this +may be publicly re-exported, while the implementation has access to the full +thing. +*/ +package opts + +import ( + "errors" + + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" + "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" +) + +// CompileConfig provides the configuration values for the Compile command. +type CompileConfig struct { + // Table is the current function table to be called. + Table funcs.FunctionTable + Transform parser.VisitorTransform + + // Permissive is a legacy option to allow FHIRpaths with *invalid* fields to be + // compiled (to reduce breakages). + Permissive bool +} + +// EvaluateConfig provides the configuration values for the Evaluate command. +type EvaluateConfig struct { + // Context is the current context information. + Context *expr.Context +} + +// Option is the base interface for FHIRPath options. +type Option[T any] interface { + updateConfig(*T) error +} + +// CompileOption is an Option that sets CompileConfig. +type CompileOption = Option[CompileConfig] + +// EvaluateOption is an Option that sets EvaluateConfig. +type EvaluateOption = Option[EvaluateConfig] + +// Transform creates either an Evaluate or Compile configuration option, done +// as a function callback. +func Transform[T any](callback func(cfg *T) error) Option[T] { + return callbackOption[T]{callback: callback} +} + +// ApplyOptions applies all the options to the given configuration. +func ApplyOptions[T any](cfg *T, opts ...Option[T]) (*T, error) { + var errs []error + for _, opt := range opts { + errs = append(errs, opt.updateConfig(cfg)) + } + return cfg, errors.Join(errs...) +} + +type callbackOption[T any] struct { + callback func(*T) error +} + +func (o callbackOption[T]) updateConfig(cfg *T) error { + return o.callback(cfg) +} diff --git a/fhirpath/internal/parser/doc.go b/fhirpath/internal/parser/doc.go new file mode 100644 index 0000000..d1885d2 --- /dev/null +++ b/fhirpath/internal/parser/doc.go @@ -0,0 +1,6 @@ +/* +Package parser provides the logic for traversing +the ANTLR generated parse tree. Provides a visitor +and related functions. +*/ +package parser diff --git a/fhirpath/internal/parser/error_handling.go b/fhirpath/internal/parser/error_handling.go new file mode 100644 index 0000000..f182902 --- /dev/null +++ b/fhirpath/internal/parser/error_handling.go @@ -0,0 +1,22 @@ +package parser + +import ( + "errors" + "fmt" + + "github.com/antlr4-go/antlr/v4" +) + +type FHIRPathErrorListener struct { + *antlr.DefaultErrorListener + errors []error +} + +func (l *FHIRPathErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) { + err := fmt.Errorf("syntax error on line %d:%d - %s", line, column, msg) + l.errors = append(l.errors, err) +} + +func (l *FHIRPathErrorListener) Error() error { + return errors.Join(l.errors...) +} diff --git a/fhirpath/internal/parser/transforms.go b/fhirpath/internal/parser/transforms.go new file mode 100644 index 0000000..3378a62 --- /dev/null +++ b/fhirpath/internal/parser/transforms.go @@ -0,0 +1,12 @@ +package parser + +import "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + +// A VisitorTransform is a function which transforms the specified +// expression. This is used in FHIRPath Patch to modify expressions. +type VisitorTransform func(expr.Expression) expr.Expression + +// IdentityTransform returns the given expression without any modification. +func IdentityTransform(e expr.Expression) expr.Expression { + return e +} diff --git a/fhirpath/internal/parser/visitor.go b/fhirpath/internal/parser/visitor.go new file mode 100644 index 0000000..75feb97 --- /dev/null +++ b/fhirpath/internal/parser/visitor.go @@ -0,0 +1,515 @@ +package parser + +import ( + "errors" + "fmt" + "strings" + + "github.com/antlr4-go/antlr/v4" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/resource" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" + "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" + "github.com/verily-src/fhirpath-go/fhirpath/internal/grammar" + "github.com/verily-src/fhirpath-go/fhirpath/internal/reflection" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +var ( + errNotSupported = errors.New("expression not currently supported") + errTooManyQualifiers = errors.New("too many type qualifiers") + errVisitingChildren = errors.New("error while visiting child expressions") + errUnresolvedFunction = errors.New("function identifier can't be resolved") +) + +type FHIRPathVisitor struct { + *antlr.BaseParseTreeVisitor + visitedRoot bool + Functions funcs.FunctionTable + Transform VisitorTransform + Permissive bool +} + +type VisitResult struct { + Result expr.Expression + Error error +} + +type typeResult struct { + result reflection.TypeSpecifier + err error +} + +// clone produces a shallow-clone of the visitor, to be used when visiting sub-expressions. +func (v *FHIRPathVisitor) clone() *FHIRPathVisitor { + return &FHIRPathVisitor{ + Functions: v.Functions, + Transform: v.Transform, + Permissive: v.Permissive, + visitedRoot: false, + } +} + +func (v *FHIRPathVisitor) transformedVisitResult(resultExpr expr.Expression) *VisitResult { + if v.Transform == nil { + v.Transform = IdentityTransform + } + return &VisitResult{v.Transform(resultExpr), nil} +} + +func (v *FHIRPathVisitor) Visit(tree antlr.ParseTree) interface{} { + return tree.Accept(v) +} + +func (v *FHIRPathVisitor) VisitProg(ctx *grammar.ProgContext) interface{} { + return v.Visit(ctx.Expression()).(*VisitResult) +} + +// VisitIndexerExpression visits both the left side expression and right side expression, and +// constructs an index expression. If the right side expression does not evaluate to an Integer, +// returns an error +func (v *FHIRPathVisitor) VisitIndexerExpression(ctx *grammar.IndexerExpressionContext) interface{} { + // visit left side expression + leftResult := v.Visit(ctx.Expression(0)).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + + // visit contained expression with new Visitor to reset root node, and construct index + rightResult := v.clone().Visit(ctx.Expression(1)).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + indexExpr := &expr.IndexExpression{Index: rightResult.Result} + + sequence := &expr.ExpressionSequence{Expressions: []expr.Expression{leftResult.Result, indexExpr}} + return v.transformedVisitResult(sequence) +} + +func (v *FHIRPathVisitor) VisitPolarityExpression(ctx *grammar.PolarityExpressionContext) interface{} { + operator := expr.Operator(ctx.GetChild(0).(antlr.TerminalNode).GetText()) + result := v.Visit(ctx.Expression()).(*VisitResult) + if result.Error != nil { + return &VisitResult{nil, result.Error} + } + + // Return initial expression if the operator is '+' + if operator != expr.Sub { + return v.transformedVisitResult(result.Result) + } + + return v.transformedVisitResult(&expr.NegationExpression{Expr: result.Result}) +} + +func (v *FHIRPathVisitor) VisitAdditiveExpression(ctx *grammar.AdditiveExpressionContext) interface{} { + leftResult := v.Visit(ctx.Expression(0)).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + rightResult := v.clone().Visit(ctx.Expression(1)).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + + operator := expr.Operator(ctx.GetChild(1).(antlr.TerminalNode).GetText()) + + var expression expr.Expression + switch operator { + case expr.Concat: + expression = &expr.ConcatExpression{Left: leftResult.Result, Right: rightResult.Result} + case expr.Add: + expression = &expr.ArithmeticExpression{Left: leftResult.Result, Right: rightResult.Result, Op: expr.EvaluateAdd} + case expr.Sub: + expression = &expr.ArithmeticExpression{Left: leftResult.Result, Right: rightResult.Result, Op: expr.EvaluateSub} + } + return v.transformedVisitResult(expression) +} + +func (v *FHIRPathVisitor) VisitMultiplicativeExpression(ctx *grammar.MultiplicativeExpressionContext) interface{} { + leftResult := v.Visit(ctx.Expression(0)).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + rightResult := v.clone().Visit(ctx.Expression(1)).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + + operator := expr.Operator(ctx.GetChild(1).(antlr.TerminalNode).GetText()) + + // Select correct operator function. + var op func(system.Any, system.Any) (system.Any, error) + switch operator { + case expr.Mul: + op = expr.EvaluateMul + case expr.Div: + op = expr.EvaluateDiv + case expr.FloorDiv: + op = expr.EvaluateFloorDiv + case expr.Mod: + op = expr.EvaluateMod + } + + return v.transformedVisitResult( + &expr.ArithmeticExpression{Left: leftResult.Result, Right: rightResult.Result, Op: op}, + ) +} + +func (v *FHIRPathVisitor) VisitUnionExpression(ctx *grammar.UnionExpressionContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +func (v *FHIRPathVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) interface{} { + leftResult := v.Visit(ctx.Expression(0)).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + rightResult := v.clone().Visit(ctx.Expression(1)).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + + operator := expr.Operator(ctx.GetChild(1).(antlr.TerminalNode).GetText()) + + expression := &expr.BooleanExpression{Left: leftResult.Result, Right: rightResult.Result, Op: operator} + return v.transformedVisitResult(expression) +} + +func (v *FHIRPathVisitor) VisitAndExpression(ctx *grammar.AndExpressionContext) interface{} { + leftResult := v.Visit(ctx.Expression(0)).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + rightResult := v.clone().Visit(ctx.Expression(1)).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + + expression := &expr.BooleanExpression{Left: leftResult.Result, Right: rightResult.Result, Op: expr.And} + return v.transformedVisitResult(expression) +} + +func (v *FHIRPathVisitor) VisitMembershipExpression(ctx *grammar.MembershipExpressionContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +func (v *FHIRPathVisitor) VisitInequalityExpression(ctx *grammar.InequalityExpressionContext) interface{} { + leftResult := v.Visit(ctx.Expression(0)).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + rightResult := v.clone().Visit(ctx.Expression(1)).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + + operator := expr.Operator(ctx.GetChild(1).(antlr.TerminalNode).GetText()) + + expression := &expr.ComparisonExpression{Left: leftResult.Result, Right: rightResult.Result, Op: operator} + return v.transformedVisitResult(expression) +} + +// VisitInvocationExpression visits both sides, and constructs an expression sequence. +func (v *FHIRPathVisitor) VisitInvocationExpression(ctx *grammar.InvocationExpressionContext) interface{} { + // Visit left side with new visitor, raising error if necessary + leftResult := v.Visit(ctx.Expression()).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + + // Visit right side, raising error if necessary + rightResult := v.Visit(ctx.Invocation()).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + + // Construct and return ExpressionSequence + expressions := []expr.Expression{leftResult.Result, rightResult.Result} + sequence := &expr.ExpressionSequence{Expressions: expressions} + return v.transformedVisitResult(sequence) +} + +// VisitEqualityExpression both equality subexpressions and constructs an Equality Expression +// from the results of each subexpression +func (v *FHIRPathVisitor) VisitEqualityExpression(ctx *grammar.EqualityExpressionContext) interface{} { + leftResult := v.Visit(ctx.Expression(0)).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + + rightResult := v.clone().Visit(ctx.Expression(1)).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + operator := ctx.GetChild(1).(antlr.TerminalNode).GetText() + var expression expr.Expression + switch operator { + case expr.Equals: + expression = &expr.EqualityExpression{Left: leftResult.Result, Right: rightResult.Result} + case expr.NotEquals: + expression = &expr.EqualityExpression{Left: leftResult.Result, Right: rightResult.Result, Not: true} + case expr.Equivalence: + // TODO (PHP-5889): Implement equivalence expressions + case expr.Inequivalence: + // TODO (PHP-5889): Implement non-equivalence expressions + } + return v.transformedVisitResult(expression) +} + +func (v *FHIRPathVisitor) VisitImpliesExpression(ctx *grammar.ImpliesExpressionContext) interface{} { + leftResult := v.Visit(ctx.Expression(0)).(*VisitResult) + if leftResult.Error != nil { + return &VisitResult{nil, leftResult.Error} + } + rightResult := v.clone().Visit(ctx.Expression(1)).(*VisitResult) + if rightResult.Error != nil { + return &VisitResult{nil, rightResult.Error} + } + + expression := &expr.BooleanExpression{Left: leftResult.Result, Right: rightResult.Result, Op: expr.Implies} + return v.transformedVisitResult(expression) +} + +func (v *FHIRPathVisitor) VisitTermExpression(ctx *grammar.TermExpressionContext) interface{} { + return v.Visit(ctx.Term()) +} + +func (v *FHIRPathVisitor) VisitTypeExpression(ctx *grammar.TypeExpressionContext) interface{} { + expression := v.Visit(ctx.Expression()).(*VisitResult) + if expression.Error != nil { + return &VisitResult{nil, expression.Error} + } + typeSpecifier := v.Visit(ctx.TypeSpecifier()).(*typeResult) + if typeSpecifier.err != nil { + return &VisitResult{nil, typeSpecifier.err} + } + operator := ctx.GetChild(1).(antlr.TerminalNode).GetText() + var typeExpression expr.Expression + if operator == expr.Is { + typeExpression = &expr.IsExpression{Expr: expression.Result, Type: typeSpecifier.result} + } + if operator == expr.As { + typeExpression = &expr.AsExpression{Expr: expression.Result, Type: typeSpecifier.result} + } + return v.transformedVisitResult(typeExpression) +} + +func (v *FHIRPathVisitor) VisitInvocationTerm(ctx *grammar.InvocationTermContext) interface{} { + return v.Visit(ctx.Invocation()) +} + +func (v *FHIRPathVisitor) VisitLiteralTerm(ctx *grammar.LiteralTermContext) interface{} { + return v.Visit(ctx.Literal()) +} + +func (v *FHIRPathVisitor) VisitExternalConstantTerm(ctx *grammar.ExternalConstantTermContext) interface{} { + ident := ctx.ExternalConstant().GetText() + ident = strings.TrimPrefix(ident, "%") + return v.transformedVisitResult(&expr.ExternalConstantExpression{Identifier: ident}) +} + +func (v *FHIRPathVisitor) VisitParenthesizedTerm(ctx *grammar.ParenthesizedTermContext) interface{} { + return v.Visit(ctx.Expression()) +} + +// VisitNullLiteral returns a NullLiteralExpression, without any error. +func (v *FHIRPathVisitor) VisitNullLiteral(ctx *grammar.NullLiteralContext) interface{} { + result := &expr.LiteralExpression{} + return v.transformedVisitResult(result) +} + +// VisitBooleanLiteral returns a BooleanLiteralExpression, returning an error if +// there is an error during creation of the Boolean. +func (v *FHIRPathVisitor) VisitBooleanLiteral(ctx *grammar.BooleanLiteralContext) interface{} { + result, err := system.ParseBoolean(ctx.GetText()) + if err != nil { + return &VisitResult{nil, err} + } + expr := &expr.LiteralExpression{Literal: result} + return v.transformedVisitResult(expr) +} + +// VisitStringLiteral returns a StringLiteralExpression, returning an error if there is an +// error during creation of the String. +func (v *FHIRPathVisitor) VisitStringLiteral(ctx *grammar.StringLiteralContext) interface{} { + result, err := system.ParseString(ctx.STRING().GetText()) + if err != nil { + return &VisitResult{nil, err} + } + expr := &expr.LiteralExpression{Literal: result} + return v.transformedVisitResult(expr) +} + +// VisitNumberLiteral returns either an integer or decimal, depending on whether or not +// the number contains a decimal. Returns an error if there is an error during creation +// of the number. +func (v *FHIRPathVisitor) VisitNumberLiteral(ctx *grammar.NumberLiteralContext) interface{} { + number := ctx.NUMBER().GetText() + + if strings.Contains(number, ".") { + result, err := system.ParseDecimal(number) + if err != nil { + return &VisitResult{nil, err} + } + expr := &expr.LiteralExpression{Literal: result} + return v.transformedVisitResult(expr) + } + + result, err := system.ParseInteger(number) + if err != nil { + return &VisitResult{nil, err} + } + expr := &expr.LiteralExpression{Literal: result} + return v.transformedVisitResult(expr) +} + +// VisitDateLiteral returns a DateLiteralExpression, returning an error if there is an +// error during creation of the Date type. +func (v *FHIRPathVisitor) VisitDateLiteral(ctx *grammar.DateLiteralContext) interface{} { + date, err := system.ParseDate(ctx.DATE().GetText()) + if err != nil { + return &VisitResult{nil, err} + } + expr := &expr.LiteralExpression{Literal: date} + return v.transformedVisitResult(expr) +} + +// VisitDateTimeLiteral returns a DateTimeLiteralExpression, returning an error if there +// is an error during creation of the DateTime type. +func (v *FHIRPathVisitor) VisitDateTimeLiteral(ctx *grammar.DateTimeLiteralContext) interface{} { + dateTime, err := system.ParseDateTime(ctx.DATETIME().GetText()) + if err != nil { + return &VisitResult{nil, err} + } + expr := &expr.LiteralExpression{Literal: dateTime} + return v.transformedVisitResult(expr) +} + +// VisitTimeLiteral returns a TimeLiteralExpression, returning an error if there is an error +// during creation of the Time type. +func (v *FHIRPathVisitor) VisitTimeLiteral(ctx *grammar.TimeLiteralContext) interface{} { + time, err := system.ParseTime(ctx.TIME().GetText()) + if err != nil { + return &VisitResult{nil, err} + } + expr := &expr.LiteralExpression{Literal: time} + return v.transformedVisitResult(expr) +} + +// VisitQuantityLiteral returns a QuantityLiteralExpression, returning an error if there +// is an error during creation of the Quantity type. +func (v *FHIRPathVisitor) VisitQuantityLiteral(ctx *grammar.QuantityLiteralContext) interface{} { + // remove string quotes from unit + unit := ctx.Quantity().Unit().GetText() + unit = strings.TrimPrefix(unit, "'") + unit = strings.TrimSuffix(unit, "'") + + quantity, err := system.ParseQuantity(ctx.Quantity().NUMBER().GetText(), unit) + if err != nil { + return &VisitResult{nil, err} + } + expr := &expr.LiteralExpression{Literal: quantity} + return v.transformedVisitResult(expr) +} + +func (v *FHIRPathVisitor) VisitExternalConstant(ctx *grammar.ExternalConstantContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +// VisitMemberInvocation checks to see if the identifier corresponds to a resource type and is the +// root of the expression. If so, it will return a TypeExpression. Otherwise, it returns a FieldExpression. +func (v *FHIRPathVisitor) VisitMemberInvocation(ctx *grammar.MemberInvocationContext) interface{} { + identifier := ctx.GetText() + var expression expr.Expression + + if resource.IsType(identifier) && !v.visitedRoot { + expression = &expr.TypeExpression{Type: identifier} + v.visitedRoot = true + } else { + expression = &expr.FieldExpression{FieldName: identifier, Permissive: v.Permissive} + } + + return v.transformedVisitResult(expression) +} + +func (v *FHIRPathVisitor) VisitFunctionInvocation(ctx *grammar.FunctionInvocationContext) interface{} { + return v.Visit(ctx.Function()) +} + +func (v *FHIRPathVisitor) VisitThisInvocation(ctx *grammar.ThisInvocationContext) interface{} { + return &VisitResult{&expr.IdentityExpression{}, nil} +} + +func (v *FHIRPathVisitor) VisitIndexInvocation(ctx *grammar.IndexInvocationContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +func (v *FHIRPathVisitor) VisitTotalInvocation(ctx *grammar.TotalInvocationContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +func (v *FHIRPathVisitor) VisitFunction(ctx *grammar.FunctionContext) interface{} { + ident := ctx.Identifier().GetText() + fn, ok := v.Functions[ident] + if !ok { + return &VisitResult{nil, fmt.Errorf("%w: %s", errUnresolvedFunction, ident)} + } + + results := []*VisitResult{} + if args := ctx.ParamList(); args != nil { + results = v.Visit(args).([]*VisitResult) + } + + errs := slices.Map(results, func(r *VisitResult) error { return r.Error }) + if err := errors.Join(errs...); err != nil { + return &VisitResult{nil, fmt.Errorf("%w: %w", errVisitingChildren, err)} + } + + expressions := slices.Map(results, func(r *VisitResult) expr.Expression { return r.Result }) + if len(expressions) < fn.MinArity || len(expressions) > fn.MaxArity { + return &VisitResult{nil, fmt.Errorf("%w: input arity outside of function arity bounds", impl.ErrWrongArity)} + } + return v.transformedVisitResult(&expr.FunctionExpression{Fn: fn.Func, Args: expressions}) +} + +func (v *FHIRPathVisitor) VisitParamList(ctx *grammar.ParamListContext) interface{} { + return slices.Map(ctx.AllExpression(), func(e grammar.IExpressionContext) *VisitResult { return v.Visit(e).(*VisitResult) }) +} + +func (v *FHIRPathVisitor) VisitQuantity(ctx *grammar.QuantityContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +func (v *FHIRPathVisitor) VisitUnit(ctx *grammar.UnitContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +func (v *FHIRPathVisitor) VisitDateTimePrecision(ctx *grammar.DateTimePrecisionContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +func (v *FHIRPathVisitor) VisitPluralDateTimePrecision(ctx *grammar.PluralDateTimePrecisionContext) interface{} { + return &VisitResult{nil, errNotSupported} +} + +func (v *FHIRPathVisitor) VisitTypeSpecifier(ctx *grammar.TypeSpecifierContext) interface{} { + identifiers := v.Visit(ctx.QualifiedIdentifier()).([]string) + if len(identifiers) == 1 { + specifier, err := reflection.NewTypeSpecifier(identifiers[0]) + return &typeResult{specifier, err} + } + if len(identifiers) == 2 { + specifier, err := reflection.NewQualifiedTypeSpecifier(identifiers[0], identifiers[1]) + return &typeResult{specifier, err} + } + return &typeResult{err: fmt.Errorf("%w: %s", errTooManyQualifiers, strings.Join(identifiers, ","))} +} + +func (v *FHIRPathVisitor) VisitQualifiedIdentifier(ctx *grammar.QualifiedIdentifierContext) interface{} { + return slices.Map(ctx.AllIdentifier(), func(i grammar.IIdentifierContext) string { return i.GetText() }) +} + +func (v *FHIRPathVisitor) VisitIdentifier(ctx *grammar.IdentifierContext) interface{} { + return &VisitResult{nil, errNotSupported} +} diff --git a/fhirpath/internal/reflection/consts.go b/fhirpath/internal/reflection/consts.go new file mode 100644 index 0000000..4cd06ca --- /dev/null +++ b/fhirpath/internal/reflection/consts.go @@ -0,0 +1,7 @@ +package reflection + +// Valid namespace constants. +const ( + FHIR = "FHIR" + System = "System" +) diff --git a/fhirpath/internal/reflection/doc.go b/fhirpath/internal/reflection/doc.go new file mode 100644 index 0000000..ed51d0b --- /dev/null +++ b/fhirpath/internal/reflection/doc.go @@ -0,0 +1,5 @@ +/* +Package reflection provides types and utility functions +to enable FHIRPath type reflection. +*/ +package reflection diff --git a/fhirpath/internal/reflection/elements.go b/fhirpath/internal/reflection/elements.go new file mode 100644 index 0000000..b182a2a --- /dev/null +++ b/fhirpath/internal/reflection/elements.go @@ -0,0 +1,38 @@ +package reflection + +import ( + "github.com/iancoleman/strcase" + "github.com/verily-src/fhirpath-go/internal/protofields" +) + +// IsValidFHIRPathElement checks if the input string represents +// a valid element name. This function is importantly case-sensitive, +// which is a distinction that is important for primitive types. +func IsValidFHIRPathElement(name string) bool { + if isPrimitive(name) || name == "BackboneElement" { + return true + } + return protofields.IsValidElementType(primitiveToLowercase(name)) +} + +func isPrimitive(name string) bool { + switch name { + case "instant", "time", "date", "dateTime", "base64Binary", + "decimal", "boolean", "url", "code", "string", "integer", "uri", + "canonical", "markdown", "id", "oid", "uuid", "unsignedInt", "positiveInt": + return true + default: + return false + } +} + +func primitiveToLowercase(name string) string { + switch name { + case "Instant", "Time", "Date", "DateTime", "Base64Binary", + "Decimal", "Boolean", "Url", "Code", "String", "Integer", "Uri", + "Canonical", "Markdown", "Id", "Oid", "Uuid", "UnsignedInt", "PositiveInt": + return strcase.ToLowerCamel(name) + default: + return name + } +} diff --git a/fhirpath/internal/reflection/type_specifier.go b/fhirpath/internal/reflection/type_specifier.go new file mode 100644 index 0000000..1e283c9 --- /dev/null +++ b/fhirpath/internal/reflection/type_specifier.go @@ -0,0 +1,138 @@ +package reflection + +import ( + "errors" + "fmt" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "github.com/verily-src/fhirpath-go/internal/protofields" +) + +var ( + errInvalidType = errors.New("invalid type name") + errInvalidNamespace = errors.New("invalid namespace") + errInvalidInput = errors.New("invalid input type") +) + +// TypeSpecifier is a FHIRPath type that enables use of +// is and as operators. Provides a namespace and type name +type TypeSpecifier struct { + namespace string + typeName string +} + +// NewQualifiedTypeSpecifier constructs a Qualified Type Specifier given a namespace and typeName. +// Returns an error if the typeName is not found within the namespace, or the namespace is invalid. +func NewQualifiedTypeSpecifier(namespace string, typeName string) (TypeSpecifier, error) { + switch namespace { + case FHIR: + if IsValidFHIRPathElement(typeName) || protofields.IsValidResourceType(typeName) || isBaseType(typeName) { + return TypeSpecifier{namespace: namespace, typeName: typeName}, nil + } + return TypeSpecifier{}, fmt.Errorf("%w: %s", errInvalidType, typeName) + case System: + if system.IsValid(typeName) { + return TypeSpecifier{namespace: namespace, typeName: typeName}, nil + } + return TypeSpecifier{}, fmt.Errorf("%w: %s", errInvalidType, typeName) + default: + return TypeSpecifier{}, fmt.Errorf("%w: %s", errInvalidNamespace, namespace) + } +} + +// NewTypeSpecifier constructs a Qualified Type Specifier given a typeName. The namespace +// is inferred with the priority rules of FHIRPath. Returns an error if the typeName cannot +// be resolved. +func NewTypeSpecifier(typeName string) (TypeSpecifier, error) { + if IsValidFHIRPathElement(typeName) || protofields.IsValidResourceType(typeName) || isBaseType(typeName) { + return TypeSpecifier{FHIR, typeName}, nil + } + if system.IsValid(typeName) { + return TypeSpecifier{System, typeName}, nil + } + return TypeSpecifier{}, fmt.Errorf("%w: %s", errInvalidType, typeName) +} + +// TypeOf retrieves the Type Specifier of the input, given that it is +// a supported FHIRPath type. Otherwise, returns an error. +func TypeOf(input any) (TypeSpecifier, error) { + if item, ok := input.(system.Any); ok { + return TypeSpecifier{System, item.Name()}, nil + } + item, ok := input.(fhir.Base) + if !ok { + return TypeSpecifier{}, fmt.Errorf("%w: no type specifier available", errInvalidInput) + } + if oneOf := protofields.UnwrapOneofField(item, "choice"); oneOf != nil { + item = oneOf + } + name := string(item.ProtoReflect().Descriptor().Name()) + if protofields.IsCodeField(item) { + return TypeSpecifier{FHIR, "code"}, nil + } + return TypeSpecifier{FHIR, primitiveToLowercase(name)}, nil +} + +// Is returns a boolean representing whether or not the receiver type is equivalent to the +// input type, or if it's a valid subtype. +func (ts TypeSpecifier) Is(input TypeSpecifier) system.Boolean { + if ts.namespace != input.namespace { + return false + } + // If the root type has been reached and the equality is still false, they are not equal + if ts == ts.parent() && ts.typeName != input.typeName { + return false + } + if ts.typeName == input.typeName { + return true + } + return ts.parent().Is(input) // Recursively compare the parent type +} + +// MustCreateTypeSpecifier creates a qualified type specifier and panics if the +// provided namespace or typeName is invalid. Returns the created TypeSpecifier +func MustCreateTypeSpecifier(namespace string, typeName string) TypeSpecifier { + typeSpecifier, err := NewQualifiedTypeSpecifier(namespace, typeName) + if err != nil { + panic(err) + } + return typeSpecifier +} + +func (ts TypeSpecifier) parent() TypeSpecifier { + if ts.namespace == System { + return TypeSpecifier{"System", "Any"} + } + switch ts.typeName { + case "code", "markdown", "id": + return TypeSpecifier{FHIR, "string"} + case "unsignedInt", "positiveInt": + return TypeSpecifier{FHIR, "integer"} + case "url", "canonical", "uuid", "oid": + return TypeSpecifier{FHIR, "uri"} + case "Duration", "MoneyQuantity", "Age", "Count", "Distance", "SimpleQuantity": + return TypeSpecifier{FHIR, "Quantity"} + case "Timing", "Dosage", "ElementDefinition": + return TypeSpecifier{FHIR, "BackboneElement"} + case "Bundle", "Binary", "Parameters", "DomainResource": + return TypeSpecifier{FHIR, "Resource"} + case "Element": + return ts + case "Resource": + return ts + default: + if IsValidFHIRPathElement(ts.typeName) { + return TypeSpecifier{FHIR, "Element"} + } + return TypeSpecifier{FHIR, "DomainResource"} + } +} + +func isBaseType(name string) bool { + switch name { + case "Element", "Resource", "DomainResource": + return true + } + return false +} diff --git a/fhirpath/internal/reflection/type_specifier_test.go b/fhirpath/internal/reflection/type_specifier_test.go new file mode 100644 index 0000000..5cc170e --- /dev/null +++ b/fhirpath/internal/reflection/type_specifier_test.go @@ -0,0 +1,308 @@ +package reflection_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/internal/reflection" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestTypeSpecifier_Is(t *testing.T) { + testCases := []struct { + name string + typeOne reflection.TypeSpecifier + typeTwo reflection.TypeSpecifier + want system.Boolean + }{ + { + name: "mismatched namespaces", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "Element"), + typeTwo: reflection.MustCreateTypeSpecifier("System", "Any"), + want: false, + }, + { + name: "same type", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "DomainResource"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "DomainResource"), + want: true, + }, + { + name: "child type is parent", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "markdown"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "string"), + want: true, + }, + { + name: "child type is base", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "markdown"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "Element"), + want: true, + }, + { + name: "check if type name is Resource", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "Patient"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "Resource"), + want: true, + }, + { + name: "check if type name is Element", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "Timing"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "BackboneElement"), + want: true, + }, + { + name: "Quantity is FHIR Element", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "Quantity"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "Element"), + want: true, + }, + { + name: "System type is Any", + typeOne: reflection.MustCreateTypeSpecifier("System", "Decimal"), + typeTwo: reflection.MustCreateTypeSpecifier("System", "Any"), + want: true, + }, + { + name: "integer types are Integers", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "positiveInt"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "integer"), + want: true, + }, + { + name: "Patient is DomainResource", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "Patient"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "DomainResource"), + want: true, + }, + { + name: "Mismatched types", + typeOne: reflection.MustCreateTypeSpecifier("FHIR", "Patient"), + typeTwo: reflection.MustCreateTypeSpecifier("FHIR", "Practitioner"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.typeOne.Is(tc.typeTwo); got != tc.want { + t.Errorf("TypeSpecifier.Is returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestNewQualifiedTypeSpecifier_CreatesValidTS(t *testing.T) { + testCases := []struct { + name string + namespace string + typeName string + want reflection.TypeSpecifier + }{ + { + name: "Valid FHIR primitive", + namespace: "FHIR", + typeName: "decimal", + want: reflection.MustCreateTypeSpecifier("FHIR", "decimal"), + }, + { + name: "Valid FHIR Element", + namespace: "FHIR", + typeName: "ContactPoint", + want: reflection.MustCreateTypeSpecifier("FHIR", "ContactPoint"), + }, + { + name: "Valid FHIR Resource", + namespace: "FHIR", + typeName: "Medication", + want: reflection.MustCreateTypeSpecifier("FHIR", "Medication"), + }, + { + name: "Valid System type", + namespace: "System", + typeName: "DateTime", + want: reflection.MustCreateTypeSpecifier("System", "DateTime"), + }, + { + name: "Valid Base type", + namespace: "FHIR", + typeName: "Element", + want: reflection.MustCreateTypeSpecifier("FHIR", "Element"), + }, + { + name: "Create DomainResource", + namespace: "FHIR", + typeName: "DomainResource", + want: reflection.MustCreateTypeSpecifier("FHIR", "DomainResource"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := reflection.NewQualifiedTypeSpecifier(tc.namespace, tc.typeName) + + if err != nil { + t.Fatalf("NewQualifiedTypeSpecifier(%s, %s) returned unexpected error: %v", tc.namespace, tc.typeName, err) + } + if got != tc.want { + t.Errorf("NewQualifiedTypeSpecifier(%s, %s) returned incorrect TypeSpecifier: got %v, want %v", tc.namespace, tc.typeName, got, tc.want) + } + }) + } +} + +func TestNewQualifiedTypeSpecifier_ReturnsError(t *testing.T) { + testCases := []struct { + name string + namespace string + typeName string + }{ + { + name: "FHIR primitive with wrong case", + namespace: "FHIR", + typeName: "Decimal", + }, + { + name: "Non-existent FHIR type", + namespace: "FHIR", + typeName: "Hospital", + }, + { + name: "Mismatched namespace", + namespace: "System", + typeName: "Medication", + }, + { + name: "System type with wrong case", + namespace: "System", + typeName: "dateTime", + }, + { + name: "invalid namespace", + namespace: "Enrichments", + typeName: "Engine", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := reflection.NewQualifiedTypeSpecifier(tc.namespace, tc.typeName); err == nil { + t.Fatalf("NewQualifiedTypeSpecifier(%s, %s) didn't return error when expected to", tc.namespace, tc.typeName) + } + }) + } +} + +func TestNewTypeSpecifier_CreatesValidTS(t *testing.T) { + testCases := []struct { + name string + typeName string + want reflection.TypeSpecifier + }{ + { + name: "creates resource specifier from Patient", + typeName: "Patient", + want: reflection.MustCreateTypeSpecifier("FHIR", "Patient"), + }, + { + name: "creates element specifier from Element", + typeName: "Element", + want: reflection.MustCreateTypeSpecifier("FHIR", "Element"), + }, + { + name: "creates system specifier from Decimal", + typeName: "Decimal", + want: reflection.MustCreateTypeSpecifier("System", "Decimal"), + }, + { + name: "creates FHIR specifier from decimal", + typeName: "decimal", + want: reflection.MustCreateTypeSpecifier("FHIR", "decimal"), + }, + { + name: "creates FHIR specifier from Quantity", + typeName: "Quantity", + want: reflection.MustCreateTypeSpecifier("FHIR", "Quantity"), + }, + { + name: "Creates System specifier from Any", + typeName: "Any", + want: reflection.MustCreateTypeSpecifier("System", "Any"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := reflection.NewTypeSpecifier(tc.typeName) + + if err != nil { + t.Fatalf("NewTypeSpecifier(%s) returned unexpected error: %v", tc.name, err) + } + if got != tc.want { + t.Errorf("NewTypeSpecifier(%s) returned incorrect TypeSpecifier: got %v, want %v", tc.typeName, got, tc.want) + } + }) + } +} + +func TestNewTypeSpecifier_ReturnsError(t *testing.T) { + if _, err := reflection.NewTypeSpecifier("notAType"); err == nil { + t.Fatalf("NewTypeSpecifier(%s) didn't raise error on invalid type name", "notAType") + } +} + +func TestTypeOf_ReturnsTS(t *testing.T) { + quantity, _ := system.ParseQuantity("123", "kg") + + testCases := []struct { + name string + input any + want reflection.TypeSpecifier + }{ + { + name: "Gets correct specifier for Patient type", + input: (*ppb.Patient)(nil), + want: reflection.MustCreateTypeSpecifier("FHIR", "Patient"), + }, + { + name: "Gets lowercase type name for primitive type", + input: (*dtpb.Decimal)(nil), + want: reflection.MustCreateTypeSpecifier("FHIR", "decimal"), + }, + { + name: "Gets correct specifier for Code type", + input: (*ppb.Patient_GenderCode)(nil), + want: reflection.MustCreateTypeSpecifier("FHIR", "code"), + }, + { + name: "Gets correct specifier for Oneof type", + input: &ppb.Patient_DeceasedX{Choice: &ppb.Patient_DeceasedX_DateTime{DateTime: fhir.DateTimeNow()}}, + want: reflection.MustCreateTypeSpecifier("FHIR", "dateTime"), + }, + { + name: "Gets correct specifier for system type", + input: quantity, + want: reflection.MustCreateTypeSpecifier("System", "Quantity"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := reflection.TypeOf(tc.input) + + if err != nil { + t.Fatalf("GetTypeSpecifier returned unexpected error: %v", err) + } + if got != tc.want { + t.Errorf("GetTypeSpecifier returned incorrect type specifier: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestGetTypeSpecifier_ReturnsError(t *testing.T) { + if _, err := reflection.TypeOf("unsupported type"); err == nil { + t.Fatalf("GetTypeSpecifier didn't return error for unsupported type") + } +} diff --git a/fhirpath/options.go b/fhirpath/options.go new file mode 100644 index 0000000..6517c33 --- /dev/null +++ b/fhirpath/options.go @@ -0,0 +1,32 @@ +package fhirpath + +import ( + "github.com/verily-src/fhirpath-go/fhirpath/compopts" + "github.com/verily-src/fhirpath-go/fhirpath/evalopts" + "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" +) + +// CompileOption is a function type that modifies a passed in compileOption. +// Can define mutator functions of this type (see WithLimitation below) +type CompileOption = opts.CompileOption + +// EvaluateOption is a function type that mutates the evalOptions type. +type EvaluateOption = opts.EvaluateOption + +// WithFunction is a compile option that allows the addition of user-defined +// functions to a FHIRPath expression. Function argument must match the signature +// func(Collection, ...any) (Collection, error), or an error will be raised. +// +// Deprecated: Use compopts.Function instead. +func WithFunction(name string, fn any) CompileOption { + return compopts.AddFunction(name, fn) +} + +// WithConstant is an EvaluateOption that allows the addition of external +// constant variables. An error will be raised if the value passed in is +// neither a fhir proto or system type. +// +// Deprecated: Use evalopts.EnvVariable instead +func WithConstant(name string, value any) EvaluateOption { + return evalopts.EnvVariable(name, value) +} diff --git a/fhirpath/patch/doc.go b/fhirpath/patch/doc.go new file mode 100644 index 0000000..aa021e8 --- /dev/null +++ b/fhirpath/patch/doc.go @@ -0,0 +1,7 @@ +/* +Package patch implements the FHIRPath Patch specification. + +More documentation about the specification can be found on HL7: +https://hl7.org/fhir/R4/fhirpatch.html#3.1.5. +*/ +package patch diff --git a/fhirpath/patch/patch.go b/fhirpath/patch/patch.go new file mode 100644 index 0000000..4d48b21 --- /dev/null +++ b/fhirpath/patch/patch.go @@ -0,0 +1,597 @@ +package patch + +import ( + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/iancoleman/strcase" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resource" + "github.com/verily-src/fhirpath-go/fhirpath" + "github.com/verily-src/fhirpath-go/fhirpath/compopts" + "github.com/verily-src/fhirpath-go/fhirpath/internal/compile" + "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" + "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" + "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +var ( + ErrNotImplemented = errors.New("not implemented") + ErrInvalidInput = errors.New("invalid input") + ErrInvalidEnum = errors.New("invalid enum value") + ErrInvalidField = fhirpath.ErrInvalidField + ErrInvalidUnsignedInt = errors.New("invalid value for unsigned int") + ErrNotSingleton = expr.ErrNotSingleton + ErrNotPatchable = errors.New("result is not patchable") +) + +// Options encapsulates all possible FHIRPath options that +// can be used in the underlying FHIRPath evaluation before +// patching. This includes both compile-time and evaluation-time +// options. +type Options struct { + CompileOpts []opts.CompileOption + EvalOpts []opts.EvaluateOption +} + +// Expression is the FHIRPath Patch expression that will be +// compiled from a FHIRPath string. +type Expression struct { + expression expr.Expression + path string +} + +// String returns the underlying FHIRPath expression. +func (e *Expression) String() string { + return e.path +} + +// Compile parses and compiles the FHIRPath Patch expression down +// to a single Expression object. +// +// If there are any syntax or semantic errors, this will return an +// error indicating the reason for the compilation failure. +func Compile(path string, options ...opts.CompileOption) (*Expression, error) { + options = append(options, compopts.Transform(func(e expr.Expression) expr.Expression { + return storeLastExpression{e} + })) + + config, err := compile.PopulateConfig(options...) + if err != nil { + return nil, err + } + + tree, err := compile.Tree(path) + if err != nil { + return nil, err + } + + visitor := &parser.FHIRPathVisitor{ + Functions: config.Table, + Transform: config.Transform, + Permissive: config.Permissive, + } + vr, ok := visitor.Visit(tree).(*parser.VisitResult) + if !ok { + return nil, errors.New("input expression currently unsupported") + } + + if vr.Error != nil { + return nil, vr.Error + } + return &Expression{ + expression: vr.Result, + path: path, + }, nil +} + +// Add appends a value to the given field name in the element. +// Add can be used for non-repeating elements so long as they do not already exist. +// The field name must be in the same case as defined in the FHIRPath spec, +// which is in camelCase +// +// Note: value inputs to Add must be FHIR elements or FHIR resources. +// +// This function will return the following errors in the given conditions: +// +// - ErrInvalidInput if the input value to add is incorrect for the result of +// the expression +// - ErrInvalidField if the specified field name does not exist in the +// returned element +// - ErrNotSingleton if the result of the evaluation returns more than one +// entry +// - ErrNotPatchable if the result of the returned entry is not a patchable +// type -- e.g. a non-scalar type like a list, a value that is already +// set, etc. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func (e *Expression) Add(res fhir.Resource, name string, value fhir.Base, options ...fhirpath.EvaluateOption) error { + if strcase.ToLowerCamel(name) != name { + // All field names in FHIRPath are in camelCase, but the protos are in + // snake_case. To avoid accidentally accepting code like "foo_value" instead + // of "fooValue", we check first that we are already in the correct form, + // and error if it would never be possible. + return fmt.Errorf("%w: '%v'", ErrInvalidField, name) + } + if res == nil { + return fmt.Errorf("%w: nil input resource", ErrInvalidInput) + } + if value == nil { + return fmt.Errorf("%w: nil replacement value", ErrInvalidInput) + } + + _, evalResult, err := e.evaluate(res, options...) + if err != nil { + return err + } + + singleton, err := evalResult.ToSingleton() + if err != nil { + return fmt.Errorf("%w: fhirpatch add requires singleton collection", ErrNotSingleton) + } + + proto, ok := singleton.(proto.Message) + if !ok { + return fmt.Errorf("%w: result of type '%T' is not patchable", ErrNotPatchable, singleton) + } + + ref := proto.ProtoReflect() + descriptor := ref.Descriptor() + fieldName := strcase.ToSnake(name) + field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) + if field == nil { + fieldName += "_value" + field = descriptor.Fields().ByName(protoreflect.Name(fieldName)) + if field == nil { + return fmt.Errorf("%w: '%v'", fhirpath.ErrInvalidField, name) + } + } + + if !field.IsList() && ref.Has(field) { + return fmt.Errorf("%w: unable to add value to populated scalar field '%v' in %v resource", ErrNotPatchable, name, resource.TypeOf(res)) + } + + var update func(m protoreflect.ProtoMessage) + var valueMessage protoreflect.Message + if field.IsList() { + list := ref.Mutable(field).List() + update = func(m protoreflect.ProtoMessage) { + list.Append(protoreflect.ValueOfMessage(m.ProtoReflect())) + } + valueMessage = list.NewElement().Message() + } else { + update = func(m protoreflect.ProtoMessage) { + ref.Set(field, protoreflect.ValueOfMessage(m.ProtoReflect())) + } + valueMessage = ref.Get(field).Message() + } + + // Special handling for oneof fields, like "Extension", "ContainedResource", etc. + if e.isSingletonOneof(valueMessage.Interface()) { + container := e.newSetOneof(valueMessage, value) + if container == nil { + return fmt.Errorf( + "%w: '%v' value provided for field '%v' (which is of type '%v')", + ErrInvalidInput, + value.ProtoReflect().Descriptor().Name(), + name, + valueMessage.Descriptor().Name(), + ) + } + update(container.Interface()) + } else { + // Normalize data being patched + value, err = e.normalizeAdd(valueMessage, value) + if err != nil { + return err + } + + if valueMessage.Descriptor() != value.ProtoReflect().Descriptor() { + return fmt.Errorf( + "%w: '%v' value provided for field '%v' (which is of type '%v')", + ErrInvalidInput, + value.ProtoReflect().Descriptor().Name(), + name, + valueMessage.Descriptor().Name(), + ) + } + update(value) + } + + return nil +} + +// stringable is an interface to check for a string-valued FHIR type. +// code, markdown and id are all specializations of string that satisfy +// this interface. +// See: https://hl7.org/fhir/r4/datatypes.html +type stringable interface { + GetValue() string +} + +// stringable is an interface to check for a integer-valued FHIR type. +// See: https://hl7.org/fhir/r4/datatypes.html +type intable interface { + GetValue() int32 +} + +// normalizeAdd normalizes a value to be patched to the correct type. +// If no normalization is required, the input value will be returned +// unmodified from its original value. +func (e *Expression) normalizeAdd(valueMessage protoreflect.Message, value fhir.Base) (fhir.Base, error) { + var newVal fhir.Base + var err error + switch value := value.(type) { + case stringable: + newVal, err = enumFromStringable(valueMessage, value) + if newVal == nil && err == nil { + // Check for a reference field - since these are dynamic, + // we need to patch them in after the reference is created, + // which is why we're only updating the ID field here. + valueField := valueMessage.Descriptor().Fields().ByName("value") + if valueField.FullName() == "google.fhir.r4.core.ReferenceId.value" { + newVal = &dtpb.ReferenceId{Value: value.GetValue()} + } + } + case intable: + newVal, err = intValueFromInt(valueMessage, value) + default: + } + + if err != nil { + return nil, err + } + // The value was normalized + if newVal != nil { + return newVal, nil + } + + // Use the original value without normalization + return value, nil +} + +func (e *Expression) evaluate(res fhir.Resource, options ...fhirpath.EvaluateOption) (*expr.Context, system.Collection, error) { + collection := system.Collection{res} + config := &opts.EvaluateConfig{ + Context: expr.InitializeContext(collection), + } + config, err := opts.ApplyOptions(config, options...) + if err != nil { + return nil, nil, err + } + + result, err := e.expression.Evaluate(config.Context, collection) + return config.Context, result, err +} + +func (e *Expression) isSingletonOneof(msg proto.Message) bool { + message := msg.ProtoReflect() + descriptor := message.Descriptor() + oneofs := descriptor.Oneofs() + return oneofs.Len() == 1 && oneofs.ByName("reference") == nil +} + +func (e *Expression) newSetOneof(msg protoreflect.Message, value proto.Message) protoreflect.Message { + container := msg.New() + descriptor := container.Descriptor() + fields := descriptor.Fields() + + for i := 0; i < fields.Len(); i++ { + field := fields.Get(i) + if value.ProtoReflect().Descriptor() == msg.Get(field).Message().Descriptor() { + container.Set(field, protoreflect.ValueOfMessage(value.ProtoReflect())) + return container + } + } + return nil +} + +// Delete removes the element of the evaluated expression. It can only remove +// single elements from a resource. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func (e *Expression) Delete(res fhir.Resource, options ...fhirpath.EvaluateOption) error { + if res == nil { + return fmt.Errorf("%w: nil input resource", ErrInvalidInput) + } + ctx, evalResult, err := e.evaluate(res, options...) + if err != nil { + return err + } + // If we have an empty value, it means the field is already deleted. + if evalResult.IsEmpty() { + return nil + } + toDelete, err := evalResult.ToSingleton() + if err != nil { + return fmt.Errorf("%w: fhirpatch delete can only delete a single element", ErrNotSingleton) + } + + if err := e.tryDelete(ctx.LastResult, toDelete); err != nil { + if err := e.tryDelete(ctx.BeforeLastResult, toDelete); err != nil { + return err + } + } + + return nil +} + +func (e *Expression) tryDelete(collection system.Collection, toDelete any) error { + var message protoreflect.Message + var field protoreflect.FieldDescriptor + var idx int + for _, entry := range collection { + var ok bool + root, ok := entry.(proto.Message) + if !ok { + continue + } + + field, idx, ok = e.getFieldForCollection(root, system.Collection{toDelete}) + if ok { + message = root.ProtoReflect() + break + } + } + if field == nil { + return fmt.Errorf("%w: field cannot be deleted", ErrNotPatchable) + } + if field.IsList() { + list := message.Get(field).List() + if idx == -1 { + if list.Len() <= 1 { + message.Clear(field) + return nil + } + return fmt.Errorf("%w: list containing more than one element cannot be deleted", ErrNotPatchable) + } + newlist := message.NewField(field).List() + for i := 0; i < idx; i++ { + newlist.Append(list.Get(i)) + } + for i := idx + 1; i < list.Len(); i++ { + newlist.Append(list.Get(i)) + } + message.Set(field, protoreflect.ValueOfList(newlist)) + } else { + message.Clear(field) + } + return nil +} + +// Insert inserts a value into the expression's list, at the 0-based index specified. +// Prefer Add() if you are inserting at the end of a list. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func (e *Expression) Insert(res fhir.Resource, value fhir.Base, index int, options ...fhirpath.EvaluateOption) error { + if res == nil { + return fmt.Errorf("%w: nil input resource", ErrInvalidInput) + } + ctx, evalResult, err := e.evaluate(res, options...) + if err != nil { + return err + } + last, err := ctx.LastResult.ToSingleton() + if err != nil { + return fmt.Errorf("%w: fhirpatch insert requires single element to operate on", ErrNotSingleton) + } + root, ok := last.(proto.Message) + if !ok { + return fmt.Errorf("%w: %T type is not a FHIR type", ErrNotPatchable, last) + } + field, _, ok := e.getFieldForCollection(root, evalResult) + if !ok { + return fmt.Errorf("%w: field is empty", ErrNotPatchable) + } + if !field.IsList() { + return fmt.Errorf("%w: named field is not a list", ErrNotPatchable) + } + + reflect := root.ProtoReflect() + existing := reflect.Get(field).List() + if index > existing.Len() || index < 0 { + return fmt.Errorf("%w: index %v is out of range", ErrNotPatchable, index) + } + if elem := existing.NewElement(); elem.Message().Descriptor() != value.ProtoReflect().Descriptor() { + return fmt.Errorf("%w: Element %T is not assignable to %T", ErrNotPatchable, value, elem) + } + // Recreate the list at this field + list := reflect.NewField(field).List() + + // Rebuild the list in order + for i := 0; i < existing.Len(); i++ { + if i == index { + list.Append(protoreflect.ValueOfMessage(value.ProtoReflect())) + } + list.Append(existing.Get(i)) + } + // Handle insertion at the end + if index == existing.Len() { + list.Append(protoreflect.ValueOfMessage(value.ProtoReflect())) + } + reflect.Set(field, protoreflect.ValueOfList(list)) + + return nil +} + +// getFieldForCollection returns the FieldDescriptor that corresponds to the field +// that contains the entries in `collection`. +func (e *Expression) getFieldForCollection(root proto.Message, collection system.Collection) (protoreflect.FieldDescriptor, int, bool) { + if len(collection) == 0 { + return nil, -1, false + } + ref := root.ProtoReflect() + descriptor := ref.Descriptor() + for _, entry := range collection { + msg, ok := entry.(proto.Message) + if !ok { + continue + } + fields := descriptor.Fields() + for i := 0; i < fields.Len(); i++ { + field := fields.Get(i) + if !ref.Has(field) { + continue + } + + value := ref.Get(field) + if field.Cardinality() == protoreflect.Repeated { + list := value.List() + for i := 0; i < list.Len(); i++ { + entry := list.Get(i).Message().Interface() + if entry == msg { + return field, i, true + } + } + } else if field.Kind() == protoreflect.MessageKind { + if value.Message().Interface() == msg { + return field, -1, true + } + } else { + return nil, -1, false + } + } + } + return nil, -1, false +} + +// Move moves an element within the expression's list from one index to another. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func (e *Expression) Move(resource fhir.Resource, sourceIndex, destIndex int, options ...fhirpath.EvaluateOption) error { + return ErrNotImplemented +} + +// Replace replaces the original value of the expression with the provided value. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func (e *Expression) Replace(resource fhir.Resource, value any, options ...fhirpath.EvaluateOption) error { + return ErrNotImplemented +} + +// Add appends a value to the element identified in the path, using the name specified. +// Add can be used for non-repeating elements so long as they do not already exist. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func Add(resource fhir.Resource, path, name string, value fhir.Base, opts *Options) error { + expr, err := Compile(path, opts.CompileOpts...) + if err != nil { + return err + } + return expr.Add(resource, name, value, opts.EvalOpts...) +} + +// Delete removes the element at the specified path. It can only remove +// single elements from a resource. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func Delete(resource fhir.Resource, path string, options ...opts.CompileOption) error { + expr, err := Compile(path, options...) + if err != nil { + return err + } + return expr.Delete(resource) +} + +// Insert inserts a value into the specified list, at the 0-based index specified. +// Prefer Add() if you are inserting at the end of a list. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func Insert(resource fhir.Resource, path string, value fhir.Base, index int, options ...opts.CompileOption) error { + expr, err := Compile(path, options...) + if err != nil { + return err + } + return expr.Insert(resource, value, index) +} + +// Move moves an element within the specified list from one index to another. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func Move(resource fhir.Resource, path string, sourceIndex, destIndex int, options ...opts.CompileOption) error { + expr, err := Compile(path, options...) + if err != nil { + return err + } + return expr.Move(resource, sourceIndex, destIndex) +} + +// Replace replaces the original value at the specified path with the provided value. +// +// See documentation: https://hl7.org/fhir/R4/fhirpatch.html#concept. +func Replace(resource fhir.Resource, path string, value any, options ...opts.CompileOption) error { + expr, err := Compile(path, options...) + if err != nil { + return err + } + return expr.Replace(resource, value) +} + +// storeLastExpression is a simple Expression object that can be used to store +// the last result of an evaluation (e.g. the last returned collection that +// occurs before the last evaluation node). +type storeLastExpression struct { + delegate expr.Expression +} + +func (e storeLastExpression) Evaluate(ctx *expr.Context, in system.Collection) (system.Collection, error) { + // Only store the last result if the slice is not identical to the previous one. + // This exists in case an intermediate or final node is a no-op that does not + // alter the slice, e.g.: `Patient.name.trace('something')` -- which would + // yield the same output as `Patient.name` would. + if !slices.IsIdentical(ctx.LastResult, in) { + ctx.BeforeLastResult = ctx.LastResult + ctx.LastResult = in + } + return e.delegate.Evaluate(ctx, in) +} + +// enumFromStringable parses a string value into an enum if +// the value field's type is an enum. +func enumFromStringable(msg protoreflect.Message, val stringable) (fhir.Base, error) { + strVal := val.GetValue() + container := msg.New() + valueField := container.Descriptor().Fields().ByName("value") + if valueField != nil && valueField.Kind() == protoreflect.EnumKind { + if strcase.ToKebab(strVal) != strVal { + return nil, fmt.Errorf("%w: %q", ErrInvalidEnum, strVal) + } + enumValueStr := protoreflect.Name(strcase.ToScreamingSnake(strVal)) + enum := valueField.Enum().Values().ByName(enumValueStr) + if enum == nil { + return nil, fmt.Errorf("%w: %q", ErrInvalidEnum, enumValueStr) + } + enumVal := protoreflect.ValueOfEnum(protoreflect.EnumNumber(enum.Number())) + container.Set(valueField, enumVal) + return container.Interface(), nil + } + return nil, nil +} + +// intValueFromInt converts an integer to the appropriate type. +// The type could be integer, unsignedInt, or positiveInt. +func intValueFromInt(msg protoreflect.Message, val intable) (fhir.Base, error) { + container := msg.New() + valueField := container.Descriptor().Fields().ByName("value") + if valueField != nil { + var intValue protoreflect.Value + switch valueField.Kind() { + case protoreflect.Int32Kind: + intValue = protoreflect.ValueOfInt32(val.GetValue()) + case protoreflect.Uint32Kind: + if val.GetValue() < 0 { + return nil, fmt.Errorf("%w: %v", ErrInvalidUnsignedInt, val.GetValue()) + } + intValue = protoreflect.ValueOfUint32(uint32(val.GetValue())) + default: + } + container.Set(valueField, intValue) + return container.Interface(), nil + } + return nil, nil +} diff --git a/fhirpath/patch/patch_test.go b/fhirpath/patch/patch_test.go new file mode 100644 index 0000000..7ee5e4d --- /dev/null +++ b/fhirpath/patch/patch_test.go @@ -0,0 +1,824 @@ +package patch_test + +import ( + "errors" + "testing" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + epb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/encounter_go_proto" + ispb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/imaging_study_go_proto" + opb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + rgpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/request_group_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/containedresource" + "github.com/verily-src/fhirpath-go/internal/element/extension" + "github.com/verily-src/fhirpath-go/internal/element/reference" + "github.com/verily-src/fhirpath-go/fhirpath" + "github.com/verily-src/fhirpath-go/fhirpath/patch" + "google.golang.org/protobuf/testing/protocmp" +) + +var patientWithBirthDate = &ppb.Patient{ + BirthDate: fhir.MustParseDate("1993-05-16"), +} + +func TestAdd_ValidInputs_ModifiesResource(t *testing.T) { + patientRef, _ := reference.Typed("Patient", "123") + + testCases := []struct { + name string + path string + field string + input fhir.Resource + value fhir.Base + want fhir.Resource + }{ + { + name: "Adds scalar field", + path: "Patient", + field: "birthDate", + input: &ppb.Patient{}, + value: fhir.MustParseDate("1993-05-16"), + want: &ppb.Patient{ + BirthDate: fhir.MustParseDate("1993-05-16"), + }, + }, { + name: "Adds scalar field with reserved name", + path: "Encounter", + field: "class", + input: &epb.Encounter{}, + value: fhir.Coding("", ""), + want: &epb.Encounter{ + ClassValue: fhir.Coding("", ""), + }, + }, { + name: "Adds non-enum string field", + path: "Patient.maritalStatus", + field: "text", + input: &ppb.Patient{ + MaritalStatus: &dtpb.CodeableConcept{}, + }, + value: fhir.String("H0H0H0"), + want: &ppb.Patient{ + MaritalStatus: fhir.CodeableConcept("H0H0H0"), + }, + }, { + name: "Adds enum field", + path: "Patient", + field: "gender", + input: &ppb.Patient{}, + value: fhir.String("male"), + want: &ppb.Patient{ + Gender: &ppb.Patient_GenderCode{ + Value: cpb.AdministrativeGenderCode_MALE, + }, + }, + }, { + name: "Adds valid integer to positiveInt field", + path: "Patient.telecom[0]", + field: "rank", + input: &ppb.Patient{ + Telecom: []*dtpb.ContactPoint{{}}, + }, + value: fhir.Integer(1), + want: &ppb.Patient{ + Telecom: []*dtpb.ContactPoint{ + { + Rank: fhir.PositiveInt(1), + }, + }, + }, + }, { + name: "Adds negative integer to integer field", + path: "Patient.extension[0]", + field: "value", + input: &ppb.Patient{ + Extension: []*dtpb.Extension{ + { + Url: fhir.URI(""), + }, + }, + }, + value: fhir.Integer(-10), + want: &ppb.Patient{ + Extension: []*dtpb.Extension{ + extension.New("", fhir.Integer(-10)), + }, + }, + }, { + name: "Adds integer to unsigned integer field", + path: "ImagingStudy", + field: "numberOfSeries", + input: &ispb.ImagingStudy{}, + value: fhir.Integer(0), + want: &ispb.ImagingStudy{ + NumberOfSeries: fhir.UnsignedInt(0), + }, + }, { + name: "Appends extension field", + path: "Patient", + field: "extension", + input: &ppb.Patient{}, + value: extension.New("", fhir.String("hello world")), + want: &ppb.Patient{ + Extension: []*dtpb.Extension{ + extension.New("", fhir.String("hello world")), + }, + }, + }, + { + name: "Adds reference field", + path: "Observation", + field: "subject", + input: &opb.Observation{}, + value: patientRef, + want: &opb.Observation{ + Subject: patientRef, + }, + }, + { + name: "Adds id to existing reference field", + path: "Observation.subject", + field: "patientId", + input: &opb.Observation{ + Subject: &dtpb.Reference{ + Type: fhir.URI("Patient"), + }, + }, + value: fhir.String("123"), + want: &opb.Observation{ + Subject: patientRef, + }, + }, + { + name: "Adds extension oneof field", + path: "Patient.extension[0]", + field: "value", + input: &ppb.Patient{ + Extension: []*dtpb.Extension{ + {}, + }, + }, + value: fhir.String("hello world"), + want: &ppb.Patient{ + Extension: []*dtpb.Extension{ + { + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_StringValue{ + StringValue: fhir.String("hello world"), + }, + }, + }, + }, + }, + }, { + name: "Adds contained resource oneof field", + path: "Bundle.entry[0]", + field: "resource", + input: &bcrpb.Bundle{ + Entry: []*bcrpb.Bundle_Entry{ + {}, + }, + }, + value: &ppb.Patient{}, + want: &bcrpb.Bundle{ + Entry: []*bcrpb.Bundle_Entry{ + { + Resource: containedresource.Wrap(&ppb.Patient{}), + }, + }, + }, + }, { + name: "Appends bundle entry", + path: "Bundle", + field: "entry", + input: &bcrpb.Bundle{ + Entry: []*bcrpb.Bundle_Entry{ + {}, + }, + }, + value: &bcrpb.Bundle_Entry{ + Resource: containedresource.Wrap(&ppb.Patient{}), + }, + want: &bcrpb.Bundle{ + Entry: []*bcrpb.Bundle_Entry{ + {}, + { + Resource: containedresource.Wrap(&ppb.Patient{}), + }, + }, + }, + }, { + name: "Setting start field of RequestGroup extension period", + path: "RequestGroup.extension.where(url='123').value as FHIR.Period", + field: "start", + input: &rgpb.RequestGroup{ + Extension: []*dtpb.Extension{ + {}, + { + Url: fhir.URI("123"), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_Period{ + Period: &dtpb.Period{}, + }, + }, + }, + {}, + }, + }, + value: fhir.MustParseDateTime("2006-01-02T15:04:05Z"), + want: &rgpb.RequestGroup{ + Extension: []*dtpb.Extension{ + {}, + { + Url: fhir.URI("123"), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_Period{ + Period: &dtpb.Period{ + Start: fhir.MustParseDateTime("2006-01-02T15:04:05Z"), + }, + }, + }, + }, + {}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := patch.Add(tc.input, tc.path, tc.field, tc.value, &patch.Options{}) + if err != nil { + t.Fatalf("Add(%s): unexpected err = %v", tc.name, err) + } + + got, want := tc.input, tc.want + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Add(%s): (-got +want):\n%v", tc.name, diff) + } + }) + } +} + +func TestAdd_InvalidInputs(t *testing.T) { + testCases := []struct { + name string + path string + field string + input fhir.Resource + value fhir.Base + wantErr error + }{ + { + name: "Invalid text case", + path: "Patient", + field: "birth_date", + input: &ppb.Patient{}, + value: fhir.MustParseDate("1993-05-16"), + wantErr: patch.ErrInvalidField, + }, { + name: "Underlying evaluation error", + path: "Patient.i_dont_exist", + field: "thisDoesntMatter", + input: &ppb.Patient{}, + value: fhir.String(""), + wantErr: patch.ErrInvalidField, + }, { + name: "Field does not exist", + path: "Patient", + field: "badField", + input: &ppb.Patient{}, + value: fhir.MustParseDate("1993-05-16"), + wantErr: patch.ErrInvalidField, + }, { + name: "Non-singleton result", + path: "Patient.name", + field: "family", + input: &ppb.Patient{ + Name: []*dtpb.HumanName{ + {}, + {}, + }, + }, + value: &dtpb.HumanName{}, + wantErr: patch.ErrNotSingleton, + }, { + name: "enum value with bad casing", + path: "Patient", + field: "gender", + input: &ppb.Patient{}, + value: fhir.String("MALE"), + wantErr: patch.ErrInvalidEnum, + }, { + name: "Invalid enum value", + path: "Patient", + field: "gender", + input: &ppb.Patient{}, + value: fhir.String("not_a_gender"), + wantErr: patch.ErrInvalidEnum, + }, { + name: "Invalid int for positiveInt field", + path: "Patient.telecom[0]", + field: "rank", + input: &ppb.Patient{ + Telecom: []*dtpb.ContactPoint{{}}, + }, + value: fhir.Integer(-1), + wantErr: patch.ErrInvalidUnsignedInt, + }, { + name: "Unpatchable result", + path: "Patient.active.value", + field: "something", + input: &ppb.Patient{ + Active: fhir.Boolean(true), + }, + value: fhir.Boolean(false), + wantErr: patch.ErrNotPatchable, + }, { + name: "Field already exists", + path: "Patient", + field: "active", + input: &ppb.Patient{ + Active: fhir.Boolean(true), + }, + value: fhir.Boolean(false), + wantErr: patch.ErrNotPatchable, + }, { + name: "Wrong input type", + path: "Patient", + field: "active", + input: &ppb.Patient{}, + value: fhir.String("true"), + wantErr: patch.ErrInvalidInput, + }, { + name: "Invalid oneof entry", + path: "Bundle.entry[0]", + field: "resource", + input: &bcrpb.Bundle{ + Entry: []*bcrpb.Bundle_Entry{ + {}, + }, + }, + value: fhir.String("I am not a resource"), + wantErr: patch.ErrInvalidInput, + }, { + name: "Nil replacement value", + path: "Bundle.entry[0]", + field: "resource", + input: &bcrpb.Bundle{ + Entry: []*bcrpb.Bundle_Entry{ + {}, + }, + }, + value: nil, + wantErr: patch.ErrInvalidInput, + }, { + name: "Nil input resource value", + path: "Bundle.entry[0]", + field: "resource", + input: nil, + value: fhir.String(""), + wantErr: patch.ErrInvalidInput, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := patch.Add(tc.input, tc.path, tc.field, tc.value, &patch.Options{}) + + if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Fatalf("Add(%s): got err '%v', want err '%v'", tc.name, got, want) + } + }) + } +} + +func TestDelete(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + path string + want fhir.Resource + }{ + { + name: "Deletes scalar field", + res: &ppb.Patient{ + BirthDate: fhir.MustParseDate("1993-05-16"), + }, + path: "Patient.birthDate", + want: &ppb.Patient{}, + }, + { + name: "Deletes single entry from end of list", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Betty"), fhir.String("Sue")}, + }, + }, + }, + path: "Patient.name.given[1]", + want: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Betty")}, + }, + }, + }, + }, + { + name: "Deletes single entry from beginning of list", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Betty"), fhir.String("Sue")}, + }, + }, + }, + path: "Patient.name.given[0]", + want: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Sue")}, + }, + }, + }, + }, + { + name: "Deletes list containing single entry", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Betty")}, + }, + }, + }, + path: "Patient.name.given", + want: &ppb.Patient{ + Name: []*dtpb.HumanName{ + {}, + }, + }, + }, + { + name: "No-ops on empty but valid field", + res: &ppb.Patient{}, + path: "Patient.birthDate", + want: &ppb.Patient{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := patch.Delete(tc.res, tc.path) + if err != nil { + t.Fatalf("Delete(%v): got unexpected err = %v", tc.name, err) + } + + got, want := tc.res, tc.want + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Delete(%s): (-got +want):\n%v", tc.name, diff) + } + }) + } +} + +func TestDelete_BadInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + path string + wantErr error + }{ + { + name: "Nil input", + res: nil, + path: "Patient.birthDate", + wantErr: patch.ErrInvalidInput, + }, + { + name: "Evaluation fails", + res: &ppb.Patient{}, + path: "Patient.no_exist", + wantErr: fhirpath.ErrInvalidField, + }, + { + name: "Attempting to delete more than one value", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Jieun"), fhir.String("IU")}, + }, + }, + }, + path: "Patient.name.given", + wantErr: patch.ErrNotSingleton, + }, + { + name: "Attempting to delete primitive value", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Jieun"), fhir.String("IU")}, + }, + }, + }, + path: "Patient.name.given[0].value", + wantErr: patch.ErrNotPatchable, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := patch.Delete(tc.res, tc.path) + + if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("Delete(%s): got error %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestInsert(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + path string + value fhir.Base + index int + want fhir.Resource + }{ + { + name: "Inserts name at beginning", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("IU")}, + }, + }, + }, + path: "Patient.name[0].given", + value: fhir.String("Jieun"), + index: 0, + want: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Jieun"), fhir.String("IU")}, + }, + }, + }, + }, { + name: "Inserts name at end", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("IU")}, + }, + }, + }, + path: "Patient.name[0].given", + value: fhir.String("Jieun"), + index: 1, + want: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("IU"), fhir.String("Jieun")}, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := patch.Insert(tc.res, tc.path, tc.value, tc.index) + if err != nil { + t.Fatalf("Insert(%v): got unexpected err = %v", tc.name, err) + } + + got, want := tc.res, tc.want + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Insert(%s): (-got +want):\n%v", tc.name, diff) + } + }) + } +} + +func TestInsert_InvalidCondition_ReturnsError(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + path string + value fhir.Base + index int + wantErr error + }{ + { + name: "Nil Input", + res: nil, + path: "Patient.name[0].given", + value: fhir.String("Jieun"), + index: 0, + wantErr: patch.ErrInvalidInput, + }, + { + name: "Evaluation fails", + res: &ppb.Patient{}, + path: "Patient.no_exist", + value: fhir.String("Jieun"), + index: 0, + wantErr: fhirpath.ErrInvalidField, + }, + { + name: "Ambiguous insertion", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Jonathan")}, + }, + { + Given: []*dtpb.String{fhir.String("Jon")}, + }, + }, + }, + path: "Patient.name.given", + value: fhir.String("Jonny-Boy"), + index: 0, + wantErr: patch.ErrNotSingleton, + }, + { + name: "Extraction type is not FHIR type", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Jonathan")}, + }, + }, + }, + path: "Patient.name.given.value.toString()", + value: fhir.String("Jonny-Boy"), + index: 0, + wantErr: patch.ErrNotPatchable, + }, + { + name: "Output is disconnected value", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("Jonathan")}, + }, + }, + }, + path: "Patient.name.given.now()", + value: fhir.String("Jonny-Boy"), + index: 0, + wantErr: patch.ErrNotPatchable, + }, + { + name: "Insert target is not a list", + res: &ppb.Patient{ + BirthDate: fhir.DateNow(), + }, + path: "Patient.birthDate.value", + value: fhir.DateNow(), + index: 0, + wantErr: patch.ErrNotPatchable, + }, + { + name: "Index is out of range", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("IU")}, + }, + }, + }, + path: "Patient.name[0].given", + value: fhir.String("Jieun"), + index: 42, + wantErr: patch.ErrNotPatchable, + }, + { + name: "Index is negative", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("IU")}, + }, + }, + }, + path: "Patient.name[0].given", + value: fhir.String("Jieun"), + index: -1, + wantErr: patch.ErrNotPatchable, + }, + { + name: "Input value is wrong type", + res: &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("IU")}, + }, + }, + }, + path: "Patient.name[0].given", + value: fhir.ID("Jieun"), + index: 0, + wantErr: patch.ErrNotPatchable, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := patch.Insert(tc.res, tc.path, tc.value, tc.index) + + if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("Insert(%s): error got = %v, want = %v", tc.name, got, want) + } + }) + } +} + +func TestMove(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + path string + srcIndex int + dstIndex int + wantRes fhir.Resource + wantErr error + }{ + { + "moves name", + &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: []*dtpb.String{fhir.String("IU"), fhir.String("Jieun")}, + }, + }, + }, + "Patient.name[0].given", + 0, + 1, + nil, + patch.ErrNotImplemented, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := patch.Move(tc.res, tc.path, tc.srcIndex, tc.dstIndex) + + if got, want := err, tc.wantErr; !errors.Is(got, want) { + t.Fatalf("Move(%s): error got = %v, want = %v", tc.name, got, want) + } + + got, want := fhir.Resource(nil), tc.wantRes + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Move(%s): (-got +want):\n%v", tc.name, diff) + } + }) + } +} + +func TestReplace(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + path string + value any + wantRes fhir.Resource + wantErr error + }{ + { + "replaces birthDate", + patientWithBirthDate, + "Patient.birthDate", + "2007-07-05", + nil, + patch.ErrNotImplemented, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := patch.Replace(tc.res, tc.path, tc.value) + + if got, want := err, tc.wantErr; !errors.Is(got, want) { + t.Fatalf("Replace(%s): error got = %v, want = %v", tc.name, got, want) + } + + got, want := fhir.Resource(nil), tc.wantRes + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Replace(%s): (-got +want):\n%v", tc.name, diff) + } + }) + } +} diff --git a/fhirpath/system/cmp.go b/fhirpath/system/cmp.go new file mode 100644 index 0000000..f9128b9 --- /dev/null +++ b/fhirpath/system/cmp.go @@ -0,0 +1,81 @@ +package system + +import "reflect" + +// Equal compares two FHIRPath System types for equality. This uses standard +// equality semantics and will return true if the value should yield a value +// that is true, and false otherwise. +// +// This is effectively sugar over calling: +// +// result, ok := TryEqual(lhs, rhs) +// return result && ok +// +// See https://hl7.org/fhirpath/n1/#equality +func Equal(lhs, rhs Any) bool { + result, ok := TryEqual(lhs, rhs) + return ok && result +} + +// TryEqual compares two FHIRPath System types for equality. This returns a +// value if and only if the comparison of the underlying System types should +// also yield a value, as defined in FHIRPath's equality operation. +// +// See https://hl7.org/fhirpath/n1/#equality +// +// For system types that define a custom "Equal" function, this will call the +// underlying function. For system types that define a custom "TryEqual" function +// this will call the underlying function. Otherwise, this will compare the raw +// representation instead. +func TryEqual(lhs, rhs Any) (bool, bool) { + lhs, rhs = Normalize(lhs, rhs), Normalize(rhs, lhs) + if result, has, ok := callTryEqual(lhs, rhs); ok { + return result, has + } + if result, ok := callEqual(lhs, rhs); ok { + return result, true + } + return lhs == rhs, true +} + +func callTryEqual(lhs, rhs Any) (bool, bool, bool) { + if eq, ok := reflect.TypeOf(lhs).MethodByName("TryEqual"); ok { + funcType := eq.Func.Type() + arg0, arg1 := funcType.In(0), funcType.In(1) + if arg0 != reflect.TypeOf(lhs) || arg1.ConvertibleTo(reflect.TypeOf(rhs)) { + return false, false, true + } + args := []reflect.Value{ + reflect.ValueOf(lhs), + reflect.ValueOf(rhs), + } + result := eq.Func.Call(args) + return result[0].Bool(), result[1].Bool(), true + } + return false, false, false +} + +func callEqual(lhs, rhs Any) (bool, bool) { + if got, ok := callBinaryComparator("Equal", lhs, rhs); ok { + return got.(bool), true + } + return false, false +} + +// callBinaryComparator invokes a binary comparator operator +func callBinaryComparator(name string, lhs, rhs any) (any, bool) { + if eq, ok := reflect.TypeOf(lhs).MethodByName(name); ok { + args := []reflect.Value{ + reflect.ValueOf(lhs), + reflect.ValueOf(rhs), + } + funcType := eq.Func.Type() + arg0, arg1 := funcType.In(0), funcType.In(1) + if arg0 != reflect.TypeOf(lhs) || arg1.ConvertibleTo(reflect.TypeOf(rhs)) { + return nil, true + } + result := eq.Func.Call(args) + return result[0].Bool(), true + } + return nil, false +} diff --git a/fhirpath/system/collection.go b/fhirpath/system/collection.go new file mode 100644 index 0000000..ae2f447 --- /dev/null +++ b/fhirpath/system/collection.go @@ -0,0 +1,239 @@ +package system + +import ( + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/narrow" + "github.com/verily-src/fhirpath-go/internal/fhir" + "google.golang.org/protobuf/proto" +) + +var ( + // ErrNotConvertible is an error raised when attempting to call Collection.To* + // to a type that is not convertible. + ErrNotConvertible = errors.New("not convertible") +) + +// Collection abstracts the input and output type for +// FHIRPath expressions as a collection that can contain anything. +type Collection []any + +// IsSingleton is a utility function to determine if a collection is a +// "Singleton" collection (i.e. contains 1 value). +func (c Collection) IsSingleton() bool { + return len(c) == 1 +} + +// IsEmpty is a utility function to determine if a collection is empty. +func (c Collection) IsEmpty() bool { + return len(c) == 0 +} + +// TryEqual compares this collection to the supplied collection for, comparing each entry +// with the corresponding entry in c2. Returns true if every entry +// in c1 is equivalent to that of c2. If the lengths are mismatched, +// returns false. +func (c Collection) TryEqual(other Collection) (bool, bool) { + if len(c) != len(other) { + return false, true + } + + for i := range other { + okOne := IsPrimitive(c[i]) + okTwo := IsPrimitive(other[i]) + if okOne != okTwo { + return false, true + } + if !okOne && !proto.Equal(c[i].(fhir.Base), other[i].(fhir.Base)) { + return false, true + } + if !okOne { + return true, true + } + primitiveOne, err := From(c[i]) + if err != nil { + return false, true + } + primitiveTwo, err := From(other[i]) + if err != nil { + return false, true + } + primitiveOne = Normalize(primitiveOne, primitiveTwo) + primitiveTwo = Normalize(primitiveTwo, primitiveOne) + equal, ok := TryEqual(primitiveOne, primitiveTwo) + if !ok { + return false, false + } + if !equal { + return false, true + } + } + return true, true +} + +// ToSingletonBoolean evaluates a collection as a boolean with singleton evaluation of +// collection rules. Returns a collection containing a single Boolean, or empty if the +// input is empty. +func (c Collection) ToSingletonBoolean() ([]Boolean, error) { + length := len(c) + if length == 0 { + return []Boolean{}, nil + } + if length > 1 { + return nil, fmt.Errorf("collection can't evaluate to bool, contains %v elements", length) + } + val, _ := From(c[0]) + if boolean, ok := val.(Boolean); ok { + return []Boolean{boolean}, nil + } + return []Boolean{true}, nil +} + +// ToSingleton evaluates a collection as a single value, returning an error if the +// collection contains 0 or more than 1 entry. +func (c Collection) ToSingleton() (any, error) { + if !c.IsSingleton() { + return nil, fmt.Errorf("collection is not singleton") + } + return c[0], nil +} + +// ToBool converts this Collection into a Go native 'bool' type, following the +// logic of singleton evaluation of booleans. If this collection is empty, +// it returns false, if it contains more than 1 entry, it will return an error. +func (c Collection) ToBool() (bool, error) { + if c.IsEmpty() { + return false, nil + } + v, err := c.ToSingleton() + if err != nil { + return false, err + } + switch val := v.(type) { + case Boolean: + return bool(val), nil + case *dtpb.Boolean: + return val.GetValue(), nil + } + // A single value that is not bool is always implicitly 'true' + return true, nil +} + +// ToInt32 converts this Collection into a Go native 'int32' type. +// If this collection is empty, or contains more than 1 entry, it will return +// an error. If the type in the collection is not a System.Integer, or something +// derived from a FHIR.Integer, this will raise an ErrNotConvertible. +func (c Collection) ToInt32() (int32, error) { + v, err := c.ToSingleton() + if err != nil { + return 0, err + } + switch val := v.(type) { + case Integer: + return int32(val), nil + case *dtpb.Integer: + return val.GetValue(), nil + case *dtpb.PositiveInt: + if val, ok := narrow.ToInt32(val.GetValue()); ok { + return val, nil + } + return 0, c.convertErr(val.GetValue(), "int32") + case *dtpb.UnsignedInt: + if val, ok := narrow.ToInt32(val.GetValue()); ok { + return val, nil + } + return 0, c.convertErr(val.GetValue(), "int32") + } + return 0, c.convertErr(v, "int32") +} + +// ToFloat64 converts this Collection into a Go native 'float64' type. +// If this collection is empty, or contains more than 1 entry, it will return +// an error. If the type in the collection is not a System.Integer, or something +// derived from a FHIR.Integer, this will raise an ErrNotConvertible. +func (c Collection) ToFloat64() (float64, error) { + v, err := c.ToSingleton() + if err != nil { + return 0, err + } + switch val := v.(type) { + case Decimal: + return decimal.Decimal(val).InexactFloat64(), nil + case Integer: + return float64(val), nil + case *dtpb.Integer: + return float64(val.GetValue()), nil + case *dtpb.PositiveInt: + return float64(val.GetValue()), nil + case *dtpb.UnsignedInt: + return float64(val.GetValue()), nil + } + return 0, c.convertErr(v, "float64") +} + +// ToString converts this Collection into a Go native 'string' type. +// If this collection is empty, or contains more than 1 entry, it will return +// an error. If the type in the collection is not a System.String, or something +// derived from a FHIR.String, this will raise an ErrNotConvertible. +func (c Collection) ToString() (string, error) { + v, err := c.ToSingleton() + if err != nil { + return "", err + } + result, err := From(v) + if err != nil { + return "", c.convertErr(v, "string") + } + if str, ok := result.(String); ok { + return string(str), nil + } + return "", c.convertErr(v, "string") +} + +// Contains returns true if the specified value is contained within this +// collection. This will normalize types to system-types if necessary to check +// for containment. +func (c Collection) Contains(value any) bool { + sys, err := From(value) + if err == nil { + return c.containsSystem(sys) + } + msg, ok := value.(proto.Message) + if ok { + return c.containsProto(msg) + } + return false +} + +func (c Collection) containsSystem(value Any) bool { + for _, v := range c { + sys, err := From(v) + if err != nil { + continue + } + if Equal(sys, value) { + return true + } + } + return false +} + +func (c Collection) containsProto(value proto.Message) bool { + for _, v := range c { + msg, ok := v.(proto.Message) + if !ok { + continue + } + if proto.Equal(msg, value) { + return true + } + } + return false +} + +func (c Collection) convertErr(got any, want string) error { + return fmt.Errorf("type %T %w to %v", got, ErrNotConvertible, want) +} diff --git a/fhirpath/system/collection_test.go b/fhirpath/system/collection_test.go new file mode 100644 index 0000000..61641fc --- /dev/null +++ b/fhirpath/system/collection_test.go @@ -0,0 +1,320 @@ +package system_test + +import ( + "testing" + + "github.com/verily-src/fhirpath-go/internal/fhir" + "google.golang.org/protobuf/proto" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestEqual_ReturnsResult(t *testing.T) { + patient1 := &ppb.Patient{ + Id: fhir.ID("123"), + } + patient2 := &ppb.Patient{ + Id: fhir.ID("234"), + } + + testCases := []struct { + name string + leftCollection system.Collection + rightCollection system.Collection + equal bool + }{ + { + name: "mismatched collection lengths", + leftCollection: system.Collection{"one"}, + rightCollection: system.Collection{"one", "two"}, + equal: false, + }, + { + name: "mismatched types (primitive and complex)", + leftCollection: system.Collection{system.String("abc")}, + rightCollection: system.Collection{&ppb.Patient{}}, + equal: false, + }, + { + name: "system types not equal", + leftCollection: system.Collection{system.String("abc")}, + rightCollection: system.Collection{system.String("abcd")}, + equal: false, + }, + { + name: "proto types not equal", + leftCollection: system.Collection{patient1}, + rightCollection: system.Collection{patient2}, + equal: false, + }, + { + name: "equal system types", + leftCollection: system.Collection{system.String("sausage")}, + rightCollection: system.Collection{system.String("sausage")}, + equal: true, + }, + { + name: "equal proto types", + leftCollection: system.Collection{fhir.Code("#blessed")}, + rightCollection: system.Collection{fhir.Code("#blessed")}, + equal: true, + }, + { + name: "equal complex types", + leftCollection: system.Collection{patient1}, + rightCollection: system.Collection{patient1}, + equal: true, + }, + { + name: "full collection not equal", + leftCollection: system.Collection{system.String("Not"), system.String("Equal")}, + rightCollection: system.Collection{system.String("Not"), system.String("Equivalent")}, + equal: false, + }, + { + name: "equal collection", + leftCollection: system.Collection{system.String("Is"), system.String("Equal")}, + rightCollection: system.Collection{system.String("Is"), system.String("Equal")}, + equal: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, ok := tc.leftCollection.TryEqual(tc.rightCollection) + + if !ok { + t.Fatalf("Collection.TryEqual: did not return value when one expected") + } + if got, want := got, tc.equal; got != want { + t.Errorf("Collection.TryEqual returned incorrect result, got: %v, want %v", got, want) + } + }) + } +} + +func TestToSingletonBoolean_ConvertsToBool(t *testing.T) { + testCases := []struct { + name string + inputCollection system.Collection + want []system.Boolean + shouldError bool + }{ + { + name: "returns error on non-singleton collection", + inputCollection: system.Collection{1, 2}, + shouldError: true, + }, + { + name: "returns contained proto boolean", + inputCollection: system.Collection{fhir.Boolean(false)}, + want: []system.Boolean{false}, + }, + { + name: "returns contained system boolean", + inputCollection: system.Collection{system.Boolean(true)}, + want: []system.Boolean{true}, + }, + { + name: "returns true on singleton non-bool collection", + inputCollection: system.Collection{system.String("1")}, + want: []system.Boolean{true}, + }, + { + name: "propagates empty collection input", + inputCollection: system.Collection{}, + want: []system.Boolean{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.inputCollection.ToSingletonBoolean() + + if gotErr, wantErr := err != nil, tc.shouldError; gotErr != wantErr { + t.Fatalf("Collection.SingletonBoolean() returned unexpected error result: gotErr %v, wantErr %v", gotErr, wantErr) + } + if !cmp.Equal(got, tc.want) { + t.Errorf("Collection.SingletonBoolean() returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestCollection_ToFloat64(t *testing.T) { + tests := []struct { + name string + c system.Collection + want float64 + wantErr bool + }{ + { + name: "errors if input is not a number", + c: system.Collection{system.String("10.1")}, + want: 0, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + c: system.Collection{system.MustParseDecimal("10.1"), system.String("10.2")}, + want: 0, + wantErr: true, + }, + { + name: "converts Decimal into float64 successfully", + c: system.Collection{system.MustParseDecimal("10.5")}, + want: 10.5, + wantErr: false, + }, + { + name: "converts Integer into float64 successfully", + c: system.Collection{fhir.Integer(-100)}, + want: -100, + wantErr: false, + }, + { + name: "converts PositiveInt into float64 successfully", + c: system.Collection{fhir.PositiveInt(100)}, + want: 100, + wantErr: false, + }, + { + name: "converts UnsignedInt into float64 successfully", + c: system.Collection{fhir.UnsignedInt(10)}, + want: 10, + wantErr: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.c.ToFloat64() + if (err != nil) != tc.wantErr { + t.Errorf("ToFloat64() error = %v, wantErr %v", err, tc.wantErr) + return + } + if got != tc.want { + t.Errorf("ToFloat64() got = %v, want %v", got, tc.want) + } + }) + } +} + +func TestCollection_ToInt32(t *testing.T) { + tests := []struct { + name string + c system.Collection + want int32 + wantErr bool + }{ + { + name: "errors if input is not a number", + c: system.Collection{system.String("10.1")}, + want: 0, + wantErr: true, + }, + { + name: "errors if input length is more than 1", + c: system.Collection{system.Integer(1), system.String("10.2")}, + want: 0, + wantErr: true, + }, + { + name: "converts positive Integer into int32 successfully", + c: system.Collection{system.Integer(100)}, + want: 100, + wantErr: false, + }, + { + name: "converts negative Integer into int32 successfully", + c: system.Collection{fhir.Integer(-100)}, + want: -100, + wantErr: false, + }, + { + name: "converts PositiveInt into int32 successfully", + c: system.Collection{fhir.PositiveInt(1000)}, + want: 1000, + wantErr: false, + }, + { + name: "converts UnsignedInt into int32 successfully", + c: system.Collection{fhir.UnsignedInt(10)}, + want: 10, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.c.ToInt32() + if (err != nil) != tt.wantErr { + t.Errorf("ToInt32() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ToInt32() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestContains(t *testing.T) { + patient := &ppb.Patient{ + Name: []*dtpb.HumanName{ + { + Given: fhir.Strings("Foo", "Barl"), + Family: fhir.String("Bazington"), + }, + }, + } + testCases := []struct { + name string + haystack system.Collection + needle any + want bool + }{ + { + name: "Haystack contains exact match", + haystack: system.Collection{system.Integer(42), system.String("Hello")}, + needle: system.String("Hello"), + want: true, + }, { + name: "Needle can convert to haystack value", + haystack: system.Collection{system.Integer(42), system.String("Hello")}, + needle: fhir.String("Hello"), + want: true, + }, { + name: "Haystack can convert to needle value", + haystack: system.Collection{system.Integer(42), fhir.String("Hello")}, + needle: system.String("Hello"), + want: true, + }, { + name: "Haystack does not contain needle", + haystack: system.Collection{system.Integer(42), fhir.String("Hello")}, + needle: system.String("World"), + want: false, + }, { + name: "Needle is a proto value in haystack", + haystack: system.Collection{system.Integer(42), patient, system.String("Foo")}, + needle: proto.Clone(patient), + want: true, + }, { + name: "Needle is a proto value not in haystack", + haystack: system.Collection{system.Integer(42), patient, system.String("Foo")}, + needle: &ppb.Patient{}, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.haystack.Contains(tc.needle) + + if got != tc.want { + t.Errorf("Collection.Contains(%v): got %v, want %v", tc.name, got, tc.want) + } + }) + } +} diff --git a/fhirpath/system/consts.go b/fhirpath/system/consts.go new file mode 100644 index 0000000..17d6cdf --- /dev/null +++ b/fhirpath/system/consts.go @@ -0,0 +1,26 @@ +package system + +import "errors" + +// Common errors. +var ( + ErrTypeMismatch = errors.New("operation not defined between given types") + // Date, Time, DateTime, and Quantity have special cases for equality and inequality logic + // where an empty collection should be returned when their precisions/units are mismatched. + ErrMismatchedPrecision = errors.New("mismatched precision") + ErrMismatchedUnit = errors.New("mismatched unit") + ErrIntOverflow = errors.New("operation resulted in integer overflow") +) + +// Type names. +const ( + stringType = "String" + booleanType = "Boolean" + integerType = "Integer" + decimalType = "Decimal" + dateType = "Date" + dateTimeType = "DateTime" + timeType = "Time" + quantityType = "Quantity" + anyType = "Any" +) diff --git a/fhirpath/system/date.go b/fhirpath/system/date.go new file mode 100644 index 0000000..acf29f0 --- /dev/null +++ b/fhirpath/system/date.go @@ -0,0 +1,269 @@ +package system + +import ( + "fmt" + "strings" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirconv" +) + +// Date represents Date values in the range 0001-01-01 to +// 9999-12-31 with a 1 day step size. It uses YYYY-MM-DD with +// optional month and day parts. +type Date struct { + date time.Time + l layout +} + +// ParseDate takes as input a string and returns a Date +// if the input is a valid FHIRPath Date string, otherwise +// returns an error. +func ParseDate(value string) (Date, error) { + dateLayouts := []string{ + dayLayout, + monthLayout, + yearLayout, + } + + var t time.Time + var err error + value = strings.TrimPrefix(value, "@") + for _, l := range dateLayouts { + if t, err = time.Parse(l, value); err == nil { + return Date{t, layout(l)}, nil + } + } + return Date{}, fmt.Errorf("unable to parse date '%s': %w", value, err) +} + +// MustParseDate takes as input a string and returns a Date if +// the input is valid. Panics if the string can't be parsed. +func MustParseDate(value string) Date { + date, err := ParseDate(value) + if err != nil { + panic(err) + } + return date +} + +// DateFromProto takes a proto Date as input and returns a system Date. +func DateFromProto(proto *dtpb.Date) (Date, error) { + t, err := fhirconv.DateToTime(proto) + if err != nil { + return Date{}, err + } + var l layout + switch proto.Precision { + case dtpb.Date_DAY: + l = dayLayout + case dtpb.Date_MONTH: + l = monthLayout + case dtpb.Date_YEAR: + l = yearLayout + } + return Date{t, l}, nil +} + +// ToProtoDate returns a proto Date based on a system Date. +func (d Date) ToProtoDate() *dtpb.Date { + date := fhir.Date(d.date) + var p dtpb.Date_Precision + switch d.l { + case dayLayout: + p = dtpb.Date_DAY + case monthLayout: + p = dtpb.Date_MONTH + case yearLayout: + p = dtpb.Date_YEAR + } + date.Precision = p + return date +} + +// String formats the time as a date string. +func (d Date) String() string { + return d.date.Format(string(d.l)) +} + +// TryEqual returns a bool representing whether or not +// the value of d is equal to input.(Date). +// This function may not return a value, depending on the precision of +// the other value; represented by the second bool return value. +func (d Date) TryEqual(input Any) (bool, bool) { + val, ok := input.(Date) + if !ok { + return false, true + } + if d.l == val.l { + return d.date.Equal(val.date), true + } + + dComponents := d.getComponents() + valComponents := val.getComponents() + + minPrecision := min(int(dateMap[d.l]), int(dateMap[val.l])) + + for i := 0; i <= minPrecision; i++ { + if dComponents[i] == valComponents[i] { + continue + } + return false, true + } + return false, false +} + +// Less returns true if the value of d is less than input.(Date). +// Compares component by component, and returns an error if there is a +// precision mismatch. If input is not a Date, returns an error. +func (d Date) Less(input Any) (Boolean, error) { + val, ok := input.(Date) + if !ok { + return false, fmt.Errorf("%w: %T, %T", ErrTypeMismatch, d, input) + } + if d.l == val.l { + return Boolean(d.date.Before(val.date)), nil + } + + dComponents := d.getComponents() + valComponents := val.getComponents() + + minPrecision := min(int(dateMap[d.l]), int(dateMap[val.l])) + + for i := 0; i <= minPrecision; i++ { + if dComponents[i] == valComponents[i] { + continue + } + return dComponents[i] < valComponents[i], nil + } + return false, ErrMismatchedPrecision +} + +// Add returns the result of d + input. Returns an +// error if it is not a valid time-valued quantity. +func (d Date) Add(input Quantity) (Date, error) { + var result time.Time + value := int(decimal.Decimal(input.value).IntPart()) + switch input.unit { + case "year", "years": + result = addYear(d.date, value) + case "month", "months": + result = addMonth(d.date, value) + case "week", "weeks": + value = 7 * value + result = d.date.AddDate(0, 0, value) + case "day", "days": + result = d.date.AddDate(0, 0, value) + default: + return Date{}, fmt.Errorf("%w: can't add to date", ErrMismatchedUnit) + } + + // Reformat to truncate date to initial precision. This causes the addition result + // to round down to the highest precision value. + result, err := time.Parse(string(d.l), result.Format(string(d.l))) + if err != nil { + return Date{}, err + } + return Date{result, d.l}, nil +} + +// Sub returns the result of d - input. Returns an error if the +// input does not represent a valid time-valued quantity. +func (d Date) Sub(input Quantity) (Date, error) { + // Handle partial dates by rounding quantity to appropriate precision. + // Subtraction is not symmetric with addition, so the solution of truncation + // as done in Add, cannot be applied here. + if d.l == yearLayout { + years, err := input.toYears() + if err != nil { + return Date{}, err + } + return Date{d.date.AddDate(-years, 0, 0), d.l}, nil + } + if d.l == monthLayout { + months, err := input.toMonths() + if err != nil { + return Date{}, err + } + return Date{d.date.AddDate(0, -months, 0), d.l}, nil + } + + // subtract appropriate position of date, for non-partial dates. + var result time.Time + value := -int(decimal.Decimal(input.value).IntPart()) + switch input.unit { + case "year", "years": + result = addYear(d.date, value) + case "month", "months": + result = addMonth(d.date, value) + case "week", "weeks": + value = 7 * value + result = d.date.AddDate(0, 0, value) + case "day", "days": + result = d.date.AddDate(0, 0, value) + default: + return Date{}, fmt.Errorf("%w: can't add to date", ErrMismatchedUnit) + } + + return Date{result, d.l}, nil +} + +// Name returns the type name. +func (d Date) Name() string { + return dateType +} + +// Equal method to override cmp.Equal. +func (d Date) Equal(d2 Date) bool { + return d.date.Format(string(d.l)) == d2.date.Format(string(d2.l)) +} + +func (d Date) getComponents() []int { + return []int{ + d.date.Year(), + int(d.date.Month()), + d.date.Day(), + } +} + +func (d Date) ToDateTime() DateTime { + var dateToDateTime = map[layout]layout{ + dayLayout: dtDayLayout, + monthLayout: dtMonthLayout, + yearLayout: dtYearLayout, + } + + return DateTime{d.date, dateToDateTime[d.l]} +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} + +// addMonth adds m months to the given time, without +// normalizing to the next month. +// eg. 2020-01-30 + 1 month = 2020-02-28. +func addMonth(t time.Time, m int) time.Time { + added := t.AddDate(0, m, 0) + if day := added.Day(); day != t.Day() { + return added.AddDate(0, 0, -day) + } + return added +} + +// addYear adds y years to the given time, without +// normalizing to the next month in the special case of leap years. +// eg. 2020-02-29 + 1 year = 2020-02-28. +func addYear(t time.Time, y int) time.Time { + added := t.AddDate(y, 0, 0) + if day := added.Day(); day != t.Day() { + return added.AddDate(0, 0, -day) + } + return added +} diff --git a/fhirpath/system/date_test.go b/fhirpath/system/date_test.go new file mode 100644 index 0000000..b663045 --- /dev/null +++ b/fhirpath/system/date_test.go @@ -0,0 +1,437 @@ +package system_test + +import ( + "errors" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/testing/protocmp" + "testing" +) + +func TestParseDate_ReturnsDate(t *testing.T) { + testCases := []struct { + name string + input string + }{ + {"Year", "2012"}, + {"Month", "2012-05"}, + {"Day", "2012-05-21"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := system.ParseDate(tc.input); err != nil { + t.Fatalf("ParseDate(%s) returned unexpected error: %v", tc.input, err) + } + }) + } +} + +func TestParseDate_ReturnsError(t *testing.T) { + testCases := []struct { + name string + input string + }{ + {"Bad format", "05-01-2007"}, + {"Bad month", "2023-13-21"}, + {"Bad day", "2023-12-34"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := system.ParseDate(tc.input); err == nil { + t.Fatalf("ParseDate(%s) didn't return error when expected", tc.input) + } + }) + } +} + +func TestDate_Equal(t *testing.T) { + testCases := []struct { + name string + dateOne system.Date + dateTwo system.Any + shouldEqual bool + wantOk bool + }{ + { + name: "same date different precision", + dateOne: system.MustParseDate("2023"), + dateTwo: system.MustParseDate("2023-01-01"), + wantOk: false, + }, + { + name: "different date", + dateOne: system.MustParseDate("2023-01-01"), + dateTwo: system.MustParseDate("2023-01-02"), + shouldEqual: false, + wantOk: true, + }, + { + name: "different type", + dateOne: system.MustParseDate("2023-01-01"), + dateTwo: system.String("2023-01-01"), + shouldEqual: false, + wantOk: true, + }, + { + name: "mismatched precision but not equal", + dateOne: system.MustParseDate("2023-02"), + dateTwo: system.MustParseDate("2023-01-29"), + shouldEqual: false, + wantOk: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, ok := tc.dateOne.TryEqual(tc.dateTwo) + + if ok != tc.wantOk { + t.Errorf("Date.Equal: ok got %v, want %v", ok, tc.wantOk) + } + if got != tc.shouldEqual { + t.Errorf("Date.Equal returned unexpected equality: got %v, want %v", got, tc.shouldEqual) + } + }) + } +} + +func TestDateFromProto_Converts(t *testing.T) { + testCases := []struct { + name string + dateProto *dtpb.Date + wantDate system.Date + }{ + { + name: "converts day precision", + dateProto: fhir.MustParseDate("2022-05-23"), + wantDate: system.MustParseDate("2022-05-23"), + }, + { + name: "converts month precision", + dateProto: fhir.MustParseDate("2012-12"), + wantDate: system.MustParseDate("2012-12"), + }, + { + name: "converts year precision", + dateProto: fhir.MustParseDate("2002"), + wantDate: system.MustParseDate("2002"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := system.DateFromProto(tc.dateProto) + if err != nil { + t.Fatalf("DateFromProto(%s) raises unexpected error: %v", tc.dateProto.String(), err) + } + if diff := cmp.Diff(tc.wantDate, got); diff != "" { + t.Errorf("DateFromProto(%s) incorrectly converts Date: (-want, +got)\n%s", tc.dateProto.String(), diff) + } + }) + } +} + +func TestDateToProtoDate_Converts(t *testing.T) { + testCases := []struct { + name string + date system.Date + want *dtpb.Date + }{ + { + name: "day precision", + date: system.MustParseDate("2025-01-30"), + want: fhir.MustParseDate("2025-01-30"), + }, + { + name: "month precision", + date: system.MustParseDate("2025-01"), + want: fhir.MustParseDate("2025-01"), + }, + { + name: "year precision", + date: system.MustParseDate("2025"), + want: fhir.MustParseDate("2025"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.date.ToProtoDate() + + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Date.ToProtoDate returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestDateLess_ReturnsBoolean(t *testing.T) { + testCases := []struct { + name string + dateOne system.Date + dateTwo system.Date + want system.Boolean + }{ + { + name: "returns true for an earlier date", + dateOne: system.MustParseDate("2020-05-01"), + dateTwo: system.MustParseDate("2020-06-01"), + want: true, + }, + { + name: "returns true for an earlier date with less precision", + dateOne: system.MustParseDate("2020-05"), + dateTwo: system.MustParseDate("2020-07-28"), + want: true, + }, + { + name: "returns false for a later date", + dateOne: system.MustParseDate("2023-05-01"), + dateTwo: system.MustParseDate("2022-02-02"), + want: false, + }, + { + name: "returns false for a later date with less precision", + dateOne: system.MustParseDate("2020"), + dateTwo: system.MustParseDate("2019-05"), + want: false, + }, + { + name: "returns false for equivalent dates", + dateOne: system.MustParseDate("2008-02-09"), + dateTwo: system.MustParseDate("2008-02-09"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.dateOne.Less(tc.dateTwo) + + if err != nil { + t.Fatalf("Date.Less returned unexpected error: %v", err) + } + if got != tc.want { + t.Errorf("Date.Less returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestDateLt_ReturnsError(t *testing.T) { + testCases := []struct { + name string + dateOne system.Date + input system.Any + wantErr error + }{ + { + name: "wrong input type", + dateOne: system.MustParseDate("2020-12-31"), + input: system.String("asdf"), + wantErr: system.ErrTypeMismatch, + }, + { + name: "date precision shorter than input", + dateOne: system.MustParseDate("2020-04"), + input: system.MustParseDate("2020-04-01"), + wantErr: system.ErrMismatchedPrecision, + }, + { + name: "input precision shorter than date", + dateOne: system.MustParseDate("2023-05-31"), + input: system.MustParseDate("2023-05"), + wantErr: system.ErrMismatchedPrecision, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.dateOne.Less(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Date.Less returned incorrect error: got %v, want %v", err, tc.wantErr) + } + }) + } +} + +func TestDateAdd_ReturnsSum(t *testing.T) { + testCases := []struct { + name string + date system.Date + input system.Quantity + want system.Date + wantErr error + }{ + { + name: "overflows months", + date: system.MustParseDate("2025-11-30"), + input: system.MustParseQuantity("2", "months"), + want: system.MustParseDate("2026-01-30"), + }, + { + name: "correctly adds days", + date: system.MustParseDate("2021-02-28"), + input: system.MustParseQuantity("2", "days"), + want: system.MustParseDate("2021-03-02"), + }, + { + name: "correctly adds years", + date: system.MustParseDate("2002-04-19"), + input: system.MustParseQuantity("18", "years"), + want: system.MustParseDate("2020-04-19"), + }, + { + name: "adds weeks as days", + date: system.MustParseDate("1974-10-30"), + input: system.MustParseQuantity("18", "weeks"), + want: system.MustParseDate("1975-03-05"), + }, + { + name: "disregards decimal part of quantity", + date: system.MustParseDate("2002-04-19"), + input: system.MustParseQuantity("2.5", "months"), + want: system.MustParseDate("2002-06-19"), + }, + { + name: "returns correct result when input month is longer than result month", + date: system.MustParseDate("2021-01-31"), + input: system.MustParseQuantity("1", "month"), + want: system.MustParseDate("2021-02-28"), + }, + { + name: "adds partials by rounding down to the highest precision", + date: system.MustParseDate("1997"), + input: system.MustParseQuantity("23", "months"), + want: system.MustParseDate("1998"), + }, + { + name: "returns error on unsupported time-valued quantity", + date: system.MustParseDate("2019-03-31"), + input: system.MustParseQuantity("12", "hours"), + wantErr: system.ErrMismatchedUnit, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.date.Add(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Date.Add returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Date.Add returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestDateSub_ReturnsSum(t *testing.T) { + testCases := []struct { + name string + date system.Date + input system.Quantity + want system.Date + wantErr error + }{ + { + name: "overflows months", + date: system.MustParseDate("2025-01-30"), + input: system.MustParseQuantity("2", "months"), + want: system.MustParseDate("2024-11-30"), + }, + { + name: "correctly subtracts days", + date: system.MustParseDate("2021-03-02"), + input: system.MustParseQuantity("2", "days"), + want: system.MustParseDate("2021-02-28"), + }, + { + name: "correctly subtracts years", + date: system.MustParseDate("2020-04-19"), + input: system.MustParseQuantity("18", "years"), + want: system.MustParseDate("2002-04-19"), + }, + { + name: "disregards decimal part of quantity", + date: system.MustParseDate("2002-04-19"), + input: system.MustParseQuantity("2.5", "months"), + want: system.MustParseDate("2002-02-19"), + }, + { + name: "subtracts year partial by rounding down to the highest precision", + date: system.MustParseDate("1997"), + input: system.MustParseQuantity("23", "months"), + want: system.MustParseDate("1996"), + }, + { + name: "subtracts month partial by rounding down to multiples of 30 days", + date: system.MustParseDate("1997-04"), + input: system.MustParseQuantity("29", "days"), + want: system.MustParseDate("1997-04"), + }, + { + name: "returns correct result when input month is longer than result month", + date: system.MustParseDate("2019-03-31"), + input: system.MustParseQuantity("1", "month"), + want: system.MustParseDate("2019-02-28"), + }, + { + name: "returns error on non time-valued quantity", + date: system.MustParseDate("2019-03-31"), + input: system.MustParseQuantity("12", "kg"), + wantErr: system.ErrMismatchedUnit, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.date.Sub(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Date.Sub returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Date.Sub returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestDate_ToDateTime(t *testing.T) { + tests := []struct { + name string + input system.Date + want system.DateTime + }{ + { + name: "returns a DateTime for a Date with day layout", + input: system.MustParseDate("2006-01-02"), + want: system.MustParseDate("2006-01-02").ToDateTime(), + }, + { + name: "returns a DateTime for a Date with month layout", + input: system.MustParseDate("2006-01"), + want: system.MustParseDate("2006-01").ToDateTime(), + }, + { + name: "returns a DateTime for a Date with year layout", + input: system.MustParseDate("2006"), + want: system.MustParseDate("2006").ToDateTime(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.input.ToDateTime() + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("Date_ToDateTime() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} diff --git a/fhirpath/system/date_time.go b/fhirpath/system/date_time.go new file mode 100644 index 0000000..5308fcb --- /dev/null +++ b/fhirpath/system/date_time.go @@ -0,0 +1,289 @@ +package system + +import ( + "fmt" + "strings" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirconv" +) + +// DateTime represents date/time values and partial date/time values +// in the format YYYY-MM-DDThh:mm:ss.fff(+|-)hh:mm format. +type DateTime struct { + dateTime time.Time + l layout +} + +// ParseDateTime parses a string and returns a DateTime object if +// the string is a valid FHIRPath DateTime string, or an error otherwise. +func ParseDateTime(value string) (DateTime, error) { + dateTimeLayouts := []string{ + dtMillisecondLayoutTZ, + dtMillisecondLayout, + dtSecondLayoutTZ, + dtSecondLayout, + dtMinuteLayoutTZ, + dtMinuteLayout, + dtHourLayoutTZ, + dtHourLayout, + dtDayLayout, + dtMonthLayout, + dtYearLayout, + } + + var t time.Time + var err error + value = strings.TrimPrefix(value, "@") + for _, l := range dateTimeLayouts { + if t, err = time.Parse(l, value); err == nil { + return DateTime{t, layout(l)}, nil + } + } + return DateTime{}, fmt.Errorf("unable to parse DateTime '%s': %w", value, err) +} + +// MustParseDateTime returns a DateTime if the string represents a +// valid DateTime. Otherwise, panics. +func MustParseDateTime(value string) DateTime { + dt, err := ParseDateTime(value) + if err != nil { + panic(err) + } + return dt +} + +// DateTimeFromProto takes a proto DateTime as input and returns a System DateTime. +// Note that the highest precision supported by System.DateTime is Millisecond. +func DateTimeFromProto(proto *dtpb.DateTime) (DateTime, error) { + t, err := fhirconv.DateTimeToTime(proto) + if err != nil { + return DateTime{}, err + } + var l layout + switch proto.Precision { + case dtpb.DateTime_MICROSECOND: + fallthrough + case dtpb.DateTime_MILLISECOND: + l = dtMillisecondLayoutTZ + case dtpb.DateTime_SECOND: + l = dtSecondLayoutTZ + case dtpb.DateTime_DAY: + l = dtDayLayout + case dtpb.DateTime_MONTH: + l = dtMonthLayout + case dtpb.DateTime_YEAR: + l = dtYearLayout + } + return DateTime{t, l}, nil +} + +// ToProtoDateTime returns a proto DateTime based on a system DateTime. +// Note that the highest precision supported by System.DateTime is Millisecond. +func (dt DateTime) ToProtoDateTime() *dtpb.DateTime { + dateTime := fhir.DateTime(dt.dateTime) + var p dtpb.DateTime_Precision + switch dt.l { + case dtMillisecondLayoutTZ, dtMillisecondLayout: + p = dtpb.DateTime_MILLISECOND + case dtSecondLayoutTZ, dtSecondLayout: + p = dtpb.DateTime_SECOND + case dtDayLayout: + p = dtpb.DateTime_DAY + case dtMonthLayout: + p = dtpb.DateTime_MONTH + case dtYearLayout: + p = dtpb.DateTime_YEAR + } + dateTime.Precision = p + return dateTime +} + +// TryEqual returns a boolean representing whether or not +// the value of dt is equal to the value of dt2. +// Not intended to be used for cmp.Equal. The comparison is +// not symmetric and may cause unexpected behaviour. +func (dt DateTime) TryEqual(input Any) (bool, bool) { + val, ok := input.(DateTime) + if !ok { + return false, true + } + if dt.l == val.l { + return dt.dateTime.Equal(val.dateTime), true + } + + // normalize time zone + dt.dateTime = dt.dateTime.UTC() + val.dateTime = val.dateTime.UTC() + + dtComponents := dt.getComponents() + valComponents := val.getComponents() + + minPrecision := min(int(dateTimeMap[dt.l]), int(dateTimeMap[val.l])) + + for i := 0; i <= minPrecision; i++ { + if dtComponents[i] == valComponents[i] && i != int(dtSecond) { + continue + } + return dtComponents[i] == valComponents[i], true + } + return false, false +} + +// Less returns true if the value of dt is less than input.(DateTime). +// Compares component by component, and returns an error if there is a +// precision mismatch. If input is not a Date, returns an error. +func (dt DateTime) Less(input Any) (Boolean, error) { + val, ok := input.(DateTime) + if !ok { + return false, fmt.Errorf("%w, %T, %T", ErrTypeMismatch, dt, input) + } + if dt.l == val.l { + return Boolean(dt.dateTime.Before(val.dateTime)), nil + } + + // normalize time zone + dt.dateTime = dt.dateTime.UTC() + val.dateTime = val.dateTime.UTC() + + dtComponents := dt.getComponents() + valComponents := val.getComponents() + + minPrecision := min(int(dateTimeMap[dt.l]), int(dateTimeMap[val.l])) + + for i := 0; i <= minPrecision; i++ { + // precisions below second are irrelevant, and should be treated the same. + if dtComponents[i] == valComponents[i] && i != int(dtSecond) { + continue + } + return dtComponents[i] < valComponents[i], nil + } + return false, ErrMismatchedPrecision +} + +// Add returns the result of dt + input. Returns an +// error if input does not represent a valid time valued quantity. +func (dt DateTime) Add(input Quantity) (DateTime, error) { + var result time.Time + value := int(decimal.Decimal(input.value).IntPart()) + switch input.unit { + case "year", "years": + result = addYear(dt.dateTime, value) + case "month", "months": + result = addMonth(dt.dateTime, value) + case "week", "weeks": + value = 7 * value + result = dt.dateTime.AddDate(0, 0, value) + case "day", "days": + result = dt.dateTime.AddDate(0, 0, value) + default: + duration, err := input.timeDuration() + if err != nil { + return DateTime{}, err + } + result = dt.dateTime.Add(duration) + } + + // Reformat to truncate DateTime to initial precision, rounding down to + // highest precision value. + result, err := time.Parse(string(dt.l), result.Format(string(dt.l))) + if err != nil { + return DateTime{}, err + } + return DateTime{result, dt.l}, nil +} + +// Sub returns the result of dt - input.(Quantity). Returns an +// error if the input is not a Quantity, or if it does not represent +// a valid time duration. +func (dt DateTime) Sub(input Quantity) (DateTime, error) { + // Handle partial dates by rounding quantity to appropriate precision. + // Subtraction is not symmetric with addition, so truncation cannot be + // applied here. + if dt.l == dtYearLayout { + years, err := input.toYears() + if err != nil { + return DateTime{}, err + } + return DateTime{dt.dateTime.AddDate(-years, 0, 0), dt.l}, nil + } + if dt.l == dtMonthLayout { + months, err := input.toMonths() + if err != nil { + return DateTime{}, err + } + return DateTime{dt.dateTime.AddDate(0, -months, 0), dt.l}, nil + } + + // Handles non-partial dates here. + var result time.Time + value := -int(decimal.Decimal(input.value).IntPart()) + switch input.unit { + case "year", "years": + result = addYear(dt.dateTime, value) + case "month", "months": + result = addMonth(dt.dateTime, value) + case "week", "weeks": + value = 7 * value + result = dt.dateTime.AddDate(0, 0, value) + case "day", "days": + result = dt.dateTime.AddDate(0, 0, value) + default: + // Get time valued duration, and round down to appropriate precision. + duration, err := input.timeDuration() + if err != nil { + return DateTime{}, err + } + duration = roundToDateTimePrecision(dateTimeMap[dt.l], duration) + result = dt.dateTime.Add(-duration) + } + return DateTime{result, dt.l}, nil +} + +// Name returns the type name. +func (dt DateTime) Name() string { + return dateTimeType +} + +// String returns a formatted DateTime string. +func (dt DateTime) String() string { + return dt.dateTime.Format(string(dt.l)) +} + +// Equal method to override cmp.Equal. +func (dt DateTime) Equal(dt2 DateTime) bool { + return dt.dateTime.Format(string(dt.l)) == dt2.dateTime.Format(string(dt2.l)) +} + +func (dt DateTime) getComponents() []int { + return []int{ + dt.dateTime.Year(), + int(dt.dateTime.Month()), + dt.dateTime.Day(), + dt.dateTime.Hour(), + dt.dateTime.Minute(), + dt.dateTime.Second()*1000000000 + dt.dateTime.Nanosecond(), + } +} + +// roundToDateTimePrecision rounds the duration down to the appropriate precision. +// Eg. 2012-03-20T + 23 'hours' = 2012-03-20T but 2012-03-20T + 24 'hours' = 2012-03-21T. +func roundToDateTimePrecision(p dateTimePrecision, d time.Duration) time.Duration { + switch p { + case dtYear: + return d / (time.Hour * 24 * 365) + case dtMonth: + return d / (time.Hour * 24 * 30) + case dtDay: + return d / (time.Hour * 24) + case dtHour: + return d / time.Hour + case dtMinute: + return d / time.Minute + default: + return d + } +} diff --git a/fhirpath/system/date_time_test.go b/fhirpath/system/date_time_test.go new file mode 100644 index 0000000..fa81d3e --- /dev/null +++ b/fhirpath/system/date_time_test.go @@ -0,0 +1,474 @@ +package system_test + +import ( + "errors" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestParseDateTime_ReturnsTime(t *testing.T) { + testCases := []struct { + name string + input string + }{ + {"Full DateTime with offset", "2009-03-06T12:09:45.556-04:30"}, + {"DateTime without offset", "2009-03-06T12:09:45.556"}, + {"Second with offset", "2006-01-02T15:04:05Z"}, + {"Minute", "2010-02-04T14:05"}, + {"Year", "2021T"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := system.ParseDateTime(tc.input); err != nil { + t.Fatalf("ParseTime(%s) returned unexpected error: %v", tc.input, err) + } + }) + } +} + +func TestParseDateTime_ReturnsError(t *testing.T) { + testCases := []struct { + name string + input string + }{ + {"Bad format", "2010-12-21T08-05"}, + {"Bad hour", "2010-12-25T25"}, + {"Bad minute", "2010-12-25T23:61"}, + {"Offset without minutes", "2010-12-25T23:59+04"}, + {"Date without T", "2010-12-25"}, + {"Time without date", "08:30Z"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := system.ParseDateTime(tc.input); err == nil { + t.Fatalf("ParseDate(%s) didn't return error when expected", tc.input) + } + }) + } +} + +func TestDateTime_Equal(t *testing.T) { + testCases := []struct { + name string + dateTimeOne system.DateTime + dateTimeTwo system.Any + shouldEqual bool + wantOk bool + }{ + { + name: "same time different format", + dateTimeOne: system.MustParseDateTime("2023-01-01T"), + dateTimeTwo: system.MustParseDateTime("2023-01-01T00:00:00.000"), + wantOk: false, + }, + { + name: "different date", + dateTimeOne: system.MustParseDateTime("2023-01-01T00:00:00.000"), + dateTimeTwo: system.MustParseDateTime("2023-01-02T00:00:00.000Z"), + shouldEqual: false, + wantOk: true, + }, + { + name: "different type", + dateTimeOne: system.MustParseDateTime("2023-01-01T00:00:00.000"), + dateTimeTwo: system.String("2023-01-01T00:00:00.000"), + shouldEqual: false, + wantOk: true, + }, + { + name: "same time different offset format", + dateTimeOne: system.MustParseDateTime("2023-01-02T00:00:00.000Z"), + dateTimeTwo: system.MustParseDateTime("2023-01-02T00:00:00.000-00:00"), + shouldEqual: true, + wantOk: true, + }, + { + name: "same time different time zone", + dateTimeOne: system.MustParseDateTime("2023-01-01T08:30:00+03:00"), + dateTimeTwo: system.MustParseDateTime("2023-01-01T05:30:00Z"), + shouldEqual: true, + wantOk: true, + }, + { + name: "not equal with mismatched precision", + dateTimeOne: system.MustParseDateTime("2023-02-01T08:30"), + dateTimeTwo: system.MustParseDateTime("2023-01-01T08:30:45"), + shouldEqual: false, + wantOk: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, ok := tc.dateTimeOne.TryEqual(tc.dateTimeTwo) + + if ok != tc.wantOk { + t.Fatalf("DateTime.Equal: ok got %v, want %v", ok, tc.wantOk) + } + if got != tc.shouldEqual { + t.Errorf("DateTime.Equal returned unexpected equality: got %v, want %v", got, tc.shouldEqual) + } + }) + } +} + +func TestDateTimeFromProto_Converts(t *testing.T) { + almostY2K, _ := system.ParseDateTime("1999-12-31T23:59:59.999Z") + dec2004, _ := system.ParseDateTime("2004-12T") + pandemic, _ := system.ParseDateTime("2020-03-15T08:30:05Z") + + testCases := []struct { + name string + dtProto *dtpb.DateTime + wantDT system.DateTime + }{ + { + name: "converts microsecond precision", + dtProto: fhir.MustParseDateTime("1999-12-31T23:59:59.999999Z"), + wantDT: almostY2K, + }, + { + name: "converts partial date precision", + dtProto: fhir.MustParseDateTime("2004-12"), + wantDT: dec2004, + }, + { + name: "converts second precision", + dtProto: fhir.MustParseDateTime("2020-03-15T08:30:05Z"), + wantDT: pandemic, + }, + { + name: "converts with alternate offset format", + dtProto: fhir.MustParseDateTime("2020-03-15T08:30:05-00:00"), + wantDT: pandemic, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := system.DateTimeFromProto(tc.dtProto) + + if err != nil { + t.Fatalf("DateTimeFromProto(%s) raised unexpected error: %v", tc.dtProto.String(), err) + } + if diff := cmp.Diff(tc.wantDT, got); diff != "" { + t.Errorf("DateTimeFromProto(%s) incorrectly converts DateTime: (-want, +got)\n%s", tc.dtProto.String(), diff) + } + }) + } +} + +func TestDateTimeToProtoDateTime_Converts(t *testing.T) { + testCases := []struct { + name string + dateTime system.DateTime + want *dtpb.DateTime + }{ + { + name: "converts millisecond precision with timezone", + dateTime: system.MustParseDateTime("1999-12-31T23:59:59.999Z"), + want: fhir.MustParseDateTime("1999-12-31T23:59:59.999Z"), + }, + { + name: "converts to second precision with timezone", + dateTime: system.MustParseDateTime("1999-12-31T23:59:59Z"), + want: fhir.MustParseDateTime("1999-12-31T23:59:59Z"), + }, + { + name: "converts to day precision", + dateTime: system.MustParseDateTime("1999-12-31T"), + want: fhir.MustParseDateTime("1999-12-31"), + }, + { + name: "converts to month precision", + dateTime: system.MustParseDateTime("1999-12T"), + want: fhir.MustParseDateTime("1999-12"), + }, + { + name: "converts to year precision", + dateTime: system.MustParseDateTime("1999T"), + want: fhir.MustParseDateTime("1999"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.dateTime.ToProtoDateTime() + + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("DateTime.ToProtoDateTime returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestDateTimeLess_ReturnsBoolean(t *testing.T) { + testCases := []struct { + name string + dateTimeOne system.DateTime + dateTimeTwo system.DateTime + want system.Boolean + }{ + { + name: "returns true for an earlier date", + dateTimeOne: system.MustParseDateTime("2012-11-15T08:12:12"), + dateTimeTwo: system.MustParseDateTime("2012-11-15T08:30:12"), + want: true, + }, + { + name: "returns false for a later time", + dateTimeOne: system.MustParseDateTime("2013-11-15T00:30:00.000Z"), + dateTimeTwo: system.MustParseDateTime("2012-11-15T00:30:00.000Z"), + want: false, + }, + { + name: "returns true for an earlier date with mismatched precision", + dateTimeOne: system.MustParseDateTime("2022-11-11T18"), + dateTimeTwo: system.MustParseDateTime("2022-11-12T18:30"), + want: true, + }, + { + name: "returns false for a later time with mismatched precision", + dateTimeOne: system.MustParseDateTime("2021-11-12T18:30"), + dateTimeTwo: system.MustParseDateTime("2021-11-04T14:30:25"), + want: false, + }, + { + name: "returns false (doesn't error) for equal times with mismatched millisecond precision", + dateTimeOne: system.MustParseDateTime("2021-11-04T14:30:25.000"), + dateTimeTwo: system.MustParseDateTime("2021-11-04T14:30:25"), + }, + { + name: "returns true for earlier time with mismatched millisecond precision", + dateTimeOne: system.MustParseDateTime("2000-01-01T18:30:01"), + dateTimeTwo: system.MustParseDateTime("2000-01-01T18:30:01.001"), + want: true, + }, + { + name: "respects time zone offset", + dateTimeOne: system.MustParseDateTime("2000-12-19T18:30:01+05:00"), + dateTimeTwo: system.MustParseDateTime("2000-12-19T13:30:02"), + want: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.dateTimeOne.Less(tc.dateTimeTwo) + + if err != nil { + t.Fatalf("DateTime.Less returned unexpected error: %v", err) + } + if got != tc.want { + t.Errorf("DateTime.Less returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestDateTimeLess_ReturnsError(t *testing.T) { + testCases := []struct { + name string + dateTimeOne system.DateTime + input system.Any + wantErr error + }{ + { + name: "incorrect input type", + dateTimeOne: system.MustParseDateTime("2020-09-02T08:30:30"), + input: system.String("2020-09-02T08:30:30"), + wantErr: system.ErrTypeMismatch, + }, + { + name: "equal until precision mismatch", + dateTimeOne: system.MustParseDateTime("2020-09-02T08:30:30"), + input: system.MustParseDateTime("2020-09-02T08:30"), + wantErr: system.ErrMismatchedPrecision, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.dateTimeOne.Less(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("DateTime.Less returned incorrect error: got %v, want %v", err, tc.wantErr) + } + }) + } +} + +func TestDateTimeAdd_ReturnsSum(t *testing.T) { + testCases := []struct { + name string + dateTimeOne system.DateTime + input system.Quantity + want system.DateTime + wantErr error + }{ + { + name: "overflows hours", + dateTimeOne: system.MustParseDateTime("2012-11-15T08:12:12"), + input: system.MustParseQuantity("24", "hours"), + want: system.MustParseDateTime("2012-11-16T08:12:12"), + }, + { + name: "adds years correctly when start date is a leap year", + dateTimeOne: system.MustParseDateTime("2020-02-29T08:12:12"), + input: system.MustParseQuantity("1", "year"), + want: system.MustParseDateTime("2021-02-28T08:12:12"), + }, + { + name: "adds decimal of seconds", + dateTimeOne: system.MustParseDateTime("2013-11-15T00:30:00.000Z"), + input: system.MustParseQuantity("1.232", "seconds"), + want: system.MustParseDateTime("2013-11-15T00:30:01.232Z"), + }, + { + name: "rounds quantity down to highest precision", + dateTimeOne: system.MustParseDateTime("2022-11-11T18"), + input: system.MustParseQuantity("59", "minutes"), + want: system.MustParseDateTime("2022-11-11T18"), + }, + { + name: "respects adding months when the result month is of a different length", + dateTimeOne: system.MustParseDateTime("2021-10-31T18:30"), + input: system.MustParseQuantity("1", "month"), + want: system.MustParseDateTime("2021-11-30T18:30"), + }, + { + name: "disregards decimal part of quantity", + dateTimeOne: system.MustParseDateTime("2021-11-04T14:30:25.000"), + input: system.MustParseQuantity("3.23", "months"), + want: system.MustParseDateTime("2022-02-04T14:30:25.000"), + }, + { + name: "adds days correctly", + dateTimeOne: system.MustParseDateTime("2021-11-04T14:30:25.000"), + input: system.MustParseQuantity("32", "days"), + want: system.MustParseDateTime("2021-12-06T14:30:25.000"), + }, + { + name: "adds weeks correctly", + dateTimeOne: system.MustParseDateTime("2021-11-01T14:30:25.000"), + input: system.MustParseQuantity("18", "weeks"), + want: system.MustParseDateTime("2022-03-07T14:30:25.000"), + }, + { + name: "returns error when adding non time-valued quantity", + dateTimeOne: system.MustParseDateTime("2021-11-01T14:30:25.000"), + input: system.MustParseQuantity("18", "lbs"), + wantErr: system.ErrMismatchedUnit, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.dateTimeOne.Add(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("DateTime.Add returned unexpected error: got: %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("DateTime.Add returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestDateTimeSub_ReturnsResults(t *testing.T) { + testCases := []struct { + name string + dateTimeOne system.DateTime + input system.Quantity + want system.DateTime + wantErr error + }{ + { + name: "overflows hours", + dateTimeOne: system.MustParseDateTime("2012-11-15T08:12:12"), + input: system.MustParseQuantity("24", "hours"), + want: system.MustParseDateTime("2012-11-14T08:12:12"), + }, + { + name: "subtracts years correctly when start date is a leap year", + dateTimeOne: system.MustParseDateTime("2020-02-29T08:12:12"), + input: system.MustParseQuantity("1", "year"), + want: system.MustParseDateTime("2019-02-28T08:12:12"), + }, + { + name: "correctly subtracts from a month partial", + dateTimeOne: system.MustParseDateTime("2020-02T"), + input: system.MustParseQuantity("12", "years"), + want: system.MustParseDateTime("2008-02T"), + }, + { + name: "correctly subtracts weeks", + dateTimeOne: system.MustParseDateTime("2020-02-03T08"), + input: system.MustParseQuantity("12", "weeks"), + want: system.MustParseDateTime("2019-11-11T08"), + }, + { + name: "subtracts decimal of seconds", + dateTimeOne: system.MustParseDateTime("2013-11-15T00:30:01.232Z"), + input: system.MustParseQuantity("1.232", "seconds"), + want: system.MustParseDateTime("2013-11-15T00:30:00.000Z"), + }, + { + name: "subtracts years correctly if in a leap year", + dateTimeOne: system.MustParseDateTime("2020-02-29T"), + input: system.MustParseQuantity("1", "year"), + want: system.MustParseDateTime("2019-02-28T"), + }, + { + name: "rounds quantity down to highest precision for hour partial", + dateTimeOne: system.MustParseDateTime("2022-11-11T18"), + input: system.MustParseQuantity("59", "minutes"), + want: system.MustParseDateTime("2022-11-11T18"), + }, + { + name: "rounds quantity down to to highest precision for year partial", + dateTimeOne: system.MustParseDateTime("2021T"), + input: system.MustParseQuantity("23", "months"), + want: system.MustParseDateTime("2020T"), + }, + { + name: "respects subtracting months when the result month is of a different length", + dateTimeOne: system.MustParseDateTime("2021-10-31T18:30"), + input: system.MustParseQuantity("1", "month"), + want: system.MustParseDateTime("2021-09-30T18:30"), + }, + { + name: "disregards decimal part of quantity", + dateTimeOne: system.MustParseDateTime("2021-11-04T14:30:25.000"), + input: system.MustParseQuantity("3.23", "months"), + want: system.MustParseDateTime("2021-08-04T14:30:25.000"), + }, + { + name: "returns error when subtracting non time-valued quantity", + dateTimeOne: system.MustParseDateTime("2021-11-01T14:30:25.000"), + input: system.MustParseQuantity("18", "lbs"), + wantErr: system.ErrMismatchedUnit, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.dateTimeOne.Sub(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("DateTime.Sub returned unexpected error: got: %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("DateTime.Sub returned unexpected diff: (-want, +got)\n%s", diff) + } + }) + } +} diff --git a/fhirpath/system/doc.go b/fhirpath/system/doc.go new file mode 100644 index 0000000..4efe87a --- /dev/null +++ b/fhirpath/system/doc.go @@ -0,0 +1,8 @@ +/* +Package system provides types and related utility functions +for all the valid FHIRPath System types to be used for literals. + +More documentation about FHIRPath System types can be found here: +- http://hl7.org/fhirpath/N1/#literals +*/ +package system diff --git a/fhirpath/system/layouts.go b/fhirpath/system/layouts.go new file mode 100644 index 0000000..cc5bfad --- /dev/null +++ b/fhirpath/system/layouts.go @@ -0,0 +1,86 @@ +package system + +// datePrecision enumerates date precision constants. +type datePrecision int + +// timePrecision enumerates time precision constants. +type timePrecision int + +// dateTimePrecision enumerates dateTime precision constants. +type dateTimePrecision int + +// layout represents a layout string for parsing. +type layout string + +// Date precision constants. +const ( + year datePrecision = iota + month + day +) + +// Time precision constants. +const ( + hour timePrecision = iota + minute + second +) + +// DateTime precision constants. +const ( + dtYear dateTimePrecision = iota + dtMonth + dtDay + dtHour + dtMinute + dtSecond +) + +// layout constants. +const ( + yearLayout = "2006" + monthLayout = "2006-01" + dayLayout = "2006-01-02" + hourLayout = "15" + minuteLayout = "15:04" + secondLayout = "15:04:05" + millisecondLayout = "15:04:05.000" + dtMillisecondLayoutTZ = "2006-01-02T15:04:05.000Z07:00" + dtMillisecondLayout = "2006-01-02T15:04:05.000" + dtSecondLayoutTZ = "2006-01-02T15:04:05Z07:00" + dtSecondLayout = "2006-01-02T15:04:05" + dtMinuteLayoutTZ = "2006-01-02T15:04Z07:00" + dtMinuteLayout = "2006-01-02T15:04" + dtHourLayoutTZ = "2006-01-02T15Z07:00" + dtHourLayout = "2006-01-02T15" + dtDayLayout = "2006-01-02T" + dtMonthLayout = "2006-01T" + dtYearLayout = "2006T" +) + +var dateMap = map[layout]datePrecision{ + dayLayout: day, + monthLayout: month, + yearLayout: year, +} + +var timeMap = map[layout]timePrecision{ + millisecondLayout: second, + secondLayout: second, + minuteLayout: minute, + hourLayout: hour, +} + +var dateTimeMap = map[layout]dateTimePrecision{ + dtMillisecondLayoutTZ: dtSecond, + dtMillisecondLayout: dtSecond, + dtSecondLayoutTZ: dtSecond, + dtSecondLayout: dtSecond, + dtMinuteLayoutTZ: dtMinute, + dtMinuteLayout: dtMinute, + dtHourLayoutTZ: dtHour, + dtHourLayout: dtHour, + dtDayLayout: dtDay, + dtMonthLayout: dtMonth, + dtYearLayout: dtYear, +} diff --git a/fhirpath/system/primitives.go b/fhirpath/system/primitives.go new file mode 100644 index 0000000..24a8297 --- /dev/null +++ b/fhirpath/system/primitives.go @@ -0,0 +1,303 @@ +package system + +import ( + "fmt" + "math" + "strconv" + "strings" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +// Boolean is a representation for a true or false value. +type Boolean bool + +// ParseBoolean parses a boolean string and returns a Boolean value +// FHIRPath docs here: https://hl7.org/fhirpath/N1/#toboolean-boolean +func ParseBoolean(input string) (Boolean, error) { + switch strings.ToLower(input) { + case "true", "t", "yes", "y", "1", "1.0": + return true, nil + case "false", "f", "no", "n", "0", "0.0": + return false, nil + default: + return false, fmt.Errorf("%v can't be parsed to boolean", input) + } +} + +// Equal returns true if the input value is a System Boolean, +// and contains the same value. +func (b Boolean) Equal(input Any) bool { + val, ok := input.(Boolean) + if !ok { + return false + } + return b == val +} + +// Less returns error for Boolean comparison. +func (b Boolean) Less(input Any) (Boolean, error) { + return false, fmt.Errorf("%w: %T, %T", ErrTypeMismatch, b, input) +} + +// Name returns the type name. +func (b Boolean) Name() string { + return booleanType +} + +// String represents string values. +type String string + +// ParseString parses the input string and replaces FHIRPath +// escape sequences with their Go-equivalent escape characters. +func ParseString(input string) (String, error) { + escSequences := []string{ + "\\'", "'", + "\\\"", "\"", + "\\`", "`", + "\\r", "\r", + "\\t", "\t", + "\\n", "\n", + "\\f", "\f", + "\\\\", "\\", + "\\", "", + // TODO PHP-5581 + } + input = strings.TrimPrefix(input, "'") + input = strings.TrimSuffix(input, "'") + replacer := strings.NewReplacer(escSequences...) + escapedString := replacer.Replace(input) + return String(escapedString), nil +} + +// Equal returns true if the input value is a System String, +// and contains the same string value. +func (s String) Equal(input Any) bool { + val, ok := input.(String) + if !ok { + return false + } + return s == val +} + +// Name returns the type name. +func (s String) Name() string { + return stringType +} + +// Less returns true if s is less than input.(String), by lexicographic +// comparison. If input is not a String, returns an error. +func (s String) Less(input Any) (Boolean, error) { + val, ok := input.(String) + if !ok { + return false, fmt.Errorf("%w: %T, %T,", ErrTypeMismatch, s, input) + } + return string(s) < string(val), nil +} + +// Add concatenates the input String to the right of s. +func (s String) Add(input String) String { + return s + input +} + +// Integer represents integer values in the range 0 to 2^31 - 1. +// Negative integers in FHIRPath are denoted with the +type Integer int32 + +// ParseInteger parses a string into an int32 value, and returns +// an error if the input does not represent a valid int32. +func ParseInteger(value string) (Integer, error) { + i, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return Integer(0), err + } + return Integer(i), nil +} + +// Equal returns true if the input value is a System Integer, and +// contains the same int32 value. +func (i Integer) Equal(input Any) bool { + val, ok := input.(Integer) + if !ok { + return false + } + return i == val +} + +// Name returns the type name. +func (i Integer) Name() string { + return integerType +} + +// Less returns true if i is less than input.(Integer). +// If input is not an Integer, returns an error. +func (i Integer) Less(input Any) (Boolean, error) { + val, ok := input.(Integer) + if !ok { + return false, fmt.Errorf("%w: %T, %T,", ErrTypeMismatch, i, input) + } + return i < val, nil +} + +// Add adds i to the input Integer. Returns an error +// if the result overflows. +func (i Integer) Add(input Integer) (Integer, error) { + result := i + input + // If i is incremented (result > i), input must be positive. + // Similarly, if i is decremented (result <= i), input must be 0 or negative. + // Otherwise, an overflow must have occured. + if (result > i) == (input > 0) { + return result, nil + } + return 0, ErrIntOverflow +} + +// Sub subtracts the input Integer from i. Returns an error +// if the result overflows. +func (i Integer) Sub(input Integer) (Integer, error) { + result := i - input + // If i is decremented (result < i), input must be positive. + // Similarly, if i is incremented (result >= i), input must be 0 or negative. + // Otherwise, an overflow must have occured. + if (result < i) == (input > 0) { + return result, nil + } + return 0, ErrIntOverflow +} + +// Mul multiplies the two integers together. Returns an +// error if the result overflows. +func (i Integer) Mul(input Integer) (Integer, error) { + if i == 0 || input == 0 { + return 0, nil + } + result := i * input + // Check if the sign of the result aligns with what it should be ( -ve == +ve * -ve ) + if (result < 0) == ((i < 0) != (input < 0)) && (result/input) == i { + return result, nil + } + return 0, ErrIntOverflow +} + +// Div divides i by input. Returns a Decimal. +func (i Integer) Div(input Integer) Decimal { + lhs, rhs := decimal.NewFromInt32(int32(i)), decimal.NewFromInt32(int32(input)) + return Decimal(lhs.Div(rhs)) +} + +// FloorDiv divides i by input and rounds down. +func (i Integer) FloorDiv(input Integer) Integer { + return i / input +} + +// Mod returns i % integer. +func (i Integer) Mod(input Integer) Integer { + return i % input +} + +// ToProtoInteger returns the proto representation of the system integer. +func (i Integer) ToProtoInteger() *dtpb.Integer { + return fhir.Integer(int32(i)) +} + +// Decimal represents fixed-point decimals. Must use +// utilities provided by "github.com/shopspring/decimal" to +// perform arithmetic. +type Decimal decimal.Decimal + +// ParseDecimal parses a string representing a decimal, and +// returns an error if the input is invalid. +func ParseDecimal(value string) (Decimal, error) { + d, err := decimal.NewFromString(value) + if err != nil { + return Decimal(decimal.Zero), err + } + return Decimal(d), nil +} + +// MustParseDecimal converts a string into a Decimal type. +// If the string is not parseable it will throw a panic(). +func MustParseDecimal(str string) Decimal { + dec, err := ParseDecimal(str) + if err != nil { + panic(err) + } + return dec +} + +// Equal returns a bool representing whether or not +// the two Decimals being compared are equal. Uses the API +// provided by the decimal library. +func (d Decimal) Equal(input Any) bool { + val, ok := input.(Decimal) + if !ok { + return false + } + return decimal.Decimal(d).Equal(decimal.Decimal(val)) +} + +// Name returns the type name. +func (d Decimal) Name() string { + return decimalType +} + +// Less returns true if d is less than input.(Decimal). If input +// is not a Decimal, returns an error. +func (d Decimal) Less(input Any) (Boolean, error) { + val, ok := input.(Decimal) + if !ok { + return false, fmt.Errorf("%w, %T, %T", ErrTypeMismatch, d, input) + } + return Boolean(decimal.Decimal(d).LessThan(decimal.Decimal(val))), nil +} + +// Add adds d to the input Decimal +func (d Decimal) Add(input Decimal) Decimal { + return Decimal(decimal.Decimal(d).Add(decimal.Decimal(input))) +} + +// Sub subtracts the input Decimal from d. +func (d Decimal) Sub(input Decimal) Decimal { + return Decimal(decimal.Decimal(d).Sub(decimal.Decimal(input))) +} + +// String returns the string value from d. +func (d Decimal) String() string { + return decimal.Decimal(d).String() +} + +// Mul multiples the two decimal values together. +func (d Decimal) Mul(input Decimal) Decimal { + return Decimal(decimal.Decimal(d).Mul(decimal.Decimal(input))) +} + +// Div divides d by input. +func (d Decimal) Div(input Decimal) Decimal { + return Decimal(decimal.Decimal(d).Div(decimal.Decimal(input))) +} + +// FloorDiv divides d by input and rounds down. +func (d Decimal) FloorDiv(input Decimal) (Integer, error) { + result := decimal.Decimal(d).Div(decimal.Decimal(input)).IntPart() + if (result < math.MinInt32) || (result > math.MaxInt32) { + return 0, ErrIntOverflow + } + return Integer(int32(result)), nil +} + +// Mod computes d % input. +func (d Decimal) Mod(input Decimal) Decimal { + return Decimal(decimal.Decimal(d).Mod(decimal.Decimal(input))) +} + +// ToProtoDecimal returns the proto Decimal representation of decimal. +func (d Decimal) ToProtoDecimal() *dtpb.Decimal { + return fhir.Decimal(decimal.Decimal(d).InexactFloat64()) +} + +// Round rounds a Decimal at the provided precision. +func (d Decimal) Round(precision int32) Decimal { + return Decimal(decimal.Decimal(d).Round(precision)) +} diff --git a/fhirpath/system/primitives_test.go b/fhirpath/system/primitives_test.go new file mode 100644 index 0000000..f00ae45 --- /dev/null +++ b/fhirpath/system/primitives_test.go @@ -0,0 +1,402 @@ +package system_test + +import ( + "errors" + "math" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestParseString_ReplacesEscapeSequences(t *testing.T) { + testCases := []struct { + name string + input string + want string + }{ + { + name: "replaces single quote", + input: `I\'m testing`, + want: `I'm testing`, + }, + { + name: "replaces double quote", + input: `"This is a quote: \""`, + want: `"This is a quote: ""`, + }, + { + name: "replaces backtick", + input: "let foo = \\`${bar}\\`", + want: "let foo = `${bar}`", + }, + { + name: "replaces carriage return", + input: `here's a return \r`, + want: "here's a return \r", + }, + { + name: "replaces newline", + input: `here's a newline \n`, + want: "here's a newline \n", + }, + { + name: "replaces a tab", + input: `\t indent`, + want: "\t indent", + }, + { + name: "replaces a form feed", + input: `\f`, + want: "\f", + }, + { + name: "replaces backslashes w/ multiple escapes", + input: `escape \n\ \p \\p`, + want: "escape \n p \\p", + }, + } + + for _, tc := range testCases { + str, err := system.ParseString(tc.input) + + if err != nil { + t.Fatalf("ParseString(%s) raised error when not expected: %v", tc.input, err) + } + if got, want := string(str), tc.want; got != want { + t.Errorf("ParseString(%s) returned mismatch: got %#v, want %#v", tc.input, got, want) + } + } +} + +func TestParseBoolean_ReturnsBoolean(t *testing.T) { + testCases := []struct { + name string + input string + want system.Boolean + wantError bool + }{ + { + name: "parse 'true'", + input: "true", + want: system.Boolean(true), + }, + { + name: "parse 'TRUE'", + input: "TRUE", + want: system.Boolean(true), + }, + { + name: "parse 't'", + input: "t", + want: system.Boolean(true), + }, + { + name: "parse 'T'", + input: "T", + want: system.Boolean(true), + }, + { + name: "parse 'yes'", + input: "yes", + want: system.Boolean(true), + }, + { + name: "parse 'YES'", + input: "YES", + want: system.Boolean(true), + }, + { + name: "parse 'y'", + input: "y", + want: system.Boolean(true), + }, + { + name: "parse 'Y'", + input: "Y", + want: system.Boolean(true), + }, + { + name: "parse '1'", + input: "1", + want: system.Boolean(true), + }, + { + name: "parse '1.0'", + input: "1.0", + want: system.Boolean(true), + }, + { + name: "parse 'false'", + input: "false", + want: system.Boolean(false), + }, + { + name: "parse 'FALSE'", + input: "FALSE", + want: system.Boolean(false), + }, + { + name: "parse 'f'", + input: "f", + want: system.Boolean(false), + }, + { + name: "parse 'F'", + input: "F", + want: system.Boolean(false), + }, + { + name: "parse 'no'", + input: "no", + want: system.Boolean(false), + }, + { + name: "parse 'NO'", + input: "NO", + want: system.Boolean(false), + }, + { + name: "parse 'n'", + input: "n", + want: system.Boolean(false), + }, + { + name: "parse 'N'", + input: "N", + want: system.Boolean(false), + }, + { + name: "parse '0'", + input: "0", + want: system.Boolean(false), + }, + { + name: "parse '0.0'", + input: "0.0", + want: system.Boolean(false), + }, + { + name: "parse '2'", + input: "2", + want: system.Boolean(false), + wantError: true, + }, + { + name: "parse '2.0'", + input: "2", + want: system.Boolean(false), + wantError: true, + }, + { + name: "parse '2.5'", + input: "2.5", + want: system.Boolean(false), + wantError: true, + }, + { + name: "parse '2.5 kg'", + input: "2.5 kg", + want: system.Boolean(false), + wantError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := system.ParseBoolean(tc.input) + if (err != nil) != tc.wantError { + t.Errorf("ParseBoolean(%s) error = %v, wantErr %v", tc.input, err, tc.wantError) + return + } + if got != tc.want { + t.Errorf("ParseBoolean(%s) parsed incorrectly, got: %v, want %v", tc.input, got, tc.want) + return + } + }) + } +} + +func TestParseInteger_ReturnsInteger(t *testing.T) { + testCases := []struct { + name string + input string + want system.Integer + }{ + { + name: "positive edge", + input: "2147483647", + want: system.Integer(2147483647), + }, + { + name: "negative edge", + input: "-2147483648", + want: system.Integer(-2147483648), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + i, err := system.ParseInteger(tc.input) + + if err != nil { + t.Fatalf("ParseInteger(%s) returns unexpected error: %v", tc.input, err) + } + if got, want := i, tc.want; got != want { + t.Errorf("ParseInteger(%s) parsed incorrectly: got %v, want %v", tc.input, got, want) + } + }) + } +} + +func TestParseInteger_ReturnsError_IfOutOfRange(t *testing.T) { + testCases := []struct { + name string + input string + }{ + { + name: "positive edge", + input: "2147483648", + }, + { + name: "negative edge", + input: "-2147483649", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := system.ParseInteger(tc.input) + + if err == nil { + t.Fatalf("ParseInteger(%s) doesn't return error when expected to", tc.input) + } + }) + } +} + +func TestIntegerAdd(t *testing.T) { + testCases := []struct { + name string + left system.Integer + right system.Integer + want system.Integer + wantErr error + }{ + { + name: "adds two integers", + left: 2000, + right: 4001, + want: 6001, + }, + { + name: "returns error when addition overflows positively", + left: math.MaxInt32, + right: 12120, + wantErr: system.ErrIntOverflow, + }, + { + name: "returns error when addition overflows negatively", + left: math.MinInt32, + right: -1, + wantErr: system.ErrIntOverflow, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.left.Add(tc.right) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Integer.Add returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Integer.Add returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestIntegerSub(t *testing.T) { + testCases := []struct { + name string + left system.Integer + right system.Integer + want system.Integer + wantErr error + }{ + { + name: "subtracts two integers", + left: 2000, + right: 4001, + want: -2001, + }, + { + name: "returns error when subtraction overflows negatively", + left: math.MinInt32, + right: 1, + wantErr: system.ErrIntOverflow, + }, + { + name: "returns error when subtraction overflows positively", + left: math.MaxInt32, + right: -1, + wantErr: system.ErrIntOverflow, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.left.Sub(tc.right) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Integer.Sub returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Integer.Sub returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestIntegerMul_ReturnsResult(t *testing.T) { + testCases := []struct { + name string + left system.Integer + right system.Integer + want system.Integer + wantErr error + }{ + { + name: "multiplies two integers", + left: 14, + right: 2, + want: 28, + }, + { + name: "returns error if multiplication causes overflow", + left: 1312312312, + right: 10, + wantErr: system.ErrIntOverflow, + }, + { + name: "returns overflow error if MinInt32 is multiplied", + left: math.MinInt32, + right: -1, + wantErr: system.ErrIntOverflow, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.left.Mul(tc.right) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Integer.Mul returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Integer.Mul returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} diff --git a/fhirpath/system/quantity.go b/fhirpath/system/quantity.go new file mode 100644 index 0000000..802dcff --- /dev/null +++ b/fhirpath/system/quantity.go @@ -0,0 +1,211 @@ +package system + +import ( + "fmt" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +// Quantity type represents a decimal value along with a UCUM unit or +// a calendar duration keyword. +type Quantity struct { + value Decimal + unit string +} + +// ParseQuantity takes as input a number string and a unit string and constructs +// a Quantity object. Returns error if the input does not fit into a valid Quantity +func ParseQuantity(number string, unit string) (Quantity, error) { + d, err := ParseDecimal(number) + if err != nil { + return Quantity{}, err + } + q, err := newQuantity(d, unit) + if err != nil { + return Quantity{}, err + } + return q, nil +} + +// MustParseQuantity constructs a Quantity object from a number string and unit string. +// Panics if the inputs do not fit into a valid Quantity. +func MustParseQuantity(number string, unit string) Quantity { + q, err := ParseQuantity(number, unit) + if err != nil { + panic(err) + } + return q +} + +// newQuantity constructs a system Quantity type, given a decimal +// value and a UCUM unit identifier. +func newQuantity(value Decimal, unit string) (Quantity, error) { + // check isValidUnit(unit) *To be implemented + return Quantity{value, unit}, nil +} + +// TryEqual returns a bool representing whether or not the +// value represented by q is equal to the value of q2. +// The comparison is not symmetric and may not return a value, represented by +// the second boolean being set to false. +func (q Quantity) TryEqual(input Any) (bool, bool) { + val, ok := input.(Quantity) + if !ok { + return false, true + } + if q.unit != val.unit { + return false, false + } + return q.value.Equal(val.value), true +} + +// Less returns true if q is less than input.(Quantity). If the units +// are mismatched, returns an error. If input is not a Quantity, returns +// an error. +func (q Quantity) Less(input Any) (Boolean, error) { + val, ok := input.(Quantity) + if !ok { + return false, fmt.Errorf("%w: %T, %T", ErrTypeMismatch, q, input) + } + if q.unit != val.unit { + return false, ErrMismatchedUnit + } + return q.value.Less(val.value) +} + +// Add returns q + input. Returns an error if the units are mismatched. +func (q Quantity) Add(input Quantity) (Quantity, error) { + if q.unit != input.unit { + return Quantity{}, ErrMismatchedUnit + } + value := Decimal(decimal.Decimal(q.value).Add(decimal.Decimal(input.value))) + return Quantity{value, q.unit}, nil +} + +// Sub returns q - input. Returns an error if the units are mismatched. +func (q Quantity) Sub(input Quantity) (Quantity, error) { + if q.unit != input.unit { + return Quantity{}, ErrMismatchedUnit + } + value := Decimal(decimal.Decimal(q.value).Sub(decimal.Decimal(input.value))) + return Quantity{value, q.unit}, nil +} + +// Name returns the type name. +func (q Quantity) Name() string { + return quantityType +} + +// String returns the quantity value formatted as a string. +func (q Quantity) String() string { + if q.unit != "" { + return fmt.Sprintf("%s %s", decimal.Decimal(q.value).String(), q.unit) + } + return fmt.Sprintf("%s", decimal.Decimal(q.value).String()) +} + +// ToProtoQuantity returns a proto Quantity based on a system Quantity. +func (q Quantity) ToProtoQuantity() *dtpb.Quantity { + res := &dtpb.Quantity{ + Value: q.value.ToProtoDecimal(), + } + + if q.unit != "" { + res.Unit = fhir.String(q.unit) + } + + return res +} + +// Equal method to override cmp.Equal. +func (q Quantity) Equal(q2 Quantity) bool { + return q.value.Equal(q2.value) && q.unit == q2.unit +} + +// Negate returns the quantity multiplied by -1. +func (q Quantity) Negate() Quantity { + negative := Decimal(decimal.NewFromInt(-1)) + return Quantity{q.value.Mul(negative), q.unit} +} + +// timeDuration returns the time.Duration represented by +// a time-valued Quantity. Returns an error if the Quantity +// doesn't represent a valid time duration. +func (q Quantity) timeDuration() (time.Duration, error) { + value := decimal.Decimal(q.value).IntPart() + + var duration time.Duration + switch q.unit { + case "hour", "hours": + duration = time.Hour * time.Duration(value) + case "minute", "minutes": + duration = time.Minute * time.Duration(value) + case "second", "seconds": + milliseconds := decimal.Decimal(q.value).Round(3).Shift(3).IntPart() // Keep decimal precision below seconds + duration = time.Millisecond * time.Duration(milliseconds) + case "millisecond": + duration = time.Millisecond * time.Duration(value) + default: + return time.Duration(0), fmt.Errorf("%w: not a time-valued unit", ErrMismatchedUnit) + } + return duration, nil +} + +// Converts valid time based quantities to a number of years, +// by rounding down. +func (q Quantity) toYears() (int, error) { + value := int(decimal.Decimal(q.value).IntPart()) + + switch q.unit { + case "year", "years": + return value, nil + case "month", "months": + return value / 12, nil + case "week", "weeks": + value = value * 7 + fallthrough + case "day", "days": + return value / 365, nil + case "hour", "hours": + return value / (365 * 24), nil + case "minute", "minutes": + return value / (365 * 24 * 60), nil + case "second", "seconds": + return value / (365 * 24 * 60 * 60), nil + case "millisecond", "milliseconds": + return value / (365 * 24 * 60 * 60) / 1000, nil + default: + return 0, fmt.Errorf("%w: not a time-valued unit", ErrMismatchedUnit) + } +} + +// Converts a valid time based quantity to a number of months, +// by rounding down. +func (q Quantity) toMonths() (int, error) { + value := int(decimal.Decimal(q.value).IntPart()) + + switch q.unit { + case "year", "years": + return value * 12, nil + case "month", "months": + return value, nil + case "week", "weeks": + value = value * 7 + fallthrough + case "day", "days": + return value / 30, nil + case "hour", "hours": + return value / (30 * 24), nil + case "minute", "minutes": + return value / (30 * 24 * 60), nil + case "second", "seconds": + return value / (30 * 24 * 60 * 60), nil + case "millisecond", "milliseconds": + return value / (30 * 24 * 60 * 60) / 1000, nil + default: + return 0, fmt.Errorf("%w: not a time-valued unit", ErrMismatchedUnit) + } +} diff --git a/fhirpath/system/quantity_test.go b/fhirpath/system/quantity_test.go new file mode 100644 index 0000000..c288fe2 --- /dev/null +++ b/fhirpath/system/quantity_test.go @@ -0,0 +1,117 @@ +package system_test + +import ( + "testing" + + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestParseQuantity_ErrorsOnInvalidString(t *testing.T) { + testCases := []struct { + name string + number string + unit string + }{ + { + name: "non-number", + number: "word", + unit: "kg", + }, + { + name: "empty strings", + number: "", + unit: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := system.ParseQuantity(tc.number, tc.unit); err == nil { + t.Fatalf("ParseQuantity(%s, %s) didn't raise error when expected", tc.number, tc.unit) + } + }) + } +} + +func TestParseQuantity_ReturnsQuantity(t *testing.T) { + testCases := []struct { + name string + number string + unit string + }{ + { + name: "unit quantity", + number: "100", + unit: "lbs", + }, + { + name: "time quantity", + number: "3", + unit: "minutes", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := system.ParseQuantity(tc.number, tc.unit); err != nil { + t.Fatalf("ParseQuantity(%s, %s) returned unexpected error: %v", tc.number, tc.unit, err) + } + }) + } +} + +func TestQuantity_Equal(t *testing.T) { + onePound, _ := system.ParseQuantity("1", "lbs") + oneLb, _ := system.ParseQuantity("1", "lbs") + oneKg, _ := system.ParseQuantity("1", "kg") + twoLbs, _ := system.ParseQuantity("2", "lbs") + + testCases := []struct { + name string + quantityOne system.Quantity + quantityTwo system.Any + shouldEqual bool + wantOk bool + }{ + { + name: "same quantity different objects", + quantityOne: onePound, + quantityTwo: oneLb, + shouldEqual: true, + wantOk: true, + }, + { + name: "same number different unit", + quantityOne: oneLb, + quantityTwo: oneKg, + wantOk: false, + }, + { + name: "same unit different number", + quantityOne: oneLb, + quantityTwo: twoLbs, + shouldEqual: false, + wantOk: true, + }, + { + name: "different type", + quantityOne: oneLb, + quantityTwo: system.String("1 lbs"), + shouldEqual: false, + wantOk: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, ok := tc.quantityOne.TryEqual(tc.quantityTwo) + + if ok != tc.wantOk { + t.Fatalf("Quantity.Equal: ok got %v, want %v", ok, tc.wantOk) + } + if got != tc.shouldEqual { + t.Errorf("Quantity.Equal returned unexpected equality: got %v, want %v", got, tc.shouldEqual) + } + }) + } +} diff --git a/fhirpath/system/time.go b/fhirpath/system/time.go new file mode 100644 index 0000000..14beff2 --- /dev/null +++ b/fhirpath/system/time.go @@ -0,0 +1,192 @@ +package system + +import ( + "fmt" + "strings" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirconv" +) + +// Time represents a time of day in the range 00:00:00.000 +// to 23:59:59.999 with a step size of 1ms. It uses the format +// hh:mm:ss.fff format to parse times. +type Time struct { + time time.Time + l layout +} + +// ParseTime takes an input string and returns a Time object if +// formatted correctly according to the FHIRPath spec, otherwise +// an error. +func ParseTime(value string) (Time, error) { + timeLayouts := []string{ + millisecondLayout, + secondLayout, + minuteLayout, + hourLayout, + } + + var t time.Time + var err error + value = strings.TrimPrefix(value, "@T") + for _, l := range timeLayouts { + if t, err = time.Parse(l, value); err == nil { + return Time{t, layout(l)}, nil + } + } + return Time{}, fmt.Errorf("unable to parse time '%s': %w", value, err) +} + +// MustParseTime takes an input string and returns a Time object +// if formatted correctly. Otherwise, panics. +func MustParseTime(value string) Time { + t, err := ParseTime(value) + if err != nil { + panic(err) + } + return t +} + +// TimeFromProto takes a proto Time and returns a System Time. +func TimeFromProto(proto *dtpb.Time) Time { + duration := fhirconv.TimeToDuration(proto) + t := time.UnixMicro(duration.Microseconds()).In(time.UTC) + var l layout + switch proto.Precision { + case dtpb.Time_MICROSECOND: + fallthrough + case dtpb.Time_MILLISECOND: + l = millisecondLayout + case dtpb.Time_SECOND: + l = secondLayout + } + return Time{t, l} +} + +// ToProtoTime returns a proto Time based on a system Time. +func (t Time) ToProtoTime() *dtpb.Time { + tp := fhir.Time(t.time) + switch t.l { + case millisecondLayout: + tp.Precision = dtpb.Time_MILLISECOND + default: + tp.Precision = dtpb.Time_SECOND + } + return tp +} + +// TryEqual returns a boolean representing whether or not +// the value of t is equal to the value of t2. +// Not intended to be used for cmp.Equal. The comparison is +// not symmetric and may cause unexpected behaviour. +func (t Time) TryEqual(input Any) (bool, bool) { + val, ok := input.(Time) + if !ok { + return false, true + } + if t.l == val.l { + return t.time.Equal(val.time), true + } + + tComponents := t.getComponents() + valComponents := val.getComponents() + + minPrecision := min(int(timeMap[t.l]), int(timeMap[val.l])) + + for i := 0; i <= minPrecision; i++ { + if tComponents[i] == valComponents[i] && i != int(second) { + continue + } + return tComponents[i] == valComponents[i], true + } + return false, false +} + +// Name returns the type name. +func (t Time) Name() string { + return timeType +} + +// Equal method to override cmp.Equal. +func (t Time) Equal(t2 Time) bool { + return t.time.Format(string(t.l)) == t2.time.Format(string(t2.l)) +} + +// String formats the time as a time string. +func (t Time) String() string { + return t.time.Format(string(t.l)) +} + +// Less returns true if the value of t is less than input.(Time). +// Compares component by component, and returns an error if there is a +// precision mismatch. If input is not a Time, returns an error. +func (t Time) Less(input Any) (Boolean, error) { + val, ok := input.(Time) + if !ok { + return false, fmt.Errorf("%w: %T, %T", ErrTypeMismatch, t, input) + } + if t.l == val.l { + return Boolean(t.time.Before(val.time)), nil + } + + tComponents := t.getComponents() + valComponents := val.getComponents() + + minPrecision := min(int(timeMap[t.l]), int(timeMap[val.l])) + + for i := 0; i <= minPrecision; i++ { + // precisions below second are irrelevant, and should be treated the same. + if tComponents[i] == valComponents[i] && i != int(second) { + continue + } + return tComponents[i] < valComponents[i], nil + } + return false, ErrMismatchedPrecision +} + +// Add returns the result of t with the time-valued quantity added to it. +// Returns an error if the Quantity does not represent a valid duration. +func (t Time) Add(input Quantity) (Time, error) { + duration, err := input.timeDuration() + if err != nil { + return Time{}, err + } + duration = roundToTimePrecision(timeMap[t.l], duration) + return Time{t.time.Add(duration), t.l}, nil +} + +// Sub returns the result of the time-valued quantity subtracted from t. +// Returns an error if the Quantity does not represent a valid duration. +func (t Time) Sub(input Quantity) (Time, error) { + duration, err := input.timeDuration() + if err != nil { + return Time{}, err + } + duration = roundToTimePrecision(timeMap[t.l], duration) + return Time{t.time.Add(-duration), t.l}, nil +} + +// roundToTimePrecision is used to round down to the highest precision of +// the time value. +// Eg. 08:30 + 59 'seconds' = 08:30, but 08:30 + 60 'seconds' = 08:31 +func roundToTimePrecision(p timePrecision, d time.Duration) time.Duration { + switch p { + case hour: + return d / time.Hour + case minute: + return d / time.Minute + default: + return d + } +} + +func (t Time) getComponents() []int { + return []int{ + t.time.Hour(), + t.time.Minute(), + t.time.Second()*1000000000 + t.time.Nanosecond(), + } +} diff --git a/fhirpath/system/time_test.go b/fhirpath/system/time_test.go new file mode 100644 index 0000000..57395b2 --- /dev/null +++ b/fhirpath/system/time_test.go @@ -0,0 +1,383 @@ +package system_test + +import ( + "errors" + "os" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +func TestParseTime_ReturnsTime(t *testing.T) { + testCases := []struct { + name string + input string + }{ + {"Hour", "14"}, + {"Minute", "14:23"}, + {"Second", "14:23:21"}, + {"Millisecond", "14:23:21.999"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := system.ParseTime(tc.input); err != nil { + t.Fatalf("ParseTime(%s) returned unexpected error: %v", tc.input, err) + } + }) + } +} + +func TestParseTime_ReturnsError(t *testing.T) { + testCases := []struct { + name string + input string + }{ + {"Bad format", "14-23-21.556"}, + {"Bad hour", "25:23:21.999"}, + {"Bad minute", "15:65:21.999"}, + {"Bad second", "15:23:65.999"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := system.ParseTime(tc.input); err == nil { + t.Fatalf("ParseDate(%s) didn't raise error when expected", tc.input) + } + }) + } +} + +func TestTime_Equal(t *testing.T) { + testCases := []struct { + name string + timeOne system.Time + timeTwo system.Any + shouldEqual bool + wantOk bool + }{ + { + name: "same time different format", + timeOne: system.MustParseTime("08"), + timeTwo: system.MustParseTime("08:00:00"), + wantOk: false, + }, + { + name: "different time with mismatched precision", + timeOne: system.MustParseTime("08:24:30"), + timeTwo: system.MustParseTime("08:30"), + shouldEqual: false, + wantOk: true, + }, + { + name: "treats millisecond and second as same precision", + timeOne: system.MustParseTime("08:30:10.000"), + timeTwo: system.MustParseTime("08:30:10"), + shouldEqual: true, + wantOk: true, + }, + { + name: "different time", + timeOne: system.MustParseTime("08:00:00"), + timeTwo: system.MustParseTime("08:30:00"), + shouldEqual: false, + wantOk: true, + }, + { + name: "different type", + timeOne: system.MustParseTime("08:00:00"), + timeTwo: system.String("08:00:00"), + wantOk: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, ok := tc.timeOne.TryEqual(tc.timeTwo) + + if ok != tc.wantOk { + t.Fatalf("Time.Equal: ok got %v, want %v", ok, tc.wantOk) + } + if got != tc.shouldEqual { + t.Errorf("Time.Equal returned unexpected equality: got %v, want %v", got, tc.shouldEqual) + } + }) + } +} + +func TestTimeFromProto_Converts(t *testing.T) { + testCases := []struct { + name string + timeProto *dtpb.Time + wantTime system.Time + }{ + { + name: "converts microsecond precision", + timeProto: fhir.MustParseTime("08:30:00.212123"), + wantTime: system.MustParseTime("08:30:00.212"), + }, + { + name: "converts second precision", + timeProto: fhir.MustParseTime("16:45:00"), + wantTime: system.MustParseTime("16:45:00"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + changeLocale(t) + got := system.TimeFromProto(tc.timeProto) + + if diff := cmp.Diff(tc.wantTime, got); diff != "" { + t.Errorf("TimeFromProto(%s) incorrectly converts Time: (-want, +got)\n%s", tc.timeProto.String(), diff) + } + }) + } +} + +func TestTimeLess_ReturnsBoolean(t *testing.T) { + testCases := []struct { + name string + timeOne system.Time + timeTwo system.Time + want system.Boolean + }{ + { + name: "returns true for an earlier time", + timeOne: system.MustParseTime("08:30:05"), + timeTwo: system.MustParseTime("12:03:05"), + want: true, + }, + { + name: "returns false for a later time", + timeOne: system.MustParseTime("18:30:05"), + timeTwo: system.MustParseTime("12:03:05"), + want: false, + }, + { + name: "returns true for an earlier time with mismatched precision", + timeOne: system.MustParseTime("18:30"), + timeTwo: system.MustParseTime("18:45:05"), + want: true, + }, + { + name: "returns false for a later time with mismatched precision", + timeOne: system.MustParseTime("18:30:34"), + timeTwo: system.MustParseTime("12:20"), + want: false, + }, + { + name: "returns false (doesn't error) for equal times with mismatched millisecond precision", + timeOne: system.MustParseTime("04:30:25"), + timeTwo: system.MustParseTime("04:30:25.000"), + want: false, + }, + { + name: "returns true for earlier time with mismatched millisecond precision", + timeOne: system.MustParseTime("18:30:01"), + timeTwo: system.MustParseTime("18:30:01.001"), + want: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.timeOne.Less(tc.timeTwo) + + if err != nil { + t.Fatalf("Time.Less returned unexpected error: %v", err) + } + if got != tc.want { + t.Errorf("Time.Less returned unexpected result: got %v, want %v", got, tc.want) + } + }) + } +} + +func TestTimeLess_ReturnsError(t *testing.T) { + testCases := []struct { + name string + timeOne system.Time + input system.Any + wantErr error + }{ + { + name: "incorrect input type", + timeOne: system.MustParseTime("08:30:30"), + input: system.String("08:30:30"), + wantErr: system.ErrTypeMismatch, + }, + { + name: "equal until precision mismatch", + timeOne: system.MustParseTime("11:11:11"), + input: system.MustParseTime("11:11"), + wantErr: system.ErrMismatchedPrecision, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.timeOne.Less(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Time.Less returned incorrect error: got %v, want %v", err, tc.wantErr) + } + }) + } +} + +func TestTimeAdd_ReturnsSum(t *testing.T) { + testCases := []struct { + name string + time system.Time + input system.Quantity + want system.Time + wantErr error + }{ + { + name: "overflows time hours", + time: system.MustParseTime("23:30:00"), + input: system.MustParseQuantity("2", "hours"), + want: system.MustParseTime("01:30:00"), + }, + { + name: "correctly adds minutes", + time: system.MustParseTime("19:45:44"), + input: system.MustParseQuantity("25", "minutes"), + want: system.MustParseTime("20:10:44"), + }, + { + name: "correctly adds seconds", + time: system.MustParseTime("08:00:00"), + input: system.MustParseQuantity("23", "seconds"), + want: system.MustParseTime("08:00:23"), + }, + { + name: "disregards decimal part of minutes", + time: system.MustParseTime("08:00:00"), + input: system.MustParseQuantity("15.5", "minutes"), + want: system.MustParseTime("08:15:00"), + }, + { + name: "adds decimal part of seconds", + time: system.MustParseTime("08:00:00.000"), + input: system.MustParseQuantity("12.24", "seconds"), + want: system.MustParseTime("08:00:12.240"), + }, + { + name: "adds partials by rounding down to the highest precision", + time: system.MustParseTime("08"), + input: system.MustParseQuantity("59", "minutes"), + want: system.MustParseTime("08"), + }, + { + name: "returns error on non time-valued quantity", + time: system.MustParseTime("08:30:00"), + input: system.MustParseQuantity("12", "kg"), + wantErr: system.ErrMismatchedUnit, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.time.Add(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Time.Add returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Time.Add returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} + +func TestTimeSub_ReturnsResult(t *testing.T) { + testCases := []struct { + name string + time system.Time + input system.Quantity + want system.Time + wantErr error + }{ + { + name: "overflows time hours", + time: system.MustParseTime("00:30:00"), + input: system.MustParseQuantity("2", "hours"), + want: system.MustParseTime("22:30:00"), + }, + { + name: "correctly subtracts minutes", + time: system.MustParseTime("19:45:44"), + input: system.MustParseQuantity("25", "minutes"), + want: system.MustParseTime("19:20:44"), + }, + { + name: "correctly subtracts seconds", + time: system.MustParseTime("08:00:00"), + input: system.MustParseQuantity("23", "seconds"), + want: system.MustParseTime("07:59:37"), + }, + { + name: "disregards decimal part of minutes", + time: system.MustParseTime("07:45:00"), + input: system.MustParseQuantity("15.5", "minutes"), + want: system.MustParseTime("07:30:00"), + }, + { + name: "subtracts decimal part of seconds", + time: system.MustParseTime("08:00:12.240"), + input: system.MustParseQuantity("12.24", "seconds"), + want: system.MustParseTime("08:00:00.000"), + }, + { + name: "subtracts partials by rounding down to highest precision", + time: system.MustParseTime("08:00"), + input: system.MustParseQuantity("119", "seconds"), + want: system.MustParseTime("07:59"), + }, + { + name: "returns error on non time-valued quantity", + time: system.MustParseTime("08:30:00"), + input: system.MustParseQuantity("12", "kg"), + wantErr: system.ErrMismatchedUnit, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.time.Sub(tc.input) + + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Time.Add returned unexpected error: got %v, want %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Time.Add returned unexpected result: (-want, +got)\n%s", diff) + } + }) + } +} + +func changeLocale(t *testing.T) { + t.Helper() + + // find a new locale + tz := os.Getenv("TZ") + newLocale := "Asia/Tokyo" + if tz == newLocale { + newLocale = "Africa/Cairo" + } + if err := os.Setenv("TZ", newLocale); err != nil { + t.Fatalf("error setting locale: %v", err) + } + + // revert locale back to original + t.Cleanup(func() { + if err := os.Setenv("TZ", tz); err != nil { + t.Fatalf("error setting locale: %v", err) + } + }) +} diff --git a/fhirpath/system/types.go b/fhirpath/system/types.go new file mode 100644 index 0000000..8ab5c37 --- /dev/null +++ b/fhirpath/system/types.go @@ -0,0 +1,166 @@ +package system + +import ( + "encoding/base64" + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirconv" + "github.com/verily-src/fhirpath-go/internal/protofields" +) + +var ErrCantBeCast = errors.New("value can't be cast to system type") + +// Any is the root abstraction for all FHIRPath system types. +type Any interface { + isSystemType() + Name() string + Less(input Any) (Boolean, error) +} + +// Stub methods on each type to implement interface Any. +func (s String) isSystemType() {} +func (b Boolean) isSystemType() {} +func (i Integer) isSystemType() {} +func (d Decimal) isSystemType() {} +func (d Date) isSystemType() {} +func (t Time) isSystemType() {} +func (d DateTime) isSystemType() {} +func (q Quantity) isSystemType() {} + +// IsValid validates whether the input string represents +// a valid system type name. +func IsValid(typeName string) bool { + switch typeName { + case stringType, booleanType, integerType, decimalType, + dateType, timeType, dateTimeType, quantityType, anyType: + return true + default: + return false + } +} + +// IsPrimitive evaluates to check whether or not the input +// is a primitive FHIR type. If so, returns true, otherwise +// returns false +func IsPrimitive(input any) bool { + switch v := input.(type) { + case *dtpb.Boolean, *dtpb.String, *dtpb.Uri, *dtpb.Url, *dtpb.Canonical, *dtpb.Code, *dtpb.Oid, *dtpb.Id, *dtpb.Uuid, *dtpb.Markdown, + *dtpb.Base64Binary, *dtpb.Integer, *dtpb.UnsignedInt, *dtpb.PositiveInt, *dtpb.Decimal, *dtpb.Date, + *dtpb.Time, *dtpb.DateTime, *dtpb.Instant, *dtpb.Quantity, Any: + return true + case fhir.Base: + return protofields.IsCodeField(v) + default: + return false + } +} + +// From converts primitive FHIR types to System types. +// Returns the input if already a System type, and an error +// if the input is not convertible. +func From(input any) (Any, error) { + switch v := input.(type) { + case *dtpb.Boolean: + return Boolean(v.Value), nil + case *dtpb.String: + return String(v.Value), nil + case *dtpb.Uri: + return String(v.Value), nil + case *dtpb.Url: + return String(v.Value), nil + case *dtpb.Code: + return String(v.Value), nil + case *dtpb.Oid: + return String(v.Value), nil + case *dtpb.Id: + return String(v.Value), nil + case *dtpb.Uuid: + return String(v.Value), nil + case *dtpb.Markdown: + return String(v.Value), nil + case *dtpb.Base64Binary: + return String(base64.StdEncoding.EncodeToString(v.Value)), nil + case *dtpb.Canonical: + return String(v.Value), nil + case *dtpb.Integer: + return Integer(v.Value), nil + case *dtpb.UnsignedInt: + return Integer(v.Value), nil + case *dtpb.PositiveInt: + return Integer(v.Value), nil + case *dtpb.Decimal: + value, err := decimal.NewFromString(v.Value) + if err != nil { + return nil, err + } + return Decimal(value), nil + case *dtpb.Date: + value, err := DateFromProto(v) + if err != nil { + return nil, err + } + return value, nil + case *dtpb.Time: + return TimeFromProto(v), nil + case *dtpb.DateTime: + value, err := DateTimeFromProto(v) + if err != nil { + return nil, err + } + return value, nil + case *dtpb.Instant: + value, err := ParseDateTime(fhirconv.InstantToString(v)) + if err != nil { + return nil, err + } + return value, nil + case *dtpb.Quantity: + value, err := decimal.NewFromString(v.Value.Value) + if err != nil { + return nil, err + } + unit := v.GetCode().GetValue() + return Quantity{Decimal(value), unit}, nil + case Any: + return v, nil + case fhir.Base: + value, ok := protofields.StringValueFromCodeField(v) + if !ok { + return nil, fmt.Errorf("%w: complex type %T", ErrCantBeCast, input) + } + return String(value), nil + default: + return nil, fmt.Errorf("%w: %T", ErrCantBeCast, input) + } +} + +// Normalize casts the "from" type to the "to" type if implicit casting +// is supported between the types. Otherwise, it returns the from input. +func Normalize(from Any, to Any) Any { + switch v := from.(type) { + case Integer: + if _, ok := to.(Decimal); ok { + return Decimal(decimal.NewFromInt32(int32(v))) + } + if q, ok := to.(Quantity); ok { + dec := Decimal(decimal.NewFromInt32(int32(v))) + return Quantity{dec, q.unit} + } + case Decimal: + if q, ok := to.(Quantity); ok { + return Quantity{v, q.unit} + } + case Date: + if _, ok := to.(DateTime); ok { + newLayout := v.l + "T" + return DateTime{v.date, newLayout} + } + default: + return from + } + return from +} diff --git a/fhirpath/system/types_test.go b/fhirpath/system/types_test.go new file mode 100644 index 0000000..6ed1731 --- /dev/null +++ b/fhirpath/system/types_test.go @@ -0,0 +1,281 @@ +package system_test + +import ( + "testing" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + mrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_request_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + qpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/shopspring/decimal" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element/canonical" + "github.com/verily-src/fhirpath-go/fhirpath/system" +) + +var date, _ = system.ParseDate("2012-12-31") +var time, _ = system.ParseTime("08:30:05") +var dateTime, _ = system.ParseDateTime("2002-04-20T08:30:00Z") +var quantity, _ = system.ParseQuantity("1.234", "m") + +type testCase struct { + name string + input any + want system.Any + shouldCast bool +} + +var testCases []testCase = []testCase{ + { + name: "converts Boolean", + input: fhir.Boolean(true), + want: system.Boolean(true), + shouldCast: true, + }, + { + name: "converts String", + input: fhir.String("string"), + want: system.String("string"), + shouldCast: true, + }, + { + name: "converts Canonical", + input: canonical.New("string"), + want: system.String("string"), + shouldCast: true, + }, + { + name: "converts Uri", + input: fhir.URI("Uri"), + want: system.String("Uri"), + shouldCast: true, + }, + { + name: "converts Url", + input: fhir.URL("Url"), + want: system.String("Url"), + shouldCast: true, + }, + { + name: "converts Oid", + input: fhir.OID("Oid"), + want: system.String("urn:oid:Oid"), + shouldCast: true, + }, + { + name: "converts Id", + input: fhir.ID("Id"), + want: system.String("Id"), + shouldCast: true, + }, + { + name: "converts code", + input: fhir.Code("Code"), + want: system.String("Code"), + shouldCast: true, + }, + { + name: "converts uuid", + input: fhir.UUID("Uuid"), + want: system.String("urn:uuid:Uuid"), + shouldCast: true, + }, + { + name: "converts markdown", + input: fhir.Markdown("Markdown"), + want: system.String("Markdown"), + shouldCast: true, + }, + { + name: "converts base64 binary", + input: fhir.Base64Binary([]byte("hello world")), + want: system.String("aGVsbG8gd29ybGQ="), + shouldCast: true, + }, + { + name: "converts integer", + input: fhir.Integer(123), + want: system.Integer(123), + shouldCast: true, + }, + { + name: "converts positive integer", + input: fhir.PositiveInt(212), + want: system.Integer(212), + shouldCast: true, + }, + { + name: "converts unsigned integer", + input: fhir.UnsignedInt(10000), + want: system.Integer(10000), + shouldCast: true, + }, + { + name: "converts decimal", + input: fhir.Decimal(1.234), + want: system.Decimal(decimal.NewFromFloat(1.234)), + shouldCast: true, + }, + { + name: "converts date", + input: fhir.MustParseDate("2012-12-31"), + want: date, + shouldCast: true, + }, + { + name: "converts time", + input: fhir.MustParseTime("08:30:05"), + want: time, + shouldCast: true, + }, + { + name: "converts dateTime", + input: fhir.MustParseDateTime("2002-04-20T08:30:00Z"), + want: dateTime, + shouldCast: true, + }, + { + name: "converts instant", + input: fhir.MustParseInstant("2002-04-20T08:30:00Z"), + want: dateTime, + shouldCast: true, + }, + { + name: "converts quantity", + input: fhir.UCUMQuantity(float64(1.234), "m"), + want: quantity, + shouldCast: true, + }, + { + name: "passes through system type", + input: system.String("pass through"), + want: system.String("pass through"), + shouldCast: true, + }, + { + name: "doesn't cast complex type", + input: &ppb.Patient{}, + shouldCast: false, + }, + { + name: "converts gender code", + input: &ppb.Patient_GenderCode{Value: cpb.AdministrativeGenderCode_MALE}, + want: system.String("male"), + shouldCast: true, + }, + { + name: "converts PublicationStatus", + input: &qpb.Questionnaire_StatusCode{Value: cpb.PublicationStatusCode_RETIRED}, + want: system.String("retired"), + shouldCast: true, + }, + { + name: "converts IntentCode", + input: &mrpb.MedicationRequest_IntentCode{Value: cpb.MedicationRequestIntentCode_ORIGINAL_ORDER}, + want: system.String("original-order"), + shouldCast: true, + }, + { + name: "converts a code with a string value", + input: &dtpb.Attachment_ContentTypeCode{Value: "image"}, + want: system.String("image"), + shouldCast: true, + }, + { + name: "converts Priority", + input: &mrpb.MedicationRequest_PriorityCode{Value: cpb.RequestPriorityCode_URGENT}, + want: system.String("urgent"), + shouldCast: true, + }, + { + name: "doesn't cast non-code type with value field", + input: &dtpb.ContactPoint{Value: fhir.String("123-456-7890")}, + shouldCast: false, + }, + { + name: "doesn't cast non-fhir type", + input: 12, + shouldCast: false, + }, +} + +func TestIsPrimitive_ReturnsBoolean(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ok := system.IsPrimitive(tc.input) + + if ok != tc.shouldCast { + t.Errorf("system.IsPrimitive(%v) returns unexpected result, casts: %v, shouldCast: %v", tc.input, ok, tc.shouldCast) + } + }) + } +} + +func TestFrom_CastsCorrectly(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.shouldCast { + got, err := system.From(tc.input) + + if err != nil { + t.Fatalf("system.From(%v) returns unexpected error: %v", tc.input, err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("system.From(%v) returns unexpected diff: (-want, +got)\n%s", tc.input, diff) + } + } + }) + } +} + +func TestNormalize(t *testing.T) { + wantQuantity, _ := system.ParseQuantity("4", "m") + wantDateTime, _ := system.ParseDateTime("2012-12-31T") + testCases := []struct { + name string + from system.Any + to system.Any + want system.Any + }{ + { + name: "converts integer to decimal", + from: system.Integer(16), + to: system.Decimal(decimal.NewFromInt32(20)), + want: system.Decimal(decimal.NewFromInt32(16)), + }, + { + name: "converts decimal to quantity", + from: system.Decimal(decimal.NewFromFloat(1.234)), + to: quantity, + want: quantity, + }, + { + name: "converts integer to quantity", + from: system.Integer(4), + to: quantity, + want: wantQuantity, + }, + { + name: "converts Date to DateTime", + from: date, + to: dateTime, + want: wantDateTime, + }, + { + name: "passes through types that can't be converted", + from: system.String("2012-12-31"), + to: date, + want: system.String("2012-12-31"), + }, + } + + for _, tc := range testCases { + got := system.Normalize(tc.from, tc.to) + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("system.Normalize(%T, %T) returns unexpected diff: (-want, +got)\n%s", tc.from, tc.to, diff) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e7bd02d --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module github.com/verily-src/fhirpath-go + +go 1.22.2 + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 + github.com/google/fhir/go v0.7.4 + github.com/google/go-cmp v0.6.0 + github.com/google/uuid v1.6.0 + github.com/iancoleman/strcase v0.3.0 + github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 + github.com/shopspring/decimal v1.4.0 + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f + google.golang.org/protobuf v1.29.0 +) + +require ( + bitbucket.org/creachadair/stringset v0.0.9 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/json-iterator/go v1.1.10 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect + github.com/stretchr/testify v1.8.1 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e873cf8 --- /dev/null +++ b/go.sum @@ -0,0 +1,715 @@ +bitbucket.org/creachadair/stringset v0.0.9 h1:L4vld9nzPt90UZNrXjNelTshD74ps4P5NGs3Iq6yN3o= +bitbucket.org/creachadair/stringset v0.0.9/go.mod h1:t+4WcQ4+PXTa8aQdNKe40ZP6iwesoMFWAxPGd3UGjyY= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= +github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/TylerBrock/colorjson v0.0.0-20180527164720-95ec53f28296/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.28.8/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/bazelbuild/rules_go v0.24.5 h1:8S5qilf+Il5/TPMZQIOfzQDAZtkhB4jALiAnwRuisDM= +github.com/bazelbuild/rules_go v0.24.5/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/buger/jsonparser v0.0.0-20200322175846-f7e751efca13/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creachadair/staticfile v0.1.3/go.mod h1:a3qySzCIXEprDGxk6tSxSI+dBBdLzqeBOMhZ+o2d3pM= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/fhir/go v0.7.4 h1:DZv5LOcX8JIO1hdWESHNm3CD0PiWREuxjYDYE6gmzn0= +github.com/google/fhir/go v0.7.4/go.mod h1:WF6g9QjYPqcQed319oPaRT5IcYWIRz610X3mxIt5TgU= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v27 v27.0.4/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.5.0/go.mod h1:LqwrLNW876eYSuUOo4ZLHBcdKc038txr/IMfbLPATa4= +github.com/hashicorp/consul/sdk v0.5.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.2.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.0/go.mod h1:YL0HO+FifKOW2u1ke99DGVu1zhcpZzNwrLIqBC7vbYU= +github.com/hashicorp/serf v0.9.2/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/krishicks/yaml-patch v0.0.10/go.mod h1:Sm5TchwZS6sm7RJoyg87tzxm2ZcKzdRE4Q7TjNhPrME= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 h1:hQWBtNqRYrI7CWIaUSXXtNKR90KzcUA5uiuxFVWw7sU= +github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/minio/minio-go v0.0.0-20190131015406-c8a261de75c1/go.mod h1:vuvdOZLJuf5HmJAJrKV64MmozrSsk+or0PB5dzdfspg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5-0.20200416053754-163badb3bac6/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pires/go-proxyproto v0.0.0-20191211124218-517ecdf5bb2b/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= +github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tchap/go-patricia v0.0.0-20160729071656-dd168db6051b/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tebeka/selenium v0.9.9/go.mod h1:5Fr8+pUvU6B1OiPfkdCKdXZyr5znvVkxuPd0NOdZCQc= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tinylib/msgp v1.1.1/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/uber/jaeger-client-go v2.16.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.0.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/z-division/go-zookeeper v0.0.0-20190128072838-6d7457066b9b/go.mod h1:JNALoWa+nCXR8SmgLluHcBNVJgyejzpKPZk9pX2yXXE= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191219041853-979b82bfef62/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/DataDog/dd-trace-go.v1 v1.17.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ldap.v2 v2.5.0/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0= +k8s.io/apiextensions-apiserver v0.17.3/go.mod h1:CJbCyMfkKftAd/X/V6OTHYhVn7zXnDdnkUjS1h0GTeY= +k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= +k8s.io/apiserver v0.17.3/go.mod h1:iJtsPpu1ZpEnHaNawpSV0nYTGBhhX2dUlnn7/QS7QiY= +k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ= +k8s.io/code-generator v0.17.3/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ= +k8s.io/component-base v0.17.3/go.mod h1:GeQf4BrgelWm64PXkIXiPh/XS0hnO42d9gx9BtbZRp8= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +vitess.io/vitess v0.7.0/go.mod h1:MjQFT3yaDsYxY+fwUwxqD0d7MRx7c8+wx0nMeXC9U/s= diff --git a/gotchas.md b/gotchas.md new file mode 100644 index 0000000..d45587d --- /dev/null +++ b/gotchas.md @@ -0,0 +1,25 @@ +# FHIRPath Gotcha’s + +## Empty collections are propagated + +* In FHIRPath, whenever an empty collection is encountered, rather than raising an error it gets propagated throughout the rest of the expression. This may make some issues difficult to catch. +* Eg. given `Patient.name` -> `{}`, `Patient.name.family + ' MD'` -> `{}` + +## Equality sometimes returns an empty collection { }, rather than false + +* If either collection is empty +* If the **precision_ _**of Date, Time, or DateTime objects are mismatched +* If the **dimension** of a Quantity unit is mismatched + +## FHIR type specifiers are case-sensitive + +* **Primitive** types are denoted with lower case specifiers. +* **Primitive** types that are written as upper case will be resolved as **System** types, not **FHIR** types. +* Eg. `Patient.birthDate is date = **true**` but `Patient.birthDate is Date = **false**` +* Case should match what’s listed [here](https://www.hl7.org/fhir/r4/datatypes.html) +* System types always begin with an uppercase letter + +## `As` Expression is _not_ a filter, expects singleton input + +* The as expression (`Observation.value as integer`) expects a singleton as input. For example, if you pass in a resource with multiple value fields, it will raise an error. +* It doesn’t filter out things that don’t match the type. For this purpose, the `where()` function should be used -> `where(value is integer)` diff --git a/internal/bundle/bundle.go b/internal/bundle/bundle.go new file mode 100644 index 0000000..ee44885 --- /dev/null +++ b/internal/bundle/bundle.go @@ -0,0 +1,79 @@ +/* +Package bundle provides utilities for working with FHIR R4 Bundle proto +definitions. This includes functionality for constructing/wrapping and +unwrapping bundle/entry objects. +*/ +package bundle + +import ( + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/bundleopt" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +// New creates a new New by building it from the bundle options. +// Use of this function directly is discouraged; prefer to use the various +// *New functions instead for the explicit types. +func New(bundleType cpb.BundleTypeCode_Value, options ...Option) *bcrpb.Bundle { + bundle := &bcrpb.Bundle{ + Type: &bcrpb.Bundle_TypeCode{ + Value: bundleType, + }, + } + return Extend(bundle, options...) +} + +// Extend extends an existing bundle with the provided bundle options. +// The options will be extended in-place; the return value is not necessary to be +// looked at, but is available for convenience when used in fluent APIs. +// +// This decision was made to avoid cloning the bundle per-invocation, since in a +// loop this would grow the cost involved with calling this function substantially. +func Extend(bundle *bcrpb.Bundle, opts ...Option) *bcrpb.Bundle { + bundleopt.Apply(bundle, opts...) + return bundle +} + +// NewTransaction is a helper function for building a transaction bundle. +func NewTransaction(options ...Option) *bcrpb.Bundle { + return New(cpb.BundleTypeCode_TRANSACTION, options...) +} + +// NewCollection is a helper function for building a collection bundle. +func NewCollection(options ...Option) *bcrpb.Bundle { + return New(cpb.BundleTypeCode_COLLECTION, options...) +} + +// NewBatch is a helper function for building a batch bundle. +func NewBatch(options ...Option) *bcrpb.Bundle { + return New(cpb.BundleTypeCode_BATCH, options...) +} + +// NewHistory is a helper function for building a history bundle. +func NewHistory(options ...Option) *bcrpb.Bundle { + return New(cpb.BundleTypeCode_HISTORY, options...) +} + +// NewSearchset is a helper function for building a searchset bundle. +func NewSearchset(options ...Option) *bcrpb.Bundle { + return New(cpb.BundleTypeCode_SEARCHSET, options...) +} + +// Unwrap unwraps a bundle into a slice of resources. +func Unwrap(bundle *bcrpb.Bundle) []fhir.Resource { + return slices.Map(bundle.GetEntry(), UnwrapEntry) +} + +// UnwrapMap unwraps a bundle into a map indexed by resource type. +func UnwrapMap(bundle *bcrpb.Bundle) map[resource.Type][]fhir.Resource { + resourceMap := map[resource.Type][]fhir.Resource{} + resources := Unwrap(bundle) + for _, res := range resources { + resourceType := resource.TypeOf(res) + resourceMap[resourceType] = append(resourceMap[resourceType], res) + } + return resourceMap +} diff --git a/internal/bundle/bundle_entry.go b/internal/bundle/bundle_entry.go new file mode 100644 index 0000000..28f0977 --- /dev/null +++ b/internal/bundle/bundle_entry.go @@ -0,0 +1,285 @@ +package bundle + +import ( + "bytes" + "encoding/json" + "fmt" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + bpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/binary_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/google/uuid" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/containedresource" + "github.com/verily-src/fhirpath-go/internal/element/reference" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +var ( + ErrInvalidIdentity = fmt.Errorf("invaild resource identity") + ErrInvalidPayload = fmt.Errorf("invalid payload") + ErrMissingPayload = fmt.Errorf("%w: nil or empty payload", ErrInvalidPayload) +) + +// EntryOption is an option interface for constructing bundle entries +// from raw data. +type EntryOption interface { + updateEntry(entry *bcrpb.Bundle_Entry) +} + +// fullURLOpt is a bundle entry option for including a full url. +type fullURLOpt string + +func (o fullURLOpt) updateEntry(entry *bcrpb.Bundle_Entry) { + if o != "" { + entry.FullUrl = &dtpb.Uri{ + Value: string(o), + } + } +} + +// ifNoneExistOpt is a bundle entry option for including the If-None-Exist header +type ifNoneExistOpt struct { + identifier *dtpb.Identifier +} + +// ifNoneExistOpt.updateEntry sets the If-None-Exist header of a POST entry +// in the format `identifier=system|value` +// Either system or value can be empty (see https://hl7.org/fhir/R4/search.html#token) +// If system is empty, only `identifier=value` supported; not `identifier=|value` +func (o ifNoneExistOpt) updateEntry(entry *bcrpb.Bundle_Entry) { + req := entry.Request + if req == nil || req.GetMethod().GetValue() != cpb.HTTPVerbCode_POST { + return + } + var sysVal string + if sys := o.identifier.GetSystem().GetValue(); sys != "" { + sysVal += sys + "|" + } + sysVal += o.identifier.GetValue().GetValue() + if sysVal == "" { + return + } + req.IfNoneExist = fhir.String( + fmt.Sprintf("identifier=%s", sysVal), + ) +} + +// WithFullURL adds a FullUrl field to a BundleEntry. +func WithFullURL(url string) EntryOption { + return fullURLOpt(url) +} + +// WithGeneratedFullURL adds a randomly generated FullUrl +// field to a BundleEntry, taking the form urn:uuid:$UUID. +func WithGeneratedFullURL() EntryOption { + url := fmt.Sprintf("urn:uuid:%s", uuid.NewString()) + return fullURLOpt(url) +} + +// WithIfNoneExist adds an identifier to the If-None-Exist request header of +// a POST BundleEntry, in the format `identifier=system|value` +func WithIfNoneExist(identifier *dtpb.Identifier) EntryOption { + return ifNoneExistOpt{identifier: identifier} +} + +// NewGetEntry constructs a bundle entry with a GET request for the head value +// of a resource. This is the FHIR "read" interaction. +// +// For use within a batch or transaction bundle. +// +// Clients should not request a versioned resource by crafting a special id +// argument; instead client should use VersionedGetEntry() below. +func NewGetEntry(typeName resource.Type, id string, opts ...EntryOption) *bcrpb.Bundle_Entry { + return NewVersionedGetEntry(typeName, id, "" /*version*/, opts...) +} + +// NewVersionedGetEntry constructs a bundle entry with a GET request +// for the versioned value of a resource. This is the FHIR "vread" interaction. +// +// For use within a batch or transaction bundle. +// +// If version is empty, the returned entry requests the head version (a simple GET). +// +// The Bundle_Entry.FullUrl element is set to the requested resource's +// relative URL (without version). This is "good enough", despite questionable +// compliance with the standard. Clients may override this using WithFullUrl(). +// +// This function does NOT support the "history-*" (eg "history-instance") +// interactions. Those interaction return all the historical versions +// for one or more resources, and are not supported within a batch +// or transaction bundle. +func NewVersionedGetEntry(typeName resource.Type, id string, version string, opts ...EntryOption) *bcrpb.Bundle_Entry { + url := string(typeName) + "/" + id + requestUrl := url + if version != "" { + requestUrl += "/_history/" + version + } + entry := bundleEntry(cpb.HTTPVerbCode_GET, nil /*resource*/, requestUrl) + entry.FullUrl = fhir.URI(url) + return applyOptions(entry, opts) +} + +// NewPostEntry wraps a FHIR Resource for a transaction bundle to be written to +// storage via a POST request. +// +// This is based on GCP documentation here: +// https://cloud.google.com/healthcare-api/docs/how-tos/fhir-bundles#resolving_references_to_resources_created_in_a_bundle +func NewPostEntry(res fhir.Resource, opts ...EntryOption) *bcrpb.Bundle_Entry { + typeString := resource.TypeOf(res) + entry := bundleEntry(cpb.HTTPVerbCode_POST, res, string(typeString)) + return applyOptions(entry, opts) +} + +// NewPutEntry wraps a FHIR Resource for a transaction bundle to be written to +// storage via a PUT request. BundleEntry.fullUrl is not populated. +// +// This is based on GCP documentation here: +// https://cloud.google.com/healthcare-api/docs/how-tos/fhir-bundles#resolving_references_to_resources_created_in_a_bundle +func NewPutEntry(res fhir.Resource, opts ...EntryOption) *bcrpb.Bundle_Entry { + uri := resource.URIString(res) + entry := bundleEntry(cpb.HTTPVerbCode_PUT, res, uri) + return applyOptions(entry, opts) +} + +// NewDeleteEntry constructs a delete resource operation via a DELETE request. +// +// For use within a batch or transaction bundle. +func NewDeleteEntry(typeName resource.Type, id string, opts ...EntryOption) *bcrpb.Bundle_Entry { + url := string(typeName) + "/" + id + entry := bundleEntry(cpb.HTTPVerbCode_DELETE, nil /*resource*/, url) + return applyOptions(entry, opts) +} + +// NewCollectionEntry takes in a FHIR Resource and creates a BundleEntry +// for the resource. +func NewCollectionEntry(res fhir.Resource) *bcrpb.Bundle_Entry { + return &bcrpb.Bundle_Entry{ + Resource: containedresource.Wrap(res), + FullUrl: resource.URI(res), + } +} + +type patchOps []struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +// validatePatch performs basic validation of the patch payload, disallowing +// unknown fields. It doesn't check existence of required fields and their +// values. +func validatePatch(payload []byte) error { + if !json.Valid(payload) { + return ErrInvalidPayload + } + var patch patchOps + dec := json.NewDecoder(bytes.NewReader(payload)) + dec.DisallowUnknownFields() + return dec.Decode(&patch) +} + +// PatchEntryFromBytes takes in a Resource Identity and a byte array payload, +// and returns a BundleEntry corresponding to a JSON Patch for the resource. +// +// See https://cloud.google.com/healthcare-api/docs/how-tos/fhir-resources#executing_a_patch_request_in_a_fhir_bundle +// for the details of how PATCH payload can be generated for known fields. +// +// For PATCH operations generation consider using one of the Go jsonpatch libraries from https://jsonpatch.com/#go. +// See the ./bundle_example_test.go/TestPatchEntry*() for the list of usage examples. +func PatchEntryFromBytes(identity *resource.Identity, payload []byte) (*bcrpb.Bundle_Entry, error) { + if identity == nil || identity.Type() == "" || identity.ID() == "" { + return nil, fmt.Errorf("unable to patch resource with identity (%v): %w", identity, ErrInvalidIdentity) + } + if payload == nil || len(payload) == 0 { + return nil, fmt.Errorf("unable to patch resource: %w", ErrMissingPayload) + } + if err := validatePatch(payload); err != nil { + return nil, fmt.Errorf("unable to patch resource, %w: %w", ErrInvalidPayload, err) + } + + br := &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Binary{ + Binary: &bpb.Binary{ + ContentType: &bpb.Binary_ContentTypeCode{Value: "application/json-patch+json"}, + Data: &dtpb.Base64Binary{Value: payload}, + }, + }, + } + return &bcrpb.Bundle_Entry{ + Resource: br, + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{ + Value: cpb.HTTPVerbCode_PATCH, + }, + Url: identity.RelativeURI(), + }, + }, nil +} + +// UnwrapEntry unwraps a bundle entry into a FHIR Resource. +// +// If the bundle entry is nil, or if the entry does not contain a resource, this +// function will return nil. +func UnwrapEntry(entry *bcrpb.Bundle_Entry) fhir.Resource { + if entry == nil { + return nil + } + return containedresource.Unwrap(entry.GetResource()) +} + +func bundleEntry(method cpb.HTTPVerbCode_Value, resource fhir.Resource, requestURL string) *bcrpb.Bundle_Entry { + return &bcrpb.Bundle_Entry{ + Resource: containedresource.Wrap(resource), + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{ + Value: method, + }, + Url: &dtpb.Uri{ + Value: requestURL, + }, + }, + } +} + +func applyOptions(entry *bcrpb.Bundle_Entry, opts []EntryOption) *bcrpb.Bundle_Entry { + for _, opt := range opts { + opt.updateEntry(entry) + } + return entry +} + +// EntryReference generates a FHIR Reference proto pointing to the Entry's +// resource. +func EntryReference(entry *bcrpb.Bundle_Entry) *dtpb.Reference { + if entry.GetResource() == nil { + return nil + } + res := UnwrapEntry(entry) + resourceType := resource.TypeOf(res) + return reference.Weak(resourceType, entry.GetFullUrl().GetValue()) +} + +// SetEntryIfMatch sets the entry.Request.IfMatch based on +// entry.Resource.Meta.VersionId. +func SetEntryIfMatch(entry *bcrpb.Bundle_Entry) { + req := entry.GetRequest() + // No request or a request with If-Match already set + if req == nil || req.GetIfMatch() != nil { + return + } + + // If-Match is only respected for PUT and PATCH, and PATCH uses a specially- + // constructed resource so it doesn't make sense to infer anything from it; + // some day If-Match on DELETE might be respected, update this code then + if req.GetMethod().GetValue() != cpb.HTTPVerbCode_PUT { + return + } + + // Set If-Match should be based on the entry.Resource + version := resource.VersionETag(UnwrapEntry(entry)) + if version != "" { + req.IfMatch = fhir.String(version) + } +} diff --git a/internal/bundle/bundle_entry_example_test.go b/internal/bundle/bundle_entry_example_test.go new file mode 100644 index 0000000..a9500ae --- /dev/null +++ b/internal/bundle/bundle_entry_example_test.go @@ -0,0 +1,94 @@ +package bundle_test + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/google/fhir/go/fhirversion" + "github.com/google/fhir/go/jsonformat" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + r4pb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/mattbaird/jsonpatch" + "github.com/verily-src/fhirpath-go/internal/bundle" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/proto" +) + +var resIdentity, _ = resource.NewIdentity("Patient", "123", "") + +func ExamplePatchEntryFromBytes_stringPatch() { + patch := `[{"op":"add","path":"/active","value":true}]` + pEntry, err := bundle.PatchEntryFromBytes(resIdentity, []byte(patch)) + if err != nil { + log.Fatal(err) + } + fmt.Printf("PatchEntry: %+v", pEntry) +} + +func ExamplePatchEntryFromBytes_mapPatch() { + patch := []map[string]interface{}{ + { + "op": "replace", + "path": "/active", + "value": true, + }, + } + payload, err := json.Marshal(patch) + if err != nil { + log.Fatal(err) + } + pEntry, err := bundle.PatchEntryFromBytes(resIdentity, payload) + if err != nil { + log.Fatal(err) + } + fmt.Printf("PatchEntry: %+v", pEntry) +} + +// ExamplePatchEntryFromBytes_diffPatch creates a patch for the diff of two +// given resources diff. +func ExamplePatchEntryFromBytes_diffPatch() { + res := &r4pb.ContainedResource{ + OneofResource: &r4pb.ContainedResource_Patient{ + Patient: &ppb.Patient{ + Id: &dtpb.Id{ + Value: "123", + }, + Active: &dtpb.Boolean{ + Value: false, + }, + }, + }, + } + newRes := proto.Clone(res).(*r4pb.ContainedResource) + newRes.GetPatient().Active = &dtpb.Boolean{Value: true} + + m, err := jsonformat.NewMarshaller(false, "", "", fhirversion.R4) + if err != nil { + log.Fatal(err) + } + resB, err := m.Marshal(res) + if err != nil { + log.Fatal(err) + } + newResB, err := m.Marshal(newRes) + if err != nil { + log.Fatal(err) + } + + patch, err := jsonpatch.CreatePatch(resB, newResB) + if err != nil { + log.Fatal(err) + } + pPayload, err := json.Marshal(patch) + if err != nil { + log.Fatal(err) + } + + pEntry, err := bundle.PatchEntryFromBytes(resIdentity, pPayload) + if err != nil { + log.Fatal(err) + } + fmt.Printf("PatchEntry: %+v", pEntry) +} diff --git a/internal/bundle/bundle_entry_test.go b/internal/bundle/bundle_entry_test.go new file mode 100644 index 0000000..266c51d --- /dev/null +++ b/internal/bundle/bundle_entry_test.go @@ -0,0 +1,477 @@ +package bundle_test + +import ( + "testing" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + bpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/binary_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/bundle" + "github.com/verily-src/fhirpath-go/internal/containedresource" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/testing/protocmp" +) + +var ( + iuURI = fhir.URI("Patient/IU") + patient = &ppb.Patient{Id: fhir.ID("IU")} + ifNoneExistFullOpt = bundle.WithIfNoneExist(&dtpb.Identifier{System: fhir.URI("system"), Value: fhir.String("value")}) + ifNoneExistSysOpt = bundle.WithIfNoneExist(&dtpb.Identifier{System: fhir.URI("system")}) + ifNoneExistValOpt = bundle.WithIfNoneExist(&dtpb.Identifier{Value: fhir.String("value")}) + ifNoneExistEmptyOpt = bundle.WithIfNoneExist(&dtpb.Identifier{}) + + patientPostEntry = makePatientEntry(cpb.HTTPVerbCode_POST, patient, nil, "") + patientPostEntryFullHeader = makePatientEntry(cpb.HTTPVerbCode_POST, patient, nil, "identifier=system|value") + patientPostEntrySysHeader = makePatientEntry(cpb.HTTPVerbCode_POST, patient, nil, "identifier=system|") + patientPostEntryValHeader = makePatientEntry(cpb.HTTPVerbCode_POST, patient, nil, "identifier=value") + uriPostEntry = makePatientEntry(cpb.HTTPVerbCode_POST, patient, iuURI, "") + patientPutEntry = makePatientEntry(cpb.HTTPVerbCode_PUT, patient, nil, "") + uriPutEntry = makePatientEntry(cpb.HTTPVerbCode_PUT, patient, iuURI, "") +) + +func TestGetEntry(t *testing.T) { + // Only a single test case because the impl is a pass-thru to VersionedGetEntry. + wantEntry := makeEntryForGet("Patient/1234", "//Full") + gotEntry := bundle.NewGetEntry(resource.Patient, "1234", + bundle.WithFullURL("//Full")) + + if diff := cmp.Diff(wantEntry, gotEntry, protocmp.Transform()); diff != "" { + t.Errorf("GetEntry mismatch (-want, +got):\n%s", diff) + } +} + +func TestVersionedGetEntry(t *testing.T) { + testCases := []struct { + name string + typeName string + id string + version string + opts []bundle.EntryOption + wantEntry *bcrpb.Bundle_Entry + }{ + {"no version", "Patient", "1234", "", nil, makeEntryForGet("Patient/1234", "Patient/1234")}, + {"with version", "Patient", "1234", "abcd", nil, makeEntryForGet("Patient/1234/_history/abcd", "Patient/1234")}, + {"with full", "Patient", "1234", "abcd", []bundle.EntryOption{bundle.WithFullURL("//Full")}, + makeEntryForGet("Patient/1234/_history/abcd", "//Full")}, + {"ignore if-none-exist", "Patient", "1234", "", []bundle.EntryOption{ifNoneExistFullOpt}, + makeEntryForGet("Patient/1234", "Patient/1234")}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + entry := bundle.NewVersionedGetEntry(resource.Type(tc.typeName), tc.id, tc.version, tc.opts...) + + got, want := entry, tc.wantEntry + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("VersionedGetEntry(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func makeEntryForGet(requestUrl string, fullUrl string) *bcrpb.Bundle_Entry { + return &bcrpb.Bundle_Entry{ + FullUrl: fhir.URI(fullUrl), + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{ + Value: cpb.HTTPVerbCode_GET, + }, + Url: fhir.URI(requestUrl), + }, + } +} + +func TestPostEntry(t *testing.T) { + testCases := []struct { + name string + resource fhir.Resource + opts []bundle.EntryOption + wantEntry *bcrpb.Bundle_Entry + }{ + {"no options", patient, nil, patientPostEntry}, + {"empty uri", patient, []bundle.EntryOption{bundle.WithFullURL("")}, patientPostEntry}, + {"uri provided", patient, []bundle.EntryOption{bundle.WithFullURL(iuURI.GetValue())}, uriPostEntry}, + {"apply if-none-exist", patient, []bundle.EntryOption{ifNoneExistFullOpt}, patientPostEntryFullHeader}, + {"apply if-none-exist with identifier.system", patient, []bundle.EntryOption{ifNoneExistSysOpt}, patientPostEntrySysHeader}, + {"apply if-none-exist with identifier.value", patient, []bundle.EntryOption{ifNoneExistValOpt}, patientPostEntryValHeader}, + {"ignore if-none-exist with empty identifier", patient, []bundle.EntryOption{ifNoneExistEmptyOpt}, patientPostEntry}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + entry := bundle.NewPostEntry(tc.resource, tc.opts...) + + got, want := entry, tc.wantEntry + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("PostEntry(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestPutEntry(t *testing.T) { + testCases := []struct { + name string + resource fhir.Resource + opts []bundle.EntryOption + wantEntry *bcrpb.Bundle_Entry + }{ + {"no options", patient, nil, patientPutEntry}, + {"empty uri", patient, []bundle.EntryOption{bundle.WithFullURL("")}, patientPutEntry}, + {"uri provided", patient, []bundle.EntryOption{bundle.WithFullURL(iuURI.GetValue())}, uriPutEntry}, + {"ignore if-none-exist", patient, []bundle.EntryOption{ifNoneExistFullOpt}, patientPutEntry}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + entry := bundle.NewPutEntry(tc.resource, tc.opts...) + + got, want := entry, tc.wantEntry + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("PutEntry(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestDeleteEntry(t *testing.T) { + testCases := []struct { + name string + inType resource.Type + inID string + opts []bundle.EntryOption + wantEntry *bcrpb.Bundle_Entry + }{ + { + name: "no options", + inType: resource.Patient, + inID: "1234", + opts: nil, + wantEntry: &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{ + Value: cpb.HTTPVerbCode_DELETE, + }, + Url: &dtpb.Uri{ + Value: "Patient/1234", + }, + }, + }, + }, + { + name: "with option id", + inType: resource.Patient, + inID: "1234", + opts: []bundle.EntryOption{bundle.WithFullURL("test-full-url")}, + wantEntry: &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{ + Value: cpb.HTTPVerbCode_DELETE, + }, + Url: &dtpb.Uri{ + Value: "Patient/1234", + }, + }, + FullUrl: &dtpb.Uri{ + Value: "test-full-url", + }, + }, + }, + { + name: "ignore if-none-exist", + inType: resource.Patient, + inID: "1234", + opts: []bundle.EntryOption{ifNoneExistFullOpt}, + wantEntry: &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{ + Value: cpb.HTTPVerbCode_DELETE, + }, + Url: &dtpb.Uri{ + Value: "Patient/1234", + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + entry := bundle.NewDeleteEntry(tc.inType, tc.inID, tc.opts...) + + got, want := entry, tc.wantEntry + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("NewDeleteEntry(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestPatchEntryFromBytes(t *testing.T) { + + patch := []byte(`[{"op":"add","path":"/active","value":true}]`) + validIdentity, _ := resource.NewIdentity("Patient", "123", "0") + + testCases := []struct { + name string + resID *resource.Identity + payload []byte + wantEntry *bcrpb.Bundle_Entry + wantError error + }{ + { + name: "Valid patch", + resID: validIdentity, + payload: patch, + wantEntry: &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{ + Value: cpb.HTTPVerbCode_PATCH, + }, + Url: &dtpb.Uri{ + Value: "Patient/123", + }, + }, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Binary{ + Binary: &bpb.Binary{ + ContentType: &bpb.Binary_ContentTypeCode{Value: "application/json-patch+json"}, + Data: &dtpb.Base64Binary{Value: patch}, + }, + }, + }, + }, + }, + { + name: "Invalid resource identity", + resID: &resource.Identity{}, + payload: patch, + wantError: bundle.ErrInvalidIdentity, + }, + { + name: "Nil resource identity", + resID: nil, + payload: patch, + wantError: bundle.ErrInvalidIdentity, + }, + { + name: "Nil payload", + resID: validIdentity, + payload: nil, + wantError: bundle.ErrMissingPayload, + }, + { + name: "Empty payload", + resID: validIdentity, + payload: []byte{}, + wantError: bundle.ErrMissingPayload, + }, + { + name: "Invalid payload - single op", + resID: validIdentity, + payload: []byte(`{"op":"add","path":"/active","value":true}`), + wantError: bundle.ErrInvalidPayload, + }, + { + name: "Invalid payload - wrong field", + resID: validIdentity, + payload: []byte(`[{"op":"add","url":"/active","value":true}]`), + wantError: bundle.ErrInvalidPayload, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + entry, err := bundle.PatchEntryFromBytes(tc.resID, tc.payload) + + if tc.wantError == nil { + got, want := entry, tc.wantEntry + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("PatchEntryFromBytes(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + } else { + got, want := err, tc.wantError + if !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("PatchEntryFromBytes(%s): unexpected error got %s, want %s", tc.name, got, want) + } + } + }) + } +} + +func TestCollectionEntry(t *testing.T) { + wantEntry := &bcrpb.Bundle_Entry{ + Resource: containedresource.Wrap(patient), + FullUrl: &dtpb.Uri{Value: "Patient/IU"}, + } + entry := bundle.NewCollectionEntry(patient) + + got, want := entry, wantEntry + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("CollectionEntry mismatch (-want, +got):\n%s", diff) + } +} + +// Test helper to create a BundleEntry of a Patient +func makePatientEntry(method cpb.HTTPVerbCode_Value, res fhir.Resource, uri *dtpb.Uri, header string) *bcrpb.Bundle_Entry { + requestURL := "Patient" + if method == cpb.HTTPVerbCode_PUT { + requestURL = resource.URIString(res) + } + entry := &bcrpb.Bundle_Entry{ + Resource: containedresource.Wrap(res), + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{ + Value: method, + }, + Url: &dtpb.Uri{ + Value: requestURL, + }, + }, + } + if uriString := uri.GetValue(); uriString != "" { + entry.FullUrl = uri + } + if header != "" { + entry.Request.IfNoneExist = fhir.String(header) + } + return entry +} + +func TestEntryReference(t *testing.T) { + entry := &bcrpb.Bundle_Entry{ + FullUrl: &dtpb.Uri{ + Value: "patient-full-url", + }, + Resource: containedresource.Wrap(resource.New("Patient")), + } + wantRef := &dtpb.Reference{ + Type: &dtpb.Uri{ + Value: "Patient", + }, + Reference: &dtpb.Reference_Uri{ + Uri: &dtpb.String{ + Value: "patient-full-url", + }, + }, + } + + ref := bundle.EntryReference(entry) + + got, want := ref, wantRef + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("EntryReference: (-got +want):\n%v", diff) + } +} + +func TestSetEntryIfMatch(t *testing.T) { + testCases := []struct { + name string + entry *bcrpb.Bundle_Entry + wantVersion string + }{ + { + "does not override ifMatch with resource-derived value if it's already set", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{ + Method: &bcrpb.Bundle_Entry_Request_MethodCode{Value: cpb.HTTPVerbCode_POST}, + IfMatch: fhir.String("already set"), + }, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("derived")}}}, + }, + }, + "already set", + }, + { + "does not set ifMatch if there's no resource version", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{Method: &bcrpb.Bundle_Entry_Request_MethodCode{Value: cpb.HTTPVerbCode_POST}}, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("")}}}, + }, + }, + "", + }, + { + "does not set ifMatch with resource-derived value if the method is unspecified", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{}, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("derived")}}}, + }, + }, + "", + }, + { + "does not set ifMatch with resource-derived value if it's a GET", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{Method: &bcrpb.Bundle_Entry_Request_MethodCode{Value: cpb.HTTPVerbCode_GET}}, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("derived")}}}, + }, + }, + "", + }, + { + "does not set ifMatch with resource-derived value if it's a HEAD", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{Method: &bcrpb.Bundle_Entry_Request_MethodCode{Value: cpb.HTTPVerbCode_HEAD}}, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("derived")}}}, + }, + }, + "", + }, + { + "does not set ifMatch with resource-derived value if it's a POST", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{Method: &bcrpb.Bundle_Entry_Request_MethodCode{Value: cpb.HTTPVerbCode_POST}}, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("derived")}}}, + }, + }, + "", + }, + { + "sets ifMatch with resource-derived value if it's a PUT", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{Method: &bcrpb.Bundle_Entry_Request_MethodCode{Value: cpb.HTTPVerbCode_PUT}}, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("derived")}}}, + }, + }, + `W/"derived"`, + }, + { + "does not set ifMatch with resource-derived value if it's a PATCH", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{Method: &bcrpb.Bundle_Entry_Request_MethodCode{Value: cpb.HTTPVerbCode_PATCH}}, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("derived")}}}, + }, + }, + "", + }, + { + "does not set ifMatch with resource-derived value if it's a DELETE", + &bcrpb.Bundle_Entry{ + Request: &bcrpb.Bundle_Entry_Request{Method: &bcrpb.Bundle_Entry_Request_MethodCode{Value: cpb.HTTPVerbCode_DELETE}}, + Resource: &bcrpb.ContainedResource{ + OneofResource: &bcrpb.ContainedResource_Patient{Patient: &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("derived")}}}, + }, + }, + "", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bundle.SetEntryIfMatch(tc.entry) + + gotVersion := tc.entry.GetRequest().GetIfMatch().GetValue() + if diff := cmp.Diff(gotVersion, tc.wantVersion); diff != "" { + t.Errorf("SetEntryIfMatch(%s) version got = %v, want = %v", tc.name, gotVersion, tc.wantVersion) + } + }) + } +} diff --git a/internal/bundle/bundle_option.go b/internal/bundle/bundle_option.go new file mode 100644 index 0000000..dd2aa4c --- /dev/null +++ b/internal/bundle/bundle_option.go @@ -0,0 +1,51 @@ +package bundle + +import ( + "time" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/bundleopt" +) + +// Option is an option interface for constructing bundles +// from raw data. +type Option = bundleopt.BundleOption + +// entriesOpt is a bundle option for including bundle entries. +type entriesOpt []*bcrpb.Bundle_Entry + +func (o entriesOpt) updateBundle(bundle *bcrpb.Bundle) { + if len(o) > 0 { + bundle.Entry = o + if bundle.GetType().GetValue() == cpb.BundleTypeCode_SEARCHSET || bundle.GetType().GetValue() == cpb.BundleTypeCode_HISTORY { + bundle.Total = fhir.UnsignedInt(uint32(len(o))) + } + } +} + +// WithEntries adds bundle entries to a bundle. +func WithEntries(entries ...*bcrpb.Bundle_Entry) Option { + entriesopt := entriesOpt(entries) + return bundleopt.Transform(entriesopt.updateBundle) +} + +// timeOpt is a bundle option for including a timestamp. +type timeOpt time.Time + +func (o timeOpt) updateBundle(bundle *bcrpb.Bundle) { + bundle.Timestamp = fhir.Instant(time.Time(o)) +} + +// WithTimestamp adds a given time to the bundle's timestamp. +func WithTimestamp(t time.Time) Option { + timeOpt := timeOpt(t) + return bundleopt.Transform(timeOpt.updateBundle) +} + +// WithTimestampNow adds the current time to the bundle's timestamp. +func WithTimestampNow() Option { + timeOpt := timeOpt(time.Now()) + return bundleopt.Transform(timeOpt.updateBundle) +} diff --git a/internal/bundle/bundle_test.go b/internal/bundle/bundle_test.go new file mode 100644 index 0000000..1f4c11f --- /dev/null +++ b/internal/bundle/bundle_test.go @@ -0,0 +1,216 @@ +package bundle_test + +import ( + "testing" + "time" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/bundle" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestBundle(t *testing.T) { + iuTime := time.Date(1993, time.May, 16, 0, 0, 0, 0, time.UTC) + iuPatient := &ppb.Patient{Id: fhir.ID("IU")} + uaenaPatient := &ppb.Patient{Id: fhir.ID("Uaena")} + firstEntry := bundle.NewPostEntry(iuPatient) + secondEntry := bundle.NewPostEntry(uaenaPatient) + baseBundle := &bcrpb.Bundle{Type: &bcrpb.Bundle_TypeCode{Value: cpb.BundleTypeCode_TRANSACTION}} + timeBundle := proto.Clone(baseBundle).(*bcrpb.Bundle) + timeBundle.Timestamp = fhir.Instant(iuTime) + fullTxnBundle := &bcrpb.Bundle{ + Type: &bcrpb.Bundle_TypeCode{Value: cpb.BundleTypeCode_TRANSACTION}, + Entry: []*bcrpb.Bundle_Entry{firstEntry, secondEntry}, + } + + fullSearchBundle := &bcrpb.Bundle{ + Type: &bcrpb.Bundle_TypeCode{Value: cpb.BundleTypeCode_SEARCHSET}, + Entry: []*bcrpb.Bundle_Entry{firstEntry, secondEntry}, + Total: fhir.UnsignedInt(2), + } + + testCases := []struct { + name string + opts []bundle.Option + bundleTypeCode cpb.BundleTypeCode_Value + wantBundle *bcrpb.Bundle + }{ + { + name: "no options", + opts: nil, + bundleTypeCode: cpb.BundleTypeCode_TRANSACTION, + wantBundle: baseBundle, + }, + { + name: "empty entries", + opts: []bundle.Option{bundle.WithEntries()}, + bundleTypeCode: cpb.BundleTypeCode_TRANSACTION, + wantBundle: baseBundle, + }, + { + name: "multiple entries in a transaction bundle", + opts: []bundle.Option{bundle.WithEntries(firstEntry, secondEntry)}, + bundleTypeCode: cpb.BundleTypeCode_TRANSACTION, + wantBundle: fullTxnBundle, + }, + { + name: "multiple entries in a search bundle", + opts: []bundle.Option{bundle.WithEntries(firstEntry, secondEntry)}, + bundleTypeCode: cpb.BundleTypeCode_SEARCHSET, + wantBundle: fullSearchBundle, + }, + { + name: "timestamp", + opts: []bundle.Option{bundle.WithTimestamp(iuTime)}, + bundleTypeCode: cpb.BundleTypeCode_TRANSACTION, + wantBundle: timeBundle, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bundle := bundle.New(tc.bundleTypeCode, tc.opts...) + + got, want := bundle, tc.wantBundle + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Bundle(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestTransactionBundle_WithNoArguments_CreatesEmptyTransactionBundle(t *testing.T) { + want := cpb.BundleTypeCode_TRANSACTION + + bundle := bundle.NewTransaction() + + got := bundle.GetType().GetValue() + if got != want { + t.Errorf("TransactionBundle: got %v, want %v", got, want) + } +} + +func TestCollectionBundle_WithNoArguments_CreatesEmptyCollectionBundle(t *testing.T) { + want := cpb.BundleTypeCode_COLLECTION + + bundle := bundle.NewCollection() + + got := bundle.GetType().GetValue() + if got != want { + t.Errorf("CollectionBundle: got %v, want %v", got, want) + } +} + +func TestBatchBundle_WithNoArguments_CreatesEmptyBatchBundle(t *testing.T) { + want := cpb.BundleTypeCode_BATCH + + bundle := bundle.NewBatch() + + got := bundle.GetType().GetValue() + if got != want { + t.Errorf("BatchBundle: got %v, want %v", got, want) + } +} + +func TestHistoryBundle_WithNoArguments_CreatesEmptyHistoryBundle(t *testing.T) { + want := cpb.BundleTypeCode_HISTORY + + bundle := bundle.NewHistory() + + got := bundle.GetType().GetValue() + if got != want { + t.Errorf("HistoryBundle: got %v, want %v", got, want) + } +} + +func TestUnwrapBundle(t *testing.T) { + testCases := []struct { + name string + want []fhir.Resource + }{ + {"Empty", []fhir.Resource{}}, + {"Contains resources", []fhir.Resource{ + fhirtest.NewResource(t, resource.Patient), + fhirtest.NewResource(t, resource.Patient), + }}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + put := func(resource fhir.Resource) *bcrpb.Bundle_Entry { + return bundle.NewPutEntry(resource) + } + sut := bundle.NewCollection( + bundle.WithEntries(slices.Map(tc.want, put)...), + ) + + got := bundle.Unwrap(sut) + + want := tc.want + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("UnwrapBundle: diff (-got,+want):\n%v", diff) + } + }) + } +} + +func TestUnwrapMap(t *testing.T) { + patientOne := fhirtest.NewResource(t, resource.Patient) + patientTwo := fhirtest.NewResource(t, resource.Patient) + taskOne := fhirtest.NewResource(t, resource.Task) + taskTwo := fhirtest.NewResource(t, resource.Task) + testCases := []struct { + name string + in []fhir.Resource + want map[resource.Type][]fhir.Resource + }{ + {"Empty", []fhir.Resource{}, map[resource.Type][]fhir.Resource{}}, + {"Contains unique resource types", []fhir.Resource{ + patientOne, + taskOne, + }, map[resource.Type][]fhir.Resource{ + resource.Patient: {patientOne}, + resource.Task: {taskOne}, + }}, + {"Contains multiple of a single type", []fhir.Resource{ + patientOne, + patientTwo, + }, map[resource.Type][]fhir.Resource{ + resource.Patient: {patientOne, patientTwo}, + }}, + {"Contains multiple of multiple types", []fhir.Resource{ + patientOne, + patientTwo, + taskOne, + taskTwo, + }, map[resource.Type][]fhir.Resource{ + resource.Task: {taskOne, taskTwo}, + resource.Patient: {patientOne, patientTwo}, + }}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + put := func(resource fhir.Resource) *bcrpb.Bundle_Entry { + return bundle.NewPutEntry(resource) + } + sut := bundle.NewCollection( + bundle.WithEntries(slices.Map(tc.in, put)...), + ) + + got := bundle.UnwrapMap(sut) + + want := tc.want + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("UnwrapBundle: diff (-got,+want):\n%v", diff) + } + }) + } +} diff --git a/internal/bundle/identity.go b/internal/bundle/identity.go new file mode 100644 index 0000000..703fd04 --- /dev/null +++ b/internal/bundle/identity.go @@ -0,0 +1,27 @@ +package bundle + +import ( + "errors" + + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/verily-src/fhirpath-go/internal/element/reference" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +var ( + ErrNoLocation = errors.New("bundle entry response missing location") +) + +// IdentityOfResponse returns a complete Identity +// (Type and ID always set, VersionID set if applicable) representing the +// location contained in the given bundle entry response. +func IdentityOfResponse(response *bcrpb.Bundle_Entry_Response) (*resource.Identity, error) { + // Per the FHIR spec, location may be a relative or absolute URI, which may + // include a _history component. + location := response.GetLocation().GetValue() + if location == "" { + return nil, ErrNoLocation + } + + return reference.IdentityFromURL(location) +} diff --git a/internal/bundle/identity_test.go b/internal/bundle/identity_test.go new file mode 100644 index 0000000..20fbc13 --- /dev/null +++ b/internal/bundle/identity_test.go @@ -0,0 +1,48 @@ +package bundle_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/bundle" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +func newIdentity(t *testing.T, typeName, id, version string) *resource.Identity { + t.Helper() + ident, err := resource.NewIdentity(typeName, id, version) + if err != nil { + t.Fatalf("NewIdentity: %v", err) + } + return ident +} + +func TestIdentityOfResponse_NoLocation_ReturnsError(t *testing.T) { + response := &bcrpb.Bundle_Entry_Response{} + _, err := bundle.IdentityOfResponse(response) + + if !cmp.Equal(err, bundle.ErrNoLocation, cmpopts.EquateErrors()) { + t.Errorf("IdentityOfResponse error got %v, want nil", err) + } +} + +func TestIdentityOfResponse(t *testing.T) { + wantIdentity := newIdentity(t, "Patient", "123", "abc") + response := &bcrpb.Bundle_Entry_Response{ + Location: &dtpb.Uri{ + Value: "https://healthcare.googleapis.com/v1/projects/123/locations/abc/datasets/def/fhirStores/ghi/fhir/Patient/123/_history/abc", + }, + } + + ident, err := bundle.IdentityOfResponse(response) + + if err != nil { + t.Fatalf("IdentityOfResponse error got %v, want nil", err) + } + if got, want := ident, wantIdentity; !got.Equal(want) { + t.Errorf("IdentityOfResponse got %s, want %s", got, want) + } +} diff --git a/internal/bundleopt/bundleopt.go b/internal/bundleopt/bundleopt.go new file mode 100644 index 0000000..5299881 --- /dev/null +++ b/internal/bundleopt/bundleopt.go @@ -0,0 +1,25 @@ +package bundleopt + +import ( + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" +) + +// BundleOption is an option interface for constructing bundles +// from raw data. +type BundleOption interface { + updateBundle(bundle *bcrpb.Bundle) +} + +type Transform func(b *bcrpb.Bundle) + +func (t Transform) updateBundle(entry *bcrpb.Bundle) { + t(entry) +} + +func Apply(bundle *bcrpb.Bundle, opts ...BundleOption) { + for _, opt := range opts { + opt.updateBundle(bundle) + } +} + +var _ BundleOption = (*Transform)(nil) diff --git a/internal/containedresource/contained_resource.go b/internal/containedresource/contained_resource.go new file mode 100644 index 0000000..033fefa --- /dev/null +++ b/internal/containedresource/contained_resource.go @@ -0,0 +1,186 @@ +package containedresource + +import ( + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element/identifier" + "github.com/verily-src/fhirpath-go/internal/resource" + "github.com/verily-src/fhirpath-go/internal/protofields" + "google.golang.org/protobuf/reflect/protoreflect" +) + +var ( + ErrGenerateIfNoneExist error = errors.New("GenerateIfNoneExist()") +) + +// Wrap creates a ContainedResource proto based on an existing FHIR proto. +// Usage: +// +// patient := &Patient{...} +// cr := Wrap(patient) +func Wrap(res fhir.Resource) *bcrpb.ContainedResource { + if res == nil { + return nil + } + name := resource.TypeOf(res) + + cr := &bcrpb.ContainedResource{} + + // This field exists for ALL valid FHIR R4 "Resource" types + field := protofields.Resources[string(name)].ContainedResource.Resource + + // The only way this can fail is if `resource` was not a valid FHIR resource + // to begin with (e.g. a custom mock, the r4 "Resource" proto (which is meant + // to only be a baste type), or some other invalid resource). This has been + // extensively tested against all valid r4 protos, and thus if this happens, it + // is best to fail early and panic, rather than make this API virally return an + // error that is impossible to experience in practice, and very difficult to + // actually trigger. + if field == nil { + panic(fmt.Sprintf("Invalid resource with name %v specified in ContainResource", name)) + } + + cr.ProtoReflect().Set(field, protoreflect.ValueOfMessage(res.ProtoReflect())) + return cr +} + +// Unwrap will extract the underlying value contained in this +// resource, if there is one, and return it. This enables downstream callers to +// switch off of the resource type, or to perform type-conversions. +// +// This function is effectively the inverse of `ContainedResource`, such that the +// following assertion will always hold: +// `proto.Equal(fhirutil.Unwrap(fhirutil.ContainedResource(resource)), resource)` +func Unwrap(cr *bcrpb.ContainedResource) fhir.Resource { + field := getContainedResourceOneOfField(cr) + + // If field is nil, it means we have no contained resource value set -- so + // the only valid value to return while unwrapping is `nil`. + if field == nil { + return nil + } + ref := cr.ProtoReflect() + message := ref.Get(field).Message() + + // All ContainedType values valid for the OneOf definition MUST satisfy the + // resource interface. This assertion can not fail. + return message.Interface().ProtoReflect().Interface().(fhir.Resource) +} + +// TypeOf is a helper for getting the type-name of a contained resource. +// +// If the contained resource is nil, or the contained resource does not contain +// any resource, this function will panic. +func TypeOf(cr *bcrpb.ContainedResource) resource.Type { + return resource.TypeOf(Unwrap(cr)) +} + +// ID is a helper for getting the ID of a contained resource. +// +// If the contained resource is nil, or the contained resource does not contain +// any resource, this will return an empty string. +func ID(cr *bcrpb.ContainedResource) string { + return resource.ID(Unwrap(cr)) +} + +// VersionID gets the version-ID of the specified contained-resource as a string. +// If `nil` is provided, this returns an empty string. +func VersionID(cr *bcrpb.ContainedResource) string { + return resource.VersionID(Unwrap(cr)) +} + +// URI is a helper for getting the URI of a contained-resource as a FHIR URI object. +// The URI is returned in the format Type/ID, e.g. Patient/123. +func URI(cr *bcrpb.ContainedResource) *dtpb.Uri { + return resource.URI(Unwrap(cr)) +} + +// URIString is a helper for getting the URI of a contained-resource in +// string form. The URI is returned in the format Type/ID, e.g. Patient/123. +func URIString(cr *bcrpb.ContainedResource) string { + return resource.URIString(Unwrap(cr)) +} + +// VersionedURI is a helper for getting the URI of a contained-resource as a +// FHIR URI object. The URI is returned in the format Type/ID/_history/VERSION. +func VersionedURI(cr *bcrpb.ContainedResource) *dtpb.Uri { + return resource.VersionedURI(Unwrap(cr)) +} + +// VersionedURIString is a helper for getting the URI of a contained-resource in +// string form. The URI is returned in the format Type/ID/_history/VERSION. +func VersionedURIString(cr *bcrpb.ContainedResource) (string, bool) { + return resource.VersionedURIString(Unwrap(cr)) +} + +// getContainedResourceOneOfField gets the field for the OneOf entry in the +// ContainedResource. This function returns nil if either the contained-resource +// is nil, or the contained-resource does not contain any resource. +func getContainedResourceOneOfField(cr *bcrpb.ContainedResource) protoreflect.FieldDescriptor { + if cr == nil { + return nil + } + if cr.GetOneofResource() == nil { + return nil + } + + // Get the active OneOf field, and return that value as an interface + const oneofField = "oneof_resource" + + reflect := cr.ProtoReflect() + descriptor := reflect.Descriptor() + oneof := descriptor.Oneofs().ByName(oneofField) + return reflect.WhichOneof(oneof) +} + +// GenerateIfNoneExist generates an If-None-Exist header value using a single +// Identifier from the contained resource. The provided system is used to +// filter identifiers to only an identifier with a matching system. +// +// If no matching Identifier is found, return error if emptyIsErr is true, or +// return empty string and no error if emptyIsErr is false. +// +// The GCP FHIR store only supports atomic conditional operations on a single +// identifier, so this function returns an error if there are multiple +// identifiers matching the query. +// +// This is used for FHIR conditional create or other conditional methods. +// Untrusted data in Identifiers is escaped both for FHIR and for URL safety. +func GenerateIfNoneExist(cr *bcrpb.ContainedResource, system string, emptyIsErr bool) (string, error) { + if cr == nil { + return "", fmt.Errorf("%w: ContainedResource is nil", ErrGenerateIfNoneExist) + } + + res := Unwrap(cr) + if res == nil { + return "", fmt.Errorf("%w: Unwrap() returned nil / no contained resource", ErrGenerateIfNoneExist) + } + + identifiers, err := resource.GetIdentifierList(res) + if err != nil { + return "", err + } + + found := slices.Filter(identifiers, func(id *dtpb.Identifier) bool { + return id.GetSystem().GetValue() == system + }) + + if len(found) == 0 { + if emptyIsErr { + return "", fmt.Errorf("%w: found no Identifiers with system=%#v", ErrGenerateIfNoneExist, system) + } else { + return "", nil + } + } + + if len(found) > 1 { + return "", fmt.Errorf("%w: found multiple Identifiers with system=%#v, want just one", ErrGenerateIfNoneExist, system) + } + + return identifier.GenerateIfNoneExist(found[0]), nil +} diff --git a/internal/containedresource/contained_resource_example_test.go b/internal/containedresource/contained_resource_example_test.go new file mode 100644 index 0000000..c1f28d4 --- /dev/null +++ b/internal/containedresource/contained_resource_example_test.go @@ -0,0 +1,88 @@ +package containedresource_test + +import ( + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/containedresource" + "google.golang.org/protobuf/proto" +) + +func ExampleWrap() { + patient := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + } + + cr := containedresource.Wrap(patient) + + fmt.Printf("Patient ID = %v", cr.GetPatient().GetId().GetValue()) + // Output: Patient ID = 12345 +} + +func ExampleUnwrap() { + patient := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + } + cr := containedresource.Wrap(patient) + + unwrapped := containedresource.Unwrap(cr).(*patient_go_proto.Patient) + + if proto.Equal(patient, unwrapped) { + fmt.Printf("Resources match!") + } + // Output: Resources match! +} + +func ExampleTypeOf() { + patient := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + } + cr := containedresource.Wrap(patient) + + crType := containedresource.TypeOf(cr) + + fmt.Printf("Contained Resource type = %v", crType) + // Output: Contained Resource type = Patient +} + +func ExampleID() { + patient := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + } + cr := containedresource.Wrap(patient) + + id := containedresource.ID(cr) + + fmt.Printf("Contained Resource ID = %v", id) + // Output: Contained Resource ID = 12345 +} + +func ExampleGenerateIfNoneExist() { + patient := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + Identifier: []*dtpb.Identifier{ + &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, + }, + }, + } + + cr := containedresource.Wrap(patient) + + value, err := containedresource.GenerateIfNoneExist(cr, "http://fake.com", true) + if err != nil { + panic(err) + } + + headers := map[string]string{} + + if value != "" { + headers["If-None-Exist"] = value + } + + fmt.Printf("If-None-Exist: %v", headers["If-None-Exist"]) + // Output: If-None-Exist: identifier=http%3A%2F%2Ffake.com%7C9efbf82d-7a58-4d14-bec1-63f8fda148a8 +} diff --git a/internal/containedresource/contained_resource_test.go b/internal/containedresource/contained_resource_test.go new file mode 100644 index 0000000..239470b --- /dev/null +++ b/internal/containedresource/contained_resource_test.go @@ -0,0 +1,291 @@ +package containedresource_test + +import ( + "errors" + "net/url" + "strings" + "testing" + + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/verily-src/fhirpath-go/internal/containedresource" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/proto" +) + +func TestWrap_WithNilContainedResource_ReturnsNil(t *testing.T) { + got := containedresource.Wrap(nil) + + if got != nil { + t.Errorf("ContainedResource: got %v, want nil", got) + } +} + +func TestWrap_WithResource_WrapsResource(t *testing.T) { + for name, resource := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + + got := containedresource.Wrap(resource) + if got == nil { + t.Fatalf("ContainedResource(%v): got nil, want value", name) + } + + if got, want := containedresource.ID(got), resource.GetId().GetValue(); got != want { + t.Errorf("ContainedResource(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestUnwrap_WithInvalidresource_ReturnsNil(t *testing.T) { + testCases := []struct { + name string + resource *bcrpb.ContainedResource + }{ + {"NilContainedResource", nil}, + // Note: ContainedResource is not a real FHIR type, and thus should never + // be populated as empty in practice when deserializing or receiving valid + // FHIR payloads; so this test should never happen to begin with. + {"EmptyContainedResource", &bcrpb.ContainedResource{}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := containedresource.Unwrap(nil) + + if got != nil { + t.Errorf("Unwrap(%v): got %v, want nil", tc.name, got) + } + }) + } +} + +func TestUnwrap_WithContainedResource_ReturnsResource(t *testing.T) { + for name, resource := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + got := containedresource.Unwrap(containedresource.Wrap(resource)) + + if !proto.Equal(got, resource) { + t.Errorf("Unwrap(%v): got %v, want %v", name, got, resource) + } + }) + } +} + +func TestTypeOf_WithNil_Panics(t *testing.T) { + defer func() { _ = recover() }() + + containedresource.TypeOf(nil) + + t.Errorf("TypeOf: expected panic") +} + +func TestTypeOf_WithEmptyContainedResource_Panics(t *testing.T) { + defer func() { _ = recover() }() + cr := &bcrpb.ContainedResource{} + + containedresource.TypeOf(cr) + + t.Errorf("TypeOf: expected panic") +} + +func TestTypeOf_WithResource_ReturnsName(t *testing.T) { + for name, res := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + got := containedresource.TypeOf(containedresource.Wrap(res)) + + if got, want := got, resource.Type(name); got != want { + t.Errorf("ContainedResourceName(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestID_WithNil_ReturnsEmptyString(t *testing.T) { + got := containedresource.ID(nil) + + if got != "" { + t.Errorf("ID: got %v, want empty string", got) + } +} + +func TestID_WithEmptyContainedResource_ReturnsEmptyString(t *testing.T) { + cr := &bcrpb.ContainedResource{} + + got := containedresource.ID(cr) + + if got != "" { + t.Errorf("ContainedResourceID: got %v, want empty string", got) + } +} + +func TestID_WithResource_ReturnsID(t *testing.T) { + for name, resource := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + got := containedresource.ID(containedresource.Wrap(resource)) + + if got, want := got, resource.GetId().GetValue(); got != want { + t.Errorf("ContainedResourceID(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestGenerateIfNoneExist_Errors(t *testing.T) { + patient0 := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + } + patient1 := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + Identifier: []*dtpb.Identifier{ + &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, + }, + }, + } + + testCases := []struct { + name string + input *bcrpb.ContainedResource + wantErr string + }{ + { + "empty ContainedResource", + &bcrpb.ContainedResource{}, + "Unwrap() returned nil / no contained resource", + }, + { + "nil ContainedResource", + nil, + "ContainedResource is nil", + }, + { + "No identifier, emptyIsErr=true", + containedresource.Wrap(patient0), + "found no Identifiers", + }, + { + "No matching identifier, emptyIsErr=true", + containedresource.Wrap(patient1), + "found no Identifiers", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + header, err := containedresource.GenerateIfNoneExist(tc.input, "no-such-system", true) + + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("got error %#v, want %#v", err.Error(), tc.wantErr) + } + if !errors.Is(err, containedresource.ErrGenerateIfNoneExist) { + t.Errorf("got error %#v, want errors.Is(..., ErrGenerateIfNoneExist)", err) + } + if header != "" { + t.Errorf("got %v, want empty string", header) + } + }) + } +} + +func TestGenerateIfNoneExist(t *testing.T) { + patient0 := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + } + patient1 := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + Identifier: []*dtpb.Identifier{ + &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, + }, + }, + } + patient2 := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + Identifier: []*dtpb.Identifier{ + &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, + }, + &dtpb.Identifier{ + Use: &dtpb.Identifier_UseCode{ + Value: codes_go_proto.IdentifierUseCode_USUAL, + }, + System: &dtpb.Uri{Value: "urn:oid:2.16.840.1.113883.2.4.6.3"}, + Value: &dtpb.String{Value: "12345"}, + }, + }, + } + + patient3 := fhirtest.NewResource(t, "Patient", fhirtest.WithGeneratedIdentifier("http://example.com/fake-id")).(*patient_go_proto.Patient) + + patient4 := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + Identifier: []*dtpb.Identifier{ + &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "foo,bar,baz|omg"}, + }, + }, + } + + patient5 := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + Identifier: []*dtpb.Identifier{ + &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, + }, + &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "7d541708-b068-4347-a8dc-cce1dcdb5314"}, + }, + }, + } + + testCases := []struct { + name string + patient *patient_go_proto.Patient + system string + want string + wantErr string + }{ + {"Patient with no Identifier, emptyIsErr false", patient0, "system", "", ""}, + {"Patient with single Identifier", patient1, "http://fake.com", "identifier=" + url.QueryEscape("http://fake.com|9efbf82d-7a58-4d14-bec1-63f8fda148a8"), ""}, + {"Patient with two Identifiers but one matching", patient2, "http://fake.com", "identifier=" + url.QueryEscape("http://fake.com|9efbf82d-7a58-4d14-bec1-63f8fda148a8"), ""}, + {"Patient with two Identifiers and both matching", patient5, "http://fake.com", "", "found multiple Identifiers"}, + {"Patient with generated ID", patient3, "http://example.com/fake-id", "identifier=" + url.QueryEscape("http://example.com/fake-id|"+patient3.Identifier[0].Value.Value), ""}, + {"Special chars in Identifier", patient4, "http://fake.com", "identifier=" + url.QueryEscape(`http://fake.com|foo\,bar\,baz\|omg`), ""}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cr := containedresource.Wrap(tc.patient) + + got, err := containedresource.GenerateIfNoneExist(cr, tc.system, false) + + if tc.wantErr == "" { + if err != nil { + t.Errorf("%#v: Bad If-None-Exist:\n got err %#v\n want nil", tc.name, err) + } + } else { + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("%#v: got error %#v, want %#v", tc.name, err.Error(), tc.wantErr) + } + if !errors.Is(err, containedresource.ErrGenerateIfNoneExist) { + t.Errorf("%#v: got error %#v, want errors.Is(..., ErrGenerateIfNoneExist)", tc.name, err) + } + } + + if got != tc.want { + t.Errorf("%#v: Bad If-None-Exist:\n got %#v\n want %#v", tc.name, got, tc.want) + } + }) + } +} diff --git a/internal/element/canonical/canonical.go b/internal/element/canonical/canonical.go new file mode 100644 index 0000000..f278c76 --- /dev/null +++ b/internal/element/canonical/canonical.go @@ -0,0 +1,151 @@ +package canonical + +import ( + "errors" + "fmt" + "regexp" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +var ( + // ErrNoCanonicalURL is an error returned when an API receives a canonical that + // does not have a specified URL field. + ErrNoCanonicalURL = errors.New("canonical does not contain url") + + // very basic regex for URL matching. + // Fragment portion is from https://build.fhir.org/references.html#literal + canonicalRegExp = regexp.MustCompile(`^(?P<url>[^|#]+)(\|(?P<version>[A-z0-9-_\.]+))?(#(?P<fragment>[A-z0-9-_\.]{1,64}))?`) +) + +// canonicalConfig is an internal struct for holding canonical information that +// can be updated from a CanonicalOpt. +type canonicalConfig struct { + fragment, version string +} + +// Option is an option interface for constructing canonicals from raw +// data. +type Option interface { + updateCanonical(data *canonicalConfig) +} + +// WithFragment adds a "fragment" portion to Canonical references. +func WithFragment(frag string) Option { + return canonicalFragOpt(frag) +} + +// canonicalFragOpt is a simple canonical option for fragment strings. +type canonicalFragOpt string + +func (o canonicalFragOpt) updateCanonical(data *canonicalConfig) { + data.fragment = string(o) +} + +// WithVersion adds a "version" portion to Canonical references. +func WithVersion(version string) Option { + return canonicalVersionOpt(version) +} + +// canonicalVersionOpt is a simple canonical option for version strings. +type canonicalVersionOpt string + +func (o canonicalVersionOpt) updateCanonical(data *canonicalConfig) { + data.version = string(o) +} + +// New constructs an R4 FHIR New element from the specified +// url string and canonical options. +// +// See: http://hl7.org/fhir/R4/datatypes.html#canonical +func New(url string, opts ...Option) *dtpb.Canonical { + data := &canonicalConfig{} + for _, opt := range opts { + opt.updateCanonical(data) + } + if data.version != "" { + url = fmt.Sprintf("%v|%v", url, data.version) + } + if data.fragment != "" { + url = fmt.Sprintf("%v#%v", url, data.fragment) + } + return &dtpb.Canonical{ + Value: url, + } +} + +// FromResource creates an R4 FHIR FromResource element from a +// resource that has a URL, such as a Questionnaire, Device, etc. +// +// If the input resource is nil, or if the resource does not have a URL +// field assigned, this function will return the error `ErrNoCanonicalURL`. +// +// See: https://hl7.org/fhir/R4/datatypes.html#canonical and +// https://hl7.org/fhir/R4/references.html#canonical +func FromResource(resource fhir.CanonicalResource) (*dtpb.Canonical, error) { + if resource == nil || resource.GetUrl() == nil { + return nil, ErrNoCanonicalURL + } + return New(resource.GetUrl().GetValue()), nil +} + +// FragmentFromResource creates an R4 FHIR Canonical element from a resource that +// has a URL, such as a Questionnaire, Device, etc., and will mark it as a +// fragment-reference. +// +// If the input resource is nil, or if the resource does not have a URL +// field assigned, this function will return the error `ErrNoCanonicalURL`. +// +// See: https://hl7.org/fhir/R4/datatypes.html#canonical and +// https://hl7.org/fhir/R4/references.html#canonical +func FragmentFromResource(resource fhir.CanonicalResource) (*dtpb.Canonical, error) { + if resource == nil || resource.GetUrl() == nil { + return nil, ErrNoCanonicalURL + } + return New(resource.GetUrl().GetValue(), WithFragment(resource.GetId().GetValue())), nil +} + +// VersionedFromResource creates an R4 FHIR Canonical element from a resource that +// has a URL, such as a Questionnaire, Device, etc, along with a version string. +// +// If the input resource is nil, or if the resource does not have a URL +// field assigned, this function will return the error `ErrNoCanonicalURL`. +// +// See: https://hl7.org/fhir/R4/datatypes.html#canonical and +// https://hl7.org/fhir/R4/references.html#canonical +func VersionedFromResource(resource fhir.CanonicalResource) (*dtpb.Canonical, error) { + if resource == nil || resource.GetUrl() == nil { + return nil, ErrNoCanonicalURL + } + url := resource.GetUrl() + version := resource.GetVersion() + if version == nil { + return New(url.GetValue()), nil + } + return New(resource.GetUrl().GetValue(), WithVersion(version.GetValue())), nil +} + +// IdentityFromReference returns an Identity object from a given canonical reference +// Replaces: ph.ParseCanonical +func IdentityFromReference(c *dtpb.Canonical) (*resource.CanonicalIdentity, error) { + value := c.GetValue() + match := canonicalRegExp.FindStringSubmatch(value) + result := make(map[string]string) + for i, name := range canonicalRegExp.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + return resource.NewCanonicalIdentity(result["url"], result["version"], result["fragment"]) +} + +// IdentityOf returns a canonicalIdentity representing the given canonical resource +func IdentityOf(res fhir.CanonicalResource) (*resource.CanonicalIdentity, error) { + if res == nil || res.GetUrl() == nil { + return nil, ErrNoCanonicalURL + } + + return resource.NewCanonicalIdentity(res.GetUrl().GetValue(), res.GetVersion().GetValue(), "") +} diff --git a/internal/element/canonical/canonical_test.go b/internal/element/canonical/canonical_test.go new file mode 100644 index 0000000..2299a40 --- /dev/null +++ b/internal/element/canonical/canonical_test.go @@ -0,0 +1,221 @@ +package canonical_test + +import ( + "errors" + "fmt" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + qpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_go_proto" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/element/canonical" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestNew(t *testing.T) { + const ( + url = "https://example.com" + version = "1.2.3" + fragment = "frag" + ) + testCases := []struct { + name string + opts []canonical.Option + want string + }{ + { + name: "NoOpts", + opts: nil, + want: url, + }, + { + name: "WithVersion", + opts: []canonical.Option{canonical.WithVersion(version)}, + want: fmt.Sprintf("%v|%v", url, version), + }, + { + name: "WithFragment", + opts: []canonical.Option{canonical.WithFragment(fragment)}, + want: fmt.Sprintf("%v#%v", url, fragment), + }, + { + name: "WithVersionAndFragment", + opts: []canonical.Option{canonical.WithVersion(version), canonical.WithFragment(fragment)}, + want: fmt.Sprintf("%v|%v#%v", url, version, fragment), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := canonical.New(url, tc.opts...) + + if got, want := got.GetValue(), tc.want; got != want { + t.Errorf("Canonical(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestCanonicalFromResource_Nil_ReturnsError(t *testing.T) { + _, err := canonical.FromResource(nil) + + if got, want := err, canonical.ErrNoCanonicalURL; !errors.Is(got, want) { + t.Errorf("CanonicalFromResource: got %v, want %v", got, want) + } +} + +func makeCanonicalResource() fhir.CanonicalResource { + const ( + url = "https://example.com" + ) + return &qpb.Questionnaire{ + Id: fhir.ID("0xdeadbeef"), + Url: fhir.URI(url), + Version: fhir.String("1.0.0"), + } +} + +func TestCanonicalFromResource_ReturnsCanonicalWithUrl(t *testing.T) { + resource := makeCanonicalResource() + + got, err := canonical.FromResource(resource) + + if err != nil { + t.Fatalf("CanonicalFromResource: unexpected error: %v", err) + } + if got, want := got.GetValue(), resource.GetUrl().GetValue(); got != want { + t.Errorf("CanonicalFromResource: got '%v', want '%v'", got, want) + } +} + +func TestVersionedFromResource_Nil_ReturnsNil(t *testing.T) { + _, err := canonical.VersionedFromResource(nil) + + if got, want := err, canonical.ErrNoCanonicalURL; !errors.Is(got, want) { + t.Errorf("VersionedFromResource: got %v, want %v", got, want) + } +} + +func TestVersionedCanonical_ReturnsCanonicalWithUrl(t *testing.T) { + const ( + url = "https://example.com" + version = "1.2.3" + ) + testCases := []struct { + name string + resource fhir.CanonicalResource + want string + }{ + { + name: "NoVersion", + resource: &qpb.Questionnaire{ + Url: fhir.URI(url), + }, + want: url, + }, + { + name: "WithVersion", + resource: &qpb.Questionnaire{ + Url: fhir.URI(url), + Version: fhir.String(version), + }, + want: fmt.Sprintf("%v|%v", url, version), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := canonical.VersionedFromResource(tc.resource) + + if err != nil { + t.Fatalf("VersionedCanonical(%v): unexpected error: %v", tc.name, err) + } + if got, want := got.GetValue(), tc.want; got != want { + t.Errorf("VersionedCanonical(%v): got '%v', want '%v'", tc.name, got, want) + } + }) + } +} + +func TestFragmentFromResourceFromResourceNil_ReturnsNil(t *testing.T) { + _, err := canonical.FragmentFromResource(nil) + + if got, want := err, canonical.ErrNoCanonicalURL; !errors.Is(got, want) { + t.Errorf("CanonicalFragmentFromResource: got %v, want %v", got, want) + } +} + +func TestFragmentFromResource(t *testing.T) { + resource := makeCanonicalResource() + want := fmt.Sprintf("%v#%v", resource.GetUrl().GetValue(), resource.GetId().GetValue()) + + got, err := canonical.FragmentFromResource(resource) + + if err != nil { + t.Fatalf("FragmentFromResource: unexpected error: %v", err) + } + if got := got.GetValue(); got != want { + t.Errorf("FragmentFromResource: got '%v', want '%v'", got, want) + } +} + +func TestIdentityFromCanonical(t *testing.T) { + testCases := []struct { + name, reference, url, version, fragment string + }{ + { + name: "basic", + url: "http://someurl/test-value", + reference: "http://someurl/test-value", + }, + { + name: "long url", + url: "https://fhir.acme.com/Questionnaire/example", + reference: "https://fhir.acme.com/Questionnaire/example", + }, + { + name: "with version", + url: "https://fhir.acme.com/Questionnaire/example", + version: "1.0.0", + reference: "https://fhir.acme.com/Questionnaire/example|1.0.0", + }, + { + name: "with fragment", + url: "http://hl7.org/fhir/ValueSet/my-valueset", + fragment: "vs1", + reference: "http://hl7.org/fhir/ValueSet/my-valueset#vs1", + }, + { + name: "with version and fragment", + url: "http://fhir.acme.com/Questionnaire/example", + version: "1.0", + fragment: "vs1", + reference: "http://fhir.acme.com/Questionnaire/example|1.0#vs1", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := &dtpb.Canonical{ + Value: tc.reference, + } + got, err := canonical.IdentityFromReference(c) + if err != nil { + t.Fatalf("IdentityFromReference(%s): unexpected error: %v", tc.name, err) + } + want := &resource.CanonicalIdentity{ + Url: tc.url, + Version: tc.version, + Fragment: tc.fragment, + } + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("IdentityFromReference(%s): %v", tc.name, diff) + } + if diff := cmp.Diff(tc.reference, got.String()); diff != "" { + t.Errorf("IdentityFromReference(%s).String: %v", tc.name, diff) + } + }) + } +} diff --git a/internal/element/codeableconcept/codeableconcept.go b/internal/element/codeableconcept/codeableconcept.go new file mode 100644 index 0000000..3628fcc --- /dev/null +++ b/internal/element/codeableconcept/codeableconcept.go @@ -0,0 +1,17 @@ +package codeableconcept + +import ( + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" +) + +// FindBySystem searches the slice of Codings within a CodeableConcept for the +// first Coding that contains the given system. +func FindCodingBySystem(codeableconcept *dtpb.CodeableConcept, system string) *dtpb.Coding { + for _, coding := range codeableconcept.GetCoding() { + codingSystem := coding.GetSystem() + if codingSystem.GetValue() == system { + return coding + } + } + return nil +} diff --git a/internal/element/coding/coding.go b/internal/element/coding/coding.go new file mode 100644 index 0000000..88345d6 --- /dev/null +++ b/internal/element/coding/coding.go @@ -0,0 +1,20 @@ +package coding + +import ( + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" +) + +// FindBySystem searches a slice of codings for the first coding that +// contains the specified system. +func FindBySystem(codings []*dtpb.Coding, system string) *dtpb.Coding { + for _, coding := range codings { + codingSystem := coding.GetSystem() + if codingSystem == nil { + continue + } + if codingSystem.GetValue() == system { + return coding + } + } + return nil +} diff --git a/internal/element/element.go b/internal/element/element.go new file mode 100644 index 0000000..5bd5545 --- /dev/null +++ b/internal/element/element.go @@ -0,0 +1,6 @@ +/* +Package element defines subpackages focused on FHIR R4 elements. Each subpackage +is named after the FHIR data type (e.g. identifier) and provides capabilities +related to that type. +*/ +package element diff --git a/internal/element/etag/etag.go b/internal/element/etag/etag.go new file mode 100644 index 0000000..856da2f --- /dev/null +++ b/internal/element/etag/etag.go @@ -0,0 +1,25 @@ +package etag + +import ( + "errors" + "fmt" + "regexp" +) + +var ( + // FHIR version ID follows the [A-Za-z0-9\-\.]{1,64} pattern + // https://build.fhir.org/resource-definitions.html#Meta.versionId + versionIdInEtagRegexp = regexp.MustCompile(`W\/"([A-Za-z0-9\-\.]{1,64})"`) + ErrInvalidEtagVersionID = errors.New("invalid version ID in etag") +) + +// Returns the version ID from an ETag string. +func VersionIDFromEtag(etag string) (string, error) { + matches := versionIdInEtagRegexp.FindStringSubmatch(etag) + // leftmost match of the regular expression in string itself and then the matches, if any. + // We expect the second match to be the version ID. + if len(matches) != 2 { + return "", fmt.Errorf("%w: got version ID: %s", ErrInvalidEtagVersionID, etag) + } + return matches[1], nil +} diff --git a/internal/element/etag/etag_test.go b/internal/element/etag/etag_test.go new file mode 100644 index 0000000..a4e0b0a --- /dev/null +++ b/internal/element/etag/etag_test.go @@ -0,0 +1,58 @@ +package etag_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/element/etag" +) + +func TestVersionIDFromEtag(t *testing.T) { + testCases := []struct { + name string + etag string + wantVersionID string + wantErr error + }{ + { + name: "valid etag", + etag: `W/"foo"`, + wantVersionID: "foo", + wantErr: nil, + }, + { + name: "empty etag", + etag: "", + wantErr: etag.ErrInvalidEtagVersionID, + }, + { + name: "etag without version ID", + etag: `W/""`, + wantErr: etag.ErrInvalidEtagVersionID, + }, + { + name: "etag with version ID with non-ASCII letters", + etag: `W/"?()"`, + wantErr: etag.ErrInvalidEtagVersionID, + }, + { + name: "etag with long version ID", + etag: `W/"12345678901234567890123456789012345678901234567890123456789012345"`, + wantErr: etag.ErrInvalidEtagVersionID, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotVersionID, gotErr := etag.VersionIDFromEtag(tc.etag) + if !cmp.Equal(gotErr, tc.wantErr, cmpopts.EquateErrors()) { + t.Fatalf("VersionIDFromEtag(%s) error mismatch: got [%v], want [%v]", tc.name, gotErr, tc.wantErr) + } + if gotErr == nil && gotVersionID != tc.wantVersionID { + t.Errorf("VersionIDFromEtag(%s) versionID mismatch: got [%s], want [%s]", + tc.name, gotVersionID, tc.wantVersionID) + } + }) + } +} diff --git a/internal/element/extension/extension.go b/internal/element/extension/extension.go new file mode 100644 index 0000000..62863a5 --- /dev/null +++ b/internal/element/extension/extension.go @@ -0,0 +1,232 @@ +package extension + +import ( + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/protofields" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// ErrInvalidValueX is an error reported if trying to create an +// Extension from an invalid data-type. Most FHIR Elements are supported, with +// only a few notable exceptions. See the definition of the ValueX constraint to +// see which elements are valid inputs. +var ErrInvalidValueX = errors.New("invalid extension ValueX type") + +// ValueX is a constraint that enumerates all the valid 'DataType' +// objects that can be used for extensions. This is used to constrain the +// `Extension` function so that it cannot fail. +// +// This type is exported so that consumers may also expose the same generic +// requirements to their clients. +// +// For more information on these types, see the definition of Extension's +// `value` fields here: https://www.hl7.org/fhir/r4/extensibility.html#Extension +type ValueX interface { + fhir.Element + *dtpb.Base64Binary | + *dtpb.Boolean | + *dtpb.Canonical | + *dtpb.Code | + *dtpb.Date | + *dtpb.DateTime | + *dtpb.Decimal | + *dtpb.Id | + *dtpb.Instant | + *dtpb.Integer | + *dtpb.Markdown | + *dtpb.Oid | + *dtpb.PositiveInt | + *dtpb.String | + *dtpb.Time | + *dtpb.UnsignedInt | + *dtpb.Uri | + *dtpb.Url | + *dtpb.Uuid | + *dtpb.Address | + *dtpb.Age | + *dtpb.Annotation | + *dtpb.Attachment | + *dtpb.CodeableConcept | + *dtpb.Coding | + *dtpb.ContactPoint | + *dtpb.Count | + *dtpb.Distance | + *dtpb.Duration | + *dtpb.HumanName | + *dtpb.Identifier | + *dtpb.Money | + *dtpb.Period | + *dtpb.Quantity | + *dtpb.Range | + *dtpb.Ratio | + *dtpb.Reference | + *dtpb.SampledData | + *dtpb.Signature | + *dtpb.Timing | + *dtpb.ContactDetail | + *dtpb.Contributor | + *dtpb.DataRequirement | + *dtpb.Expression | + *dtpb.ParameterDefinition | + *dtpb.RelatedArtifact | + *dtpb.TriggerDefinition | + *dtpb.UsageContext | + *dtpb.Dosage +} + +// New creates an New extension object from a concrete, and legal, +// extension type. Unlike `FromElement`, this function cannot fail since the +// type has been checked ot be valid extension type with Go-1.18 constraints. +func New[T ValueX](uri string, element T) *dtpb.Extension { + ext, err := FromElement(uri, element) + if err != nil { + // This branch is unreachable due to the type constraint. + // Tested in extension_test.go + panic(err) + } + return ext +} + +// FromElement creates an extension from the specified datatype resource. +// If the datatype is not a valid input, this function returns an error. +// +// For convenience functions that cannot return an error, see the various +// `Extension*` functions. +func FromElement(uri string, element fhir.Element) (*dtpb.Extension, error) { + if element == nil { + return nil, fmt.Errorf("no value provided to datatype extension") + } + name := protofields.DescriptorName(element) + + fields, ok := protofields.Elements[name] + if !ok || fields.Extension.ValueX == nil { + return nil, fmt.Errorf("extension %v: %w", name, ErrInvalidValueX) + } + + valueX := &dtpb.Extension_ValueX{} + reflect := valueX.ProtoReflect() + reflect.Set(fields.Extension.ValueX, protoreflect.ValueOfMessage(element.ProtoReflect())) + + extension := &dtpb.Extension{ + Url: fhir.URI(uri), + Value: valueX, + } + return extension, nil +} + +// Unwrap will return the wrapped Element in this extension, if one +// exists. If there is none, or if extension is nil, this returns nil. +func Unwrap(extension *dtpb.Extension) fhir.Element { + if extension == nil || extension.Value.GetChoice() == nil { + return nil + } + const choiceField = "choice" + + reflect := extension.Value.ProtoReflect() + descriptor := reflect.Descriptor() + oneof := descriptor.Oneofs().ByName(choiceField) + field := reflect.WhichOneof(oneof) + message := reflect.Get(field).Message() + + // Extensions can only have a limited number of extension values, all of which + // satisfy 'Element'. This assertion cannot fail unless an invalid oneof value + // has been provided, which violates protobuf definitions (and could never + // legally happen during transport). + return message.Interface().(fhir.Element) +} + +// Clear removes all extensions from the specified FHIR Element +// or Resource. +func Clear(ext fhir.Extendable) { + if ext == nil { + return + } + + message := ext.ProtoReflect() + field := message.Descriptor().Fields().ByName("extension") + message.Clear(field) +} + +// Upsert always replaces or inserts the extension by the URL. +func Upsert(ext fhir.Extendable, extension *dtpb.Extension) { + if ext == nil { + panic("No extendable object specified for Upsert; ext is nil.") + } + + for _, currExt := range ext.GetExtension() { + if currExt.GetUrl().GetValue() == extension.GetUrl().GetValue() { + currMessage := currExt.ProtoReflect() + valueDesc := currMessage.Descriptor().Fields().ByName("value") + currMessage.Set(valueDesc, protoreflect.ValueOfMessage(extension.GetValue().ProtoReflect())) + return + } + } + + AppendInto(ext, extension) +} + +// SetByURL always remove all extensions with url, +// and creates N extensions with url and values... +func SetByURL[T ValueX](ext fhir.Extendable, url string, values ...T) { + if ext == nil { + panic("No extendable object specified for SetByURL; ext is nil.") + } + + var newExtensionList []*dtpb.Extension + + for _, currExt := range ext.GetExtension() { + if currExt.GetUrl().GetValue() != url { + newExtensionList = append(newExtensionList, currExt) + } + } + + for _, val := range values { + newExtensionList = append(newExtensionList, New(url, val)) + } + + Overwrite(ext, newExtensionList...) +} + +// Overwrite modifies a Resource or Element in-place to overwrite all of the +// extensions in the given object with the provided ones. +// +// This function can only be called with extendable resources or data-types, and +// thus does not have any error-cases to surface back to the caller. +// +// This function will panic if ext is nil. +func Overwrite(ext fhir.Extendable, extensions ...*dtpb.Extension) { + updateExtensionsIn(ext, protoreflect.Message.NewField, extensions...) +} + +// AppendInto appends extensions into the specified Resource or Element. +// Unlike most "append" operations, this mutates the source object rather than +// copying and returning a new changed object. This is done to prevent expensive +// copy-operations on large FHIR resources that are being extended. +// +// This function can only be called with extendable resources or data-types, and +// thus does not have any error-cases to surface back to the caller. +// +// This function will panic if ext is nil. +func AppendInto(ext fhir.Extendable, extensions ...*dtpb.Extension) { + updateExtensionsIn(ext, protoreflect.Message.Mutable, extensions...) +} + +// updateExtensionsIn updates the state of extensions inside of the extendable +// resource, using the specified field accessor to help. +func updateExtensionsIn(ext fhir.Extendable, get protofields.FieldToValueFunc, extensions ...*dtpb.Extension) { + if ext == nil { + panic("No extendable object specified for updateExtensionsIn; ext is nil.") + } + message := ext.ProtoReflect() + field := message.Descriptor().Fields().ByName("extension") + extensionsList := get(message, field).List() + for _, ext := range extensions { + val := protoreflect.ValueOfMessage(ext.ProtoReflect()) + extensionsList.Append(val) + } + message.Set(field, protoreflect.ValueOfList(extensionsList)) +} diff --git a/internal/element/extension/extension_example_test.go b/internal/element/extension/extension_example_test.go new file mode 100644 index 0000000..adab42d --- /dev/null +++ b/internal/element/extension/extension_example_test.go @@ -0,0 +1,42 @@ +package extension_test + +import ( + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/task_go_proto" + "github.com/verily-src/fhirpath-go/internal/element/extension" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +func ExampleOverwrite() { + const urlBase = "https://verily-src.github.io/vhp-hds-vvs-fhir-ig/StructureDefinitions" + task := &task_go_proto.Task{ + Extension: []*dtpb.Extension{ + extension.New(fmt.Sprintf("%v/%v", urlBase, "my-int"), fhir.Integer(42)), + }, + } + + extension.Overwrite(task, + extension.New(fmt.Sprintf("%v/%v", urlBase, "my-string"), fhir.String("hello world")), + extension.New(fmt.Sprintf("%v/%v", urlBase, "my-bool"), fhir.Boolean(true)), + ) + fmt.Printf("%v extensions in Task!", len(task.GetExtension())) + // Output: 2 extensions in Task! +} + +func ExampleAppendInto() { + const urlBase = "http://example.com/StructureDefinitions" + task := &task_go_proto.Task{ + Extension: []*dtpb.Extension{ + extension.New(fmt.Sprintf("%v/%v", urlBase, "my-int"), fhir.Integer(42)), + }, + } + + extension.AppendInto(task, + extension.New(fmt.Sprintf("%v/%v", urlBase, "my-string"), fhir.String("hello world")), + extension.New(fmt.Sprintf("%v/%v", urlBase, "my-bool"), fhir.Boolean(true)), + ) + fmt.Printf("%v extensions in Task!", len(task.GetExtension())) + // Output: 3 extensions in Task! +} diff --git a/internal/element/extension/extension_test.go b/internal/element/extension/extension_test.go new file mode 100644 index 0000000..6c0bb4b --- /dev/null +++ b/internal/element/extension/extension_test.go @@ -0,0 +1,446 @@ +package extension_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/element/canonical" + "github.com/verily-src/fhirpath-go/internal/element/extension" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +func isValueXType(element fhir.Element) bool { + switch element.(type) { + case *dtpb.Base64Binary, + *dtpb.Boolean, + *dtpb.Canonical, + *dtpb.Code, + *dtpb.Date, + *dtpb.DateTime, + *dtpb.Decimal, + *dtpb.Id, + *dtpb.Instant, + *dtpb.Integer, + *dtpb.Markdown, + *dtpb.Oid, + *dtpb.PositiveInt, + *dtpb.String, + *dtpb.Time, + *dtpb.UnsignedInt, + *dtpb.Uri, + *dtpb.Url, + *dtpb.Uuid, + *dtpb.Address, + *dtpb.Age, + *dtpb.Annotation, + *dtpb.Attachment, + *dtpb.CodeableConcept, + *dtpb.Coding, + *dtpb.ContactPoint, + *dtpb.Count, + *dtpb.Distance, + *dtpb.Duration, + *dtpb.HumanName, + *dtpb.Identifier, + *dtpb.Money, + *dtpb.Period, + *dtpb.Quantity, + *dtpb.Range, + *dtpb.Ratio, + *dtpb.Reference, + *dtpb.SampledData, + *dtpb.Signature, + *dtpb.Timing, + *dtpb.ContactDetail, + *dtpb.Contributor, + *dtpb.DataRequirement, + *dtpb.Expression, + *dtpb.ParameterDefinition, + *dtpb.RelatedArtifact, + *dtpb.TriggerDefinition, + *dtpb.UsageContext, + *dtpb.Dosage: + return true + } + return false +} + +func TestRoundTrip(t *testing.T) { + for name, element := range fhirtest.Elements { + if !isValueXType(element) { + t.Skip() + } + + t.Run(name, func(t *testing.T) { + ext, err := extension.FromElement("foo", element) + if err != nil { + t.Fatalf("RoundTrip(%v): got unexpected error %v", name, err) + } + + got := extension.Unwrap(ext) + + if got, want := got, element; got != want { + t.Errorf("RoundTrip(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestNew_Base64Binary_ReturnsExtension(t *testing.T) { + const url = "http://example.com" + input := fhir.Base64Binary([]byte{0xde, 0xad, 0xbe, 0xef}) + want := &dtpb.Extension{ + Url: fhir.URI(url), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_Base64Binary{ + Base64Binary: input, + }, + }, + } + + got := extension.New(url, input) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Extension(Base64Binary): (-got,+want):\n%v", diff) + } +} + +func TestNew_Boolean_ReturnsExtension(t *testing.T) { + const url = "http://example.com" + input := fhir.Boolean(true) + want := &dtpb.Extension{ + Url: fhir.URI(url), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_Boolean{ + Boolean: input, + }, + }, + } + + got := extension.New(url, input) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Extension(Boolean): (-got,+want):\n%v", diff) + } +} + +func TestNew_Canonical_ReturnsExtension(t *testing.T) { + const url = "http://example.com" + input := canonical.New(url) + want := &dtpb.Extension{ + Url: fhir.URI(url), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_Canonical{ + Canonical: input, + }, + }, + } + + got := extension.New(url, input) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Extension(Boolean): (-got,+want):\n%v", diff) + } +} +func TestNew_Address_ReturnsExtension(t *testing.T) { + const url = "http://example.com" + input := &dtpb.Address{ + Text: fhir.String("hello world"), + } + want := &dtpb.Extension{ + Url: fhir.URI(url), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_Address{ + Address: input, + }, + }, + } + + got := extension.New(url, input) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Extension(Address): (-got,+want):\n%v", diff) + } +} + +func TestNew_Id_ReturnsExtension(t *testing.T) { + const url = "http://example.com" + input := fhir.String("hello world") + want := &dtpb.Extension{ + Url: fhir.URI(url), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_StringValue{ + StringValue: input, + }, + }, + } + + got := extension.New(url, input) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Extension(String): (-got,+want):\n%v", diff) + } +} + +func TestUnwrap_NilInput_ReturnsNil(t *testing.T) { + got := extension.Unwrap(nil) + + if got != nil { + t.Errorf("Unwrap: got %v, want nil", got) + } +} + +func TestUnwrap(t *testing.T) { + for name, element := range fhirtest.Elements { + t.Run(name, func(t *testing.T) { + ext, err := extension.FromElement("url", element) + if err != nil { + // Only test using elements that are valid for extensions. + t.Skip() + } + + got := extension.Unwrap(ext) + + want := element + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Unwrap(%v): (-got,+want):\n%v", name, diff) + } + }) + } +} + +func TestFromElement_NilInput_ReturnsError(t *testing.T) { + _, err := extension.FromElement("url", nil) + + if err == nil { + t.Errorf("Extension: got nil, want err") + } +} + +func TestFromElement(t *testing.T) { + const url = "http://example.com" + testCases := []struct { + name string + input fhir.Element + want *dtpb.Extension + wantErr error + }{ + { + name: "Nil", + input: nil, + want: nil, + wantErr: cmpopts.AnyError, + }, { + name: "String", + input: fhir.String("hello world"), + want: &dtpb.Extension{ + Url: fhir.URI(url), + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_StringValue{ + StringValue: fhir.String("hello world"), + }, + }, + }, + wantErr: nil, + }, { + name: "Extension", + input: &dtpb.Extension{}, + want: nil, + wantErr: extension.ErrInvalidValueX, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := extension.FromElement(url, tc.input) + + if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("FromElement(%v): got err %v, want %v", tc.name, got, want) + } + if got, want := got, tc.want; !cmp.Equal(got, want, protocmp.Transform()) { + t.Errorf("FromElement(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestOverwrite_WithNil_Panics(t *testing.T) { + defer func() { _ = recover() }() + + extension.Overwrite(nil) + + t.Errorf("Overwrite: expected panic") +} + +func TestOverwrite_WithExtendableResources_ExtendsResource(t *testing.T) { + const url = "url" + ext := extension.New(url, fhir.Boolean(true)) + + for name, object := range fhirtest.DomainResources { + t.Run(name, func(t *testing.T) { + object := proto.Clone(object).(fhir.DomainResource) + extension.Overwrite(object, ext) + + if len(object.GetExtension()) != 1 { + t.Fatalf("Overwrite: got len %v, want 1", len(object.GetExtension())) + } + got := object.GetExtension()[0] + if !proto.Equal(got, ext) { + t.Errorf("Overwrite: got extension %v, want %v", got, ext) + } + }) + } +} + +func TestAppendInto_WithNil_Panics(t *testing.T) { + defer func() { _ = recover() }() + + extension.AppendInto(nil) + + t.Errorf("AppendInto: expected panic") +} + +func TestAppendInto_WithResources_ExtendsResource(t *testing.T) { + const url = "url" + ext := extension.New(url, fhir.Boolean(true)) + + for name, object := range fhirtest.DomainResources { + t.Run(name, func(t *testing.T) { + object := proto.Clone(object).(fhir.DomainResource) + extension.Overwrite(object, extension.New(url, fhir.String("Some other value"))) + + extension.AppendInto(object, ext) + + if len(object.GetExtension()) != 2 { + t.Fatalf("AppendInto: got len %v, want 1", len(object.GetExtension())) + } + got := object.GetExtension()[1] + if !proto.Equal(got, ext) { + t.Errorf("AppendInto: got extension %v, want %v", got, ext) + } + }) + } +} + +func TestUpsert_WithNil_Panics(t *testing.T) { + defer func() { _ = recover() }() + + extension.AppendInto(nil) + + t.Errorf("AppendInto: expected panic") +} + +func TestUpsert_WithResources_ExtendsResource(t *testing.T) { + const ( + urlA = "urlA" + urlB = "urlB" + ) + ext := extension.New(urlA, fhir.Boolean(true)) + + for name, object := range fhirtest.DomainResources { + t.Run(name, func(t *testing.T) { + object := proto.Clone(object).(fhir.DomainResource) + extension.Overwrite(object, extension.New(urlB, fhir.String("Some other value"))) + + extension.Upsert(object, ext) + + if len(object.GetExtension()) != 2 { + t.Fatalf("Upsert: got len %v, want 2", len(object.GetExtension())) + } + got := object.GetExtension()[1] + if !proto.Equal(got, ext) { + t.Errorf("AppendInto: got extension %v, want %v", got, ext) + } + }) + } +} + +func TestUpsert_WithResources_ModifiesResource(t *testing.T) { + const urlA = "urlA" + + ext := extension.New(urlA, fhir.Boolean(true)) + + for name, object := range fhirtest.DomainResources { + t.Run(name, func(t *testing.T) { + object := proto.Clone(object).(fhir.DomainResource) + extension.Overwrite(object, extension.New(urlA, fhir.String("Some other value"))) + + extension.Upsert(object, ext) + + if len(object.GetExtension()) != 1 { + t.Fatalf("Upsert: got len %v, want 1", len(object.GetExtension())) + } + got := object.GetExtension()[0] + if !proto.Equal(got, ext) { + t.Errorf("AppendInto: got extension %v, want %v", got, ext) + } + }) + } +} + +func TestSetByURL_WithNil_Panics(t *testing.T) { + defer func() { _ = recover() }() + + extension.SetByURL(nil, "some-url", fhir.Boolean(true)) + + t.Errorf("AppendInto: expected panic") +} + +func TestSetByURL_WithResources_ModifiesResource(t *testing.T) { + const ( + urlA = "urlA" + urlB = "urlB" + ) + extB := extension.New(urlB, fhir.String("Some B value")) + extA := extension.New(urlA, fhir.String("Some A value")) + wantExtensions := []*dtpb.Extension{extB, extension.New(urlA, fhir.Boolean(true)), extension.New(urlA, fhir.Boolean(false))} + for name, object := range fhirtest.DomainResources { + t.Run(name, func(t *testing.T) { + object := proto.Clone(object).(fhir.DomainResource) + extension.Overwrite(object, extA) + extension.AppendInto(object, extB) + + extension.SetByURL(object, urlA, fhir.Boolean(true), fhir.Boolean(false)) + + if len(object.GetExtension()) != 3 { + t.Fatalf("Upsert: got len %v, want 3", len(object.GetExtension())) + } + gotExtensions := object.GetExtension() + if diff := cmp.Diff(gotExtensions, wantExtensions, protocmp.Transform()); diff != "" { + t.Errorf("AppendInto: (-got, +want) %v", diff) + } + }) + } +} + +func TestClear_DoesNothing(t *testing.T) { + extension.Clear(nil) + + // There really isn't anything to assert on here; there is no input or reaction. + // There is no crash either. +} + +func TestClear_ResourceHasExtensions_RemovesExtensions(t *testing.T) { + const url = "http://example.com" + for name, resource := range fhirtest.DomainResources { + t.Run(name, func(t *testing.T) { + got := proto.Clone(resource).(fhir.DomainResource) + want := proto.Clone(resource).(fhir.DomainResource) + extension.Overwrite(got, + extension.New(url, fhir.String("hello world")), + extension.New(url, fhir.Boolean(true)), + ) + + extension.Clear(got) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Clear(%v): (-got,+want):\n%v", name, diff) + } + }) + } +} diff --git a/internal/element/extract.go b/internal/element/extract.go new file mode 100644 index 0000000..b85a0fb --- /dev/null +++ b/internal/element/extract.go @@ -0,0 +1,208 @@ +package element + +import ( + "errors" + "fmt" + "sort" + "strings" + + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protopath" + "google.golang.org/protobuf/reflect/protorange" + "google.golang.org/protobuf/reflect/protoreflect" +) + +var ( + ErrFhirPathNotImplemented error = errors.New("FHIRPath labeling not yet implemented") +) + +// A ElementWithPath holds a FHIR element as a proto message and its corresponding +// FHIRPath. All FHIR elements (including scalar-ish elements like string +// and URI) are proto messages. +// See http://hl7.org/fhir/R4/fhirpath.html. +type ElementWithPath[elementT proto.Message] struct { + Element elementT + FHIRPath string +} + +// SortSliceOfElementWithPath sorts the given slice in-place according to the lexical +// ordering of the FHIR path of each element. +func SortSliceOfElementWithPath[elementT proto.Message](s []ElementWithPath[elementT]) { + sort.SliceStable(s, func(i, j int) bool { + return s[i].FHIRPath < s[j].FHIRPath + }) +} + +// ExtractAll returns all the elements of type elementT within the resource. +// The returned elements are not copies: mutations of the returned elements +// will mutate the resource. The order of the returned list is unspecified. +func ExtractAll[elementT proto.Message](resource fhir.Resource) ([]elementT, error) { + elements, err := extractAllImpl[elementT](resource, false) + if err != nil { + return nil, err + } + return slices.Map(elements, func(e ElementWithPath[elementT]) elementT { + return e.Element + }), nil +} + +// ExtractAllWithPath returns all the elements of typeT within the resource, +// along with the FHIR path of each such element. The returned elements +// are not copies: mutations of the returned elements will mutate the resource. +// The returned list is sorted by the lexical order of the FHIR path of each +// element. +func ExtractAllWithPath[elementT proto.Message](resource fhir.Resource) ([]ElementWithPath[elementT], error) { + elements, err := extractAllImpl[elementT](resource, true) + if err != nil { + return nil, err + } + SortSliceOfElementWithPath(elements) + return elements, err +} + +// extractAllImpl extracts all the instances of elementT from resource. +// If addPaths is true, all returns elements include the FHIR path +// of that element; otherwise each element's FHIR path is an empty string. +// The FHIR path labelling is optional because it may trigger error +// conditions that otherwise would not occur. (Also it is slower). +// +// Once FHIR path labelling is proven robust consider always doing it +// because it yields a nice determinstic sort. +func extractAllImpl[elementT proto.Message](resource fhir.Resource, addPaths bool) ([]ElementWithPath[elementT], error) { + elements := []ElementWithPath[elementT]{} + err := protorange.Range(resource.ProtoReflect(), func(pv protopath.Values) error { + element, found := getElementOfProtoPath[elementT](pv) + if found { + var fhirpath string + if addPaths { + var err error + fhirpath, err = computeFHIRPathOfProtoPath(pv.Path) + if err != nil { + return err + } + } + elementWithPath := ElementWithPath[elementT]{Element: element, FHIRPath: fhirpath} + elements = append(elements, elementWithPath) + } + return nil + }) + return elements, err +} + +// getElementOfProtoPath returns FHIR element referenced by pv. +// +// Returns (element, true) if pv references an element of type elementT; +// else returns (typed-nil, false). +func getElementOfProtoPath[elementT proto.Message](pv protopath.Values) (elementT, bool) { + currStep := pv.Path.Index(-1) + currV := pv.Values[pv.Len()-1] + var prm protoreflect.Message + switch currStep.Kind() { + case protopath.FieldAccessStep: + // FieldAccess describes access of a field within a message. + // The type of the current step value is determined by the field descriptor. + fd := currStep.FieldDescriptor() + if fd.Kind() == protoreflect.MessageKind && fd.Cardinality() != protoreflect.Repeated { + prm = currV.Message() + } + case protopath.ListIndexStep: + // ListIndex describes index of an element within a list. + // The previous step value is always a list. + // The previous step type is a field access for lists with message kind. + prevStep := pv.Path.Index(-2) + prevV := pv.Values[pv.Len()-2] + if prevStep.Kind() != protopath.FieldAccessStep { + break + } + if prevStep.FieldDescriptor().Kind() == protoreflect.MessageKind { + prm = prevV.List().Get(currStep.ListIndex()).Message() + } + case protopath.MapIndexStep: + // MapIndex describes index of an entry within a map. + // The previous step value is always a map. + // The previous step type is a field access for maps with message kind. + prevStep := pv.Path.Index(-2) + prevV := pv.Values[pv.Len()-2] + if prevStep.Kind() != protopath.FieldAccessStep { + break + } + if prevStep.FieldDescriptor().Kind() == protoreflect.MessageKind { + prm = prevV.Map().Get(currStep.MapIndex()).Message() + } + } + if prm != nil { + if element, ok := prm.Interface().(elementT); ok { + return element, true + } + } + var nilElement elementT + return nilElement, false +} + +// leafElementsByMsgFullName is set of Descriptor full names of elements +// that have proto implementation with subfields that don't matter +// at the FHIR element level. When computing the FHIR path of a proto path, +// proto fields of these message types are ignored. +var leafElementsByMsgFullName = map[string]struct{}{ + "google.fhir.r4.core.Date": {}, + "google.fhir.r4.core.DateTime": {}, + "google.fhir.r4.core.Instant": {}, + "google.fhir.r4.core.Time": {}, +} + +// computeFHIRPathOfProtoPath returns the FHIR path of p. +// +// The following cases are supported: +// - Typical single and repeated elements. +// - extensions. Extensions are expessed as "Resource.extension[k]" instead +// of as "Resource.extension('<extension-url>')" because the former is unique +// even with repeated extensions of the same URL. (And because +// it is simpler to implement here.) +// - "choice" fields. +// - Special date fields: see above leafElementsByMsgFullName. +// +// The following cases will return an error: +// - Any element inside a ContainedResource. Typically this happens +// for a Bundle. +// +// WATCHOUT: There are almost certainly cases that do not return an error +// but return an incorrect FHIRPath. +func computeFHIRPathOfProtoPath(p protopath.Path) (string, error) { + fhirpath := []string{} + for _, step := range p { + switch step.Kind() { + case protopath.RootStep: + fhirpath = append(fhirpath, string(step.MessageDescriptor().Name())) + + case protopath.FieldAccessStep: + fd := step.FieldDescriptor() + cfn := string(fd.ContainingMessage().FullName()) + if cfn == "google.fhir.r4.core.ContainedResource" { + // Only elements of Resource have well defined FHIRpath within + // Bundle.entry.resource. See PHP-8862. Punt on this for now. + return "", fmt.Errorf("%w: for %s", ErrFhirPathNotImplemented, cfn) + } + elementName := fd.JSONName() + if cof := fd.ContainingOneof(); cof != nil && cof.Name() == "choice" { + cappedName := strings.ToUpper(elementName[0:1]) + elementName[1:] + fhirpath[len(fhirpath)-1] += cappedName + } else { + fhirpath = append(fhirpath, elementName) + } + if _, found := leafElementsByMsgFullName[cfn]; found { + // The fhirpath component above is suffient. The remaining protopath + // steps are implementation fields that do not appear in the spec. + break + } + + case protopath.ListIndexStep: + fhirpath[len(fhirpath)-1] += fmt.Sprintf("[%d]", step.ListIndex()) + + default: + return "", fmt.Errorf("%w: for step %v", ErrFhirPathNotImplemented, step) + } + } + return strings.Join(fhirpath, "."), nil +} diff --git a/internal/element/extract_test.go b/internal/element/extract_test.go new file mode 100644 index 0000000..36dde22 --- /dev/null +++ b/internal/element/extract_test.go @@ -0,0 +1,315 @@ +package element_test + +// For extraction, we test Reference and CodeableConcept elements. +// There shouldn't be any need to exhastively test all possible element types, +// since they they are all proto messages (even scalar-ish elements like +// strings and URIs). + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + acpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/account_go_proto" + appb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/bundle" + "github.com/verily-src/fhirpath-go/internal/element" + "github.com/verily-src/fhirpath-go/internal/element/extension" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +func Test_ExtractAll_OfReference_ValuesCorrect(t *testing.T) { + refUri := &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: fhir.String("uri-ref"), + }, + } + refRelatedPersonId := &dtpb.Reference{ + Reference: &dtpb.Reference_RelatedPersonId{ + RelatedPersonId: &dtpb.ReferenceId{ + Id: fhir.String("related-ref"), + }, + }, + } + testCases := []struct { + name string + resource fhir.Resource + wantRefs []*dtpb.Reference + wantPaths []string + }{ + { + name: "No reference", + resource: fhirtest.NewResource(t, "Patient"), + wantRefs: []*dtpb.Reference{}, + }, + { + name: "Single reference", + resource: fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.ManagingOrganization = refUri + })), + wantRefs: []*dtpb.Reference{refUri}, + wantPaths: []string{"Patient.managingOrganization"}, + }, + { + name: "Multiple references", + resource: fhirtest.NewResource(t, "Appointment", fhirtest.WithResourceModification(func(a *appb.Appointment) { + a.Participant = []*appb.Appointment_Participant{ + { + Actor: refRelatedPersonId, + }, + { + Type: []*dtpb.CodeableConcept{fhir.CodeableConcept("", fhir.Coding("systest", "code"))}, + Actor: refUri, + }, + } + })), + wantRefs: []*dtpb.Reference{refRelatedPersonId, refUri}, + wantPaths: []string{"Appointment.participant[0].actor", "Appointment.participant[1].actor"}, + }, + { + name: "Repeated field references", + resource: fhirtest.NewResource(t, "Account", fhirtest.WithResourceModification(func(a *acpb.Account) { + a.Subject = []*dtpb.Reference{refRelatedPersonId, refUri} + })), + wantRefs: []*dtpb.Reference{refRelatedPersonId, refUri}, + wantPaths: []string{"Account.subject[0]", "Account.subject[1]"}, + }, + { + name: "Repeated identical references", + resource: fhirtest.NewResource(t, "Account", fhirtest.WithResourceModification(func(a *acpb.Account) { + a.Subject = []*dtpb.Reference{refRelatedPersonId, refRelatedPersonId} + })), + wantRefs: []*dtpb.Reference{refRelatedPersonId, refRelatedPersonId}, + wantPaths: []string{"Account.subject[0]", "Account.subject[1]"}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + gotReferences, _ := element.ExtractAll[*dtpb.Reference](testCase.resource) + + opts := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortSlices(func(a *dtpb.Reference, b *dtpb.Reference) bool { return a.String() < b.String() }), + } + if !cmp.Equal(testCase.wantRefs, gotReferences, opts...) { + t.Errorf("ExtractAll(): got '%v', want '%v'", gotReferences, testCase.wantRefs) + } + + gotRefsWithPath, err := element.ExtractAllWithPath[*dtpb.Reference](testCase.resource) + if err != nil { + t.Fatalf("ExtractAllElementWithPath(%s): got error %v", testCase.name, err) + } + wantRefsWithPath := zipElementsAndPaths(testCase.wantRefs, testCase.wantPaths) + if !cmp.Equal(gotRefsWithPath, wantRefsWithPath, protocmp.Transform(), cmpopts.EquateEmpty()) { + t.Errorf("ExtractAllElementWithPath(%s): got '%v', want '%v'", testCase.name, gotRefsWithPath, wantRefsWithPath) + } + }) + } +} + +func Test_ExtractAll_OfCodeableConcept_ValuesCorrect(t *testing.T) { + concept1 := fhir.CodeableConcept("thing1") + concept2 := fhir.CodeableConcept("thing2") + concept3 := fhir.CodeableConcept("thing2") + testCases := []struct { + name string + resource fhir.Resource + wantConcepts []*dtpb.CodeableConcept + wantPaths []string + wantError error + }{ + { + name: "single", + resource: fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.MaritalStatus = concept1 + })), + wantConcepts: []*dtpb.CodeableConcept{concept1}, + wantPaths: []string{"Patient.maritalStatus"}, + }, + { + name: "repeated nested repeated with dup", + resource: fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.Contact = []*ppb.Patient_Contact{ + {Relationship: []*dtpb.CodeableConcept{concept1, concept2}}, + {Relationship: []*dtpb.CodeableConcept{concept2, concept3}}, + } + })), + wantConcepts: []*dtpb.CodeableConcept{concept1, concept2, concept2, concept3}, + wantPaths: []string{ + "Patient.contact[0].relationship[0]", + "Patient.contact[0].relationship[1]", + "Patient.contact[1].relationship[0]", + "Patient.contact[1].relationship[1]", + }, + }, + { + name: "repeated extension", + resource: fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.Extension = []*dtpb.Extension{ + extension.New("my-extension-url", concept1), + extension.New("my-extension-url", concept2), + } + })), + wantConcepts: []*dtpb.CodeableConcept{concept1, concept2}, + wantPaths: []string{ + "Patient.extension[0].valueCodeableConcept", + "Patient.extension[1].valueCodeableConcept", + }, + }, + { + name: "inside ContainedResource inside Bundle", + resource: bundle.NewCollection(bundle.WithEntries(bundle.NewCollectionEntry( + fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.MaritalStatus = concept1 + }))))), + wantError: element.ErrFhirPathNotImplemented, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotElementsWithPath, gotErr := element.ExtractAllWithPath[*dtpb.CodeableConcept](tc.resource) + if !cmp.Equal(gotErr, tc.wantError, cmpopts.EquateErrors()) { + t.Fatalf("ExtractAllWithPath(%s) error got '%v', want '%v'", tc.name, gotErr, tc.wantError) + } + wantElementsWithPath := zipElementsAndPaths(tc.wantConcepts, tc.wantPaths) + if !cmp.Equal(gotElementsWithPath, wantElementsWithPath, protocmp.Transform(), cmpopts.EquateEmpty()) { + t.Errorf("ExtractAllElementWithPath(%s): got '%v', want '%v'", + tc.name, gotElementsWithPath, wantElementsWithPath) + } + }) + } +} + +func Test_ExtractAll_OfSpecial(t *testing.T) { + date1 := fhir.DateNow() + datetime1 := fhir.DateTimeNow() + instant1 := fhir.InstantNow() + instant2 := fhir.InstantNow() + instant3 := fhir.InstantNow() + time1 := fhir.TimeNow() + + testCases := []struct { + name string + resource fhir.Resource + extractFunc func(fhir.Resource) ([]element.ElementWithPath[fhir.Element], error) + wantElements []fhir.Element + wantPaths []string + }{ + {name: "simple Date", + resource: fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.BirthDate = date1 + })), + extractFunc: func(res fhir.Resource) ([]element.ElementWithPath[fhir.Element], error) { + asDates, err := element.ExtractAllWithPath[*dtpb.Date](res) + return asElements(asDates), err + }, + wantElements: []fhir.Element{date1}, + wantPaths: []string{"Patient.birthDate"}, + }, + {name: "DateTime inside choice", + resource: fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.Deceased = &ppb.Patient_DeceasedX{ + Choice: &ppb.Patient_DeceasedX_DateTime{DateTime: datetime1}, + } + })), + extractFunc: func(res fhir.Resource) ([]element.ElementWithPath[fhir.Element], error) { + asDateTimes, err := element.ExtractAllWithPath[*dtpb.DateTime](res) + return asElements(asDateTimes), err + }, + wantElements: []fhir.Element{datetime1}, + wantPaths: []string{"Patient.deceasedDateTime"}, + }, + { + name: "Instant", + resource: fhirtest.NewResource(t, "Appointment", fhirtest.WithResourceModification(func(a *appb.Appointment) { + a.Meta.LastUpdated = instant1 + a.Start = instant2 + a.End = instant3 + })), + extractFunc: func(res fhir.Resource) ([]element.ElementWithPath[fhir.Element], error) { + asInstants, err := element.ExtractAllWithPath[*dtpb.Instant](res) + return asElements(asInstants), err + }, + wantElements: []fhir.Element{instant3, instant1, instant2}, + wantPaths: []string{"Appointment.end", "Appointment.meta.lastUpdated", "Appointment.start"}, + }, + {name: "Time inside extension", + resource: fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.Extension = []*dtpb.Extension{ + extension.New("my-extension-url", time1), + } + })), + extractFunc: func(res fhir.Resource) ([]element.ElementWithPath[fhir.Element], error) { + asTimes, err := element.ExtractAllWithPath[*dtpb.Time](res) + return asElements(asTimes), err + }, + wantElements: []fhir.Element{time1}, + wantPaths: []string{"Patient.extension[0].valueTime"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotElementsWithPath, _ := tc.extractFunc(tc.resource) + wantElementsWithPath := zipElementsAndPaths(tc.wantElements, tc.wantPaths) + if !cmp.Equal(gotElementsWithPath, wantElementsWithPath, protocmp.Transform(), cmpopts.EquateEmpty()) { + t.Errorf("ExtractAllElementWithPath(%s): got '%v', want '%v'", + tc.name, gotElementsWithPath, wantElementsWithPath) + } + }) + } +} + +// asElements maps a slice of ElementWithPath[elementT] +// to a new slice of [ElementWithPath[fhir.Element]. +func asElements[elementT fhir.Element](elements []element.ElementWithPath[elementT]) []element.ElementWithPath[fhir.Element] { + return slices.Map(elements, + func(e element.ElementWithPath[elementT]) element.ElementWithPath[fhir.Element] { + return element.ElementWithPath[fhir.Element]{Element: e.Element, FHIRPath: e.FHIRPath} + }, + ) +} + +func zipElementsAndPaths[elementT proto.Message](elements []elementT, paths []string) []element.ElementWithPath[elementT] { + zipped := []element.ElementWithPath[elementT]{} + for idx, ele := range elements { + var path string + if idx < len(paths) { + path = paths[idx] + } + zipped = append(zipped, element.ElementWithPath[elementT]{Element: ele, FHIRPath: path}) + } + return zipped +} + +func Test_ExtractAll_Modifiable(t *testing.T) { + resource := fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.ManagingOrganization = &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: fhir.String("old-ref"), + }, + } + })).(*ppb.Patient) + + references, _ := element.ExtractAll[*dtpb.Reference](resource) + + if len(references) != 1 { + t.Fatalf("Expected single reference") + } + + references[0].Reference = &dtpb.Reference_Uri{ + Uri: fhir.String("new-ref"), + } + + if got, want := resource.ManagingOrganization, references[0]; got != want { + t.Errorf("ExtractAll() reference update failed, got '%v', want '%v'", got, want) + } +} diff --git a/internal/element/identifier/docs.go b/internal/element/identifier/docs.go new file mode 100644 index 0000000..6ff17c7 --- /dev/null +++ b/internal/element/identifier/docs.go @@ -0,0 +1,7 @@ +/* +Package identifier provides utilities for constructing and working with +FHIR R4 Identifier elements. + +See: http://hl7.org/fhir/R4/datatypes.html#Identifier +*/ +package identifier diff --git a/internal/element/identifier/identifier.go b/internal/element/identifier/identifier.go new file mode 100644 index 0000000..5bc161f --- /dev/null +++ b/internal/element/identifier/identifier.go @@ -0,0 +1,174 @@ +package identifier + +import ( + "fmt" + "net/url" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +// Use is an alias of the Identifier Use-codes for easier access and readability. +type Use = cpb.IdentifierUseCode_Value + +const ( + // UseUsual is an alias of the USUAL Identifier use-code for easier access and + // readability. + UseUsual Use = cpb.IdentifierUseCode_USUAL + + // UseOfficial is an alias of the OFFICIAL Identifier use-code for easier access + // and readability. + UseOfficial Use = cpb.IdentifierUseCode_OFFICIAL + + // UseTemp is an alias of the TEMP Identifier use-code for easier access and + // readability. + UseTemp Use = cpb.IdentifierUseCode_TEMP + + // UseSecondary is an alias of the SECONDARY Identifier use-code for easier + // access and readability. + UseSecondary Use = cpb.IdentifierUseCode_SECONDARY + + // UseOld is an alias of the OLD Identifier use-code for easier access and + // readability. + UseOld Use = cpb.IdentifierUseCode_OLD +) + +// New constructs a new Identifier object from the given options. +func New(value, system string, opts ...Option) *dtpb.Identifier { + identifier := &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + } + return Update(identifier, opts...) +} + +// Usual is a convenience constructor for forming an identifier with a "Usual" +// Use-code assigned. +func Usual(value, system string, opts ...Option) *dtpb.Identifier { + return newWithUse(UseUsual, value, system, opts...) +} + +// Official is a convenience constructor for forming an identifier with an +// "Official" Use-code assigned. +func Official(value, system string, opts ...Option) *dtpb.Identifier { + return newWithUse(UseOfficial, value, system, opts...) +} + +// Temp is a convenience constructor for forming an identifier with a "Temp" +// Use-code assigned. +func Temp(value, system string, opts ...Option) *dtpb.Identifier { + return newWithUse(UseTemp, value, system, opts...) +} + +// Secondary is a convenience constructor for forming an identifier with a +// "Secondary" Use-code assigned. +func Secondary(value, system string, opts ...Option) *dtpb.Identifier { + return newWithUse(UseSecondary, value, system, opts...) +} + +// Old is a convenience constructor for forming an identifier with an "Old" +// Use-code assigned. +func Old(value, system string, opts ...Option) *dtpb.Identifier { + return newWithUse(UseOld, value, system, opts...) +} + +func newWithUse(use Use, value, system string, opts ...Option) *dtpb.Identifier { + identifier := &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Use: &dtpb.Identifier_UseCode{ + Value: use, + }, + } + return Update(identifier, opts...) +} + +// Update modifies an identifier in-place with the given identifier options. +// +// This function returns the input identifier to allow for functional chaining. +func Update(identifier *dtpb.Identifier, opts ...Option) *dtpb.Identifier { + for _, opt := range opts { + opt.update(identifier) + } + return identifier +} + +// Equivalent checks if two identifiers are equivalent by comparing the +// system and values of the identifier. +func Equivalent(lhs, rhs *dtpb.Identifier) bool { + lsystem, rsystem := lhs.GetSystem(), rhs.GetSystem() + lvalue, rvalue := lhs.GetValue(), rhs.GetValue() + + if lsystem.GetValue() != rsystem.GetValue() { + return false + } + if lvalue.GetValue() != rvalue.GetValue() { + return false + } + return true +} + +// FindBySystem searches a slice of identifiers for the first identifier that +// contains the specified system. +func FindBySystem(identifiers []*dtpb.Identifier, system string) *dtpb.Identifier { + for _, identifier := range identifiers { + identifierSystem := identifier.GetSystem() + if identifierSystem == nil { + continue + } + if identifierSystem.GetValue() == system { + return identifier + } + } + return nil +} + +// QueryString formats a system and value for use in a Search query, +// escaping FHIR special characters `,|$\` in the input. +// Use this in a query param as the value with key `identifier`. +func QueryString(system string, value string) string { + // escape special characters like `|` from identifier + escapedSystem := fhir.EscapeSearchParam(system) + escapedValue := fhir.EscapeSearchParam(value) + return fmt.Sprintf("%s|%s", escapedSystem, escapedValue) +} + +// QueryIdentifier formats an Identifier proto for use in a Search query, +// escaping FHIR special characters `,|$\` in the input. +// Use this in a query param as the value with key `identifier`. +func QueryIdentifier(id *dtpb.Identifier) string { + return QueryString(id.System.Value, id.Value.Value) +} + +// GenerateIfNoneExist takes an Identifier and generates a query appropriate for use in an If-None-Exist header. +// This is used for FHIR conditional create or other conditional methods. +// +// Untrusted data in Identifiers is escaped both for FHIR and for URL safety. +// +// Returns an empty string if identifier is nil. +// +// This function only accepts a single identifier due to limitations of the GCP +// FHIR store. Important note: +// The GCP FHIR store only supports conditional queries on a single identifier, +// with no modifiers (so identifier=foo is OK, while identifier:exact=foo is +// invalid). Deviating from this in API v1 will result in an HTTP 400 invalid +// query error. NB: Deviating from this in API v1beta1 results in silent +// fallback to non-transactional search, meaning the conditional queries will +// have race conditions. +func GenerateIfNoneExist(identifier *dtpb.Identifier) string { + if identifier == nil { + return "" + } + + // build up a query string like format for the header + // data is URL encoded in case the identifier contains special characters + qs := url.Values{} + + search := QueryIdentifier(identifier) + // use identifier= rather than identifier:exact=, see limitation above + qs.Add("identifier", search) + + // URL encode the result + return qs.Encode() +} diff --git a/internal/element/identifier/identifier_example_test.go b/internal/element/identifier/identifier_example_test.go new file mode 100644 index 0000000..a5a16d9 --- /dev/null +++ b/internal/element/identifier/identifier_example_test.go @@ -0,0 +1,52 @@ +package identifier_test + +import ( + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/element/identifier" +) + +func ExampleQueryIdentifier() { + ident := &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "b0459744-b74b-441a-aee4-9dd97c80c642"}, + } + + search := identifier.QueryIdentifier(ident) + fmt.Printf("identifier:exact=%s", search) + // Output: identifier:exact=http://fake.com|b0459744-b74b-441a-aee4-9dd97c80c642 +} + +func ExampleQueryIdentifier_escape() { + ident := &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "foo,bar|baz"}, + } + + search := identifier.QueryIdentifier(ident) + fmt.Printf("identifier:exact=%s", search) + // Output: identifier:exact=http://fake.com|foo\,bar\|baz +} + +func ExampleQueryString() { + search := identifier.QueryString("http://fake.com", "1234") + fmt.Printf("identifier:exact=%s", search) + // Output: identifier:exact=http://fake.com|1234 +} +func ExampleQueryString_escape() { + search := identifier.QueryString("http://fake.com", `$foo|bar\baz`) + fmt.Printf("identifier:exact=%s", search) + // Output: identifier:exact=http://fake.com|\$foo\|bar\\baz +} + +func ExampleGenerateIfNoneExist() { + id := &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, + } + + header := identifier.GenerateIfNoneExist(id) + fmt.Printf("If-None-Exist: %v", header) + // Output: If-None-Exist: identifier=http%3A%2F%2Ffake.com%7C9efbf82d-7a58-4d14-bec1-63f8fda148a8 +} diff --git a/internal/element/identifier/identifier_test.go b/internal/element/identifier/identifier_test.go new file mode 100644 index 0000000..c2c2578 --- /dev/null +++ b/internal/element/identifier/identifier_test.go @@ -0,0 +1,329 @@ +package identifier_test + +import ( + "net/url" + "testing" + + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/uuid" + "github.com/verily-src/fhirpath-go/internal/element/identifier" + "github.com/verily-src/fhirpath-go/internal/fhir" + "google.golang.org/protobuf/proto" +) + +// New also tests the behaviors of various opts. +func TestNew(t *testing.T) { + const system = "system" + const value = "value" + period := &dtpb.Period{ + Start: fhir.DateTimeNow(), + } + ref := &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: fhir.String("uri"), + }, + } + cc := &dtpb.CodeableConcept{ + Text: fhir.String("example"), + } + ext := &dtpb.Extension{ + Url: fhir.URI("url"), + } + testCases := []struct { + name string + opts []identifier.Option + want *dtpb.Identifier + }{ + { + name: "No options", + want: &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + }, + }, { + name: "WithUse", + opts: []identifier.Option{identifier.WithUse(identifier.UseOfficial)}, + want: &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Use: &dtpb.Identifier_UseCode{ + Value: identifier.UseOfficial, + }, + }, + }, { + name: "WithAssigner", + opts: []identifier.Option{identifier.WithAssigner(ref)}, + want: &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Assigner: ref, + }, + }, { + name: "WithPeriod", + opts: []identifier.Option{identifier.WithPeriod(period)}, + want: &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Period: period, + }, + }, { + name: "WithType", + opts: []identifier.Option{identifier.WithType(cc)}, + want: &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Type: cc, + }, + }, { + name: "WithExtensions", + opts: []identifier.Option{identifier.WithExtensions(ext)}, + want: &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Extension: []*dtpb.Extension{ext}, + }, + }, { + name: "IncludeExtensions", + opts: []identifier.Option{identifier.IncludeExtensions(ext)}, + want: &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Extension: []*dtpb.Extension{ext}, + }, + }, { + name: "WithID", + opts: []identifier.Option{identifier.WithID("id")}, + want: &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Id: fhir.String("id"), + }, + }, + } + + for _, tc := range testCases { + got := identifier.New(value, system, tc.opts...) + + if want := tc.want; !proto.Equal(got, want) { + t.Errorf("New(%v): got %v, want %v", tc.name, got, want) + } + } +} + +func TestUpdate(t *testing.T) { + const system = "system" + const value = "value" + + ext := &dtpb.Extension{ + Url: fhir.URI("url"), + } + testCases := []struct { + name string + opts []identifier.Option + want *dtpb.Identifier + }{ + { + name: "IncludeExtensions", + opts: []identifier.Option{identifier.IncludeExtensions(ext)}, + want: &dtpb.Identifier{ + Extension: []*dtpb.Extension{ext}, + }, + }, { + name: "WithSystemString", + opts: []identifier.Option{identifier.WithSystemString(system)}, + want: &dtpb.Identifier{ + System: fhir.URI(system), + }, + }, { + name: "WithValue", + opts: []identifier.Option{identifier.WithValue(value)}, + want: &dtpb.Identifier{ + Value: fhir.String(value), + }, + }, + } + + for _, tc := range testCases { + + got := identifier.Update(&dtpb.Identifier{}, tc.opts...) + + if want := tc.want; !proto.Equal(got, want) { + t.Errorf("Update(%v): got %v, want %v", tc.name, got, want) + } + } +} + +func TestNewWithUse(t *testing.T) { + const system = "system" + const value = "value" + + testCases := []struct { + name string + fn func(string, string, ...identifier.Option) *dtpb.Identifier + use identifier.Use + }{ + {"Usual", identifier.Usual, identifier.UseUsual}, + {"Official", identifier.Official, identifier.UseOfficial}, + {"Temp", identifier.Temp, identifier.UseTemp}, + {"Secondary", identifier.Secondary, identifier.UseSecondary}, + {"Old", identifier.Old, identifier.UseOld}, + } + + for _, tc := range testCases { + got := tc.fn(value, system) + + want := &dtpb.Identifier{ + System: fhir.URI(system), + Value: fhir.String(value), + Use: &dtpb.Identifier_UseCode{ + Value: tc.use, + }, + } + if !proto.Equal(got, want) { + t.Errorf("%v: got %v, want %v", tc.name, got, want) + } + } +} + +func TestEquivalent(t *testing.T) { + const value = "value" + const system = "system" + testCases := []struct { + name string + lhs *dtpb.Identifier + rhs *dtpb.Identifier + want bool + }{ + { + name: "Systems don't match", + lhs: identifier.New(value, system), + rhs: identifier.New(value, system+"1"), + want: false, + }, { + name: "Values don't match", + lhs: identifier.New(value, system), + rhs: identifier.New(value+"1", system), + want: false, + }, { + name: "Both system and values don't match", + lhs: identifier.New(value, system), + rhs: identifier.New(value+"1", system+"1"), + want: false, + }, { + name: "Both systems and values match", + lhs: identifier.New(value, system), + rhs: identifier.New(value, system), + want: true, + }, + } + + for _, tc := range testCases { + got := identifier.Equivalent(tc.lhs, tc.rhs) + + if got != tc.want { + t.Errorf("Equivalent(%v): got %v, want %v", tc.name, got, tc.want) + } + } +} + +func TestFindBySystem(t *testing.T) { + want := identifier.New("value", "system") + + testCases := []struct { + name string + haystack []*dtpb.Identifier + want *dtpb.Identifier + }{ + { + name: "Input empty", + haystack: nil, + want: nil, + }, { + name: "Input not found", + haystack: []*dtpb.Identifier{ + identifier.New("a", "a-system"), + identifier.New("b", "b-system"), + }, + want: nil, + }, { + name: "Input is first value", + haystack: []*dtpb.Identifier{ + want, + identifier.New("a", "a-system"), + identifier.New("b", "b-system"), + }, + want: want, + }, { + name: "Input is last value", + haystack: []*dtpb.Identifier{ + identifier.New("a", "a-system"), + identifier.New("b", "b-system"), + want, + }, + want: want, + }, { + name: "Input is middle entry", + haystack: []*dtpb.Identifier{ + identifier.New("a", "a-system"), + want, + identifier.New("b", "b-system"), + }, + want: want, + }, + } + + for _, tc := range testCases { + got := identifier.FindBySystem(tc.haystack, want.System.Value) + + if got != tc.want { + t.Errorf("FindBySystem(%v): got %v, want %v", tc.name, got, tc.want) + } + } +} + +func TestGenerateIfNoneExist(t *testing.T) { + + id1 := &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, + } + id2 := &dtpb.Identifier{ + Use: &dtpb.Identifier_UseCode{ + Value: codes_go_proto.IdentifierUseCode_USUAL, + }, + System: &dtpb.Uri{Value: "urn:oid:2.16.840.1.113883.2.4.6.3"}, + Value: &dtpb.String{Value: "12345"}, + } + + id3 := &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://example.com/fake-id"}, + Value: &dtpb.String{Value: uuid.NewString()}, + } + + id4 := &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "foo,bar,baz|omg"}, + } + + testCases := []struct { + name string + input *dtpb.Identifier + want string + }{ + {"nil Identifier", nil, ""}, + {"single Identifier", id1, "identifier=" + url.QueryEscape("http://fake.com|9efbf82d-7a58-4d14-bec1-63f8fda148a8")}, + {"Identifier with use code", id2, "identifier=" + url.QueryEscape("urn:oid:2.16.840.1.113883.2.4.6.3|12345")}, + {"generated ID", id3, "identifier=" + url.QueryEscape("http://example.com/fake-id|"+id3.Value.Value)}, + {"Special chars in Identifier", id4, "identifier=" + url.QueryEscape(`http://fake.com|foo\,bar\,baz\|omg`)}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := identifier.GenerateIfNoneExist(tc.input) + if got != tc.want { + t.Errorf("%#v: Bad If-None-Exist:\n got %#v\n want %#v", tc.name, got, tc.want) + } + }) + } +} diff --git a/internal/element/identifier/opts.go b/internal/element/identifier/opts.go new file mode 100644 index 0000000..9a5b854 --- /dev/null +++ b/internal/element/identifier/opts.go @@ -0,0 +1,103 @@ +package identifier + +import ( + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +// Option is an abstraction for options to construct or modify Identifier elements. +type Option interface { + update(*dtpb.Identifier) +} + +// WithUse returns an Identifier Option that sets the Identifier.Use to the +// specified use. +func WithUse(use Use) Option { + return withCallback(func(i *dtpb.Identifier) { + i.Use = &dtpb.Identifier_UseCode{ + Value: use, + } + }) +} + +// WithExtensions return an Identifier Option that sets the Identifier.Extension +// field to the specified extensions. +func WithExtensions(ext ...*dtpb.Extension) Option { + return withCallback(func(i *dtpb.Identifier) { + i.Extension = ext + }) +} + +// IncludeExtensions return an Identifier Option that appends the specified +// extensions to the Identifier.Extension field. +func IncludeExtensions(ext ...*dtpb.Extension) Option { + return withCallback(func(i *dtpb.Identifier) { + i.Extension = append(i.Extension, ext...) + }) +} + +// WithType returns an Identifier Option that sets the Identifier.Type to the +// specified type. +func WithType(ty *dtpb.CodeableConcept) Option { + return withCallback(func(i *dtpb.Identifier) { + i.Type = ty + }) +} + +// WithSystem returns an Identifier Option that sets the Identifier.System to the +// specified system. +func WithSystem(system *dtpb.Uri) Option { + return withCallback(func(i *dtpb.Identifier) { + i.System = system + }) +} + +// WithSystemString returns an Identifier Option that sets the Identifier.System +// to the specified system string. +func WithSystemString(system string) Option { + return WithSystem(fhir.URI(system)) +} + +// WithValue returns an Identifier Option that sets the Identifier.Value to the +// specified value. +func WithValue(value string) Option { + return withCallback(func(i *dtpb.Identifier) { + i.Value = fhir.String(value) + }) +} + +// WithPeriod returns an Identifier Option that sets the Identifier.Period to the +// specified period. +func WithPeriod(period *dtpb.Period) Option { + return withCallback(func(i *dtpb.Identifier) { + i.Period = period + }) +} + +// WithAssigner returns an Identifier Option that sets the Identifier.Assigner to the +// specified assigner reference. +func WithAssigner(assigner *dtpb.Reference) Option { + return withCallback(func(i *dtpb.Identifier) { + i.Assigner = assigner + }) +} + +// WithID returns an Identifier Option that sets the Identifier.Id to the +// specified ID. +func WithID(id string) Option { + return withCallback(func(i *dtpb.Identifier) { + i.Id = fhir.String(id) + }) +} + +type callbackOpt struct { + callback func(*dtpb.Identifier) +} + +func (o callbackOpt) update(i *dtpb.Identifier) { + o.callback(i) +} + +func withCallback(callback func(*dtpb.Identifier)) Option { + return callbackOpt{callback} +} diff --git a/internal/element/meta/meta.go b/internal/element/meta/meta.go new file mode 100644 index 0000000..726217b --- /dev/null +++ b/internal/element/meta/meta.go @@ -0,0 +1,95 @@ +package meta + +import ( + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// Option is an option interface for modifying meta in place. +type Option interface { + updateMeta(meta *dtpb.Meta) +} + +// Update updates meta in place with given opts. +func Update(meta *dtpb.Meta, opts ...Option) *dtpb.Meta { + for _, opt := range opts { + opt.updateMeta(meta) + } + return meta +} + +// WithTags replaces meta.tag. +func WithTags(tags ...*dtpb.Coding) Option { + return withCodingOpt(tags) +} + +type withCodingOpt []*dtpb.Coding + +func (wco withCodingOpt) updateMeta(meta *dtpb.Meta) { + meta.Tag = wco +} + +// WithExtensions replaces meta.extension. +func WithExtensions(exts ...*dtpb.Extension) Option { + return withExtensionOpt(exts) +} + +type withExtensionOpt []*dtpb.Extension + +func (weo withExtensionOpt) updateMeta(meta *dtpb.Meta) { + meta.Extension = weo +} + +// IncludeTags appends to meta.tag. +func IncludeTags(tags ...*dtpb.Coding) Option { + return includeCodingOpt(tags) +} + +type includeCodingOpt []*dtpb.Coding + +func (ico includeCodingOpt) updateMeta(meta *dtpb.Meta) { + meta.Tag = append(meta.Tag, ico...) +} + +// WithProfiles replaces meta.profile. +func WithProfiles(profiles ...*dtpb.Canonical) Option { + return withCanonicalOpt(profiles) +} + +type withCanonicalOpt []*dtpb.Canonical + +func (wco withCanonicalOpt) updateMeta(meta *dtpb.Meta) { + meta.Profile = wco +} + +// IncludeProfiles appends to meta.profile. +func IncludeProfiles(profiles ...*dtpb.Canonical) Option { + return includeCanonicalOpt(profiles) +} + +type includeCanonicalOpt []*dtpb.Canonical + +func (ico includeCanonicalOpt) updateMeta(meta *dtpb.Meta) { + meta.Profile = append(meta.Profile, ico...) +} + +// ReplaceInResource replaces the resource meta field with the provided meta +// object. +func ReplaceInResource(resource fhir.Resource, meta *dtpb.Meta) { + reflect := resource.ProtoReflect() + metaField := getMetaField(reflect) + + reflect.Set(metaField, protoreflect.ValueOfMessage(meta.ProtoReflect())) +} + +// EnsureInResource ensures that the resource meta field exists. +func EnsureInResource(resource fhir.Resource) { + if resource.GetMeta() == nil { + ReplaceInResource(resource, &dtpb.Meta{}) + } +} + +func getMetaField(reflect protoreflect.Message) protoreflect.FieldDescriptor { + return reflect.Descriptor().Fields().ByName("meta") +} diff --git a/internal/element/meta/meta_example_test.go b/internal/element/meta/meta_example_test.go new file mode 100644 index 0000000..b00acad --- /dev/null +++ b/internal/element/meta/meta_example_test.go @@ -0,0 +1,25 @@ +package meta_test + +import ( + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element/canonical" + "github.com/verily-src/fhirpath-go/internal/element/meta" +) + +func ExampleUpdate() { + m := &dtpb.Meta{} + + meta.Update(m, + meta.WithTags(fhir.Coding("urn:oid:verily/sample-tag-system", "sample-tag-value")), + meta.WithProfiles(canonical.New("urn:oid:verily/sample-profile")), + ) + + fmt.Printf("meta.profile: %q\n", m.Profile[0].Value) + fmt.Printf("meta.tag: {%q, %q}", m.Tag[0].System.Value, m.Tag[0].Code.Value) + // Output: + // meta.profile: "urn:oid:verily/sample-profile" + // meta.tag: {"urn:oid:verily/sample-tag-system", "sample-tag-value"} +} diff --git a/internal/element/meta/meta_test.go b/internal/element/meta/meta_test.go new file mode 100644 index 0000000..bb5fbb1 --- /dev/null +++ b/internal/element/meta/meta_test.go @@ -0,0 +1,233 @@ +package meta_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element/canonical" + "github.com/verily-src/fhirpath-go/internal/element/extension" + "github.com/verily-src/fhirpath-go/internal/element/meta" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestWithTags(t *testing.T) { + at := fhir.Coding("at-s", "at-v") + bt := fhir.Coding("b-s", "b-v") + testCases := []struct { + name string + inMeta *dtpb.Meta + inTags []*dtpb.Coding + wantTags []*dtpb.Coding + }{ + { + name: "Clears Tags", + inMeta: &dtpb.Meta{ + Tag: []*dtpb.Coding{at}, + }, + inTags: nil, + wantTags: nil, + }, + { + name: "Replaces Tags", + inMeta: &dtpb.Meta{ + Tag: []*dtpb.Coding{at}, + }, + inTags: []*dtpb.Coding{bt}, + wantTags: []*dtpb.Coding{bt}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + inMeta := proto.Clone(testCase.inMeta).(*dtpb.Meta) + + meta.Update(inMeta, meta.WithTags(testCase.inTags...)) + + if diff := cmp.Diff(inMeta.GetTag(), testCase.wantTags, protocmp.Transform()); diff != "" { + t.Errorf("WithTags(): (-want, +got)\n%v", diff) + } + }) + } +} + +func TestIncludesTags(t *testing.T) { + at := fhir.Coding("at-s", "at-v") + bt := fhir.Coding("b-s", "b-v") + testCases := []struct { + name string + inMeta *dtpb.Meta + inTags []*dtpb.Coding + wantTags []*dtpb.Coding + }{ + { + name: "Maintains Tags", + inMeta: &dtpb.Meta{ + Tag: []*dtpb.Coding{at}, + }, + inTags: nil, + wantTags: []*dtpb.Coding{at}, + }, + { + name: "Appends Tags", + inMeta: &dtpb.Meta{ + Tag: []*dtpb.Coding{at}, + }, + inTags: []*dtpb.Coding{bt}, + wantTags: []*dtpb.Coding{at, bt}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + inMeta := proto.Clone(testCase.inMeta).(*dtpb.Meta) + + meta.Update(inMeta, meta.IncludeTags(testCase.inTags...)) + + if diff := cmp.Diff(inMeta.GetTag(), testCase.wantTags, protocmp.Transform()); diff != "" { + t.Errorf("IncludeTags(): (-want, +got)\n%v", diff) + } + }) + } +} + +func TestWithMetaProfiles(t *testing.T) { + ap := canonical.New("ap") + bp := canonical.New("bp") + testCases := []struct { + name string + inMeta *dtpb.Meta + inProfiles []*dtpb.Canonical + wantProfiles []*dtpb.Canonical + }{ + { + name: "Clears Profiles", + inMeta: &dtpb.Meta{ + Profile: []*dtpb.Canonical{ap}, + }, + inProfiles: nil, + wantProfiles: nil, + }, + { + name: "Replaces Profiles", + inMeta: &dtpb.Meta{ + Profile: []*dtpb.Canonical{ap}, + }, + inProfiles: []*dtpb.Canonical{bp}, + wantProfiles: []*dtpb.Canonical{bp}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + inMeta := proto.Clone(testCase.inMeta).(*dtpb.Meta) + + meta.Update(inMeta, meta.WithProfiles(testCase.inProfiles...)) + + if diff := cmp.Diff(inMeta.GetProfile(), testCase.wantProfiles, protocmp.Transform()); diff != "" { + t.Errorf("WithProfiles(): (-want, +got)\n%v", diff) + } + }) + } +} + +func TestIncludeMetaProfiles(t *testing.T) { + ap := canonical.New("ap") + bp := canonical.New("bp") + testCases := []struct { + name string + inMeta *dtpb.Meta + inProfiles []*dtpb.Canonical + wantProfiles []*dtpb.Canonical + }{ + { + name: "Maintains Profiles", + inMeta: &dtpb.Meta{ + Profile: []*dtpb.Canonical{ap}, + }, + inProfiles: nil, + wantProfiles: []*dtpb.Canonical{ap}, + }, + { + name: "Appends Profiles", + inMeta: &dtpb.Meta{ + Profile: []*dtpb.Canonical{ap}, + }, + inProfiles: []*dtpb.Canonical{bp}, + wantProfiles: []*dtpb.Canonical{ap, bp}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + inMeta := proto.Clone(testCase.inMeta).(*dtpb.Meta) + + meta.Update(inMeta, meta.IncludeProfiles(testCase.inProfiles...)) + + if diff := cmp.Diff(inMeta.GetProfile(), testCase.wantProfiles, protocmp.Transform()); diff != "" { + t.Errorf("IncludeProfiles(): (-want, +got)\n%v", diff) + } + }) + } +} + +func TestReplaceMeta(t *testing.T) { + patient := &ppb.Patient{Meta: &dtpb.Meta{}} + wantMeta := &dtpb.Meta{VersionId: fhir.ID("apple")} + + t.Run("ReplaceMeta", func(t *testing.T) { + meta.ReplaceInResource(patient, &dtpb.Meta{VersionId: fhir.ID("apple")}) + if diff := cmp.Diff(patient.GetMeta(), wantMeta, protocmp.Transform()); diff != "" { + t.Errorf("ReplaceMeta(): (-want, +got)\n%v", diff) + } + }) +} + +func TestEnsureMeta(t *testing.T) { + patient := &ppb.Patient{} + wantMeta := &dtpb.Meta{} + + t.Run("EnsureMeta", func(t *testing.T) { + meta.EnsureInResource(patient) + if diff := cmp.Diff(patient.GetMeta(), wantMeta, protocmp.Transform()); diff != "" { + t.Errorf("EnsureMeta(): (-want, +got)\n%v", diff) + } + }) +} + +func TestWithExtension(t *testing.T) { + oldExtension := extension.New("extension-url-old", fhir.String("extension-old")) + newExtension := extension.New("extension-url-new", fhir.String("extension-new")) + testCases := []struct { + name string + inMeta *dtpb.Meta + inExtension []*dtpb.Extension + wantExtension []*dtpb.Extension + }{ + { + name: "Clears Extension", + inMeta: &dtpb.Meta{ + Extension: []*dtpb.Extension{oldExtension}, + }, + inExtension: nil, + wantExtension: nil, + }, + { + name: "Replaces Extension", + inMeta: &dtpb.Meta{ + Extension: []*dtpb.Extension{oldExtension}, + }, + inExtension: []*dtpb.Extension{newExtension}, + wantExtension: []*dtpb.Extension{newExtension}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + inMeta := proto.Clone(testCase.inMeta).(*dtpb.Meta) + meta.Update(inMeta, meta.WithExtensions(testCase.inExtension...)) + if diff := cmp.Diff(inMeta.GetExtension(), testCase.wantExtension, protocmp.Transform()); diff != "" { + t.Errorf("WithExtensions(): (-want, +got)\n%v", diff) + } + }) + } +} diff --git a/internal/element/reference/identity.go b/internal/element/reference/identity.go new file mode 100644 index 0000000..ef4043a --- /dev/null +++ b/internal/element/reference/identity.go @@ -0,0 +1,131 @@ +package reference + +import ( + "errors" + "fmt" + "regexp" + "strings" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/iancoleman/strcase" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// Source: https://hl7.org/fhir/r4/references.html#literal +// WATCHOUT: See PHP-9300 about anchored matches. +var restFHIRServiceBaseURLRegex = regexp.MustCompile(`^(http|https):\/\/([A-Za-z0-9\-\\\.\:\%\$]*\/)+$`) +var restFHIRServiceResourceURLRegex = regexp.MustCompile(`^((http|https):\/\/([A-Za-z0-9\-\\\.\:\%\$\_]*\/)+)?(Account|ActivityDefinition|AdverseEvent|AllergyIntolerance|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BiologicallyDerivedProduct|BodyStructure|Bundle|CapabilityStatement|CarePlan|CareTeam|CatalogEntry|ChargeItem|ChargeItemDefinition|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition|Consent|Contract|Coverage|CoverageEligibilityRequest|CoverageEligibilityResponse|DetectedIssue|Device|DeviceDefinition|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EffectEvidenceSynthesis|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|EventDefinition|Evidence|EvidenceVariable|ExampleScenario|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingStudy|Immunization|ImmunizationEvaluation|ImmunizationRecommendation|ImplementationGuide|InsurancePlan|Invoice|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationKnowledge|MedicationRequest|MedicationStatement|MedicinalProduct|MedicinalProductAuthorization|MedicinalProductContraindication|MedicinalProductIndication|MedicinalProductIngredient|MedicinalProductInteraction|MedicinalProductManufactured|MedicinalProductPackaged|MedicinalProductPharmaceutical|MedicinalProductUndesirableEffect|MessageDefinition|MessageHeader|MolecularSequence|NamingSystem|NutritionOrder|Observation|ObservationDefinition|OperationDefinition|OperationOutcome|Organization|OrganizationAffiliation|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|Provenance|Questionnaire|QuestionnaireResponse|RelatedPerson|RequestGroup|ResearchDefinition|ResearchElementDefinition|ResearchStudy|ResearchSubject|RiskAssessment|RiskEvidenceSynthesis|Schedule|SearchParameter|ServiceRequest|Slot|Specimen|SpecimenDefinition|StructureDefinition|StructureMap|Subscription|Substance|SubstanceNucleicAcid|SubstancePolymer|SubstanceProtein|SubstanceReferenceInformation|SubstanceSourceMaterial|SubstanceSpecification|SupplyDelivery|SupplyRequest|Task|TerminologyCapabilities|TestReport|TestScript|ValueSet|VerificationResult|VisionPrescription)\/[A-Za-z0-9\-\.]{1,64}(\/_history\/[A-Za-z0-9\-\.]{1,64})?$`) + +var ( + ErrInvalidAbsoluteURL = errors.New("invalid absolute uri") + ErrInvalidRelativeURI = errors.New("invalid relative uri") + ErrInvalidURL = errors.New("invalid url") + ErrInvalidURI = errors.New("invalid reference uri") + ErrFragmentMissingType = errors.New("fragment reference missing type") + ErrReferenceOneOfResourceNotSet = errors.New("reference.oneof_resource was not set") +) + +// oneofReferenceDescriptor returns the FieldDescriptor for the oneof option that has been set +// within the Reference. An error is returned if no option is set. +func oneofReferenceDescriptor(x *dtpb.Reference) (protoreflect.FieldDescriptor, error) { + msg := x.ProtoReflect() + oneofDescriptor := msg.Descriptor().Oneofs().ByName("reference") + fd := msg.WhichOneof(oneofDescriptor) + if fd == nil { + return nil, ErrReferenceOneOfResourceNotSet + } + return fd, nil +} + +// IdentityOf returns a complete Identity (Type and ID always set, +// VersionID set if applicable) representing the given reference. +func IdentityOf(ref *dtpb.Reference) (*resource.Identity, error) { + // Fragment + if ref.GetFragment() != nil { + if refType := ref.GetType(); refType != nil { + return resource.NewIdentity(refType.GetValue(), ref.GetFragment().GetValue(), "") + } + return nil, ErrFragmentMissingType + } + + // Absolute and Relative URIs + if uri := ref.GetUri(); uri != nil { + return IdentityFromURL(uri.GetValue()) + } + return identityOfStrong(ref) +} + +func identityOfStrong(ref *dtpb.Reference) (*resource.Identity, error) { + fd, err := oneofReferenceDescriptor(ref) + if err != nil { + return nil, err + } + + // All "reference" fields are named "[resource_type]_id" in the FHIR protos. + // If we chop off "_id" and convert to camel-case, it's the resource type + // name. + resType := string(fd.Name()) + resType, _ = strings.CutSuffix(resType, "_id") + resType = strcase.ToCamel(resType) + + m := ref.ProtoReflect().Get(fd).Message().Interface() + refID, ok := m.(*dtpb.ReferenceId) + if !ok { + return nil, fmt.Errorf("unable to extract refID") + } + identID := refID.GetValue() + identVersion := refID.GetHistory().GetValue() + + return resource.NewIdentity(resType, identID, identVersion) +} + +// IdentityFromURL returns a complete Identity (Type and ID always +// set, VersionID set if applicable) representing the resource at the given +// relative or absolute URL, which may optionally include a _history component. +func IdentityFromURL(url string) (*resource.Identity, error) { + lit, err := LiteralInfoFromURI(url) + if err != nil { + return nil, err + } + if lit.identity == nil { + return nil, ErrInvalidURL + } + return lit.identity, nil +} + +// IdentityFromAbsoluteURL returns a complete Identity (Type and ID always +// set, VersionID set if applicable) representing the resource at the given +// absolute URL, which may optionally include a _history component. It +// validates the url against FHIR-provided regex for FHIR REST servers. +// Example absolute: "https://healthcare.googleapis.com/v1/projects/${project}/locations/${location}/datasets/${dataset}/fhirStores/${fhirStore}/fhir/Patient/123/_history/abc" +// Example relative: "Patient/123/_history/abc" +func IdentityFromAbsoluteURL(url string) (*resource.Identity, error) { + lit, err := LiteralInfoFromURI(url) + if err != nil { + return nil, err + } + if lit.identity == nil || lit.serviceBaseURL == "" { + return nil, ErrInvalidAbsoluteURL + } + return lit.identity, nil +} + +// IdentityFromRelativeURI returns a complete Identity (Type and ID always +// set, VersionID set if applicable) representing the resource at the given +// relative URI, which may optionally include a _history component. +func IdentityFromRelativeURI(uri string) (*resource.Identity, error) { + uriParts := strings.Split(uri, "/") + switch len(uriParts) { + case 2: + // e.g. Patient/123 + return resource.NewIdentity(uriParts[0], uriParts[1], "") + case 4: + // e.g. Patient/123/_history/abc + if uriParts[2] != "_history" { + break + } + return resource.NewIdentity(uriParts[0], uriParts[1], uriParts[3]) + } + return nil, fmt.Errorf("%w: %s", ErrInvalidRelativeURI, uri) +} diff --git a/internal/element/reference/identity_test.go b/internal/element/reference/identity_test.go new file mode 100644 index 0000000..778ad85 --- /dev/null +++ b/internal/element/reference/identity_test.go @@ -0,0 +1,306 @@ +package reference_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/element/reference" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +func newIdentity(t *testing.T, typeName, id, version string) *resource.Identity { + t.Helper() + ident, err := resource.NewIdentity(typeName, id, version) + if err != nil { + t.Fatalf("NewIdentity: %v", err) + } + return ident +} + +func TestIdentityOf_BadReference_ReturnsError(t *testing.T) { + testCases := []struct { + name string + reference *dtpb.Reference + wantErr error + }{ + { + "invalid absolute uri", + &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: &dtpb.String{ + Value: "https://example.com", + }, + }, + }, + reference.ErrInvalidURI, + }, + { + "fragment without type", + &dtpb.Reference{ + Reference: &dtpb.Reference_Fragment{ + Fragment: &dtpb.String{ + Value: "https://example.com", + }, + }, + }, + reference.ErrFragmentMissingType, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := reference.IdentityOf(tc.reference) + + got, want := err, tc.wantErr + if !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("IdentityOf(%s) error got '%v', want '%v'", tc.name, got, want) + } + }) + } +} + +func TestIdentityOf(t *testing.T) { + testCases := []struct { + name string + reference *dtpb.Reference + want *resource.Identity + }{ + { + "Reference", + &dtpb.Reference{ + Reference: &dtpb.Reference_AccountId{ + AccountId: &dtpb.ReferenceId{Value: "123"}, + }, + }, + newIdentity(t, "Account", "123", ""), + }, + { + "Reference with history", + &dtpb.Reference{ + Reference: &dtpb.Reference_PatientId{ + PatientId: &dtpb.ReferenceId{Value: "123", History: &dtpb.Id{Value: "abc"}}, + }, + }, + newIdentity(t, "Patient", "123", "abc"), + }, + { + "Relative uri reference", + &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: &dtpb.String{ + Value: "Patient/123", + }, + }, + }, + newIdentity(t, "Patient", "123", ""), + }, + { + "Relative uri reference with history", + &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: &dtpb.String{ + Value: "Patient/123/_history/abc", + }, + }, + }, + newIdentity(t, "Patient", "123", "abc"), + }, + { + "Absolute uri reference", + &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: &dtpb.String{ + Value: "https://healthcare.googleapis.com/v1/projects/123/locations/abc/datasets/def/fhirStores/ghi/fhir/Patient/123", + }, + }, + }, + newIdentity(t, "Patient", "123", ""), + }, + { + "Absolute uri reference with history", + &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: &dtpb.String{ + Value: "https://healthcare.googleapis.com/v1/projects/123/locations/abc/datasets/def/fhirStores/ghi/fhir/Patient/123/_history/abc", + }, + }, + }, + newIdentity(t, "Patient", "123", "abc"), + }, + { + "Fragment reference", + &dtpb.Reference{ + Reference: &dtpb.Reference_Fragment{ + Fragment: &dtpb.String{ + Value: "123", + }, + }, + Type: &dtpb.Uri{ + Value: "Patient", + }, + }, + newIdentity(t, "Patient", "123", ""), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ident, err := reference.IdentityOf(tc.reference) + + if err != nil { + t.Fatalf("IdentityOf(%s) error got %v, want nil", tc.name, err) + } + if got, want := ident, tc.want; !got.Equal(want) { + t.Errorf("IdentityOf(%s) got %s, want %s", tc.name, got, want) + } + }) + } +} + +func TestIdentityFromAbsoluteURL_BadInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + url string + wantErr error + }{ + { + "url with fragment", + "https://healthcare.googleapis.com/v1/projects/123/locations/abc/datasets/def/fhirStores/ghi/fhir/Patient/123#crID", + reference.ErrInvalidURI, + }, + { + "canonical url", + "https://healthcare.googleapis.com/v1/projects/123/locations/abc/datasets/def/fhirStores/ghi/fhir/Patient/123|1", + reference.ErrInvalidURI, + }, + { + "invalid server url", + "https://not/a/fhir/server/url", + cmpopts.AnyError, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := reference.IdentityFromAbsoluteURL(tc.url) + + got, want := err, tc.wantErr + if !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("IdentityFromAbsoluteURL(%s) error got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestIdentityFromAbsoluteURL(t *testing.T) { + testCases := []struct { + name string + url string + want *resource.Identity + }{ + { + "no version", + "https://healthcare.googleapis.com/v1/projects/123/locations/abc/datasets/def/fhirStores/ghi/fhir/Patient/123", + newIdentity(t, "Patient", "123", ""), + }, + { + "versioned", + "https://healthcare.googleapis.com/v1/projects/123/locations/abc/datasets/def/fhirStores/ghi/fhir/Patient/123/_history/abc", + newIdentity(t, "Patient", "123", "abc"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ident, err := reference.IdentityFromAbsoluteURL(tc.url) + + if err != nil { + t.Fatalf("IdentityFromAbsoluteURL(%s) error got %v, want nil", tc.name, err) + } + if got, want := ident, tc.want; !cmp.Equal(got, want) { + t.Errorf("IdentityFromAbsoluteURL(%s) got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestIdentityFromRelativeURI_BadURL_ReturnsError(t *testing.T) { + _, err := reference.IdentityFromRelativeURI("Patient/123/_history") + + if got, want := err, reference.ErrInvalidRelativeURI; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Fatalf("IdentityFromRelativeURI error got %v, want %v", got, want) + } +} + +func TestIdentityFromRelativeURI(t *testing.T) { + testCases := []struct { + name string + url string + want *resource.Identity + }{ + { + "no version", + "Patient/123", + newIdentity(t, "Patient", "123", ""), + }, + { + "versioned", + "Patient/123/_history/abc", + newIdentity(t, "Patient", "123", "abc"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ident, err := reference.IdentityFromRelativeURI(tc.url) + + if err != nil { + t.Fatalf("IdentityFromRelativeURI(%s) error got %v, want nil", tc.name, err) + } + if got, want := ident, tc.want; !cmp.Equal(got, want) { + t.Errorf("IdentityFromRelativeURI(%s) got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestIdentityFromURL_BadURL_ReturnsError(t *testing.T) { + _, err := reference.IdentityFromURL("Patient/123/_history") + + if got, want := err, reference.ErrInvalidURI; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Fatalf("IdentityFromURL error got %v, want %v", got, want) + } +} + +func TestIdentityFromURL(t *testing.T) { + testCases := []struct { + name string + url string + want *resource.Identity + }{ + { + "absolute url", + "https://healthcare.googleapis.com/v1/projects/123/locations/abc/datasets/def/fhirStores/ghi/fhir/Patient/123", + newIdentity(t, "Patient", "123", ""), + }, + { + "relative uri", + "Patient/123/_history/abc", + newIdentity(t, "Patient", "123", "abc"), + }, + { + "relative uri using RequestGroup resource type", + "RequestGroup/123/_history/abc", + newIdentity(t, "RequestGroup", "123", "abc"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ident, err := reference.IdentityFromURL(tc.url) + + if err != nil { + t.Fatalf("IdentityFromURL(%s) error got %v, want nil", tc.name, err) + } + if got, want := ident, tc.want; !cmp.Equal(got, want) { + t.Errorf("IdentityFromURL(%s) got %v, want %v", tc.name, got, want) + } + }) + } +} diff --git a/internal/element/reference/literal.go b/internal/element/reference/literal.go new file mode 100644 index 0000000..b9980bd --- /dev/null +++ b/internal/element/reference/literal.go @@ -0,0 +1,346 @@ +package reference + +import ( + "errors" + "fmt" + "net/url" + "strings" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +var ( + ErrNotLiteral error = errors.New("Reference is not a literal") + ErrTypeInvalid error = errors.New("Reference.type is invalid") + ErrTypeInconsistent error = errors.New("Reference.reference and Reference.type are inconsistent") + ErrWeakInvalid error = errors.New("Reference.reference is invalid weak URI") + ErrStrongInvalid error = errors.New("Reference.reference is invalid strong URI") + ErrExplicitFragmentInvalid error = errors.New("Reference.reference is invalid explicit fragment") + ErrServiceBaseURLInvalid error = errors.New("ServiceBaseURL is invalid") +) + +// A LiteralInfo is the result of parsing a FHIR Reference when that reference +// contains a literal reference. It is immutable. +// +// LiteralInfo supports all valid forms a literal reference: +// - REST URI: absolute and relative, versioned and unversioned, +// strong and weak. +// - FragmentID: a reference to a contained resource within an implied +// containing resource. +// - non-REST URI: URNs (eg., UUID and OID) and versionless canonicals. +// These occur most commonly in request bundles (vs response bundles). +// +// A key goal of LiteralInfo is to canonicalize between the "weak" and "strong" +// representations of a literal reference. Note that this distinction is +// specific to the GCP proto mapping of FHIR and does not appear in the standard. +// +// The fields fragmentID, identity, and nonRESTURI are mutually exclusive: exactly +// one will be non-nil. +// +// See FHIR spec: http://hl7.org/fhir/R4/references.html#literal. +type LiteralInfo struct { + // If non-nil, the type of the referenced resource. + // If non-nil and identity also present, both guaranteed to have the same type. + resType *resource.Type + + // If non-nil, the fragment ID that is relative to an implied containing resource. + // A fragment ID may be an empty string, which indicates the implied + // containing resource itself (as opposed to a contained resource within + // the containing resource). + fragmentID *string + + // The identity (type, ID and optional version) of the referenced resource. + // Applies to REST URLs + identity *resource.Identity + + // The service base URL of the FHIR store that holds the referenced resource. + // Only non-empty if identity is non-nil and the reference is absolute. + // Generally references to resources in the same store (as the reference itself) + // are relative, and the service base URL will be empty. + // See http://hl7.org/fhir/R4/http.html#general + serviceBaseURL string + + // If non-nil, the non-REST URI of the referenced resource. + // Typically an URN (UUID or OID). See NonRESTURI() accessor for details. + nonRESTURI *string +} + +// clone returns a shallow copy. +// +// All fields have immutable types, so a shallow copy is safe. +// LiteralInfo itself is immutable; thus, clone() is only used internally +// by the With*() mutators. +func (lit *LiteralInfo) clone() *LiteralInfo { + return &LiteralInfo{ + resType: lit.resType, + fragmentID: lit.fragmentID, + identity: lit.identity, + serviceBaseURL: lit.serviceBaseURL, + nonRESTURI: lit.nonRESTURI, + } +} + +// Type returns the resource type of the reference resource, if known. +func (lit *LiteralInfo) Type() (resource.Type, bool) { + if lit.resType == nil { + var emptyType resource.Type + return emptyType, false + } + return *lit.resType, true +} + +// FragmentID returns the fragment ID of the reference fragment, +// if the literal reference is to fragment. +func (lit *LiteralInfo) FragmentID() (string, bool) { + if lit.fragmentID == nil { + return "", false + } + return *lit.fragmentID, true +} + +// Identity returns the identity of the reference resource, +// if the referenced resource has REST identity. +func (lit *LiteralInfo) Identity() (*resource.Identity, bool) { + return lit.identity, lit.identity != nil +} + +// Identity returns the identity of the reference resource. +// Only non-empty if the reference resource has a REST identity, +// and that REST identity was absolute (not relative). +func (lit *LiteralInfo) ServiceBaseURL() string { + return lit.serviceBaseURL +} + +// WithServiceBaseURL returns a LiteralInfo with the given serviceBaseURL. +// The given serviceBaseURL must be empty or a valid serviceBaseURL. +func (lit *LiteralInfo) WithServiceBaseURL(serviceBaseURL string) (*LiteralInfo, error) { + // WATCHOUT: Our regex wants a trailing slash, but our convention is to omit it. + // We add the slash here rather than change the regex to keep traceability to the spec. + if serviceBaseURL != "" && !restFHIRServiceBaseURLRegex.MatchString(serviceBaseURL+"/") { + return nil, fmt.Errorf("%w: '%s'", ErrServiceBaseURLInvalid, serviceBaseURL) + } + if serviceBaseURL == lit.serviceBaseURL { + return lit, nil + } + newLit := lit.clone() + newLit.serviceBaseURL = serviceBaseURL + return newLit, nil +} + +// NonRESTURI returns the non-REST URI of the referenced resource, +// if the referenced resource has a non-REST URI. A non-REST URI +// is typically an URN (UUID or OID) or a versionless canonical URL. +// Th URN form is typically only used in bundle transaction requests +// when the REST URI is not yet known. +// +// See http://hl7.org/fhir/R4/references.html#literal, and specifically: +// - "... in a bundle during a transaction, reference URLs may actually +// contain logical URIs (e.g. OIDs or UUIDs) that resolve within the +// transaction." +// - "The URL may contain a reference to a canonical URL and applications can +// use the canonical URL resolution methods they support ..." +func (lit *LiteralInfo) NonRESTURI() (string, bool) { + if lit.nonRESTURI == nil { + return "", false + } + return *lit.nonRESTURI, true +} + +// URI returns the LiteralInfo's URI equivalent as a *dtpb.Uri. +func (lit *LiteralInfo) URI() *dtpb.Uri { + s := lit.URIString() + if s == "" { + return nil + } + return fhir.URI(s) +} + +// URIString returns the LiteralInfo's URI equivalent as a string. +// The returned URI is suitable for use in Reference.reference. +// This function is the inverse of LiteralInfoFromURI(). +func (lit *LiteralInfo) URIString() string { + if lit == nil { + return "" + } + if lit.fragmentID != nil { + return "#" + *lit.fragmentID + } + if lit.identity != nil { + uri := lit.identity.PreferRelativeVersionedURIString() + // Could use net/url.JoinPath but it does validation on serviceBaseURL + // that we don't want. + if lit.serviceBaseURL != "" { + uri = lit.serviceBaseURL + "/" + uri + } + return uri + } + if lit.nonRESTURI != nil { + return *lit.nonRESTURI + } + // UNREACHED + return "" +} + +// PreferRelativeVersionedURIString returns the relative URI with version if +// available, otherwise just relative URI without version. +func (lit *LiteralInfo) PreferRelativeVersionedURIString() string { + identity, ok := lit.Identity() + if !ok { + return "" + } + return identity.PreferRelativeVersionedURIString() +} + +// LiteralInfoOf parses the given literal reference. +// +// Returns an error if the given reference is not a valid literal reference. +func LiteralInfoOf(ref *dtpb.Reference) (*LiteralInfo, error) { + var explicitType *resource.Type + if resTypeStr := ref.GetType(); resTypeStr != nil { + t, err := resource.NewType(resTypeStr.GetValue()) + if err != nil { + // Returned err includes the bad type, so no need to duplicate here. + return nil, fmt.Errorf("%w: %v", ErrTypeInvalid, err) + } + explicitType = &t + } + + // Fragment + if frag := ref.GetFragment(); frag != nil { + fragStr := frag.GetValue() + // WATCHOUT: an empty fragStr is a valid fragID but not a valid ID. + if fragStr != "" && !fhir.IsID(fragStr) { + return nil, fmt.Errorf("%w: invalid fragment ID", ErrExplicitFragmentInvalid) + } + return &LiteralInfo{ + resType: explicitType, + fragmentID: &fragStr, + }, nil + } + + // An absolute or relative weak URI, a weak fragment URI, or a non-REST URI. + if uri := ref.GetUri(); uri != nil { + litUri, err := LiteralInfoFromURI(uri.GetValue()) + if err != nil { + return nil, fmt.Errorf("%w: uri='%v': %v", ErrWeakInvalid, uri.GetValue(), err) + } + if litUri.resType == nil { + // This will occur for a fragment or URN. + litUri.resType = explicitType + } else { + if explicitType != nil && *litUri.resType != *explicitType { + return nil, fmt.Errorf("%w: weak='%v' vs type='%v'", + ErrTypeInconsistent, *litUri.resType, explicitType) + } + } + return litUri, nil + } + + // Strong relative URIs + identity, err := identityOfStrong(ref) + if err != nil { + if errors.Is(err, ErrReferenceOneOfResourceNotSet) { + return nil, ErrNotLiteral + } + return nil, fmt.Errorf("%w: %v", ErrStrongInvalid, err) + } + strongType := identity.Type() + if explicitType != nil && *explicitType != strongType { + return nil, fmt.Errorf("%w: strong='%v' vs type='%v'", + ErrTypeInconsistent, strongType, explicitType) + } + return &LiteralInfo{ + resType: &strongType, + identity: identity, + }, nil +} + +// LiteralInfoFromURI parses a Reference.reference URI string, +// where Reference is the FHIR element. Within the GCP proto mapping +// (where Reference.reference is a oneof), this function parses +// the Reference.uri member of that oneof. +// +// Parsing a Reference URI is easier than parsing general resource URL: +// - We don't need to worry about canonical URLs. (AFIAK). Canonical URLs +// are there own datatype, not a Reference element. +// - Fragments within Reference URI are always relative to the containing +// resource. That is, Reference.reference cannot specify a containing +// resource (relative or absolute) reference that is suffixed by a fragment. +// (Canonical references do allow this). +// +// This supports the special case of an fragment without an ID. This is used +// by a contained resource to reference its containing resource. +// +// Any returned error does NOT include the uri. That is left to the caller. +func LiteralInfoFromURI(uri string) (*LiteralInfo, error) { + if uri[0] == '#' { + // Note that "#" alone is a valid fragment. + fragStr := uri[1:] + if fragStr != "" && !fhir.IsID(fragStr) { + return nil, fmt.Errorf("%w: invalid fragment id", ErrInvalidURI) + } + return &LiteralInfo{fragmentID: &fragStr}, nil + } + if strings.Contains(uri, "#") { + return nil, fmt.Errorf("%w: non-relative fragment found", ErrInvalidURI) + } + if strings.Contains(uri, "|") { + return nil, fmt.Errorf("%w: canonical version found", ErrInvalidURI) + } + + // This regexp matches both relative and absolute REST URIs. + uriIndexes := restFHIRServiceResourceURLRegex.FindStringSubmatchIndex(uri) + + if uriIndexes == nil { + // Try as a non-REST URI (typically an URN or a versionless canonical). + parsedUrl, err := url.Parse(uri) + if err != nil { + return nil, fmt.Errorf("%w: unparsable: %v", ErrInvalidURI, err) + } + if parsedUrl.Scheme == "" { + return nil, fmt.Errorf("%w: non-REST and missing scheme component", ErrInvalidURI) + } + if parsedUrl.Opaque != "" || strings.TrimLeft(parsedUrl.Path, "/") != "" { + // An URN is parsed as Opaque and a canonical is parsed as a Path. + return &LiteralInfo{ + nonRESTURI: &uri, + }, nil + } + return nil, fmt.Errorf("%w: non-REST and missing both Opaque and Path", ErrInvalidURI) + } + + // The relative portion of the URI is the 4th submatch in the regexp. + relStartIdx := uriIndexes[8] + baseUrl := strings.TrimRight(uri[0:relStartIdx], "/") + relUrl := uri[relStartIdx:] + + // The REST regexp could be used to identify all the parts of the relative URI, + // but easier just to split. + relParts := strings.Split(relUrl, "/") + var identity *resource.Identity + if !resource.IsType(relParts[0]) { + return nil, fmt.Errorf("%w: resource type is invalid", ErrInvalidURI) + } + if !fhir.IsID(relParts[1]) { + return nil, fmt.Errorf("%w: resource id is invalid", ErrInvalidURI) + } + if len(relParts) == 2 { + identity, _ = resource.NewIdentity(relParts[0], relParts[1], "") + } else if len(relParts) == 4 && relParts[2] == "_history" { + if !fhir.IsID(relParts[3]) { + return nil, fmt.Errorf("%w: version id is invalid", ErrInvalidURI) + } + identity, _ = resource.NewIdentity(relParts[0], relParts[1], relParts[3]) + } else { + return nil, fmt.Errorf("%w: invalid relative component", ErrInvalidURI) + } + + identityType := identity.Type() + return &LiteralInfo{ + resType: &identityType, + identity: identity, + serviceBaseURL: baseUrl, + }, nil +} diff --git a/internal/element/reference/literal_test.go b/internal/element/reference/literal_test.go new file mode 100644 index 0000000..d5cd712 --- /dev/null +++ b/internal/element/reference/literal_test.go @@ -0,0 +1,361 @@ +package reference + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +var patientType, _ = resource.NewType("Patient") + +func TestLiteralInfoGetters_WhenEmpty(t *testing.T) { + // WATCHOUT: This empty literal isn't realistic. + lit := &LiteralInfo{} + _, ok := lit.Type() + if ok { + t.Errorf("Expected Type not ok") + } + _, ok = lit.FragmentID() + if ok { + t.Errorf("Expected FragmentID not ok") + } + _, ok = lit.Identity() + if ok { + t.Errorf("Expected Identity not ok") + } + url := lit.ServiceBaseURL() + if url != "" { + t.Errorf("Expected ServiceBaseURL empty") + } + _, ok = lit.NonRESTURI() + if ok { + t.Errorf("Expected NonRESTURI not ok") + } +} + +func TestLiteralInfoGetters_WhenFull(t *testing.T) { + wantFrag := "frag1" + wantIdent := mustNewIdentity("Patient", "123", "") + wantBaseURL := "https://my.site/fhir" + wantNonRESTURI := "nonrest" + // WATCHOUT: This full literal isn't realistic. + lit := &LiteralInfo{resType: &patientType, fragmentID: &wantFrag, + identity: wantIdent, serviceBaseURL: wantBaseURL, + nonRESTURI: &wantNonRESTURI} + + gotType, ok := lit.Type() + if !ok { + t.Errorf("Expected Type ok") + } + if gotType != patientType { + t.Errorf("Expected Patient type: %s", gotType) + } + + gotFrag, ok := lit.FragmentID() + if !ok { + t.Errorf("Expected FragmentID ok") + } + if gotFrag != wantFrag { + t.Errorf("FragmentID(): got %s, want %s", gotFrag, wantFrag) + } + + gotIdent, ok := lit.Identity() + if !ok { + t.Errorf("Expected Identity ok") + } + if gotIdent != wantIdent { + t.Errorf("Identity(): got %s, want %s", gotIdent, wantIdent) + } + + gotBaseURL := lit.ServiceBaseURL() + if gotBaseURL != wantBaseURL { + t.Errorf("Wrong ServiceBaseURL got %s, want %s", gotBaseURL, wantBaseURL) + } + + gotNonRESTURI, ok := lit.NonRESTURI() + if !ok { + t.Errorf("Expected NonRESTURI ok") + } + if gotNonRESTURI != wantNonRESTURI { + t.Errorf("Wrong NonRESTURI got %s, want %s", gotNonRESTURI, wantNonRESTURI) + } +} + +func TestLiteralInfoSetters_WithServiceBaseURL(t *testing.T) { + baseUrl := "http://fhir.my.com/scope/teststore" + + testCases := []struct { + name string + startLit *LiteralInfo + setBaseUrl string + wantLit *LiteralInfo + wantErr error + }{ + { + name: "clear serviceBaseURL", + startLit: &LiteralInfo{serviceBaseURL: baseUrl}, + setBaseUrl: "", + wantLit: &LiteralInfo{serviceBaseURL: ""}, + }, + { + name: "set serviceBaseURL", + startLit: &LiteralInfo{}, + setBaseUrl: baseUrl, + wantLit: &LiteralInfo{serviceBaseURL: baseUrl}, + }, + { + name: "bad serviceBaseURL", + startLit: &LiteralInfo{}, + setBaseUrl: "this is not valid URL", + wantErr: ErrServiceBaseURLInvalid, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotLit, gotErr := tc.startLit.WithServiceBaseURL(tc.setBaseUrl) + if !cmp.Equal(gotErr, tc.wantErr, cmpopts.EquateErrors()) { + t.Fatalf("SetServiceBaseURL(%s) error mismatch: got '%v', want '%v'", + tc.name, gotErr, tc.wantErr) + } + if diff := cmp.Diff(gotLit, tc.wantLit, cmp.AllowUnexported(LiteralInfo{})); diff != "" { + t.Errorf("SetServiceBaseURL(%s) literal (-got, +want)\n%s\n", tc.name, diff) + } + }) + } +} + +func TestLiteralInfo_PreferRelativeVersionedURIString(t *testing.T) { + baseUrl := "http://fhir.my.com/scope/teststore" + + testCases := []struct { + name string + startLit *LiteralInfo + wantRelativeURI string + }{ + { + name: "Get relativeURI with version", + startLit: &LiteralInfo{serviceBaseURL: baseUrl, identity: mustNewIdentity("Patient", "1234", "abc")}, + wantRelativeURI: "Patient/1234/_history/abc", + }, + { + name: "Get relativeURI without version", + startLit: &LiteralInfo{serviceBaseURL: baseUrl, identity: mustNewIdentity("Patient", "1234", "")}, + wantRelativeURI: "Patient/1234", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotRelativeURI := tc.startLit.PreferRelativeVersionedURIString() + if diff := cmp.Diff(gotRelativeURI, tc.wantRelativeURI); diff != "" { + t.Errorf("relativeURI(%s) of literal (-got, +want)\n%s\n", tc.name, diff) + } + }) + } +} + +// Tests converting a Reference -> Literal -> URI. +// This isn't strictly a round trip, but close enough. +func TestLiteralInfo_Reference_RoundTrip(t *testing.T) { + strongRef, _ := Typed("Patient", "123") + strongVersionedRef := &dtpb.Reference{ + Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_PatientId{ + PatientId: &dtpb.ReferenceId{Value: "123", History: fhir.ID("abc")}, + }, + } + inconsistentStrongRef, _ := Typed("Patient", "123") + inconsistentStrongRef.Type = fhir.URI("Person") + testCases := []struct { + name string + inputRef *dtpb.Reference + wantLit *LiteralInfo // vs result of calling LiteralInfoOf(inputRef) + wantErr error + wantUri string // vs result of calling gotLit.URI() + }{ + // equivalent weak and strong relative unversioned + { + name: "weak basic", + inputRef: Weak("Patient", "Patient/123"), + wantLit: &LiteralInfo{resType: &patientType, identity: mustNewIdentity("Patient", "123", "")}, + wantUri: "Patient/123", + }, + { + name: "weak basic no type", + inputRef: &dtpb.Reference{Reference: &dtpb.Reference_Uri{Uri: fhir.String("Patient/123")}}, + wantLit: &LiteralInfo{resType: &patientType, identity: mustNewIdentity("Patient", "123", "")}, + wantUri: "Patient/123", + }, + { + name: "strong basic", + inputRef: strongRef, + wantLit: &LiteralInfo{resType: &patientType, identity: mustNewIdentity("Patient", "123", "")}, + wantUri: "Patient/123", + }, + + // equivalent weak and strong relative versioned + { + name: "weak versioned", + inputRef: Weak("Patient", "Patient/123/_history/abc"), + wantLit: &LiteralInfo{resType: &patientType, identity: mustNewIdentity("Patient", "123", "abc")}, + wantUri: "Patient/123/_history/abc", + }, + { + name: "strong versioned", + inputRef: strongVersionedRef, + wantLit: &LiteralInfo{resType: &patientType, identity: mustNewIdentity("Patient", "123", "abc")}, + wantUri: "Patient/123/_history/abc", + }, + + // equivalent good uri (weak-like) and explicit fragments + { + name: "uri fragment", + inputRef: &dtpb.Reference{Type: fhir.URI("Patient"), Reference: &dtpb.Reference_Uri{Uri: fhir.String("#hello")}}, + wantLit: &LiteralInfo{resType: &patientType, fragmentID: ptrString("hello")}, + wantUri: "#hello", + }, + { + name: "explicit fragment", + inputRef: &dtpb.Reference{Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_Fragment{Fragment: fhir.String("hello")}}, + wantLit: &LiteralInfo{resType: &patientType, fragmentID: ptrString("hello")}, + wantUri: "#hello", + }, + + // various valid edge cases of fragments + { + name: "empty uri fragment", + inputRef: &dtpb.Reference{Type: fhir.URI("Patient"), Reference: &dtpb.Reference_Uri{Uri: fhir.String("#")}}, + wantLit: &LiteralInfo{resType: &patientType, fragmentID: ptrString("")}, + wantUri: "#", + }, + { + name: "empty explicit fragment", + inputRef: &dtpb.Reference{Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_Fragment{Fragment: fhir.String("")}}, + wantLit: &LiteralInfo{resType: &patientType, fragmentID: ptrString("")}, + wantUri: "#", + }, + { + name: "uri fragment no type", + inputRef: &dtpb.Reference{Reference: &dtpb.Reference_Uri{Uri: fhir.String("#hello")}}, + wantLit: &LiteralInfo{fragmentID: ptrString("hello")}, + wantUri: "#hello", + }, + + // equivalent invalid uri and explicit fragments + { + name: "bad uri fragment", + inputRef: &dtpb.Reference{Type: fhir.URI("Patient"), Reference: &dtpb.Reference_Uri{Uri: fhir.String("#@@@@")}}, + wantErr: ErrWeakInvalid, + }, + { + name: "bad explicit fragment", + inputRef: &dtpb.Reference{Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_Fragment{Fragment: fhir.String("@@@@")}}, + wantErr: ErrExplicitFragmentInvalid, + }, + + {"nil ref", nil, nil, ErrNotLiteral, ""}, + {"empty", &dtpb.Reference{}, nil, ErrNotLiteral, ""}, + {"type only", &dtpb.Reference{Type: fhir.URI("Patient")}, nil, ErrNotLiteral, ""}, + {"bad type", &dtpb.Reference{Type: fhir.URI("Blah")}, nil, ErrTypeInvalid, ""}, + {"bad weak", Weak("Patient", "bogus-uri"), nil, ErrWeakInvalid, ""}, + {"weak w/frag", Weak("Patient", "Patient/124#blah"), nil, ErrWeakInvalid, ""}, + {"weak w/other type", Weak("Person", "Patient/124"), nil, ErrTypeInconsistent, ""}, + {"strong w/other type", inconsistentStrongRef, nil, ErrTypeInconsistent, ""}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotLit, gotErr := LiteralInfoOf(tc.inputRef) + if !cmp.Equal(gotErr, tc.wantErr, cmpopts.EquateErrors()) { + t.Fatalf("RoundTrip(%s) LiteralInfoOf() error got '%v', want '%v'", + tc.name, gotErr, tc.wantErr) + } + if diff := cmp.Diff(gotLit, tc.wantLit, cmp.AllowUnexported(LiteralInfo{})); diff != "" { + t.Errorf("RoundTrip(%s) LiteralInfoOf() mismatch (-got, +want)\n%s\n", tc.name, diff) + } + gotUri := gotLit.URI().GetValue() + if gotUri != tc.wantUri { + t.Errorf("RoundTrip(%s) URI() mismatch: got '%s', want '%s'", tc.name, gotUri, tc.wantUri) + } + }) + } +} + +func TestLiteralInfo_URI_RoundTrip(t *testing.T) { + baseUrl := "http://fhir.my.com/scope/teststore" + testCases := []struct { + name string + inputUri string + wantLit *LiteralInfo + wantErr error + }{ + {"rel-uri", "Patient/1234", + &LiteralInfo{resType: &patientType, identity: mustNewIdentity("Patient", "1234", "")}, nil}, + {"rel-vers-uri", "Patient/1234/_history/abc", + &LiteralInfo{resType: &patientType, identity: mustNewIdentity("Patient", "1234", "abc")}, nil}, + {"abs-uri", baseUrl + "/Patient/1234", + &LiteralInfo{resType: &patientType, serviceBaseURL: baseUrl, identity: mustNewIdentity("Patient", "1234", "")}, nil}, + {"abs-vers-uri", baseUrl + "/Patient/1234/_history/abc", + &LiteralInfo{resType: &patientType, serviceBaseURL: baseUrl, identity: mustNewIdentity("Patient", "1234", "abc")}, nil}, + + {"rel-missing-vid", "Patient/1234/_history", nil, ErrInvalidURI}, + {"rel-bad-type", "NotAPatient/1234", nil, ErrInvalidURI}, + {"rel-bad-resource-id", "Patient/@@@@", nil, ErrInvalidURI}, + {"rel-bad-version-id", "Patient/1234/_history/@@@@", nil, ErrInvalidURI}, + + {"frag-normal", "#1234", &LiteralInfo{fragmentID: ptrString("1234")}, nil}, + {"frag-bad-id", "#@@@@", nil, ErrInvalidURI}, + + // This is special case. Note that the frag string is empty but present (not nil). + {"frag-container", "#", &LiteralInfo{fragmentID: ptrString("")}, nil}, + + {"bad-nonlocal-frag", "Patient/1234#frag1", nil, ErrInvalidURI}, + {"bad-canonical-version", "Patient/1234|v12", nil, ErrInvalidURI}, + {"bad-history-token", "Patient/1234/nothistory/abc", nil, ErrInvalidURI}, + {"bad-abs-uri", "my.site.com/Patient/1234", nil, ErrInvalidURI}, + + {"urn-uuid", "urn:uuid:1234", + &LiteralInfo{nonRESTURI: ptrString("urn:uuid:1234")}, nil}, + {"urn-bad", "urn:", nil, ErrInvalidURI}, + {"canonical-http", "http://example.com/my-thing", + &LiteralInfo{nonRESTURI: ptrString("http://example.com/my-thing")}, nil}, + {"canonical-bad", "http://example.com/", nil, ErrInvalidURI}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotLit, gotErr := LiteralInfoFromURI(tc.inputUri) + if !cmp.Equal(gotErr, tc.wantErr, cmpopts.EquateErrors()) { + t.Fatalf("RoundTrip(%s): LiteralInfoFromURI() error got '%v', want '%v'. Got LiteralInfo='%v'", + tc.name, gotErr, tc.wantErr, gotLit) + } + if diff := cmp.Diff(gotLit, tc.wantLit, cmp.AllowUnexported(LiteralInfo{})); diff != "" { + t.Errorf("RoundTrip(%s): LiteralInfoFromURI() literal (-got, +want)\n%s\n", tc.name, diff) + } + gotUri := gotLit.URI().GetValue() + wantUri := tc.inputUri + if tc.wantErr != nil { + wantUri = "" + } + if gotUri != wantUri { + t.Errorf("RoundTrip(%s): URI() got '%s', want '%s'", tc.name, gotUri, wantUri) + } + }) + } +} + +func mustNewIdentity(resType, id, version string) *resource.Identity { + identity, err := resource.NewIdentity(resType, id, version) + if err != nil { + panic(err) + } + return identity +} + +func ptrString(s string) *string { + return &s +} diff --git a/internal/element/reference/reference.go b/internal/element/reference/reference.go new file mode 100644 index 0000000..1fb6aff --- /dev/null +++ b/internal/element/reference/reference.go @@ -0,0 +1,184 @@ +package reference + +import ( + "errors" + "fmt" + "path" + + "github.com/google/fhir/go/jsonformat" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element" + "github.com/verily-src/fhirpath-go/internal/resource" + "github.com/verily-src/fhirpath-go/internal/protofields" + "google.golang.org/protobuf/proto" +) + +var ( + ErrNotCanonicalResource = errors.New("resource type is not a Canonical resource") + ErrNotResource = errors.New("not a resource type") + ErrNoResourceID = errors.New("no resource ID") + ErrNoResourceVersion = errors.New("no resource version") + ErrStrongConversion = errors.New("error converting to strong reference from weak reference") +) + +// ExtractAll finds all references contained in a resource. +func ExtractAll(resource fhir.Resource) ([]*dtpb.Reference, error) { + return element.ExtractAll[*dtpb.Reference](resource) +} + +// Canonical creates a Canonical reference for the given resource type and +// raw reference. This will error if the provided resource type is not +// a Canonical FHIR resource. +func Canonical(resourceType resource.Type, reference string) (*dtpb.Reference, error) { + res, ok := protofields.Resources[resourceType.String()] + if !ok { + return nil, fmt.Errorf("%w: %T", ErrNotResource, resourceType) + } + testResource := res.New() + _, isCanonical := testResource.(fhir.CanonicalResource) + if !isCanonical { + return nil, fmt.Errorf("%w: %T", ErrNotCanonicalResource, resourceType) + } + + return Weak(resourceType, reference), nil +} + +// Weak creates an weak reference for the given resource type and +// raw reference. The resource type must be a valid FHIR resource type. +// Generally, it is preferred to use Canonoical() for Canonical references. +// Examples: +// +// { +// "type": "Questionnaire", +// "reference": "https://example.com/questionnaire" +// } +// +// { +// "type": "GuidanceResponse", +// "reference": "urn:uuid:5a17b7c2-e01c-4bc7-b973-31d4156b11d7" +// } +// +// For more info on references see: +// - https://www.hl7.org/fhir/references.html#canonical +// - https://www.hl7.org/fhir/bundle.html#references +func Weak(resourceType resource.Type, reference string) *dtpb.Reference { + return &dtpb.Reference{ + Type: fhir.URI(resourceType.String()), + Reference: &dtpb.Reference_Uri{ + Uri: fhir.String(reference), + }, + } +} + +// Typed creates a typed, literal FHIR reference for the given resource type and id. +// Returns an error if resourceId is invalid or resourceType is outside +// of the known R4 types (which should be impossible). +func Typed(resourceType resource.Type, resourceId string) (*dtpb.Reference, error) { + return typedFromURIString(resourceType, path.Join(resourceType.String(), resourceId)) +} + +func typedFromURIString(resourceType resource.Type, uri string) (*dtpb.Reference, error) { + weakReference := Weak(resourceType, uri) + message := proto.Clone(weakReference) + err := jsonformat.NormalizeReference(message) + if err != nil { + return nil, fmt.Errorf("error normalizing reference: %w", err) + } + typedReference := message.(*dtpb.Reference) + // If conversion to a typed reference succeeded then the untyped URI reference + // will be removed and replaced with a different option in the oneof. + // https://github.com/google/fhir/blob/master/proto/google/fhir/proto/r4/core/datatypes.proto#L3303-L3304 + if typedReference.GetUri().GetValue() != "" { + return nil, fmt.Errorf("%w: %v", ErrStrongConversion, typedReference) + } + return message.(*dtpb.Reference), nil +} + +// Logical creates a logical FHIR reference for the given resource type, identifier +// system, and identifier value. +// Replaces ph.LogicalReference +func Logical(resourceType resource.Type, identifierSystem, identifierValue string) *dtpb.Reference { + return &dtpb.Reference{ + Type: fhir.URI(resourceType.String()), + Identifier: fhir.Identifier(identifierSystem, identifierValue), + } +} + +// LogicalReferenceIdentifier creates a logical FHIR reference for a given resource type and +// Indentifier. +// Replaces: ph.LogicalReferenceIdentifier +func LogicalFromIdentifier(resourceType resource.Type, identifier *dtpb.Identifier) *dtpb.Reference { + return &dtpb.Reference{ + Type: fhir.URI(resourceType.String()), + Identifier: identifier, + } +} + +// TypedFromResource returns a reference to the given resource that +// is strongly typed and without a version. +func TypedFromResource(res fhir.Resource) (*dtpb.Reference, error) { + return Typed(resource.TypeOf(res), resource.ID(res)) +} + +// TypedFromIdentity returns a reference to the given identity that +// is always relative, always strongly typed, and will include a version ID +// if-and-only-if the given identity does. +func TypedFromIdentity(identity *resource.Identity) *dtpb.Reference { + ref, err := typedFromURIString(identity.Type(), identity.PreferRelativeVersionedURIString()) + if err != nil { + // Impossible to trigger this error (or at least I couldn't find a way). + panic(err) + } + return ref +} + +// WeakRelativeVersioned returns a reference to the given resource +// is is weakly typed, relative (to the FHIR service base URL), and versioned. +// It returns an error if either the resource's ID or version is missing. +func WeakRelativeVersioned(res fhir.Resource) (*dtpb.Reference, error) { + identity, ok := resource.IdentityOf(res) + if !ok { + return nil, ErrNoResourceID // Missing ID seems most likely cause... + } + uri, ok := identity.RelativeVersionedURIString() + if !ok { + return nil, ErrNoResourceVersion + } + return Weak(identity.Type(), uri), nil +} + +// Is compares two references for referencial equality. +// If lhs can be determined to refer to the same resource as rhs, then this +// returns true -- otherwise this returns false. If the underlying resource +// cannot be determined, then the result is false. +func Is(lhs, rhs *dtpb.Reference) bool { + // If the two references have the same value layout, we don't need to do + // more complicated extraction. + if proto.Equal(lhs, rhs) { + return true + } + + // Check for logical referencial equality + if li, ri := lhs.GetIdentifier(), rhs.GetIdentifier(); !proto.Equal(li, ri) { + return false + } + + // Check for literal referencial equality. This requires determining the literal + // identity of the referenced resource, since protos represent references as + // either a URI _or_ a strongly-typed reference ID in a oneof field. This will + // extract the relevant parts to allow for proper equality. + if lhs.Reference != nil || rhs.Reference != nil { + left, err := IdentityOf(lhs) + if err != nil { + return false + } + right, err := IdentityOf(rhs) + if err != nil { + return false + } + + return left.Equal(right) + } + return true +} diff --git a/internal/element/reference/reference_test.go b/internal/element/reference/reference_test.go new file mode 100644 index 0000000..cb3c0fc --- /dev/null +++ b/internal/element/reference/reference_test.go @@ -0,0 +1,455 @@ +package reference_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + acpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/account_go_proto" + appb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/element/reference" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/testing/protocmp" +) + +func Test_ExtractAll_ValuesCorrect(t *testing.T) { + refUri := &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: fhir.String("uri-ref"), + }, + } + refRelatedPersonId := &dtpb.Reference{ + Reference: &dtpb.Reference_RelatedPersonId{ + RelatedPersonId: &dtpb.ReferenceId{ + Id: fhir.String("related-ref"), + }, + }, + } + testCases := []struct { + name string + resource fhir.Resource + references []*dtpb.Reference + }{ + { + name: "No reference", + resource: fhirtest.NewResource(t, "Patient"), + }, + { + name: "Single reference", + resource: fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.ManagingOrganization = refUri + })), + references: []*dtpb.Reference{refUri}, + }, + { + name: "Multiple references", + resource: fhirtest.NewResource(t, "Appointment", fhirtest.WithResourceModification(func(a *appb.Appointment) { + a.Participant = []*appb.Appointment_Participant{ + { + Type: []*dtpb.CodeableConcept{fhir.CodeableConcept("", fhir.Coding("systest", "code"))}, + Actor: refUri, + }, + { + Actor: refRelatedPersonId, + }, + } + })), + references: []*dtpb.Reference{refRelatedPersonId, refUri}, + }, + { + name: "Repeated field references", + resource: fhirtest.NewResource(t, "Account", fhirtest.WithResourceModification(func(a *acpb.Account) { + a.Subject = []*dtpb.Reference{refRelatedPersonId, refUri} + })), + references: []*dtpb.Reference{refRelatedPersonId, refUri}, + }, + { + name: "Repeated identical references", + resource: fhirtest.NewResource(t, "Account", fhirtest.WithResourceModification(func(a *acpb.Account) { + a.Subject = []*dtpb.Reference{refRelatedPersonId, refRelatedPersonId} + })), + references: []*dtpb.Reference{refRelatedPersonId, refRelatedPersonId}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + gotReferences, _ := reference.ExtractAll(testCase.resource) + + opts := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortSlices(func(a *dtpb.Reference, b *dtpb.Reference) bool { return a.String() < b.String() }), + cmpopts.EquateEmpty(), + } + if !cmp.Equal(testCase.references, gotReferences, opts...) { + t.Errorf("ExtractAll(): got '%v', want '%v'", gotReferences, testCase.references) + } + }) + } +} + +func Test_ExtractAll_Modifiable(t *testing.T) { + resource := fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.ManagingOrganization = &dtpb.Reference{ + Reference: &dtpb.Reference_Uri{ + Uri: fhir.String("old-ref"), + }, + } + })).(*ppb.Patient) + + references, _ := reference.ExtractAll(resource) + + if len(references) != 1 { + t.Fatalf("Expected single reference") + } + + references[0].Reference = &dtpb.Reference_Uri{ + Uri: fhir.String("new-ref"), + } + + if got, want := resource.ManagingOrganization, references[0]; got != want { + t.Errorf("ExtractAll() reference update failed, got '%v', want '%v'", got, want) + } +} + +func canonicalReference(t resource.Type, ref string) *dtpb.Reference { + return &dtpb.Reference{ + Type: fhir.URI(t.String()), + Reference: &dtpb.Reference_Uri{ + Uri: fhir.String(ref), + }, + } +} + +func TestCanonical(t *testing.T) { + testCases := []struct { + name string + resourceType resource.Type + reference string + wantError error + }{ + {"canonical reference", "Questionnaire", "https://example.com/questionnaire", nil}, + {"invalid resource type", "NotAResource", "https://example.com/questionnaire", reference.ErrNotResource}, + {"resource reference errors", "Patient", "Patient/123", reference.ErrNotCanonicalResource}, + {"bundle reference errors", "GuidanceResponse", "urn:uuid:5a17b7c2-e01c-4bc7-b973-31d4156b11d7", reference.ErrNotCanonicalResource}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ref, err := reference.Canonical(tc.resourceType, tc.reference) + + if got, want := err, tc.wantError; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Fatalf("Canonical(%s) error got %v, want %v", tc.name, got, want) + } + if tc.wantError == nil { + wantRef := canonicalReference(tc.resourceType, tc.reference) + got, want := ref, wantRef + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Canonical(%s) (-got, +want)\n%s\n", tc.name, diff) + } + } + }) + } +} + +func TestWeak(t *testing.T) { + testCases := []struct { + name string + resourceType resource.Type + reference string + }{ + {"canonical reference", "Questionnaire", "https://example.com/questionnaire"}, + {"resource reference", "Patient", "Patient/123"}, + {"bundle reference", "GuidanceResponse", "urn:uuid:5a17b7c2-e01c-4bc7-b973-31d4156b11d7"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + wantRef := canonicalReference(tc.resourceType, tc.reference) + + ref := reference.Weak(tc.resourceType, tc.reference) + + got, want := ref, wantRef + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Weak(%s) (-got, +want)\n%s\n", tc.name, diff) + } + }) + } +} + +func TestTyped(t *testing.T) { + testCases := []struct { + name string + resourceType resource.Type + resourceId string + wantError error + want *dtpb.Reference + }{ + {"resource reference", "Patient", "test-patient-id", nil, &dtpb.Reference{ + Reference: &dtpb.Reference_PatientId{PatientId: &dtpb.ReferenceId{Value: "test-patient-id"}}, + Type: &dtpb.Uri{Value: "Patient"}, + }}, + {"invalid ref type", "InvalidResource", "5a17b7c2-e01c-4bc7-b973-31d4156b11d7", reference.ErrStrongConversion, nil}, + {"missing ref id", "InvalidResource", "", reference.ErrStrongConversion, nil}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ref, err := reference.Typed(tc.resourceType, tc.resourceId) + + if !cmp.Equal(err, tc.wantError, cmpopts.EquateErrors()) { + t.Fatalf("Typed(%s) error got %v, want %v", tc.name, err, tc.wantError) + } + + if tc.wantError == nil { + got, want := ref, tc.want + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Typed(%s) (-got, +want)\n%s\n", tc.name, diff) + } + } + }) + } +} + +func TestLogical(t *testing.T) { + resourceType := resource.Patient + identifierSystem := "IdentifierSystem" + identifierValue := "IdentifierValue" + reference := reference.Logical(resourceType, identifierSystem, identifierValue) + + if diff := cmp.Diff(reference.GetType().GetValue(), resourceType.String()); diff != "" { + t.Errorf("logicalReference.type (-got, +want)\n%s\n", diff) + } + if diff := cmp.Diff(reference.GetIdentifier().GetSystem().GetValue(), identifierSystem); diff != "" { + t.Errorf("logicalReference.identifier.system (-got, +want)\n%s\n", diff) + } + if diff := cmp.Diff(reference.GetIdentifier().GetValue().GetValue(), identifierValue); diff != "" { + t.Errorf("logicalReference.identifier.value (-got, +want)\n%s\n", diff) + } +} + +func TestLogicalFromIdentifier(t *testing.T) { + resourceType := resource.Patient + identifierSystem := "IdentifierSystem" + identifierValue := "IdentifierValue" + identifier := fhir.Identifier(identifierSystem, identifierValue) + identifierCc := fhir.CodeableConcept("", fhir.Coding("system", "code")) + identifier.Type = identifierCc + + reference := reference.LogicalFromIdentifier(resourceType, identifier) + if diff := cmp.Diff(reference.GetType().GetValue(), resourceType.String()); diff != "" { + t.Errorf("logicalReference.type (-got, +want)\n%s\n", diff) + } + if diff := cmp.Diff(reference.GetIdentifier().GetSystem().GetValue(), identifierSystem); diff != "" { + t.Errorf("logicalReference.identifier.system (-got, +want)\n%s\n", diff) + } + if diff := cmp.Diff(reference.GetIdentifier().GetValue().GetValue(), identifierValue); diff != "" { + t.Errorf("logicalReference.identifier.value (-got, +want)\n%s\n", diff) + } + + actualCc := reference.GetIdentifier().GetType() + if diff := cmp.Diff(identifierCc, actualCc, protocmp.Transform()); diff != "" { + t.Errorf("unexpected proto diff:\n%v", diff) + } +} + +func TestTypedFromResource(t *testing.T) { + patient := &ppb.Patient{Id: fhir.ID("1234"), + Meta: &dtpb.Meta{VersionId: fhir.ID("abcd")}, // ignored + } + got, err := reference.TypedFromResource(patient) + if err != nil { + t.Fatalf("TypedFromResource failed: %v", err) + } + want := &dtpb.Reference{Type: fhir.URI("Patient"), Reference: &dtpb.Reference_PatientId{ + PatientId: &dtpb.ReferenceId{ + Value: "1234", + }, + }} + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("TypedFromResource (-got, +want)\n%s\n", diff) + } +} + +func TestTypedFromResource_WhenMissingId(t *testing.T) { + patient := &ppb.Patient{} // no ID + _, err := reference.TypedFromResource(patient) + wantErr := reference.ErrStrongConversion + if !cmp.Equal(err, wantErr, cmpopts.EquateErrors()) { + t.Errorf("TypedFromResource error got [%v], want [%v]", err, wantErr) + } +} + +func TestTypedFromIdentity(t *testing.T) { + testCases := []struct { + name string + inputIdentity *resource.Identity + wantRef *dtpb.Reference + }{ + { + name: "unversioned", + inputIdentity: mustNewIdentity("Patient", "1234", ""), + wantRef: &dtpb.Reference{ + Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_PatientId{PatientId: &dtpb.ReferenceId{Value: "1234"}}, + }, + }, + { + name: "versioned", + inputIdentity: mustNewIdentity("Patient", "1234", "abc"), + wantRef: &dtpb.Reference{ + Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_PatientId{ + PatientId: &dtpb.ReferenceId{Value: "1234", History: fhir.ID("abc")}}, + }, + }, + { + name: "missing-id", + inputIdentity: mustNewIdentity("Patient", "", ""), + wantRef: &dtpb.Reference{ + Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_PatientId{PatientId: &dtpb.ReferenceId{Value: ""}}, + }, + }, + { + name: "missing-id with version", + inputIdentity: mustNewIdentity("Patient", "", "abc"), + wantRef: &dtpb.Reference{ + Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_PatientId{ + PatientId: &dtpb.ReferenceId{Value: "", History: fhir.ID("abc")}}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotRef := reference.TypedFromIdentity(tc.inputIdentity) + if diff := cmp.Diff(gotRef, tc.wantRef, protocmp.Transform()); diff != "" { + t.Errorf("TypedFromIdentity(%s) (-got, +want)\n%s\n", tc.name, diff) + } + }) + } +} + +func TestWeakRelativeVersioned(t *testing.T) { + testCases := []struct { + name string + inputRes fhir.Resource + wantRef *dtpb.Reference + wantError error + }{ + {"normal", + &ppb.Patient{Id: fhir.ID("1234"), Meta: &dtpb.Meta{VersionId: fhir.ID("abcd")}}, + &dtpb.Reference{Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_Uri{Uri: fhir.String("Patient/1234/_history/abcd")}, + }, + nil}, + {"missing-resource-id", &ppb.Patient{}, nil, reference.ErrNoResourceID}, + {"missing-version-id", &ppb.Patient{Id: fhir.ID("1234")}, nil, reference.ErrNoResourceVersion}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := reference.WeakRelativeVersioned(tc.inputRes) + if !cmp.Equal(err, tc.wantError, cmpopts.EquateErrors()) { + t.Errorf("WeakRelativeVersioned(%s) error got [%v], want [%v]", tc.name, err, tc.wantError) + } + if diff := cmp.Diff(got, tc.wantRef, protocmp.Transform()); diff != "" { + t.Errorf("WeakRelativeVersioned(%s) (-got, +want)\n%s\n", tc.name, diff) + } + }) + } +} + +func mustNewIdentity(resourceType, id, versionID string) *resource.Identity { + identity, err := resource.NewIdentity(resourceType, id, versionID) + if err != nil { + panic(err) + } + return identity +} + +func TestIs(t *testing.T) { + const ( + system = "FooSystem" + systemValue = "value" + id = "3591dd62-fdcc-438f-882d-50540d3f5c18" + url = "https://example.com" + ) + testCases := []struct { + name string + left *dtpb.Reference + right *dtpb.Reference + want bool + }{ + { + name: "Two identical logical references", + left: reference.Logical(resource.Account, system, systemValue), + right: reference.Logical(resource.Account, system, systemValue), + want: true, + }, { + name: "Two different logical references", + left: reference.Logical(resource.Account, system, systemValue), + right: reference.Logical(resource.Account, system, systemValue+"-different"), + want: false, + }, { + name: "Two identical literal references", + left: mustNewLiteral(resource.Patient, id), + right: mustNewLiteral(resource.Patient, id), + want: true, + }, { + name: "Two different literal references", + left: mustNewLiteral(resource.Patient, id), + right: mustNewLiteral(resource.Patient, id+"1"), + want: false, + }, { + name: "Two identical literal URLs", + left: reference.Weak(resource.Account, url), + right: reference.Weak(resource.Account, url), + want: true, + }, { + name: "Two different literal URLs", + left: reference.Weak(resource.Account, url), + right: reference.Weak(resource.Account, url+"/something-else"), + want: false, + }, { + name: "Left unknown reference type", + left: reference.Weak(resource.Account, url), + right: mustNewLiteral(resource.Patient, id), + want: false, + }, { + name: "Right unknown reference type", + left: mustNewLiteral(resource.Patient, id), + right: reference.Weak(resource.Account, url), + want: false, + }, { + name: "Left logical, right literal reference", + left: mustNewLiteral(resource.Patient, id), + right: reference.Logical(resource.Account, system, systemValue), + want: false, + }, { + name: "Left literal, right logical reference", + left: reference.Logical(resource.Account, system, systemValue), + right: mustNewLiteral(resource.Patient, id), + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := reference.Is(tc.left, tc.right) + + if got != tc.want { + t.Errorf("Is(%v): got %v, want %v", tc.name, got, tc.want) + } + }) + } +} + +func mustNewLiteral(res resource.Type, id string) *dtpb.Reference { + got, err := reference.Typed(res, id) + if err != nil { + panic(err) + } + return got +} diff --git a/internal/fhir/constraints.go b/internal/fhir/constraints.go new file mode 100644 index 0000000..e900bd0 --- /dev/null +++ b/internal/fhir/constraints.go @@ -0,0 +1,129 @@ +package fhir + +import ( + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" +) + +type primitiveDataType interface { + *dtpb.Base64Binary | + *dtpb.Boolean | + *dtpb.Canonical | + *dtpb.Code | + *dtpb.Date | + *dtpb.DateTime | + *dtpb.Decimal | + *dtpb.Id | + *dtpb.Instant | + *dtpb.Integer | + *dtpb.Markdown | + *dtpb.Oid | + *dtpb.PositiveInt | + *dtpb.String | + *dtpb.Time | + *dtpb.UnsignedInt | + *dtpb.Uri | + *dtpb.Url | + *dtpb.Uuid +} + +type complexDataType interface { + *dtpb.Address | + *dtpb.Age | + *dtpb.Attachment | + *dtpb.CodeableConcept | + *dtpb.Coding | + *dtpb.ContactPoint | + *dtpb.Count | + *dtpb.Distance | + *dtpb.Duration | + *dtpb.HumanName | + *dtpb.Identifier | + *dtpb.Money | + *dtpb.MoneyQuantity | + *dtpb.Period | + *dtpb.Quantity | + *dtpb.Range | + *dtpb.Ratio | + *dtpb.SampledData | + *dtpb.Signature | + *dtpb.SimpleQuantity | + *dtpb.Timing +} + +type metaDataType interface { + *dtpb.ContactDetail | + *dtpb.Contributor | + *dtpb.DataRequirement | + *dtpb.Expression | + *dtpb.ParameterDefinition | + *dtpb.RelatedArtifact | + *dtpb.TriggerDefinition | + *dtpb.UsageContext +} + +type specialPurposeDataType interface { + *dtpb.Dosage | + *dtpb.ElementDefinition | + *dtpb.Extension | + *dtpb.MarketingStatus | + *dtpb.Meta | + *dtpb.Narrative | + *dtpb.ProductShelfLife | + *dtpb.Reference +} + +// DataType is an constraint-definition of FHIR datatypes, which all support ID +// and Extension fields, in addition to their base values. +// +// Note: "DataType" is also an "Element", so these interfaces are logically +// equivalent -- and so this is represented as a constraint of valid datatypes. +// +// The R4 spec doesn't explicitly refer to "DataType" as a distinction from +// "Element", but the R5 spec does, and its definition is compatible with R4. +// This is retained here so that we can have a proper vernacular and mechanism +// for referring to these types in generic ways through constraints. +// +// See https://www.hl7.org/fhir/r5/types.html#DataType for more details. +type DataType interface { + Element + primitiveDataType | complexDataType | metaDataType | specialPurposeDataType +} + +// PrimitiveType is a constraint-definition of FHIR datatypes, which all support ID +// and Extension fields, in addition to their base values. +// +// Note: "DataType" is also an "Element", so these interfaces are logically +// equivalent -- and so this is represented as a constraint of valid datatypes. +// +// The R4 spec doesn't explicitly refer to "PrimitiveType" as a distinction from +// "Element", but the R5 spec does, and its definition is compatible with R4. +// This is retained here so that we can have a proper vernacular and mechanism +// for referring to these types in generic ways through constraints. +// +// See https://www.hl7.org/fhir/types.html#PrimitiveType for more details. +type PrimitiveType interface { + Element + primitiveDataType +} + +// BackboneType is a constraint-definition of FHIR backbone element, which all +// support ID, Extension, and modifier-extension fields, in addition to their +// base values. +// +// Note: "BackboneType" is also an "BackboneElement", so these interfaces are logically +// equivalent -- and so this is represented as a constraint of valid datatypes. +// +// The R4 spec doesn't explicitly refer to "BackboneType" as a distinction from +// "BackboneElement", but the R5 spec does, and its definition is compatible with R4. +// This is retained here so that we can have a proper vernacular and mechanism +// for referring to these types in generic ways through constraints. +// +// See https://www.hl7.org/fhir/r5/types.html#BackboneType for more details. +type BackboneType interface { + BackboneElement + *dtpb.Timing | + *dtpb.ElementDefinition | + *dtpb.MarketingStatus | + *dtpb.ProductShelfLife | + *dtpb.Dosage +} diff --git a/internal/fhir/doc.go b/internal/fhir/doc.go new file mode 100644 index 0000000..19fe986 --- /dev/null +++ b/internal/fhir/doc.go @@ -0,0 +1,13 @@ +/* +Package fhir provides a library for working with R4 Google FHIR protos: +https://github.com/google/fhir. + +This provides various quality-of-life utilities over the base FHIR definitions, +such as: + +- Defining abstract base resources as their respective interfaces +- Creation functions for Elements +- Access utilities for ContainedResources and References +- etc. +*/ +package fhir diff --git a/internal/fhir/duration.go b/internal/fhir/duration.go new file mode 100644 index 0000000..3748134 --- /dev/null +++ b/internal/fhir/duration.go @@ -0,0 +1,110 @@ +package fhir + +import ( + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/units" +) + +// DurationFromTime converts an R4 FHIR Time Element into an R4 FHIR Duration +// value. +// +// If the underlying time has Second-based precision, the returned Duration will +// also have seconds precision; otherwise this will fallback into nanosecond +// precision. +func DurationFromTime(t *dtpb.Time) *dtpb.Duration { + // Constrain the time to 24 hours + duration := time.Microsecond * time.Duration(t.GetValueUs()) % (time.Hour * 24) + + switch t.GetPrecision() { + case dtpb.Time_SECOND: + return Seconds(duration) + case dtpb.Time_MILLISECOND: + return Milliseconds(duration) + case dtpb.Time_MICROSECOND: + fallthrough + default: + return Microseconds(duration) + } +} + +// Duration creates a Duration proto with the provided value, computing the +// largest whole-unit of time that can be used to represent the time. +func Duration(d time.Duration) *dtpb.Duration { + value := d.Nanoseconds() + if value == 0 { + return durationValue(float64(value), units.Days) + } + unitConversions := []struct { + unit units.Time + duration time.Duration + }{ + {units.Days, 24 * time.Hour}, + {units.Hours, time.Hour}, + {units.Minutes, time.Minute}, + {units.Seconds, time.Second}, + {units.Milliseconds, time.Millisecond}, + {units.Microseconds, time.Microsecond}, + {units.Nanoseconds, time.Nanosecond}, + } + for _, conversion := range unitConversions { + if d >= conversion.duration && d == d.Round(conversion.duration) { + numUnits := d / conversion.duration + return durationValue(float64(numUnits), conversion.unit) + } + } + return durationValue(float64(value), units.Nanoseconds) +} + +// Nanoseconds creates a Duration proto with the specified time value, rounded +// to nanosecond accuracy. +func Nanoseconds(value time.Duration) *dtpb.Duration { + return durationValue(float64(value.Nanoseconds()), units.Nanoseconds) +} + +// Milliseconds creates a Duration proto with the specified time value, rounded +// to millisecond accuracy. +func Milliseconds(value time.Duration) *dtpb.Duration { + millis := float64(value.Nanoseconds()) / float64(time.Millisecond.Nanoseconds()) + return durationValue(millis, units.Milliseconds) +} + +// Microseconds creates a Duration proto with the specified time value, rounded +// to microsecond accuracy. +func Microseconds(value time.Duration) *dtpb.Duration { + micros := float64(value.Nanoseconds()) / float64(time.Microsecond.Nanoseconds()) + return durationValue(micros, units.Microseconds) +} + +// Seconds creates a Duration proto with the specified time value, rounded +// to second accuracy. +func Seconds(value time.Duration) *dtpb.Duration { + return durationValue(value.Seconds(), units.Seconds) +} + +// Minutes creates a Duration proto with the specified time value, rounded +// to minute accuracy. +func Minutes(value time.Duration) *dtpb.Duration { + return durationValue(value.Minutes(), units.Minutes) +} + +// Hours creates a Duration proto with the specified time value, rounded +// to hour-accuracy. +func Hours(value time.Duration) *dtpb.Duration { + return durationValue(value.Hours(), units.Hours) +} + +// Days creates a Duration proto with the specified time value, rounded +// to day-accuracy. +func Days(value time.Duration) *dtpb.Duration { + return durationValue(value.Hours()/24, units.Days) +} + +func durationValue(value float64, unit units.Time) *dtpb.Duration { + return &dtpb.Duration{ + Value: Decimal(value), + Code: Code(unit.Symbol()), + System: URI(unit.System()), + } +} diff --git a/internal/fhir/duration_test.go b/internal/fhir/duration_test.go new file mode 100644 index 0000000..60922c8 --- /dev/null +++ b/internal/fhir/duration_test.go @@ -0,0 +1,161 @@ +package fhir_test + +import ( + "testing" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/units" + "google.golang.org/protobuf/testing/protocmp" +) + +func newDuration(value int64, unit units.Time) *dtpb.Duration { + return &dtpb.Duration{ + Value: fhir.Decimal(float64(value)), + Code: fhir.Code(unit.Symbol()), + System: fhir.URI(unit.System()), + } +} + +func TestDurationFromTime(t *testing.T) { + testCases := []struct { + name string + time string + value int64 + multiplier int64 + unit units.Time + }{ + {"Seconds", "00:01:00", 1, int64(time.Minute) / int64(time.Second), units.Seconds}, + {"Milliseconds", "00:01:00.000", 1, int64(time.Minute) / int64(time.Millisecond), units.Milliseconds}, + {"Microseconds", "00:01:00.000000", 1, int64(time.Minute) / int64(time.Microsecond), units.Microseconds}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input := fhir.MustParseTime(tc.time) + value := tc.value * tc.multiplier + want := newDuration(value, tc.unit) + + got := fhir.DurationFromTime(input) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("DurationFromTime(%v): (-got +want):\n%v", tc.name, diff) + } + }) + } +} + +func TestDuration(t *testing.T) { + testCases := []struct { + name string + value int64 + multiplier int64 + unit units.Time + }{ + {"Zero", 0, 1, units.Days}, + {"Nanoseconds", 805, 1, units.Nanoseconds}, + {"Microseconds", 15, int64(time.Microsecond), units.Microseconds}, + {"Milliseconds", 32, int64(time.Millisecond), units.Milliseconds}, + {"Seconds", 42, int64(time.Second), units.Seconds}, + {"Minutes", 1, int64(time.Minute), units.Minutes}, + {"Hours", 12, int64(time.Hour), units.Hours}, + {"Days", 3, 24 * int64(time.Hour), units.Days}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value := tc.value * tc.multiplier + timeDuration := time.Duration(value) + want := newDuration(tc.value, tc.unit) + + got := fhir.Duration(timeDuration) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Duration(%v): (-got +want):\n%v", tc.name, diff) + } + }) + } +} + +func TestNanoseconds(t *testing.T) { + value := time.Duration(400) + want := newDuration(int64(value), units.Nanoseconds) + + got := fhir.Nanoseconds(value) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Nanoseconds: (-got +want):\n%v", diff) + } +} + +func TestMicroseconds(t *testing.T) { + value := time.Duration(400) + duration := time.Microsecond * value + want := newDuration(int64(value), units.Microseconds) + + got := fhir.Microseconds(duration) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Microseconds: (-got +want):\n%v", diff) + } +} + +func TestMilliseconds(t *testing.T) { + value := 400 + duration := time.Millisecond * 400 + want := newDuration(int64(value), units.Milliseconds) + + got := fhir.Milliseconds(duration) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Milliseconds: (-got +want):\n%v", diff) + } +} + +func TestSeconds(t *testing.T) { + value := 400 + duration := time.Second * 400 + want := newDuration(int64(value), units.Seconds) + + got := fhir.Seconds(duration) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Seconds: (-got +want):\n%v", diff) + } +} + +func TestMinutes(t *testing.T) { + value := 400 + duration := time.Minute * 400 + want := newDuration(int64(value), units.Minutes) + + got := fhir.Minutes(duration) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Minutes: (-got +want):\n%v", diff) + } +} + +func TestHours(t *testing.T) { + value := 400 + duration := time.Hour * 400 + want := newDuration(int64(value), units.Hours) + + got := fhir.Hours(duration) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Hours: (-got +want):\n%v", diff) + } +} + +func TestDays(t *testing.T) { + value := 400 + duration := time.Hour * 24 * 400 + want := newDuration(int64(value), units.Days) + + got := fhir.Days(duration) + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Days: (-got +want):\n%v", diff) + } +} diff --git a/internal/fhir/elements_general.go b/internal/fhir/elements_general.go new file mode 100644 index 0000000..b7e9290 --- /dev/null +++ b/internal/fhir/elements_general.go @@ -0,0 +1,307 @@ +package fhir + +import ( + "time" + + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/slices" +) + +const ucumUnitSystem = "http://unitsofmeasure.org" + +// General-Purpose Data Types: +// +// The section below defines types from the "Data Types" heading in +// http://hl7.org/fhir/R4/datatypes.html#open + +// Annotation creates an R4 FHIR Annotation element with the specified text, +// author, and time of creation. +// +// See: http://hl7.org/fhir/R4/datatypes.html#annotation +func Annotation(text, author string, when time.Time) *dtpb.Annotation { + return &dtpb.Annotation{ + Text: Markdown(text), + Author: &dtpb.Annotation_AuthorX{ + Choice: &dtpb.Annotation_AuthorX_StringValue{ + StringValue: String(author), + }, + }, + Time: DateTime(when), + } +} + +// AnnotationReference creates an R4 FHIR Annotation element with the specified +// text, a reference to the author, and the time of creation. +// +// See: http://hl7.org/fhir/R4/datatypes.html#annotation +func AnnotationReference(text string, author *dtpb.Reference, when time.Time) *dtpb.Annotation { + return &dtpb.Annotation{ + Text: Markdown(text), + Author: &dtpb.Annotation_AuthorX{ + Choice: &dtpb.Annotation_AuthorX_Reference{ + Reference: author, + }, + }, + Time: DateTime(when), + } +} + +// Coding creates an R4 FHIR Coding element with the provided system and code. +// +// See: http://hl7.org/fhir/R4/datatypes.html#coding +func Coding(system, code string) *dtpb.Coding { + return &dtpb.Coding{ + System: URI(system), + Code: Code(code), + } +} + +// CodingWithVersion creates an R4 FHIR Coding element with the provided system, +// code, and version. +// +// See: http://hl7.org/fhir/R4/datatypes.html#coding +func CodingWithVersion(system, code, version string) *dtpb.Coding { + return &dtpb.Coding{ + System: URI(system), + Code: Code(code), + Version: String(version), + } +} + +// CodeableConcept creates an R4 FHIR CodeableConcept with the specified codings, +// and with the Text element if the given text argument is non-empty. +// +// Providing a non-empty Text element is good practice but not required. +// See: http://hl7.org/fhir/R4/datatypes.html#codeableconcept +func CodeableConcept(text string, coding ...*dtpb.Coding) *dtpb.CodeableConcept { + concept := &dtpb.CodeableConcept{ + Coding: coding, + } + if text != "" { + concept.Text = String(text) + } + return concept +} + +// ContactPoint creates an R4 FHIR ContactPoint element from the system and value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#contactpoint +func ContactPoint(system cpb.ContactPointSystemCode_Value, value string) *dtpb.ContactPoint { + return &dtpb.ContactPoint{ + System: &dtpb.ContactPoint_SystemCode{ + Value: system, + }, + Value: String(value), + } +} + +// EmailContactPoint creates an R4 FHIR ContactPoint element for the Email +// system given a value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#contactpoint +func EmailContactPoint(value string) *dtpb.ContactPoint { + return ContactPoint(cpb.ContactPointSystemCode_EMAIL, value) +} + +// PhoneContactPoint creates an R4 FHIR ContactPoint element for the Phone +// system given a value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#contactpoint +func PhoneContactPoint(value string) *dtpb.ContactPoint { + return ContactPoint(cpb.ContactPointSystemCode_PHONE, value) +} + +// SmsContactPoint creates an R4 FHIR ContactPoint element for the SMS system +// given a value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#contactpoint +func SmsContactPoint(value string) *dtpb.ContactPoint { + return ContactPoint(cpb.ContactPointSystemCode_SMS, value) +} + +// PagerContactPoint creates an R4 FHIR ContactPoint element for the Pager +// system given a value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#contactpoint +func PagerContactPoint(value string) *dtpb.ContactPoint { + return ContactPoint(cpb.ContactPointSystemCode_PAGER, value) +} + +// FaxContactPoint creates an R4 FHIR ContactPoint element for the Fax system +// given a value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#contactpoint +func FaxContactPoint(value string) *dtpb.ContactPoint { + return ContactPoint(cpb.ContactPointSystemCode_FAX, value) +} + +// OtherContactPoint creates an R4 FHIR ContactPoint element for the Other +// system given a value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#contactpoint +func OtherContactPoint(value string) *dtpb.ContactPoint { + return ContactPoint(cpb.ContactPointSystemCode_OTHER, value) +} + +// Identifier creates an R4 FHIR Identifier element with the provided system +// and value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#identifier +func Identifier(system, value string) *dtpb.Identifier { + return &dtpb.Identifier{ + System: URI(system), + Value: String(value), + } +} + +// Money creates an R4 FHIR Money element from the money value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#Money +func Money(value float64) *dtpb.Money { + return &dtpb.Money{ + Value: Decimal(value), + } +} + +// MoneyQuantity creates an R4 FHIR MoneyQuantity element from the value and units. +// +// See: http://hl7.org/fhir/R4/datatypes.html#MoneyQuantity +func MoneyQuantity(value float64, unit string) *dtpb.MoneyQuantity { + return &dtpb.MoneyQuantity{ + Value: Decimal(value), + Unit: String(unit), + } +} + +// Period creates an R4 FHIR Period element with the provided start and end times. +// +// See: http://hl7.org/fhir/R4/datatypes.html#period +func Period(start, end time.Time) *dtpb.Period { + return &dtpb.Period{ + Start: DateTime(start), + End: DateTime(end), + } +} + +// Quantity creates an R4 FHIR Quantity element from the given value and units. +// +// See: http://hl7.org/fhir/R4/datatypes.html#quantity +func Quantity(value float64, unit string) *dtpb.Quantity { + return &dtpb.Quantity{ + Value: Decimal(value), + Unit: String(unit), + } +} + +// UCUMQuantity creates an R4 FHIR Quantity element representing a +// value and UCUM unit. +// +// See: http://hl7.org/fhir/R4/datatypes.html#quantity +// TODO(PHP-9521): Add a unit package to validate against UCUM units. +func UCUMQuantity(value float64, unit string) *dtpb.Quantity { + return &dtpb.Quantity{ + Value: Decimal(value), + Unit: String(unit), + Code: Code(unit), + System: URI(ucumUnitSystem), + } +} + +// QuantityFromSimpleQuantity is a convenience utility for converting a +// SimpleQuantity to its base-class definition of Quantity. +// If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func QuantityFromSimpleQuantity(value *dtpb.SimpleQuantity) *dtpb.Quantity { + return quantityFrom(value, value == nil) +} + +// QuantityFromDuration is a convenience utility for converting a +// Duration to its base-class definition of Quantity. +// If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func QuantityFromDuration(value *dtpb.Duration) *dtpb.Quantity { + return quantityFrom(value, value == nil) +} + +// QuantityFromMoneyQuantity is a convenience utility for converting a +// MoneyQuantity to its base-class definition of Quantity. +// If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func QuantityFromMoneyQuantity(value *dtpb.MoneyQuantity) *dtpb.Quantity { + return quantityFrom(value, value == nil) +} + +// quantityLike is a helper interface for implementing the QuantityFrom* +// functions. This defines the common interface for all Quantity objects. +type quantityLike interface { + GetValue() *dtpb.Decimal + GetUnit() *dtpb.String + GetSystem() *dtpb.Uri + GetCode() *dtpb.Code +} + +// quantityFrom is a helper function for implementing the QuantityFrom* functions +// which all have the same implementation. +// +// This function takes 'isNil' as a boolean argument to work around the fact that +// a nil pointer passed to an interface forms a non-nil interface in Go, and +// reflection is more costly than a bool check. +func quantityFrom(value quantityLike, isNil bool) *dtpb.Quantity { + if isNil { + return nil + } + return &dtpb.Quantity{ + Value: value.GetValue(), + Unit: value.GetUnit(), + System: value.GetSystem(), + Code: value.GetCode(), + } +} + +// Range creates an R4 FHIR Range element with the given low and high end of the +// range, using the specified units. +// +// See: http://hl7.org/fhir/R4/datatypes.html#range +func Range(low, high float64, unit string) *dtpb.Range { + return &dtpb.Range{ + Low: SimpleQuantity(low, unit), + High: SimpleQuantity(high, unit), + } +} + +// Ratio creates an R4 FHIR Ratio element with the given numerator and denominator. +// +// See: http://hl7.org/fhir/R4/datatypes.html#ratio +func Ratio(numerator, denominator float64) *dtpb.Ratio { + return &dtpb.Ratio{ + Numerator: &dtpb.Quantity{Value: Decimal(numerator)}, + Denominator: &dtpb.Quantity{Value: Decimal(denominator)}, + } +} + +// SimpleQuantity creates an R4 FHIR SimpleQuantity element from the given value +// and units. +// +// See: http://hl7.org/fhir/R4/datatypes.html#SimpleQuantity +func SimpleQuantity(value float64, unit string) *dtpb.SimpleQuantity { + return &dtpb.SimpleQuantity{ + Value: Decimal(value), + Unit: String(unit), + } +} + +// Timing creates an R4 FHIR Timing element observing the events specified in `times`. +// +// See: http://hl7.org/fhir/R4/datatypes.html#timing +func Timing(times ...time.Time) *dtpb.Timing { + return &dtpb.Timing{ + Event: slices.Map(times, DateTime), + } +} diff --git a/internal/fhir/elements_general_test.go b/internal/fhir/elements_general_test.go new file mode 100644 index 0000000..8dd8f4e --- /dev/null +++ b/internal/fhir/elements_general_test.go @@ -0,0 +1,45 @@ +package fhir_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestCodeableConcept_WithoutText(t *testing.T) { + myCoding := fhir.Coding("my-system", "my-code") + yourCoding := fhir.Coding("your-system", "your-code") + testCases := []struct { + name string + text string + codings []*dtpb.Coding + want *dtpb.CodeableConcept + }{ + {"empty", "", nil, &dtpb.CodeableConcept{}}, + {"full", "my-text", []*dtpb.Coding{myCoding, yourCoding}, + &dtpb.CodeableConcept{ + Coding: []*dtpb.Coding{myCoding, yourCoding}, + Text: fhir.String("my-text"), + }, + }, + {"without text", "", []*dtpb.Coding{myCoding}, + &dtpb.CodeableConcept{ + Coding: []*dtpb.Coding{myCoding}, + // The key behavior is the absence of the Text element. + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sut := fhir.CodeableConcept(tc.text, tc.codings...) + if diff := cmp.Diff(tc.want, sut, protocmp.Transform()); diff != "" { + t.Errorf("CodeableConcept mismatch (-want, +got):\n%s", diff) + } + }) + } + +} diff --git a/internal/fhir/elements_metadata.go b/internal/fhir/elements_metadata.go new file mode 100644 index 0000000..bb02072 --- /dev/null +++ b/internal/fhir/elements_metadata.go @@ -0,0 +1,19 @@ +package fhir + +import dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + +// Metadata Types: +// +// The section below defines types from the "MetaDataTypes" heading in +// http://hl7.org/fhir/R4/datatypes.html#open + +// ContactDetail creates an R4 FHIR ContactDetail element from a string value +// and the specified contact-points. +// +// See: http://hl7.org/fhir/R4/metadatatypes.html#ContactDetail +func ContactDetail(name string, telecom ...*dtpb.ContactPoint) *dtpb.ContactDetail { + return &dtpb.ContactDetail{ + Name: String(name), + Telecom: telecom, + } +} diff --git a/internal/fhir/elements_primitive.go b/internal/fhir/elements_primitive.go new file mode 100644 index 0000000..37fa5bf --- /dev/null +++ b/internal/fhir/elements_primitive.go @@ -0,0 +1,343 @@ +package fhir + +import ( + "errors" + "fmt" + "math" + "regexp" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/uuid" + "github.com/verily-src/fhirpath-go/internal/slices" +) + +var ( + // ErrIntegerDataLoss is an error raised in APIs that might unintentionally + // truncate integral values that the user wouldn't expect. + ErrIntegerDataLoss = errors.New("data-loss occurred during integer conversion") +) + +// Primitive Types: +// +// The section below defines types from the "Primitive Types" heading in +// http://hl7.org/fhir/R4/datatypes.html#open + +// Base64Binary creates an R4 FHIR Base64Binary element the specified bytes. +// +// See: https://hl7.org/fhir/R4/datatypes.html#base64Binary +func Base64Binary(value []byte) *dtpb.Base64Binary { + return &dtpb.Base64Binary{ + Value: value, + } +} + +// Boolean creates a Boolean proto from a primitive value. +// +// See: https://hl7.org/fhir/R4/datatypes.html#boolean +func Boolean(value bool) *dtpb.Boolean { + return &dtpb.Boolean{ + Value: value, + } +} + +// Canonical defined in canonical.go + +// Code creates an R4 FHIR Code element from a string value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#code +func Code(value string) *dtpb.Code { + return &dtpb.Code{ + Value: value, + } +} + +// Date and DateTime in time.go + +// Decimal creates an R4 FHIR Decimal element from a float64 (double) value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#decimal +func Decimal(value float64) *dtpb.Decimal { + return &dtpb.Decimal{ + Value: fmt.Sprint(value), + } +} + +// ID creates an R4 FHIR ID element from a string value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#id +func ID(value string) *dtpb.Id { + return &dtpb.Id{ + Value: value, + } +} + +var idRegexp *regexp.Regexp = regexp.MustCompile(`^[A-Za-z0-9\-\.]{1,64}$`) + +// IsID returns true if the given string a valid FHIR ID. +// See http://hl7.org/fhir/R4/datatypes.html#id. +func IsID(id string) bool { + return idRegexp.MatchString(id) +} + +// RandomID generates a new random R4 FHIR ID element. +func RandomID() *dtpb.Id { + return ID(uuid.NewString()) +} + +// Instant in time.go + +// Integer creates an R4 FHIR Integer element from an int32 value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#integer +func Integer(value int32) *dtpb.Integer { + return &dtpb.Integer{ + Value: value, + } +} + +// IntegerFromInt creates an R4 FHIR Integer element from an int value. +// +// Go's int type is architecture dependent, values greater than int32 will be +// truncated, as described in the go tour: https://tour.golang.org/basics/11 +// If this occurs, this function returns an error. +func IntegerFromInt(value int) (*dtpb.Integer, error) { + if val, ok := tryNarrowInt32(value); ok { + return &dtpb.Integer{ + Value: val, + }, nil + } + return nil, fmt.Errorf("integer(%v): %w", value, ErrIntegerDataLoss) +} + +// tryNarrowInt32 is an implementation function used in the `IntegerFromInt` +// that narrows an int to an in32 and tests if there was any truncation. +func tryNarrowInt32(v int) (int32, bool) { + v32 := int32(v) + if int(v32) == v { + return v32, true + } + return 0, false +} + +// IntegerFromPositiveInt attempts to create an R4 FHIR Integer element from a +// PositiveInt value. This function may fail of the value stored in the PositiveInt +// exceeds the cardinality of Integer, which may cause a signed integer overflow. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func IntegerFromPositiveInt(value *dtpb.PositiveInt) (*dtpb.Integer, error) { + return integerFrom(value) +} + +// IntegerFromUnsignedInt attempts to create an R4 FHIR Integer element from an +// UnsignedInt value. This function may fail of the value stored in the UnsignedInt +// exceeds the cardinality of Integer, which may cause a signed integer overflow. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func IntegerFromUnsignedInt(value *dtpb.UnsignedInt) (*dtpb.Integer, error) { + return integerFrom(value) +} + +// integerFrom is an implementation function used in the `IntegerFrom*` +// functions to minimize code duplication. +func integerFrom(value interface{ GetValue() uint32 }) (*dtpb.Integer, error) { + if isLossyConversionToInt32(value.GetValue()) { + return nil, fmt.Errorf("integer(%v): %w", value, ErrIntegerDataLoss) + } + return &dtpb.Integer{ + Value: int32(value.GetValue()), + }, nil +} + +// isLossyConversionToInt32 checks if a uint32 value can be converted to an int32 +// value without losing its original value, which can happen if the sign-bit is +// set in the uint32. +func isLossyConversionToInt32(value uint32) bool { + // This works by testing that casting a uint32 to an int32 and checking that + // the value is still positive. If it changes to negative, the cast was never + // valid. + return value > math.MaxInt32 +} + +// Markdown creates an R4 FHIR Markdown element from a string value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#markdown +func Markdown(value string) *dtpb.Markdown { + return &dtpb.Markdown{ + Value: value, + } +} + +// OID creates an R4 FHIR OID element from a OID-string value, prepending the +// necessary "urn:oid:" to the value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#oid +func OID(value string) *dtpb.Oid { + return &dtpb.Oid{ + Value: fmt.Sprintf("urn:oid:%v", value), + } +} + +// PositiveInt creates an R4 FHIR PositiveInt element from a uint32 value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#positiveInt +func PositiveInt(value uint32) *dtpb.PositiveInt { + return &dtpb.PositiveInt{ + Value: value, + } +} + +// String creates an R4 FHIR String element from a string value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#string +func String(value string) *dtpb.String { + return &dtpb.String{ + Value: value, + } +} + +// Strings creates an array of R4 FHIR String elements from a string value. This +// is offered as a convenience function, since many FHIR protos have arrays of +// FHIR string types, and converting between Go strings and FHIR strings is a +// common and repetitive process for some types. +func Strings(values ...string) []*dtpb.String { + return slices.Map(values, String) +} + +// StringFromCode is a convenience utility for converting a Code to its +// base-class definition of String. If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func StringFromCode(code *dtpb.Code) *dtpb.String { + return stringFrom(code, code == nil) +} + +// StringFromMarkdown is a convenience utility for converting Markdown to its +// base-class definition of String. If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func StringFromMarkdown(markdown *dtpb.Markdown) *dtpb.String { + return stringFrom(markdown, markdown == nil) +} + +// StringFromID is a convenience utility for converting an Id to its +// base-class definition of String. If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func StringFromID(id *dtpb.Id) *dtpb.String { + return stringFrom(id, id == nil) +} + +// stringFrom is an implementation of the `StringFrom*` series of functions to +// cut down on repetition. +// +// This function takes 'isNil' as a boolean argument to work around the fact that +// a nil pointer passed to an interface forms a non-nil interface in Go, and +// reflection is more costly than a bool check. +func stringFrom(value interface{ GetValue() string }, isNil bool) *dtpb.String { + if isNil { + return nil + } + return &dtpb.String{ + Value: value.GetValue(), + } +} + +// Time in time.go + +// UnsignedInt creates an R4 FHIR UnsignedInt element from a uint32 value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#unsignedInt +func UnsignedInt(value uint32) *dtpb.UnsignedInt { + return &dtpb.UnsignedInt{ + Value: value, + } +} + +// URI creates an R4 FHIR URI element from a string value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#uri +func URI(value string) *dtpb.Uri { + return &dtpb.Uri{ + Value: value, + } +} + +// URIFromCanonical is a convenience utility for converting a canonical to its +// base-class definition of URI. If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func URIFromCanonical(canonical *dtpb.Canonical) *dtpb.Uri { + return uriFrom(canonical, canonical == nil) +} + +// URIFromOID is a convenience utility for converting an OID to its +// base-class definition of URI. If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func URIFromOID(oid *dtpb.Oid) *dtpb.Uri { + return uriFrom(oid, oid == nil) +} + +// URIFromURL is a convenience utility for converting a URL to its +// base-class definition of URI. If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func URIFromURL(url *dtpb.Url) *dtpb.Uri { + return uriFrom(url, url == nil) +} + +// URIFromUUID is a convenience utility for converting a UUID to its +// base-class definition of URI. If the input is nil, this returns nil. +// +// For more information, see the diagram for Primitive Types here: +// https://www.hl7.org/fhir/datatypes.html +func URIFromUUID(uuid *dtpb.Uuid) *dtpb.Uri { + return uriFrom(uuid, uuid == nil) +} + +// uriFrom is a convenience helper for implementing the various `UriFrom*` +// functions which are all the same repetative logic. +// +// This function takes 'isNil' as a boolean argument to work around the fact that +// a nil pointer passed to an interface forms a non-nil interface in Go, and +// reflection is more costly than a bool check. +func uriFrom(other interface{ GetValue() string }, isNil bool) *dtpb.Uri { + if isNil { + return nil + } + return URI(other.GetValue()) +} + +// URL creates an R4 FHIR URL element from a string value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#url +func URL(value string) *dtpb.Url { + return &dtpb.Url{ + Value: value, + } +} + +// UUID creates an R4 FHIR UUID element from a uuid-string value, prepending the +// necessary "urn:uuid:" to the value. +// +// See: http://hl7.org/fhir/R4/datatypes.html#uuid +func UUID(value string) *dtpb.Uuid { + return &dtpb.Uuid{ + Value: fmt.Sprintf("urn:uuid:%v", value), + } +} + +// RandomUUID generates a random new UUID. +// +// See: http://hl7.org/fhir/R4/datatypes.html#uuid +func RandomUUID() *dtpb.Uuid { + return UUID(uuid.NewString()) +} diff --git a/internal/fhir/elements_primitive_test.go b/internal/fhir/elements_primitive_test.go new file mode 100644 index 0000000..0a76bd4 --- /dev/null +++ b/internal/fhir/elements_primitive_test.go @@ -0,0 +1,390 @@ +package fhir_test + +import ( + "math" + "strings" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element/canonical" +) + +func TestBase64Binary(t *testing.T) { + want := []byte{0xde, 0xad, 0xbe, 0xef} + + sut := fhir.Base64Binary(want) + + if got := sut.GetValue(); !cmp.Equal(got, want) { + t.Errorf("Base64Binary: got %v, want %v", got, want) + } +} + +func TestBoolean(t *testing.T) { + want := true + + sut := fhir.Boolean(want) + + if got := sut.GetValue(); !cmp.Equal(got, want) { + t.Errorf("Boolean: got %v, want %v", got, want) + } +} + +func TestCode(t *testing.T) { + want := "value" + + sut := fhir.Code(want) + + if got := sut.GetValue(); !cmp.Equal(got, want) { + t.Errorf("Code: got %v, want %v", got, want) + } +} + +func TestID(t *testing.T) { + want := "id" + + sut := fhir.ID(want) + + if got := sut.GetValue(); !cmp.Equal(got, want) { + t.Errorf("ID: got %v, want %v", got, want) + } +} + +func TestIsID(t *testing.T) { + testCases := []struct { + name string + inputId string + wantOk bool + }{ + {"empty", "", false}, + {"typical", "NormalId-.123", true}, + {"valid inside bogus", "&&&hello", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotOk := fhir.IsID(tc.inputId) + if gotOk != tc.wantOk { + t.Errorf("IsID(%s) ok mismatch: got %v, want %v", tc.name, gotOk, tc.wantOk) + } + }) + } +} + +func TestInteger(t *testing.T) { + want := int32(42) + + sut := fhir.Integer(want) + + if got := sut.GetValue(); !cmp.Equal(got, want) { + t.Errorf("Integer: got %v, want %v", got, want) + } +} + +func TestIntegerFromInt_Truncates_ReturnsError(t *testing.T) { + input := math.MaxInt64 + + _, err := fhir.IntegerFromInt(input) + + if got, want := err, fhir.ErrIntegerDataLoss; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("IntegerFromInt: got %v, err %v", got, want) + } +} + +func TestIntegerFromInt_ValidValue_ReturnsInteger(t *testing.T) { + testCases := []struct { + name string + value int + }{ + {"Zero", 0}, + {"MaxInt32", math.MaxInt32}, + {"MinInt32", math.MinInt32}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sut, err := fhir.IntegerFromInt(tc.value) + if err != nil { + t.Fatalf("IntegerFromInt(%v): unexpected error '%v'", tc.name, err) + } + + if got, want := sut.GetValue(), tc.value; got != int32(want) { + t.Errorf("IntegerFromInt(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestIntegerFromPositiveInt_Truncates_ReturnsError(t *testing.T) { + testCases := []struct { + name string + value uint32 + }{ + {"OneOverInt32Max", math.MaxInt32 + 1}, + {"UInt32Max", math.MaxUint32}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fhir.IntegerFromPositiveInt(fhir.PositiveInt(tc.value)) + + if got, want := err, fhir.ErrIntegerDataLoss; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("IntegerFromPositiveInt(%v): got %v, err %v", tc.name, got, want) + } + }) + } +} + +func TestIntegerFromPositiveInt_ValidValue_ReturnsInteger(t *testing.T) { + testCases := []struct { + name string + value uint32 + }{ + {"Zero", 0}, + {"MaxInt32", math.MaxInt32}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sut, err := fhir.IntegerFromPositiveInt(fhir.PositiveInt(tc.value)) + if err != nil { + t.Fatalf("IntegerFromPositiveInt(%v): unexpected error '%v'", tc.name, err) + } + + if got, want := sut.GetValue(), tc.value; got != int32(want) { + t.Errorf("IntegerFromPositiveInt(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestIntegerFromUnsignedInt_Truncates_ReturnsError(t *testing.T) { + testCases := []struct { + name string + value uint32 + }{ + {"OneOverInt32Max", math.MaxInt32 + 1}, + {"UInt32Max", math.MaxUint32}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fhir.IntegerFromUnsignedInt(fhir.UnsignedInt(tc.value)) + + if got, want := err, fhir.ErrIntegerDataLoss; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("IntegerFromUnsignedInt(%v): got %v, err %v", tc.name, got, want) + } + }) + } +} + +func TestIntegerFromUnsignedInt_ValidValue_ReturnsInteger(t *testing.T) { + testCases := []struct { + name string + value uint32 + }{ + {"Zero", 0}, + {"MaxInt32", math.MaxInt32}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sut, err := fhir.IntegerFromUnsignedInt(fhir.UnsignedInt(tc.value)) + if err != nil { + t.Fatalf("IntegerFromUnsignedInt(%v): unexpected error '%v'", tc.name, err) + } + + if got, want := sut.GetValue(), tc.value; got != int32(want) { + t.Errorf("IntegerFromUnsignedInt(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestMarkdown(t *testing.T) { + want := "This is **basically** just for code _coverage_; let's be honest." + + sut := fhir.Markdown(want) + + if got := sut.GetValue(); !cmp.Equal(got, want) { + t.Errorf("Markdown: got %v, want %v", got, want) + } +} + +func TestOID(t *testing.T) { + const wantPrefix = "urn:oid:" + want := "foobar" + + sut := fhir.OID(want) + + if got := sut.GetValue(); !strings.HasPrefix(got, wantPrefix) { + t.Errorf("OID: got value '%v', want prefix '%v'", wantPrefix, got) + } + if got := sut.GetValue(); !strings.HasSuffix(got, want) { + t.Errorf("OID: got value '%v', want suffix '%v'", want, got) + } +} + +func TestString(t *testing.T) { + want := "Lorem ipsum" + + sut := fhir.String(want) + + if got := sut.GetValue(); !cmp.Equal(got, want) { + t.Errorf("String: got %v, want %v", got, want) + } +} + +func TestStrings(t *testing.T) { + want := []string{"Lorem", "ipsum", "dalor", "sit", "amet"} + toString := func(s *dtpb.String) string { return s.GetValue() } + + got := fhir.Strings(want...) + + if got := slices.Map(got, toString); !cmp.Equal(got, want) { + t.Errorf("String: got %v, want %v", got, want) + } +} + +func TestStringFromCode(t *testing.T) { + testCases := []struct { + name string + value *dtpb.Code + }{ + {"Nil", nil}, + {"WithValue", fhir.Code("foobar")}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := fhir.StringFromCode(tc.value) + + if got, want := got.GetValue(), tc.value.GetValue(); got != want { + t.Errorf("StringFromCode(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestStringFromMarkdown(t *testing.T) { + testCases := []struct { + name string + value *dtpb.Markdown + }{ + {"Nil", nil}, + {"WithValue", fhir.Markdown("foobar")}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := fhir.StringFromMarkdown(tc.value) + + if got, want := got.GetValue(), tc.value.GetValue(); got != want { + t.Errorf("StringFromMarkdown(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestStringFromID(t *testing.T) { + testCases := []struct { + name string + value *dtpb.Id + }{ + {"Nil", nil}, + {"WithValue", fhir.ID("foobar")}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := fhir.StringFromID(tc.value) + + if got, want := got.GetValue(), tc.value.GetValue(); got != want { + t.Errorf("StringFromID(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestURIFromCanonical(t *testing.T) { + testCases := []struct { + name string + value *dtpb.Canonical + }{ + {"Nil", nil}, + {"WithValue", canonical.New("https://some-uri")}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := fhir.URIFromCanonical(tc.value) + + if got, want := got.GetValue(), tc.value.GetValue(); got != want { + t.Errorf("URIFromCanonical(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestURIFromOID(t *testing.T) { + testCases := []struct { + name string + value *dtpb.Oid + }{ + {"Nil", nil}, + {"WithValue", fhir.OID("https://some-uri")}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := fhir.URIFromOID(tc.value) + + if got, want := got.GetValue(), tc.value.GetValue(); got != want { + t.Errorf("URIFromOID(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestURIFromURL(t *testing.T) { + testCases := []struct { + name string + value *dtpb.Url + }{ + {"Nil", nil}, + {"WithValue", fhir.URL("https://some-uri.com")}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := fhir.URIFromURL(tc.value) + + if got, want := got.GetValue(), tc.value.GetValue(); got != want { + t.Errorf("URIFromURL(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestURIFromUUID(t *testing.T) { + testCases := []struct { + name string + value *dtpb.Uuid + }{ + {"Nil", nil}, + {"WithValue", fhir.RandomUUID()}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := fhir.URIFromUUID(tc.value) + + if got, want := got.GetValue(), tc.value.GetValue(); got != want { + t.Errorf("URIFromUUID(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} diff --git a/internal/fhir/elements_special.go b/internal/fhir/elements_special.go new file mode 100644 index 0000000..629148f --- /dev/null +++ b/internal/fhir/elements_special.go @@ -0,0 +1,26 @@ +package fhir + +import dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + +// Special Types: +// +// The section below defines types from the "Special Types" heading in +// http://hl7.org/fhir/R4/datatypes.html#open + +// Narrative creates a R4 FHIR Narrative element from a string value. +// +// See: http://hl7.org/fhir/R4/narrative.html +func Narrative(value string) *dtpb.Narrative { + return &dtpb.Narrative{ + Div: XHTML(value), + } +} + +// XHTML creates an R4 FHIR XHTML element from a string value. +// +// See: http://hl7.org/fhir/R4/narrative.html#xhtml +func XHTML(value string) *dtpb.Xhtml { + return &dtpb.Xhtml{ + Value: value, + } +} diff --git a/internal/fhir/elements_special_test.go b/internal/fhir/elements_special_test.go new file mode 100644 index 0000000..5c30b6e --- /dev/null +++ b/internal/fhir/elements_special_test.go @@ -0,0 +1,28 @@ +package fhir_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +func TestNarrative(t *testing.T) { + want := "<blah></blah>" + + sut := fhir.Narrative(want) + + if got := sut.GetDiv().GetValue(); !cmp.Equal(got, want) { + t.Errorf("Narrative: got %v, want %v", got, want) + } +} + +func TestXHTML(t *testing.T) { + want := "<blah></blah>" + + sut := fhir.XHTML(want) + + if got := sut.GetValue(); !cmp.Equal(got, want) { + t.Errorf("XHTML: got %v, want %v", got, want) + } +} diff --git a/internal/fhir/encoding.go b/internal/fhir/encoding.go new file mode 100644 index 0000000..2297dbe --- /dev/null +++ b/internal/fhir/encoding.go @@ -0,0 +1,26 @@ +package fhir + +import "strings" + +// These characters have special meaning in FHIR Search queries +const SearchSpecialChars = `\,$|` + +// Escape values intended for use as a parameter in a FHIR Search. +// +// These characters have special meaning in Search queries and must be backslash escaped: +// +// `\`, `|`, `,`, `$` +// +// This function assumes that URL-encoding is performed later. (Percent +// encoding is automatically handled by the healthcare client library when +// query params are passed as a map.) +// +// For example, `foo,bar` becomes `foo\,bar` +func EscapeSearchParam(value string) string { + out := value + for _, crune := range SearchSpecialChars { + c := string(crune) + out = strings.ReplaceAll(out, c, `\`+c) + } + return out +} diff --git a/internal/fhir/encoding_test.go b/internal/fhir/encoding_test.go new file mode 100644 index 0000000..d8f3815 --- /dev/null +++ b/internal/fhir/encoding_test.go @@ -0,0 +1,33 @@ +package fhir_test + +import ( + "fmt" + "testing" + + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +func TestEscapeSearchParam(t *testing.T) { + + testCases := []struct { + input string + want string + }{ + {``, ``}, + {`\`, `\\`}, + {`$`, `\$`}, + {`,`, `\,`}, + {`|`, `\|`}, + {`C:\bin\go foo, bar, baz | omg $500!`, `C:\\bin\\go foo\, bar\, baz \| omg \$500!`}, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("testCases[%d]", i), func(t *testing.T) { + got := fhir.EscapeSearchParam(tc.input) + + if got != tc.want { + t.Errorf("got %#v, want %#v", got, tc.want) + } + }) + } +} diff --git a/internal/fhir/iface.go b/internal/fhir/iface.go new file mode 100644 index 0000000..4dc2919 --- /dev/null +++ b/internal/fhir/iface.go @@ -0,0 +1,146 @@ +package fhir + +import ( + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +// Base is the interface-definition of the FHIR abstract base type which is the +// ancestor of all FHIR objects (both resources and elements). +// +// Represented in Go, this simply embeds the proto.Message interface, since this +// is a utility for the google/fhir proto definitions. +type Base interface { + proto.Message +} + +// Resource is the interface-definition of the FHIR Abstract base type which is +// the ancestor of all FHIR resources. +// See https://www.hl7.org/fhir/r4/resource.html#Resource for more details. +// +// This interface is defined by embedding the proto.Message interface, since all +// FHIR resources in this library must also be proto.Message types +type Resource interface { + GetId() *dtpb.Id + GetImplicitRules() *dtpb.Uri + GetMeta() *dtpb.Meta + GetLanguage() *dtpb.Code + Base +} + +// Extendable is an interface for abstraction resources or data-types that have +// extension properties. +// +// This is not an official FHIR abstract class; this is something simply named +// here for the general convenience, since not all FHIR types are extendable. +// +// This embeds the proto.Message interface into this interface to help distinguish +// that this still refers to protos in the process. +type Extendable interface { + GetExtension() []*dtpb.Extension + Base +} + +// DomainResource is the interface-definition of the FHIR Abstract base type +// which is the ancestor of all FHIR domain resource objects (effectively +// everything that is not a datatype or bundle/contained-resource). +// See https://www.hl7.org/fhir/r4/domainresource.html for more details. +// +// This interface extends from the `Resource` interface by embedding it. Any +// `DomainResource` is also a `Resource`. +type DomainResource interface { + GetText() *dtpb.Narrative + GetContained() []*anypb.Any + GetModifierExtension() []*dtpb.Extension + Extendable + Resource +} + +// CanonicalResource represents resources that have a canonical URL: +// +// - They have a canonical URL (note: all resources with a canonical URL are +// specializations of this type) +// - They have version, status, and data properties to help manage their publication +// - They carry some additional metadata about their use, including copyright information +// +// CanonicalResource objects may be the logical target of Canonical references. +// +// Note: This is technically an "R5" interface type that is not officially part +// of the R4 spec, however its definition is still applicable and applies to "R4" +// resource types. Using this still provides us with a proper vernacular for +// referring to these resources. +// +// See https://www.hl7.org/fhir/r5/canonicalresource.html for more details. +type CanonicalResource interface { + GetUrl() *dtpb.Uri + GetIdentifier() []*dtpb.Identifier + GetVersion() *dtpb.String + GetName() *dtpb.String + GetTitle() *dtpb.String + GetExperimental() *dtpb.Boolean + GetDate() *dtpb.DateTime + GetPublisher() *dtpb.String + GetContact() []*dtpb.ContactDetail + GetDescription() *dtpb.Markdown + GetUseContext() []*dtpb.UsageContext + GetJurisdiction() []*dtpb.CodeableConcept + GetPurpose() *dtpb.Markdown + GetCopyright() *dtpb.Markdown + // This interface should technically also have 'GetStatus()', however the + // return type differs based on resource type in the proto definitions -- and + // so this can't be referred to in a homogeneous way. + // GetStatus() interface{} + + DomainResource +} + +// MetadataResource represents resources that carry additional publication +// metadata over other CanonicalResources, describing their review and use in +// more details. +// +// As an interface, this type is never created directly. +// +// Note: This is technically an "R5" interface type that is not officially part +// of the R4 spec, however its definition is still applicable and applies to "R4" +// resource types. Using this still provides us with a proper vernacular for +// referring to these resources. +// +// See https://www.hl7.org/fhir/r5/metadataresource.html for more details. +type MetadataResource interface { + GetApprovalDate() *dtpb.Date + GetLastReviewDate() *dtpb.Date + GetEffectivePeriod() *dtpb.Period + GetTopic() []*dtpb.CodeableConcept + GetAuthor() []*dtpb.ContactDetail + GetEditor() []*dtpb.ContactDetail + GetReviewer() []*dtpb.ContactDetail + GetEndorser() []*dtpb.ContactDetail + GetRelatedArtifact() []*dtpb.RelatedArtifact + + CanonicalResource +} + +// Element is the base definition for all elements in a resource. +// +// See https://www.hl7.org/fhir/r4/element.html for more details. +// +// This interface is defined by embedding the proto.Message interface, since all +// FHIR elements in this library must also be proto.Message types. +type Element interface { + GetId() *dtpb.String + Extendable + Base +} + +// BackboneElement is the base definition for all elements that are defined +// inside a resource - but not those in a data type. +// +// See https://www.hl7.org/fhir/r4/backboneelement.html for more details. +// +// This interface is defined by embedding the proto.Message interface, since all +// FHIR backbone elements in this library must also be proto.Message types. +type BackboneElement interface { + GetModifierExtension() []*dtpb.Extension + Element +} diff --git a/internal/fhir/iface_test.go b/internal/fhir/iface_test.go new file mode 100644 index 0000000..d201521 --- /dev/null +++ b/internal/fhir/iface_test.go @@ -0,0 +1,483 @@ +package fhir_test + +import ( + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/account_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/activity_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/adverse_event_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/allergy_intolerance_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/audit_event_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/basic_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/binary_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/biologically_derived_product_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/body_structure_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/capability_statement_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/care_plan_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/care_team_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/catalog_entry_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/charge_item_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/charge_item_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/claim_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/claim_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/clinical_impression_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/code_system_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/communication_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/communication_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/compartment_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/composition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/concept_map_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/condition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/consent_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/contract_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/coverage_eligibility_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/coverage_eligibility_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/coverage_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/detected_issue_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_metric_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_use_statement_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/diagnostic_report_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/document_manifest_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/document_reference_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/effect_evidence_synthesis_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/encounter_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/endpoint_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/enrollment_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/enrollment_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/episode_of_care_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/event_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/evidence_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/evidence_variable_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/example_scenario_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/explanation_of_benefit_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/family_member_history_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/flag_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/goal_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/graph_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/group_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/guidance_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/healthcare_service_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/imaging_study_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/immunization_evaluation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/immunization_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/immunization_recommendation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/implementation_guide_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/insurance_plan_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/invoice_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/library_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/linkage_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/list_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/location_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/measure_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/measure_report_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/media_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_administration_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_dispense_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_knowledge_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_statement_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_authorization_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_contraindication_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_indication_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_ingredient_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_interaction_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_manufactured_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_packaged_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_pharmaceutical_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_undesirable_effect_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/message_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/message_header_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/molecular_sequence_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/naming_system_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/nutrition_order_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/operation_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/operation_outcome_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/organization_affiliation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/organization_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/parameters_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/payment_notice_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/payment_reconciliation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/person_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/plan_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/practitioner_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/practitioner_role_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/procedure_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/provenance_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/related_person_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/request_group_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_element_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_study_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_subject_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/risk_assessment_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/risk_evidence_synthesis_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/schedule_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/search_parameter_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/service_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/slot_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/specimen_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/specimen_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/structure_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/structure_map_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/subscription_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_nucleic_acid_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_polymer_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_protein_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_reference_information_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_source_material_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_specification_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/supply_delivery_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/supply_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/task_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/terminology_capabilities_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/test_report_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/test_script_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/value_set_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/verification_result_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/vision_prescription_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +// Note: The tests in this file are all statically validated by the interface +// type assigned to the anonymous variables. + +// Resource types (https://www.hl7.org/fhir/resource.html#Resource). +// This may exclude certain Trial-Use types that google/fhir doesn't implement. + +var _ fhir.Resource = (*account_go_proto.Account)(nil) +var _ fhir.Resource = (*activity_definition_go_proto.ActivityDefinition)(nil) +var _ fhir.Resource = (*adverse_event_go_proto.AdverseEvent)(nil) +var _ fhir.Resource = (*allergy_intolerance_go_proto.AllergyIntolerance)(nil) +var _ fhir.Resource = (*appointment_go_proto.Appointment)(nil) +var _ fhir.Resource = (*appointment_response_go_proto.AppointmentResponse)(nil) +var _ fhir.Resource = (*audit_event_go_proto.AuditEvent)(nil) +var _ fhir.Resource = (*basic_go_proto.Basic)(nil) +var _ fhir.Resource = (*biologically_derived_product_go_proto.BiologicallyDerivedProduct)(nil) +var _ fhir.Resource = (*body_structure_go_proto.BodyStructure)(nil) +var _ fhir.Resource = (*capability_statement_go_proto.CapabilityStatement)(nil) +var _ fhir.Resource = (*care_plan_go_proto.CarePlan)(nil) +var _ fhir.Resource = (*care_team_go_proto.CareTeam)(nil) +var _ fhir.Resource = (*catalog_entry_go_proto.CatalogEntry)(nil) +var _ fhir.Resource = (*charge_item_go_proto.ChargeItem)(nil) +var _ fhir.Resource = (*charge_item_definition_go_proto.ChargeItemDefinition)(nil) +var _ fhir.Resource = (*claim_go_proto.Claim)(nil) +var _ fhir.Resource = (*claim_response_go_proto.ClaimResponse)(nil) +var _ fhir.Resource = (*clinical_impression_go_proto.ClinicalImpression)(nil) +var _ fhir.Resource = (*code_system_go_proto.CodeSystem)(nil) +var _ fhir.Resource = (*communication_go_proto.Communication)(nil) +var _ fhir.Resource = (*communication_request_go_proto.CommunicationRequest)(nil) +var _ fhir.Resource = (*compartment_definition_go_proto.CompartmentDefinition)(nil) +var _ fhir.Resource = (*composition_go_proto.Composition)(nil) +var _ fhir.Resource = (*concept_map_go_proto.ConceptMap)(nil) +var _ fhir.Resource = (*condition_go_proto.Condition)(nil) +var _ fhir.Resource = (*consent_go_proto.Consent)(nil) +var _ fhir.Resource = (*contract_go_proto.Contract)(nil) +var _ fhir.Resource = (*coverage_go_proto.Coverage)(nil) +var _ fhir.Resource = (*coverage_eligibility_request_go_proto.CoverageEligibilityRequest)(nil) +var _ fhir.Resource = (*coverage_eligibility_response_go_proto.CoverageEligibilityResponse)(nil) +var _ fhir.Resource = (*detected_issue_go_proto.DetectedIssue)(nil) +var _ fhir.Resource = (*device_go_proto.Device)(nil) +var _ fhir.Resource = (*device_definition_go_proto.DeviceDefinition)(nil) +var _ fhir.Resource = (*device_metric_go_proto.DeviceMetric)(nil) +var _ fhir.Resource = (*device_request_go_proto.DeviceRequest)(nil) +var _ fhir.Resource = (*device_use_statement_go_proto.DeviceUseStatement)(nil) +var _ fhir.Resource = (*diagnostic_report_go_proto.DiagnosticReport)(nil) +var _ fhir.Resource = (*document_manifest_go_proto.DocumentManifest)(nil) +var _ fhir.Resource = (*document_reference_go_proto.DocumentReference)(nil) +var _ fhir.Resource = (*effect_evidence_synthesis_go_proto.EffectEvidenceSynthesis)(nil) +var _ fhir.Resource = (*encounter_go_proto.Encounter)(nil) +var _ fhir.Resource = (*endpoint_go_proto.Endpoint)(nil) +var _ fhir.Resource = (*enrollment_request_go_proto.EnrollmentRequest)(nil) +var _ fhir.Resource = (*enrollment_response_go_proto.EnrollmentResponse)(nil) +var _ fhir.Resource = (*episode_of_care_go_proto.EpisodeOfCare)(nil) +var _ fhir.Resource = (*event_definition_go_proto.EventDefinition)(nil) +var _ fhir.Resource = (*evidence_go_proto.Evidence)(nil) +var _ fhir.Resource = (*evidence_variable_go_proto.EvidenceVariable)(nil) +var _ fhir.Resource = (*example_scenario_go_proto.ExampleScenario)(nil) +var _ fhir.Resource = (*explanation_of_benefit_go_proto.ExplanationOfBenefit)(nil) +var _ fhir.Resource = (*family_member_history_go_proto.FamilyMemberHistory)(nil) +var _ fhir.Resource = (*flag_go_proto.Flag)(nil) +var _ fhir.Resource = (*goal_go_proto.Goal)(nil) +var _ fhir.Resource = (*graph_definition_go_proto.GraphDefinition)(nil) +var _ fhir.Resource = (*group_go_proto.Group)(nil) +var _ fhir.Resource = (*guidance_response_go_proto.GuidanceResponse)(nil) +var _ fhir.Resource = (*healthcare_service_go_proto.HealthcareService)(nil) +var _ fhir.Resource = (*imaging_study_go_proto.ImagingStudy)(nil) +var _ fhir.Resource = (*immunization_go_proto.Immunization)(nil) +var _ fhir.Resource = (*immunization_evaluation_go_proto.ImmunizationEvaluation)(nil) +var _ fhir.Resource = (*immunization_recommendation_go_proto.ImmunizationRecommendation)(nil) +var _ fhir.Resource = (*implementation_guide_go_proto.ImplementationGuide)(nil) +var _ fhir.Resource = (*insurance_plan_go_proto.InsurancePlan)(nil) +var _ fhir.Resource = (*invoice_go_proto.Invoice)(nil) +var _ fhir.Resource = (*library_go_proto.Library)(nil) +var _ fhir.Resource = (*linkage_go_proto.Linkage)(nil) +var _ fhir.Resource = (*list_go_proto.List)(nil) +var _ fhir.Resource = (*location_go_proto.Location)(nil) +var _ fhir.Resource = (*measure_go_proto.Measure)(nil) +var _ fhir.Resource = (*measure_report_go_proto.MeasureReport)(nil) +var _ fhir.Resource = (*media_go_proto.Media)(nil) +var _ fhir.Resource = (*medication_go_proto.Medication)(nil) +var _ fhir.Resource = (*medication_administration_go_proto.MedicationAdministration)(nil) +var _ fhir.Resource = (*medication_dispense_go_proto.MedicationDispense)(nil) +var _ fhir.Resource = (*medication_knowledge_go_proto.MedicationKnowledge)(nil) +var _ fhir.Resource = (*medication_request_go_proto.MedicationRequest)(nil) +var _ fhir.Resource = (*medication_statement_go_proto.MedicationStatement)(nil) +var _ fhir.Resource = (*medicinal_product_go_proto.MedicinalProduct)(nil) +var _ fhir.Resource = (*medicinal_product_authorization_go_proto.MedicinalProductAuthorization)(nil) +var _ fhir.Resource = (*medicinal_product_contraindication_go_proto.MedicinalProductContraindication)(nil) +var _ fhir.Resource = (*medicinal_product_indication_go_proto.MedicinalProductIndication)(nil) +var _ fhir.Resource = (*medicinal_product_ingredient_go_proto.MedicinalProductIngredient)(nil) +var _ fhir.Resource = (*medicinal_product_interaction_go_proto.MedicinalProductInteraction)(nil) +var _ fhir.Resource = (*medicinal_product_manufactured_go_proto.MedicinalProductManufactured)(nil) +var _ fhir.Resource = (*medicinal_product_packaged_go_proto.MedicinalProductPackaged)(nil) +var _ fhir.Resource = (*medicinal_product_pharmaceutical_go_proto.MedicinalProductPharmaceutical)(nil) +var _ fhir.Resource = (*medicinal_product_undesirable_effect_go_proto.MedicinalProductUndesirableEffect)(nil) +var _ fhir.Resource = (*message_definition_go_proto.MessageDefinition)(nil) +var _ fhir.Resource = (*message_header_go_proto.MessageHeader)(nil) +var _ fhir.Resource = (*molecular_sequence_go_proto.MolecularSequence)(nil) +var _ fhir.Resource = (*naming_system_go_proto.NamingSystem)(nil) +var _ fhir.Resource = (*nutrition_order_go_proto.NutritionOrder)(nil) +var _ fhir.Resource = (*observation_go_proto.Observation)(nil) +var _ fhir.Resource = (*observation_definition_go_proto.ObservationDefinition)(nil) +var _ fhir.Resource = (*operation_definition_go_proto.OperationDefinition)(nil) +var _ fhir.Resource = (*operation_outcome_go_proto.OperationOutcome)(nil) +var _ fhir.Resource = (*organization_go_proto.Organization)(nil) +var _ fhir.Resource = (*organization_affiliation_go_proto.OrganizationAffiliation)(nil) +var _ fhir.Resource = (*patient_go_proto.Patient)(nil) +var _ fhir.Resource = (*payment_notice_go_proto.PaymentNotice)(nil) +var _ fhir.Resource = (*payment_reconciliation_go_proto.PaymentReconciliation)(nil) +var _ fhir.Resource = (*person_go_proto.Person)(nil) +var _ fhir.Resource = (*plan_definition_go_proto.PlanDefinition)(nil) +var _ fhir.Resource = (*practitioner_go_proto.Practitioner)(nil) +var _ fhir.Resource = (*practitioner_role_go_proto.PractitionerRole)(nil) +var _ fhir.Resource = (*procedure_go_proto.Procedure)(nil) +var _ fhir.Resource = (*provenance_go_proto.Provenance)(nil) +var _ fhir.Resource = (*questionnaire_go_proto.Questionnaire)(nil) +var _ fhir.Resource = (*questionnaire_response_go_proto.QuestionnaireResponse)(nil) +var _ fhir.Resource = (*related_person_go_proto.RelatedPerson)(nil) +var _ fhir.Resource = (*request_group_go_proto.RequestGroup)(nil) +var _ fhir.Resource = (*research_definition_go_proto.ResearchDefinition)(nil) +var _ fhir.Resource = (*research_element_definition_go_proto.ResearchElementDefinition)(nil) +var _ fhir.Resource = (*research_study_go_proto.ResearchStudy)(nil) +var _ fhir.Resource = (*research_subject_go_proto.ResearchSubject)(nil) +var _ fhir.Resource = (*risk_assessment_go_proto.RiskAssessment)(nil) +var _ fhir.Resource = (*risk_evidence_synthesis_go_proto.RiskEvidenceSynthesis)(nil) +var _ fhir.Resource = (*schedule_go_proto.Schedule)(nil) +var _ fhir.Resource = (*search_parameter_go_proto.SearchParameter)(nil) +var _ fhir.Resource = (*service_request_go_proto.ServiceRequest)(nil) +var _ fhir.Resource = (*slot_go_proto.Slot)(nil) +var _ fhir.Resource = (*specimen_go_proto.Specimen)(nil) +var _ fhir.Resource = (*specimen_definition_go_proto.SpecimenDefinition)(nil) +var _ fhir.Resource = (*structure_definition_go_proto.StructureDefinition)(nil) +var _ fhir.Resource = (*structure_map_go_proto.StructureMap)(nil) +var _ fhir.Resource = (*subscription_go_proto.Subscription)(nil) +var _ fhir.Resource = (*substance_go_proto.Substance)(nil) +var _ fhir.Resource = (*substance_nucleic_acid_go_proto.SubstanceNucleicAcid)(nil) +var _ fhir.Resource = (*substance_polymer_go_proto.SubstancePolymer)(nil) +var _ fhir.Resource = (*substance_protein_go_proto.SubstanceProtein)(nil) +var _ fhir.Resource = (*substance_reference_information_go_proto.SubstanceReferenceInformation)(nil) +var _ fhir.Resource = (*substance_source_material_go_proto.SubstanceSourceMaterial)(nil) +var _ fhir.Resource = (*substance_specification_go_proto.SubstanceSpecification)(nil) +var _ fhir.Resource = (*supply_delivery_go_proto.SupplyDelivery)(nil) +var _ fhir.Resource = (*supply_request_go_proto.SupplyRequest)(nil) +var _ fhir.Resource = (*task_go_proto.Task)(nil) +var _ fhir.Resource = (*terminology_capabilities_go_proto.TerminologyCapabilities)(nil) +var _ fhir.Resource = (*test_report_go_proto.TestReport)(nil) +var _ fhir.Resource = (*test_script_go_proto.TestScript)(nil) +var _ fhir.Resource = (*value_set_go_proto.ValueSet)(nil) +var _ fhir.Resource = (*verification_result_go_proto.VerificationResult)(nil) +var _ fhir.Resource = (*vision_prescription_go_proto.VisionPrescription)(nil) +var _ fhir.Resource = (*parameters_go_proto.Parameters)(nil) +var _ fhir.Resource = (*binary_go_proto.Binary)(nil) +var _ fhir.Resource = (*bundle_and_contained_resource_go_proto.Bundle)(nil) + +// DomainResource types (https://www.hl7.org/fhir/resource.html#DomainResource). +// This may exclude certain Trial-Use types that google/fhir doesn't implement. + +var _ fhir.DomainResource = (*account_go_proto.Account)(nil) +var _ fhir.DomainResource = (*activity_definition_go_proto.ActivityDefinition)(nil) +var _ fhir.DomainResource = (*adverse_event_go_proto.AdverseEvent)(nil) +var _ fhir.DomainResource = (*allergy_intolerance_go_proto.AllergyIntolerance)(nil) +var _ fhir.DomainResource = (*appointment_go_proto.Appointment)(nil) +var _ fhir.DomainResource = (*appointment_response_go_proto.AppointmentResponse)(nil) +var _ fhir.DomainResource = (*audit_event_go_proto.AuditEvent)(nil) +var _ fhir.DomainResource = (*basic_go_proto.Basic)(nil) +var _ fhir.DomainResource = (*biologically_derived_product_go_proto.BiologicallyDerivedProduct)(nil) +var _ fhir.DomainResource = (*body_structure_go_proto.BodyStructure)(nil) +var _ fhir.DomainResource = (*capability_statement_go_proto.CapabilityStatement)(nil) +var _ fhir.DomainResource = (*care_plan_go_proto.CarePlan)(nil) +var _ fhir.DomainResource = (*care_team_go_proto.CareTeam)(nil) +var _ fhir.DomainResource = (*catalog_entry_go_proto.CatalogEntry)(nil) +var _ fhir.DomainResource = (*charge_item_go_proto.ChargeItem)(nil) +var _ fhir.DomainResource = (*charge_item_definition_go_proto.ChargeItemDefinition)(nil) +var _ fhir.DomainResource = (*claim_go_proto.Claim)(nil) +var _ fhir.DomainResource = (*claim_response_go_proto.ClaimResponse)(nil) +var _ fhir.DomainResource = (*clinical_impression_go_proto.ClinicalImpression)(nil) +var _ fhir.DomainResource = (*code_system_go_proto.CodeSystem)(nil) +var _ fhir.DomainResource = (*communication_go_proto.Communication)(nil) +var _ fhir.DomainResource = (*communication_request_go_proto.CommunicationRequest)(nil) +var _ fhir.DomainResource = (*compartment_definition_go_proto.CompartmentDefinition)(nil) +var _ fhir.DomainResource = (*composition_go_proto.Composition)(nil) +var _ fhir.DomainResource = (*concept_map_go_proto.ConceptMap)(nil) +var _ fhir.DomainResource = (*condition_go_proto.Condition)(nil) +var _ fhir.DomainResource = (*consent_go_proto.Consent)(nil) +var _ fhir.DomainResource = (*contract_go_proto.Contract)(nil) +var _ fhir.DomainResource = (*coverage_go_proto.Coverage)(nil) +var _ fhir.DomainResource = (*coverage_eligibility_request_go_proto.CoverageEligibilityRequest)(nil) +var _ fhir.DomainResource = (*coverage_eligibility_response_go_proto.CoverageEligibilityResponse)(nil) +var _ fhir.DomainResource = (*detected_issue_go_proto.DetectedIssue)(nil) +var _ fhir.DomainResource = (*device_go_proto.Device)(nil) +var _ fhir.DomainResource = (*device_definition_go_proto.DeviceDefinition)(nil) +var _ fhir.DomainResource = (*device_metric_go_proto.DeviceMetric)(nil) +var _ fhir.DomainResource = (*device_request_go_proto.DeviceRequest)(nil) +var _ fhir.DomainResource = (*device_use_statement_go_proto.DeviceUseStatement)(nil) +var _ fhir.DomainResource = (*diagnostic_report_go_proto.DiagnosticReport)(nil) +var _ fhir.DomainResource = (*document_manifest_go_proto.DocumentManifest)(nil) +var _ fhir.DomainResource = (*document_reference_go_proto.DocumentReference)(nil) +var _ fhir.DomainResource = (*effect_evidence_synthesis_go_proto.EffectEvidenceSynthesis)(nil) +var _ fhir.DomainResource = (*encounter_go_proto.Encounter)(nil) +var _ fhir.DomainResource = (*endpoint_go_proto.Endpoint)(nil) +var _ fhir.DomainResource = (*enrollment_request_go_proto.EnrollmentRequest)(nil) +var _ fhir.DomainResource = (*enrollment_response_go_proto.EnrollmentResponse)(nil) +var _ fhir.DomainResource = (*episode_of_care_go_proto.EpisodeOfCare)(nil) +var _ fhir.DomainResource = (*event_definition_go_proto.EventDefinition)(nil) +var _ fhir.DomainResource = (*evidence_go_proto.Evidence)(nil) +var _ fhir.DomainResource = (*evidence_variable_go_proto.EvidenceVariable)(nil) +var _ fhir.DomainResource = (*example_scenario_go_proto.ExampleScenario)(nil) +var _ fhir.DomainResource = (*explanation_of_benefit_go_proto.ExplanationOfBenefit)(nil) +var _ fhir.DomainResource = (*family_member_history_go_proto.FamilyMemberHistory)(nil) +var _ fhir.DomainResource = (*flag_go_proto.Flag)(nil) +var _ fhir.DomainResource = (*goal_go_proto.Goal)(nil) +var _ fhir.DomainResource = (*graph_definition_go_proto.GraphDefinition)(nil) +var _ fhir.DomainResource = (*group_go_proto.Group)(nil) +var _ fhir.DomainResource = (*guidance_response_go_proto.GuidanceResponse)(nil) +var _ fhir.DomainResource = (*healthcare_service_go_proto.HealthcareService)(nil) +var _ fhir.DomainResource = (*imaging_study_go_proto.ImagingStudy)(nil) +var _ fhir.DomainResource = (*immunization_go_proto.Immunization)(nil) +var _ fhir.DomainResource = (*immunization_evaluation_go_proto.ImmunizationEvaluation)(nil) +var _ fhir.DomainResource = (*immunization_recommendation_go_proto.ImmunizationRecommendation)(nil) +var _ fhir.DomainResource = (*implementation_guide_go_proto.ImplementationGuide)(nil) +var _ fhir.DomainResource = (*insurance_plan_go_proto.InsurancePlan)(nil) +var _ fhir.DomainResource = (*invoice_go_proto.Invoice)(nil) +var _ fhir.DomainResource = (*library_go_proto.Library)(nil) +var _ fhir.DomainResource = (*linkage_go_proto.Linkage)(nil) +var _ fhir.DomainResource = (*list_go_proto.List)(nil) +var _ fhir.DomainResource = (*location_go_proto.Location)(nil) +var _ fhir.DomainResource = (*measure_go_proto.Measure)(nil) +var _ fhir.DomainResource = (*measure_report_go_proto.MeasureReport)(nil) +var _ fhir.DomainResource = (*media_go_proto.Media)(nil) +var _ fhir.DomainResource = (*medication_go_proto.Medication)(nil) +var _ fhir.DomainResource = (*medication_administration_go_proto.MedicationAdministration)(nil) +var _ fhir.DomainResource = (*medication_dispense_go_proto.MedicationDispense)(nil) +var _ fhir.DomainResource = (*medication_knowledge_go_proto.MedicationKnowledge)(nil) +var _ fhir.DomainResource = (*medication_request_go_proto.MedicationRequest)(nil) +var _ fhir.DomainResource = (*medication_statement_go_proto.MedicationStatement)(nil) +var _ fhir.DomainResource = (*medicinal_product_go_proto.MedicinalProduct)(nil) +var _ fhir.DomainResource = (*medicinal_product_authorization_go_proto.MedicinalProductAuthorization)(nil) +var _ fhir.DomainResource = (*medicinal_product_contraindication_go_proto.MedicinalProductContraindication)(nil) +var _ fhir.DomainResource = (*medicinal_product_indication_go_proto.MedicinalProductIndication)(nil) +var _ fhir.DomainResource = (*medicinal_product_ingredient_go_proto.MedicinalProductIngredient)(nil) +var _ fhir.DomainResource = (*medicinal_product_interaction_go_proto.MedicinalProductInteraction)(nil) +var _ fhir.DomainResource = (*medicinal_product_manufactured_go_proto.MedicinalProductManufactured)(nil) +var _ fhir.DomainResource = (*medicinal_product_packaged_go_proto.MedicinalProductPackaged)(nil) +var _ fhir.DomainResource = (*medicinal_product_pharmaceutical_go_proto.MedicinalProductPharmaceutical)(nil) +var _ fhir.DomainResource = (*medicinal_product_undesirable_effect_go_proto.MedicinalProductUndesirableEffect)(nil) +var _ fhir.DomainResource = (*message_definition_go_proto.MessageDefinition)(nil) +var _ fhir.DomainResource = (*message_header_go_proto.MessageHeader)(nil) +var _ fhir.DomainResource = (*molecular_sequence_go_proto.MolecularSequence)(nil) +var _ fhir.DomainResource = (*naming_system_go_proto.NamingSystem)(nil) +var _ fhir.DomainResource = (*nutrition_order_go_proto.NutritionOrder)(nil) +var _ fhir.DomainResource = (*observation_go_proto.Observation)(nil) +var _ fhir.DomainResource = (*observation_definition_go_proto.ObservationDefinition)(nil) +var _ fhir.DomainResource = (*operation_definition_go_proto.OperationDefinition)(nil) +var _ fhir.DomainResource = (*operation_outcome_go_proto.OperationOutcome)(nil) +var _ fhir.DomainResource = (*organization_go_proto.Organization)(nil) +var _ fhir.DomainResource = (*organization_affiliation_go_proto.OrganizationAffiliation)(nil) +var _ fhir.DomainResource = (*patient_go_proto.Patient)(nil) +var _ fhir.DomainResource = (*payment_notice_go_proto.PaymentNotice)(nil) +var _ fhir.DomainResource = (*payment_reconciliation_go_proto.PaymentReconciliation)(nil) +var _ fhir.DomainResource = (*person_go_proto.Person)(nil) +var _ fhir.DomainResource = (*plan_definition_go_proto.PlanDefinition)(nil) +var _ fhir.DomainResource = (*practitioner_go_proto.Practitioner)(nil) +var _ fhir.DomainResource = (*practitioner_role_go_proto.PractitionerRole)(nil) +var _ fhir.DomainResource = (*procedure_go_proto.Procedure)(nil) +var _ fhir.DomainResource = (*provenance_go_proto.Provenance)(nil) +var _ fhir.DomainResource = (*questionnaire_go_proto.Questionnaire)(nil) +var _ fhir.DomainResource = (*questionnaire_response_go_proto.QuestionnaireResponse)(nil) +var _ fhir.DomainResource = (*related_person_go_proto.RelatedPerson)(nil) +var _ fhir.DomainResource = (*request_group_go_proto.RequestGroup)(nil) +var _ fhir.DomainResource = (*research_definition_go_proto.ResearchDefinition)(nil) +var _ fhir.DomainResource = (*research_element_definition_go_proto.ResearchElementDefinition)(nil) +var _ fhir.DomainResource = (*research_study_go_proto.ResearchStudy)(nil) +var _ fhir.DomainResource = (*research_subject_go_proto.ResearchSubject)(nil) +var _ fhir.DomainResource = (*risk_assessment_go_proto.RiskAssessment)(nil) +var _ fhir.DomainResource = (*risk_evidence_synthesis_go_proto.RiskEvidenceSynthesis)(nil) +var _ fhir.DomainResource = (*schedule_go_proto.Schedule)(nil) +var _ fhir.DomainResource = (*search_parameter_go_proto.SearchParameter)(nil) +var _ fhir.DomainResource = (*service_request_go_proto.ServiceRequest)(nil) +var _ fhir.DomainResource = (*slot_go_proto.Slot)(nil) +var _ fhir.DomainResource = (*specimen_go_proto.Specimen)(nil) +var _ fhir.DomainResource = (*specimen_definition_go_proto.SpecimenDefinition)(nil) +var _ fhir.DomainResource = (*structure_definition_go_proto.StructureDefinition)(nil) +var _ fhir.DomainResource = (*structure_map_go_proto.StructureMap)(nil) +var _ fhir.DomainResource = (*subscription_go_proto.Subscription)(nil) +var _ fhir.DomainResource = (*substance_go_proto.Substance)(nil) +var _ fhir.DomainResource = (*substance_nucleic_acid_go_proto.SubstanceNucleicAcid)(nil) +var _ fhir.DomainResource = (*substance_polymer_go_proto.SubstancePolymer)(nil) +var _ fhir.DomainResource = (*substance_protein_go_proto.SubstanceProtein)(nil) +var _ fhir.DomainResource = (*substance_reference_information_go_proto.SubstanceReferenceInformation)(nil) +var _ fhir.DomainResource = (*substance_source_material_go_proto.SubstanceSourceMaterial)(nil) +var _ fhir.DomainResource = (*substance_specification_go_proto.SubstanceSpecification)(nil) +var _ fhir.DomainResource = (*supply_delivery_go_proto.SupplyDelivery)(nil) +var _ fhir.DomainResource = (*supply_request_go_proto.SupplyRequest)(nil) +var _ fhir.DomainResource = (*task_go_proto.Task)(nil) +var _ fhir.DomainResource = (*terminology_capabilities_go_proto.TerminologyCapabilities)(nil) +var _ fhir.DomainResource = (*test_report_go_proto.TestReport)(nil) +var _ fhir.DomainResource = (*test_script_go_proto.TestScript)(nil) +var _ fhir.DomainResource = (*value_set_go_proto.ValueSet)(nil) +var _ fhir.DomainResource = (*verification_result_go_proto.VerificationResult)(nil) +var _ fhir.DomainResource = (*vision_prescription_go_proto.VisionPrescription)(nil) + +// CanonicalResource types from https://www.hl7.org/fhir/canonicalresource.html#bnr. +// The list of CanonicalResources here is slightly smaller than on HL7's +// site since it includes trial-use types, and objects with trial-use fields +// which are not modeled in the google/fhir protos. + +var _ fhir.CanonicalResource = (*activity_definition_go_proto.ActivityDefinition)(nil) +var _ fhir.CanonicalResource = (*code_system_go_proto.CodeSystem)(nil) +var _ fhir.CanonicalResource = (*event_definition_go_proto.EventDefinition)(nil) +var _ fhir.CanonicalResource = (*library_go_proto.Library)(nil) +var _ fhir.CanonicalResource = (*measure_go_proto.Measure)(nil) +var _ fhir.CanonicalResource = (*message_definition_go_proto.MessageDefinition)(nil) +var _ fhir.CanonicalResource = (*plan_definition_go_proto.PlanDefinition)(nil) +var _ fhir.CanonicalResource = (*questionnaire_go_proto.Questionnaire)(nil) +var _ fhir.CanonicalResource = (*research_definition_go_proto.ResearchDefinition)(nil) +var _ fhir.CanonicalResource = (*research_element_definition_go_proto.ResearchElementDefinition)(nil) +var _ fhir.CanonicalResource = (*structure_definition_go_proto.StructureDefinition)(nil) +var _ fhir.CanonicalResource = (*structure_map_go_proto.StructureMap)(nil) +var _ fhir.CanonicalResource = (*value_set_go_proto.ValueSet)(nil) + +// MetadataResource types from https://www.hl7.org/fhir/metadataresource.html#bnr. +// The list of MetadataResources here is slightly smaller than on HL7's +// site since it includes trial-use types, and objects with trial-use fields +// which are not modeled in the google/fhir protos. + +var _ fhir.MetadataResource = (*activity_definition_go_proto.ActivityDefinition)(nil) +var _ fhir.MetadataResource = (*event_definition_go_proto.EventDefinition)(nil) +var _ fhir.MetadataResource = (*library_go_proto.Library)(nil) +var _ fhir.MetadataResource = (*measure_go_proto.Measure)(nil) +var _ fhir.MetadataResource = (*plan_definition_go_proto.PlanDefinition)(nil) +var _ fhir.MetadataResource = (*research_definition_go_proto.ResearchDefinition)(nil) +var _ fhir.MetadataResource = (*research_element_definition_go_proto.ResearchElementDefinition)(nil) diff --git a/internal/fhir/protofields.go b/internal/fhir/protofields.go new file mode 100644 index 0000000..c594c51 --- /dev/null +++ b/internal/fhir/protofields.go @@ -0,0 +1,13 @@ +package fhir + +import ( + "github.com/verily-src/fhirpath-go/internal/protofields" +) + +// UnwrapValueX obtains the underlying Message for oneof ValueX +// elements, which use a nested Choice field. Returns nil if the input message +// doesn't have a Choice field, or if the Oneof descriptor is unpopulated. +// See wrapped implementation for more information. +func UnwrapValueX(element Base) Base { + return protofields.UnwrapOneofField(element, "choice") +} diff --git a/internal/fhir/time.go b/internal/fhir/time.go new file mode 100644 index 0000000..40aac7e --- /dev/null +++ b/internal/fhir/time.go @@ -0,0 +1,358 @@ +package fhir + +import ( + "fmt" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" +) + +// extractTimezone gets the string representation of a UTC offset. +// +// This source is taken from: +// https://github.com/google/fhir/blob/1d1e7189749fcdbbececc1c70e00dd498bfb33d1/go/jsonformat/internal/jsonpbhelper/fhirutil.go#L453-L463 +func extractTimezone(t time.Time) string { + _, offset := t.Zone() + sign := "+" + if offset < 0 { + sign = "-" + offset = -offset + } + hour := offset / 3600 + minute := offset % 3600 / 60 + return fmt.Sprintf("%s%02d:%02d", sign, hour, minute) +} + +// Date creates an R4 FHIR Date element from a Time value, accurate to a given +// day. +// +// See: http://hl7.org/fhir/R4/datatypes.html#date +func Date(t time.Time) *dtpb.Date { + return &dtpb.Date{ + ValueUs: t.UnixMicro(), + Precision: dtpb.Date_DAY, + Timezone: extractTimezone(t), + } +} + +// DateNow creates an R4 FHIR Date element at the current time using the +// highest available precision. +func DateNow() *dtpb.Date { + return Date(time.Now()) +} + +// DateTime creates an R4 FHIR DateTime element from a Time value, accurate +// to the microsecond. +// +// See: http://hl7.org/fhir/R4/datatypes.html#datetime +func DateTime(t time.Time) *dtpb.DateTime { + return &dtpb.DateTime{ + ValueUs: t.UnixMicro(), + Precision: dtpb.DateTime_MICROSECOND, + Timezone: extractTimezone(t), + } +} + +// DateTimeNow creates an R4 FHIR DateTime element at the current time using the +// highest available precision. +func DateTimeNow() *dtpb.DateTime { + return DateTime(time.Now()) +} + +// Instant creates an R4 FHIR Instant element from a Time value, accurate to the +// microsecond. +// +// See: http://hl7.org/fhir/R4/datatypes.html#instant +func Instant(t time.Time) *dtpb.Instant { + return &dtpb.Instant{ + ValueUs: t.UnixMicro(), + Precision: dtpb.Instant_MICROSECOND, + Timezone: extractTimezone(t), + } +} + +// InstantNow creates an R4 FHIR Instant element at the current time using the +// highest available precision. +func InstantNow() *dtpb.Instant { + return Instant(time.Now()) +} + +// Time creates an R4 FHIR Time element from a Time value. +// +// FHIR Time elements represent a time of day, disconnected from any date. +// As a result, the value stored in this proto will be modulo 24-hours to keep +// it within that 1 day time. Put differently, this will only ever be populated +// with the number of microseconds since the start of the unix epoch, modulo one +// day in microseconds. +// +// See: http://hl7.org/fhir/R4/datatypes.html#time +func Time(t time.Time) *dtpb.Time { + return &dtpb.Time{ + ValueUs: t.UnixMicro() % (time.Hour * 24).Microseconds(), + Precision: dtpb.Time_MICROSECOND, + } +} + +// TimeNow creates an R4 FHIR Time element at the current time using the +// highest available precision. +func TimeNow() *dtpb.Time { + return Time(time.Now()) +} + +// TimeOfDay creates a Time proto at the specified time. +// +// This function will return an error if any of the values exceed the valid +// range for their time unit (e.g. if 'hour' exceeds 24, or minute exceeds 60, etc). +// +// The precision is determine by the value set for the micro second parameter; +// if the value is 0, the precision is set to seconds. If the value is +// a multiple of 1000, the value is set to millisecond precision; otherwise, its +// set to microsecond precision. +func TimeOfDay(hour, minute, second, micros int64) (*dtpb.Time, error) { + const ( + maxHour = 24 + maxMinute = 60 + maxSecond = 60 + maxMicros = 1_000_000 + ) + if hour >= maxHour || hour < 0 { + return nil, fmt.Errorf("invalid hour '%v'; expected range is [0, %v)", hour, maxHour) + } + if minute >= maxMinute || minute < 0 { + return nil, fmt.Errorf("invalid minute '%v'; expected range is [0, %v)", minute, maxMinute) + } + if second >= maxSecond || second < 0 { + return nil, fmt.Errorf("invalid second '%v'; expected range is [0, %v)", second, maxSecond) + } + if micros >= maxMicros || micros < 0 { + return nil, fmt.Errorf("invalid micros '%v'; expected range is [0, %v)", micros, maxMicros) + } + + precision := dtpb.Time_MICROSECOND + if micros == 0 { + precision = dtpb.Time_SECOND + } else if (micros % 1_000) == 0 { + precision = dtpb.Time_MILLISECOND + } + + return &dtpb.Time{ + ValueUs: hour*time.Hour.Microseconds() + + minute*time.Minute.Microseconds() + + second*time.Second.Microseconds() + + micros, + Precision: precision, + }, nil +} + +// ParseDate converts the input string into a FHIR Date element. +// The format of the input string must follow the FHIR Date format as defined +// in http://hl7.org/fhir/R4/datatypes.html#date, e.g. +// +// - YYYY, +// - YYYY-MM, or +// - YYYY-MM-DD +// +// The returned Date will have a precision equal to what was specified in the +// input string. +func ParseDate(value string) (*dtpb.Date, error) { + dateFormats := []struct { + format string + precision dtpb.Date_Precision + }{ + {"2006-01-02", dtpb.Date_DAY}, + {"2006-01", dtpb.Date_MONTH}, + {"2006", dtpb.Date_YEAR}, + } + + var t time.Time + var err error + for _, format := range dateFormats { + t, err = time.Parse(format.format, value) + if err == nil { + return &dtpb.Date{ + ValueUs: t.UnixMicro(), + Timezone: extractTimezone(t), + Precision: format.precision, + }, nil + } + } + return nil, fmt.Errorf("unable to parse date '%v': %w", value, err) +} + +// MustParseDate parses a date as according to ParseDate, but panics if the date +// is invalid. +func MustParseDate(value string) *dtpb.Date { + result, err := ParseDate(value) + if err != nil { + panic(err) + } + return result +} + +// ParseDateTime converts the input string into a FHIR DateTime element. +// The format of the input string must follow the FHIR DateTime format as defined +// in http://hl7.org/fhir/R4/datatypes.html#datetime, e.g. +// +// - YYYY, +// - YYYY-MM, +// - YYYY-MM-DD, or +// - YYYY-MM-DDThh:mm:ss+zz:zz (with optional milli/micro precision) +// +// The returned DateTime will have a precision equal to what was specified in the +// input string. +func ParseDateTime(value string) (*dtpb.DateTime, error) { + dateFormats := []struct { + format string + precision dtpb.DateTime_Precision + }{ + {"2006-01-02T15:04:05.000000-07:00", dtpb.DateTime_MICROSECOND}, + {"2006-01-02T15:04:05.000000Z", dtpb.DateTime_MICROSECOND}, + {"2006-01-02T15:04:05.000-07:00", dtpb.DateTime_MILLISECOND}, + {"2006-01-02T15:04:05.000Z", dtpb.DateTime_MILLISECOND}, + {"2006-01-02T15:04:05-07:00", dtpb.DateTime_SECOND}, + {"2006-01-02T15:04:05Z", dtpb.DateTime_SECOND}, + {"2006-01-02", dtpb.DateTime_DAY}, + {"2006-01", dtpb.DateTime_MONTH}, + {"2006", dtpb.DateTime_YEAR}, + } + + var t time.Time + var err error + for _, format := range dateFormats { + t, err = time.Parse(format.format, value) + if err == nil { + return &dtpb.DateTime{ + ValueUs: t.UnixMicro(), + Timezone: extractTimezone(t), + Precision: format.precision, + }, nil + } + } + return nil, fmt.Errorf("unable to parse datetime '%v': %w", value, err) +} + +// MustParseDateTime parses a date as according to ParseDateTime, but panics if +// the date is invalid. +func MustParseDateTime(value string) *dtpb.DateTime { + result, err := ParseDateTime(value) + if err != nil { + panic(err) + } + return result +} + +// ParseInstant converts the input string into a FHIR Instant element. +// The format of the input string must follow the FHIR Instant format as defined +// in http://hl7.org/fhir/R4/datatypes.html#instant, e.g. +// +// - yyyy-mm-ddThh:mm:ss+zz:zz, +// - yyyy-mm-ddThh:mm:ss.000+zz:zz, or +// - yyyy-mm-ddThh:mm:ss.000000+zz:zz, +// +// The returned Instant will have a precision equal to what was specified in the +// input string. +func ParseInstant(value string) (*dtpb.Instant, error) { + dateFormats := []struct { + format string + precision dtpb.Instant_Precision + }{ + {"2006-01-02T15:04:05.000000-07:00", dtpb.Instant_MICROSECOND}, + {"2006-01-02T15:04:05.000000Z", dtpb.Instant_MICROSECOND}, + {"2006-01-02T15:04:05.000-07:00", dtpb.Instant_MILLISECOND}, + {"2006-01-02T15:04:05.000Z", dtpb.Instant_MILLISECOND}, + {"2006-01-02T15:04:05-07:00", dtpb.Instant_SECOND}, + {"2006-01-02T15:04:05Z", dtpb.Instant_SECOND}, + } + + var t time.Time + var err error + for _, format := range dateFormats { + t, err = time.Parse(format.format, value) + if err == nil { + return &dtpb.Instant{ + ValueUs: t.UnixMicro(), + Timezone: extractTimezone(t), + Precision: format.precision, + }, nil + } + } + return nil, fmt.Errorf("unable to parse instant '%v': %w", value, err) +} + +// MustParseInstant parses a date as according to ParseInstant, but panics if +// the time is invalid. +func MustParseInstant(value string) *dtpb.Instant { + result, err := ParseInstant(value) + if err != nil { + panic(err) + } + return result +} + +// yearZeroBase is the time set for 0000-01-01. +// +// The time.Parse has the opinionated choice that any parsed time without any +// year associated to it _must_ be offset from this date. Thus, we store this +// so that parsed times will be offset from the unix timestamp instead. +var yearZeroBase time.Time + +func init() { + tm, err := time.Parse("2006-01-02", "0000-01-01") + if err != nil { + panic(fmt.Sprintf("Unexpected error while creating base time: %v", err)) + } + yearZeroBase = tm +} + +// ParseTime converts the input string into a FHIR Time element. +// The format of the input string must follow the FHIR Time format as defined +// in http://hl7.org/fhir/R4/datatypes.html#time, e.g. +// +// - hh:mm:ss, +// - hh:mm:ss.000, or +// - hh:mm:ss.000000, +// +// The returned Time will have a precision equal to what was specified in the +// input string. +func ParseTime(value string) (*dtpb.Time, error) { + dateFormats := []struct { + format string + precision dtpb.Time_Precision + }{ + {"15:04:05.000000", dtpb.Time_MICROSECOND}, + {"15:04:05.000", dtpb.Time_MILLISECOND}, + {"15:04:05", dtpb.Time_SECOND}, + } + + var t time.Time + var err error + for _, format := range dateFormats { + t, err = time.Parse(format.format, value) + if err != nil { + continue + } + + // time.Parse without any date context forms a time offset from `0000-01-01` + // for some strange reason. This causes a large negative unix timestamp + // which is hard to make work for FHIR Time types. To compensate for this, + // we subtract the 0000-01-01 timestamp from our time, so that 00:00:00 will + // form a time of '0', e.g. it forces all times to be relative to the + // unix epoch. + value := t.UnixMicro() - yearZeroBase.UnixMicro() + return &dtpb.Time{ + ValueUs: value, + Precision: format.precision, + }, nil + } + return nil, fmt.Errorf("unable to parse time '%v': %w", value, err) +} + +// MustParseTime parses a date as according to ParseTime, but panics if +// the time is invalid. +func MustParseTime(time string) *dtpb.Time { + result, err := ParseTime(time) + if err != nil { + panic(err) + } + return result +} diff --git a/internal/fhir/time_test.go b/internal/fhir/time_test.go new file mode 100644 index 0000000..a91858b --- /dev/null +++ b/internal/fhir/time_test.go @@ -0,0 +1,352 @@ +package fhir_test + +import ( + "testing" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +func TestTimeOfDay_BadInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + hour int64 + minute int64 + second int64 + microseconds int64 + }{ + {"BigHour", 24, 10, 0, 0}, + {"NegativeHour", -1, 10, 0, 0}, + {"BigMinute", 12, 60, 0, 0}, + {"NegativeMinute", 12, -1, 0, 0}, + {"BigSecond", 12, 10, 60, 0}, + {"NegativeSecond", 12, 10, -1, 0}, + {"BigMicrosecond", 12, 10, 0, 1_000_000}, + {"NegativeMicrosecond", 12, 10, 0, -1}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fhir.TimeOfDay(tc.hour, tc.minute, tc.second, tc.microseconds) + + if err == nil { + t.Errorf("TimeOfDay(%v): want err, got nil", tc.name) + } + }) + } +} + +func TestTimeOfDay_GoodInput_ReturnsTimeBelow24Hours(t *testing.T) { + testCases := []struct { + name string + hour int64 + minute int64 + second int64 + microseconds int64 + }{ + {"NormalValue", 4, 20, 6, 9}, + {"MinHour", 0, 20, 6, 9}, + {"MaxHour", 23, 20, 6, 9}, + {"MinMinute", 4, 0, 6, 9}, + {"MaxMinute", 4, 59, 6, 9}, + {"MinSecond", 4, 20, 0, 9}, + {"MaxSecond", 4, 20, 59, 9}, + {"MinMicros", 4, 20, 6, 0}, + {"MaxMicros", 4, 20, 6, 999_999}, + {"AlmostMidnight", 23, 59, 59, 999_999}, + {"Midnight", 0, 0, 0, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + day := (time.Hour * 24).Microseconds() + got, err := fhir.TimeOfDay(tc.hour, tc.minute, tc.second, tc.microseconds) + if err != nil { + t.Fatalf("TimeOfDay(%v): unexpected error: %v", tc.name, got) + } + + if got, want := got.ValueUs, day; got >= want { + t.Errorf("TimeOfDay(%v): got us %v, want below %v", tc.name, got, want) + } + }) + } +} + +func TestParseTime_GoodInput_ReturnsTime(t *testing.T) { + testCases := []struct { + name string + value string + wantPrecision dtpb.Time_Precision + }{ + {"Second", "01:02:03", dtpb.Time_SECOND}, + {"Milliseconds", "01:02:03.456", dtpb.Time_MILLISECOND}, + {"Microseconds", "01:02:03.456789", dtpb.Time_MICROSECOND}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := fhir.ParseTime(tc.value) + if err != nil { + t.Fatalf("ParseTime(%v): got err: %v", tc.name, err) + } + + t.Run("HasExpectedPrecision", func(t *testing.T) { + if got, want := got.Precision, tc.wantPrecision; got != want { + t.Errorf("ParseTime(%v): got %v, want %v", tc.name, got, want) + } + }) + }) + } +} + +func TestParseTime_BadInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + value string + }{ + {"BadHour", "25:02:03"}, + {"BadMinute", "01:61:03"}, + {"BadSecond", "01:02:61"}, + {"WrongFormat", "01:02:61:72"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fhir.ParseTime(tc.value) + + if err == nil { + t.Errorf("ParseTime(%v): want err, got nil", tc.name) + } + }) + } +} + +func TestMustParseTime_GoodInput_ReturnsTime(t *testing.T) { + got := fhir.MustParseTime("01:02:03.456789") + + if got, want := got.Precision, dtpb.Time_MICROSECOND; got != want { + t.Errorf("MustParseTime: got %v, want %v", got, want) + } +} + +func TestMustParseTime_BadInput_Panics(t *testing.T) { + defer func() { _ = recover() }() + + fhir.MustParseTime("March 26, 1993") + + // If code reaches here, it means we didn't panic + t.Errorf("MustParseTime: expected panic") +} + +func TestParseDate_GoodInput_ReturnsDate(t *testing.T) { + testCases := []struct { + name string + value string + wantPrecision dtpb.Date_Precision + }{ + {"Year", "2012", dtpb.Date_YEAR}, + {"Month", "2012-10", dtpb.Date_MONTH}, + {"Day", "2012-10-15", dtpb.Date_DAY}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := fhir.ParseDate(tc.value) + if err != nil { + t.Fatalf("ParseDate(%v): got err: %v", tc.name, err) + } + + t.Run("HasExpectedPrecision", func(t *testing.T) { + if got, want := got.Precision, tc.wantPrecision; got != want { + t.Errorf("ParseDate(%v): got %v, want %v", tc.name, got, want) + } + }) + }) + } +} + +func TestParseDate_BadInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + value string + }{ + {"BadMonth", "2012-13"}, + {"BadDay", "2012-10-32"}, + {"WrongFormat", "2012-10-32T10:32:00"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fhir.ParseDate(tc.value) + + if err == nil { + t.Fatalf("ParseDate(%v): want err, got nil", tc.name) + } + }) + } +} + +func TestMustParseDate_GoodInput_ReturnsTime(t *testing.T) { + got := fhir.MustParseDate("2012") + + if got, want := got.Precision, dtpb.Date_YEAR; got != want { + t.Errorf("MustParseDate: got %v, want %v", got, want) + } +} + +func TestMustParseDate_BadInput_Panics(t *testing.T) { + defer func() { _ = recover() }() + + fhir.MustParseDate("March 26, 1993") + + // If code reaches here, it means we didn't panic + t.Errorf("MustParseDate: expected panic") +} + +func TestParseDateTime_GoodInput_ReturnsDateTime(t *testing.T) { + testCases := []struct { + name string + value string + wantPrecision dtpb.DateTime_Precision + }{ + {"Year", "2012", dtpb.DateTime_YEAR}, + {"Month", "2012-10", dtpb.DateTime_MONTH}, + {"Day", "2012-10-15", dtpb.DateTime_DAY}, + {"Second", "2012-10-15T01:02:03-04:00", dtpb.DateTime_SECOND}, + {"Millisecond", "2012-10-15T01:02:03.123-04:00", dtpb.DateTime_MILLISECOND}, + {"Microsecond", "2012-10-15T01:02:03.123456-04:00", dtpb.DateTime_MICROSECOND}, + {"SecondZ", "2012-10-15T01:02:03Z", dtpb.DateTime_SECOND}, + {"MillisecondZ", "2012-10-15T01:02:03.123Z", dtpb.DateTime_MILLISECOND}, + {"MicrosecondZ", "2012-10-15T01:02:03.123456Z", dtpb.DateTime_MICROSECOND}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := fhir.ParseDateTime(tc.value) + if err != nil { + t.Fatalf("ParseDateTime(%v): got err: %v", tc.name, err) + } + + t.Run("HasExpectedPrecision", func(t *testing.T) { + if got, want := got.Precision, tc.wantPrecision; got != want { + t.Errorf("ParseDateTime(%v): got %v, want %v", tc.name, got, want) + } + }) + }) + } +} + +func TestParseDateTime_BadInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + value string + }{ + {"BadMonth", "2012-13"}, + {"BadDay", "2012-10-32"}, + {"BadHour", "2012-10-15T24:02:03-04:00"}, + {"BadMinute", "2012-10-15T01:61:03-04:00"}, + {"BadSecond", "2012-10-15T01:02:61-04:00"}, + {"BadTimezone", "2012-10-15T01:02:61-61:00"}, + {"WrongFormat", "January 02, 2015"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fhir.ParseDateTime(tc.value) + + if err == nil { + t.Errorf("ParseDateTime(%v): want err, got nil", tc.name) + } + }) + } +} + +func TestMustParseDateTime_GoodInput_ReturnsTime(t *testing.T) { + got := fhir.MustParseDateTime("2012") + + if got, want := got.Precision, dtpb.DateTime_YEAR; got != want { + t.Errorf("MustParseDate: got %v, want %v", got, want) + } +} + +func TestMustParseDateTime_BadInput_Panics(t *testing.T) { + defer func() { _ = recover() }() + + fhir.MustParseDateTime("March 26, 1993") + + // If code reaches here, it means we didn't panic + t.Errorf("MustParseDateTime: expected panic") +} + +func TestParseInstant_GoodInput_ReturnsInstant(t *testing.T) { + testCases := []struct { + name string + value string + wantPrecision dtpb.Instant_Precision + }{ + {"Second", "2019-01-02T01:02:03-04:00", dtpb.Instant_SECOND}, + {"Millisecond", "2019-01-02T01:02:03.123-04:00", dtpb.Instant_MILLISECOND}, + {"Microsecond", "2019-01-02T01:02:03.123456-04:00", dtpb.Instant_MICROSECOND}, + {"SecondZ", "2019-01-02T01:02:03Z", dtpb.Instant_SECOND}, + {"MillisecondZ", "2019-01-02T01:02:03.123Z", dtpb.Instant_MILLISECOND}, + {"MicrosecondZ", "2019-01-02T01:02:03.123456Z", dtpb.Instant_MICROSECOND}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := fhir.ParseInstant(tc.value) + if err != nil { + t.Fatalf("ParseInstant(%v): got err: %v", tc.name, err) + } + + t.Run("HasExpectedPrecision", func(t *testing.T) { + if got, want := got.Precision, tc.wantPrecision; got != want { + t.Errorf("ParseInstant(%v): got %v, want %v", tc.name, got, want) + } + }) + }) + } +} + +func TestParseInstant_BadInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + value string + }{ + {"BadMonth", "2019-20-02T10:02:03-04:00"}, + {"BadDay", "2019-01-40T10:02:03-04:00"}, + {"BadHour", "2019-01-02T24:02:03-04:00"}, + {"BadMinute", "2019-01-02T01:60:03-04:00"}, + {"BadSecond", "2019-01-02T01:02:60-04:00"}, + {"BadTimeZone", "2019-01-02T01:02:60-64:00"}, + {"WrongFormat", "January 02, 2015"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fhir.ParseInstant(tc.value) + + if err == nil { + t.Errorf("ParseInstant(%v): want err, got nil", tc.name) + } + }) + } +} + +func TestMustParseInstant_GoodInput_ReturnsTime(t *testing.T) { + got := fhir.MustParseInstant("2019-10-02T01:02:03-04:00") + + if got, want := got.Precision, dtpb.Instant_SECOND; got != want { + t.Errorf("MustParseInstant: got %v, want %v", got, want) + } +} + +func TestMustParseInstant_BadInput_Panics(t *testing.T) { + defer func() { _ = recover() }() + + fhir.MustParseInstant("March 26, 1993") + + // If code reaches here, it means we didn't panic + t.Errorf("MustParseInstant: expected panic") +} diff --git a/internal/fhirconv/doc.go b/internal/fhirconv/doc.go new file mode 100644 index 0000000..6dae525 --- /dev/null +++ b/internal/fhirconv/doc.go @@ -0,0 +1,5 @@ +/* +Package fhirconv provides conversion utilities to Go-native types from FHIR R4 +Elements. +*/ +package fhirconv diff --git a/internal/fhirconv/integer.go b/internal/fhirconv/integer.go new file mode 100644 index 0000000..5d09906 --- /dev/null +++ b/internal/fhirconv/integer.go @@ -0,0 +1,112 @@ +package fhirconv + +import ( + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/narrow" + "golang.org/x/exp/constraints" +) + +var ( + // ErrIntegerTruncated is an error raised when an integer truncation occurs + // during integer conversion + ErrIntegerTruncated = errors.New("integer truncation") +) + +// integerType is a constraint for FHIR integer types. +type integerType interface { + *dtpb.Integer | *dtpb.UnsignedInt | *dtpb.PositiveInt +} + +// ToInt8 converts a FHIR Integer type into a Go native int8. +func ToInt8[From integerType](v From) (int8, error) { + return ToInteger[int8](v) +} + +// ToInt16 converts a FHIR Integer type into a Go native int16. +func ToInt16[From integerType](v From) (int16, error) { + return ToInteger[int16](v) +} + +// ToInt32 converts a FHIR Integer type into a Go native int32. +func ToInt32[From integerType](v From) (int32, error) { + return ToInteger[int32](v) +} + +// ToInt64 converts a FHIR Integer type into a Go native int64. +func ToInt64[From integerType](v From) (int64, error) { + return ToInteger[int64](v) +} + +// ToInt converts a FHIR Integer type into a Go native int. +func ToInt[From integerType](v From) (int, error) { + return ToInteger[int](v) +} + +// ToUint8 converts a FHIR Integer type into a Go native uint8. +func ToUint8[From integerType](v From) (uint8, error) { + return ToInteger[uint8](v) +} + +// ToUint16 converts a FHIR Integer type into a Go native uint16. +func ToUint16[From integerType](v From) (uint16, error) { + return ToInteger[uint16](v) +} + +// ToUint32 converts a FHIR Integer type into a Go native uint32. +func ToUint32[From integerType](v From) (uint32, error) { + return ToInteger[uint32](v) +} + +// ToUint64 converts a FHIR Integer type into a Go native uint64. +func ToUint64[From integerType](v From) (uint64, error) { + return ToInteger[uint64](v) +} + +// ToUint converts a FHIR Integer type into a Go native uint. +func ToUint[From integerType](v From) (uint, error) { + return ToInteger[uint](v) +} + +// ToInteger converts a FHIR Integer type into a Go native integer type. +// +// If the value of the integer does not fit into the receiver integer type, +// this function will return an ErrIntegerTruncated. +func ToInteger[To constraints.Integer, From integerType](v From) (To, error) { + var result To + if val, ok := any(v).(interface{ GetValue() uint32 }); ok { + if result, ok := narrow.ToInteger[To](uint64(val.GetValue())); ok { + return result, nil + } + return 0, truncationError[To](val.GetValue()) + } else if val, ok := any(v).(interface{ GetValue() int32 }); ok { + + if result, ok := narrow.ToInteger[To](int64(val.GetValue())); ok { + return result, nil + } + return 0, truncationError[To](val.GetValue()) + } + // This cannot be reached because this function is constrained to only + // take FHIR Elements that fit one of the above two types. + return result, ErrIntegerTruncated +} + +// MustConvertToInteger converts a FHIR Integer type into a Go native integer type. +// +// If the value stored in the integer type cannot fit into the receiver type, +// this function will panic. +func MustConvertToInteger[To constraints.Integer, From integerType](v From) To { + result, err := ToInteger[To](v) + if err != nil { + panic(err) + } + return result +} + +// truncationError forms an Error type for truncation errors. +func truncationError[To constraints.Integer, From constraints.Integer](value From) error { + var result To + return fmt.Errorf("%w: type %T with value %v does not fit into receiver %T", ErrIntegerTruncated, value, value, result) +} diff --git a/internal/fhirconv/integer_test.go b/internal/fhirconv/integer_test.go new file mode 100644 index 0000000..95bcc07 --- /dev/null +++ b/internal/fhirconv/integer_test.go @@ -0,0 +1,182 @@ +package fhirconv_test + +import ( + "errors" + "math" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirconv" + "golang.org/x/exp/constraints" +) + +type integerType interface { + *dtpb.Integer | *dtpb.UnsignedInt | *dtpb.PositiveInt +} + +func wantTruncation[To constraints.Integer, From integerType](value From) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + var to To + + _, err := fhirconv.ToInteger[To](value) + + if got, want := err, fhirconv.ErrIntegerTruncated; !errors.Is(got, want) { + t.Errorf("ToInteger[%T]: got err %v, want %v", to, got, want) + } + } +} + +func wantConversion[To constraints.Integer, From integerType, Input constraints.Integer](conv func(Input) From, input Input) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + var to To + value := conv(input) + + got, err := fhirconv.ToInteger[To](value) + if err != nil { + t.Fatalf("ToInteger[%T]: unexpected error '%v'", to, err) + } + + if got, want := got, To(input); got != want { + t.Errorf("ToInteger[%T]: got err %v, want %v", to, got, want) + } + } +} + +func TestToInteger_ValueExceedsReceiver_ReturnsError(t *testing.T) { + // Note: nested sub-tests are being done both for organization and test naming. + // We also can't use table-driven tests here, since the input is a type -- so + // this forces duplication. + t.Run("FromPositiveInt", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](fhir.PositiveInt(math.MaxUint32))) + t.Run("ToInt16", wantTruncation[int16](fhir.PositiveInt(math.MaxUint32))) + t.Run("ToInt32", wantTruncation[int32](fhir.PositiveInt(math.MaxUint32))) + t.Run("ToUint8", wantTruncation[uint8](fhir.PositiveInt(math.MaxUint32))) + t.Run("ToUint16", wantTruncation[uint16](fhir.PositiveInt(math.MaxUint32))) + }) + + t.Run("FromUnsignedInt", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](fhir.UnsignedInt(math.MaxUint32))) + t.Run("ToInt16", wantTruncation[int16](fhir.UnsignedInt(math.MaxUint32))) + t.Run("ToInt32", wantTruncation[int32](fhir.UnsignedInt(math.MaxUint32))) + t.Run("ToUint8", wantTruncation[uint8](fhir.UnsignedInt(math.MaxUint32))) + t.Run("ToUint16", wantTruncation[uint16](fhir.UnsignedInt(math.MaxUint32))) + }) + + t.Run("FromInteger", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](fhir.Integer(math.MaxInt32))) + t.Run("ToInt16", wantTruncation[int16](fhir.Integer(math.MaxInt32))) + // int32, int64, and int can't truncate + t.Run("ToUint8", wantTruncation[uint8](fhir.Integer(-1))) + t.Run("ToUint16", wantTruncation[uint16](fhir.Integer(-1))) + t.Run("ToUint32", wantTruncation[uint32](fhir.Integer(-1))) + t.Run("ToUint32", wantTruncation[uint64](fhir.Integer(-1))) + t.Run("ToUintptr", wantTruncation[uintptr](fhir.Integer(-1))) + t.Run("ToUint", wantTruncation[uint](fhir.Integer(-1))) + }) +} + +func TestToInteger_ValueWithinRange_ReturnsValue(t *testing.T) { + // Note: nested sub-tests are being done both for organization and test naming. + // We also can't use table-driven tests here, since the input is a type -- so + // this forces duplication. + t.Run("FromPositiveInt", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](fhir.PositiveInt, 0)) + t.Run("ToInt8Max", wantConversion[int8](fhir.PositiveInt, math.MaxInt8)) + t.Run("ToInt16", wantConversion[int16](fhir.PositiveInt, 0)) + t.Run("ToInt16Max", wantConversion[int16](fhir.PositiveInt, math.MaxInt16)) + t.Run("ToInt32", wantConversion[int32](fhir.PositiveInt, 0)) + t.Run("ToInt32Max", wantConversion[int32](fhir.PositiveInt, math.MaxInt32)) + t.Run("ToInt64", wantConversion[int64](fhir.PositiveInt, 0)) + t.Run("ToInt64MaxUint32", wantConversion[int64](fhir.PositiveInt, math.MaxUint32)) + t.Run("ToInt", wantConversion[int](fhir.PositiveInt, 0)) + t.Run("ToIntMaxInt32", wantConversion[int](fhir.PositiveInt, math.MaxInt32)) + t.Run("ToUint8", wantConversion[uint8](fhir.PositiveInt, 0)) + t.Run("ToUint8Max", wantConversion[uint8](fhir.PositiveInt, math.MaxUint8)) + t.Run("ToUint16", wantConversion[uint16](fhir.PositiveInt, 0)) + t.Run("ToUint16Max", wantConversion[uint16](fhir.PositiveInt, math.MaxUint8)) + t.Run("ToUint32", wantConversion[uint32](fhir.PositiveInt, 0)) + t.Run("ToUint32Max", wantConversion[uint32](fhir.PositiveInt, math.MaxUint32)) + t.Run("ToUint64", wantConversion[uint64](fhir.PositiveInt, 0)) + t.Run("ToUint64MaxUint32", wantConversion[uint64](fhir.PositiveInt, math.MaxUint32)) + t.Run("ToUint", wantConversion[uint](fhir.PositiveInt, 0)) + t.Run("ToUintMaxUint32", wantConversion[uint](fhir.PositiveInt, math.MaxUint32)) + t.Run("ToUintptr", wantConversion[uintptr](fhir.PositiveInt, 0)) + }) + + t.Run("FromUnsignedInt", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](fhir.UnsignedInt, 0)) + t.Run("ToInt8Max", wantConversion[int8](fhir.UnsignedInt, math.MaxInt8)) + t.Run("ToInt16", wantConversion[int16](fhir.UnsignedInt, 0)) + t.Run("ToInt16Max", wantConversion[int16](fhir.UnsignedInt, math.MaxInt16)) + t.Run("ToInt32", wantConversion[int32](fhir.UnsignedInt, 0)) + t.Run("ToInt32Max", wantConversion[int32](fhir.UnsignedInt, math.MaxInt32)) + t.Run("ToInt64", wantConversion[int64](fhir.UnsignedInt, 0)) + t.Run("ToInt64MaxUint32", wantConversion[int64](fhir.UnsignedInt, math.MaxUint32)) + t.Run("ToInt", wantConversion[int](fhir.UnsignedInt, 0)) + t.Run("ToIntMaxInt32", wantConversion[int](fhir.UnsignedInt, math.MaxInt32)) + t.Run("ToUint8", wantConversion[uint8](fhir.UnsignedInt, 0)) + t.Run("ToUint8Max", wantConversion[uint8](fhir.UnsignedInt, math.MaxUint8)) + t.Run("ToUint16", wantConversion[uint16](fhir.UnsignedInt, 0)) + t.Run("ToUint16Max", wantConversion[uint16](fhir.UnsignedInt, math.MaxUint8)) + t.Run("ToUint32", wantConversion[uint32](fhir.UnsignedInt, 0)) + t.Run("ToUint32Max", wantConversion[uint32](fhir.UnsignedInt, math.MaxUint32)) + t.Run("ToUint64", wantConversion[uint64](fhir.UnsignedInt, 0)) + t.Run("ToUint64MaxUint32", wantConversion[uint64](fhir.UnsignedInt, math.MaxUint32)) + t.Run("ToUint", wantConversion[uint](fhir.UnsignedInt, 0)) + t.Run("ToUintMaxUint32", wantConversion[uint](fhir.UnsignedInt, math.MaxUint32)) + t.Run("ToUintptr", wantConversion[uintptr](fhir.UnsignedInt, 0)) + }) + + t.Run("FromInteger", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](fhir.Integer, 0)) + t.Run("ToInt8Max", wantConversion[int8](fhir.Integer, math.MaxInt8)) + t.Run("ToInt8Min", wantConversion[int8](fhir.Integer, math.MinInt8)) + t.Run("ToInt16", wantConversion[int16](fhir.Integer, 0)) + t.Run("ToInt16Max", wantConversion[int16](fhir.Integer, math.MaxInt16)) + t.Run("ToInt16Min", wantConversion[int16](fhir.Integer, math.MinInt16)) + t.Run("ToInt32", wantConversion[int32](fhir.Integer, 0)) + t.Run("ToInt32Max", wantConversion[int32](fhir.Integer, math.MaxInt32)) + t.Run("ToInt32Min", wantConversion[int32](fhir.Integer, math.MinInt32)) + t.Run("ToInt64", wantConversion[int64](fhir.Integer, 0)) + t.Run("ToInt64MaxInt32", wantConversion[int64](fhir.Integer, math.MaxInt32)) + t.Run("ToInt64MinInt32", wantConversion[int64](fhir.Integer, math.MinInt32)) + t.Run("ToInt", wantConversion[int](fhir.Integer, 0)) + t.Run("ToIntMaxInt32", wantConversion[int](fhir.Integer, math.MaxInt32)) + t.Run("ToIntMinInt32", wantConversion[int](fhir.Integer, math.MinInt32)) + t.Run("ToUint8", wantConversion[uint8](fhir.Integer, 0)) + t.Run("ToUint8Max", wantConversion[uint8](fhir.Integer, math.MaxUint8)) + t.Run("ToUint16", wantConversion[uint16](fhir.Integer, 0)) + t.Run("ToUint16Max", wantConversion[uint16](fhir.Integer, math.MaxUint16)) + t.Run("ToUint32", wantConversion[uint32](fhir.Integer, 0)) + t.Run("ToUint32MaxInt32", wantConversion[uint32](fhir.Integer, math.MaxInt32)) + t.Run("ToUint64", wantConversion[uint64](fhir.Integer, 0)) + t.Run("ToUint64MaxInt32", wantConversion[uint64](fhir.Integer, math.MaxInt32)) + t.Run("ToUint", wantConversion[uint](fhir.Integer, 0)) + t.Run("ToUintMaxInt32", wantConversion[uint](fhir.Integer, math.MaxInt32)) + t.Run("ToUintptr", wantConversion[uintptr](fhir.Integer, 0)) + }) +} + +func TestMustConvertToInteger_ValueWithinRange_ReturnsValue(t *testing.T) { + input := int32(42) + value := fhir.Integer(input) + + result := fhirconv.MustConvertToInteger[int32](value) + + if got, want := result, input; got != want { + t.Fatalf("MustConvertToInteger[int32]: got %v, want %v", got, want) + } +} + +func TestMustConvertToInteger_ValueOutsideRange_Panics(t *testing.T) { + defer func() { _ = recover() }() + value := fhir.Integer(-1) + + fhirconv.MustConvertToInteger[uint8](value) + + // This can't be reached if a panic occurs + t.Fatalf("MustConvertToInt: expected panic") +} diff --git a/internal/fhirconv/string.go b/internal/fhirconv/string.go new file mode 100644 index 0000000..3e4516d --- /dev/null +++ b/internal/fhirconv/string.go @@ -0,0 +1,158 @@ +package fhirconv + +import ( + b64 "encoding/base64" + "fmt" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +// ToString converts a basic FHIR Primitive into a human-readable string +// representation. +func ToString[T fhir.PrimitiveType](val T) string { + return toString(val) +} + +// toString is the implementation of the ToString function. +// +// This is a separate function written in terms of an interface, rather than +// being generic, to avoid unnecessary code-bloat -- since generics will generate +// code for every instantiation with a different type. +func toString(val fhir.Element) string { + switch val := val.(type) { + case interface{ GetValue() string }: + // URI, URL, OID, UUID, Canonical, String, ID, Markdown, Code, Decimal + return val.GetValue() + case interface{ GetValue() bool }: + // Boolean + return fmt.Sprintf("%v", val.GetValue()) + case interface{ GetValue() uint32 }: + // PositiveInteger, UnsignedInt + return fmt.Sprintf("%v", val.GetValue()) + case interface{ GetValue() int32 }: + // Integer + return fmt.Sprintf("%v", val.GetValue()) + case interface{ GetValue() []byte }: + // Base64Binary + return b64.StdEncoding.EncodeToString(val.GetValue()) + case *dtpb.Instant: + return InstantToString(val) + case *dtpb.DateTime: + return DateTimeToString(val) + case *dtpb.Time: + return TimeToString(val) + case *dtpb.Date: + return DateToString(val) + } + // This can't be reached; the above switch is exhaustive for all possible + // inputs, which is restricted by the type constraint. + return "" +} + +// InstantToString converts the FHIR Instant element into its string reprsentation +// as defined in http://hl7.org/fhir/R4/datatypes.html#instant. +// +// The level of precision in the output is equivalent to the precision defined +// in the input Instant proto. +func InstantToString(val *dtpb.Instant) string { + if tm, err := InstantToTime(val); err == nil { + switch val.GetPrecision() { + case dtpb.Instant_SECOND: + return tm.Format("2006-01-02T15:04:05-07:00") + case dtpb.Instant_MILLISECOND: + return tm.Format("2006-01-02T15:04:05.000-07:00") + case dtpb.Instant_MICROSECOND: + fallthrough + default: + return tm.Format("2006-01-02T15:04:05.000000-07:00") + } + } + // Fall-back to a basic representation (this shouldn't happen unless timezone + // information is garbage, which is a developer-driven issue). + return fmt.Sprintf("Instant(%v)", val.GetValueUs()) +} + +// DateTimeToString converts the FHIR DateTime element into its string reprsentation +// as defined in http://hl7.org/fhir/R4/datatypes.html#datetime. +// +// The level of precision in the output is equivalent to the precision defined +// in the input DateTime proto. +func DateTimeToString(val *dtpb.DateTime) string { + if tm, err := DateTimeToTime(val); err == nil { + switch val.GetPrecision() { + case dtpb.DateTime_YEAR: + return tm.Format("2006") + case dtpb.DateTime_MONTH: + return tm.Format("2006-01") + case dtpb.DateTime_DAY: + return tm.Format("2006-01-02") + case dtpb.DateTime_SECOND: + return tm.Format("2006-01-02T15:04:05-07:00") + case dtpb.DateTime_MILLISECOND: + return tm.Format("2006-01-02T15:04:05.000-07:00") + case dtpb.DateTime_MICROSECOND: + fallthrough + default: + return tm.Format("2006-01-02T15:04:05.000000-07:00") + } + } + + // Fall-back to a basic representation (this shouldn't happen unless timezone + // information is garbage, which is a developer-driven issue). + return fmt.Sprintf("DateTime(%v)", val.GetValueUs()) +} + +// DateToString converts the FHIR Date element into its string reprsentation +// as defined in http://hl7.org/fhir/R4/datatypes.html#date. +// +// The level of precision in the output is equivalent to the precision defined +// in the input Date proto. +func DateToString(val *dtpb.Date) string { + if tm, err := DateToTime(val); err == nil { + switch val.GetPrecision() { + case dtpb.Date_YEAR: + return tm.Format("2006") + case dtpb.Date_MONTH: + return tm.Format("2006-01") + case dtpb.Date_DAY: + fallthrough + default: + return tm.Format("2006-01-02") + } + } + + // Fall-back to a basic representation (this shouldn't happen unless timezone + // information is garbage, which is a developer-driven issue). + return fmt.Sprintf("Date(%v)", val.GetValueUs()) +} + +// TimeToString converts the FHIR Time element into its string reprsentation +// as defined in http://hl7.org/fhir/R4/datatypes.html#time. +// +// The level of precision in the output is equivalent to the precision defined +// in the input Time proto. +func TimeToString(val *dtpb.Time) string { + duration := TimeToDuration(val) + + hours := (duration / time.Hour) % (time.Hour * 24) + duration %= time.Hour + minutes := duration / time.Minute + duration %= time.Minute + seconds := duration / time.Second + duration %= time.Second + micros := duration / time.Microsecond + + switch val.GetPrecision() { + case dtpb.Time_SECOND: + return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) + case dtpb.Time_MILLISECOND: + millis := micros / 1_000 + return fmt.Sprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis) + case dtpb.Time_MICROSECOND: + fallthrough + default: + return fmt.Sprintf("%02d:%02d:%02d.%06d", hours, minutes, seconds, micros) + } +} diff --git a/internal/fhirconv/string_test.go b/internal/fhirconv/string_test.go new file mode 100644 index 0000000..0d7f550 --- /dev/null +++ b/internal/fhirconv/string_test.go @@ -0,0 +1,358 @@ +package fhirconv_test + +import ( + "fmt" + "testing" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirconv" +) + +func TestToString_String_ReturnsValue(t *testing.T) { + want := "Hello world" + str := fhir.String(want) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[String]: got %v, want %v", got, want) + } +} + +func TestToString_Code_ReturnsValue(t *testing.T) { + want := "Hello world" + str := fhir.Code(want) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[Code]: got %v, want %v", got, want) + } +} + +func TestToString_ID_ReturnsValue(t *testing.T) { + want := "0xdeadbeef" + str := fhir.ID(want) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[Id]: got %v, want %v", got, want) + } +} + +func TestToString_Markdown_ReturnsValue(t *testing.T) { + want := "**This** is markdown" + str := fhir.Markdown(want) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[Markdown]: got %v, want %v", got, want) + } +} + +func TestToString_URI_ReturnsValue(t *testing.T) { + want := "https://example.com" + str := fhir.URI(want) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[Uri]: got %v, want %v", got, want) + } +} + +func TestToString_URL_ReturnsValue(t *testing.T) { + want := "https://example.com" + str := fhir.URL(want) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[Url]: got %v, want %v", got, want) + } +} + +func TestToString_OID_ReturnsValue(t *testing.T) { + want := "123456" + str := fhir.OID(want) + + got := fhirconv.ToString(str) + + if want := fmt.Sprintf("urn:oid:%v", want); got != want { + t.Errorf("String[Oid]: got %v, want %v", got, want) + } +} + +func TestToString_UUID_ReturnsValue(t *testing.T) { + want := "25674bb5-6153-4dd7-9d4e-00fecc9058f1" + str := fhir.UUID(want) + + got := fhirconv.ToString(str) + + if want := fmt.Sprintf("urn:uuid:%v", want); got != want { + t.Errorf("String[Uuid]: got %v, want %v", got, want) + } +} + +func TestToString_Boolean_ReturnsValue(t *testing.T) { + want := "true" + str := fhir.Boolean(true) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[Boolean]: got %v, want %v", got, want) + } +} + +func TestToString_PositiveInt_ReturnsValue(t *testing.T) { + want := "42" + str := fhir.PositiveInt(42) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[PositiveInt]: got %v, want %v", got, want) + } +} + +func TestToString_UnsignedInt_ReturnsValue(t *testing.T) { + want := "42" + str := fhir.UnsignedInt(42) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[UnsignedInt]: got %v, want %v", got, want) + } +} + +func TestToString_Integer_ReturnsValue(t *testing.T) { + want := "42" + str := fhir.Integer(42) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[Integer]: got %v, want %v", got, want) + } +} + +func TestToString_Base64Binary_ReturnsValue(t *testing.T) { + want := "3q2+7w==" + str := fhir.Base64Binary([]byte{0xde, 0xad, 0xbe, 0xef}) + + got := fhirconv.ToString(str) + + if got != want { + t.Errorf("String[Base64Binary]: got %v, want %v", got, want) + } +} + +func TestToString_Instant_ReturnsValue(t *testing.T) { + testCases := []struct { + name string + want string + precision dtpb.Instant_Precision + }{ + {"UnspecifiedPrecision", "1970-01-01T00:00:32.000000+00:00", dtpb.Instant_PRECISION_UNSPECIFIED}, + {"MicrosecondPrecision", "1970-01-01T00:00:32.000000+00:00", dtpb.Instant_MICROSECOND}, + {"MillisecondPrecision", "1970-01-01T00:00:32.000+00:00", dtpb.Instant_MILLISECOND}, + {"SecondPrecision", "1970-01-01T00:00:32+00:00", dtpb.Instant_SECOND}, + } + timestamp := int64(time.Second * 32 / time.Microsecond) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input := fhir.Instant(time.UnixMicro(timestamp).UTC()) + input.Precision = tc.precision + + got := fhirconv.ToString(input) + + if got, want := got, tc.want; got != want { + t.Errorf("ToString[Instant]: got %v, want %v", got, want) + } + }) + } +} + +func TestToString_DateTime_ReturnsValue(t *testing.T) { + testCases := []struct { + name string + want string + precision dtpb.DateTime_Precision + }{ + {"UnspecifiedPrecision", "1970-01-01T00:00:32.000000+00:00", dtpb.DateTime_PRECISION_UNSPECIFIED}, + {"MicrosecondPrecision", "1970-01-01T00:00:32.000000+00:00", dtpb.DateTime_MICROSECOND}, + {"MillisecondPrecision", "1970-01-01T00:00:32.000+00:00", dtpb.DateTime_MILLISECOND}, + {"SecondPrecision", "1970-01-01T00:00:32+00:00", dtpb.DateTime_SECOND}, + {"DayPrecision", "1970-01-01", dtpb.DateTime_DAY}, + {"MonthPrecision", "1970-01", dtpb.DateTime_MONTH}, + {"YearPrecision", "1970", dtpb.DateTime_YEAR}, + } + timestamp := int64(time.Second * 32 / time.Microsecond) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input := fhir.DateTime(time.UnixMicro(timestamp).UTC()) + input.Precision = tc.precision + + got := fhirconv.ToString(input) + + if got, want := got, tc.want; got != want { + t.Errorf("ToString[DateTime]: got %v, want %v", got, want) + } + }) + } +} + +func TestToString_Date_ReturnsValue(t *testing.T) { + testCases := []struct { + name string + want string + precision dtpb.Date_Precision + }{ + {"DayPrecision", "1970-01-01", dtpb.Date_DAY}, + {"MonthPrecision", "1970-01", dtpb.Date_MONTH}, + {"YearPrecision", "1970", dtpb.Date_YEAR}, + } + timestamp := int64(time.Second * 32 / time.Microsecond) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input := fhir.Date(time.UnixMicro(timestamp).UTC()) + input.Precision = tc.precision + + got := fhirconv.ToString(input) + + if got, want := got, tc.want; got != want { + t.Errorf("ToString[Date]: got %v, want %v", got, want) + } + }) + } +} + +func TestToString_Time_ReturnsValue(t *testing.T) { + testCases := []struct { + name string + hour, minute, second, microsecond int64 + want string + }{ + {"MicrosecondPrecision", 12, 32, 10, 123456, "12:32:10.123456"}, + {"MillisecondPrecision", 12, 32, 10, 123_000, "12:32:10.123"}, + {"SecondPrecision", 12, 32, 10, 0, "12:32:10"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input, err := fhir.TimeOfDay(tc.hour, tc.minute, tc.second, tc.microsecond) + if err != nil { + t.Fatalf("ToString[Time]: error setting up time-of-day: %v", err) + } + + got := fhirconv.ToString(input) + + if got, want := got, tc.want; got != want { + t.Errorf("ToString[Date]: got %v, want %v", got, want) + } + }) + } +} + +func TestDateToString_RoundTrip_ReturnsInput(t *testing.T) { + testCases := []struct { + name string + want string + }{ + {"YearPrecision", "2019"}, + {"MonthPrecision", "2019-01"}, + {"DayPrecision", "2019-01-02"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value := fhir.MustParseDate(tc.want) + + got := fhirconv.ToString(value) + + if got != tc.want { + t.Errorf("RoundTrip(%v): got %v, want %v", tc.name, got, tc.want) + } + }) + } +} + +func TestTimeToString_RoundTrip_ReturnsInput(t *testing.T) { + testCases := []struct { + name string + want string + }{ + {"MicrosecondPrecision", "01:02:03.123456"}, + {"MillisecondPrecision", "01:02:03.123"}, + {"SecondPrecision", "01:02:03"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value := fhir.MustParseTime(tc.want) + + got := fhirconv.ToString(value) + + if got != tc.want { + t.Errorf("RoundTrip(%v): got %v, want %v", tc.name, got, tc.want) + } + }) + } +} + +func TestDateTimeToString_RoundTrip_ReturnsInput(t *testing.T) { + testCases := []struct { + name string + want string + }{ + {"YearPrecision", "2019"}, + {"MonthPrecision", "2019-01"}, + {"DayPrecision", "2019-01-02"}, + {"SecondPrecision", "2019-01-02T01:02:03-04:00"}, + {"MillisecondPrecision", "2019-01-02T01:02:03.123-04:00"}, + {"MicrosecondPrecision", "2019-01-02T01:02:03.123456-04:00"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value := fhir.MustParseDateTime(tc.want) + + got := fhirconv.ToString(value) + + if got != tc.want { + t.Errorf("RoundTrip(%v): got %v, want %v", tc.name, got, tc.want) + } + }) + } +} +func TestInstantToString_RoundTrip_ReturnsInput(t *testing.T) { + testCases := []struct { + name string + want string + }{ + {"MicrosecondPrecision", "2019-01-02T01:02:03.123456-04:00"}, + {"MillisecondPrecision", "2019-01-02T01:02:03.123-04:00"}, + {"SecondPrecision", "2019-01-02T01:02:03-04:00"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value := fhir.MustParseInstant(tc.want) + + got := fhirconv.ToString(value) + + if got != tc.want { + t.Errorf("RoundTrip(%v): got %v, want %v", tc.name, got, tc.want) + } + }) + } +} diff --git a/internal/fhirconv/time.go b/internal/fhirconv/time.go new file mode 100644 index 0000000..f7e3f83 --- /dev/null +++ b/internal/fhirconv/time.go @@ -0,0 +1,138 @@ +package fhirconv + +import ( + "fmt" + "strconv" + "time" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/units" +) + +// DateTimeToTime converts a FHIR DateTime element into a Go time.Time value. +// +// This function will only error if the TimeZone field is invalid. +// +// Note: the error that can be returned from this function is unlikely to actually +// occur in practice. FHIR Timezones are always required to be specified in the +// form `+zz:zz` or `-zz:zz` -- and the JSON conversion translates this into +// exactly this form, an empty string, or "UTC" in some cases. These are all +// valid locations and should never fail as a result. +func DateTimeToTime(dt *dtpb.DateTime) (time.Time, error) { + tz, err := parseLocation(dt.GetTimezone()) + if err != nil { + return time.Time{}, fmt.Errorf("fhirconv.TimeFromDateTime: %w", err) + } + return time.UnixMicro(dt.GetValueUs()).In(tz), nil +} + +// InstantToTime converts a FHIR Instant element into a Go time.Time value. +// +// This function will only error if the TimeZone field is invalid. +// +// Note: the error that can be returned from this function is unlikely to actually +// occur in practice. FHIR Timezones are always required to be specified in the +// form `+zz:zz` or `-zz:zz` -- and the JSON conversion translates this into +// exactly this form, an empty string, or "UTC" in some cases. These are all +// valid locations and should never fail as a result. +func InstantToTime(dt *dtpb.Instant) (time.Time, error) { + tz, err := parseLocation(dt.GetTimezone()) + if err != nil { + return time.Time{}, fmt.Errorf("fhirconv.TimeFromInstant: %w", err) + } + return time.UnixMicro(dt.GetValueUs()).In(tz), nil +} + +// DateToTime converts a FHIR Date element into a Go time.Time value. +// +// This function will only error if the TimeZone field is invalid. +// +// Note: the error that can be returned from this function is unlikely to actually +// occur in practice. FHIR Timezones are always required to be specified in the +// form `+zz:zz` or `-zz:zz` -- and the JSON conversion translates this into +// exactly this form, an empty string, or "UTC" in some cases. These are all +// valid locations and should never fail as a result. +func DateToTime(dt *dtpb.Date) (time.Time, error) { + tz, err := parseLocation(dt.GetTimezone()) + if err != nil { + return time.Time{}, fmt.Errorf("fhirconv.Date: %w", err) + } + return time.UnixMicro(dt.GetValueUs()).In(tz), nil +} + +// TimeToDuration converts a FHIR Time element into a Go time.Duration value. +// +// Despite the name `Time` for the FHIR Element, the time is not actually +// associated to any real date -- and thus does not correspond to a distinct +// chronological point, and thus cannot be converted logically into a `time.Time` +// object. +func TimeToDuration(dt *dtpb.Time) time.Duration { + return time.Microsecond * time.Duration(dt.GetValueUs()) +} + +// parseLocation attempts to parse the timezone location from the zone string. +// +// Timezones may be specified in one of 3 formats: +// - Z +// - +zz:zz or -zz:zz +// - UTC (or some name) +// +// Additionally, this function supports empty strings being translated into +// UTC. +func parseLocation(zone string) (*time.Location, error) { + if zone == "" { + return time.UTC, nil + } else if tm, err := time.Parse("MST", zone); err == nil { + return tm.Location(), nil + } else if tm, err := time.Parse("Z07:00", zone); err == nil { + return tm.Location(), nil + } else { + return nil, fmt.Errorf("unable to parse time-zone from '%v'", zone) + } +} + +// DurationToDuration converts a FHIR Duration element into a Go native +// time.Duration object. +// +// This function may return an error in the following conditions: +// - The underlying Decimal value is not able to be parsed into a float64 +// - The unit is not a valid time unit +func DurationToDuration(d *dtpb.Duration) (time.Duration, error) { + value := d.GetValue().GetValue() + decimal, err := strconv.ParseFloat(value, 64) + if err != nil { + return durationToDurationError("bad decimal value '%v': %v", value, err) + } + code := d.GetCode().GetValue() + + unit, err := units.TimeFromSymbol(code) + if err != nil { + return durationToDurationError("invalid unit symbol '%v'", code) + } + + symbol := unit.Symbol() + + // Special handling is necessary as days are not supported by + // time.ParseDuration, as well as the minutes unit being m, not min + switch unit { + case units.Minutes: + symbol = "m" + case units.Days: + decimal *= 24 + symbol = units.Hours.Symbol() + } + + duration, err := time.ParseDuration(fmt.Sprintf("%v%s", decimal, symbol)) + if err != nil { + // This branch should not be possible to be reached. If we have reached this, + // something really bad has happened -- because we form the format string + // manually above. + return durationToDurationError("%v", err) + } + return duration, nil +} + +func durationToDurationError(format string, args ...any) (time.Duration, error) { + message := fmt.Sprintf(format, args...) + return time.Duration(0), fmt.Errorf("fhirconv.DurationToDuration: %v", message) +} diff --git a/internal/fhirconv/time_test.go b/internal/fhirconv/time_test.go new file mode 100644 index 0000000..7c776ed --- /dev/null +++ b/internal/fhirconv/time_test.go @@ -0,0 +1,290 @@ +package fhirconv_test + +import ( + "testing" + "time" + + "github.com/google/fhir/go/fhirversion" + "github.com/google/fhir/go/jsonformat" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirconv" + "google.golang.org/protobuf/testing/protocmp" +) + +func newMarshaller(t *testing.T) *jsonformat.Marshaller { + t.Helper() + + marshaller, err := jsonformat.NewMarshaller(false, "", "", fhirversion.R4) + if err != nil { + t.Fatalf("Error creating marshaller: %v", err) + } + return marshaller +} + +func newUnmarshaller(t *testing.T, zone string) *jsonformat.Unmarshaller { + t.Helper() + + unmarshaller, err := jsonformat.NewUnmarshaller(zone, fhirversion.R4) + if err != nil { + t.Fatalf("Error creating unmarshaller: %v", err) + } + return unmarshaller +} + +func dateTimeExtension(dt *dtpb.DateTime) *dtpb.Extension { + return &dtpb.Extension{ + Value: &dtpb.Extension_ValueX{ + Choice: &dtpb.Extension_ValueX_DateTime{ + DateTime: dt, + }, + }, + } +} + +func loadLocation(t *testing.T, name string) *time.Location { + t.Helper() + + tz, err := time.LoadLocation(name) + if err != nil { + t.Fatalf("Unable to load location: %v", err) + } + return tz +} + +func TestDateTimeToTime_InvalidInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + zone string + }{ + {"InvalidTimeZoneCode", "XYZBLAH"}, + {"LargePositiveMinuteOffset", "10:-90"}, + {"LargeNegativeMinuteOffset", "10:90"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dt := fhir.DateTimeNow() + dt.Timezone = tc.zone + + _, err := fhirconv.DateTimeToTime(dt) + + if err == nil { + t.Errorf("DateTimeToTime(%v): got nil, want err", tc.name) + } + }) + } +} + +func TestDateTimeToTime_ValidInput_ReturnsTime(t *testing.T) { + testCases := []struct { + name string + zone string + location *time.Location + }{ + {"EmptyString", "", time.UTC}, + {"UTC", "UTC", time.UTC}, + {"EST", "EST", loadLocation(t, "EST")}, + {"Z", "Z", time.UTC}, + {"SmallPositiveOffset", "+00:01", time.FixedZone("", 60)}, + {"SmallNegativeOffset", "-00:02", time.FixedZone("", -120)}, + {"LargePositiveOffset", "+12:03", time.FixedZone("", 43380)}, + {"LargeNegativeOffset", "-13:04", time.FixedZone("", -47040)}, + } + const value = 1000 + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dt := fhir.DateTime(time.UnixMicro(value)) + dt.Timezone = tc.zone + want := time.UnixMicro(value).In(tc.location) + + got, err := fhirconv.DateTimeToTime(dt) + if err != nil { + t.Fatalf("DateTimeToTime(%v): got err '%v', want nil", tc.name, err) + } + + if !cmp.Equal(got, want) { + t.Errorf("DateTimeToTime(%v): got '%v', want '%v'", tc.name, got, want) + } + }) + } +} + +func TestDateToTime_InvalidInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + zone string + }{ + {"InvalidTimeZoneCode", "XYZBLAH"}, + {"LargePositiveMinuteOffset", "10:-90"}, + {"LargeNegativeMinuteOffset", "10:90"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dt := fhir.DateNow() + dt.Timezone = tc.zone + + _, err := fhirconv.DateToTime(dt) + + if err == nil { + t.Errorf("DateToTime(%v): got nil, want err", tc.name) + } + }) + } +} + +func TestTimeToTime_ValidInput_ReturnsTime(t *testing.T) { + testCases := []struct { + name string + zone string + location *time.Location + }{ + {"EmptyString", "", time.UTC}, + {"UTC", "UTC", time.UTC}, + {"EST", "EST", loadLocation(t, "EST")}, + {"Z", "Z", time.UTC}, + {"SmallPositiveOffset", "+00:01", time.FixedZone("", 60)}, + {"SmallNegativeOffset", "-00:02", time.FixedZone("", -120)}, + {"LargePositiveOffset", "+12:03", time.FixedZone("", 43380)}, + {"LargeNegativeOffset", "-13:04", time.FixedZone("", -47040)}, + } + const value = 1000 + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dt := fhir.Date(time.UnixMicro(value)) + dt.Timezone = tc.zone + want := time.UnixMicro(value).In(tc.location) + + got, err := fhirconv.DateToTime(dt) + if err != nil { + t.Fatalf("DateToTime(%v): got err '%v', want nil", tc.name, err) + } + + if !cmp.Equal(got, want) { + t.Errorf("DateToTime(%v): got '%v', want '%v'", tc.name, got, want) + } + }) + } +} + +func TestInstantToTime_InvalidInput_ReturnsError(t *testing.T) { + testCases := []struct { + name string + zone string + }{ + {"InvalidTimeZoneCode", "XYZBLAH"}, + {"LargePositiveMinuteOffset", "10:-90"}, + {"LargeNegativeMinuteOffset", "10:90"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dt := fhir.InstantNow() + dt.Timezone = tc.zone + + _, err := fhirconv.InstantToTime(dt) + + if err == nil { + t.Errorf("InstantToTime(%v): got nil, want err", tc.name) + } + }) + } +} + +func TestInstantToTime_ValidInput_ReturnsTime(t *testing.T) { + testCases := []struct { + name string + zone string + location *time.Location + }{ + {"EmptyString", "", time.UTC}, + {"UTC", "UTC", time.UTC}, + {"EST", "EST", loadLocation(t, "EST")}, + {"Z", "Z", time.UTC}, + {"SmallPositiveOffset", "+00:01", time.FixedZone("", 60)}, + {"SmallNegativeOffset", "-00:02", time.FixedZone("", -120)}, + {"LargePositiveOffset", "+12:03", time.FixedZone("", 43380)}, + {"LargeNegativeOffset", "-13:04", time.FixedZone("", -47040)}, + } + const value = 1000 + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dt := fhir.Instant(time.UnixMicro(value)) + dt.Timezone = tc.zone + want := time.UnixMicro(value).In(tc.location) + + got, err := fhirconv.InstantToTime(dt) + if err != nil { + t.Fatalf("InstantToTime(%v): got err '%v', want nil", tc.name, err) + } + + if !cmp.Equal(got, want) { + t.Errorf("InstantToTime(%v): got '%v', want '%v'", tc.name, got, want) + } + }) + } +} + +func TestDurationToDuration_BadInput_ReturnsError(t *testing.T) { + // Creates a base "good" Duration that we can modify for test purposes without + // coupling to the units package explicitly. + makeDuration := func(setup func(*dtpb.Duration)) *dtpb.Duration { + base := fhir.Seconds(time.Second * 10) + setup(base) + return base + } + + testCases := []struct { + name string + duration *dtpb.Duration + }{ + {"BadFloatValue", makeDuration(func(d *dtpb.Duration) { d.Value.Value = "4e99999" })}, + {"BadUnitSymbol", makeDuration(func(d *dtpb.Duration) { d.Code = fhir.Code("blarg") })}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fhirconv.DurationToDuration(tc.duration) + + if err == nil { + t.Errorf("DurationToDuration(%v): got nil, want err", tc.name) + } + }) + } +} + +func TestDurationToDuration_RoundTrip(t *testing.T) { + testCases := []struct { + name string + duration *dtpb.Duration + }{ + {"Nanoseconds", fhir.Nanoseconds(42)}, + {"Microseconds", fhir.Microseconds(time.Microsecond * 3)}, + {"Milliseconds", fhir.Milliseconds(time.Millisecond * 11)}, + {"Seconds", fhir.Seconds(time.Second * 314)}, + {"Minutes", fhir.Minutes(time.Minute * 8)}, + {"Hours", fhir.Hours(time.Hour * 12)}, + {"Days", fhir.Days(time.Hour * 24)}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + duration, err := fhirconv.DurationToDuration(tc.duration) + if err != nil { + t.Fatalf("DurationToDuration(%v): unexpected error %v", tc.name, err) + } + + element := fhir.Duration(duration) + + got, want := element, tc.duration + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("DurationToDuration(%v): (-got +want):\n%v", tc.name, diff) + } + }) + } +} diff --git a/internal/fhirtest/doc.go b/internal/fhirtest/doc.go new file mode 100644 index 0000000..804e44f --- /dev/null +++ b/internal/fhirtest/doc.go @@ -0,0 +1,12 @@ +/* +Package fhirtest provides resource test dummies and useful utilities to enable +better testing of the R4 FHIR Protos. + +This provides: + - Pseudo-randomized FHIR resource identity generation + - Construction utilities for forming new resources at runtime + - Utilities for emulating Meta updates to FHIR resources + - Resources are organized by their higher-level interface abstractions (e.g. + organized by Resource, DomainResource, etc), and are keyed by resource-name. +*/ +package fhirtest diff --git a/internal/fhirtest/elements.go b/internal/fhirtest/elements.go new file mode 100644 index 0000000..344295d --- /dev/null +++ b/internal/fhirtest/elements.go @@ -0,0 +1,46 @@ +package fhirtest + +import ( + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/protofields" +) + +var ( + // Elements is a map of all element-names to an instance of that element type. + // + // The elements in this map are not guaranteed to contain any specific value; + // it is only guaranteed to contain a non-nil instance of a concrete element + // of the associated name. + Elements map[string]fhir.Element + + // BackboneElements is a map of all backbone element-names to an instance of + // that element type. + // + // The elements in this map are not guaranteed to contain any specific value; + // it is only guaranteed to contain a non-nil instance of a concrete backbone + // element of the associated name. + BackboneElements map[string]fhir.BackboneElement +) + +func init() { + Elements = map[string]fhir.Element{} + BackboneElements = map[string]fhir.BackboneElement{} + + for _, msg := range protofields.Elements { + element, ok := msg.New().(fhir.Element) + + // The proto definition of the XHtml type is missing `GetValue()`, and thus + // fails this check. This is added to avoid errors here that are otherwise + // correct for all other cases. + if !ok { + continue + } + + name := protofields.DescriptorName(element) + + Elements[name] = element + if val, ok := any(element).(fhir.BackboneElement); ok { + BackboneElements[name] = val + } + } +} diff --git a/internal/fhirtest/meta.go b/internal/fhirtest/meta.go new file mode 100644 index 0000000..d05d929 --- /dev/null +++ b/internal/fhirtest/meta.go @@ -0,0 +1,65 @@ +package fhirtest + +import ( + "time" + + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/element/meta" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// TouchMeta touches the meta to generate a new version ID and use now as the +// time. In most cases, this is likely what is required for tests -- since code +// should seldomly rely on what the version-id or update times discretely are, and +// this helps to ensure proper uniqueness just as the real FHIRStore would. +func TouchMeta(resource fhir.Resource) { + UpdateMeta(resource, randomVersionID(), time.Now()) +} + +// UpdateID will update the resource's ID to the specified resourceID string. +func UpdateID(resource fhir.Resource, resourceID string) { + reflect := resource.ProtoReflect() + field := reflect.Descriptor().Fields().ByName("id") + + id := &datatypes_go_proto.Id{ + Value: resourceID, + } + reflect.Set(field, protoreflect.ValueOfMessage(id.ProtoReflect())) +} + +// UpdateMeta updates the meta contents of the fhir resource to use the new +// version-ID and update-time. +func UpdateMeta(resource fhir.Resource, versionID string, updateTime time.Time) { + reflect := resource.ProtoReflect() + metaField := getMetaField(reflect) + time := fhir.Instant(updateTime) + version := fhir.ID(versionID) + + if resource.GetMeta() == nil { + meta := &datatypes_go_proto.Meta{ + LastUpdated: time, + VersionId: version, + } + reflect.Set(metaField, protoreflect.ValueOfMessage(meta.ProtoReflect())) + } + + message := reflect.Get(metaField).Message() + descriptor := message.Descriptor() + fields := descriptor.Fields() + updateField := fields.ByName("last_updated") + versionField := fields.ByName("version_id") + + message.Set(updateField, protoreflect.ValueOfMessage(time.ProtoReflect())) + message.Set(versionField, protoreflect.ValueOfMessage(version.ProtoReflect())) +} + +// NOTE: This method is deprecated and should use the production one in +// "github.com/verily-src/fhirpath-go/internal/element/meta" +func ReplaceMeta(resource fhir.Resource, m *datatypes_go_proto.Meta) { + meta.ReplaceInResource(resource, m) +} + +func getMetaField(reflect protoreflect.Message) protoreflect.FieldDescriptor { + return reflect.Descriptor().Fields().ByName("meta") +} diff --git a/internal/fhirtest/random.go b/internal/fhirtest/random.go new file mode 100644 index 0000000..76f626f --- /dev/null +++ b/internal/fhirtest/random.go @@ -0,0 +1,69 @@ +package fhirtest + +import ( + "fmt" + "time" + + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/stablerand" +) + +// stableRandomID generates a random ID value that will be stable across +// multiple test executions. +func stableRandomID() *datatypes_go_proto.Id { + return &datatypes_go_proto.Id{ + Value: randomID(), + } +} + +// stableRandomVersionID generates a random version-ID value that will be +// stable across multiple test executions. +func stableRandomVersionID() *datatypes_go_proto.Id { + return &datatypes_go_proto.Id{ + Value: randomVersionID(), + } +} + +func stableRandomInstant() *datatypes_go_proto.Instant { + return fhir.Instant(stableRandomTime()) +} + +func StableRandomMeta() *datatypes_go_proto.Meta { + return &datatypes_go_proto.Meta{ + LastUpdated: stableRandomInstant(), + VersionId: stableRandomVersionID(), + } +} + +func randomVersionID() string { + const versionIDLength = 26 + return stablerand.AlnumString(versionIDLength) +} + +// This is a different implementation than fhir.RandomID() since this test +// library manually sets a random seed. +func randomID() string { + uuidBase := stablerand.HexString(32) + return fmt.Sprintf( + "%v-%v-%v-%v-%v", + uuidBase[0:8], + uuidBase[8:12], + uuidBase[12:16], + uuidBase[16:20], + uuidBase[20:], + ) +} + +func stableRandomTime() time.Time { + const ( + // Timestamp for 2020-01-01 T12:00:00 + baseTime int64 = 1577898000 + + // Variation of up to 1 year + timeVariation = time.Hour * 24 * 365 + ) + + base := time.Unix(baseTime, 0) + return stablerand.Time(base, timeVariation) +} diff --git a/internal/fhirtest/resources.go b/internal/fhirtest/resources.go new file mode 100644 index 0000000..b1a0826 --- /dev/null +++ b/internal/fhirtest/resources.go @@ -0,0 +1,298 @@ +package fhirtest + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/protofields" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// Resources is a map of all resource-names to an instance of that resource type. +var Resources map[string]fhir.Resource + +// DomainResources is a map of all domain-resource-names to an instance of that +// domain-resource type. +var DomainResources map[string]fhir.DomainResource + +// CanonicalResources is a map of all canonical-resource-names to an instance of +// that canonical-resource type. +var CanonicalResources map[string]fhir.CanonicalResource + +// MetadataResources is a map of all metadata-resource-names to an instance of +// that metadata-resource type +var MetadataResources map[string]fhir.MetadataResource + +// ResourceOption is an option type acting on resources for setting up a resource. +type ResourceOption func(*testing.T, fhir.Resource) + +// NewResource creates a dummy resource object for the purposes of testing +// of type `resourceType. If `resourceType` does not name a valid R4 FHIR resource, +// this will fail the calling test. +func NewResource(t *testing.T, resourceType resource.Type, opts ...ResourceOption) fhir.Resource { + t.Helper() + + if val, ok := Resources[string(resourceType)]; ok { + object := proto.Clone(val).(fhir.Resource) + ReplaceMeta(object, StableRandomMeta()) + UpdateID(object, stableRandomID().Value) + for _, opt := range opts { + opt(t, object) + } + return object + } + + // This should be unreachable in valid code + t.Fatalf("No resource of type %v found. Please specify a valid resource, or update this package.", resourceType) + return nil +} + +// NewResource creates a dummy resource object for the purposes of testing +// of type `T. If `T` is not a valid FHIR R4 resource, this will fail testing. +func NewResourceOf[T fhir.Resource](t *testing.T, opts ...ResourceOption) T { + t.Helper() + var res T + resourceType := resource.TypeOf(res) + + return NewResource(t, resourceType, opts...).(T) +} + +// NewResourceFromBase returns a copy of a resource modified by given options. The +// input resource remains unmodified. Useful for tweaking an existing resource +// for test inputs. +func NewResourceFromBase(t *testing.T, resource fhir.Resource, opts ...ResourceOption) fhir.Resource { + t.Helper() + + modifiedResource := proto.Clone(resource).(fhir.Resource) + for _, opt := range opts { + opt(t, modifiedResource) + } + + return modifiedResource +} + +// WithResourceModification applies a modifier on a resource. Resource and +// modifier must be the same type. +func WithResourceModification[T fhir.Resource](modifier func(T)) ResourceOption { + return func(t *testing.T, res fhir.Resource) { + t.Helper() + val, ok := res.(T) + if !ok { + var modifierType T + t.Fatalf("Modifier type != applied resource type: (%s) != (%s)", resource.TypeOf(modifierType), resource.TypeOf(res)) + } + modifier(val) + } +} + +// WithJSONField creates a ResourceOpt for setting up the specified proto-field +// with the given value in JSON format. +// +// This will fail tests if the field is invalid, or if the JSON does not +// unmarshal correctly. +func WithJSONField(field, value string) ResourceOption { + return func(t *testing.T, r fhir.Resource) { + t.Helper() + msg := r.ProtoReflect() + field := getProtoField(t, msg, field) + + s := msg.Get(field).Message().New().Interface() + if err := protojson.Unmarshal([]byte(value), s); err != nil { + t.Fatalf("Invalid JSON '%v': %v", value, err) + } + + msg.Set(field, protoreflect.ValueOfMessage(s.ProtoReflect())) + } +} + +// WithCodeField creates a ResourceOpt for setting up the specified proto-field +// with the value of a resource code. +// +// This can be used as a short-hand for constructing arbitrary strongly-typed +// code-fields from a simple string, such as `WithCodeField("status", "DRAFT")` +// for producing the "Draft" state for a CanonicalResource status. +// +// This will fail tests if the field is invalid, or if the JSON does not +// unmarshal correctly. +func WithCodeField(field, value string) ResourceOption { + // All "Code" objects are serialized in JSON as just { "value": "<value>" }; + // using this property enables us to set different codes for the fhir protos + // which otherwise use strong-types (e.g. Questionnaire.Status is a different + // type from Account.Status, but both use the underlying PublicationCode string.) + return WithJSONField(field, fmt.Sprintf(`{ "value": "%v" }`, value)) +} + +// WithProtoField creates a ResourceOpt for setting up the specified proto-field +// with the value set in the proto message. +// +// This will fail tests if the field is invalid, or panic if message is the +// wrong input type. +func WithProtoField(field string, message proto.Message) ResourceOption { + return func(t *testing.T, r fhir.Resource) { + t.Helper() + msg := r.ProtoReflect() + field := getProtoField(t, msg, field) + + msg.Set(field, protoreflect.ValueOfMessage(message.ProtoReflect())) + } +} + +// WithRepeatedProtoField creates a ResourceOpt for setting up the specified +// repeated proto-field with the values set in the supplied proto messages. +// +// This will fail tests if the field is invalid, or panic if message is the +// wrong input type. +func WithRepeatedProtoField(field string, messages ...proto.Message) ResourceOption { + return func(t *testing.T, r fhir.Resource) { + t.Helper() + msg := r.ProtoReflect() + field := getProtoField(t, msg, field) + list := msg.Mutable(field).List() + for _, message := range messages { + list.Append(protoreflect.ValueOfMessage(message.ProtoReflect())) + } + msg.Set(field, protoreflect.ValueOfList(list)) + } +} + +// WithGeneratedIdentifier creates a ResourceOpt for automatically +// generating an Identifier for the given resource with the specified system. +// If the resource implements GetIdentifier(), an Identifier will be generated +// and added. +// If the resource does not, then we will fail the test. +func WithGeneratedIdentifier(system string) ResourceOption { + return func(t *testing.T, r fhir.Resource) { + // set Identifier if available + if cast, ok := r.(resource.HasGetIdentifierList); ok { + ids := []*datatypes_go_proto.Identifier{generateIdentifier(system)} + setIdentifierList(cast, ids) + } else if cast, ok := r.(resource.HasGetIdentifierSingle); ok { + id := generateIdentifier(system) + setIdentifier(cast, id) + } else { + t.Errorf("WithGeneratedIdentifier: invalid resource type %v has no GetIdentifier()", r) + } + } +} + +func init() { + Resources = make(map[string]fhir.Resource, len(protofields.Resources)) + DomainResources = map[string]fhir.DomainResource{} + CanonicalResources = map[string]fhir.CanonicalResource{} + MetadataResources = map[string]fhir.MetadataResource{} + + for name, res := range protofields.Resources { + resource := res.New().(fhir.Resource) + + ReplaceMeta(resource, StableRandomMeta()) + UpdateID(resource, stableRandomID().Value) + + Resources[name] = resource + // The various resource interfaces grow off of one another, so we can + // minimize the number of checks here by leveraging this fact. + if v, ok := resource.(fhir.MetadataResource); ok { + setCanonicalFields(v) + MetadataResources[name] = v + CanonicalResources[name] = v + DomainResources[name] = v + } else if v, ok := resource.(fhir.CanonicalResource); ok { + setCanonicalFields(v) + CanonicalResources[name] = v + DomainResources[name] = v + } else if v, ok := resource.(fhir.DomainResource); ok { + DomainResources[name] = v + } + } +} + +// setCanonicalFields will automatically set canonical fields. +func setCanonicalFields(res fhir.CanonicalResource) { + setCanonicalURL(res, fmt.Sprintf( + "https://example.com/%v/%v", + strings.ToLower(string(resource.TypeOf(res))), + res.GetId().GetValue()), + ) + setCanonicalVersion(res, "1.0") + setCanonicalStatus(res, "DRAFT") +} + +// setResourceProtoField will set the resource's field to the given proto message type. +func setResourceProtoField(resource fhir.Resource, fieldName string, value proto.Message) { + reflect := resource.ProtoReflect() + descriptor := reflect.Descriptor() + field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) + reflect.Set(field, protoreflect.ValueOfMessage(value.ProtoReflect())) +} + +// setCanonicalURL will set a canonical URL for the given resource. +func setCanonicalURL(resource fhir.CanonicalResource, url string) { + setResourceProtoField(resource, "url", &datatypes_go_proto.Uri{ + Value: url, + }) +} + +// setCanonicalVersion will set a canonical version for the given resource. +func setCanonicalVersion(resource fhir.CanonicalResource, version string) { + setResourceProtoField(resource, "version", &datatypes_go_proto.String{ + Value: version, + }) +} + +func setCanonicalStatus(resource fhir.CanonicalResource, status string) { + msg := resource.ProtoReflect() + descriptor := msg.Descriptor() + field := descriptor.Fields().ByName("status") + + s := msg.Get(field).Message().New().Interface() + payload := fmt.Sprintf(`{ "value": "%v" }`, status) + if err := protojson.Unmarshal([]byte(payload), s); err != nil { + // This is tested to not happen, and is only privately called internal to + // the setup of this library. + panic(err) + } + + msg.Set(field, protoreflect.ValueOfMessage(s.ProtoReflect())) +} + +// setIdentifier sets the singleton Identifier on the given resource. +func setIdentifier(resource resource.HasGetIdentifierSingle, identifier *datatypes_go_proto.Identifier) { + setResourceProtoField(resource, "identifier", identifier) +} + +// setIdentifierList sets the Identifier list on the given resource. +func setIdentifierList(resource resource.HasGetIdentifierList, identifiers []*datatypes_go_proto.Identifier) { + msg := resource.ProtoReflect() + descriptor := msg.Descriptor() + field := descriptor.Fields().ByName("identifier") + + list := msg.Mutable(field).List() + for _, identifier := range identifiers { + list.Append(protoreflect.ValueOfMessage(identifier.ProtoReflect())) + } + msg.Set(field, protoreflect.ValueOfList(list)) +} + +// generateIdentifier generates a (stable) random Identifier with the given system +func generateIdentifier(system string) *datatypes_go_proto.Identifier { + return &datatypes_go_proto.Identifier{ + System: &datatypes_go_proto.Uri{Value: system}, + Value: &datatypes_go_proto.String{Value: stableRandomID().Value}, + } +} + +// getProtoField is a helper function for retrieving the named proto field +func getProtoField(t *testing.T, msg protoreflect.Message, fieldName string) protoreflect.FieldDescriptor { + t.Helper() + descriptor := msg.Descriptor() + field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) + if field == nil { + t.Fatalf("Proto field '%v' not found", field) + } + return field +} diff --git a/internal/fhirtest/resources_example_test.go b/internal/fhirtest/resources_example_test.go new file mode 100644 index 0000000..1ac88da --- /dev/null +++ b/internal/fhirtest/resources_example_test.go @@ -0,0 +1,46 @@ +package fhirtest_test + +import ( + "fmt" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirtest" +) + +func ExampleWithResourceModification() { + t := &testing.T{} + + patient := fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.Name = []*dtpb.HumanName{{Family: fhir.String("Ursa")}} + })).(*ppb.Patient) + + fmt.Printf("patient.Name[0].Family = %v", patient.Name[0].Family) + // Output: patient.Name[0].Family = value:"Ursa" +} + +func ExampleNewResourceFromBase() { + t := &testing.T{} + original := &ppb.Patient{ + Id: fhir.ID("uuid-a"), + Name: []*dtpb.HumanName{{Family: fhir.String("Ursa")}}, + } + + // Apply options on original. + modified := fhirtest.NewResourceFromBase(t, original, + fhirtest.WithResourceModification(func(p *ppb.Patient) { + p.Name[0].Family = fhir.String("Major") + p.Name[0].Given = fhir.Strings("Aseem") + }), + fhirtest.WithProtoField("id", fhir.ID("uuid-b")), + ).(*ppb.Patient) + + fmt.Printf("ID = '%v', Family = '%v', Given = '%v'", + modified.Id.Value, + modified.Name[0].Family.Value, + modified.Name[0].Given[0].Value, + ) + // Output: ID = 'uuid-b', Family = 'Major', Given = 'Aseem' +} diff --git a/internal/fhirtest/resources_test.go b/internal/fhirtest/resources_test.go new file mode 100644 index 0000000..88104d8 --- /dev/null +++ b/internal/fhirtest/resources_test.go @@ -0,0 +1,326 @@ +package fhirtest_test + +import ( + "regexp" + "testing" + + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestNewResource_GeneratesIdentityInformation(t *testing.T) { + for name := range fhirtest.Resources { + sut := fhirtest.NewResource(t, resource.Type(name)) + + if sut.GetId().GetValue() == "" { + t.Errorf("NewResource(%v): expected random ID", name) + } + if sut.GetMeta().GetVersionId().GetValue() == "" { + t.Errorf("NewResource(%v): expected random version ID", name) + } + if sut.GetMeta().GetLastUpdated().GetValueUs() == 0 { + t.Errorf("NewResource(%v): expected random last-update time", name) + } + } +} + +func TestNewResourceFromBase_PreservesInput(t *testing.T) { + for name := range fhirtest.Resources { + sut := fhirtest.NewResource(t, resource.Type(name)) + sutDuplicate := proto.Clone(sut).(fhir.Resource) + + fhirtest.NewResourceFromBase(t, sut, + fhirtest.WithProtoField("meta", &dtpb.Meta{Source: fhir.URI("urn:uuid:A")})) + + if diff := cmp.Diff(sut, sutDuplicate, protocmp.Transform()); diff != "" { + t.Errorf("NewResourceFromBase() modified input (-got, +want): %s", diff) + } + } +} + +func TestNewResourceFromBase_AppliesOption(t *testing.T) { + for name := range fhirtest.Resources { + sut := fhirtest.NewResource(t, resource.Type(name)) + newMeta := &dtpb.Meta{Source: fhir.URI("urn:uuid:A")} + wantMsut := proto.Clone(sut).(fhir.Resource) + fhirtest.ReplaceMeta(wantMsut, newMeta) + + gotMsut := fhirtest.NewResourceFromBase(t, sut, + fhirtest.WithProtoField("meta", newMeta)) + + if diff := cmp.Diff(gotMsut, wantMsut, protocmp.Transform()); diff != "" { + t.Errorf("NewResourceFromBase() didn't modify properly (-got, +want): %s", diff) + } + } +} + +func TestNewResourceOf_WithPatient_CreatesPatient(t *testing.T) { + got := fhirtest.NewResourceOf[*patient_go_proto.Patient](t) + + if got == nil { + t.Errorf("NewResourceOf: got nil, want patient") + } +} + +func TestWithResourceModification_ChangesPatientName(t *testing.T) { + pIn := &patient_go_proto.Patient{ + Name: []*dtpb.HumanName{ + { + Family: fhir.String("VonBatchery"), + Given: []*dtpb.String{fhir.String("GivenA"), fhir.String("GivenB")}, + }, + }, + } + pWant := &patient_go_proto.Patient{ + Name: []*dtpb.HumanName{ + { + Family: fhir.String("Rosewater"), + Given: []*dtpb.String{fhir.String("GivenB")}, + }, + }, + } + + fhirtest.WithResourceModification(func(patient *patient_go_proto.Patient) { + patient.Name[0].Family = fhir.String("Rosewater") + patient.Name[0].Given = patient.Name[0].Given[1:] + })(t, pIn) + + if diff := cmp.Diff(pWant, pIn, protocmp.Transform()); diff != "" { + t.Errorf("WithResourceModification didn't produce correct patient (-got, +want): %s", diff) + } +} + +func TestWithFieldJSON_SetsJSONField(t *testing.T) { + for name := range fhirtest.CanonicalResources { + sut := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithJSONField("status", `{"value": "DRAFT"}`), + ) + + // 'NewResource' would fail the test implicitly if this didn't work correctly; + // so if we make it here, it means we have passed. + // Since the "status" field is a different type for each resource, we can't + // generically test this value over an iterative sequence. + _ = sut + } +} + +func TestWithFieldJSON_SetsAccountStatusField(t *testing.T) { + sut := fhirtest.NewResource(t, "Questionnaire", + fhirtest.WithJSONField("status", `{"value": "DRAFT"}`), + ).(*questionnaire_go_proto.Questionnaire) + + if got, want := sut.GetStatus().GetValue(), codes_go_proto.PublicationStatusCode_DRAFT; got != want { + t.Errorf("WithFieldJSON: got code %v, want code %v", got, want) + } +} + +func TestWithFieldCodeJSON_SetsJSONField(t *testing.T) { + for name := range fhirtest.CanonicalResources { + sut := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithCodeField("status", "DRAFT"), + ) + + // 'NewResource' would fail the test implicitly if this didn't work correctly; + // so if we make it here, it means we have passed. + // Since the "status" field is a different type for each resource, we can't + // generically test this value over an iterative sequence. + _ = sut + } +} + +func TestWithFieldCodeJSON_SetsAccountStatusField(t *testing.T) { + sut := fhirtest.NewResource(t, "Questionnaire", + fhirtest.WithCodeField("status", "DRAFT"), + ).(*questionnaire_go_proto.Questionnaire) + + if got, want := sut.GetStatus().GetValue(), codes_go_proto.PublicationStatusCode_DRAFT; got != want { + t.Errorf("WithFieldCodeJSON: got code %v, want code %v", got, want) + } +} + +func TestWithFieldProto_SetsField(t *testing.T) { + want := &dtpb.String{ + Value: "test", + } + for name := range fhirtest.CanonicalResources { + sut := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithProtoField("name", want), + ).(fhir.CanonicalResource) + + if got := sut.GetName(); got != want { + t.Errorf("WithFieldProto(%v): got '%v', wanted '%v'", name, got, want) + } + } +} + +func TestWithRepeatedFieldProto_SetsField(t *testing.T) { + toProtoMessage := func(in []*dtpb.Identifier) []proto.Message { + result := make([]proto.Message, 0, len(in)) + for _, v := range in { + result = append(result, v) + } + return result + } + + want := []proto.Message{ + &dtpb.Identifier{ + Value: &dtpb.String{ + Value: "foo", + }, + System: &dtpb.Uri{ + Value: "https://example.com/foo", + }, + }, + &dtpb.Identifier{ + Value: &dtpb.String{ + Value: "bar", + }, + System: &dtpb.Uri{ + Value: "https://example.com/bar", + }, + }, + } + for name := range fhirtest.CanonicalResources { + sut := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithRepeatedProtoField("identifier", want...), + ).(fhir.CanonicalResource) + + if got := sut.GetIdentifier(); !cmp.Equal(toProtoMessage(got), want, protocmp.Transform()) { + t.Errorf("WithRepeatedFieldProto(%v): got '%v', wanted '%v'", name, got, want) + } + } +} + +// Assert that an identifier list is present and has expected values +func assertIdentifier(t *testing.T, identifiers []*dtpb.Identifier, name string, resource fhir.Resource) { + if identifiers == nil { + t.Errorf("Nil list of ids for %v: %v", name, resource) + return + } + if len(identifiers) == 0 { + t.Errorf("Empty list of ids for %v: %v", name, resource) + return + } + + id := identifiers[0] + + if got, want := id.GetSystem().GetValue(), "http://example.com/fake-id"; got != want { + t.Errorf("%v Resource.Identifier[0].System: got %v, want %v", name, got, want) + } + + value := id.GetValue().GetValue() + matched, _ := regexp.MatchString("^[a-f0-9-]+$", value) + if !matched { + t.Errorf("%v Resource.Identifier[0].Value: got %v, expected uuid", name, value) + } +} + +// All CanonicalResources should implement GetIdentifier() +func TestWithGeneratedIdentifier_CanonicalResources(t *testing.T) { + for name := range fhirtest.CanonicalResources { + t.Run(name, func(t *testing.T) { + res := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithGeneratedIdentifier("http://example.com/fake-id"), + ).(fhir.CanonicalResource) + + ids := res.GetIdentifier() + assertIdentifier(t, ids, name, res) + }) + } +} + +// Test that all resources can either be cast or not +func TestWithGeneratedIdentifier_AllResources(t *testing.T) { + for name, stockRes := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + if _, ok := stockRes.(resource.HasGetIdentifierList); ok { + // GetIdentifier() is list + + res := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithGeneratedIdentifier("http://example.com/fake-id"), + ).(resource.HasGetIdentifierList) + + ids := res.GetIdentifier() + assertIdentifier(t, ids, name, res) + + } else if _, ok := stockRes.(resource.HasGetIdentifierSingle); ok { + // GetIdentifier() is singleton + + res := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithGeneratedIdentifier("http://example.com/fake-id"), + ).(resource.HasGetIdentifierSingle) + + id := res.GetIdentifier() + assertIdentifier(t, []*dtpb.Identifier{id}, name, res) + } else { + // not compatible with GetIdentifier() interface + return + } + }) + } +} + +// Test a few specific types +func TestWithGeneratedIdentifier_SpotTest(t *testing.T) { + // resources w/ Identifier as list + for _, name := range []string{"Patient", "DocumentReference"} { + t.Run(name, func(t *testing.T) { + res := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithGeneratedIdentifier("http://example.com/fake-id"), + ).(resource.HasGetIdentifierList) + + ids := res.GetIdentifier() + assertIdentifier(t, ids, name, res) + }) + } + + // resources w/ Identifier as singleton + for _, name := range []string{"Bundle", "QuestionnaireResponse"} { + t.Run(name, func(t *testing.T) { + res := fhirtest.NewResource(t, resource.Type(name), + fhirtest.WithGeneratedIdentifier("http://example.com/fake-id"), + ).(resource.HasGetIdentifierSingle) + + id := res.GetIdentifier() + assertIdentifier(t, []*dtpb.Identifier{id}, name, res) + }) + } +} + +func TestResources_ResourceHasNonEmptyID(t *testing.T) { + for name, resource := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + if resource.GetId().GetValue() == "" { + t.Errorf("Resource(%v): got empty id", name) + } + }) + } +} + +func TestResources_ResourceHasNonEmptyVersionID(t *testing.T) { + for name, resource := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + if resource.GetMeta().GetVersionId().GetValue() == "" { + t.Errorf("Resource(%v): got empty version id", name) + } + }) + } +} + +func TestResources_ResourceHasLastModified(t *testing.T) { + for name, resource := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + if resource.GetMeta().GetLastUpdated().GetValueUs() == 0 { + t.Errorf("Resource(%v): got unset last-update time", name) + } + }) + } +} diff --git a/internal/narrow/narrow.go b/internal/narrow/narrow.go new file mode 100644 index 0000000..c0d5037 --- /dev/null +++ b/internal/narrow/narrow.go @@ -0,0 +1,176 @@ +/* +Package narrow provides conversion functionality for narrowing +integer types in a safe and generic manner. + +These operations will return not only the casted integer, but also a boolean +indicating whether the value was safely casted to the receiver type -- e.g. +that the sign didn't change through casting, and that the value was not unexpectedly +truncated. This can be really useful for cases where data requires explicit +integer-widths, and the incoming data may exceed that integer width. + +Conversion is done through one of the two following function types: + + - `narrow.ToInteger[To](from) (To, bool)` -- converts to the specified int-type + - `narrow.To*(from) (*, bool)` -- convenience functions for converting + to explicit integer-width types to avoid needing generics (just calls + into the above). + +The former's generic-interface allows better composability with other generic +functions, whereas the latter allows a more idiomatic and visible call for +explicit cases (e.g. `ints.ToInt32(...)`) +*/ +package narrow + +import ( + "math" + + "golang.org/x/exp/constraints" +) + +// isSigned checks that the integer input is a Signed integer type. +func isSigned[T constraints.Integer]() bool { + // If T is unsigned, -1 will form a large positive number when casted to + // T -- which makes this a simple way to check for signedness. + val := -1 + return T(val) < 0 +} + +// ToInteger converts `From` to a `To` type, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToInteger[To constraints.Integer, From constraints.Integer](from From) (To, bool) { + if isSigned[From]() { + return To(from), canSafelyNarrowSigned[To](int64(from)) + } + return To(from), canSafelyNarrowUnsigned[To](uint64(from)) +} + +// ToInt converts an integer type into an `int`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToInt[From constraints.Integer](from From) (int, bool) { + return ToInteger[int](from) +} + +// ToInt8 converts an integer type into an `int8`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToInt8[From constraints.Integer](from From) (int8, bool) { + return ToInteger[int8](from) +} + +// ToInt16 converts an integer type into an `int16`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToInt16[From constraints.Integer](from From) (int16, bool) { + return ToInteger[int16](from) +} + +// ToInt32 converts an integer type into an `int32`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToInt32[From constraints.Integer](from From) (int32, bool) { + return ToInteger[int32](from) +} + +// ToInt64 converts an integer type into an `int64`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToInt64[From constraints.Integer](from From) (int64, bool) { + return ToInteger[int64](from) +} + +// ToUint converts an integer type into an `uint`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToUint[From constraints.Integer](from From) (uint, bool) { + return ToInteger[uint](from) +} + +// ToUint8 converts an integer type into an `uint8`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToUint8[From constraints.Integer](from From) (uint8, bool) { + return ToInteger[uint8](from) +} + +// ToUint16 converts an integer type into an `uint16`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToUint16[From constraints.Integer](from From) (uint16, bool) { + return ToInteger[uint16](from) +} + +// ToUint32 converts an integer type into an `uint32`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToUint32[From constraints.Integer](from From) (uint32, bool) { + return ToInteger[uint32](from) +} + +// ToUint64 converts an integer type into an `uint64`, additionally returning whether the +// value was converted successfully without any truncation and loss-of-data occurring. +func ToUint64[From constraints.Integer](from From) (uint64, bool) { + return ToInteger[uint64](from) +} + +// ToUintptr converts an integer type into an `uintptr`, as well as whether the value was +// truncated. +func ToUintptr[From constraints.Integer](from From) (uintptr, bool) { + return ToInteger[uintptr](from) +} + +// canSafelyNarrowSigned tests whether the supplied val can be converted into 'To' +// without truncation or loss-of-data. +func canSafelyNarrowSigned[To constraints.Integer](val int64) bool { + var v To + switch any(v).(type) { + case uint: + // Cast here is to avoid possible "overflow" errors if uint is 64-bits + return uint64(val) <= math.MaxUint && val >= 0 + case uintptr: + return int64(uintptr(val)) == val && val >= 0 + case uint8: + return val <= math.MaxUint8 && val >= 0 + case uint16: + return val <= math.MaxUint16 && val >= 0 + case uint32: + return val <= math.MaxUint32 && val >= 0 + case uint64: + return val >= 0 + case int: + return val <= math.MaxInt && val >= math.MinInt + case int8: + return val <= math.MaxInt8 && val >= math.MinInt8 + case int16: + return val <= math.MaxInt16 && val >= math.MinInt16 + case int32: + return val <= math.MaxInt32 && val >= math.MinInt32 + case int64: + return true // trivially true (identity conversion) + } + return false // this should be unreachable +} + +// canSafelyNarrowUnsigned tests whether the supplied val can be converted into 'To' +// without truncation or loss-of-data. +func canSafelyNarrowUnsigned[To constraints.Integer](val uint64) bool { + var v To + switch any(v).(type) { + case uint: + // Cast here is to avoid possible "overflow" errors if uint is 64-bits + return uint64(val) <= math.MaxUint + case uintptr: + return uint64(uintptr(val)) == val + case uint8: + return val <= math.MaxUint8 + case uint16: + return val <= math.MaxUint16 + case uint32: + return val <= math.MaxUint32 + case uint64: + return true // trivially true (identity conversion) + case int: + // Cast here is to avoid possible "overflow" errors if int is 64-bits + return uint64(val) <= math.MaxInt + case int8: + return val <= math.MaxInt8 + case int16: + return val <= math.MaxInt16 + case int32: + return val <= math.MaxInt32 + case int64: + return val <= math.MaxInt64 + } + return false // this should be unreachable +} diff --git a/internal/narrow/narrow_example_test.go b/internal/narrow/narrow_example_test.go new file mode 100644 index 0000000..8b767fd --- /dev/null +++ b/internal/narrow/narrow_example_test.go @@ -0,0 +1,268 @@ +package narrow_test + +import ( + "fmt" + "math" + + "github.com/verily-src/fhirpath-go/internal/narrow" +) + +func ExampleToInteger_narrows() { + from := -1 + + val, ok := narrow.ToInteger[uint8](from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to uint8! +} + +func ExampleToInteger_no_narrowing() { + from := uint32(42) + + val, ok := narrow.ToInteger[uint8](from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to uint8! +} + +func ExampleToInt8_narrows() { + from := 10_000 + + val, ok := narrow.ToInt8(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to int8! +} + +func ExampleToInt8_no_narrowing() { + from := 42 + + val, ok := narrow.ToInt8(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to int8! +} + +func ExampleToUint8_narrows() { + from := 10_000 + + val, ok := narrow.ToUint8(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to uint8! +} + +func ExampleToUint8_no_narrowing() { + from := 42 + + val, ok := narrow.ToUint8(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to uint8! +} + +func ExampleToInt16_narrows() { + from := math.MaxInt32 + + val, ok := narrow.ToInt16(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to int16! +} + +func ExampleToInt16_no_narrowing() { + from := 42 + + val, ok := narrow.ToInt16(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to int16! +} + +func ExampleToUint16_narrows() { + from := math.MaxInt32 + + val, ok := narrow.ToUint16(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to uint16! +} + +func ExampleToUint16_no_narrowing() { + from := 42 + + val, ok := narrow.ToUint16(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to uint16! +} + +func ExampleToInt32_narrows() { + from := math.MaxInt64 + + val, ok := narrow.ToInt32(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to int32! +} + +func ExampleToInt32_no_narrowing() { + from := 42 + + val, ok := narrow.ToInt32(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to int32! +} + +func ExampleToUint32_narrows() { + from := math.MaxInt64 + + val, ok := narrow.ToUint32(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to uint32! +} + +func ExampleToUint32_no_narrowing() { + from := 42 + + val, ok := narrow.ToUint32(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to uint32! +} + +func ExampleToInt64_no_narrowing() { + from := 42 + + val, ok := narrow.ToInt64(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to int64! +} + +func ExampleToUint64_narrows() { + from := -1 + + val, ok := narrow.ToUint64(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to uint64! +} + +func ExampleToUint64_no_narrowing() { + from := 42 + + val, ok := narrow.ToUint64(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to uint64! +} + +func ExampleToInt_no_narrowing() { + from := 42 + + val, ok := narrow.ToInt32(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to int32! +} + +func ExampleToUint_narrows() { + from := -1 + + val, ok := narrow.ToUint(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Unable to convert to uint! +} + +func ExampleToUint_no_narrowing() { + from := 42 + + val, ok := narrow.ToUint(from) + if !ok { + fmt.Printf("Unable to convert to %T!", val) + } else { + fmt.Printf("Converted %v to %T!", from, val) + } + // Output: + // Converted 42 to uint! +} diff --git a/internal/narrow/narrow_test.go b/internal/narrow/narrow_test.go new file mode 100644 index 0000000..63f729f --- /dev/null +++ b/internal/narrow/narrow_test.go @@ -0,0 +1,703 @@ +package narrow_test + +import ( + "math" + "testing" + + "github.com/verily-src/fhirpath-go/internal/narrow" + "golang.org/x/exp/constraints" +) + +func wantTruncation[To constraints.Integer, From constraints.Integer](input From) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + var to To + + _, ok := narrow.ToInteger[To](input) + + if got, want := ok, false; got != want { + t.Errorf("ToInteger[%T]: got %v, want %v", to, got, want) + } + } +} + +func wantConversion[To constraints.Integer, From constraints.Integer](input From) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + var to To + + got, ok := narrow.ToInteger[To](input) + if ok == false { + t.Fatalf("ToInteger[%T]: unexpected truncation %v => %v", to, input, got) + } + + if got, want := From(got), input; got != want { + t.Errorf("ToInteger[%T]: got %v, want %v", to, got, want) + } + } +} + +func wantConversionFunc[To constraints.Integer, From constraints.Integer](fn func(From) (To, bool), from From) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + var to To + + got, ok := fn(from) + if ok == false { + t.Fatalf("%T => %T: unexpected truncation %v => %v", from, to, from, got) + } + + if got, want := From(got), from; got != want { + t.Errorf("%T => %T: got %v, want %v", from, to, got, want) + } + } +} + +func TestToInteger_ValueExceedsReceiver_ReturnsFalse(t *testing.T) { + // Note: nested sub-tests are being done both for organization and test naming. + // We also can't use table-driven tests here, since the input is a type -- so + // this forces duplication. + t.Run("FromUint8", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](uint8(math.MaxUint8))) + }) + + t.Run("FromUint16", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](uint16(math.MaxUint16))) + t.Run("ToInt16", wantTruncation[int16](uint16(math.MaxUint16))) + t.Run("ToUint8", wantTruncation[uint8](uint16(math.MaxUint16))) + }) + + t.Run("FromUint32", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](uint32(math.MaxUint32))) + t.Run("ToInt16", wantTruncation[int16](uint32(math.MaxUint32))) + t.Run("ToInt32", wantTruncation[int32](uint32(math.MaxUint32))) + + t.Run("ToUint8", wantTruncation[uint8](uint32(math.MaxUint32))) + t.Run("ToUint16", wantTruncation[uint16](uint32(math.MaxUint32))) + }) + + t.Run("FromUint64", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](uint64(math.MaxUint64))) + t.Run("ToInt16", wantTruncation[int16](uint64(math.MaxUint64))) + t.Run("ToInt32", wantTruncation[int32](uint64(math.MaxUint64))) + t.Run("ToInt64", wantTruncation[int64](uint64(math.MaxUint64))) + t.Run("ToInt", wantTruncation[int](uint64(math.MaxUint64))) + + t.Run("ToUint8", wantTruncation[uint8](uint64(math.MaxUint64))) + t.Run("ToUint16", wantTruncation[uint16](uint64(math.MaxUint64))) + t.Run("ToUint32", wantTruncation[uint32](uint64(math.MaxUint64))) + }) + + t.Run("FromUint", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](uint(math.MaxUint))) + t.Run("ToInt16", wantTruncation[int16](uint(math.MaxUint))) + t.Run("ToInt32", wantTruncation[int32](uint(math.MaxUint))) + + t.Run("ToUint8", wantTruncation[uint8](uint(math.MaxUint))) + t.Run("ToUint16", wantTruncation[uint16](uint(math.MaxUint))) + // We can't reliably test against uint32, since uint may be AT LEAST 32 bits. + }) + + t.Run("FromInt8", func(t *testing.T) { + t.Run("ToUint8", wantTruncation[uint8](int8(-1))) + t.Run("ToUint16", wantTruncation[uint16](int8(-1))) + t.Run("ToUint32", wantTruncation[uint32](int8(-1))) + t.Run("ToUint32", wantTruncation[uint64](int8(-1))) + t.Run("ToUintptr", wantTruncation[uintptr](int8(-1))) + t.Run("ToUint", wantTruncation[uint](int8(-1))) + }) + + t.Run("FromInt16", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](int16(math.MaxInt16))) + + t.Run("ToUint8", wantTruncation[uint8](int16(-1))) + t.Run("ToUint16", wantTruncation[uint16](int16(-1))) + t.Run("ToUint32", wantTruncation[uint32](int16(-1))) + t.Run("ToUint32", wantTruncation[uint64](int16(-1))) + t.Run("ToUintptr", wantTruncation[uintptr](int16(-1))) + t.Run("ToUint", wantTruncation[uint](int16(-1))) + }) + + t.Run("FromInt32", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](int32(math.MaxInt32))) + t.Run("ToInt16", wantTruncation[int16](int32(math.MaxInt32))) + + t.Run("ToUint8", wantTruncation[uint8](int32(-1))) + t.Run("ToUint16", wantTruncation[uint16](int32(-1))) + t.Run("ToUint32", wantTruncation[uint32](int32(-1))) + t.Run("ToUint32", wantTruncation[uint64](int32(-1))) + t.Run("ToUintptr", wantTruncation[uintptr](int32(-1))) + t.Run("ToUint", wantTruncation[uint](int32(-1))) + }) + + t.Run("FromInt64", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](int64(math.MaxInt64))) + t.Run("ToInt16", wantTruncation[int16](int64(math.MaxInt64))) + t.Run("ToInt32", wantTruncation[int32](int64(math.MaxInt64))) + + t.Run("ToUint8", wantTruncation[uint8](int64(-1))) + t.Run("ToUint16", wantTruncation[uint16](int64(-1))) + t.Run("ToUint32", wantTruncation[uint32](int64(-1))) + t.Run("ToUint32", wantTruncation[uint64](int64(-1))) + t.Run("ToUintptr", wantTruncation[uintptr](int64(-1))) + t.Run("ToUint", wantTruncation[uint](int64(-1))) + }) + + t.Run("FromInt", func(t *testing.T) { + t.Run("ToInt8", wantTruncation[int8](int(math.MaxInt))) + t.Run("ToInt16", wantTruncation[int16](int(math.MaxInt))) + // We can't reliably test against int32, since int is at LEAST 32 bits. + + t.Run("ToUint8", wantTruncation[uint8](int(-1))) + t.Run("ToUint16", wantTruncation[uint16](int(-1))) + t.Run("ToUint32", wantTruncation[uint32](int(-1))) + t.Run("ToUint32", wantTruncation[uint64](int(-1))) + t.Run("ToUintptr", wantTruncation[uintptr](int(-1))) + t.Run("ToUint", wantTruncation[uint](int(-1))) + }) +} + +func TestToInteger_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + // Note: nested sub-tests are being done both for organization and test naming. + // We also can't use table-driven tests here, since the input is a type -- so + // this forces duplication. + t.Run("FromUint", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](uint(0))) + t.Run("ToInt8Max", wantConversion[int8](uint(math.MaxInt8))) + t.Run("ToInt16", wantConversion[int16](uint(0))) + t.Run("ToInt16Max", wantConversion[int16](uint(math.MaxInt16))) + t.Run("ToInt32", wantConversion[int32](uint(0))) + t.Run("ToInt32Max", wantConversion[int32](uint(math.MaxInt32))) + t.Run("ToInt64", wantConversion[int64](uint(0))) + t.Run("ToInt64MaxUint32", wantConversion[int64](uint(math.MaxUint32))) + t.Run("ToInt", wantConversion[int](uint(0))) + t.Run("ToIntMaxInt32", wantConversion[int](uint(math.MaxInt32))) + t.Run("ToUint8", wantConversion[uint8](uint(0))) + t.Run("ToUint8Max", wantConversion[uint8](uint(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](uint(0))) + t.Run("ToUint16Max", wantConversion[uint16](uint(math.MaxUint16))) + t.Run("ToUint32", wantConversion[uint32](uint(0))) + t.Run("ToUint32Max", wantConversion[uint32](uint(math.MaxUint32))) + t.Run("ToUint64", wantConversion[uint64](uint(0))) + t.Run("ToUint64MaxUint32", wantConversion[uint64](uint(math.MaxUint32))) + t.Run("ToUint", wantConversion[uint](uint(0))) + t.Run("ToUintMaxUint32", wantConversion[uint](uint(math.MaxUint32))) + t.Run("ToUintptr", wantConversion[uintptr](uint(0))) + }) + + t.Run("FromUint8", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](uint8(0))) + t.Run("ToInt8Max", wantConversion[int8](uint8(math.MaxInt8))) + t.Run("ToInt16", wantConversion[int16](uint8(0))) + t.Run("ToInt16MaxInt8", wantConversion[int16](uint8(math.MaxInt8))) + t.Run("ToInt32", wantConversion[int32](uint8(0))) + t.Run("ToInt32MaxUint8", wantConversion[int32](uint8(math.MaxUint8))) + t.Run("ToInt64", wantConversion[int64](uint8(0))) + t.Run("ToInt64MaxUint8", wantConversion[int64](uint8(math.MaxUint8))) + t.Run("ToInt", wantConversion[int](uint8(0))) + t.Run("ToIntMaxInt8", wantConversion[int](uint8(math.MaxInt8))) + t.Run("ToUint8", wantConversion[uint8](uint8(0))) + t.Run("ToUint8Max", wantConversion[uint8](uint8(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](uint8(0))) + t.Run("ToUint16MaxInt8", wantConversion[uint16](uint8(math.MaxUint8))) + t.Run("ToUint32", wantConversion[uint32](uint8(0))) + t.Run("ToUint32MaxUint8", wantConversion[uint32](uint8(math.MaxUint8))) + t.Run("ToUint64", wantConversion[uint64](uint8(0))) + t.Run("ToUint64MaxUint8", wantConversion[uint64](uint8(math.MaxUint8))) + t.Run("ToUint", wantConversion[uint](uint8(0))) + t.Run("ToUintMaxUint8", wantConversion[uint](uint8(math.MaxUint8))) + t.Run("ToUintptr", wantConversion[uintptr](uint8(0))) + }) + + t.Run("FromUint16", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](uint16(0))) + t.Run("ToInt8Max", wantConversion[int8](uint16(math.MaxInt8))) + t.Run("ToInt16", wantConversion[int16](uint16(0))) + t.Run("ToInt16Max", wantConversion[int16](uint16(math.MaxInt16))) + t.Run("ToInt32", wantConversion[int32](uint16(0))) + t.Run("ToInt32MaxUint16", wantConversion[int32](uint16(math.MaxInt16))) + t.Run("ToInt64", wantConversion[int64](uint16(0))) + t.Run("ToInt64MaxUint16", wantConversion[int64](uint16(math.MaxUint16))) + t.Run("ToInt", wantConversion[int](uint16(0))) + t.Run("ToIntMaxInt16", wantConversion[int](uint16(math.MaxInt16))) + t.Run("ToUint8", wantConversion[uint8](uint16(0))) + t.Run("ToUint8Max", wantConversion[uint8](uint16(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](uint16(0))) + t.Run("ToUint16Max", wantConversion[uint16](uint16(math.MaxUint16))) + t.Run("ToUint32", wantConversion[uint32](uint16(0))) + t.Run("ToUint32MaxUint16", wantConversion[uint32](uint16(math.MaxUint16))) + t.Run("ToUint64", wantConversion[uint64](uint16(0))) + t.Run("ToUint64MaxUint16", wantConversion[uint64](uint16(math.MaxUint16))) + t.Run("ToUint", wantConversion[uint](uint16(0))) + t.Run("ToUintMaxUint16", wantConversion[uint](uint16(math.MaxUint16))) + t.Run("ToUintptr", wantConversion[uintptr](uint16(0))) + }) + + t.Run("FromUint32", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](uint32(0))) + t.Run("ToInt8Max", wantConversion[int8](uint32(math.MaxInt8))) + t.Run("ToInt16", wantConversion[int16](uint32(0))) + t.Run("ToInt16Max", wantConversion[int16](uint32(math.MaxInt16))) + t.Run("ToInt32", wantConversion[int32](uint32(0))) + t.Run("ToInt32Max", wantConversion[int32](uint32(math.MaxInt32))) + t.Run("ToInt64", wantConversion[int64](uint32(0))) + t.Run("ToInt64MaxUint32", wantConversion[int64](uint32(math.MaxUint32))) + t.Run("ToInt", wantConversion[int](uint32(0))) + t.Run("ToIntMaxInt32", wantConversion[int](uint32(math.MaxInt32))) + t.Run("ToUint8", wantConversion[uint8](uint32(0))) + t.Run("ToUint8Max", wantConversion[uint8](uint32(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](uint32(0))) + t.Run("ToUint16Max", wantConversion[uint16](uint32(math.MaxUint16))) + t.Run("ToUint32", wantConversion[uint32](uint32(0))) + t.Run("ToUint32Max", wantConversion[uint32](uint32(math.MaxUint32))) + t.Run("ToUint64", wantConversion[uint64](uint32(0))) + t.Run("ToUint64MaxUint32", wantConversion[uint64](uint32(math.MaxUint32))) + t.Run("ToUint", wantConversion[uint](uint32(0))) + t.Run("ToUintMaxUint32", wantConversion[uint](uint32(math.MaxUint32))) + t.Run("ToUintptr", wantConversion[uintptr](uint32(0))) + }) + + t.Run("FromUint64", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](uint64(0))) + t.Run("ToInt8Max", wantConversion[int8](uint64(math.MaxInt8))) + t.Run("ToInt16", wantConversion[int16](uint64(0))) + t.Run("ToInt16Max", wantConversion[int16](uint64(math.MaxInt16))) + t.Run("ToInt32", wantConversion[int32](uint64(0))) + t.Run("ToInt32Max", wantConversion[int32](uint64(math.MaxInt32))) + t.Run("ToInt64", wantConversion[int64](uint64(0))) + t.Run("ToInt64Max", wantConversion[int64](uint64(math.MaxInt64))) + t.Run("ToInt", wantConversion[int](uint64(0))) + t.Run("ToIntMax", wantConversion[int](uint64(math.MaxInt64))) + t.Run("ToUint8", wantConversion[uint8](uint64(0))) + t.Run("ToUint8Max", wantConversion[uint8](uint64(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](uint64(0))) + t.Run("ToUint16Max", wantConversion[uint16](uint64(math.MaxUint16))) + t.Run("ToUint32", wantConversion[uint32](uint64(0))) + t.Run("ToUint32Max", wantConversion[uint32](uint64(math.MaxUint32))) + t.Run("ToUint64", wantConversion[uint64](uint64(0))) + t.Run("ToUint64Max", wantConversion[uint64](uint64(math.MaxUint64))) + t.Run("ToUint", wantConversion[uint](uint64(0))) + t.Run("ToUintMax", wantConversion[uint](uint64(math.MaxUint64))) + t.Run("ToUintptr", wantConversion[uintptr](uint64(0))) + }) + + t.Run("FromInt", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](int(0))) + t.Run("ToInt8Max", wantConversion[int8](int(math.MaxInt8))) + t.Run("ToInt8Min", wantConversion[int8](int(math.MinInt8))) + t.Run("ToInt16", wantConversion[int16](int(0))) + t.Run("ToInt16Max", wantConversion[int16](int(math.MaxInt16))) + t.Run("ToInt16Min", wantConversion[int16](int(math.MinInt16))) + t.Run("ToInt32", wantConversion[int32](int(0))) + t.Run("ToInt32Max", wantConversion[int32](int(math.MaxInt32))) + t.Run("ToInt32Min", wantConversion[int32](int(math.MinInt32))) + t.Run("ToInt64", wantConversion[int64](int(0))) + t.Run("ToInt64MaxInt32", wantConversion[int64](int(math.MaxInt32))) + t.Run("ToInt64MinInt32", wantConversion[int64](int(math.MinInt32))) + t.Run("ToInt", wantConversion[int](int(0))) + t.Run("ToIntMaxInt32", wantConversion[int](int(math.MaxInt32))) + t.Run("ToIntMinInt32", wantConversion[int](int(math.MinInt32))) + t.Run("ToUint8", wantConversion[uint8](int(0))) + t.Run("ToUint8Max", wantConversion[uint8](int(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](int(0))) + t.Run("ToUint16Max", wantConversion[uint16](int(math.MaxUint16))) + t.Run("ToUint32", wantConversion[uint32](int(0))) + t.Run("ToUint32MaxInt32", wantConversion[uint32](int(math.MaxInt32))) + t.Run("ToUint64", wantConversion[uint64](int(0))) + t.Run("ToUint64MaxInt32", wantConversion[uint64](int(math.MaxInt32))) + t.Run("ToUint", wantConversion[uint](int(0))) + t.Run("ToUintMaxInt32", wantConversion[uint](int(math.MaxInt32))) + t.Run("ToUintptr", wantConversion[uintptr](int(0))) + }) + + t.Run("FromInt8", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](int8(0))) + t.Run("ToInt8Max", wantConversion[int8](int8(math.MaxInt8))) + t.Run("ToInt8Min", wantConversion[int8](int8(math.MinInt8))) + t.Run("ToInt16", wantConversion[int16](int8(0))) + t.Run("ToInt16MaxInt8", wantConversion[int16](int8(math.MaxInt8))) + t.Run("ToInt16MinInt8", wantConversion[int16](int8(math.MinInt8))) + t.Run("ToInt32", wantConversion[int32](int8(0))) + t.Run("ToInt32MaxInt8", wantConversion[int32](int8(math.MaxInt8))) + t.Run("ToInt32MinInt8", wantConversion[int32](int8(math.MinInt8))) + t.Run("ToInt64", wantConversion[int64](int8(0))) + t.Run("ToInt64MaxInt8", wantConversion[int64](int8(math.MaxInt8))) + t.Run("ToInt64MinInt8", wantConversion[int64](int8(math.MinInt8))) + t.Run("ToInt", wantConversion[int](int8(0))) + t.Run("ToIntMaxInt8", wantConversion[int](int8(math.MaxInt8))) + t.Run("ToIntMinInt8", wantConversion[int](int8(math.MinInt8))) + t.Run("ToUint8", wantConversion[uint8](int8(0))) + t.Run("ToUint8MaxInt8", wantConversion[uint8](int8(math.MaxInt8))) + t.Run("ToUint16", wantConversion[uint16](int8(0))) + t.Run("ToUint16MaxInt8", wantConversion[uint16](int8(math.MaxInt8))) + t.Run("ToUint32", wantConversion[uint32](int8(0))) + t.Run("ToUint32MaxInt9", wantConversion[uint32](int8(math.MaxInt8))) + t.Run("ToUint64", wantConversion[uint64](int8(0))) + t.Run("ToUint64MaxInt8", wantConversion[uint64](int8(math.MaxInt8))) + t.Run("ToUint", wantConversion[uint](int8(0))) + t.Run("ToUintMaxInt8", wantConversion[uint](int8(math.MaxInt8))) + t.Run("ToUintptr", wantConversion[uintptr](int8(0))) + }) + + t.Run("FromInt16", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](int16(0))) + t.Run("ToInt8Max", wantConversion[int8](int16(math.MaxInt8))) + t.Run("ToInt8Min", wantConversion[int8](int16(math.MinInt8))) + t.Run("ToInt16", wantConversion[int16](int16(0))) + t.Run("ToInt16Max", wantConversion[int16](int16(math.MaxInt16))) + t.Run("ToInt16Min", wantConversion[int16](int16(math.MinInt16))) + t.Run("ToInt32", wantConversion[int32](int16(0))) + t.Run("ToInt32MaxInt16", wantConversion[int32](int16(math.MaxInt16))) + t.Run("ToInt32MinInt16", wantConversion[int32](int16(math.MinInt16))) + t.Run("ToInt64", wantConversion[int64](int16(0))) + t.Run("ToInt64MaxInt16", wantConversion[int64](int16(math.MaxInt16))) + t.Run("ToInt64MinInt16", wantConversion[int64](int16(math.MinInt16))) + t.Run("ToInt", wantConversion[int](int16(0))) + t.Run("ToIntMaxInt16", wantConversion[int](int16(math.MaxInt16))) + t.Run("ToIntMinInt16", wantConversion[int](int16(math.MinInt16))) + t.Run("ToUint8", wantConversion[uint8](int16(0))) + t.Run("ToUint8Max", wantConversion[uint8](int16(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](int16(0))) + t.Run("ToUint16MaxInt16", wantConversion[uint16](int16(math.MaxInt16))) + t.Run("ToUint32", wantConversion[uint32](int16(0))) + t.Run("ToUint32MaxInt16", wantConversion[uint32](int16(math.MaxInt16))) + t.Run("ToUint64", wantConversion[uint64](int16(0))) + t.Run("ToUint64MaxInt16", wantConversion[uint64](int16(math.MaxInt16))) + t.Run("ToUint", wantConversion[uint](int16(0))) + t.Run("ToUintMaxInt16", wantConversion[uint](int16(math.MaxInt16))) + t.Run("ToUintptr", wantConversion[uintptr](int16(0))) + }) + + t.Run("FromInt32", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](int32(0))) + t.Run("ToInt8Max", wantConversion[int8](int32(math.MaxInt8))) + t.Run("ToInt8Min", wantConversion[int8](int32(math.MinInt8))) + t.Run("ToInt16", wantConversion[int16](int32(0))) + t.Run("ToInt16Max", wantConversion[int16](int32(math.MaxInt16))) + t.Run("ToInt16Min", wantConversion[int16](int32(math.MinInt16))) + t.Run("ToInt32", wantConversion[int32](int32(0))) + t.Run("ToInt32Max", wantConversion[int32](int32(math.MaxInt32))) + t.Run("ToInt32Min", wantConversion[int32](int32(math.MinInt32))) + t.Run("ToInt64", wantConversion[int64](int32(0))) + t.Run("ToInt64MaxInt32", wantConversion[int64](int32(math.MaxInt32))) + t.Run("ToInt64MinInt32", wantConversion[int64](int32(math.MinInt32))) + t.Run("ToInt", wantConversion[int](int32(0))) + t.Run("ToIntMaxInt32", wantConversion[int](int32(math.MaxInt32))) + t.Run("ToIntMinInt32", wantConversion[int](int32(math.MinInt32))) + t.Run("ToUint8", wantConversion[uint8](int32(0))) + t.Run("ToUint8Max", wantConversion[uint8](int32(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](int32(0))) + t.Run("ToUint16Max", wantConversion[uint16](int32(math.MaxUint16))) + t.Run("ToUint32", wantConversion[uint32](int32(0))) + t.Run("ToUint32MaxInt32", wantConversion[uint32](int32(math.MaxInt32))) + t.Run("ToUint64", wantConversion[uint64](int32(0))) + t.Run("ToUint64MaxInt32", wantConversion[uint64](int32(math.MaxInt32))) + t.Run("ToUint", wantConversion[uint](int32(0))) + t.Run("ToUintMaxInt32", wantConversion[uint](int32(math.MaxInt32))) + t.Run("ToUintptr", wantConversion[uintptr](int32(0))) + }) + + t.Run("FromInt64", func(t *testing.T) { + t.Run("ToInt8", wantConversion[int8](int64(0))) + t.Run("ToInt8Max", wantConversion[int8](int64(math.MaxInt8))) + t.Run("ToInt8Min", wantConversion[int8](int64(math.MinInt8))) + t.Run("ToInt16", wantConversion[int16](int64(0))) + t.Run("ToInt16Max", wantConversion[int16](int64(math.MaxInt16))) + t.Run("ToInt16Min", wantConversion[int16](int64(math.MinInt16))) + t.Run("ToInt32", wantConversion[int32](int64(0))) + t.Run("ToInt32Max", wantConversion[int32](int64(math.MaxInt32))) + t.Run("ToInt32Min", wantConversion[int32](int64(math.MinInt32))) + t.Run("ToInt64", wantConversion[int64](int64(0))) + t.Run("ToInt64Max", wantConversion[int64](int64(math.MaxInt64))) + t.Run("ToInt64Min", wantConversion[int64](int64(math.MinInt64))) + t.Run("ToInt", wantConversion[int](int64(0))) + t.Run("ToIntMaxInt", wantConversion[int](int64(math.MaxInt64))) + t.Run("ToIntMinInt", wantConversion[int](int64(math.MinInt64))) + t.Run("ToUint8", wantConversion[uint8](int64(0))) + t.Run("ToUint8Max", wantConversion[uint8](int64(math.MaxUint8))) + t.Run("ToUint16", wantConversion[uint16](int64(0))) + t.Run("ToUint16Max", wantConversion[uint16](int64(math.MaxUint16))) + t.Run("ToUint32", wantConversion[uint32](int64(0))) + t.Run("ToUint32Max", wantConversion[uint32](int64(math.MaxUint32))) + t.Run("ToUint64", wantConversion[uint64](int64(0))) + t.Run("ToUint64Max", wantConversion[uint64](int64(math.MaxInt64))) + t.Run("ToUint", wantConversion[uint](int64(0))) + t.Run("ToUintMaxInt", wantConversion[uint](int64(math.MaxInt64))) + t.Run("ToUintptr", wantConversion[uintptr](int64(0))) + }) +} + +func TestToInt_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToInt[int], 0)) + t.Run("FromIntMax", wantConversionFunc(narrow.ToInt[int], math.MaxInt)) + t.Run("FromIntMin", wantConversionFunc(narrow.ToInt[int], math.MinInt)) + t.Run("FromInt8", wantConversionFunc(narrow.ToInt[int8], 0)) + t.Run("FromInt8Min", wantConversionFunc(narrow.ToInt[int8], math.MinInt8)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToInt[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToInt[int16], 0)) + t.Run("FromInt16Min", wantConversionFunc(narrow.ToInt[int16], math.MinInt16)) + t.Run("FromInt16Max", wantConversionFunc(narrow.ToInt[int16], math.MaxInt16)) + t.Run("FromInt32", wantConversionFunc(narrow.ToInt[int32], 0)) + t.Run("FromInt32Min", wantConversionFunc(narrow.ToInt[int32], math.MinInt32)) + t.Run("FromInt32Max", wantConversionFunc(narrow.ToInt[int32], math.MaxInt32)) + t.Run("FromInt64", wantConversionFunc(narrow.ToInt[int64], 0)) + t.Run("FromInt64MaxInt", wantConversionFunc(narrow.ToInt[int64], math.MaxInt)) + t.Run("FromInt64MinInt", wantConversionFunc(narrow.ToInt[int64], math.MinInt)) + + t.Run("FromUint", wantConversionFunc(narrow.ToInt[uint], 0)) + t.Run("FromUintMaxInt", wantConversionFunc(narrow.ToInt[uint], math.MaxInt)) + t.Run("FromUint8", wantConversionFunc(narrow.ToInt[uint8], 0)) + t.Run("FromUint8MaxInt8", wantConversionFunc(narrow.ToInt[uint8], math.MaxInt8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToInt[uint16], 0)) + t.Run("FromUint16MaxInt16", wantConversionFunc(narrow.ToInt[uint16], math.MaxInt16)) + t.Run("FromUint32", wantConversionFunc(narrow.ToInt[uint32], 0)) + t.Run("FromUint32MaxInt32", wantConversionFunc(narrow.ToInt[uint32], math.MaxInt32)) + t.Run("FromUint64", wantConversionFunc(narrow.ToInt[uint64], 0)) + t.Run("FromUint64MaxInt", wantConversionFunc(narrow.ToInt[uint64], math.MaxInt)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToInt[uintptr], 0)) +} + +func TestToInt8_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToInt8[int], 0)) + t.Run("FromIntMaxInt8", wantConversionFunc(narrow.ToInt8[int], math.MaxInt8)) + t.Run("FromIntMinInt8", wantConversionFunc(narrow.ToInt8[int], math.MinInt8)) + t.Run("FromInt8", wantConversionFunc(narrow.ToInt8[int8], 0)) + t.Run("FromInt8Min", wantConversionFunc(narrow.ToInt8[int8], math.MinInt8)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToInt8[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToInt8[int16], 0)) + t.Run("FromInt16MinInt8", wantConversionFunc(narrow.ToInt8[int16], math.MinInt8)) + t.Run("FromInt16MaxInt8", wantConversionFunc(narrow.ToInt8[int16], math.MaxInt8)) + t.Run("FromInt32", wantConversionFunc(narrow.ToInt8[int32], 0)) + t.Run("FromInt32MinInt8", wantConversionFunc(narrow.ToInt8[int32], math.MinInt8)) + t.Run("FromInt32MaxInt8", wantConversionFunc(narrow.ToInt8[int32], math.MaxInt8)) + t.Run("FromInt64", wantConversionFunc(narrow.ToInt8[int64], 0)) + t.Run("FromInt64MaxInt8", wantConversionFunc(narrow.ToInt8[int64], math.MaxInt8)) + t.Run("FromInt64MinInt8", wantConversionFunc(narrow.ToInt8[int64], math.MinInt8)) + + t.Run("FromUint", wantConversionFunc(narrow.ToInt8[uint], 0)) + t.Run("FromUintMaxInt8", wantConversionFunc(narrow.ToInt8[uint], math.MaxInt8)) + t.Run("FromUint8", wantConversionFunc(narrow.ToInt8[uint8], 0)) + t.Run("FromUint8Max", wantConversionFunc(narrow.ToInt8[uint8], math.MaxInt8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToInt8[uint16], 0)) + t.Run("FromUint16MaxInt8", wantConversionFunc(narrow.ToInt8[uint16], math.MaxInt8)) + t.Run("FromUint32", wantConversionFunc(narrow.ToInt8[uint32], 0)) + t.Run("FromUint32MaxInt8", wantConversionFunc(narrow.ToInt8[uint32], math.MaxInt8)) + t.Run("FromUint64", wantConversionFunc(narrow.ToInt8[uint64], 0)) + t.Run("FromUint64MaxInt8", wantConversionFunc(narrow.ToInt8[uint64], math.MaxInt8)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToInt8[uintptr], 0)) +} + +func TestToInt16_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToInt16[int], 0)) + t.Run("FromIntMaxInt16", wantConversionFunc(narrow.ToInt16[int], math.MaxInt16)) + t.Run("FromIntMinInt16", wantConversionFunc(narrow.ToInt16[int], math.MinInt16)) + t.Run("FromInt8", wantConversionFunc(narrow.ToInt16[int8], 0)) + t.Run("FromInt8Min", wantConversionFunc(narrow.ToInt16[int8], math.MinInt8)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToInt16[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToInt16[int16], 0)) + t.Run("FromInt16Min", wantConversionFunc(narrow.ToInt16[int16], math.MinInt16)) + t.Run("FromInt16Max", wantConversionFunc(narrow.ToInt16[int16], math.MaxInt16)) + t.Run("FromInt32", wantConversionFunc(narrow.ToInt16[int32], 0)) + t.Run("FromInt32MinInt16", wantConversionFunc(narrow.ToInt16[int32], math.MinInt16)) + t.Run("FromInt32MaxInt16", wantConversionFunc(narrow.ToInt16[int32], math.MaxInt16)) + t.Run("FromInt64", wantConversionFunc(narrow.ToInt16[int64], 0)) + t.Run("FromInt64MaxInt16", wantConversionFunc(narrow.ToInt16[int64], math.MaxInt16)) + t.Run("FromInt64MinInt16", wantConversionFunc(narrow.ToInt16[int64], math.MinInt16)) + + t.Run("FromUint", wantConversionFunc(narrow.ToInt16[uint], 0)) + t.Run("FromUintMaxInt16", wantConversionFunc(narrow.ToInt16[uint], math.MaxInt16)) + t.Run("FromUint8", wantConversionFunc(narrow.ToInt16[uint8], 0)) + t.Run("FromUint8Max", wantConversionFunc(narrow.ToInt16[uint8], math.MaxInt8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToInt16[uint16], 0)) + t.Run("FromUint16MaxInt16", wantConversionFunc(narrow.ToInt16[uint16], math.MaxInt16)) + t.Run("FromUint32", wantConversionFunc(narrow.ToInt16[uint32], 0)) + t.Run("FromUint32MaxInt16", wantConversionFunc(narrow.ToInt16[uint32], math.MaxInt16)) + t.Run("FromUint64", wantConversionFunc(narrow.ToInt16[uint64], 0)) + t.Run("FromUint64MaxInt16", wantConversionFunc(narrow.ToInt16[uint64], math.MaxInt16)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToInt16[uintptr], 0)) +} + +func TestToInt32_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToInt32[int], 0)) + t.Run("FromIntMaxInt32", wantConversionFunc(narrow.ToInt32[int], math.MaxInt32)) + t.Run("FromIntMinInt32", wantConversionFunc(narrow.ToInt32[int], math.MinInt32)) + t.Run("FromInt8", wantConversionFunc(narrow.ToInt32[int8], 0)) + t.Run("FromInt8Min", wantConversionFunc(narrow.ToInt32[int8], math.MinInt8)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToInt32[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToInt32[int16], 0)) + t.Run("FromInt16Min", wantConversionFunc(narrow.ToInt32[int16], math.MinInt16)) + t.Run("FromInt16Max", wantConversionFunc(narrow.ToInt32[int16], math.MaxInt16)) + t.Run("FromInt32", wantConversionFunc(narrow.ToInt32[int32], 0)) + t.Run("FromInt32Min", wantConversionFunc(narrow.ToInt32[int32], math.MinInt32)) + t.Run("FromInt32Max", wantConversionFunc(narrow.ToInt32[int32], math.MaxInt32)) + t.Run("FromInt64", wantConversionFunc(narrow.ToInt32[int64], 0)) + t.Run("FromInt64MaxInt32", wantConversionFunc(narrow.ToInt32[int64], math.MaxInt32)) + t.Run("FromInt64MinInt32", wantConversionFunc(narrow.ToInt32[int64], math.MinInt32)) + + t.Run("FromUint", wantConversionFunc(narrow.ToInt32[uint], 0)) + t.Run("FromUintMaxInt32", wantConversionFunc(narrow.ToInt32[uint], math.MaxInt32)) + t.Run("FromUint8", wantConversionFunc(narrow.ToInt32[uint8], 0)) + t.Run("FromUint8MaxInt8", wantConversionFunc(narrow.ToInt32[uint8], math.MaxInt8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToInt32[uint16], 0)) + t.Run("FromUint16MaxInt16", wantConversionFunc(narrow.ToInt32[uint16], math.MaxInt16)) + t.Run("FromUint32", wantConversionFunc(narrow.ToInt32[uint32], 0)) + t.Run("FromUint32MaxInt32", wantConversionFunc(narrow.ToInt32[uint32], math.MaxInt32)) + t.Run("FromUint64", wantConversionFunc(narrow.ToInt32[uint64], 0)) + t.Run("FromUint64MaxInt32", wantConversionFunc(narrow.ToInt32[uint64], math.MaxInt32)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToInt32[uintptr], 0)) +} + +func TestToInt64_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToInt64[int], 0)) + t.Run("FromIntMaxInt32", wantConversionFunc(narrow.ToInt64[int], math.MaxInt32)) + t.Run("FromIntMin", wantConversionFunc(narrow.ToInt64[int], math.MinInt32)) + t.Run("FromInt8", wantConversionFunc(narrow.ToInt64[int8], 0)) + t.Run("FromInt8Min", wantConversionFunc(narrow.ToInt64[int8], math.MinInt8)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToInt64[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToInt64[int16], 0)) + t.Run("FromInt16Min", wantConversionFunc(narrow.ToInt64[int16], math.MinInt16)) + t.Run("FromInt16Max", wantConversionFunc(narrow.ToInt64[int16], math.MaxInt16)) + t.Run("FromInt32", wantConversionFunc(narrow.ToInt64[int32], 0)) + t.Run("FromInt32Min", wantConversionFunc(narrow.ToInt64[int32], math.MinInt32)) + t.Run("FromInt32Max", wantConversionFunc(narrow.ToInt64[int32], math.MaxInt32)) + t.Run("FromInt64", wantConversionFunc(narrow.ToInt64[int64], 0)) + t.Run("FromInt64Max", wantConversionFunc(narrow.ToInt64[int64], math.MaxInt64)) + t.Run("FromInt64Min", wantConversionFunc(narrow.ToInt64[int64], math.MinInt64)) + + t.Run("FromUint", wantConversionFunc(narrow.ToInt64[uint], 0)) + t.Run("FromUintMaxUint32", wantConversionFunc(narrow.ToInt64[uint], math.MaxUint32)) + t.Run("FromUint8", wantConversionFunc(narrow.ToInt64[uint8], 0)) + t.Run("FromUint8Max", wantConversionFunc(narrow.ToInt64[uint8], math.MaxUint8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToInt64[uint16], 0)) + t.Run("FromUint16Max", wantConversionFunc(narrow.ToInt64[uint16], math.MaxUint16)) + t.Run("FromUint32", wantConversionFunc(narrow.ToInt64[uint32], 0)) + t.Run("FromUint32Max", wantConversionFunc(narrow.ToInt64[uint32], math.MaxUint32)) + t.Run("FromUint64", wantConversionFunc(narrow.ToInt64[uint64], 0)) + t.Run("FromUint64Max", wantConversionFunc(narrow.ToInt64[uint64], math.MaxInt64)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToInt64[uintptr], 0)) +} + +func TestToUint_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToUint[int], 0)) + t.Run("FromIntMaxInt32", wantConversionFunc(narrow.ToUint[int], math.MaxInt32)) + t.Run("FromInt8", wantConversionFunc(narrow.ToUint[int8], 0)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToUint[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToUint[int16], 0)) + t.Run("FromInt16Max", wantConversionFunc(narrow.ToUint[int16], math.MaxInt16)) + t.Run("FromInt32", wantConversionFunc(narrow.ToUint[int32], 0)) + t.Run("FromInt32Max", wantConversionFunc(narrow.ToUint[int32], math.MaxInt32)) + t.Run("FromInt64", wantConversionFunc(narrow.ToUint[int64], 0)) + t.Run("FromInt64MaxUint32", wantConversionFunc(narrow.ToUint[int64], math.MaxInt32)) + + t.Run("FromUint", wantConversionFunc(narrow.ToUint[uint], 0)) + t.Run("FromUintMaxUint32", wantConversionFunc(narrow.ToUint[uint], math.MaxUint32)) + t.Run("FromUint8", wantConversionFunc(narrow.ToUint[uint8], 0)) + t.Run("FromUint8Max", wantConversionFunc(narrow.ToUint[uint8], math.MaxUint8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToUint[uint16], 0)) + t.Run("FromUint16Max", wantConversionFunc(narrow.ToUint[uint16], math.MaxUint16)) + t.Run("FromUint32", wantConversionFunc(narrow.ToUint[uint32], 0)) + t.Run("FromUint32Max", wantConversionFunc(narrow.ToUint[uint32], math.MaxUint32)) + t.Run("FromUint64", wantConversionFunc(narrow.ToUint[uint64], 0)) + t.Run("FromUint64MaxUint32", wantConversionFunc(narrow.ToUint[uint64], math.MaxUint32)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToUint[uintptr], 0)) +} + +func TestToUint8_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToUint8[int], 0)) + t.Run("FromIntMaxUint8", wantConversionFunc(narrow.ToUint8[int], math.MaxUint8)) + t.Run("FromInt8", wantConversionFunc(narrow.ToUint8[int8], 0)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToUint8[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToUint8[int16], 0)) + t.Run("FromInt16MaxUint8", wantConversionFunc(narrow.ToUint8[int16], math.MaxInt8)) + t.Run("FromInt32", wantConversionFunc(narrow.ToUint8[int32], 0)) + t.Run("FromInt32MaxUint8", wantConversionFunc(narrow.ToUint8[int32], math.MaxUint8)) + t.Run("FromInt64", wantConversionFunc(narrow.ToUint8[int64], 0)) + t.Run("FromInt64MaxUint8", wantConversionFunc(narrow.ToUint8[int64], math.MaxUint8)) + + t.Run("FromUint", wantConversionFunc(narrow.ToUint8[uint], 0)) + t.Run("FromUintMaxUint8", wantConversionFunc(narrow.ToUint8[uint], math.MaxUint8)) + t.Run("FromUint8", wantConversionFunc(narrow.ToUint8[uint8], 0)) + t.Run("FromUint8Max", wantConversionFunc(narrow.ToUint8[uint8], math.MaxUint8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToUint8[uint16], 0)) + t.Run("FromUint16MaxUint8", wantConversionFunc(narrow.ToUint8[uint16], math.MaxUint8)) + t.Run("FromUint32", wantConversionFunc(narrow.ToUint8[uint32], 0)) + t.Run("FromUint32MaxUint8", wantConversionFunc(narrow.ToUint8[uint32], math.MaxUint8)) + t.Run("FromUint64", wantConversionFunc(narrow.ToUint8[uint64], 0)) + t.Run("FromUint64MaxUint8", wantConversionFunc(narrow.ToUint8[uint64], math.MaxUint8)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToUint8[uintptr], 0)) +} + +func TestToUint16_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToUint16[int], 0)) + t.Run("FromIntMaxUint16", wantConversionFunc(narrow.ToUint16[int], math.MaxUint16)) + t.Run("FromInt8", wantConversionFunc(narrow.ToUint16[int8], 0)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToUint16[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToUint16[int16], 0)) + t.Run("FromInt16Max", wantConversionFunc(narrow.ToUint16[int16], math.MaxInt16)) + t.Run("FromInt32", wantConversionFunc(narrow.ToUint16[int32], 0)) + t.Run("FromInt32MaxUint16", wantConversionFunc(narrow.ToUint16[int32], math.MaxUint16)) + t.Run("FromInt64", wantConversionFunc(narrow.ToUint16[int64], 0)) + t.Run("FromInt64MaxUint16", wantConversionFunc(narrow.ToUint16[int64], math.MaxUint16)) + + t.Run("FromUint", wantConversionFunc(narrow.ToUint16[uint], 0)) + t.Run("FromUintMaxUint16", wantConversionFunc(narrow.ToUint16[uint], math.MaxUint16)) + t.Run("FromUint8", wantConversionFunc(narrow.ToUint16[uint8], 0)) + t.Run("FromUint8Max", wantConversionFunc(narrow.ToUint16[uint8], math.MaxUint8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToUint16[uint16], 0)) + t.Run("FromUint16Max", wantConversionFunc(narrow.ToUint16[uint16], math.MaxUint16)) + t.Run("FromUint32", wantConversionFunc(narrow.ToUint16[uint32], 0)) + t.Run("FromUint32MaxUint16", wantConversionFunc(narrow.ToUint16[uint32], math.MaxUint16)) + t.Run("FromUint64", wantConversionFunc(narrow.ToUint16[uint64], 0)) + t.Run("FromUint64MaxUint16", wantConversionFunc(narrow.ToUint16[uint64], math.MaxUint16)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToUint16[uintptr], 0)) +} + +func TestToUint32_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToUint32[int], 0)) + t.Run("FromIntMaxInt32", wantConversionFunc(narrow.ToUint32[int], math.MaxInt32)) + t.Run("FromInt8", wantConversionFunc(narrow.ToUint32[int8], 0)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToUint32[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToUint32[int16], 0)) + t.Run("FromInt16Max", wantConversionFunc(narrow.ToUint32[int16], math.MaxInt16)) + t.Run("FromInt32", wantConversionFunc(narrow.ToUint32[int32], 0)) + t.Run("FromInt32Max", wantConversionFunc(narrow.ToUint32[int32], math.MaxInt32)) + t.Run("FromInt64", wantConversionFunc(narrow.ToUint32[int64], 0)) + t.Run("FromInt64MaxUint32", wantConversionFunc(narrow.ToUint32[int64], math.MaxInt32)) + + t.Run("FromUint", wantConversionFunc(narrow.ToUint32[uint], 0)) + t.Run("FromUintMaxUint32", wantConversionFunc(narrow.ToUint32[uint], math.MaxUint32)) + t.Run("FromUint8", wantConversionFunc(narrow.ToUint32[uint8], 0)) + t.Run("FromUint8Max", wantConversionFunc(narrow.ToUint32[uint8], math.MaxUint8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToUint32[uint16], 0)) + t.Run("FromUint16Max", wantConversionFunc(narrow.ToUint32[uint16], math.MaxUint16)) + t.Run("FromUint32", wantConversionFunc(narrow.ToUint32[uint32], 0)) + t.Run("FromUint32Max", wantConversionFunc(narrow.ToUint32[uint32], math.MaxUint32)) + t.Run("FromUint64", wantConversionFunc(narrow.ToUint32[uint64], 0)) + t.Run("FromUint64MaxUint32", wantConversionFunc(narrow.ToUint32[uint64], math.MaxUint32)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToUint32[uintptr], 0)) +} + +func TestToUint64_ValueFitsIntoReceiver_ReturnsValueAndTrue(t *testing.T) { + t.Run("FromInt", wantConversionFunc(narrow.ToUint64[int], 0)) + t.Run("FromIntMaxInt32", wantConversionFunc(narrow.ToUint64[int], math.MaxInt32)) + t.Run("FromInt8", wantConversionFunc(narrow.ToUint64[int8], 0)) + t.Run("FromInt8Max", wantConversionFunc(narrow.ToUint64[int8], math.MaxInt8)) + t.Run("FromInt16", wantConversionFunc(narrow.ToUint64[int16], 0)) + t.Run("FromInt16Max", wantConversionFunc(narrow.ToUint64[int16], math.MaxInt16)) + t.Run("FromInt32", wantConversionFunc(narrow.ToUint64[int32], 0)) + t.Run("FromInt32Max", wantConversionFunc(narrow.ToUint64[int32], math.MaxInt32)) + t.Run("FromInt64", wantConversionFunc(narrow.ToUint64[int64], 0)) + t.Run("FromInt64Max", wantConversionFunc(narrow.ToUint64[int64], math.MaxInt64)) + + t.Run("FromUint", wantConversionFunc(narrow.ToUint64[uint], 0)) + t.Run("FromUintMaxUint32", wantConversionFunc(narrow.ToUint64[uint], math.MaxUint32)) + t.Run("FromUint8", wantConversionFunc(narrow.ToUint64[uint8], 0)) + t.Run("FromUint8Max", wantConversionFunc(narrow.ToUint64[uint8], math.MaxUint8)) + t.Run("FromUint16", wantConversionFunc(narrow.ToUint64[uint16], 0)) + t.Run("FromUint16Max", wantConversionFunc(narrow.ToUint64[uint16], math.MaxUint16)) + t.Run("FromUint32", wantConversionFunc(narrow.ToUint64[uint32], 0)) + t.Run("FromUint32Max", wantConversionFunc(narrow.ToUint64[uint32], math.MaxUint32)) + t.Run("FromUint64", wantConversionFunc(narrow.ToUint64[uint64], 0)) + t.Run("FromUint64Max", wantConversionFunc(narrow.ToUint64[uint64], math.MaxUint64)) + t.Run("FromUintptr", wantConversionFunc(narrow.ToUint64[uintptr], 0)) +} diff --git a/internal/protofields/descriptor.go b/internal/protofields/descriptor.go new file mode 100644 index 0000000..e90d936 --- /dev/null +++ b/internal/protofields/descriptor.go @@ -0,0 +1,12 @@ +package protofields + +import "google.golang.org/protobuf/proto" + +// DescriptorName gets the type name of a proto Message. If value is nil, this +// returns an empty string. +func DescriptorName(value proto.Message) string { + if value == nil { + return "" + } + return string(value.ProtoReflect().Descriptor().Name()) +} diff --git a/internal/protofields/dummies.go b/internal/protofields/dummies.go new file mode 100644 index 0000000..9253fd4 --- /dev/null +++ b/internal/protofields/dummies.go @@ -0,0 +1,364 @@ +package protofields + +import ( + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/account_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/activity_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/adverse_event_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/allergy_intolerance_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/audit_event_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/basic_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/binary_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/biologically_derived_product_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/body_structure_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/capability_statement_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/care_plan_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/care_team_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/catalog_entry_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/charge_item_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/charge_item_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/claim_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/claim_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/clinical_impression_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/code_system_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/communication_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/communication_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/compartment_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/composition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/concept_map_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/condition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/consent_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/contract_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/coverage_eligibility_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/coverage_eligibility_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/coverage_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/detected_issue_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_metric_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_use_statement_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/diagnostic_report_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/document_manifest_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/document_reference_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/effect_evidence_synthesis_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/encounter_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/endpoint_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/enrollment_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/enrollment_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/episode_of_care_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/event_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/evidence_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/evidence_variable_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/example_scenario_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/explanation_of_benefit_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/family_member_history_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/flag_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/goal_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/graph_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/group_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/guidance_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/healthcare_service_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/imaging_study_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/immunization_evaluation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/immunization_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/immunization_recommendation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/implementation_guide_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/insurance_plan_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/invoice_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/library_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/linkage_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/list_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/location_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/measure_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/measure_report_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/media_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_administration_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_dispense_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_knowledge_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_statement_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_authorization_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_contraindication_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_indication_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_ingredient_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_interaction_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_manufactured_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_packaged_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_pharmaceutical_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medicinal_product_undesirable_effect_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/message_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/message_header_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/molecular_sequence_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/naming_system_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/nutrition_order_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/operation_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/operation_outcome_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/organization_affiliation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/organization_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/parameters_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/payment_notice_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/payment_reconciliation_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/person_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/plan_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/practitioner_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/practitioner_role_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/procedure_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/provenance_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_response_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/related_person_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/request_group_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_element_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_study_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_subject_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/risk_assessment_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/risk_evidence_synthesis_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/schedule_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/search_parameter_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/service_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/slot_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/specimen_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/specimen_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/structure_definition_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/structure_map_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/subscription_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_nucleic_acid_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_polymer_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_protein_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_reference_information_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_source_material_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/substance_specification_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/supply_delivery_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/supply_request_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/task_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/terminology_capabilities_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/test_report_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/test_script_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/value_set_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/verification_result_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/vision_prescription_go_proto" + "google.golang.org/protobuf/proto" +) + +// dummyResources is an array of nil pointers to all resource messages. +var dummyResources = []proto.Message{ + (*account_go_proto.Account)(nil), + (*activity_definition_go_proto.ActivityDefinition)(nil), + (*adverse_event_go_proto.AdverseEvent)(nil), + (*allergy_intolerance_go_proto.AllergyIntolerance)(nil), + (*appointment_go_proto.Appointment)(nil), + (*appointment_response_go_proto.AppointmentResponse)(nil), + (*audit_event_go_proto.AuditEvent)(nil), + (*basic_go_proto.Basic)(nil), + (*biologically_derived_product_go_proto.BiologicallyDerivedProduct)(nil), + (*body_structure_go_proto.BodyStructure)(nil), + (*capability_statement_go_proto.CapabilityStatement)(nil), + (*care_plan_go_proto.CarePlan)(nil), + (*care_team_go_proto.CareTeam)(nil), + (*catalog_entry_go_proto.CatalogEntry)(nil), + (*charge_item_go_proto.ChargeItem)(nil), + (*charge_item_definition_go_proto.ChargeItemDefinition)(nil), + (*claim_go_proto.Claim)(nil), + (*claim_response_go_proto.ClaimResponse)(nil), + (*clinical_impression_go_proto.ClinicalImpression)(nil), + (*code_system_go_proto.CodeSystem)(nil), + (*communication_go_proto.Communication)(nil), + (*communication_request_go_proto.CommunicationRequest)(nil), + (*compartment_definition_go_proto.CompartmentDefinition)(nil), + (*composition_go_proto.Composition)(nil), + (*concept_map_go_proto.ConceptMap)(nil), + (*condition_go_proto.Condition)(nil), + (*consent_go_proto.Consent)(nil), + (*contract_go_proto.Contract)(nil), + (*coverage_go_proto.Coverage)(nil), + (*coverage_eligibility_request_go_proto.CoverageEligibilityRequest)(nil), + (*coverage_eligibility_response_go_proto.CoverageEligibilityResponse)(nil), + (*detected_issue_go_proto.DetectedIssue)(nil), + (*device_go_proto.Device)(nil), + (*device_definition_go_proto.DeviceDefinition)(nil), + (*device_metric_go_proto.DeviceMetric)(nil), + (*device_request_go_proto.DeviceRequest)(nil), + (*device_use_statement_go_proto.DeviceUseStatement)(nil), + (*diagnostic_report_go_proto.DiagnosticReport)(nil), + (*document_manifest_go_proto.DocumentManifest)(nil), + (*document_reference_go_proto.DocumentReference)(nil), + (*effect_evidence_synthesis_go_proto.EffectEvidenceSynthesis)(nil), + (*encounter_go_proto.Encounter)(nil), + (*endpoint_go_proto.Endpoint)(nil), + (*enrollment_request_go_proto.EnrollmentRequest)(nil), + (*enrollment_response_go_proto.EnrollmentResponse)(nil), + (*episode_of_care_go_proto.EpisodeOfCare)(nil), + (*event_definition_go_proto.EventDefinition)(nil), + (*evidence_go_proto.Evidence)(nil), + (*evidence_variable_go_proto.EvidenceVariable)(nil), + (*example_scenario_go_proto.ExampleScenario)(nil), + (*explanation_of_benefit_go_proto.ExplanationOfBenefit)(nil), + (*family_member_history_go_proto.FamilyMemberHistory)(nil), + (*flag_go_proto.Flag)(nil), + (*goal_go_proto.Goal)(nil), + (*graph_definition_go_proto.GraphDefinition)(nil), + (*group_go_proto.Group)(nil), + (*guidance_response_go_proto.GuidanceResponse)(nil), + (*healthcare_service_go_proto.HealthcareService)(nil), + (*imaging_study_go_proto.ImagingStudy)(nil), + (*immunization_go_proto.Immunization)(nil), + (*immunization_evaluation_go_proto.ImmunizationEvaluation)(nil), + (*immunization_recommendation_go_proto.ImmunizationRecommendation)(nil), + (*implementation_guide_go_proto.ImplementationGuide)(nil), + (*insurance_plan_go_proto.InsurancePlan)(nil), + (*invoice_go_proto.Invoice)(nil), + (*library_go_proto.Library)(nil), + (*linkage_go_proto.Linkage)(nil), + (*list_go_proto.List)(nil), + (*location_go_proto.Location)(nil), + (*measure_go_proto.Measure)(nil), + (*measure_report_go_proto.MeasureReport)(nil), + (*media_go_proto.Media)(nil), + (*medication_go_proto.Medication)(nil), + (*medication_administration_go_proto.MedicationAdministration)(nil), + (*medication_dispense_go_proto.MedicationDispense)(nil), + (*medication_knowledge_go_proto.MedicationKnowledge)(nil), + (*medication_request_go_proto.MedicationRequest)(nil), + (*medication_statement_go_proto.MedicationStatement)(nil), + (*medicinal_product_go_proto.MedicinalProduct)(nil), + (*medicinal_product_authorization_go_proto.MedicinalProductAuthorization)(nil), + (*medicinal_product_contraindication_go_proto.MedicinalProductContraindication)(nil), + (*medicinal_product_indication_go_proto.MedicinalProductIndication)(nil), + (*medicinal_product_ingredient_go_proto.MedicinalProductIngredient)(nil), + (*medicinal_product_interaction_go_proto.MedicinalProductInteraction)(nil), + (*medicinal_product_manufactured_go_proto.MedicinalProductManufactured)(nil), + (*medicinal_product_packaged_go_proto.MedicinalProductPackaged)(nil), + (*medicinal_product_pharmaceutical_go_proto.MedicinalProductPharmaceutical)(nil), + (*medicinal_product_undesirable_effect_go_proto.MedicinalProductUndesirableEffect)(nil), + (*message_definition_go_proto.MessageDefinition)(nil), + (*message_header_go_proto.MessageHeader)(nil), + (*molecular_sequence_go_proto.MolecularSequence)(nil), + (*naming_system_go_proto.NamingSystem)(nil), + (*nutrition_order_go_proto.NutritionOrder)(nil), + (*observation_go_proto.Observation)(nil), + (*observation_definition_go_proto.ObservationDefinition)(nil), + (*operation_definition_go_proto.OperationDefinition)(nil), + (*operation_outcome_go_proto.OperationOutcome)(nil), + (*organization_go_proto.Organization)(nil), + (*organization_affiliation_go_proto.OrganizationAffiliation)(nil), + (*patient_go_proto.Patient)(nil), + (*payment_notice_go_proto.PaymentNotice)(nil), + (*payment_reconciliation_go_proto.PaymentReconciliation)(nil), + (*person_go_proto.Person)(nil), + (*plan_definition_go_proto.PlanDefinition)(nil), + (*practitioner_go_proto.Practitioner)(nil), + (*practitioner_role_go_proto.PractitionerRole)(nil), + (*procedure_go_proto.Procedure)(nil), + (*provenance_go_proto.Provenance)(nil), + (*questionnaire_go_proto.Questionnaire)(nil), + (*questionnaire_response_go_proto.QuestionnaireResponse)(nil), + (*related_person_go_proto.RelatedPerson)(nil), + (*request_group_go_proto.RequestGroup)(nil), + (*research_definition_go_proto.ResearchDefinition)(nil), + (*research_element_definition_go_proto.ResearchElementDefinition)(nil), + (*research_study_go_proto.ResearchStudy)(nil), + (*research_subject_go_proto.ResearchSubject)(nil), + (*risk_assessment_go_proto.RiskAssessment)(nil), + (*risk_evidence_synthesis_go_proto.RiskEvidenceSynthesis)(nil), + (*schedule_go_proto.Schedule)(nil), + (*search_parameter_go_proto.SearchParameter)(nil), + (*service_request_go_proto.ServiceRequest)(nil), + (*slot_go_proto.Slot)(nil), + (*specimen_go_proto.Specimen)(nil), + (*specimen_definition_go_proto.SpecimenDefinition)(nil), + (*structure_definition_go_proto.StructureDefinition)(nil), + (*structure_map_go_proto.StructureMap)(nil), + (*subscription_go_proto.Subscription)(nil), + (*substance_go_proto.Substance)(nil), + (*substance_nucleic_acid_go_proto.SubstanceNucleicAcid)(nil), + (*substance_polymer_go_proto.SubstancePolymer)(nil), + (*substance_protein_go_proto.SubstanceProtein)(nil), + (*substance_reference_information_go_proto.SubstanceReferenceInformation)(nil), + (*substance_source_material_go_proto.SubstanceSourceMaterial)(nil), + (*substance_specification_go_proto.SubstanceSpecification)(nil), + (*supply_delivery_go_proto.SupplyDelivery)(nil), + (*supply_request_go_proto.SupplyRequest)(nil), + (*task_go_proto.Task)(nil), + (*terminology_capabilities_go_proto.TerminologyCapabilities)(nil), + (*test_report_go_proto.TestReport)(nil), + (*test_script_go_proto.TestScript)(nil), + (*value_set_go_proto.ValueSet)(nil), + (*verification_result_go_proto.VerificationResult)(nil), + (*vision_prescription_go_proto.VisionPrescription)(nil), + (*parameters_go_proto.Parameters)(nil), + (*binary_go_proto.Binary)(nil), + (*bundle_and_contained_resource_go_proto.Bundle)(nil), +} + +// dummyElements is an array of nil pointers to all Element messages. +var dummyElements = []proto.Message{ + (*datatypes_go_proto.Address)(nil), + (*datatypes_go_proto.Age)(nil), + (*datatypes_go_proto.Annotation)(nil), + (*datatypes_go_proto.Attachment)(nil), + (*datatypes_go_proto.Base64Binary)(nil), + (*datatypes_go_proto.Boolean)(nil), + (*datatypes_go_proto.Canonical)(nil), + (*datatypes_go_proto.Code)(nil), + (*datatypes_go_proto.CodeableConcept)(nil), + (*datatypes_go_proto.Coding)(nil), + (*datatypes_go_proto.ContactDetail)(nil), + (*datatypes_go_proto.ContactPoint)(nil), + (*datatypes_go_proto.Contributor)(nil), + (*datatypes_go_proto.Count)(nil), + (*datatypes_go_proto.DataRequirement)(nil), + (*datatypes_go_proto.Date)(nil), + (*datatypes_go_proto.DateTime)(nil), + (*datatypes_go_proto.Decimal)(nil), + (*datatypes_go_proto.Distance)(nil), + (*datatypes_go_proto.Dosage)(nil), + (*datatypes_go_proto.Duration)(nil), + (*datatypes_go_proto.ElementDefinition)(nil), + (*datatypes_go_proto.Expression)(nil), + (*datatypes_go_proto.Extension)(nil), + (*datatypes_go_proto.HumanName)(nil), + (*datatypes_go_proto.Id)(nil), + (*datatypes_go_proto.Identifier)(nil), + (*datatypes_go_proto.Instant)(nil), + (*datatypes_go_proto.Integer)(nil), + (*datatypes_go_proto.Markdown)(nil), + (*datatypes_go_proto.MarketingStatus)(nil), + (*datatypes_go_proto.Meta)(nil), + (*datatypes_go_proto.Money)(nil), + (*datatypes_go_proto.MoneyQuantity)(nil), + (*datatypes_go_proto.Narrative)(nil), + (*datatypes_go_proto.Oid)(nil), + (*datatypes_go_proto.ParameterDefinition)(nil), + (*datatypes_go_proto.Period)(nil), + (*datatypes_go_proto.PositiveInt)(nil), + (*datatypes_go_proto.ProductShelfLife)(nil), + (*datatypes_go_proto.Quantity)(nil), + (*datatypes_go_proto.Range)(nil), + (*datatypes_go_proto.Ratio)(nil), + (*datatypes_go_proto.Reference)(nil), + (*datatypes_go_proto.RelatedArtifact)(nil), + (*datatypes_go_proto.SampledData)(nil), + (*datatypes_go_proto.Signature)(nil), + (*datatypes_go_proto.SimpleQuantity)(nil), + (*datatypes_go_proto.String)(nil), + (*datatypes_go_proto.Time)(nil), + (*datatypes_go_proto.Timing)(nil), + (*datatypes_go_proto.TriggerDefinition)(nil), + (*datatypes_go_proto.UnsignedInt)(nil), + (*datatypes_go_proto.Uri)(nil), + (*datatypes_go_proto.Url)(nil), + (*datatypes_go_proto.UsageContext)(nil), + (*datatypes_go_proto.Uuid)(nil), + (*datatypes_go_proto.Xhtml)(nil), +} diff --git a/internal/protofields/fields.go b/internal/protofields/fields.go new file mode 100644 index 0000000..5d2a097 --- /dev/null +++ b/internal/protofields/fields.go @@ -0,0 +1,212 @@ +package protofields + +import ( + "strings" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + "github.com/iancoleman/strcase" + "github.com/verily-src/fhirpath-go/internal/slices" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// FieldToValueFunc is a function converting a protoreflect.Message's field-descriptor +// into a Value. +// +// This corresponds to either `protoreflect.Message.Mutable` or `protoreflect.Message.NewField`. +// +// This type is an implementation-detail shared through various APIs that support +// a mix of appending or overwriting sequences. +type FieldToValueFunc func(protoreflect.Message, protoreflect.FieldDescriptor) protoreflect.Value + +// ResourceFieldRefs is a container of back-references for a specified Resource. +// This enables finding one-ofs that can work with the resource it represents, +// for easier/faster lookup than waiting on protoreflect APIs. +type ResourceFieldRefs struct { + // ContainedResource contains back-references for the ContainedResource to + // this type + ContainedResource struct { + // Resource is the ContainedResource.Resource field that corresponds to + // this resource. + // + // This field cannot be nil, as all R4 resources are valid contained-resource + // types. + Resource protoreflect.FieldDescriptor + } + + // New is a function that will create a new instance of this FHIR Resource. + New func() proto.Message +} + +// ElementFieldRefs is a container of back-references for a specified Element. +// This enables finding one-ofs that can work with the resource it represents, +// for easier/faster lookup than waiting on protoreflect APIs. +type ElementFieldRefs struct { + // Extension contains back-references for the Extension to this type + Extension struct { + // ValueX is the Extension.ValueX field that corresponds to + // this resource. + // + // This field cannot be nil, as all R4 resources are valid contained-resource + // types. + ValueX protoreflect.FieldDescriptor + } + + // New is a function that will create a new instance of this FHIR element. + New func() proto.Message +} + +var ( + // Resources is a map of all resource names to their corresponding field references. + Resources map[string]*ResourceFieldRefs + + // Elements is a map of all element names to their corresponding field references. + Elements map[string]*ElementFieldRefs +) + +// IsValidResourceType checks that the given name is a valid resource name +func IsValidResourceType(name string) bool { + _, ok := Resources[name] + return ok +} + +// IsValidElementType checks that the given name is a valid element name +func IsValidElementType(name string) bool { + _, ok := Elements[name] + return ok +} + +// TypeToContainedResourceOneOfFieldName converts a resource type name into their +// respective ContainedResource "OneOf" field. +func TypeToContainedResourceOneOfFieldName(resource string) protoreflect.Name { + return protoreflect.Name(toSnakeCase(resource)) +} + +// UnwrapOneofField obtains the underlying Message for "Oneof" elements +// contained in fields with the given fieldName. Returns nil if the input message +// doesn't have the given field, or if the Oneof descriptor is unpopulated. +func UnwrapOneofField(element proto.Message, fieldName string) proto.Message { + message := element.ProtoReflect() + oneOfDescriptor := message.Descriptor().Oneofs().ByName(protoreflect.Name(fieldName)) + if oneOfDescriptor == nil { + return nil + } + fd := message.WhichOneof(oneOfDescriptor) + if fd == nil { + return nil + } + return message.Get(fd).Message().Interface() +} + +// IsCodeField returns true if the message represents a FHIR code type. +// Codes with enum values and string values are both considered valid. +func IsCodeField(message proto.Message) bool { + reflect := message.ProtoReflect() + name := string(reflect.Descriptor().Name()) + field := reflect.Descriptor().Fields().ByName(protoreflect.Name("value")) + if field != nil { + allowedKinds := []protoreflect.Kind{protoreflect.EnumKind, protoreflect.StringKind} + isValidFieldType := slices.Includes(allowedKinds, field.Kind()) + return strings.HasSuffix(name, "Code") && isValidFieldType + } + return false +} + +// StringValueFromCodeField gets the Field Descriptor of a message that +// represents a FHIR Code type. Returns the string value of the enum or +// string value of the Code, along with a boolean flag representing +// whether or not the input is a code type. +func StringValueFromCodeField(message proto.Message) (string, bool) { + if IsCodeField(message) { + reflect := message.ProtoReflect() + field := reflect.Descriptor().Fields().ByName(protoreflect.Name("value")) + if field.Kind() == protoreflect.EnumKind { + enum := reflect.Get(field).Enum() + code := string(field.Enum().Values().ByNumber(enum).Name()) + return strcase.ToKebab(code), true + } + if field.Kind() == protoreflect.StringKind { + return reflect.Get(field).String(), true + } + } + return "", false +} + +// Field is a struct containing both the Value and FieldDescriptor for a proto field. +type Field struct { + Value protoreflect.Value + Descriptor protoreflect.FieldDescriptor +} + +// GetField retrieves the proto field of the specified name from the message. +func GetField(message proto.Message, name string) (*Field, bool) { + fieldName := strcase.ToSnake(name) + msg := message.ProtoReflect() + descriptor := msg.Descriptor() + field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) + if field == nil { + return nil, false + } + + value := msg.Get(field) + return &Field{ + Value: value, + Descriptor: field, + }, true +} + +func getContainedResourceOneOf(message proto.Message) protoreflect.FieldDescriptor { + cr := (*bcrpb.ContainedResource)(nil) + + name := DescriptorName(message) + fieldName := TypeToContainedResourceOneOfFieldName(name) + return cr.ProtoReflect().Descriptor().Fields().ByName(fieldName) +} + +// typeToExtensionFieldName converts a data-type name into their expected +// Extension ValueX field name. +func typeToExtensionFieldName(name string) protoreflect.Name { + fieldName := toSnakeCase(name) + if fieldName == "string" { + // The protobufs use "string_value" rather than "string" because "string" is + // a keyword. + fieldName = "string_value" + } + return protoreflect.Name(fieldName) +} + +func getExtensionValueX(message proto.Message) protoreflect.FieldDescriptor { + valueX := (*dtpb.Extension_ValueX)(nil) + + reflect := valueX.ProtoReflect() + name := DescriptorName(message) + fieldName := typeToExtensionFieldName(name) + return reflect.Descriptor().Fields().ByName(fieldName) +} + +func newProto(msg protoreflect.ProtoMessage) func() proto.Message { + return func() proto.Message { + return msg.ProtoReflect().New().Interface() + } +} + +func init() { + Resources = make(map[string]*ResourceFieldRefs) + Elements = make(map[string]*ElementFieldRefs) + + for _, msg := range dummyResources { + name := DescriptorName(msg) + fields := &ResourceFieldRefs{} + fields.ContainedResource.Resource = getContainedResourceOneOf(msg) + fields.New = newProto(msg) + Resources[name] = fields + } + for _, msg := range dummyElements { + name := DescriptorName(msg) + fields := &ElementFieldRefs{} + fields.New = newProto(msg) + fields.Extension.ValueX = getExtensionValueX(msg) + Elements[name] = fields + } +} diff --git a/internal/protofields/fields_test.go b/internal/protofields/fields_test.go new file mode 100644 index 0000000..eb24aa4 --- /dev/null +++ b/internal/protofields/fields_test.go @@ -0,0 +1,61 @@ +package protofields_test + +import ( + "testing" + + opb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/protofields" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestUnwrapChoiceField_GetsUnderlyingMessage(t *testing.T) { + dateTime := fhir.DateTimeNow() + + testCases := []struct { + name string + input proto.Message + want proto.Message + }{ + { + name: "gets boolean of Patient deceased field", + input: &ppb.Patient_DeceasedX{ + Choice: &ppb.Patient_DeceasedX_Boolean{ + Boolean: fhir.Boolean(true), + }, + }, + want: fhir.Boolean(true), + }, + { + name: "gets date of Patient deceased field", + input: &ppb.Patient_DeceasedX{ + Choice: &ppb.Patient_DeceasedX_DateTime{ + DateTime: dateTime, + }, + }, + want: dateTime, + }, + { + name: "", + input: &opb.Observation_Component_ValueX{ + Choice: &opb.Observation_Component_ValueX_StringValue{ + StringValue: fhir.String("some string"), + }, + }, + want: fhir.String("some string"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := protofields.UnwrapOneofField(tc.input, "choice") + + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("UnwrapChoiceField returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} diff --git a/internal/protofields/strcase.go b/internal/protofields/strcase.go new file mode 100644 index 0000000..0c145ad --- /dev/null +++ b/internal/protofields/strcase.go @@ -0,0 +1,22 @@ +package protofields + +import ( + "regexp" + "strings" +) + +var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") +var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + +// toSnakeCase is a helper function to convert CamelCase names to snake_case. +// This is needed for finding fields in the Proto descriptors, which are snake_case, +// from resource-names that are CamelCase. +// +// Note: strcase.ToSnake does not work for converting Base64Binary to +// base64_binary, so this function exists to do it for us with the semantics we +// want. +func toSnakeCase(str string) string { + snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") + snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") + return strings.ToLower(snake) +} diff --git a/internal/protofields/update.go b/internal/protofields/update.go new file mode 100644 index 0000000..5288abf --- /dev/null +++ b/internal/protofields/update.go @@ -0,0 +1,61 @@ +package protofields + +import ( + "fmt" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// Overwrite overwrites a field of the given name for the specified message. +// If `values` is empty, the field is cleared. +// If `values` contains more than one entry for a non-repeated field, this panics. +func Overwrite(in proto.Message, fieldName string, values ...proto.Message) { + msg := in.ProtoReflect() + descriptor := msg.Descriptor() + field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) + + // No values -- remove the field entirely + if len(values) == 0 { + msg.Clear(field) + return + } + + // For lists, append each one after clearing the previously stored value + if field.IsList() { + msg.Clear(field) + list := msg.Mutable(field).List() + for _, v := range values { + list.Append(protoreflect.ValueOfMessage(v.ProtoReflect())) + } + return + } + + // For single values on non-repeated fields, just set it. + if len(values) == 1 { + msg.Set(field, protoreflect.ValueOfMessage(values[0].ProtoReflect())) + return + } + + panic( + fmt.Sprintf( + "invalid use of Overwrite; non-repeated field '%v' used with '%v' values", + fieldName, + len(values), + ), + ) +} + +// AppendList updates a field of the given name in-place for the specified message. +// +// This function will panic if the field is not a repeated-field. +func AppendList(in proto.Message, fieldName string, values ...proto.Message) { + msg := in.ProtoReflect() + descriptor := msg.Descriptor() + field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) + + list := msg.Mutable(field).List() + for _, v := range values { + list.Append(protoreflect.ValueOfMessage(v.ProtoReflect())) + } +} diff --git a/internal/protofields/update_test.go b/internal/protofields/update_test.go new file mode 100644 index 0000000..3e96fcb --- /dev/null +++ b/internal/protofields/update_test.go @@ -0,0 +1,138 @@ +package protofields_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/protofields" + "google.golang.org/protobuf/proto" +) + +func TestOverwrite(t *testing.T) { + value := &dtpb.String{ + Value: "hello world", + } + testCases := []struct { + name string + field string + values []proto.Message + input proto.Message + want proto.Message + }{ + { + name: "Solo field", + field: "text", + values: []proto.Message{value}, + input: &dtpb.HumanName{}, + want: &dtpb.HumanName{ + Text: value, + }, + }, { + name: "Solo field no input", + field: "text", + values: []proto.Message{}, + input: &dtpb.HumanName{ + Text: value, + }, + want: &dtpb.HumanName{}, + }, { + name: "Repeated field with single input", + field: "prefix", + values: []proto.Message{value}, + input: &dtpb.HumanName{}, + want: &dtpb.HumanName{ + Prefix: []*dtpb.String{value}, + }, + }, { + name: "Repeated field with multiple inputs", + field: "prefix", + values: []proto.Message{value, value}, + input: &dtpb.HumanName{}, + want: &dtpb.HumanName{ + Prefix: []*dtpb.String{value, value}, + }, + }, { + name: "Repeated field no input", + field: "prefix", + values: []proto.Message{}, + input: &dtpb.HumanName{ + Prefix: []*dtpb.String{value, value}, + }, + want: &dtpb.HumanName{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + protofields.Overwrite(tc.input, tc.field, tc.values...) + + if got, want := tc.input, tc.want; !proto.Equal(got, want) { + t.Errorf("Overwrite(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestOverwrite_WrongCardinality_Panics(t *testing.T) { + defer func() { _ = recover() }() + value := &dtpb.String{ + Value: "hello world", + } + name := &dtpb.HumanName{} + + protofields.Overwrite(name, "text", value, value) + + t.Errorf("Overwrite: expected panic") +} + +func TestAppendList(t *testing.T) { + toAppend := &dtpb.String{ + Value: "hello world", + } + value := &dtpb.String{ + Value: "another string", + } + testCases := []struct { + name string + field string + input proto.Message + want proto.Message + }{ + { + name: "Repeated field with no inputs", + field: "prefix", + input: &dtpb.HumanName{}, + want: &dtpb.HumanName{ + Prefix: []*dtpb.String{toAppend}, + }, + }, { + name: "Repeated field with 1 input", + field: "prefix", + input: &dtpb.HumanName{ + Prefix: []*dtpb.String{value}, + }, + want: &dtpb.HumanName{ + Prefix: []*dtpb.String{value, toAppend}, + }, + }, { + name: "Repeated field with multiple inputs", + field: "prefix", + input: &dtpb.HumanName{ + Prefix: []*dtpb.String{value, value}, + }, + want: &dtpb.HumanName{ + Prefix: []*dtpb.String{value, value, toAppend}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + protofields.AppendList(tc.input, tc.field, toAppend) + + if got, want := tc.input, tc.want; !proto.Equal(got, want) { + t.Errorf("AppendList(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} diff --git a/internal/resource/canonical_identity.go b/internal/resource/canonical_identity.go new file mode 100644 index 0000000..0f6a039 --- /dev/null +++ b/internal/resource/canonical_identity.go @@ -0,0 +1,58 @@ +package resource + +import ( + "errors" + "fmt" + "strings" +) + +var ( + // ErrMissingCanonicalURL is thrown when creating a canonical identity without having a URL. + ErrMissingCanonicalURL = errors.New("missing canonical url") + + delimiter = "/" +) + +// CanonicalIdentity is a canonical representation of a FHIR Resource. +// +// This object stores the individual pieces of id used in creating a canonical reference. +type CanonicalIdentity struct { + Version string + Url string + Fragment string // only used if a fragment of resource is targetted +} + +// Type attempts to identify the resource type associated with the identity. +func (c *CanonicalIdentity) Type() (Type, bool) { + for _, r := range strings.Split(c.Url, delimiter) { + if IsType(r) { + return Type(r), true + } + } + return Type(""), false +} + +// String returns a string representation of this CanonicalIdentity. +func (c *CanonicalIdentity) String() string { + res := c.Url + if c.Version != "" { + res = fmt.Sprintf("%s|%s", res, c.Version) + } + if c.Fragment != "" { + res = fmt.Sprintf("%s#%s", res, c.Fragment) + } + return res +} + +// NewCanonicalIdentity creates a canonicalIdentity based on the given url, version and fragment +func NewCanonicalIdentity(url, version, fragment string) (*CanonicalIdentity, error) { + if url == "" { + return nil, ErrMissingCanonicalURL + } + + return &CanonicalIdentity{ + Url: url, + Version: version, + Fragment: fragment, + }, nil +} diff --git a/internal/resource/canonical_identity_test.go b/internal/resource/canonical_identity_test.go new file mode 100644 index 0000000..57aaf3d --- /dev/null +++ b/internal/resource/canonical_identity_test.go @@ -0,0 +1,88 @@ +package resource_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestCanonicalIdentity_EmptyURL_ReturnsError(t *testing.T) { + _, got := resource.NewCanonicalIdentity("", "v1", "") + + if got != resource.ErrMissingCanonicalURL { + t.Errorf("NewCanonicalIdentity: got %v, want %v", got, resource.ErrMissingCanonicalURL) + } +} + +func TestCanonicalIdentity(t *testing.T) { + testCases := []struct { + name, url, version, fragment string + want *resource.CanonicalIdentity + wantString string + wantType resource.Type + hasType bool + }{ + { + name: "basic", + url: "http://someurl/test-value", + wantString: "http://someurl/test-value", + }, + { + name: "long url", + url: "https://fhir.acme.com/Questionnaire/example", + wantString: "https://fhir.acme.com/Questionnaire/example", + hasType: true, + wantType: resource.Questionnaire, + }, + { + name: "with version", + url: "https://fhir.acme.com/PlanDefinition/example", + version: "1.0.0", + wantString: "https://fhir.acme.com/PlanDefinition/example|1.0.0", + hasType: true, + wantType: resource.PlanDefinition, + }, + { + name: "with fragment", + url: "http://hl7.org/fhir/ValueSet/my-valueset", + fragment: "vs1", + wantString: "http://hl7.org/fhir/ValueSet/my-valueset#vs1", + }, + { + name: "with version and fragment", + url: "http://fhir.acme.com/ActivityDefinition/example", + version: "1.0", + fragment: "vs1", + wantString: "http://fhir.acme.com/ActivityDefinition/example|1.0#vs1", + hasType: true, + wantType: resource.ActivityDefinition, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, _ := resource.NewCanonicalIdentity(tc.url, tc.version, tc.fragment) + want := &resource.CanonicalIdentity{ + Url: tc.url, + Version: tc.version, + Fragment: tc.fragment, + } + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("CanonicalIdentity(%s): %v", tc.name, diff) + } + + if s := got.String(); tc.wantString != s { + t.Errorf("CanonicalIdentity(%s).String: want: %s, got: %s", tc.name, tc.wantString, s) + } + if tc.hasType { + gt, ok := got.Type() + if !ok || gt != tc.wantType { + t.Errorf("CanonicalIdentity(%s).Type: want: %s, got: %s", tc.name, tc.wantType, gt) + } + } + }) + } +} diff --git a/internal/resource/consts.go b/internal/resource/consts.go new file mode 100644 index 0000000..7e4ee9c --- /dev/null +++ b/internal/resource/consts.go @@ -0,0 +1,441 @@ +package resource + +const ( + // Account is the TypeSpecifier constant for the "Account" resource. + Account Type = "Account" + + // ActivityDefinition is the TypeSpecifier constant for the "ActivityDefinition" resource. + ActivityDefinition Type = "ActivityDefinition" + + // AdverseEvent is the TypeSpecifier constant for the "AdverseEvent" resource. + AdverseEvent Type = "AdverseEvent" + + // AllergyIntolerance is the TypeSpecifier constant for the "AllergyIntolerance" resource. + AllergyIntolerance Type = "AllergyIntolerance" + + // Appointment is the TypeSpecifier constant for the "Appointment" resource. + Appointment Type = "Appointment" + + // AppointmentResponse is the TypeSpecifier constant for the "AppointmentResponse" resource. + AppointmentResponse Type = "AppointmentResponse" + + // AuditEvent is the TypeSpecifier constant for the "AuditEvent" resource. + AuditEvent Type = "AuditEvent" + + // Basic is the TypeSpecifier constant for the "Basic" resource. + Basic Type = "Basic" + + // Binary is the TypeSpecifier constant for the "Binary" resource. + Binary Type = "Binary" + + // BiologicallyDerivedProduct is the TypeSpecifier constant for the "BiologicallyDerivedProduct" resource. + BiologicallyDerivedProduct Type = "BiologicallyDerivedProduct" + + // BodyStructure is the TypeSpecifier constant for the "BodyStructure" resource. + BodyStructure Type = "BodyStructure" + + // Bundle is the TypeSpecifier constant for the "Bundle" resource. + Bundle Type = "Bundle" + + // CapabilityStatement is the TypeSpecifier constant for the "CapabilityStatement" resource. + CapabilityStatement Type = "CapabilityStatement" + + // CarePlan is the TypeSpecifier constant for the "CarePlan" resource. + CarePlan Type = "CarePlan" + + // CareTeam is the TypeSpecifier constant for the "CareTeam" resource. + CareTeam Type = "CareTeam" + + // CatalogEntry is the TypeSpecifier constant for the "CatalogEntry" resource. + CatalogEntry Type = "CatalogEntry" + + // ChargeItem is the TypeSpecifier constant for the "ChargeItem" resource. + ChargeItem Type = "ChargeItem" + + // ChargeItemDefinition is the TypeSpecifier constant for the "ChargeItemDefinition" resource. + ChargeItemDefinition Type = "ChargeItemDefinition" + + // Claim is the TypeSpecifier constant for the "Claim" resource. + Claim Type = "Claim" + + // ClaimResponse is the TypeSpecifier constant for the "ClaimResponse" resource. + ClaimResponse Type = "ClaimResponse" + + // ClinicalImpression is the TypeSpecifier constant for the "ClinicalImpression" resource. + ClinicalImpression Type = "ClinicalImpression" + + // CodeSystem is the TypeSpecifier constant for the "CodeSystem" resource. + CodeSystem Type = "CodeSystem" + + // Communication is the TypeSpecifier constant for the "Communication" resource. + Communication Type = "Communication" + + // CommunicationRequest is the TypeSpecifier constant for the "CommunicationRequest" resource. + CommunicationRequest Type = "CommunicationRequest" + + // CompartmentDefinition is the TypeSpecifier constant for the "CompartmentDefinition" resource. + CompartmentDefinition Type = "CompartmentDefinition" + + // Composition is the TypeSpecifier constant for the "Composition" resource. + Composition Type = "Composition" + + // ConceptMap is the TypeSpecifier constant for the "ConceptMap" resource. + ConceptMap Type = "ConceptMap" + + // Condition is the TypeSpecifier constant for the "Condition" resource. + Condition Type = "Condition" + + // Consent is the TypeSpecifier constant for the "Consent" resource. + Consent Type = "Consent" + + // Contract is the TypeSpecifier constant for the "Contract" resource. + Contract Type = "Contract" + + // Coverage is the TypeSpecifier constant for the "Coverage" resource. + Coverage Type = "Coverage" + + // CoverageEligibilityRequest is the TypeSpecifier constant for the "CoverageEligibilityRequest" resource. + CoverageEligibilityRequest Type = "CoverageEligibilityRequest" + + // CoverageEligibilityResponse is the TypeSpecifier constant for the "CoverageEligibilityResponse" resource. + CoverageEligibilityResponse Type = "CoverageEligibilityResponse" + + // DetectedIssue is the TypeSpecifier constant for the "DetectedIssue" resource. + DetectedIssue Type = "DetectedIssue" + + // Device is the TypeSpecifier constant for the "Device" resource. + Device Type = "Device" + + // DeviceDefinition is the TypeSpecifier constant for the "DeviceDefinition" resource. + DeviceDefinition Type = "DeviceDefinition" + + // DeviceMetric is the TypeSpecifier constant for the "DeviceMetric" resource. + DeviceMetric Type = "DeviceMetric" + + // DeviceRequest is the TypeSpecifier constant for the "DeviceRequest" resource. + DeviceRequest Type = "DeviceRequest" + + // DeviceUseStatement is the TypeSpecifier constant for the "DeviceUseStatement" resource. + DeviceUseStatement Type = "DeviceUseStatement" + + // DiagnosticReport is the TypeSpecifier constant for the "DiagnosticReport" resource. + DiagnosticReport Type = "DiagnosticReport" + + // DocumentManifest is the TypeSpecifier constant for the "DocumentManifest" resource. + DocumentManifest Type = "DocumentManifest" + + // DocumentReference is the TypeSpecifier constant for the "DocumentReference" resource. + DocumentReference Type = "DocumentReference" + + // EffectEvidenceSynthesis is the TypeSpecifier constant for the "EffectEvidenceSynthesis" resource. + EffectEvidenceSynthesis Type = "EffectEvidenceSynthesis" + + // Encounter is the TypeSpecifier constant for the "Encounter" resource. + Encounter Type = "Encounter" + + // Endpoint is the TypeSpecifier constant for the "Endpoint" resource. + Endpoint Type = "Endpoint" + + // EnrollmentRequest is the TypeSpecifier constant for the "EnrollmentRequest" resource. + EnrollmentRequest Type = "EnrollmentRequest" + + // EnrollmentResponse is the TypeSpecifier constant for the "EnrollmentResponse" resource. + EnrollmentResponse Type = "EnrollmentResponse" + + // EpisodeOfCare is the TypeSpecifier constant for the "EpisodeOfCare" resource. + EpisodeOfCare Type = "EpisodeOfCare" + + // EventDefinition is the TypeSpecifier constant for the "EventDefinition" resource. + EventDefinition Type = "EventDefinition" + + // Evidence is the TypeSpecifier constant for the "Evidence" resource. + Evidence Type = "Evidence" + + // EvidenceVariable is the TypeSpecifier constant for the "EvidenceVariable" resource. + EvidenceVariable Type = "EvidenceVariable" + + // ExampleScenario is the TypeSpecifier constant for the "ExampleScenario" resource. + ExampleScenario Type = "ExampleScenario" + + // ExplanationOfBenefit is the TypeSpecifier constant for the "ExplanationOfBenefit" resource. + ExplanationOfBenefit Type = "ExplanationOfBenefit" + + // FamilyMemberHistory is the TypeSpecifier constant for the "FamilyMemberHistory" resource. + FamilyMemberHistory Type = "FamilyMemberHistory" + + // Flag is the TypeSpecifier constant for the "Flag" resource. + Flag Type = "Flag" + + // Goal is the TypeSpecifier constant for the "Goal" resource. + Goal Type = "Goal" + + // GraphDefinition is the TypeSpecifier constant for the "GraphDefinition" resource. + GraphDefinition Type = "GraphDefinition" + + // Group is the TypeSpecifier constant for the "Group" resource. + Group Type = "Group" + + // GuidanceResponse is the TypeSpecifier constant for the "GuidanceResponse" resource. + GuidanceResponse Type = "GuidanceResponse" + + // HealthcareService is the TypeSpecifier constant for the "HealthcareService" resource. + HealthcareService Type = "HealthcareService" + + // ImagingStudy is the TypeSpecifier constant for the "ImagingStudy" resource. + ImagingStudy Type = "ImagingStudy" + + // Immunization is the TypeSpecifier constant for the "Immunization" resource. + Immunization Type = "Immunization" + + // ImmunizationEvaluation is the TypeSpecifier constant for the "ImmunizationEvaluation" resource. + ImmunizationEvaluation Type = "ImmunizationEvaluation" + + // ImmunizationRecommendation is the TypeSpecifier constant for the "ImmunizationRecommendation" resource. + ImmunizationRecommendation Type = "ImmunizationRecommendation" + + // ImplementationGuide is the TypeSpecifier constant for the "ImplementationGuide" resource. + ImplementationGuide Type = "ImplementationGuide" + + // InsurancePlan is the TypeSpecifier constant for the "InsurancePlan" resource. + InsurancePlan Type = "InsurancePlan" + + // Invoice is the TypeSpecifier constant for the "Invoice" resource. + Invoice Type = "Invoice" + + // Library is the TypeSpecifier constant for the "Library" resource. + Library Type = "Library" + + // Linkage is the TypeSpecifier constant for the "Linkage" resource. + Linkage Type = "Linkage" + + // List is the TypeSpecifier constant for the "List" resource. + List Type = "List" + + // Location is the TypeSpecifier constant for the "Location" resource. + Location Type = "Location" + + // Measure is the TypeSpecifier constant for the "Measure" resource. + Measure Type = "Measure" + + // MeasureReport is the TypeSpecifier constant for the "MeasureReport" resource. + MeasureReport Type = "MeasureReport" + + // Media is the TypeSpecifier constant for the "Media" resource. + Media Type = "Media" + + // Medication is the TypeSpecifier constant for the "Medication" resource. + Medication Type = "Medication" + + // MedicationAdministration is the TypeSpecifier constant for the "MedicationAdministration" resource. + MedicationAdministration Type = "MedicationAdministration" + + // MedicationDispense is the TypeSpecifier constant for the "MedicationDispense" resource. + MedicationDispense Type = "MedicationDispense" + + // MedicationKnowledge is the TypeSpecifier constant for the "MedicationKnowledge" resource. + MedicationKnowledge Type = "MedicationKnowledge" + + // MedicationRequest is the TypeSpecifier constant for the "MedicationRequest" resource. + MedicationRequest Type = "MedicationRequest" + + // MedicationStatement is the TypeSpecifier constant for the "MedicationStatement" resource. + MedicationStatement Type = "MedicationStatement" + + // MedicinalProduct is the TypeSpecifier constant for the "MedicinalProduct" resource. + MedicinalProduct Type = "MedicinalProduct" + + // MedicinalProductAuthorization is the TypeSpecifier constant for the "MedicinalProductAuthorization" resource. + MedicinalProductAuthorization Type = "MedicinalProductAuthorization" + + // MedicinalProductContraindication is the TypeSpecifier constant for the "MedicinalProductContraindication" resource. + MedicinalProductContraindication Type = "MedicinalProductContraindication" + + // MedicinalProductIndication is the TypeSpecifier constant for the "MedicinalProductIndication" resource. + MedicinalProductIndication Type = "MedicinalProductIndication" + + // MedicinalProductIngredient is the TypeSpecifier constant for the "MedicinalProductIngredient" resource. + MedicinalProductIngredient Type = "MedicinalProductIngredient" + + // MedicinalProductInteraction is the TypeSpecifier constant for the "MedicinalProductInteraction" resource. + MedicinalProductInteraction Type = "MedicinalProductInteraction" + + // MedicinalProductManufactured is the TypeSpecifier constant for the "MedicinalProductManufactured" resource. + MedicinalProductManufactured Type = "MedicinalProductManufactured" + + // MedicinalProductPackaged is the TypeSpecifier constant for the "MedicinalProductPackaged" resource. + MedicinalProductPackaged Type = "MedicinalProductPackaged" + + // MedicinalProductPharmaceutical is the TypeSpecifier constant for the "MedicinalProductPharmaceutical" resource. + MedicinalProductPharmaceutical Type = "MedicinalProductPharmaceutical" + + // MedicinalProductUndesirableEffect is the TypeSpecifier constant for the "MedicinalProductUndesirableEffect" resource. + MedicinalProductUndesirableEffect Type = "MedicinalProductUndesirableEffect" + + // MessageDefinition is the TypeSpecifier constant for the "MessageDefinition" resource. + MessageDefinition Type = "MessageDefinition" + + // MessageHeader is the TypeSpecifier constant for the "MessageHeader" resource. + MessageHeader Type = "MessageHeader" + + // MolecularSequence is the TypeSpecifier constant for the "MolecularSequence" resource. + MolecularSequence Type = "MolecularSequence" + + // NamingSystem is the TypeSpecifier constant for the "NamingSystem" resource. + NamingSystem Type = "NamingSystem" + + // NutritionOrder is the TypeSpecifier constant for the "NutritionOrder" resource. + NutritionOrder Type = "NutritionOrder" + + // Observation is the TypeSpecifier constant for the "Observation" resource. + Observation Type = "Observation" + + // ObservationDefinition is the TypeSpecifier constant for the "ObservationDefinition" resource. + ObservationDefinition Type = "ObservationDefinition" + + // OperationDefinition is the TypeSpecifier constant for the "OperationDefinition" resource. + OperationDefinition Type = "OperationDefinition" + + // OperationOutcome is the TypeSpecifier constant for the "OperationOutcome" resource. + OperationOutcome Type = "OperationOutcome" + + // Organization is the TypeSpecifier constant for the "Organization" resource. + Organization Type = "Organization" + + // OrganizationAffiliation is the TypeSpecifier constant for the "OrganizationAffiliation" resource. + OrganizationAffiliation Type = "OrganizationAffiliation" + + // Parameters is the TypeSpecifier constant for the "Parameters" resource. + Parameters Type = "Parameters" + + // Patient is the TypeSpecifier constant for the "Patient" resource. + Patient Type = "Patient" + + // PaymentNotice is the TypeSpecifier constant for the "PaymentNotice" resource. + PaymentNotice Type = "PaymentNotice" + + // PaymentReconciliation is the TypeSpecifier constant for the "PaymentReconciliation" resource. + PaymentReconciliation Type = "PaymentReconciliation" + + // Person is the TypeSpecifier constant for the "Person" resource. + Person Type = "Person" + + // PlanDefinition is the TypeSpecifier constant for the "PlanDefinition" resource. + PlanDefinition Type = "PlanDefinition" + + // Practitioner is the TypeSpecifier constant for the "Practitioner" resource. + Practitioner Type = "Practitioner" + + // PractitionerRole is the TypeSpecifier constant for the "PractitionerRole" resource. + PractitionerRole Type = "PractitionerRole" + + // Procedure is the TypeSpecifier constant for the "Procedure" resource. + Procedure Type = "Procedure" + + // Provenance is the TypeSpecifier constant for the "Provenance" resource. + Provenance Type = "Provenance" + + // Questionnaire is the TypeSpecifier constant for the "Questionnaire" resource. + Questionnaire Type = "Questionnaire" + + // QuestionnaireResponse is the TypeSpecifier constant for the "QuestionnaireResponse" resource. + QuestionnaireResponse Type = "QuestionnaireResponse" + + // RelatedPerson is the TypeSpecifier constant for the "RelatedPerson" resource. + RelatedPerson Type = "RelatedPerson" + + // RequestGroup is the TypeSpecifier constant for the "RequestGroup" resource. + RequestGroup Type = "RequestGroup" + + // ResearchDefinition is the TypeSpecifier constant for the "ResearchDefinition" resource. + ResearchDefinition Type = "ResearchDefinition" + + // ResearchElementDefinition is the TypeSpecifier constant for the "ResearchElementDefinition" resource. + ResearchElementDefinition Type = "ResearchElementDefinition" + + // ResearchStudy is the TypeSpecifier constant for the "ResearchStudy" resource. + ResearchStudy Type = "ResearchStudy" + + // ResearchSubject is the TypeSpecifier constant for the "ResearchSubject" resource. + ResearchSubject Type = "ResearchSubject" + + // RiskAssessment is the TypeSpecifier constant for the "RiskAssessment" resource. + RiskAssessment Type = "RiskAssessment" + + // RiskEvidenceSynthesis is the TypeSpecifier constant for the "RiskEvidenceSynthesis" resource. + RiskEvidenceSynthesis Type = "RiskEvidenceSynthesis" + + // Schedule is the TypeSpecifier constant for the "Schedule" resource. + Schedule Type = "Schedule" + + // SearchParameter is the TypeSpecifier constant for the "SearchParameter" resource. + SearchParameter Type = "SearchParameter" + + // ServiceRequest is the TypeSpecifier constant for the "ServiceRequest" resource. + ServiceRequest Type = "ServiceRequest" + + // Slot is the TypeSpecifier constant for the "Slot" resource. + Slot Type = "Slot" + + // Specimen is the TypeSpecifier constant for the "Specimen" resource. + Specimen Type = "Specimen" + + // SpecimenDefinition is the TypeSpecifier constant for the "SpecimenDefinition" resource. + SpecimenDefinition Type = "SpecimenDefinition" + + // StructureDefinition is the TypeSpecifier constant for the "StructureDefinition" resource. + StructureDefinition Type = "StructureDefinition" + + // StructureMap is the TypeSpecifier constant for the "StructureMap" resource. + StructureMap Type = "StructureMap" + + // Subscription is the TypeSpecifier constant for the "Subscription" resource. + Subscription Type = "Subscription" + + // Substance is the TypeSpecifier constant for the "Substance" resource. + Substance Type = "Substance" + + // SubstanceNucleicAcid is the TypeSpecifier constant for the "SubstanceNucleicAcid" resource. + SubstanceNucleicAcid Type = "SubstanceNucleicAcid" + + // SubstancePolymer is the TypeSpecifier constant for the "SubstancePolymer" resource. + SubstancePolymer Type = "SubstancePolymer" + + // SubstanceProtein is the TypeSpecifier constant for the "SubstanceProtein" resource. + SubstanceProtein Type = "SubstanceProtein" + + // SubstanceReferenceInformation is the TypeSpecifier constant for the "SubstanceReferenceInformation" resource. + SubstanceReferenceInformation Type = "SubstanceReferenceInformation" + + // SubstanceSourceMaterial is the TypeSpecifier constant for the "SubstanceSourceMaterial" resource. + SubstanceSourceMaterial Type = "SubstanceSourceMaterial" + + // SubstanceSpecification is the TypeSpecifier constant for the "SubstanceSpecification" resource. + SubstanceSpecification Type = "SubstanceSpecification" + + // SupplyDelivery is the TypeSpecifier constant for the "SupplyDelivery" resource. + SupplyDelivery Type = "SupplyDelivery" + + // SupplyRequest is the TypeSpecifier constant for the "SupplyRequest" resource. + SupplyRequest Type = "SupplyRequest" + + // Task is the TypeSpecifier constant for the "Task" resource. + Task Type = "Task" + + // TerminologyCapabilities is the TypeSpecifier constant for the "TerminologyCapabilities" resource. + TerminologyCapabilities Type = "TerminologyCapabilities" + + // TestReport is the TypeSpecifier constant for the "TestReport" resource. + TestReport Type = "TestReport" + + // TestScript is the TypeSpecifier constant for the "TestScript" resource. + TestScript Type = "TestScript" + + // ValueSet is the TypeSpecifier constant for the "ValueSet" resource. + ValueSet Type = "ValueSet" + + // VerificationResult is the TypeSpecifier constant for the "VerificationResult" resource. + VerificationResult Type = "VerificationResult" + + // VisionPrescription is the TypeSpecifier constant for the "VisionPrescription" resource. + VisionPrescription Type = "VisionPrescription" +) diff --git a/internal/resource/identity.go b/internal/resource/identity.go new file mode 100644 index 0000000..ea2de53 --- /dev/null +++ b/internal/resource/identity.go @@ -0,0 +1,187 @@ +package resource + +import ( + "fmt" + "regexp" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + + "github.com/verily-src/fhirpath-go/internal/fhir" +) + +const ( + resourceTypePattern = "[A-Za-z]+" + idPattern = "[0-9A-Za-z.-]{1,64}" // https://www.hl7.org/fhir/R4/datatypes.html#id + resourceIDPattern = idPattern // https://www.hl7.org/fhir/R4/resource.html#resource + versionIDPattern = idPattern // https://www.hl7.org/fhir/R4/resource.html#Meta +) + +var ( + historyURLRegexp = regexp.MustCompile(fmt.Sprintf("^.*/(%s)/(%s)/_history/(%s)$", resourceTypePattern, resourceIDPattern, versionIDPattern)) +) + +// Identity is a representation of a FHIR Resource's temporal instance. +// +// This is similar to a FHIR Reference, except without any explicit sementics of +// a referential relationship. Rather, this object simply acts as a carrier for +// the data that may be used for general identity purposes, such as logging. +type Identity struct { + typeName Type + id string + version string +} + +// Equal implements equality comparison between identity instances. +// +// The Equal() method, when used by the "cmp" package MUST +// support nils: see "...even if x or y is nil" from second bullet +// in https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal. +func (i *Identity) Equal(other *Identity) bool { + if i == other { + return true + } + if i == nil || other == nil { + return false + } + return i.typeName == other.typeName && i.id == other.id && i.version == other.version +} + +// Type returns the resource Identity's underlying type. This is guaranteed to +// always be a valid resource type. +func (i *Identity) Type() Type { + return i.typeName +} + +// ID returns the resource's ID. +func (i *Identity) ID() string { + return i.id +} + +// VersionID returns the explicit version of the resource, if it is known. +func (i *Identity) VersionID() (string, bool) { + return i.version, i.version != "" +} + +// RelativeURI returns a relative URI of this resource. +func (i *Identity) RelativeURI() *dtpb.Uri { + return fhir.URI(fmt.Sprintf("%v/%v", i.typeName, i.id)) +} + +// RelativeURIString returns a string representation of the RelativeURI for +// convenience. +func (i *Identity) RelativeURIString() string { + return i.RelativeURI().GetValue() +} + +// RelativeVersionedURI returns a relative URI of this resource including the +// version identifier, if it is known. +func (i *Identity) RelativeVersionedURI() (*dtpb.Uri, bool) { + if i.version == "" { + return nil, false + } + return fhir.URI(fmt.Sprintf("%v/%v/_history/%v", i.typeName, i.id, i.version)), true +} + +// RelativeVersionedURIString returns a string representation of the RelativeVersionedURI +// for convenience. +func (i *Identity) RelativeVersionedURIString() (string, bool) { + val, ok := i.RelativeVersionedURI() + if ok { + return val.GetValue(), true + } + return "", false +} + +// PreferRelativeVersionURI returns the relative version URI if available, +// otherwise the relative URI only. +func (i *Identity) PreferRelativeVersionedURI() *dtpb.Uri { + if uri, ok := i.RelativeVersionedURI(); ok { + return uri + } + return i.RelativeURI() +} + +// PreferRelativeVersionedURIString returns the relative version URI string if +// available, otherwise the relative URI only. +func (i *Identity) PreferRelativeVersionedURIString() string { + if uri, ok := i.RelativeVersionedURIString(); ok { + return uri + } + return i.RelativeURIString() +} + +// String returns a string representation of this Identity. +// +// The exact representation should not be relied on for any practical purpose; +// the only thing that is guaranteed is that for the unique triple of data +// containing (type, id, version), the String will contain these details -- but +// the exact form is unspecified. +func (i *Identity) String() string { + if i.version == "" { + return fmt.Sprintf("%v/%v", i.typeName, i.id) + } + return fmt.Sprintf("%v/%v/_history/%v", i.typeName, i.id, i.version) +} + +// Unversioned returns a new Identity that does not have a VersionID. +func (i *Identity) Unversioned() *Identity { + return &Identity{ + typeName: i.typeName, + id: i.id, + } +} + +// WithNewVersion returns a new Identity that has the specified VersionID. +func (i *Identity) WithNewVersion(versionID string) *Identity { + return &Identity{ + typeName: i.typeName, + id: i.id, + version: versionID, + } +} + +// NewIdentity attempts to create a new Identity object from a runtime-provided +// string resourceType name, and its id/versionID. If the provided resourceType +// does not name a valid resource-type (case-sensitive), this function will +// return an ErrBadType error. +func NewIdentity(resourceType, id, versionID string) (*Identity, error) { + name, err := NewType(resourceType) + if err != nil { + return nil, err + } + + return &Identity{ + typeName: name, + id: id, + version: versionID, + }, nil +} + +// NewIdentityFromHistoryURL attempts to create a new Identity object from a +// runtime-provided history URL. +// +// Input: [FHIR Proxy/Store URL]/fhir/[resourceType]/[resourceId]/_history/[versionId] +// Output: [resourceID] +func NewIdentityFromHistoryURL(url string) (*Identity, error) { + matches := historyURLRegexp.FindStringSubmatch(url) + if len(matches) != 4 { + return nil, fmt.Errorf("error parsing history URL: %s", url) + } + return NewIdentity(matches[1], matches[2], matches[3]) +} + +// IdentityOf attempts to form a resource Identity object to the named +// resource. If the specified resource is either nil, or does not contain an +// ID value, no resource identity will be formed and this function will return +// nil. +func IdentityOf(resource fhir.Resource) (*Identity, bool) { + if resource == nil || resource.GetId() == nil { + return nil, false + } + + return &Identity{ + typeName: TypeOf(resource), + id: resource.GetId().GetValue(), + version: resource.GetMeta().GetVersionId().GetValue(), + }, true +} diff --git a/internal/resource/identity_test.go b/internal/resource/identity_test.go new file mode 100644 index 0000000..06ce42f --- /dev/null +++ b/internal/resource/identity_test.go @@ -0,0 +1,137 @@ +package resource_test + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +func TestIdentityOf_NilInputs_ReturnsNoValue(t *testing.T) { + _, got := resource.IdentityOf(nil) + + if got, want := got, false; got != want { + t.Errorf("IdentityOf: got %v, want %v", got, want) + } +} + +func TestIdentityOf(t *testing.T) { + for name, res := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + want, err := resource.NewIdentity( + string(resource.TypeOf(res)), + res.GetId().GetValue(), + res.GetMeta().GetVersionId().GetValue(), + ) + if err != nil { + t.Fatalf("IdentityOf(%v): got unexpected err: %v", name, err) + } + + got, ok := resource.IdentityOf(res) + if !ok { + t.Fatalf("IdentityOf(%v): got false for ok", name) + } + + if !cmp.Equal(got, want) { + t.Errorf("IdentityOf(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestNewIdentity_BadInput_ReturnsErrBadType(t *testing.T) { + _, err := resource.NewIdentity("", "1234", "5678") + + if got, want := err, resource.ErrBadType; !errors.Is(got, want) { + t.Errorf("NewIdentity: got err '%v', want err '%v'", got, want) + } +} + +func TestNewIdentityFromHistoryURL(t *testing.T) { + testCases := []struct { + name string + historyUrl string + expectedValue *resource.Identity + }{ + { + "URL", + "https://healthcare.googleapis.com/v1/projects/my-project-name/locations/us-east4/datasets/my-dataset-name/fhirStores/my-fhir-store-name/fhir/Binary/123/_history/456", + mustNewIdentity("Binary", "123", "456"), + }, + { + "URI", + "projects/my-project-name/locations/us-east4/datasets/my-dataset-name/fhirStores/my-fhir-store-name/fhir/Patient/abc/_history/def", + mustNewIdentity("Patient", "abc", "def"), + }, + { + "Invalid", + "ThisIsNotAValidResourceName", + nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, _ := resource.NewIdentityFromHistoryURL(tc.historyUrl) + if ((result != nil) != (tc.expectedValue != nil)) || + (result != nil && tc.expectedValue != nil && *result != *tc.expectedValue) { + t.Errorf("%s: Got = %v, want = %v", tc.name, result, tc.expectedValue) + } + }) + } +} + +func TestIdentityEqual(t *testing.T) { + identityA := mustNewIdentity("Patient", "A", "v1") + testCases := []struct { + name string + lhs *resource.Identity + rhs *resource.Identity + wantEqual bool + }{ + {"both nil", nil, nil, true}, + {"lhs nil", nil, identityA, false}, + {"rhs nil", identityA, nil, false}, + {"same", identityA, mustNewIdentity("Patient", "A", "v1"), true}, + {"different type", identityA, mustNewIdentity("Person", "A", "v1"), false}, + {"different id", identityA, mustNewIdentity("Patient", "B", "v1"), false}, + {"different version", identityA, mustNewIdentity("Patient", "A", "v2"), false}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotEqual := tc.lhs.Equal(tc.rhs) + if gotEqual != tc.wantEqual { + t.Errorf("Equal(%s) got %v want %v", tc.name, gotEqual, tc.wantEqual) + } + }) + } +} + +func TestIdentity_Unversioned(t *testing.T) { + withVersion := mustNewIdentity("Patient", "123", "abc") + got := withVersion.Unversioned() + want := mustNewIdentity("Patient", "123", "") + if !cmp.Equal(got, want) { + t.Errorf("Unversioned: got %v, want %v", got, want) + } +} + +func mustNewIdentity(resourceType, id, versionID string) *resource.Identity { + identity, err := resource.NewIdentity(resourceType, id, versionID) + if err != nil { + panic(err) + } + return identity +} + +func TestWithNewVersion(t *testing.T) { + originalIdentity := mustNewIdentity("Patient", "foo", "") + wantIdentity := mustNewIdentity("Patient", "foo", "bar") + + gotIdentity := originalIdentity.WithNewVersion("bar") + + if !cmp.Equal(gotIdentity, wantIdentity) { + t.Errorf("WithNewVersion: got %v, want %v", gotIdentity, wantIdentity) + } +} diff --git a/internal/resource/options.go b/internal/resource/options.go new file mode 100644 index 0000000..32911ad --- /dev/null +++ b/internal/resource/options.go @@ -0,0 +1,31 @@ +package resource + +import ( + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resourceopt" +) + +// WithMeta returns a resource Option for setting the Resource Meta with the +// specified meta entry. +func WithMeta(meta *dtpb.Meta) Option { + return resourceopt.WithProtoField("meta", meta) +} + +// WithID returns a resource Option for setting the Resourec ID with the id of +// the provided string. +func WithID(id string) Option { + return resourceopt.WithProtoField("id", fhir.ID(id)) +} + +// WithImplicitRules returns a resource Option for setting the Resource implicit +// rules with the provided string. +func WithImplicitRules(rules string) Option { + return resourceopt.WithProtoField("implicit_rules", fhir.URI(rules)) +} + +// WithLanguage returns a resource Option for setting the Resource language to +// the code of the provided string. +func WithLanguage(language string) Option { + return resourceopt.WithProtoField("language", fhir.Code(language)) +} diff --git a/internal/resource/options_test.go b/internal/resource/options_test.go new file mode 100644 index 0000000..569567f --- /dev/null +++ b/internal/resource/options_test.go @@ -0,0 +1,82 @@ +package resource_test + +import ( + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/proto" +) + +func TestWithMeta(t *testing.T) { + want := &dtpb.Meta{ + VersionId: &dtpb.Id{ + Value: "deadbeef", + }, + } + + for name, res := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + got := proto.Clone(res).(fhir.Resource) + + resource.Update(got, resource.WithMeta(want)) + + if got, want := got.GetMeta(), want; !proto.Equal(got, want) { + t.Errorf("WithMeta(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestWithID(t *testing.T) { + const id = "123456789" + want := fhir.ID(id) + + for name, res := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + got := proto.Clone(res).(fhir.Resource) + + resource.Update(got, resource.WithID(id)) + + if got, want := got.GetId(), want; !proto.Equal(got, want) { + t.Errorf("WithID(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestWithImplicitRules(t *testing.T) { + const rules = "https://example.com/some/rules" + want := fhir.URI(rules) + + for name, res := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + got := proto.Clone(res).(fhir.Resource) + + resource.Update(got, resource.WithImplicitRules(rules)) + + if got, want := got.GetImplicitRules(), want; !proto.Equal(got, want) { + t.Errorf("WithImplicitRules(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestWithLanguage(t *testing.T) { + const language = "en-gb" + want := fhir.Code(language) + + for name, res := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + got := proto.Clone(res).(fhir.Resource) + + resource.Update(got, resource.WithLanguage(language)) + + if got, want := got.GetLanguage(), want; !proto.Equal(got, want) { + t.Errorf("WithLanguage(%v): got %v, want %v", name, got, want) + } + }) + } +} diff --git a/internal/resource/patient/patient.go b/internal/resource/patient/patient.go new file mode 100644 index 0000000..31b6121 --- /dev/null +++ b/internal/resource/patient/patient.go @@ -0,0 +1,172 @@ +package patient + +import ( + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + appb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_go_proto" + arpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_response_go_proto" + cppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/care_plan_go_proto" + clpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/claim_go_proto" + commpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/communication_go_proto" + crpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/communication_request_go_proto" + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/condition_go_proto" + cerpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/coverage_eligibility_request_go_proto" + dpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_go_proto" + drpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_request_go_proto" + epb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/encounter_go_proto" + erpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/enrollment_request_go_proto" + eobpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/explanation_of_benefit_go_proto" + irpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/immunization_recommendation_go_proto" + lpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/list_go_proto" + mrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_request_go_proto" + nopb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/nutrition_order_go_proto" + opb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + procpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/procedure_go_proto" + qrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_response_go_proto" + rppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/related_person_go_proto" + rgpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/request_group_go_proto" + rspb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_subject_go_proto" + rapb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/risk_assessment_go_proto" + srpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/service_request_go_proto" + surpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/supply_request_go_proto" + tpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/task_go_proto" + vppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/vision_prescription_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +var ( + ErrExtractingPatientID = errors.New("extracting patient id") + ErrUnsupportedType = errors.New("extraction unsupported for type") +) + +// IDFromResource returns the patient's ID from a FHIR Resource. +// This function provides a mapping from an input resource to the patient ID it references. +func IDFromResource(res fhir.Resource) (string, error) { + idOrError := func(patientRef *dtpb.Reference) (string, error) { + if id := patientRef.GetPatientId().GetValue(); id != "" { + return id, nil + } + return "", fmt.Errorf("%w from %T", ErrExtractingPatientID, res) + } + + switch res := res.(type) { + + //--------------------------------------------------------------------------- + // Unpatterned Resources + //--------------------------------------------------------------------------- + + case *ppb.Patient: + if id := res.GetId().GetValue(); id != "" { + return id, nil + } + return "", fmt.Errorf("%w from %T", ErrExtractingPatientID, res) + case *epb.Encounter: + return idOrError(res.GetSubject()) + case *dpb.Device: + return idOrError(res.GetPatient()) + case *eobpb.ExplanationOfBenefit: + return idOrError(res.GetPatient()) + case *rspb.ResearchSubject: + return idOrError(res.GetIndividual()) + case *rppb.RelatedPerson: + return idOrError(res.GetPatient()) + case *lpb.List: + return idOrError(res.GetSubject()) + + //--------------------------------------------------------------------------- + // Event Pattern Resources + //--------------------------------------------------------------------------- + + case *qrpb.QuestionnaireResponse: + return idOrError(res.GetSubject()) + case *rapb.RiskAssessment: + return idOrError(res.GetSubject()) + case *cpb.Condition: + return idOrError(res.GetSubject()) + case *procpb.Procedure: + return idOrError(res.GetSubject()) + case *opb.Observation: + return idOrError(res.GetSubject()) + case *tpb.Task: + // TODO(b/254654059): Remove usage of Task.for once decision engine supports event-patient extraction override. + if id := res.GetFocus().GetPatientId().GetValue(); id != "" { + return id, nil + } else if id := res.GetForValue().GetPatientId().GetValue(); id != "" { + return id, nil + } + return "", fmt.Errorf("%w from %T", ErrExtractingPatientID, res) + case *commpb.Communication: + return idOrError(res.GetSubject()) + + //--------------------------------------------------------------------------- + // Request Pattern Resources + //--------------------------------------------------------------------------- + + case *appb.Appointment: + // TODO(b/240690479): Appointments are a request-pattern type that supports + // multiple participants, which may be of type Practitioner, Patient, etc. + // This means we may have to support multiple patient IDs. Currently we + // assume only 1 patient by searching for and returning the first patient we + // discover. + for _, participant := range res.GetParticipant() { + if id := participant.GetActor().GetPatientId().GetValue(); id != "" { + return id, nil + } + } + return "", fmt.Errorf("%w from %T", ErrExtractingPatientID, res) + case *arpb.AppointmentResponse: + return idOrError(res.GetActor()) + case *cppb.CarePlan: + return idOrError(res.GetSubject()) + case *clpb.Claim: + return idOrError(res.GetPatient()) + case *crpb.CommunicationRequest: + return idOrError(res.GetSubject()) + case *cerpb.CoverageEligibilityRequest: + return idOrError(res.GetPatient()) + case *drpb.DeviceRequest: + return idOrError(res.GetSubject()) + case *erpb.EnrollmentRequest: + return idOrError(res.GetCandidate()) + case *irpb.ImmunizationRecommendation: + return idOrError(res.GetPatient()) + case *mrpb.MedicationRequest: + return idOrError(res.GetSubject()) + case *nopb.NutritionOrder: + return idOrError(res.GetPatient()) + case *rgpb.RequestGroup: + return idOrError(res.GetSubject()) + case *srpb.ServiceRequest: + return idOrError(res.GetSubject()) + case *surpb.SupplyRequest: + // Supply requests may contain PatientID in two possible locations: either as + // the source of the request, or as the destination for the request + if id := res.GetDeliverTo().GetPatientId().GetValue(); id != "" { + return id, nil + } else if id := res.GetRequester().GetPatientId().GetValue(); id != "" { + return id, nil + } + return "", fmt.Errorf("%w from %T", ErrExtractingPatientID, res) + case *vppb.VisionPrescription: + return idOrError(res.GetPatient()) + default: + return "", fmt.Errorf("%w: %T", ErrUnsupportedType, res) + } +} + +// Reference creates a literal Patient reference. +// This replaces verily-go-fhir/protohelpers.PatientReference. +func Reference(patientID string) *dtpb.Reference { + return &dtpb.Reference{ + Type: fhir.URI(resource.Patient.String()), + Reference: &dtpb.Reference_PatientId{ + PatientId: &dtpb.ReferenceId{ + Value: patientID, + }, + }, + } +} diff --git a/internal/resource/patient/patient_test.go b/internal/resource/patient/patient_test.go new file mode 100644 index 0000000..d63635c --- /dev/null +++ b/internal/resource/patient/patient_test.go @@ -0,0 +1,172 @@ +package patient_test + +import ( + "fmt" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + apb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/account_go_proto" + appb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_go_proto" + arpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/appointment_response_go_proto" + cppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/care_plan_go_proto" + clpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/claim_go_proto" + crpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/communication_request_go_proto" + cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/condition_go_proto" + cerpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/coverage_eligibility_request_go_proto" + dpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_go_proto" + drpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_request_go_proto" + epb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/encounter_go_proto" + erpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/enrollment_request_go_proto" + eobpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/explanation_of_benefit_go_proto" + irpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/immunization_recommendation_go_proto" + mrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/medication_request_go_proto" + nopb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/nutrition_order_go_proto" + opb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + procpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/procedure_go_proto" + qrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_response_go_proto" + rppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/related_person_go_proto" + rgpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/request_group_go_proto" + rspb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/research_subject_go_proto" + rapb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/risk_assessment_go_proto" + srpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/service_request_go_proto" + surpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/supply_request_go_proto" + tpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/task_go_proto" + vppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/vision_prescription_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resource/patient" + "google.golang.org/protobuf/testing/protocmp" +) + +// TODO(PHP-7652): Update testing +func TestIDFromResource(t *testing.T) { + mockPatientID := "patient-id" + mockPatientRef := patient.Reference(mockPatientID) + mockAccount := &apb.Account{} + + type TestCase struct { + name string + resource fhir.Resource + expected string + expectedError error + } + + // Helpers to make pass and fail cases consistent + pass := func(name string, res fhir.Resource) TestCase { + return TestCase{ + name: fmt.Sprintf("%v as input, contains patient id, returns id", name), + resource: res, + expected: mockPatientID, + expectedError: nil, + } + } + fail := func(name string, res fhir.Resource) TestCase { + return TestCase{ + name: fmt.Sprintf("%v as input, does not contain id, returns err", name), + resource: res, + expected: "", + expectedError: patient.ErrExtractingPatientID, + } + } + + testCases := []TestCase{ + + // General Error Conditions + {"unsupported resource type, returns err", mockAccount, "", patient.ErrUnsupportedType}, + + // Resources + pass("patient", &ppb.Patient{Id: fhir.ID(mockPatientID)}), + fail("patient", &ppb.Patient{}), + pass("encounter", &epb.Encounter{Subject: mockPatientRef}), + fail("encounter", &epb.Encounter{}), + pass("device", &dpb.Device{Patient: mockPatientRef}), + fail("device", &dpb.Device{}), + pass("explanation of benefit", &eobpb.ExplanationOfBenefit{Patient: mockPatientRef}), + fail("explanation of benefit", &eobpb.ExplanationOfBenefit{}), + pass("research subject", &rspb.ResearchSubject{Individual: mockPatientRef}), + fail("research subject", &rspb.ResearchSubject{}), + pass("related person", &rppb.RelatedPerson{Patient: mockPatientRef}), + fail("related person", &rppb.RelatedPerson{}), + + // Event Patterns + pass("questionnaire response", &qrpb.QuestionnaireResponse{Subject: mockPatientRef}), + fail("questionnaire response", &qrpb.QuestionnaireResponse{}), + pass("risk assessment", &rapb.RiskAssessment{Subject: mockPatientRef}), + fail("risk assessment", &rapb.RiskAssessment{}), + pass("condition", &cpb.Condition{Subject: mockPatientRef}), + fail("condition", &cpb.Condition{}), + pass("procedure", &procpb.Procedure{Subject: mockPatientRef}), + fail("procedure", &procpb.Procedure{}), + pass("observation", &opb.Observation{Subject: mockPatientRef}), + fail("observation", &opb.Observation{}), + pass("task", &tpb.Task{ForValue: mockPatientRef}), + pass("task", &tpb.Task{Focus: mockPatientRef}), + fail("task", &tpb.Task{}), + + // Request Patterns + pass("appointment response", &arpb.AppointmentResponse{Actor: mockPatientRef}), + fail("appointment response", &arpb.AppointmentResponse{}), + pass("appointment", &appb.Appointment{Participant: []*appb.Appointment_Participant{ + {Actor: mockPatientRef}, + }}), + fail("appointment", &appb.Appointment{}), + pass("care plan", &cppb.CarePlan{Subject: mockPatientRef}), + fail("care plan", &cppb.CarePlan{}), + pass("claim", &clpb.Claim{Patient: mockPatientRef}), + fail("claim", &clpb.Claim{}), + pass("communication request", &crpb.CommunicationRequest{Subject: mockPatientRef}), + fail("communication reuqest", &crpb.CommunicationRequest{}), + pass("coverage eligibility request", &cerpb.CoverageEligibilityRequest{Patient: mockPatientRef}), + fail("coverage eligibility request", &cerpb.CoverageEligibilityRequest{}), + pass("device request", &drpb.DeviceRequest{Subject: mockPatientRef}), + fail("device request", &drpb.DeviceRequest{}), + pass("enrollment request", &erpb.EnrollmentRequest{Candidate: mockPatientRef}), + fail("enrollment request", &erpb.EnrollmentRequest{}), + pass("immunization recommendation", &irpb.ImmunizationRecommendation{Patient: mockPatientRef}), + fail("immunization recommendation", &irpb.ImmunizationRecommendation{}), + pass("medication request", &mrpb.MedicationRequest{Subject: mockPatientRef}), + fail("medication request", &mrpb.MedicationRequest{}), + pass("nutrition order", &nopb.NutritionOrder{Patient: mockPatientRef}), + fail("nutrition order", &nopb.NutritionOrder{}), + pass("request group", &rgpb.RequestGroup{Subject: mockPatientRef}), + fail("request group", &rgpb.RequestGroup{}), + pass("service request", &srpb.ServiceRequest{Subject: mockPatientRef}), + fail("service request", &srpb.ServiceRequest{}), + pass("supply request", &surpb.SupplyRequest{DeliverTo: mockPatientRef}), + pass("supply request", &surpb.SupplyRequest{Requester: mockPatientRef}), + fail("supply request", &surpb.SupplyRequest{}), + pass("vision prescription", &vppb.VisionPrescription{Patient: mockPatientRef}), + fail("vision prescription", &vppb.VisionPrescription{}), + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + patientID, err := patient.IDFromResource(tc.resource) + if got, want := err, tc.expectedError; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Fatalf("IDFromResource(%s) error got = %v, want = %v", tc.name, got, want) + } + if got, want := patientID, tc.expected; got != want { + t.Errorf("IDFromResource(%s) got = %v, want = %v", tc.name, got, want) + } + }) + } +} + +func TestReference(t *testing.T) { + want := &dtpb.Reference{ + Type: fhir.URI("Patient"), + Reference: &dtpb.Reference_PatientId{ + PatientId: &dtpb.ReferenceId{ + Value: "123", + }, + }, + } + + got := patient.Reference("123") + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Reference mismatch (-want, +got)\n%s", diff) + } +} diff --git a/internal/resource/resource.go b/internal/resource/resource.go new file mode 100644 index 0000000..f4214ce --- /dev/null +++ b/internal/resource/resource.go @@ -0,0 +1,244 @@ +/* +Package resource contains utilities for working with abstract FHIR Resource +objects. +*/ +package resource + +import ( + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resourceopt" + "github.com/verily-src/fhirpath-go/internal/protofields" +) + +var ( + ErrGetIdentifierList = errors.New("GetIdentifierList()") +) + +// Option is an option that may be supplied to updates or creations of Resource +// types. +type Option = resourceopt.Option + +// NewFromString attempts to construct a resource of the specified string name type, +// using the specified options to construct it. This function returns an error +// if 'name' does not name a valid type. +func NewFromString(name string, opts ...Option) (fhir.Resource, error) { + fields, ok := protofields.Resources[name] + if !ok { + return nil, fmt.Errorf("invalid resource name '%v'", name) + } + resource := fields.New().(fhir.Resource) + + return Update(resource, opts...), nil +} + +// New constructs a new resource from the input type, using the specified +// options to construct it. +// +// This function assumes that Type is a validly-constructed type object. +// Failure to pass a valid type will result in a panic. +func New(name Type, opts ...Option) fhir.Resource { + resource, err := NewFromString(string(name), opts...) + if err != nil { + // This is unreachable with validly-constructed Type objects + panic(err) + } + return resource +} + +// NewOf constructs a new resource of the named T resource type, using the +// specified options to construct it. +func NewOf[T fhir.Resource](opts ...Option) fhir.Resource { + var t T + return New(TypeOf(t), opts...) +} + +// Update modifies the input resource in-place with the specified options. +func Update(res fhir.Resource, opts ...Option) fhir.Resource { + return resourceopt.ApplyOptions(res, opts...) +} + +// ID gets the ID of the specified resource as a string. If `nil` is +// provided, this returns an empty string. +func ID(resource fhir.Resource) string { + if resource == nil { + return "" + } + return resource.GetId().GetValue() +} + +// VersionID gets the version-ID of the specified resource as a string. +// If `nil` is provided, this returns an empty string. +// +// This function on its own just simplifies the need of calling +// `GetMeta().GetVersionId().GetValue()` all the time. +func VersionID(resource fhir.Resource) string { + if resource == nil { + return "" + } + return resource.GetMeta().GetVersionId().GetValue() +} + +// VersionETag pulls the "version" from the resource if it's an existing resource +// that was queried from a FHIR store; the version returned matches the ETag header +// returned by GET fhir-prefix/{resourceType}/{id} for this resource. This is used +// for optimistic locking on resources per https://hl7.org/fhir/http.html#concurrency +func VersionETag(r fhir.Resource) string { + version := VersionID(r) + if version == "" { + return "" + } + return fmt.Sprintf(`W/"%s"`, version) +} + +// URI is a helper for getting the URI of a resource as a URI object. +// The URI is returned in the format Type/ID, e.g. Patient/123. +// +// If the resource is nil, this will return a nil URI. +func URI(resource fhir.Resource) *dtpb.Uri { + uri := URIString(resource) + if uri == "" { + return nil + } + return fhir.URI(uri) +} + +// URIString is a helper for getting the URI of a resource in +// string form. The URI is returned in the format Type/ID, e.g. Patient/123. +// +// If the resource is nil, this will return an empty string. +func URIString(resource fhir.Resource) string { + if resource == nil { + return "" + } + id := resource.GetId().GetValue() + return fmt.Sprintf("%v/%v", TypeOf(resource), id) +} + +// VersionedURI is a helper for getting the URI of a resource as a URI object. +// The URI is returned in the format Type/ID/_history/VERSION. +// +// If the resource is nil, this will return a nil URI. +func VersionedURI(resource fhir.Resource) *dtpb.Uri { + uri, found := VersionedURIString(resource) + if !found { + return nil + } + return fhir.URI(uri) +} + +// VersionedURIString is a helper for getting the URI of a resource in +// string form. The URI is returned in the format Type/ID/_history/VERSION. +// +// If the resource is nil, this will return an empty string. +func VersionedURIString(resource fhir.Resource) (string, bool) { + if resource == nil { + return "", false + } + vID := VersionID(resource) + if vID == "" { + return "", false + } + id := resource.GetId().GetValue() + return fmt.Sprintf("%v/%v/_history/%v", TypeOf(resource), id, vID), true +} + +// RemoveDuplicates finds all duplicates of resources -- determined by the +// same <resource>/<id>/<version-id> -- and removes them, returning an +// updated list of resources. +// +// Nil resources are skipped. +func RemoveDuplicates(resources []fhir.Resource) []fhir.Resource { + deduper := map[string]struct{}{} + + result := make([]fhir.Resource, 0, len(resources)) + for _, res := range resources { + if res == nil { + continue + } + // Note: using this instead of VersionedURIString, since whether a version-id + // exists or not will not affect the behavior here. + key := fmt.Sprintf("%v/%v/%v", TypeOf(res), ID(res), res.GetMeta().GetVersionId().GetValue()) + + if _, ok := deduper[key]; ok { + continue + } + result = append(result, res) + deduper[key] = struct{}{} + } + return result +} + +// GroupResources organizes all resources by their underlying resource Type, +// and returns a map of the Type to the list of resources of that given type. +// +// Nil resources are skipped. +// Resources with existing IDs are skipped +func GroupResources(resources []fhir.Resource) map[Type][]fhir.Resource { + result := map[Type][]fhir.Resource{} + seen := map[string]struct{}{} + for _, res := range resources { + if res == nil { + continue + } + if id := res.GetId(); id != nil { + uri := URIString(res) + if _, ok := seen[uri]; ok { + continue // skip ones we have seen before + } + // add in new ones + seen[uri] = struct{}{} + } + + key := TypeOf(res) + result[key] = append(result[key], res) + } + return result +} + +// HasGetIdentifierList is a custom interface for duck typing resources that +// have a GetIdentifier method that returns a slice of Identifiers. +type HasGetIdentifierList interface { + GetIdentifier() []*dtpb.Identifier + + // embed Resource since anything with an Identifier is also a Resource + fhir.Resource +} + +// HasGetIdentifierSingle is a custom interface for duck typing resources that +// have a GetIdentifier method that returns a single Identifier. +type HasGetIdentifierSingle interface { + GetIdentifier() *dtpb.Identifier + + // embed Resource since anything with an Identifier is also a Resource + fhir.Resource +} + +// GetIdentifierList takes a Resource and returns a list of Identifiers. +// It uses duck typing to determine whether the resource has a GetIdentifier() +// method, and if so, whether it returns a list or a single Identifier. +// It returns ErrGetIdentifierList if the resource does not implement GetIdentifier(). +// The list may be nil or empty if no identifiers are present. +// See interfaces: fhir.HasGetIdentifierList, fhir.HasGetIdentifierSingle +func GetIdentifierList(res fhir.Resource) ([]*dtpb.Identifier, error) { + + if cast, ok := res.(HasGetIdentifierList); ok { + // resource implements GetIdentifier() as a list + return cast.GetIdentifier(), nil + } + + if cast, ok := res.(HasGetIdentifierSingle); ok { + // resource implements GetIdentifier() as a single Identifier + id := cast.GetIdentifier() + if id == nil { + return nil, nil + } + return []*dtpb.Identifier{id}, nil + } + + // This is likely a bug / results from passing an unexpected type of resource + return nil, fmt.Errorf("%w: Resource does not implement GetIdentifier(): %v", ErrGetIdentifierList, res) +} diff --git a/internal/resource/resource_example_test.go b/internal/resource/resource_example_test.go new file mode 100644 index 0000000..db7928a --- /dev/null +++ b/internal/resource/resource_example_test.go @@ -0,0 +1,32 @@ +package resource_test + +import ( + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +func ExampleGetIdentifierList() { + patient := &patient_go_proto.Patient{ + Id: fhir.ID("12345"), + Identifier: []*dtpb.Identifier{ + &dtpb.Identifier{ + System: &dtpb.Uri{Value: "http://fake.com"}, + Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, + }, + }, + } + + ids, err := resource.GetIdentifierList(patient) + if err != nil { + panic(err) + } else if ids == nil || len(ids) == 0 { + panic("no identifiers") + } else { + fmt.Printf("Identifier value: %#v", ids[0].GetValue().Value) + // Output: Identifier value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8" + } +} diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go new file mode 100644 index 0000000..a90ef8d --- /dev/null +++ b/internal/resource/resource_test.go @@ -0,0 +1,390 @@ +package resource_test + +import ( + "errors" + "regexp" + "strings" + "testing" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" + dpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/device_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/document_reference_go_proto" + ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" + "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_response_go_proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestVersionETag(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + want string + }{ + { + "failure: VersionId is the empty string", + &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("")}}, + "", + }, + { + "extracted the VersionId from a Patient", + &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("abc")}}, + `W/"abc"`, + }, + { + "extracted the VersionId from a Device", + &dpb.Device{Meta: &dtpb.Meta{VersionId: fhir.ID("xyz")}}, + `W/"xyz"`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := resource.VersionETag(tc.res) + + if got != tc.want { + t.Errorf("VersionETag(%s) version got = %v, want = %v", tc.name, got, tc.want) + } + }) + } +} + +func TestVersionedURI(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + want *dtpb.Uri + }{ + { + "nil resource", + &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("")}}, + nil, + }, + { + "no version", + &ppb.Patient{Id: fhir.ID("abc")}, + nil, + }, + { + "versioned resource", + &dpb.Device{Id: fhir.ID("123"), Meta: &dtpb.Meta{VersionId: fhir.ID("abc")}}, + &dtpb.Uri{Value: "Device/123/_history/abc"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := resource.VersionedURI(tc.res) + + if diff := cmp.Diff(got, tc.want, protocmp.Transform()); diff != "" { + t.Fatalf("VersionedURI(%s): (-got, +want):\n%s", tc.name, diff) + } + }) + } +} + +func TestVersionedURIString(t *testing.T) { + testCases := []struct { + name string + res fhir.Resource + want string + wantFound bool + }{ + { + "nil resource", + &ppb.Patient{Meta: &dtpb.Meta{VersionId: fhir.ID("")}}, + "", + false, + }, + { + "no version", + &ppb.Patient{Id: fhir.ID("abc")}, + "", + false, + }, + { + "versioned resource", + &dpb.Device{Id: fhir.ID("123"), Meta: &dtpb.Meta{VersionId: fhir.ID("abc")}}, + "Device/123/_history/abc", + true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, found := resource.VersionedURIString(tc.res) + + if found != tc.wantFound { + t.Fatalf("VersionedURIString(%s) found got = %v, want = %v", tc.name, got, tc.wantFound) + } + if got != tc.want { + t.Errorf("VersionedURIString(%s) got = %v, want = %v", tc.name, got, tc.want) + } + }) + } +} + +func TestRemoveDuplicates(t *testing.T) { + patient := fhirtest.NewResource(t, resource.Patient) + device := fhirtest.NewResource(t, resource.Device) + account := fhirtest.NewResource(t, resource.Account) + + testCases := []struct { + name string + input []fhir.Resource + want []fhir.Resource + }{ + { + name: "Inputs are unique", + input: []fhir.Resource{patient, device, account}, + want: []fhir.Resource{patient, device, account}, + }, + { + name: "Duplicates are removed", + input: []fhir.Resource{patient, device, account, device, account, patient}, + want: []fhir.Resource{patient, device, account}, + }, + { + name: "Removes nil", + input: []fhir.Resource{nil, patient, nil}, + want: []fhir.Resource{patient}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := resource.RemoveDuplicates(tc.input) + + opts := []cmp.Option{ + cmpopts.SortSlices(func(lhs, rhs fhir.Resource) bool { + return resource.URIString(lhs) < resource.URIString(rhs) + }), + protocmp.Transform(), + } + if want := tc.want; !cmp.Equal(got, want, opts...) { + t.Errorf("RemoveDuplicates(%v): got '%v', want '%v'", tc.name, got, want) + } + }) + } +} + +func TestGroupResources(t *testing.T) { + patient := fhirtest.NewResource(t, resource.Patient) + device := fhirtest.NewResource(t, resource.Device) + account := fhirtest.NewResource(t, resource.Account) + + testCases := []struct { + name string + input []fhir.Resource + want map[resource.Type][]fhir.Resource + }{ + { + name: "Inputs are sorted", + input: []fhir.Resource{patient, device, account}, + want: map[resource.Type][]fhir.Resource{ + resource.Patient: {patient}, + resource.Device: {device}, + resource.Account: {account}, + }, + }, + { + name: "Removes nil", + input: []fhir.Resource{nil, patient, nil}, + want: map[resource.Type][]fhir.Resource{ + resource.Patient: {patient}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := resource.GroupResources(tc.input) + + opts := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortMaps(func(lhs, rhs resource.Type) bool { + return lhs < rhs + }), + } + if want := tc.want; !cmp.Equal(got, want, opts...) { + t.Errorf("GroupResources(%v): got '%v', want '%v'", tc.name, got, want) + } + }) + } +} + +// Assert that an identifier list is present and has expected values +func assertIdentifier(t *testing.T, identifiers []*dtpb.Identifier, name string, resource fhir.Resource) { + if identifiers == nil { + t.Errorf("Nil list of ids for %v: %v", name, resource) + return + } + if len(identifiers) == 0 { + t.Errorf("Empty list of ids for %v: %v", name, resource) + return + } + + id := identifiers[0] + + if got, want := id.GetSystem().Value, "http://example.com/fake-id"; got != want { + t.Errorf("%v Resource.Identifier[0].System: got %v, want %v", name, got, want) + } + + value := id.GetValue().Value + matched, _ := regexp.MatchString("^[a-f0-9-]+$", value) + if !matched { + t.Errorf("%v Resource.Identifier[0].Value: got %v, expected uuid", name, value) + } +} + +func TestGetIdentifier(t *testing.T) { + // Test that all resources return nil by default + for name := range fhirtest.Resources { + t.Run("Resources/"+name, func(t *testing.T) { + res := fhirtest.NewResource( + t, + resource.Type(name), + ) + + ids, _ := resource.GetIdentifierList(res) + + if ids != nil { + t.Errorf("%v Resource.Identifier: got %v, want nil -- not supposed to have Identifier", name, ids) + } + }) + } + + // Test that all types compatible with CanonicalResource actually return an identifier + for name := range fhirtest.CanonicalResources { + t.Run("CanonicalResources/"+name, func(t *testing.T) { + res := fhirtest.NewResource( + t, + resource.Type(name), + fhirtest.WithGeneratedIdentifier("http://example.com/fake-id"), + ) + ids, err := resource.GetIdentifierList(res) + if err != nil { + t.Errorf("got %v, want nil -- unexpected error", err) + } + + assertIdentifier(t, ids, name, res) + }) + } + + // Sanity check a few specific types that we know have Identifier + for _, name := range []string{"Patient", "DocumentReference", "AdverseEvent", "Bundle"} { + t.Run("CanonicalResources/"+name, func(t *testing.T) { + res := fhirtest.NewResource( + t, + resource.Type(name), + fhirtest.WithGeneratedIdentifier("http://example.com/fake-id"), + ) + + ids, err := resource.GetIdentifierList(res) + if err != nil { + t.Errorf("got %v, want nil -- unexpected error", err) + } + + assertIdentifier(t, ids, name, res) + }) + } + +} + +func TestGetIdentifier_single(t *testing.T) { + // Sanity check a few specific types that have a singleton Identifier + // Bundle, QuestionnaireResponse + + testIds := []*dtpb.Identifier{ + { + System: &dtpb.Uri{Value: "http://example.com/fake-id"}, + Value: &dtpb.String{Value: "35c423fc-0651-4c83-b63f-9008e0c96445"}, + }, + { + System: &dtpb.Uri{Value: "http://example.com/fake-id"}, + Value: &dtpb.String{Value: "ddec1b6e-4539-4aae-becf-b4dced32189f"}, + }, + } + + testCases := []struct { + name string + res fhir.Resource + want *dtpb.Identifier + }{ + { + "Bundle", + &bundle_and_contained_resource_go_proto.Bundle{ + Identifier: testIds[0], + }, + testIds[0], + }, + { + "QuestionnaireResponse", + &questionnaire_response_go_proto.QuestionnaireResponse{ + Identifier: testIds[1], + }, + testIds[1], + }, + } + for _, tc := range testCases { + + ids, err := resource.GetIdentifierList(tc.res) + want := []*dtpb.Identifier{tc.want} + + if err != nil { + t.Errorf("got %v, want nil", err) + return + } + + assertIdentifier(t, ids, tc.name, tc.res) + + if len(ids) != len(want) { + t.Errorf("got %v, want %v", ids, want) + continue + } + if ids[0] != want[0] { + t.Errorf("got %v, want %v", ids[0], want[0]) + } + } +} + +func TestGetIdentifier_nil(t *testing.T) { + // Sanity check a few specific types that we know do NOT have Identifier + resourcesWithoutIdentifiers := []string{ + "Provenance", + "Linkage", + } + for _, name := range resourcesWithoutIdentifiers { + t.Run("CanonicalResources/"+name, func(t *testing.T) { + res := fhirtest.NewResource(t, resource.Type(name)) + + ids, err := resource.GetIdentifierList(res) + + if ids != nil { + t.Errorf("%v Resource.Identifier: got %v, want nil -- not supposed to have Identifier", name, ids) + } + + if err == nil { + t.Errorf("got nil, want error") + } + + wanterr := "Resource does not implement GetIdentifier()" + if !strings.Contains(err.Error(), wanterr) { + t.Errorf("got %#v, want %#v", err.Error(), wanterr) + } + if !errors.Is(err, resource.ErrGetIdentifierList) { + t.Errorf("got error %#v, want errors.Is(..., ErrGenerateIfNoneExist)", err) + } + }) + } +} + +// Sanity check that a few resources have GetIdentifier() as a list. This is not a complete list. +var _ resource.HasGetIdentifierList = (*ppb.Patient)(nil) +var _ resource.HasGetIdentifierList = (*document_reference_go_proto.DocumentReference)(nil) + +// Sanity check that a few resources have GetIdentifier() as a single ID. This is not a complete list. +var _ resource.HasGetIdentifierSingle = (*bundle_and_contained_resource_go_proto.Bundle)(nil) +var _ resource.HasGetIdentifierSingle = (*questionnaire_response_go_proto.QuestionnaireResponse)(nil) diff --git a/internal/resource/type.go b/internal/resource/type.go new file mode 100644 index 0000000..d2acc63 --- /dev/null +++ b/internal/resource/type.go @@ -0,0 +1,86 @@ +package resource + +import ( + "errors" + "fmt" + + dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/protofields" +) + +// ErrBadType is an error raised when a bad type is provided. +var ErrBadType = errors.New("bad resource type") + +// Type is a FHIR Resource type object. This is similar to a reflect.Type that +// encodes its name identifier +// +// Type objects should never be constructed manually; rather, use the `CheckType` +// or `TypeOf` functions to get a valid type object. Invalid instances of Type +// may lead to unexpected implicit `panic` behavior, as any code consuming this +// is allowed to assume that `Type` always names a valid instance. +type Type string + +// TypeOf gets the underlying type of the named resource. +// +// This function panics if resource is nil. Note that this is only an issue if +// the interface `fhir.Resource` is nil, *not* if the underlying resource is a +// pointer that is nil. E.g. the following holds true: +// +// assert.True(resource.TypeOf((*ppb.Patient)(nil)) == resource.Patient) +func TypeOf(resource fhir.Resource) Type { + if resource == nil { + panic("TypeOf provided nil Resource") + } + return Type(resource.ProtoReflect().Descriptor().Name()) +} + +// NewType checks whether the string type name is a valid resource.Type instance. +// If it is, an instance of the type is returned. If the provided type is not a +// valid type, an ErrBadType is returned, and the type result is garbage. +// +// Note: This is case-sensitive, and expects CamelCase, just as the FHIR spec uses. +func NewType(resourceType string) (Type, error) { + if !IsType(resourceType) { + return "", fmt.Errorf("%w '%v'", ErrBadType, resourceType) + } + return Type(resourceType), nil +} + +// String converts this Type into a string. +func (t Type) String() string { + return string(t) +} + +// New returns an instance of the FHIR Resource which this type names, using +// the provided options to toggle. +// +// This function will panic if this does not name a valid Resource Type. +func (t Type) New(opts ...Option) fhir.Resource { + return New(t, opts...) +} + +// URI returns a URI object containing the resource type name. +func (t Type) URI() *dtpb.Uri { + return &dtpb.Uri{ + Value: string(t), + } +} + +// StructureDefinitionURI returns an absolute URI to the structure-definition +// URL. +func (t Type) StructureDefinitionURI() *dtpb.Uri { + const baseURL = "http://hl7.org/fhir/StructureDefinition" + + return &dtpb.Uri{ + Value: fmt.Sprintf("%v/%v", baseURL, t), + } +} + +// IsType queries whether the given string names a Resource type. +// +// Note: This is case-sensitive, and expects CamelCase, jus as the FHIR spec uses. +func IsType(name string) bool { + _, ok := protofields.Resources[name] + return ok +} diff --git a/internal/resource/type_test.go b/internal/resource/type_test.go new file mode 100644 index 0000000..432e9be --- /dev/null +++ b/internal/resource/type_test.go @@ -0,0 +1,127 @@ +package resource_test + +import ( + "errors" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/fhirtest" + "github.com/verily-src/fhirpath-go/internal/resource" +) + +func TestTypeOf_ReturnsType(t *testing.T) { + for name, res := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + want := string(res.ProtoReflect().Descriptor().Name()) + + got := resource.TypeOf(res) + + if !cmp.Equal(string(got), want) { + t.Errorf("TypeOf(%v): got '%v', want '%v'", name, got, want) + } + }) + } +} + +func TestTypeOf_NilInput_Panics(t *testing.T) { + defer func() { _ = recover() }() + + resource.TypeOf(nil) + + t.Errorf("TypeOf: expected panic") +} + +func TestNewType_ValidTypeName_ReturnsType(t *testing.T) { + for name, res := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + want := resource.TypeOf(res) + + got, err := resource.NewType(name) + if err != nil { + t.Fatalf("NewType: got unexpected err '%v' from NewType", err) + } + + if !cmp.Equal(got, want) { + t.Errorf("NewType(%v): got %v, want %v", name, got, want) + } + }) + } +} + +func TestNewType_InvalidTypeName_ReturnsErrBadType(t *testing.T) { + testCases := []struct { + name string + value string + }{ + {"Empty", ""}, + {"NotAnElement", "Bad-Element"}, + {"AnonymousElement", "Bundle_Entry"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := resource.NewType(tc.value) + + if got, want := err, resource.ErrBadType; !errors.Is(got, want) { + t.Errorf("NewType(%v): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestIsType_ValidTypeName_ReturnsTrue(t *testing.T) { + for name := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + got := resource.IsType(name) + + if got != true { + t.Errorf("IsType(%v): got %v, want true", name, got) + } + }) + } +} + +func TestIsType_InvalidTypeName_ReturnsFalse(t *testing.T) { + testCases := []struct { + name string + value string + }{ + {"Empty", ""}, + {"NotAResource", "ContainedResource"}, + {"Element", "String"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := resource.IsType(tc.value) + + if got != false { + t.Errorf("IsType(%v): got %v, want false", tc.name, got) + } + }) + } +} + +func TestTypeNew_ReturnsElementOfType(t *testing.T) { + for name, elem := range fhirtest.Resources { + t.Run(name, func(t *testing.T) { + ty := resource.TypeOf(elem) + want := reflect.TypeOf(elem) + + got := ty.New() + + if reflect.TypeOf(got) != want { + t.Errorf("Type.New: got %v, want %v", got, want) + } + }) + } +} + +func TestTypeNew_Unspecified_ReturnsNil(t *testing.T) { + defer func() { _ = recover() }() + + resource.Type("").New() + + t.Errorf("Type.New: expected panic") +} diff --git a/internal/resourceopt/resourceopt.go b/internal/resourceopt/resourceopt.go new file mode 100644 index 0000000..d82f97a --- /dev/null +++ b/internal/resourceopt/resourceopt.go @@ -0,0 +1,83 @@ +/* +Package resourceopt is an internal package that provides helper utilities +for forming resource-options in resource packages. +*/ +package resourceopt + +import ( + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "github.com/verily-src/fhirpath-go/internal/protofields" + "google.golang.org/protobuf/proto" +) + +// Option is the definition of a resource Option used for creating and updating +// FHIR Resources. +type Option interface { + update(fhir.Resource) +} + +// ApplyOptions applies the specified options to the input resource. +// +// This function is defined here due to the Option interface providing an +// unexported field. This is needed so that the other packages using this can +// accumulate the options without having access to the unexported call. +func ApplyOptions[T fhir.Resource, O Option](r T, opts ...O) T { + for _, opt := range opts { + opt.update(r) + } + return r +} + +// WithProtoField is a resource Option that sets the specified 'field' in the proto +// to the values. If values is empty, the field is cleared. If values is +// not 1, and a field is not repeated, this functon will panic. +// +// Note: This is an internal function intended to be used to form generic +// resource options that will work with all FHIR resources. +func WithProtoField[T proto.Message](fieldName string, values ...T) Option { + // SAFETY: + // MustConvert cannot fail here, since the 'T' constraint above ensures that + // all inputs will be valid proto.Message types. + return withProtoFieldImpl(fieldName, slices.MustConvert[proto.Message](values)...) +} + +func withProtoFieldImpl(fieldName string, values ...proto.Message) Option { + return WithCallback(func(r fhir.Resource) { + protofields.Overwrite(r, fieldName, values...) + }) +} + +// IncludeProtoField is a resource Option that appends the specified entries to +// the given 'field' in the proto. This function will panic if the given field +// is not a repeated field in the proto. +// +// Note: This is an internal function intended to be used to form generic +// resource options that will work with all FHIR resources. +func IncludeProtoField[T proto.Message](fieldName string, values ...T) Option { + // SAFETY: + // MustConvert cannot fail here, since the 'T' constraint above ensures that + // all inputs will be valid proto.Message types. + return includeProtoFieldImpl(fieldName, slices.MustConvert[proto.Message](values)...) +} + +func includeProtoFieldImpl(fieldName string, values ...proto.Message) Option { + return WithCallback(func(r fhir.Resource) { + protofields.AppendList(r, fieldName, values...) + }) +} + +// WithCallback returns a resource Option that simply passes the resource being +// created back into the specified callback. This exists to be built into +// larger, more strongly-typed options. +func WithCallback[T fhir.Resource](callback func(T)) Option { + return &callbackOpt[T]{callback} +} + +type callbackOpt[T fhir.Resource] struct { + callback func(T) +} + +func (o *callbackOpt[T]) update(r fhir.Resource) { + o.callback(r.(T)) +} diff --git a/internal/slices/slices.go b/internal/slices/slices.go new file mode 100644 index 0000000..4e0e26f --- /dev/null +++ b/internal/slices/slices.go @@ -0,0 +1,219 @@ +// Package slices provides helpful functions +// for searching, sorting and manipulating slices. +package slices + +import ( + "fmt" + "reflect" + "sort" + + "golang.org/x/exp/constraints" + "google.golang.org/protobuf/proto" +) + +// IsIdentical checks if two slices refer to the same underlying slice object. +func IsIdentical[S2 ~[]T, S1 ~[]T, T any](lhs S1, rhs S2) bool { + if len(lhs) != len(rhs) { + return false + } + if len(lhs) == 0 { + return true + } + return &lhs[0] == &rhs[0] +} + +// All returns true if every element in a slice satisfies the +// given condition. Defaults to true for an empty slice. +func All[S ~[]T, T any](vals S, comp func(T) bool) bool { + for _, val := range vals { + if !comp(val) { + return false + } + } + return true +} + +// Any returns true if at least one element in a slice satisfies +// the given condition. Defaults to false for an empty slice. +func Any[S ~[]T, T any](vals S, comp func(T) bool) bool { + for _, val := range vals { + if comp(val) { + return true + } + } + return false +} + +// Filter returns a subset of the original slice, consisting of all the +// elements in the original slice which satisfy the given condition. +func Filter[S ~[]T, T any](vals S, comp func(T) bool) S { + matches := make(S, 0) + for _, val := range vals { + if comp(val) { + matches = append(matches, val) + } + } + return matches +} + +// Count returns the number of elements in a slice that match the given +// condition. +func Count[S ~[]T, T any](vals S, comp func(T) bool) int { + matches := 0 + for _, val := range vals { + if comp(val) { + matches += 1 + } + } + return matches +} + +// Includes returns true if a slice contains the target value. +func Includes[S ~[]T, T any](vals S, target T) bool { + return IndexOf(vals, target) > -1 +} + +// IndexOf returns the index of the target value in a slice. +// Returns the first index found, and -1 if the value is not found. +func IndexOf[S ~[]T, T any](vals S, target T) int { + _, isProto := any(target).(proto.Message) + for index, val := range vals { + if isProto && proto.Equal(any(val).(proto.Message), any(target).(proto.Message)) { + return index + } else if reflect.DeepEqual(val, target) { + return index + } + } + return -1 +} + +// Join returns a string consisting of all elements in a slice, +// separated by the given delimiter. +func Join[S ~[]T, T any](vals S, delimiter string) string { + if len(vals) == 0 { + return "" + } + combined := fmt.Sprintf("%v", vals[0]) + for _, val := range vals[1:] { + combined += fmt.Sprintf("%s%v", delimiter, val) + } + return combined +} + +// Map returns a new slice with the elements of the original +// slice transformed according to the provided function. +func Map[T any, U any](vals []T, mapper func(T) U) []U { + mapped := make([]U, 0, len(vals)) + for _, val := range vals { + mapped = append(mapped, mapper(val)) + } + return mapped +} + +// Reverse performs an in-place reversal of the elements in a slice. +// Performance testing has not been done to compare to alternatives. +func Reverse[S ~[]T, T any](t S) { + sort.SliceStable(t, func(i, j int) bool { + return i > j + }) +} + +// Sort performs an in-place sort of a slice. +// Performance testing has not been done to compare to alternatives. +func Sort[S ~[]T, T constraints.Ordered](vals S) { + sort.SliceStable(vals, func(i, j int) bool { + return vals[i] < vals[j] + }) +} + +// Convert returns an array of objects of type To from an array of objects of +// type From. If any conversion fails, this will return an error. +// +// This function, along with the non-failing `MustConvert` equivalent, are +// useful for converting one array type []T to an array of another type []U, +// which requires manual iteration in Go. This provides a more convenient +// mechanism for casting between arrays of interfaces and concrete types, +// provided all elements can safely be casted to. +// +// Note: this function only works with language-level type-casts (e.g. `t.(U)`). +// For converting between concrete struct types, use `Map`. +func Convert[To any, S ~[]From, From any](from S) ([]To, error) { + result := make([]To, 0, len(from)) + for _, val := range from { + if to, ok := any(val).(To); ok { + result = append(result, to) + } else { + return nil, fmt.Errorf("slices.Convert[%T](from): unable to convert from %T", to, from) + } + } + return result, nil +} + +// MustConvert returns an array of To objects by converting every element in +// From to To. This will panic on failure to convert. +// +// This function, along with the failing `Convert` equivalent, are +// useful for converting one array type []T to an array of another type []U, +// which requires manual iteration in Go. This provides a more convenient +// mechanism for casting between arrays of interfaces and concrete types, +// provided all elements can safely be casted to. +// +// This function in particular should only be used if the 'To' type is guaranteed +// to always be convertible -- such as converting a concrete type to its base +// interface, or to any. +func MustConvert[To any, S ~[]From, From any](from S) []To { + return Map(from, func(from From) To { return any(from).(To) }) +} + +// Transform performs an in-place transformation of a slice. +func Transform[S ~[]T, T any](vals S, transform func(T) T) { + for i := range vals { + vals[i] = transform(vals[i]) + } +} + +// IsUnique returns true if all elements of a slice are unique. +func IsUnique[T comparable](vals []T) bool { + seen := map[T]struct{}{} + for _, val := range vals { + if _, found := seen[val]; found { + return false + } + seen[val] = struct{}{} + } + return true +} + +// Unique returns a new slice of the unique elements in a given slice. +// It keeps the first instance of any duplicate values and ignores any +// subsequent instances of the value. +func Unique[S ~[]T, T comparable](vals S) S { + set := map[T]struct{}{} + uniqueSlice := make(S, 0) + for _, val := range vals { + if _, found := set[val]; !found { + set[val] = struct{}{} + uniqueSlice = append(uniqueSlice, val) + } + } + return uniqueSlice +} + +// Chunk divides the given slice into multiple new slices, each at most +// having lengths of the given size. The last chunk may have a smaller +// length if the length of vals is not evenly divisible by size. +func Chunk[T any](vals []T, size int) [][]T { + var chunks [][]T + for i := 0; i < len(vals); { + chunkLen := size + remainingLen := len(vals[i:]) + if remainingLen < size { + chunkLen = remainingLen + } + chunk := make([]T, chunkLen) + copy(chunk, vals[i:i+chunkLen]) + chunks = append(chunks, chunk) + i += chunkLen + } + return chunks +} diff --git a/internal/slices/slices_example_test.go b/internal/slices/slices_example_test.go new file mode 100644 index 0000000..90a6f05 --- /dev/null +++ b/internal/slices/slices_example_test.go @@ -0,0 +1,220 @@ +package slices_test + +import ( + "fmt" + "strings" + + "github.com/verily-src/fhirpath-go/internal/slices" +) + +func ExampleAll() { + greaterThanFive := func(i int) bool { return i > 5 } + arr := []int{1, 2, 3} + arr2 := []int{7, 8, 9} + + all := slices.All(arr, greaterThanFive) + fmt.Printf("arr all > 5: %v\n", all) + + all = slices.All(arr2, greaterThanFive) + fmt.Printf("arr2 all > 5: %v\n", all) + + // Output: + // arr all > 5: false + // arr2 all > 5: true +} + +func ExampleAny() { + isEven := func(i int) bool { return i%2 == 0 } + oddArr := []int{1, 3, 5, 7} + evenArr := []int{1, 2, 9} + + any := slices.Any(oddArr, isEven) + fmt.Printf("oddArr any even: %v\n", any) + + any = slices.Any(evenArr, isEven) + fmt.Printf("evenArr any even: %v\n", any) + + // Output: + // oddArr any even: false + // evenArr any even: true +} + +func ExampleFilter() { + isNegative := func(i int) bool { return i < 0 } + arr := []int{1, 3, -9, -5, 6} + + filtered := slices.Filter(arr, isNegative) + fmt.Printf("filtered arr: %v\n", filtered) + + // Output: + // filtered arr: [-9 -5] +} + +func ExampleCount() { + isNegative := func(i int) bool { return i < 0 } + arr := []int{1, 3, -9, -5, 6} + + countNeg := slices.Count(arr, isNegative) + fmt.Printf("count of negative nums: %v\n", countNeg) + + // Output: + // count of negative nums: 2 +} + +func ExampleIncludes() { + arr := []string{"a", "c", "e"} + + includesB := slices.Includes(arr, "b") + fmt.Printf("arr includes b: %v\n", includesB) + + includesA := slices.Includes(arr, "a") + fmt.Printf("arr includes a: %v\n", includesA) + + // Output: + // arr includes b: false + // arr includes a: true +} + +func ExampleIndexOf() { + letters := []string{"w", "o", "o", "a", "h"} + + indexM := slices.IndexOf(letters, "m") + fmt.Printf("index of m: %v\n", indexM) + + indexO := slices.IndexOf(letters, "o") + fmt.Printf("index of o: %v\n", indexO) + + // Output: + // index of m: -1 + // index of o: 1 +} + +func ExampleJoin() { + arr := []int{0, 0} + + joined := slices.Join(arr, ".") + fmt.Printf("joined string: %v\n", joined) + + // Output: + // joined string: 0.0 +} + +func ExampleMap() { + arr := []int{1, 2, 3} + mapper := func(i int) string { return fmt.Sprintf("%v_%v", i, i) } + + mapped := slices.Map(arr, mapper) + fmt.Printf("mapped arr: %v\n", mapped) + + // Output: + // mapped arr: [1_1 2_2 3_3] +} + +func ExampleReverse() { + arr := []int{1, 2, 3, 3, 4} + + slices.Reverse(arr) + fmt.Printf("reversed arr: %v\n", arr) + + // Output: + // reversed arr: [4 3 3 2 1] +} + +func ExampleSort() { + arr := []int{1, -2, 3, -4, 5} + + slices.Sort(arr) + fmt.Printf("sorted arr: %v\n", arr) + + // Output: + // sorted arr: [-4 -2 1 3 5] +} + +func ExampleConvert_good_conversion() { + arr := []any{1, 2, 3} + + intArr, err := slices.Convert[int](arr) + if err != nil { + fmt.Printf("Unable to convert slice!") + } else { + fmt.Printf("int array: %v", intArr) + } + + // Output: + // int array: [1 2 3] +} + +func ExampleConvert_bad_conversion() { + arr := []any{1, 2, 3} + + intArr, err := slices.Convert[string](arr) + if err != nil { + fmt.Printf("Unable to convert slice!") + } else { + fmt.Printf("int array: %v", intArr) + } + + // Output: + // Unable to convert slice! +} + +func ExampleMustConvert() { + // Easy mechanism to cast arrays to interfaces and back + arr := []any{1, 2, 3} + + intArr := slices.MustConvert[int](arr) + fmt.Printf("int array: %v", intArr) + + // Output: + // int array: [1 2 3] +} + +func ExampleTransform() { + arr := []string{"\t hello\n", "\t world\n"} + + slices.Transform(arr, strings.TrimSpace) + fmt.Printf("transformed arr: %v\n", arr) + + // Output: + // transformed arr: [hello world] +} + +func ExampleIsUnique() { + strArr := []string{"hello", "world"} + intArr := []int{1, 2, 3, 2, 1} + + strUnique := slices.IsUnique(strArr) + fmt.Printf("str arr unique: %v\n", strUnique) + + intUnique := slices.IsUnique(intArr) + fmt.Printf("int arr unique: %v\n", intUnique) + + // Output: + // str arr unique: true + // int arr unique: false +} + +func ExampleUnique() { + arr := []int{1, 2, 3, 2, 1} + + uniqueArr := slices.Unique(arr) + fmt.Printf("unique arr: %v\n", uniqueArr) + + // Output: + // unique arr: [1 2 3] +} + +func ExampleChunk() { + strArr := []string{"a", "b", "c", "d", "e"} + strChunks := slices.Chunk(strArr, 5) + fmt.Printf("str arr chunks: %v\n", strChunks) + + intArr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + intChunks := slices.Chunk(intArr, 3) + fmt.Printf("int arr chunks: %v\n", intChunks) + + // Output: + // str arr chunks: [[a b c d e]] + // int arr chunks: [[1 2 3] [4 5 6] [7 8 9] [10]] + +} diff --git a/internal/slices/slices_test.go b/internal/slices/slices_test.go new file mode 100644 index 0000000..c080d3c --- /dev/null +++ b/internal/slices/slices_test.go @@ -0,0 +1,707 @@ +package slices_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/verily-src/fhirpath-go/internal/slices" + "github.com/verily-src/fhirpath-go/internal/fhir" + "golang.org/x/exp/constraints" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/testing/protocmp" +) + +type StrongSlice[T any] []T + +type arrTestCase[T constraints.Ordered] struct { + name string + arr StrongSlice[T] + expected StrongSlice[T] +} + +type boolTestCase[T any] struct { + name string + arr StrongSlice[T] + expected bool +} + +type includesTestCase[T any] struct { + name string + arr StrongSlice[T] + target T + expected bool +} + +type indexTestCase[T any] struct { + name string + arr StrongSlice[T] + target T + expected int +} + +type intTestCase[T any] struct { + name string + arr StrongSlice[T] + expected int +} + +type mapTestCase[T any, U any] struct { + name string + arr StrongSlice[T] + expected []U +} + +type stringTestCase[T any] struct { + name string + arr StrongSlice[T] + delimiter string + expected string +} + +func TestIsIdentical(t *testing.T) { + type s1 []int + type s2 []int + + base := []int{1, 2, 3, 4} + testCases := []struct { + name string + lhs s1 + rhs s2 + want bool + }{ + { + name: "Same reference", + lhs: s1(base), + rhs: s2(base), + want: true, + }, { + name: "Different size", + lhs: s1(base), + rhs: append(s2(base), 5), + want: false, + }, { + name: "Same size, empty", + lhs: nil, + rhs: nil, + want: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := slices.IsIdentical(tc.lhs, tc.rhs) + + if got != tc.want { + t.Errorf("IsIdentical(%v): got %v, want %v", tc.name, got, tc.want) + } + }) + } +} + +func TestAll(t *testing.T) { + lengthFive := func(a string) bool { return len(a) == 5 } + testCases := []boolTestCase[string]{ + { + "empty slice", + []string{}, + true, + }, { + "no matching element", + []string{"cat", "dog"}, + false, + }, { + "some matching elements", + []string{"apple", "orange", "banana"}, + false, + }, { + "all matching elements", + []string{"daisy", "tulip"}, + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := slices.All(tc.arr, lengthFive) + if got, want := result, tc.expected; got != want { + t.Errorf("All(%s) want = %v, got = %v", tc.name, want, got) + } + }) + } +} + +func TestAny(t *testing.T) { + multOfFour := func(a int) bool { return a%4 == 0 } + testCases := []boolTestCase[int]{ + { + "empty slice", + []int{}, + false, + }, { + "no matching element", + []int{1, 2, 3}, + false, + }, { + "matching element", + []int{1, 2, 3, 4, 5}, + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := slices.Any(tc.arr, multOfFour) + if got, want := result, tc.expected; got != want { + t.Errorf("Any(%s) want = %v, got = %v", tc.name, want, got) + } + }) + } +} + +func TestFilter(t *testing.T) { + lessThanPi := func(a float32) bool { return a < 3.14159 } + testCases := []arrTestCase[float32]{ + { + "empty slice", + []float32{}, + []float32{}, + }, { + "no matching elements", + []float32{4.1, 9.12}, + []float32{}, + }, { + "matching elements", + []float32{1.0, 2.718, 5.56}, + []float32{1.0, 2.718}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := slices.Filter(tc.arr, lessThanPi) + got, want := result, tc.expected + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Filter(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestCount(t *testing.T) { + lessThanPi := func(a float32) bool { return a < 3.14159 } + testCases := []intTestCase[float32]{ + { + "empty slice", + []float32{}, + 0, + }, { + "no matching elements", + []float32{4.1, 9.12}, + 0, + }, { + "matching elements", + []float32{1.0, 2.718, 5.56, -3.0}, + 3, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := slices.Count(tc.arr, lessThanPi) + got, want := result, tc.expected + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Count(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + + // Count() should equal len(Filter()) + filtered := slices.Filter(tc.arr, lessThanPi) + got = len(filtered) + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Count / len(Filter(%s)) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestIncludes(t *testing.T) { + testCases := []includesTestCase[int]{ + { + "empty slice", + []int{}, + 1, + false, + }, { + "no matching elements", + []int{0, 1}, + 2, + false, + }, { + "found element", + []int{5, 7, 8}, + 7, + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := slices.Includes(tc.arr, tc.target) + if got, want := result, tc.expected; got != want { + t.Errorf("Includes(%s) want = %v, got = %v", tc.name, want, got) + } + }) + } +} + +func TestIndexOf(t *testing.T) { + firstCoding := fhir.CodeableConcept("", fhir.Coding("test-system", "1")) + secondCoding := fhir.CodeableConcept("", fhir.Coding("test-system", "2")) + firstProto := proto.Message(firstCoding) + secondProto := proto.Message(secondCoding) + testCases := []indexTestCase[any]{ + { + "empty slice", + StrongSlice[any]{}, + firstCoding, + -1, + }, { + "no matching elements", + StrongSlice[any]{secondCoding}, + firstCoding, + -1, + }, { + "found element - struct pointer", + StrongSlice[any]{firstCoding, secondCoding}, + secondCoding, + 1, + }, { + "found element - proto pointer", + StrongSlice[any]{firstProto, secondProto}, + secondProto, + 1, + }, { + "multiple matching elements", + StrongSlice[any]{secondCoding, firstCoding, firstCoding}, + firstCoding, + 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := slices.IndexOf(tc.arr, tc.target) + if got, want := result, tc.expected; got != want { + t.Errorf("IndexOf(%s) want = %v, got = %v", tc.name, want, got) + } + }) + } +} + +func TestJoin(t *testing.T) { + testCases := []stringTestCase[uint]{ + { + "empty slice", + StrongSlice[uint]{}, + ",", + "", + }, { + "one element", + StrongSlice[uint]{0}, + ",", + "0", + }, { + "multiple elements no delimiter", + StrongSlice[uint]{0, 3}, + "", + "03", + }, { + "multiple elements with delimiter", + StrongSlice[uint]{0, 3, 7, 8}, + ",", + "0,3,7,8", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := slices.Join(tc.arr, tc.delimiter) + if got, want := result, tc.expected; got != want { + t.Errorf("Join(%s) want = %v, got = %v", tc.name, want, got) + } + }) + } +} + +func TestMap(t *testing.T) { + strLen := func(s string) int { return len(s) } + testCases := []mapTestCase[string, int]{ + { + "empty slice", + StrongSlice[string]{}, + []int{}, + }, { + "one element", + StrongSlice[string]{"four"}, + []int{4}, + }, { + "multiple elements", + StrongSlice[string]{"one", "two", "three"}, + []int{3, 3, 5}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := slices.Map(tc.arr, strLen) + got, want := result, tc.expected + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Map(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestReverse(t *testing.T) { + testCases := []arrTestCase[int]{ + { + "empty slice", + StrongSlice[int]{}, + StrongSlice[int]{}, + }, { + "one element", + StrongSlice[int]{4}, + StrongSlice[int]{4}, + }, { + "multiple elements", + StrongSlice[int]{1, 2, 3, 4, 5}, + StrongSlice[int]{5, 4, 3, 2, 1}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + slices.Reverse(tc.arr) + got, want := tc.arr, tc.expected + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Reverse(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestSort(t *testing.T) { + testCases := []arrTestCase[int]{ + { + "empty slice", + StrongSlice[int]{}, + StrongSlice[int]{}, + }, { + "one element", + StrongSlice[int]{4}, + StrongSlice[int]{4}, + }, { + "multiple elements", + StrongSlice[int]{5, 4, 3, 2, 1}, + StrongSlice[int]{1, 2, 3, 4, 5}, + }, { + "duplicate values", + StrongSlice[int]{5, 4, 3, 2, 5, 1, 3}, + StrongSlice[int]{1, 2, 3, 3, 4, 5, 5}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + slices.Sort(tc.arr) + got, want := tc.arr, tc.expected + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Sort(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +type testType int + +func (t testType) String() string { + return fmt.Sprintf("%v", int(t)) +} + +type stringer interface { + String() string +} + +func TestConvert_Upcast_ReturnsConvertedType(t *testing.T) { + input := StrongSlice[testType]{1, 2, 3} + want := []stringer{input[0], input[1], input[2]} + + got, err := slices.Convert[stringer](input) + + if err != nil { + t.Fatalf("Convert: got unexpected err %v", err) + } + if !cmp.Equal(got, want) { + t.Errorf("Convert: got %v, want %v", got, want) + } +} + +func TestConvert_Downcast_ReturnsConvertedType(t *testing.T) { + input := StrongSlice[testType]{1, 2, 3} + want := []any{input[0], input[1], input[2]} + + got, err := slices.Convert[any](input) + + if err != nil { + t.Fatalf("Convert: got unexpected err %v", err) + } + if !cmp.Equal(got, want) { + t.Errorf("Convert: got %v, want %v", got, want) + } +} + +func TestConvert_Identity_ReturnsSelf(t *testing.T) { + want := []int{1, 2, 3} + + got, err := slices.Convert[int](want) + + if err != nil { + t.Fatalf("Convert: got unexpected err %v", err) + } + if !cmp.Equal(got, want) { + t.Errorf("Convert: got %v, want %v", got, want) + } +} + +func TestConvert_InvalidCast_ReturnsErr(t *testing.T) { + want := StrongSlice[int]{1, 2, 3} + + _, err := slices.Convert[string](want) + + if err == nil { + t.Fatalf("Convert: expected err, got nil") + } +} + +func TestMustConvert_OnSuccess_ReturnsConversion(t *testing.T) { + want := []int{4, 5, 6} + input := StrongSlice[any]{4, 5, 6} + + result := slices.MustConvert[int](input) + + if got := result; !cmp.Equal(got, want) { + t.Errorf("MustConvert: got %v, want %v", got, want) + } +} + +func TestMustConvert_OnBadConversion_Panics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + input := []any{4, 5, 6} + + slices.MustConvert[string](input) +} + +func TestTransform(t *testing.T) { + testCases := []struct { + name string + slice StrongSlice[string] + transform func(string) string + want StrongSlice[string] + }{ + { + name: "append", + slice: []string{"hello", "world"}, + transform: func(s string) string { return "_" + s + "_" }, + want: []string{"_hello_", "_world_"}, + }, { + name: "trim", + slice: []string{" \thello ", "\t world\n"}, + transform: strings.TrimSpace, + want: []string{"hello", "world"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + slices.Transform(tc.slice, tc.transform) + + got, want := tc.slice, tc.want + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Transform(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestIsUnique_SimpleType(t *testing.T) { + testCases := []struct { + name string + slice StrongSlice[string] + want bool + }{ + { + name: "unique", + slice: []string{"hello", "world"}, + want: true, + }, { + name: "duplicate", + slice: []string{"hello", "hello", "world"}, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + unique := slices.IsUnique(tc.slice) + + if got, want := unique, tc.want; got != want { + t.Errorf("IsUnique(%s): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestIsUnique_Proto(t *testing.T) { + firstCoding := fhir.CodeableConcept("", fhir.Coding("test-system", "1")) + secondCoding := fhir.CodeableConcept("", fhir.Coding("test-system", "2")) + firstProto := proto.Message(firstCoding) + secondProto := proto.Message(secondCoding) + + testCases := []struct { + name string + slice StrongSlice[protoreflect.ProtoMessage] + want bool + }{ + { + name: "unique", + slice: []protoreflect.ProtoMessage{firstProto, secondProto}, + want: true, + }, { + name: "duplicate", + slice: []protoreflect.ProtoMessage{secondProto, firstProto, secondProto}, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + unique := slices.IsUnique(tc.slice) + + if got, want := unique, tc.want; got != want { + t.Errorf("IsUnique_Proto(%s): got %v, want %v", tc.name, got, want) + } + }) + } +} + +func TestUnique_SimpleType(t *testing.T) { + testCases := []struct { + name string + slice StrongSlice[string] + want StrongSlice[string] + }{ + { + name: "already unique", + slice: []string{"hello", "world"}, + want: []string{"hello", "world"}, + }, { + name: "duplicates removed", + slice: []string{"hello", "hello", "world"}, + want: []string{"hello", "world"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + uniqueSlice := slices.Unique(tc.slice) + + got, want := uniqueSlice, tc.want + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Unique(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestUnique_Proto(t *testing.T) { + firstCoding := fhir.CodeableConcept("", fhir.Coding("test-system", "1")) + secondCoding := fhir.CodeableConcept("", fhir.Coding("test-system", "2")) + firstProto := proto.Message(firstCoding) + secondProto := proto.Message(secondCoding) + + testCases := []struct { + name string + slice StrongSlice[protoreflect.ProtoMessage] + want StrongSlice[protoreflect.ProtoMessage] + }{ + { + name: "already unique", + slice: []protoreflect.ProtoMessage{firstProto, secondProto}, + want: []protoreflect.ProtoMessage{firstProto, secondProto}, + }, { + name: "duplicates removed", + slice: []protoreflect.ProtoMessage{firstProto, firstProto, secondProto}, + want: []protoreflect.ProtoMessage{firstProto, secondProto}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + uniqueSlice := slices.Unique(tc.slice) + + got, want := uniqueSlice, tc.want + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Unique(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} + +func TestChunk(t *testing.T) { + testCases := []struct { + name string + vals []int + size int + want [][]int + }{ + { + name: "less than 1 chunk size", + vals: []int{1, 2, 3, 4}, + size: 5, + want: [][]int{{1, 2, 3, 4}}, + }, + { + name: "exactly 1 chunk size", + vals: []int{1, 2, 3, 4, 5}, + size: 5, + want: [][]int{{1, 2, 3, 4, 5}}, + }, + { + name: "over 1 chunk size", + vals: []int{1, 2, 3, 4, 5, 6}, + size: 5, + want: [][]int{{1, 2, 3, 4, 5}, {6}}, + }, + { + name: "4 chunks", + vals: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + size: 3, + want: [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := slices.Chunk(tc.vals, tc.size) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Chunk(%s) mismatch (-want, +got):\n%s", tc.name, diff) + } + }) + } +} diff --git a/internal/stablerand/doc.go b/internal/stablerand/doc.go new file mode 100644 index 0000000..f20349c --- /dev/null +++ b/internal/stablerand/doc.go @@ -0,0 +1,18 @@ +/* +Package stablerand is a small helper utility that encapsulates its random engine +and always uses the same seed value for its randomness. + +This ensures reproducibility and stability across executions, giving a +pseudo-random distribution, but with deterministic predictability. This is +primarily intended for generating content for tests, which ensures that inputs +are still pseudo-random, but predictible and consistent across unchanged +executions. + +Functions in this package are thread-safe, although use in threaded contexts +will remove any guarantees of determinism. + +Note: This is primarily used internally for the fhirtest package to implement +"random" IDs and meta-IDs so that test resources retain the same general +values across executions. +*/ +package stablerand diff --git a/internal/stablerand/rand.go b/internal/stablerand/rand.go new file mode 100644 index 0000000..7c44b7c --- /dev/null +++ b/internal/stablerand/rand.go @@ -0,0 +1,120 @@ +package stablerand + +import ( + "math/rand" + "sync" + "time" +) + +var ( + // stableRand is the random engine used for generating random data in package + // fhirtest. + stableRand *rand.Rand + + // randMutex provides thread-safety for stableRand, in case any tests are + // executed with t.Parallel(). Parallelism will affect the stability of the + // randomness, since the generated values will no longer be deterministic once + // concurrency is involved; but this doesn't mean the code should fail. + randMutex sync.Mutex +) + +const ( + // randSeed is the seed used for the random engine used in package fhirtest. + // This seed is constant so that subsequent test executions will always receive + // the same data. + randSeed = 0xbadc0ffee + + // alnumAlphabet is a string containing all the upper and lowercase ascii + // characters for letters and digits. + alnumAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + // hexAlphabet is a string containing lowercase hex characters. + hexAlphabet = "abcdef0123456789" + + // decAlphabet is a string containing all decimal ascii characters. + decAlphabet = "0123456789" +) + +func init() { + // Seed the random engine with a static value so that generation is consistent + // across test executions, but still produces "unique" values. + stableRand = rand.New(rand.NewSource(randSeed)) +} + +// Intn returns, as an int, a non-negative pseudo-random number in the half-open +// interval [0,n). It panics if n <= 0. +func Intn(n int) int { + randMutex.Lock() + defer randMutex.Unlock() + + return stableRand.Intn(n) +} + +// Int63n returns, as an int64, a non-negative pseudo-random number in the +// half-open interval [0,n). It panics if n <= 0. +func Int63n(n int64) int64 { + randMutex.Lock() + defer randMutex.Unlock() + + return stableRand.Int63n(n) +} + +// String returns, as a string, a pseudo-random string containing n characters +// all consisting of values within the supplied alphabet string. +// It panics if the alphabet string is empty. +func String(n int, alphabet string) string { + if alphabet == "" { + panic("No alphabet specified") + } + randMutex.Lock() + defer randMutex.Unlock() + b := make([]rune, n) + for i := range b { + b[i] = rune(alphabet[stableRand.Intn(len(alphabet))]) + } + return string(b) +} + +// AlnumString returns, as a string, a pseudo-random string containing n +// alphanumeric characters. +func AlnumString(n int) string { + return String(n, alnumAlphabet) +} + +// HexString returns, as a string, a pseudo-random string containing n +// hex characters. +func HexString(n int) string { + return String(n, hexAlphabet) +} + +// DecString returns, as a string, a pseudo-random string containing n +// decimal characters. +func DecString(n int) string { + return String(n, decAlphabet) +} + +// Time returns, as a time.Time object, a pseudo-random time starting with the +// base time, and adding a random amount between the half-open interval +// [0, variation) to the time. It panics if variation is negative. +func Time(base time.Time, variation time.Duration) time.Time { + randMutex.Lock() + defer randMutex.Unlock() + + offset := time.Duration(stableRand.Int63n(int64(variation))) + base.Add(offset) + return base +} + +// OneOf returns, as a T object, a pseudo-randomly selected value from args. +// It panics if args is empty. +func OneOf[T any](args ...T) T { + if len(args) == 0 { + panic("No arguments specified to OneOf") + } + randMutex.Lock() + defer randMutex.Unlock() + + i := stableRand.Intn(len(args)) + + return args[i] +} diff --git a/internal/units/doc.go b/internal/units/doc.go new file mode 100644 index 0000000..6b4c9e9 --- /dev/null +++ b/internal/units/doc.go @@ -0,0 +1,5 @@ +/* +Package units provides basic unit constants that are used for various FHIR +Quantity types. +*/ +package units diff --git a/internal/units/time.go b/internal/units/time.go new file mode 100644 index 0000000..7784b1e --- /dev/null +++ b/internal/units/time.go @@ -0,0 +1,88 @@ +package units + +import "fmt" + +// Time is a unit of measure for measuring the passage of time. +type Time int + +const ( + // Nanoseconds is a Time unit that measures time in nanoseconds. + Nanoseconds Time = iota + + // Microseconds is a Time unit that measures time in microseconds. + Microseconds + + // Milliseconds is a Time unit that measures time in milliseconds. + Milliseconds + + // Seconds is a Time unit that measures time in seconds. + Seconds + + // Minutes is a Time unit that measures time in minutes. + Minutes + + // Hours is a Time unit that measures time in hours. + Hours + + // Days is a Time unit that measures time in days. + Days +) + +const ( + nanosecondSymbol = "ns" + microsecondSymbol = "us" + millisecondSymbol = "ms" + secondsSymbol = "s" + minutesSymbol = "min" + hoursSymbol = "h" + daysSymbol = "d" +) + +// Symbol returns the symbol used to represent the underlying unit. +func (t Time) Symbol() string { + switch t { + case Nanoseconds: + return nanosecondSymbol + case Microseconds: + return microsecondSymbol + case Milliseconds: + return millisecondSymbol + case Seconds: + return secondsSymbol + case Minutes: + return minutesSymbol + case Hours: + return hoursSymbol + case Days: + return daysSymbol + } + // This is a closed enumeration in an internal package. If this panic ever + // gets reached, it means that a developer is using this package wrong. + panic(fmt.Sprintf("invalid time value %v", t)) +} + +// System returns the time system that this unit comes from. +func (t Time) System() string { + return "http://unitsofmeasure.org" +} + +// TimeFromSymbol creates the Time object +func TimeFromSymbol(symbol string) (Time, error) { + switch symbol { + case nanosecondSymbol: + return Nanoseconds, nil + case microsecondSymbol: + return Microseconds, nil + case millisecondSymbol: + return Milliseconds, nil + case secondsSymbol: + return Seconds, nil + case minutesSymbol: + return Minutes, nil + case hoursSymbol: + return Hours, nil + case daysSymbol: + return Days, nil + } + return Time(0), fmt.Errorf("unknown Time symbol '%v'", symbol) +}