Skip to content

Commit 5ab5723

Browse files
AlexJSullyCopybara
andauthored
Implement resolve, has, and as (#33)
This PR includes the following: - #27 by Marwan Tammam (@Quarz0) & integrated by Vicknesh Suresh (@VickSuresh) - #30 by Marwan Tammam (@Quarz0) & integrated by Alexander Sullivan (@AlexJSully) - Go version and packages update by Arthur Pang (@arthurpang) - Go decimal package update by Joel Phillip (@joelphillip1) - Update for handling extensions by Bharath Vemula (@bharath-v1) GitOrigin-RevId: 65cbe989a46576d9ba6ee97a8d3b98a066515888 Co-authored-by: Copybara <copybara@example.com>
1 parent e6a3ed3 commit 5ab5723

File tree

13 files changed

+474
-339
lines changed

13 files changed

+474
-339
lines changed

fhirpath/fhirpath_test.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,9 @@ func testEvaluate(t *testing.T, testCases []evaluateTestCase) {
219219

220220
func TestResolve(t *testing.T) {
221221
var patientChuRef = &dtpb.Reference{
222-
Type: fhir.URI("Patient"),
223-
Id: fhir.String("123"),
222+
Reference: &dtpb.Reference_Uri{
223+
Uri: &dtpb.String{Value: "Patient/123"},
224+
},
224225
}
225226

226227
var obsWithPatientChuRef = &opb.Observation{
@@ -240,7 +241,7 @@ func TestResolve(t *testing.T) {
240241
obsWithPatientChuRef,
241242
},
242243
evaluateOptions: []fhirpath.EvaluateOption{
243-
evalopts.WithResolver(resolvertest.HappyResolver(patientChu))},
244+
evalopts.WithResolver(resolvertest.NewSimpleResolver(resolvertest.Entry("Patient/123", patientChu)))},
244245
wantCollection: system.Collection{patientChu},
245246
},
246247
{
@@ -281,7 +282,7 @@ func TestResolve(t *testing.T) {
281282
obsWithPatientTsuRef,
282283
},
283284
evaluateOptions: []fhirpath.EvaluateOption{
284-
evalopts.WithResolver(resolvertest.HappyResolver(patientChu)),
285+
evalopts.WithResolver(resolvertest.NewSimpleResolver(resolvertest.Entry("Patient/123", patientChu))),
285286
},
286287
wantCollection: system.Collection{patientChu},
287288
},
@@ -1140,6 +1141,48 @@ func TestFunctionInvocation_Evaluates(t *testing.T) {
11401141
fhir.String("Chu"),
11411142
},
11421143
},
1144+
{
1145+
name: "hasValue() returns true",
1146+
inputPath: "deceased.hasValue() and name[0].family.hasValue()",
1147+
inputCollection: []fhirpath.Resource{patientVoldemort},
1148+
wantCollection: system.Collection{system.Boolean(true)},
1149+
},
1150+
{
1151+
name: "passes through as function",
1152+
inputPath: "Patient.as(Patient)",
1153+
inputCollection: []fhirpath.Resource{patientChu},
1154+
wantCollection: system.Collection{patientChu},
1155+
},
1156+
{
1157+
name: "passes through as function for subtype relationship - fhirpath.resource",
1158+
inputPath: "Patient.name.use[0].as(FHIR.Element)",
1159+
inputCollection: []fhirpath.Resource{patientChu},
1160+
wantCollection: system.Collection{patientChu.Name[0].Use},
1161+
},
1162+
{
1163+
name: "passes through as function for subtype relationship - fhir.resource",
1164+
inputPath: "Patient.name.use[0].as(FHIR.Element)",
1165+
inputCollection: []fhir.Resource{patientChu},
1166+
wantCollection: system.Collection{patientChu.Name[0].Use},
1167+
},
1168+
{
1169+
name: "returns empty if as function is not correct type",
1170+
inputPath: "Patient.name.family[0].as(HumanName)",
1171+
inputCollection: []fhirpath.Resource{patientChu},
1172+
wantCollection: system.Collection{},
1173+
},
1174+
{
1175+
name: "unwraps polymorphic type with as function",
1176+
inputPath: "Patient.deceased.as(boolean)",
1177+
inputCollection: []fhirpath.Resource{patientVoldemort},
1178+
wantCollection: system.Collection{fhir.Boolean(true)},
1179+
},
1180+
{
1181+
name: "passes through system type with as function",
1182+
inputPath: "@2000-12-05.as(Date)",
1183+
inputCollection: []fhirpath.Resource{},
1184+
wantCollection: system.Collection{system.MustParseDate("2000-12-05")},
1185+
},
11431186
}
11441187

11451188
testEvaluate(t, testCases)

fhirpath/internal/funcs/impl/conversion.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import (
1010
"time"
1111

1212
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
13+
"github.com/verily-src/fhirpath-go/fhirpath/internal/reflection"
1314
"github.com/verily-src/fhirpath-go/fhirpath/system"
15+
"github.com/verily-src/fhirpath-go/internal/fhir"
16+
"github.com/verily-src/fhirpath-go/internal/protofields"
1417
)
1518

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

614+
// As converts the input to the specified type.
615+
// FHIRPath docs here: https://hl7.org/fhirpath/N1/#astype-type-specifier
616+
func As(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) {
617+
if len(args) != 1 {
618+
return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args))
619+
}
620+
621+
var typeSpecifier reflection.TypeSpecifier
622+
typeExpr, ok := args[0].(*expr.TypeExpression)
623+
if !ok {
624+
return nil, fmt.Errorf("received invalid argument, expected a type")
625+
}
626+
627+
var err error
628+
if parts := strings.Split(typeExpr.Type, "."); len(parts) == 2 {
629+
if typeSpecifier, err = reflection.NewQualifiedTypeSpecifier(parts[0], parts[1]); err != nil {
630+
return nil, err
631+
}
632+
} else if typeSpecifier, err = reflection.NewTypeSpecifier(typeExpr.Type); err != nil {
633+
return nil, err
634+
}
635+
636+
result := system.Collection{}
637+
for _, item := range input {
638+
inputType, err := reflection.TypeOf(item)
639+
if err != nil {
640+
return nil, err
641+
}
642+
643+
if !inputType.Is(typeSpecifier) {
644+
continue
645+
}
646+
647+
// attempt to unwrap polymorphic types
648+
message, ok := item.(fhir.Base)
649+
if !ok {
650+
result = append(result, item)
651+
} else if oneOf := protofields.UnwrapOneofField(message, "choice"); oneOf != nil {
652+
result = append(result, oneOf)
653+
} else {
654+
result = append(result, item)
655+
}
656+
}
657+
658+
return result, nil
659+
}
660+
611661
func isValidUnitConversion(outputFormat string) bool {
612662
validFormats := map[string]bool{
613663
"years": true,

fhirpath/internal/funcs/impl/conversion_test.go

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import (
44
"errors"
55
"testing"
66

7+
ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto"
8+
9+
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
710
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest"
11+
"github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl"
812
"github.com/verily-src/fhirpath-go/fhirpath/internal/reflection"
13+
"github.com/verily-src/fhirpath-go/fhirpath/system"
914
"github.com/verily-src/fhirpath-go/internal/fhir"
10-
"google.golang.org/protobuf/testing/protocmp"
1115

12-
ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto"
1316
"github.com/google/go-cmp/cmp"
1417
"github.com/google/go-cmp/cmp/cmpopts"
15-
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
16-
"github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl"
17-
"github.com/verily-src/fhirpath-go/fhirpath/system"
18+
"google.golang.org/protobuf/testing/protocmp"
1819
)
1920

2021
func TestConvertsToBoolean(t *testing.T) {
@@ -2242,3 +2243,76 @@ func TestIif(t *testing.T) {
22422243
})
22432244
}
22442245
}
2246+
2247+
func TestAs(t *testing.T) {
2248+
deceased := &ppb.Patient_DeceasedX{
2249+
Choice: &ppb.Patient_DeceasedX_Boolean{
2250+
Boolean: fhir.Boolean(true),
2251+
},
2252+
}
2253+
2254+
testCases := []struct {
2255+
name string
2256+
input system.Collection
2257+
args expr.Expression
2258+
want system.Collection
2259+
wantErr bool
2260+
}{
2261+
{
2262+
name: "input is of specified type (returns input)",
2263+
input: system.Collection{fhir.Code("#blessed")},
2264+
args: &expr.TypeExpression{Type: "FHIR.code"},
2265+
want: system.Collection{fhir.Code("#blessed")},
2266+
},
2267+
{
2268+
name: "input is not of specified type (returns empty)",
2269+
input: system.Collection{fhir.Integer(12)},
2270+
args: &expr.TypeExpression{Type: "FHIR.string"},
2271+
want: system.Collection{},
2272+
},
2273+
{
2274+
name: "input is empty collection (returns empty)",
2275+
input: system.Collection{},
2276+
args: &expr.TypeExpression{Type: "FHIR.string"},
2277+
want: system.Collection{},
2278+
},
2279+
{
2280+
name: "input is a polymorphic oneOf type",
2281+
input: system.Collection{deceased},
2282+
args: &expr.TypeExpression{Type: "FHIR.boolean"},
2283+
want: system.Collection{fhir.Boolean(true)},
2284+
},
2285+
{
2286+
name: "input is a system type",
2287+
input: system.Collection{system.Boolean(true)},
2288+
args: &expr.TypeExpression{Type: "System.Boolean"},
2289+
want: system.Collection{system.Boolean(true)},
2290+
},
2291+
{
2292+
name: "input is a collection (filters non matching types)",
2293+
input: system.Collection{system.Boolean(true), system.Integer(1)},
2294+
args: &expr.TypeExpression{Type: "System.Integer"},
2295+
want: system.Collection{system.Integer(1)},
2296+
},
2297+
{
2298+
name: "subexpression errors",
2299+
input: system.Collection{fhir.Code("#blessed")},
2300+
args: exprtest.Error(errors.New("some error")),
2301+
wantErr: true,
2302+
},
2303+
}
2304+
2305+
for _, tc := range testCases {
2306+
t.Run(tc.name, func(t *testing.T) {
2307+
got, err := impl.As(&expr.Context{}, tc.input, tc.args)
2308+
if (err != nil) != tc.wantErr {
2309+
t.Errorf("As() error = %v, wantErr %v", err, tc.wantErr)
2310+
return
2311+
}
2312+
2313+
if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
2314+
t.Errorf("As() returned unexpected diff (-want, +got)\n%s", diff)
2315+
}
2316+
})
2317+
}
2318+
}

fhirpath/internal/funcs/impl/r4.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
77
"github.com/verily-src/fhirpath-go/fhirpath/system"
88
"github.com/verily-src/fhirpath-go/internal/fhir"
9+
"github.com/verily-src/fhirpath-go/internal/protofields"
10+
11+
"google.golang.org/protobuf/reflect/protoreflect"
912
)
1013

1114
// Extension is syntactic sugar over `extension.where(url = ...)`, and is
@@ -40,3 +43,36 @@ func Extension(ctx *expr.Context, input system.Collection, args ...expr.Expressi
4043
}
4144
return result, nil
4245
}
46+
47+
// HasValue returns true if the input collection contains a single value which is a FHIR primitive,
48+
// and it has a primitive value (e.g. as opposed to not having a value and just having extensions).
49+
//
50+
// For more details, see https://hl7.org/fhir/R4/fhirpath.html#functions
51+
func HasValue(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) {
52+
if len(args) != 0 {
53+
return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args))
54+
}
55+
if !input.IsSingleton() {
56+
return system.Collection{system.Boolean(false)}, nil
57+
}
58+
59+
if primitive, ok := input[0].(fhir.Base); ok {
60+
msg := primitive.ProtoReflect()
61+
62+
// attempt to unwrap polymorphic types
63+
oneOf := protofields.UnwrapOneofField(input[0].(fhir.Base), "choice")
64+
if oneOf != nil {
65+
msg = oneOf.ProtoReflect()
66+
} else if !system.IsPrimitive(input[0]) {
67+
return system.Collection{system.Boolean(false)}, nil
68+
}
69+
70+
descriptor := msg.Descriptor()
71+
field := descriptor.Fields().ByName(protoreflect.Name("value"))
72+
if field != nil && msg.Has(field) {
73+
return system.Collection{system.Boolean(true)}, nil
74+
}
75+
}
76+
77+
return system.Collection{system.Boolean(false)}, nil
78+
}

0 commit comments

Comments
 (0)