From d5b4dd6fb745a16c49a4607e3bd5cc3742c03f3d Mon Sep 17 00:00:00 2001 From: Copybara Date: Tue, 9 Sep 2025 14:42:01 -0400 Subject: [PATCH] Project import generated by Copybara. GitOrigin-RevId: 520ee835d324807ada9458b1e75563cddacb5818 --- fhirpath/fhirpath_test.go | 397 +++++++++++++----- .../fhirpathtest/fhirpathtest_example_test.go | 119 ------ fhirpath/internal/funcs/impl/existence.go | 79 ++++ .../internal/funcs/impl/existence_test.go | 318 ++++++++++++++ fhirpath/internal/funcs/table.go | 7 +- .../resolver/resolvertest/resolvertest.go | 3 + fhirpath/system/quantity.go | 2 +- 7 files changed, 706 insertions(+), 219 deletions(-) diff --git a/fhirpath/fhirpath_test.go b/fhirpath/fhirpath_test.go index d00b771..2a60de2 100644 --- a/fhirpath/fhirpath_test.go +++ b/fhirpath/fhirpath_test.go @@ -14,13 +14,13 @@ import ( 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" + qrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_response_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/fhirpath" "github.com/verily-src/fhirpath-go/fhirpath/compopts" "github.com/verily-src/fhirpath-go/fhirpath/evalopts" + "github.com/verily-src/fhirpath-go/fhirpath/fhirjson" "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" "github.com/verily-src/fhirpath-go/fhirpath/resolver/resolvertest" "github.com/verily-src/fhirpath-go/fhirpath/system" @@ -28,6 +28,10 @@ import ( "github.com/verily-src/fhirpath-go/internal/element/reference" "github.com/verily-src/fhirpath-go/internal/fhir" "github.com/verily-src/fhirpath-go/internal/fhirconv" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/shopspring/decimal" "google.golang.org/protobuf/testing/protocmp" ) @@ -41,119 +45,156 @@ type evaluateTestCase struct { 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}, +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, + 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"), }, - Given: []*dtpb.String{fhir.String("Senpai")}, - Family: fhir.String("Chu"), }, - { - Use: &dtpb.HumanName_UseCode{ - Value: cpb.NameUseCode_OFFICIAL, + Contact: []*ppb.Patient_Contact{ + { + Name: &dtpb.HumanName{ + Given: []*dtpb.String{fhir.String("Senpai")}, + Family: fhir.String("Rodusek"), + }, }, - 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"), + } + fooExtension, _ = extension.FromElement("foourl", fhir.String("foovalue")) + barExtension, _ = extension.FromElement("barurl", fhir.String("barvalue")) + nameVoldemort = &dtpb.HumanName{ + Given: []*dtpb.String{ + fhir.String("Lord"), + }, + Family: fhir.String("Voldemort"), + } + 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), }, }, - }, -} -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"), + MultipleBirth: &ppb.Patient_MultipleBirthX{ + Choice: &ppb.Patient_MultipleBirthX_Integer{ + Integer: fhir.Integer(int32(2)), }, }, - }, - 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", + Meta: &dtpb.Meta{ + Tag: []*dtpb.Coding{ + { + Code: fhir.Code("#blessed"), }, - Url: fhir.URL("http://image"), - Title: fhir.String("title"), }, }, - }, -} -var questionnaireRef, _ = reference.Typed("Questionnaire", "1234") -var obsWithRef = &opb.Observation{ - Meta: &dtpb.Meta{ + Name: []*dtpb.HumanName{nameVoldemort}, Extension: []*dtpb.Extension{ + fooExtension, + barExtension, + }, + } + docRef = &drpb.DocumentReference{ + Status: &drpb.DocumentReference_StatusCode{ + Value: cpb.DocumentReferenceStatusCode_CURRENT, + }, + Content: []*drpb.DocumentReference_Content{ { - Url: fhir.URI("https://extension"), - Value: &dtpb.Extension_ValueX{ - Choice: &dtpb.Extension_ValueX_Reference{ - Reference: questionnaireRef, + Attachment: &dtpb.Attachment{ + ContentType: &dtpb.Attachment_ContentTypeCode{ + Value: "image", }, + Url: fhir.URL("http://image"), + Title: fhir.String("title"), }, }, }, - }, - DerivedFrom: []*dtpb.Reference{ - questionnaireRef, - }, -} -var listWithNilRef = &lpb.List{ - Entry: []*lpb.List_Entry{ - {Item: &dtpb.Reference{Type: fhir.URI("Location")}}, - }, -} + } + questionnaireRef, _ = reference.Typed("Questionnaire", "1234") + 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, + }, + } + listWithNilRef = &lpb.List{ + Entry: []*lpb.List_Entry{ + {Item: &dtpb.Reference{Type: fhir.URI("Location")}}, + }, + } + + patientOfficialName = &dtpb.HumanName{ + Given: []*dtpb.String{ + {Value: "John"}, + }, + Family: &dtpb.String{Value: "Doe"}, + Use: &dtpb.HumanName_UseCode{Value: *cpb.NameUseCode_OFFICIAL.Enum()}, + } + + testPatient = &ppb.Patient{ + Name: []*dtpb.HumanName{ + patientOfficialName, + { + Given: []*dtpb.String{ + {Value: "Johnny"}, + }, + Family: &dtpb.String{Value: "Doe"}, + Use: &dtpb.HumanName_UseCode{Value: *cpb.NameUseCode_NICKNAME.Enum()}, + }, + }, + } + + testQuestionnaireResponse = &qrpb.QuestionnaireResponse{ + Item: []*qrpb.QuestionnaireResponse_Item{ + { + Answer: []*qrpb.QuestionnaireResponse_Item_Answer{ + {Value: &qrpb.QuestionnaireResponse_Item_Answer_ValueX{ + Choice: &qrpb.QuestionnaireResponse_Item_Answer_ValueX_Coding{ + Coding: &dtpb.Coding{Code: &dtpb.Code{Value: "foo"}}, + }, + }}, + }, + }, + }, + } +) func testEvaluate(t *testing.T, testCases []evaluateTestCase) { for _, tc := range testCases { @@ -1598,3 +1639,163 @@ func TestMustCompile_ValidExpression_ReturnsExpression(t *testing.T) { t.Errorf("MustCompile: Expected result") } } + +// Testing that by using only the fhirpath package, we can still evaluate +// a FHIRPath expression that returns a resource. +func TestFHIRPathResource(t *testing.T) { + want := system.Collection{ + &dtpb.String{Value: "John"}, + } + + // Compile the FHIRPath expression + expr := fhirpath.MustCompile("name.given") + + // Wrap the Patient resource in a FHIRPath Resource + resource := []fhirpath.Resource{testPatient} + + // Evaluate the expression against the Patient resource + got, err := expr.Evaluate(resource) + if err != nil { + t.Errorf("Error evaluating FHIRPath expression: %v", err) + } + + // Check if the results match the expected output + if len(got) != 2 { + t.Errorf("Expected 2 results, got %d", len(got)) + } + gotValue := got[0].(*dtpb.String).GetValue() + wantValue := want[0].(*dtpb.String).GetValue() + if gotValue != wantValue { + t.Errorf("Expected %s, got %s", wantValue, gotValue) + } +} + +func TestFHIRPathWhere(t *testing.T) { + resource := []fhirpath.Resource{testPatient} + + // Test where: name is official (cpb.NameUseCode_OFFICIAL) + exprWhere := fhirpath.MustCompile("name.where(use = 'official')") + gotWhere, err := exprWhere.Evaluate(resource) + if err != nil { + t.Errorf("Error evaluating where: %v", err) + } + wantWhere := patientOfficialName + + if len(gotWhere) != 1 { + t.Errorf("Expected 1 result from where, got %d", len(gotWhere)) + } + if len(gotWhere) == 1 { + gotValue := gotWhere[0].(*dtpb.HumanName).GetGiven()[0].GetValue() + wantValue := wantWhere.GetGiven()[0].GetValue() + if gotValue != wantValue { + t.Errorf("Expected %s from where, got %q", wantValue, gotValue) + } + } +} + +func TestFHIRPathCombine(t *testing.T) { + // Use the testQuestionnaireResponse defined above (empty for now, but can be extended) + resource := []fhirpath.Resource{testQuestionnaireResponse} + + // Evaluate the FHIRPath expression: QuestionnaireResponse.item.answer.combine(today()) + expr := fhirpath.MustCompile("item.answer.combine(today())") + got, err := expr.Evaluate(resource) + if err != nil { + t.Fatalf("Error evaluating combine: %v", err) + } + + if got == nil { + t.Errorf("Expected non-nil result from combine, got nil") + } + if len(got) != 2 { + t.Errorf("Expected 2 results from combine, got %d", len(got)) + } +} + +func TestFHIRPathUnmarshalJSON(t *testing.T) { + // JSON data from an endpoint + jsonData := []byte(`{ + "resourceType": "Patient", + "id": "example-patient", + "name": [ + { + "use": "official", + "given": ["John"], + "family": "Doe" + } + ], + "gender": "male", + "address": [ + { + "line": ["123 Main Street"], + "city": "Toronto", + "country": "CA" + } + ], + "telecom": [ + { + "system": "phone", + "value": "123-456-7890", + "use": "home" + } + ] + }`) + + // Convert JSON to FHIRPath Resource using the system package + resource, err := fhirjson.UnmarshalNew(jsonData) + if err != nil { + t.Fatalf("Failed to unmarshal FHIR resource: %v", err) + } + + // Test additional FHIRPath expressions + testCases := []struct { + name string + expression string + expectedCount int + expectedValue string + }{ + { + name: "Extract family name", + expression: "name.family", + expectedCount: 1, + expectedValue: "Doe", + }, + { + name: "Extract phone number", + expression: "telecom.where(system='phone').value", + expectedCount: 1, + expectedValue: "123-456-7890", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expr := fhirpath.MustCompile(tc.expression) + results, err := expr.Evaluate([]fhirpath.Resource{resource}) + if err != nil { + t.Errorf("Error evaluating expression %q: %v", tc.expression, err) + return + } + + if len(results) != tc.expectedCount { + t.Errorf("Expected %d results for %q, got %d", tc.expectedCount, tc.expression, len(results)) + return + } + + if len(results) > 0 { + var gotValue string + switch v := results[0].(type) { + case *dtpb.String: + gotValue = v.GetValue() + default: + t.Errorf("Unexpected result type: %T", v) + return + } + + if gotValue != tc.expectedValue { + t.Errorf("Expected %q for expression %q, got %q", tc.expectedValue, tc.expression, gotValue) + } + } + }) + } +} diff --git a/fhirpath/fhirpathtest/fhirpathtest_example_test.go b/fhirpath/fhirpathtest/fhirpathtest_example_test.go index 4a02d3e..591535b 100644 --- a/fhirpath/fhirpathtest/fhirpathtest_example_test.go +++ b/fhirpath/fhirpathtest/fhirpathtest_example_test.go @@ -3,16 +3,10 @@ package fhirpathtest_test import ( "errors" "fmt" - "testing" "github.com/verily-src/fhirpath-go/fhirpath" "github.com/verily-src/fhirpath-go/fhirpath/fhirpathtest" "github.com/verily-src/fhirpath-go/fhirpath/system" - - 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" - pb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" - qrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/questionnaire_response_go_proto" ) func ExampleError() { @@ -51,116 +45,3 @@ func ExampleReturnCollection() { fmt.Printf("got = %v", bool(got[0].(system.Boolean))) // Output: got = true } - -// Create a FHIR Patient resource -var ( - patientOfficialName = &dtpb.HumanName{ - Given: []*dtpb.String{ - {Value: "John"}, - }, - Family: &dtpb.String{Value: "Doe"}, - Use: &dtpb.HumanName_UseCode{Value: *cpb.NameUseCode_OFFICIAL.Enum()}, - } - - testPatient = &pb.Patient{ - Name: []*dtpb.HumanName{ - patientOfficialName, - { - Given: []*dtpb.String{ - {Value: "Johnny"}, - }, - Family: &dtpb.String{Value: "Doe"}, - Use: &dtpb.HumanName_UseCode{Value: *cpb.NameUseCode_NICKNAME.Enum()}, - }, - }, - } - - testQuestionnaireResponse = &qrpb.QuestionnaireResponse{ - Item: []*qrpb.QuestionnaireResponse_Item{ - { - Answer: []*qrpb.QuestionnaireResponse_Item_Answer{ - {Value: &qrpb.QuestionnaireResponse_Item_Answer_ValueX{ - Choice: &qrpb.QuestionnaireResponse_Item_Answer_ValueX_Coding{ - Coding: &dtpb.Coding{Code: &dtpb.Code{Value: "foo"}}, - }, - }}, - }, - }, - }, - } -) - -// TestFHIRPathResource is based on issue can't call Evaluate() because fhir.Resource is internal? #18 -// https://github.com/verily-src/fhirpath-go/issues/18 -// -// Testing that by using only the fhirpath package, we can still evaluate -// a FHIRPath expression that returns a resource. -func TestFHIRPathResource(t *testing.T) { - want := system.Collection{ - &dtpb.String{Value: "John"}, - } - - // Compile the FHIRPath expression - expr := fhirpath.MustCompile("name.given") - - // Wrap the Patient resource in a FHIRPath Resource - resource := []fhirpath.Resource{testPatient} - - // Evaluate the expression against the Patient resource - got, err := expr.Evaluate(resource) - if err != nil { - t.Errorf("Error evaluating FHIRPath expression: %v", err) - } - - // Check if the results match the expected output - if len(got) != 2 { - t.Errorf("Expected 2 results, got %d", len(got)) - } - gotValue := got[0].(*dtpb.String).GetValue() - wantValue := want[0].(*dtpb.String).GetValue() - if gotValue != wantValue { - t.Errorf("Expected %s, got %s", wantValue, gotValue) - } -} - -func TestFHIRPathWhere(t *testing.T) { - resource := []fhirpath.Resource{testPatient} - - // Test where: name is official (cpb.NameUseCode_OFFICIAL) - exprWhere := fhirpath.MustCompile("name.where(use = 'official')") - gotWhere, err := exprWhere.Evaluate(resource) - if err != nil { - t.Errorf("Error evaluating where: %v", err) - } - wantWhere := patientOfficialName - - if len(gotWhere) != 1 { - t.Errorf("Expected 1 result from where, got %d", len(gotWhere)) - } - if len(gotWhere) == 1 { - gotValue := gotWhere[0].(*dtpb.HumanName).GetGiven()[0].GetValue() - wantValue := wantWhere.GetGiven()[0].GetValue() - if gotValue != wantValue { - t.Errorf("Expected %s from where, got %q", wantValue, gotValue) - } - } -} - -func TestFHIRPathCombine(t *testing.T) { - // Use the testQuestionnaireResponse defined above (empty for now, but can be extended) - resource := []fhirpath.Resource{testQuestionnaireResponse} - - // Evaluate the FHIRPath expression: QuestionnaireResponse.item.answer.combine(today()) - expr := fhirpath.MustCompile("item.answer.combine(today())") - got, err := expr.Evaluate(resource) - if err != nil { - t.Fatalf("Error evaluating combine: %v", err) - } - - if got == nil { - t.Errorf("Expected non-nil result from combine, got nil") - } - if len(got) != 2 { - t.Errorf("Expected 2 results from combine, got %d", len(got)) - } -} diff --git a/fhirpath/internal/funcs/impl/existence.go b/fhirpath/internal/funcs/impl/existence.go index e26c70b..6ee2dd1 100644 --- a/fhirpath/internal/funcs/impl/existence.go +++ b/fhirpath/internal/funcs/impl/existence.go @@ -5,6 +5,7 @@ import ( "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" "github.com/verily-src/fhirpath-go/fhirpath/system" + "google.golang.org/protobuf/proto" ) // AllTrue Takes a collection of Boolean values and returns true if all the items are true. @@ -140,3 +141,81 @@ func Empty(ctx *expr.Context, input system.Collection, args ...expr.Expression) } return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) } + +// SubsetOf returns true if all items in the input collection are members of the +// collection passed as the other argument. Membership is determined using the +// = (Equals) operation. +// +// If the input collection is empty, the result is true. +// If the other collection is empty but input is not, the result is false. +// +// https://hl7.org/fhirpath/N1/index.html#subsetofother-collection-boolean +func SubsetOf(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { + // Validate exactly one argument is provided + if len(args) != 1 { + return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) + } + + // Evaluate the other collection argument in the current context + otherCollection, err := args[0].Evaluate(ctx, input) + if err != nil { + return nil, err + } + + // Empty input collection is always a subset (conceptually true) + if input.IsEmpty() { + return system.Collection{system.Boolean(true)}, nil + } + + // Non-empty input with empty other collection means false + if otherCollection.IsEmpty() { + return system.Collection{system.Boolean(false)}, nil + } + + // Implement bag semantics using a boolean slice to track used elements. + used := make([]bool, len(otherCollection)) + + // Check each input element for membership in the other collection + for _, inputItem := range input { + found := false + + // Search for an unused matching element in the other collection + for i, otherItem := range otherCollection { + // Skip if this element has already been used (bag semantics) + if used[i] { + continue + } + + // Convert both items to system types and check equality + sysInput, errInput := system.From(inputItem) + sysOther, errOther := system.From(otherItem) + + // Use FHIRPath equality if both conversions succeeded + if errInput == nil && errOther == nil { + if eq, hasResult := system.TryEqual(sysInput, sysOther); hasResult && eq { + // Element found - mark it as used to prevent reuse (bag semantics) + used[i] = true + found = true + break + } + } else { + // Fallback to proto.Equal comparison + inputMsg, inputOK := inputItem.(proto.Message) + otherMsg, otherOK := otherItem.(proto.Message) + if inputOK && otherOK && proto.Equal(inputMsg, otherMsg) { + // Proto equality check succeeded, mark it as used + used[i] = true + found = true + break + } + } + } + + // If no matching unused element found, input is not a subset + if !found { + return system.Collection{system.Boolean(false)}, nil + } + } + + return system.Collection{system.Boolean(true)}, nil +} diff --git a/fhirpath/internal/funcs/impl/existence_test.go b/fhirpath/internal/funcs/impl/existence_test.go index 3a5cb4e..0b71e8a 100644 --- a/fhirpath/internal/funcs/impl/existence_test.go +++ b/fhirpath/internal/funcs/impl/existence_test.go @@ -665,3 +665,321 @@ func TestEmpty_RaisesError(t *testing.T) { }) } } + +func TestSubsetOf_Success(t *testing.T) { + testCases := []struct { + name string + input system.Collection + args []expr.Expression + want system.Collection + }{ + { + name: "returns true if input is empty", + input: system.Collection{}, + args: []expr.Expression{exprtest.Return(system.Integer(1), system.Integer(2))}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false if argument collection is empty but input is not", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + }, + args: []expr.Expression{exprtest.Return()}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true if input is subset of argument collection", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + }, + args: []expr.Expression{exprtest.Return( + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false if input is not subset of argument collection", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(5), + }, + args: []expr.Expression{exprtest.Return( + system.Integer(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + )}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true if input equals argument collection", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + system.Integer(3), + }, + args: []expr.Expression{exprtest.Return( + system.Integer(1), + system.Integer(2), + system.Integer(3), + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns true with fhir.Integer types - subset case", + input: system.Collection{ + fhir.Integer(1), + fhir.Integer(2), + }, + args: []expr.Expression{exprtest.Return( + fhir.Integer(1), + fhir.Integer(2), + fhir.Integer(3), + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false with fhir.Integer types - not subset case", + input: system.Collection{ + fhir.Integer(1), + fhir.Integer(4), + }, + args: []expr.Expression{exprtest.Return( + fhir.Integer(1), + fhir.Integer(2), + fhir.Integer(3), + )}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true with mixed system and fhir types", + input: system.Collection{ + system.Integer(1), + fhir.Integer(2), + }, + args: []expr.Expression{exprtest.Return( + fhir.Integer(1), + system.Integer(2), + system.Integer(3), + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns true with string collections - subset case", + input: system.Collection{ + system.String("a"), + system.String("b"), + }, + args: []expr.Expression{exprtest.Return( + system.String("a"), + system.String("b"), + system.String("c"), + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false with string collections - not subset case", + input: system.Collection{ + system.String("a"), + system.String("d"), + }, + args: []expr.Expression{exprtest.Return( + system.String("a"), + system.String("b"), + system.String("c"), + )}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true with coding collections - subset case", + input: system.Collection{ + fhir.Coding("loinc-system", "loinc-code"), + fhir.Coding("snomed-system", "snomed-code"), + }, + args: []expr.Expression{exprtest.Return( + fhir.Coding("loinc-system", "loinc-code"), + fhir.Coding("snomed-system", "snomed-code"), + fhir.Coding("icd10-system", "icd10-code"), + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false with coding collections - not subset case", + input: system.Collection{ + fhir.Coding("loinc-system", "loinc-code"), + fhir.Coding("missing-system", "missing-code"), + }, + args: []expr.Expression{exprtest.Return( + fhir.Coding("loinc-system", "loinc-code"), + fhir.Coding("snomed-system", "snomed-code"), + )}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "treats FHIR messages with different field orders as equivalent", + input: system.Collection{ + &dtpb.HumanName{ + Given: []*dtpb.String{fhir.String("John")}, + Family: fhir.String("Doe"), + }, + }, + args: []expr.Expression{exprtest.Return( + &dtpb.HumanName{ + Family: fhir.String("Doe"), + Given: []*dtpb.String{fhir.String("John")}, + }, + &dtpb.HumanName{ + Given: []*dtpb.String{fhir.String("Jane")}, + Family: fhir.String("Smith"), + }, + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false for large collections - element not in superset", + 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(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(6), + system.Integer(7), + )}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true for large collections - proper subset", + 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(1), + system.Integer(2), + system.Integer(3), + system.Integer(4), + system.Integer(5), + system.Integer(6), + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns false for multiset - duplicate elements in input", + input: system.Collection{ + system.Integer(1), + system.Integer(1), + }, + args: []expr.Expression{exprtest.Return( + system.Integer(1), + system.Integer(2), + )}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "returns false for multiset - input has more duplicates than other collection", + input: system.Collection{ + system.Integer(1), + system.Integer(1), + system.Integer(1), + }, + args: []expr.Expression{exprtest.Return( + system.Integer(1), + system.Integer(2), + system.Integer(3), + )}, + want: system.Collection{system.Boolean(false)}, + }, + { + name: "returns true for multiset - input has fewer duplicates than other collection", + input: system.Collection{ + system.Integer(1), + system.Integer(2), + }, + args: []expr.Expression{exprtest.Return( + system.Integer(1), + system.Integer(1), + system.Integer(2), + system.Integer(2), + )}, + want: system.Collection{system.Boolean(true)}, + }, + { + name: "returns true for single element subset", + input: system.Collection{system.Integer(1)}, + args: []expr.Expression{exprtest.Return(system.Integer(1), system.Integer(2))}, + want: system.Collection{system.Boolean(true)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := impl.SubsetOf(&expr.Context{}, tc.input, tc.args...) + if err != nil { + t.Errorf("SubsetOf() error = %v", err) + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("SubsetOf() returned unexpected diff (-want, +got)\n%s", diff) + } + }) + } +} + +func TestSubsetOf_Errors(t *testing.T) { + testCases := []struct { + name string + inputArgs []expr.Expression + inputCollection system.Collection + wantErrMsg string + }{ + { + name: "no arguments provided", + inputArgs: []expr.Expression{}, + inputCollection: system.Collection{system.Integer(1), system.Integer(2)}, + wantErrMsg: "incorrect function arity: received 0 arguments, expected 1", + }, + { + name: "multiple arguments provided", + inputArgs: []expr.Expression{ + exprtest.Return(system.Integer(1)), + exprtest.Return(system.Integer(2)), + }, + inputCollection: system.Collection{system.Integer(1)}, + wantErrMsg: "incorrect function arity: received 2 arguments, expected 1", + }, + { + name: "argument expression raises error", + inputArgs: []expr.Expression{exprtest.Error(errors.New("some error"))}, + inputCollection: system.Collection{system.Integer(1)}, + wantErrMsg: "some error", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := impl.SubsetOf(&expr.Context{}, tc.inputCollection, tc.inputArgs...) + if err == nil { + t.Fatalf("evaluating SubsetOf function didn't return error when expected") + } + + if err.Error() != tc.wantErrMsg { + t.Errorf("SubsetOf() error = %v, wantErrMsg %v", err, tc.wantErrMsg) + } + }) + } +} diff --git a/fhirpath/internal/funcs/table.go b/fhirpath/internal/funcs/table.go index 347b8fa..20745dc 100644 --- a/fhirpath/internal/funcs/table.go +++ b/fhirpath/internal/funcs/table.go @@ -56,7 +56,12 @@ var baseTable = FunctionTable{ 0, false, }, - "subsetOf": notImplemented, + "subsetOf": Function{ + impl.SubsetOf, + 1, + 1, + false, + }, "supersetOf": notImplemented, "count": Function{ impl.Count, diff --git a/fhirpath/resolver/resolvertest/resolvertest.go b/fhirpath/resolver/resolvertest/resolvertest.go index b42c075..fc63e16 100644 --- a/fhirpath/resolver/resolvertest/resolvertest.go +++ b/fhirpath/resolver/resolvertest/resolvertest.go @@ -10,16 +10,19 @@ import ( type resolverFunc func(input []string) ([]fhir.Resource, error) +// Resolve calls the underlying resolverFunc, fulfilling the resolver.Resolver interface. func (rf resolverFunc) Resolve(input []string) ([]fhir.Resource, error) { return rf(input) } +// HappyResolver returns a resolver.Resolver that always returns the provided resources and no error. func HappyResolver(resources ...fhirpath.Resource) resolver.Resolver { return resolverFunc(func(input []string) ([]fhir.Resource, error) { return resources, nil }) } +// ErroringResolver returns a resolver.Resolver that always returns the given error and no resources. func ErroringResolver(err error) resolver.Resolver { return resolverFunc(func(input []string) ([]fhir.Resource, error) { return nil, err diff --git a/fhirpath/system/quantity.go b/fhirpath/system/quantity.go index 879f91d..34b0ef8 100644 --- a/fhirpath/system/quantity.go +++ b/fhirpath/system/quantity.go @@ -120,8 +120,8 @@ func (q Quantity) ToProtoQuantity() *dtpb.Quantity { return res } -// TODO: According to our initial investigations, Equal should return empty when value is not set. // Equal method to override cmp.Equal. +// TODO: According to our initial investigations, Equal should return empty when value is not set. func (q Quantity) Equal(q2 Quantity) bool { return q.value.Equal(q2.value) && q.unit == q2.unit }