Skip to content

Commit c3f49a2

Browse files
committed
1 parent 4ed96d4 commit c3f49a2

File tree

6 files changed

+304
-0
lines changed

6 files changed

+304
-0
lines changed

fhirpath/fhirpath_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,48 @@ func TestFunctionInvocation_Evaluates(t *testing.T) {
11301130
wantCollection: system.Collection{system.String("Chu-Chu")},
11311131
compileOptions: []fhirpath.CompileOption{compopts.WithExperimentalFuncs()},
11321132
},
1133+
{
1134+
name: "hasValue() returns true",
1135+
inputPath: "deceased.hasValue() and name[0].family.hasValue()",
1136+
inputCollection: []fhirpath.Resource{patientVoldemort},
1137+
wantCollection: system.Collection{system.Boolean(true)},
1138+
},
1139+
{
1140+
name: "passes through as function",
1141+
inputPath: "Patient.as(Patient)",
1142+
inputCollection: []fhirpath.Resource{patientChu},
1143+
wantCollection: system.Collection{patientChu},
1144+
},
1145+
{
1146+
name: "passes through as function for subtype relationship - fhirpath.resource",
1147+
inputPath: "Patient.name.use[0].as(FHIR.Element)",
1148+
inputCollection: []fhirpath.Resource{patientChu},
1149+
wantCollection: system.Collection{patientChu.Name[0].Use},
1150+
},
1151+
{
1152+
name: "passes through as function for subtype relationship - fhir.resource",
1153+
inputPath: "Patient.name.use[0].as(FHIR.Element)",
1154+
inputCollection: []fhir.Resource{patientChu},
1155+
wantCollection: system.Collection{patientChu.Name[0].Use},
1156+
},
1157+
{
1158+
name: "returns empty if as function is not correct type",
1159+
inputPath: "Patient.name.family[0].as(HumanName)",
1160+
inputCollection: []fhirpath.Resource{patientChu},
1161+
wantCollection: system.Collection{},
1162+
},
1163+
{
1164+
name: "unwraps polymorphic type with as function",
1165+
inputPath: "Patient.deceased.as(boolean)",
1166+
inputCollection: []fhirpath.Resource{patientVoldemort},
1167+
wantCollection: system.Collection{fhir.Boolean(true)},
1168+
},
1169+
{
1170+
name: "passes through system type with as function",
1171+
inputPath: "@2000-12-05.as(Date)",
1172+
inputCollection: []fhirpath.Resource{},
1173+
wantCollection: system.Collection{system.MustParseDate("2000-12-05")},
1174+
},
11331175
}
11341176

11351177
testEvaluate(t, testCases)

fhirpath/internal/funcs/impl/conversion.go

Lines changed: 46 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,49 @@ 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+
var err error
627+
if parts := strings.Split(typeExpr.Type, "."); len(parts) == 2 {
628+
if typeSpecifier, err = reflection.NewQualifiedTypeSpecifier(parts[0], parts[1]); err != nil {
629+
return nil, err
630+
}
631+
} else if typeSpecifier, err = reflection.NewTypeSpecifier(typeExpr.Type); err != nil {
632+
return nil, err
633+
}
634+
635+
result := system.Collection{}
636+
for _, item := range input {
637+
inputType, err := reflection.TypeOf(item)
638+
if err != nil {
639+
return nil, err
640+
}
641+
if !inputType.Is(typeSpecifier) {
642+
continue
643+
}
644+
// attempt to unwrap polymorphic types
645+
message, ok := item.(fhir.Base)
646+
if !ok {
647+
result = append(result, item)
648+
} else if oneOf := protofields.UnwrapOneofField(message, "choice"); oneOf != nil {
649+
result = append(result, oneOf)
650+
} else {
651+
result = append(result, item)
652+
}
653+
}
654+
return result, nil
655+
}
656+
611657
func isValidUnitConversion(outputFormat string) bool {
612658
validFormats := map[string]bool{
613659
"years": true,

fhirpath/internal/funcs/impl/conversion_test.go

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

fhirpath/internal/funcs/impl/existence.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import (
55

66
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
77
"github.com/verily-src/fhirpath-go/fhirpath/system"
8+
"github.com/verily-src/fhirpath-go/internal/fhir"
9+
"github.com/verily-src/fhirpath-go/internal/protofields"
810
"google.golang.org/protobuf/proto"
11+
"google.golang.org/protobuf/reflect/protoreflect"
912
)
1013

1114
// AllTrue Takes a collection of Boolean values and returns true if all the items are true.
@@ -250,3 +253,33 @@ func subset(input system.Collection, other system.Collection) (system.Collection
250253

251254
return system.Collection{system.Boolean(true)}, nil
252255
}
256+
257+
// HasValue returns true if the input collection contains a single value which is a FHIR primitive,
258+
// and it has a primitive value (e.g. as opposed to not having a value and just having extensions).
259+
func HasValue(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) {
260+
if len(args) != 0 {
261+
return nil, fmt.Errorf("%w: received %v arguments, expected 0", ErrWrongArity, len(args))
262+
}
263+
if !input.IsSingleton() {
264+
return system.Collection{system.Boolean(false)}, nil
265+
}
266+
267+
if primitive, ok := input[0].(fhir.Base); ok {
268+
msg := primitive.ProtoReflect()
269+
270+
// attempt to unwrap polymorphic types
271+
oneOf := protofields.UnwrapOneofField(input[0].(fhir.Base), "choice")
272+
if oneOf != nil {
273+
msg = oneOf.ProtoReflect()
274+
} else if !system.IsPrimitive(input[0]) {
275+
return system.Collection{system.Boolean(false)}, nil
276+
}
277+
278+
descriptor := msg.Descriptor()
279+
field := descriptor.Fields().ByName(protoreflect.Name("value"))
280+
if field != nil && msg.Has(field) {
281+
return system.Collection{system.Boolean(true)}, nil
282+
}
283+
}
284+
return system.Collection{system.Boolean(false)}, nil
285+
}

fhirpath/internal/funcs/impl/existence_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto"
8+
ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto"
89
"github.com/google/go-cmp/cmp"
910
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
1011
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest"
@@ -1293,3 +1294,101 @@ func TestSupersetOf_Errors(t *testing.T) {
12931294
})
12941295
}
12951296
}
1297+
1298+
func TestHasValue(t *testing.T) {
1299+
deceased := &ppb.Patient_DeceasedX{
1300+
Choice: &ppb.Patient_DeceasedX_Boolean{
1301+
Boolean: fhir.Boolean(true),
1302+
},
1303+
}
1304+
testCases := []struct {
1305+
name string
1306+
input system.Collection
1307+
args []expr.Expression
1308+
want system.Collection
1309+
wantErr bool
1310+
}{
1311+
{
1312+
name: "non-singleton collection returns false",
1313+
input: system.Collection{fhir.String("a"), fhir.String("b")},
1314+
want: system.Collection{system.Boolean(false)},
1315+
},
1316+
{
1317+
name: "empty collection returns false",
1318+
input: system.Collection{},
1319+
want: system.Collection{system.Boolean(false)},
1320+
},
1321+
{
1322+
name: "non-primitive type returns false",
1323+
input: system.Collection{coding[0]},
1324+
want: system.Collection{system.Boolean(false)},
1325+
},
1326+
{
1327+
name: "primitive with string value returns true",
1328+
input: system.Collection{fhir.String("value")},
1329+
want: system.Collection{system.Boolean(true)},
1330+
},
1331+
{
1332+
name: "primitive with boolean value returns true",
1333+
input: system.Collection{fhir.Boolean(true)},
1334+
want: system.Collection{system.Boolean(true)},
1335+
},
1336+
{
1337+
name: "primitive with non-zero integer value returns true",
1338+
input: system.Collection{fhir.Integer(123)},
1339+
want: system.Collection{system.Boolean(true)},
1340+
},
1341+
{
1342+
name: "primitive as polmorphic oneOf type returns true",
1343+
input: system.Collection{deceased},
1344+
want: system.Collection{system.Boolean(true)},
1345+
},
1346+
{
1347+
name: "primitive with value and extension returns true",
1348+
input: system.Collection{&dtpb.String{
1349+
Value: "hello",
1350+
Extension: []*dtpb.Extension{
1351+
{Url: &dtpb.Uri{Value: "http://example.com"}},
1352+
},
1353+
}},
1354+
want: system.Collection{system.Boolean(true)},
1355+
},
1356+
{
1357+
name: "primitive with only extension returns false",
1358+
input: system.Collection{&dtpb.String{
1359+
Extension: []*dtpb.Extension{
1360+
{Url: &dtpb.Uri{Value: "http://example.com"}},
1361+
},
1362+
}},
1363+
want: system.Collection{system.Boolean(false)},
1364+
},
1365+
{
1366+
name: "primitive with only id returns false",
1367+
input: system.Collection{&dtpb.String{
1368+
Id: fhir.String("some-id"),
1369+
}},
1370+
want: system.Collection{system.Boolean(false)},
1371+
},
1372+
{
1373+
name: "primitive with id and extension but no value returns false",
1374+
input: system.Collection{&dtpb.String{
1375+
Id: fhir.String("some-id"),
1376+
Extension: []*dtpb.Extension{
1377+
{Url: &dtpb.Uri{Value: "http://example.com"}},
1378+
},
1379+
}},
1380+
want: system.Collection{system.Boolean(false)},
1381+
},
1382+
}
1383+
for _, tc := range testCases {
1384+
t.Run(tc.name, func(t *testing.T) {
1385+
got, err := impl.HasValue(&expr.Context{}, tc.input, tc.args...)
1386+
if (err != nil) != tc.wantErr {
1387+
t.Fatalf("HasValue() error = %v, wantErr %v", err, tc.wantErr)
1388+
}
1389+
if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
1390+
t.Errorf("HasValue() returned unexpected diff (-want, +got)\n%s", diff)
1391+
}
1392+
})
1393+
}
1394+
}

fhirpath/internal/funcs/table.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,18 @@ var baseTable = FunctionTable{
427427
0,
428428
false,
429429
},
430+
"hasValue": Function{
431+
impl.HasValue,
432+
0,
433+
0,
434+
false,
435+
},
436+
"as": Function{
437+
impl.As,
438+
1,
439+
1,
440+
true,
441+
},
430442
}
431443

432444
// ExperimentalTable holds the mapping of all

0 commit comments

Comments
 (0)