Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/pr-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MESSAGE: >
Thank you for your interest in this project! At this moment, we are not currently accepting community contributions in the form of PRs.
If you would like to make a proposal,
we will do our best to review it, implement it ourselves, and include it in the next release.
If you would like to make a proposal,
we will do our best to review it, implement it ourselves, and include it in the next release.
If enough proposals come through, we will certainly revisit this policy to make the package as useful as possible.
[Contribution Guidelines](https://github.com/verily-src/fhirpath-go/CONTRIBUTING.md).

Expand All @@ -33,7 +33,7 @@ jobs:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
body="${PR_AUTHOR}"
body=$(echo "${body}" | sed -E -e '/OliverCardoza|bitwizeshift|VickSuresh|assefamaru|jonhayesverily|alexlaurinmath|Copybara/!d')
body=$(echo "${body}" | sed -E -e '/OliverCardoza|bitwizeshift|VickSuresh|assefamaru|jonhayesverily|alexlaurinmath|AlexJSully|Copybara/!d')

if [ -z "$body"]; then
echo "status=failure" >> "${GITHUB_OUTPUT}"
Expand Down Expand Up @@ -68,4 +68,3 @@ jobs:
continue-on-error: true
run: |
gh pr comment "${{ env.PR_URL }}" -b "${{ env.AUTHOR }} ${{ env.MESSAGE }}"

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if err != nil {
The compilation result can then be run against a resource:

```go
inputResources := []fhir.Resource{somePatient, someMedication}
inputResources := []fhirpath.Resource{somePatient, someMedication}

result, err := expression.Evaluate(inputResources)
if err != nil {
Expand Down Expand Up @@ -74,7 +74,7 @@ The constraints on external constants are as follows:

```go
customVar := system.String("custom variable")
result, err := expression.Evaluate([]fhir.Resource{someResource}, evalopts.EnvVariable("var", customVar))
result, err := expression.Evaluate([]fhirpath.Resource{someResource}, evalopts.EnvVariable("var", customVar))
```

### System Types
Expand Down
4 changes: 3 additions & 1 deletion fhirpath/compopts/compopts.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
"github.com/verily-src/fhirpath-go/fhirpath/internal/parser"
)

var ErrMultipleTransforms = errors.New("multiple transforms provided")
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.
Expand Down
9 changes: 9 additions & 0 deletions fhirpath/evalopts/evalopts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/verily-src/fhirpath-go/fhirpath/internal/opts"
"github.com/verily-src/fhirpath-go/fhirpath/resolver"
"github.com/verily-src/fhirpath-go/fhirpath/system"
"github.com/verily-src/fhirpath-go/internal/fhir"
)
Expand Down Expand Up @@ -72,3 +73,11 @@ func validateType(input any) error {
}
return err
}

// WithResolver returns an EvaluateOption that sets the FHIR reference resolver.
func WithResolver(resolver resolver.Resolver) opts.EvaluateOption {
return opts.Transform(func(cfg *opts.EvaluateConfig) error {
cfg.Context.Resolver = resolver
return nil
})
}
131 changes: 128 additions & 3 deletions fhirpath/fhirpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"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/internal/funcs/impl"
"github.com/verily-src/fhirpath-go/fhirpath/resolver/resolvertest"
"github.com/verily-src/fhirpath-go/fhirpath/system"
"github.com/verily-src/fhirpath-go/internal/element/extension"
"github.com/verily-src/fhirpath-go/internal/element/reference"
Expand All @@ -34,6 +36,7 @@ type evaluateTestCase struct {
inputPath string
inputCollection []fhirpath.Resource
wantCollection system.Collection
wantErr error
compileOptions []fhirpath.CompileOption
evaluateOptions []fhirpath.EvaluateOption
}
Expand Down Expand Up @@ -160,18 +163,91 @@ func testEvaluate(t *testing.T, testCases []evaluateTestCase) {
t.Fatalf("Compiling \"%s\" returned unexpected error: %v", tc.inputPath, err)
}

got, err := compiledExpression.Evaluate(tc.inputCollection, tc.evaluateOptions...)
got, gotErr := compiledExpression.Evaluate(tc.inputCollection, tc.evaluateOptions...)

if err != nil {
t.Fatalf("Evaluating \"%s\" returned unexpected error: %v", tc.inputPath, err)
if !cmp.Equal(gotErr, tc.wantErr, cmpopts.EquateErrors()) {
t.Errorf("Resolve() gotErr = %v, wantErr = %v", gotErr, tc.wantErr)
}

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 TestResolve(t *testing.T) {
var patientChuRef = &dtpb.Reference{
Type: fhir.URI("Patient"),
Id: fhir.String("123"),
}

var obsWithPatientChuRef = &opb.Observation{
Subject: patientChuRef,
}
var obsWithPatientTsuRef = &opb.Observation{
Subject: reference.Weak("Patient", "123456"),
}

resolveErr := errors.New("some resolve() error")

testCases := []evaluateTestCase{
{
name: "successful resolution",
inputPath: "Observation.subject.resolve()",
inputCollection: []fhirpath.Resource{
obsWithPatientChuRef,
},
evaluateOptions: []fhirpath.EvaluateOption{
evalopts.WithResolver(resolvertest.HappyResolver(patientChu))},
wantCollection: system.Collection{patientChu},
},
{
name: "Resolver not configured",
inputPath: "Observation.subject.resolve()",
inputCollection: []fhirpath.Resource{
obsWithPatientChuRef,
},
evaluateOptions: []fhirpath.EvaluateOption{},
wantErr: impl.ErrUnconfiguredResolver,
},
{
name: "resolve() returns an error",
inputPath: "Observation.subject.resolve()",
inputCollection: []fhirpath.Resource{
obsWithPatientChuRef,
},
evaluateOptions: []fhirpath.EvaluateOption{
evalopts.WithResolver(
resolvertest.ErroringResolver(resolveErr)),
},
wantErr: resolveErr,
},
{
name: "empty input",
inputPath: "Observation.subject.resolve()",
inputCollection: []fhirpath.Resource{},
evaluateOptions: []fhirpath.EvaluateOption{
evalopts.WithResolver(resolvertest.HappyResolver(patientChu)),
},
wantCollection: system.Collection{},
},
{
name: "multiple inputs",
inputPath: "Observation.subject.resolve()",
inputCollection: []fhirpath.Resource{
obsWithPatientChuRef,
obsWithPatientTsuRef,
},
evaluateOptions: []fhirpath.EvaluateOption{
evalopts.WithResolver(resolvertest.HappyResolver(patientChu)),
},
wantCollection: system.Collection{patientChu},
},
}
testEvaluate(t, testCases)
}

func TestEvaluate_PathSelection_ReturnsError(t *testing.T) {
end := system.MustParseDateTime("@2016-01-01T12:22:33Z")
task := makeTaskWithEndTime(end)
Expand Down Expand Up @@ -854,6 +930,7 @@ func TestFunctionInvocation_Evaluates(t *testing.T) {
wantCollection: system.Collection{testDateTime},
evaluateOptions: []fhirpath.EvaluateOption{evalopts.OverrideTime(testTime)},
},

{
name: "evaluate with custom function 'patient()'",
inputPath: "patient() = Patient",
Expand Down Expand Up @@ -951,6 +1028,54 @@ func TestFunctionInvocation_Evaluates(t *testing.T) {
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{system.Boolean(false), system.Boolean(true)},
},
{
name: "filters child fields with ofType()",
inputPath: "children().ofType(string)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{fhir.ID("123"), &ppb.Patient_GenderCode{Value: cpb.AdministrativeGenderCode_FEMALE}},
},
{
name: "return fhir resource with ofType()",
inputPath: "Patient.ofType(Patient)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{patientChu},
},
{
name: "return fhir resource with ofType() type and namespace",
inputPath: "Patient.ofType(FHIR.Patient)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{patientChu},
},
{
name: "return fhir resource with ofType() using base type",
inputPath: "Patient.ofType(FHIR.Resource)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{patientChu},
},
{
name: "returns empty with ofType()",
inputPath: "Patient.ofType(FHIR.Observation)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{},
},
{
name: "ofType() returns gender field",
inputPath: "Patient.gender.ofType(code)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{&ppb.Patient_GenderCode{Value: cpb.AdministrativeGenderCode_FEMALE}},
},
{
name: "ofType() returns name.use fields",
inputPath: "Patient.name.ofType(HumanName).use",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{&dtpb.HumanName_UseCode{Value: cpb.NameUseCode_NICKNAME}, &dtpb.HumanName_UseCode{Value: cpb.NameUseCode_OFFICIAL}},
},
{
name: "ofType() returns name.use fields using a base type",
inputPath: "Patient.name.ofType(FHIR.Element).use.exists()",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{system.Boolean(true)},
},
{
name: "returns concatenated family name value with join()",
inputPath: "name.family.value.join('-')",
Expand Down
99 changes: 85 additions & 14 deletions fhirpath/fhirpathtest/fhirpathtest_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
"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() {
Expand Down Expand Up @@ -50,32 +52,59 @@ func ExampleReturnCollection() {
// Output: got = true
}

// 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"},
// 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()},
}
// Create a FHIR Patient resource
patient := &pb.Patient{

testPatient = &pb.Patient{
Name: []*dtpb.HumanName{
patientOfficialName,
{
Given: []*dtpb.String{
{Value: "John"},
{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{patient}
resource := []fhirpath.Resource{testPatient}

// Evaluate the expression against the Patient resource
got, err := expr.Evaluate(resource)
Expand All @@ -84,12 +113,54 @@ func TestFHIRPathResource(t *testing.T) {
}

// Check if the results match the expected output
if len(got) != 1 {
t.Errorf("Expected 1 result, got %d", len(got))
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))
}
}
Loading