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
51 changes: 47 additions & 4 deletions fhirpath/fhirpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,9 @@ func testEvaluate(t *testing.T, testCases []evaluateTestCase) {

func TestResolve(t *testing.T) {
var patientChuRef = &dtpb.Reference{
Type: fhir.URI("Patient"),
Id: fhir.String("123"),
Reference: &dtpb.Reference_Uri{
Uri: &dtpb.String{Value: "Patient/123"},
},
}

var obsWithPatientChuRef = &opb.Observation{
Expand All @@ -240,7 +241,7 @@ func TestResolve(t *testing.T) {
obsWithPatientChuRef,
},
evaluateOptions: []fhirpath.EvaluateOption{
evalopts.WithResolver(resolvertest.HappyResolver(patientChu))},
evalopts.WithResolver(resolvertest.NewSimpleResolver(resolvertest.Entry("Patient/123", patientChu)))},
wantCollection: system.Collection{patientChu},
},
{
Expand Down Expand Up @@ -281,7 +282,7 @@ func TestResolve(t *testing.T) {
obsWithPatientTsuRef,
},
evaluateOptions: []fhirpath.EvaluateOption{
evalopts.WithResolver(resolvertest.HappyResolver(patientChu)),
evalopts.WithResolver(resolvertest.NewSimpleResolver(resolvertest.Entry("Patient/123", patientChu))),
},
wantCollection: system.Collection{patientChu},
},
Expand Down Expand Up @@ -1140,6 +1141,48 @@ func TestFunctionInvocation_Evaluates(t *testing.T) {
fhir.String("Chu"),
},
},
{
name: "hasValue() returns true",
inputPath: "deceased.hasValue() and name[0].family.hasValue()",
inputCollection: []fhirpath.Resource{patientVoldemort},
wantCollection: system.Collection{system.Boolean(true)},
},
{
name: "passes through as function",
inputPath: "Patient.as(Patient)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{patientChu},
},
{
name: "passes through as function for subtype relationship - fhirpath.resource",
inputPath: "Patient.name.use[0].as(FHIR.Element)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{patientChu.Name[0].Use},
},
{
name: "passes through as function for subtype relationship - fhir.resource",
inputPath: "Patient.name.use[0].as(FHIR.Element)",
inputCollection: []fhir.Resource{patientChu},
wantCollection: system.Collection{patientChu.Name[0].Use},
},
{
name: "returns empty if as function is not correct type",
inputPath: "Patient.name.family[0].as(HumanName)",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{},
},
{
name: "unwraps polymorphic type with as function",
inputPath: "Patient.deceased.as(boolean)",
inputCollection: []fhirpath.Resource{patientVoldemort},
wantCollection: system.Collection{fhir.Boolean(true)},
},
{
name: "passes through system type with as function",
inputPath: "@2000-12-05.as(Date)",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{system.MustParseDate("2000-12-05")},
},
}

testEvaluate(t, testCases)
Expand Down
50 changes: 50 additions & 0 deletions fhirpath/internal/funcs/impl/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import (
"time"

"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
"github.com/verily-src/fhirpath-go/fhirpath/internal/reflection"
"github.com/verily-src/fhirpath-go/fhirpath/system"
"github.com/verily-src/fhirpath-go/internal/fhir"
"github.com/verily-src/fhirpath-go/internal/protofields"
)

// DefaultQuantityUnit is defined by the following FHIRPath rules:
Expand Down Expand Up @@ -608,6 +611,53 @@ func Iif(ctx *expr.Context, input system.Collection, args ...expr.Expression) (s
return args[1].Evaluate(ctx, input)
}

// As converts the input to the specified type.
// FHIRPath docs here: https://hl7.org/fhirpath/N1/#astype-type-specifier
func As(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))
}

var typeSpecifier reflection.TypeSpecifier
typeExpr, ok := args[0].(*expr.TypeExpression)
if !ok {
return nil, fmt.Errorf("received invalid argument, expected a type")
}

var err error
if parts := strings.Split(typeExpr.Type, "."); len(parts) == 2 {
if typeSpecifier, err = reflection.NewQualifiedTypeSpecifier(parts[0], parts[1]); err != nil {
return nil, err
}
} else if typeSpecifier, err = reflection.NewTypeSpecifier(typeExpr.Type); err != nil {
return nil, err
}

result := system.Collection{}
for _, item := range input {
inputType, err := reflection.TypeOf(item)
if err != nil {
return nil, err
}

if !inputType.Is(typeSpecifier) {
continue
}

// attempt to unwrap polymorphic types
message, ok := item.(fhir.Base)
if !ok {
result = append(result, item)
} else if oneOf := protofields.UnwrapOneofField(message, "choice"); oneOf != nil {
result = append(result, oneOf)
} else {
result = append(result, item)
}
}

return result, nil
}

func isValidUnitConversion(outputFormat string) bool {
validFormats := map[string]bool{
"years": true,
Expand Down
84 changes: 79 additions & 5 deletions fhirpath/internal/funcs/impl/conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import (
"errors"
"testing"

ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto"

"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/internal/reflection"
"github.com/verily-src/fhirpath-go/fhirpath/system"
"github.com/verily-src/fhirpath-go/internal/fhir"
"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/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"
"google.golang.org/protobuf/testing/protocmp"
)

func TestConvertsToBoolean(t *testing.T) {
Expand Down Expand Up @@ -2242,3 +2243,76 @@ func TestIif(t *testing.T) {
})
}
}

func TestAs(t *testing.T) {
deceased := &ppb.Patient_DeceasedX{
Choice: &ppb.Patient_DeceasedX_Boolean{
Boolean: fhir.Boolean(true),
},
}

testCases := []struct {
name string
input system.Collection
args expr.Expression
want system.Collection
wantErr bool
}{
{
name: "input is of specified type (returns input)",
input: system.Collection{fhir.Code("#blessed")},
args: &expr.TypeExpression{Type: "FHIR.code"},
want: system.Collection{fhir.Code("#blessed")},
},
{
name: "input is not of specified type (returns empty)",
input: system.Collection{fhir.Integer(12)},
args: &expr.TypeExpression{Type: "FHIR.string"},
want: system.Collection{},
},
{
name: "input is empty collection (returns empty)",
input: system.Collection{},
args: &expr.TypeExpression{Type: "FHIR.string"},
want: system.Collection{},
},
{
name: "input is a polymorphic oneOf type",
input: system.Collection{deceased},
args: &expr.TypeExpression{Type: "FHIR.boolean"},
want: system.Collection{fhir.Boolean(true)},
},
{
name: "input is a system type",
input: system.Collection{system.Boolean(true)},
args: &expr.TypeExpression{Type: "System.Boolean"},
want: system.Collection{system.Boolean(true)},
},
{
name: "input is a collection (filters non matching types)",
input: system.Collection{system.Boolean(true), system.Integer(1)},
args: &expr.TypeExpression{Type: "System.Integer"},
want: system.Collection{system.Integer(1)},
},
{
name: "subexpression errors",
input: system.Collection{fhir.Code("#blessed")},
args: exprtest.Error(errors.New("some error")),
wantErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := impl.As(&expr.Context{}, tc.input, tc.args)
if (err != nil) != tc.wantErr {
t.Errorf("As() error = %v, wantErr %v", err, tc.wantErr)
return
}

if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
t.Errorf("As() returned unexpected diff (-want, +got)\n%s", diff)
}
})
}
}
36 changes: 36 additions & 0 deletions fhirpath/internal/funcs/impl/r4.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
"github.com/verily-src/fhirpath-go/fhirpath/system"
"github.com/verily-src/fhirpath-go/internal/fhir"
"github.com/verily-src/fhirpath-go/internal/protofields"

"google.golang.org/protobuf/reflect/protoreflect"
)

// Extension is syntactic sugar over `extension.where(url = ...)`, and is
Expand Down Expand Up @@ -40,3 +43,36 @@ func Extension(ctx *expr.Context, input system.Collection, args ...expr.Expressi
}
return result, nil
}

// HasValue returns true if the input collection contains a single value which is a FHIR primitive,
// and it has a primitive value (e.g. as opposed to not having a value and just having extensions).
//
// For more details, see https://hl7.org/fhir/R4/fhirpath.html#functions
func HasValue(ctx *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))
}
if !input.IsSingleton() {
return system.Collection{system.Boolean(false)}, nil
}

if primitive, ok := input[0].(fhir.Base); ok {
msg := primitive.ProtoReflect()

// attempt to unwrap polymorphic types
oneOf := protofields.UnwrapOneofField(input[0].(fhir.Base), "choice")
if oneOf != nil {
msg = oneOf.ProtoReflect()
} else if !system.IsPrimitive(input[0]) {
return system.Collection{system.Boolean(false)}, nil
}

descriptor := msg.Descriptor()
field := descriptor.Fields().ByName(protoreflect.Name("value"))
if field != nil && msg.Has(field) {
return system.Collection{system.Boolean(true)}, nil
}
}

return system.Collection{system.Boolean(false)}, nil
}
Loading