Skip to content

Commit d555d7d

Browse files
committed
1 parent 4ed96d4 commit d555d7d

File tree

6 files changed

+302
-0
lines changed

6 files changed

+302
-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/r4.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ 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+
"google.golang.org/protobuf/reflect/protoreflect"
911
)
1012

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

fhirpath/internal/funcs/impl/r4_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,101 @@ func TestExtension_InvalidInput_RaisesError(t *testing.T) {
118118
})
119119
}
120120
}
121+
122+
func TestHasValue(t *testing.T) {
123+
deceased := &ppb.Patient_DeceasedX{
124+
Choice: &ppb.Patient_DeceasedX_Boolean{
125+
Boolean: fhir.Boolean(true),
126+
},
127+
}
128+
testCases := []struct {
129+
name string
130+
input system.Collection
131+
args []expr.Expression
132+
want system.Collection
133+
wantErr bool
134+
}{
135+
{
136+
name: "non-singleton collection returns false",
137+
input: system.Collection{fhir.String("a"), fhir.String("b")},
138+
want: system.Collection{system.Boolean(false)},
139+
},
140+
{
141+
name: "empty collection returns false",
142+
input: system.Collection{},
143+
want: system.Collection{system.Boolean(false)},
144+
},
145+
{
146+
name: "non-primitive type returns false",
147+
input: system.Collection{coding[0]},
148+
want: system.Collection{system.Boolean(false)},
149+
},
150+
{
151+
name: "primitive with string value returns true",
152+
input: system.Collection{fhir.String("value")},
153+
want: system.Collection{system.Boolean(true)},
154+
},
155+
{
156+
name: "primitive with boolean value returns true",
157+
input: system.Collection{fhir.Boolean(true)},
158+
want: system.Collection{system.Boolean(true)},
159+
},
160+
{
161+
name: "primitive with non-zero integer value returns true",
162+
input: system.Collection{fhir.Integer(123)},
163+
want: system.Collection{system.Boolean(true)},
164+
},
165+
{
166+
name: "primitive as polmorphic oneOf type returns true",
167+
input: system.Collection{deceased},
168+
want: system.Collection{system.Boolean(true)},
169+
},
170+
{
171+
name: "primitive with value and extension returns true",
172+
input: system.Collection{&dtpb.String{
173+
Value: "hello",
174+
Extension: []*dtpb.Extension{
175+
{Url: &dtpb.Uri{Value: "http://example.com"}},
176+
},
177+
}},
178+
want: system.Collection{system.Boolean(true)},
179+
},
180+
{
181+
name: "primitive with only extension returns false",
182+
input: system.Collection{&dtpb.String{
183+
Extension: []*dtpb.Extension{
184+
{Url: &dtpb.Uri{Value: "http://example.com"}},
185+
},
186+
}},
187+
want: system.Collection{system.Boolean(false)},
188+
},
189+
{
190+
name: "primitive with only id returns false",
191+
input: system.Collection{&dtpb.String{
192+
Id: fhir.String("some-id"),
193+
}},
194+
want: system.Collection{system.Boolean(false)},
195+
},
196+
{
197+
name: "primitive with id and extension but no value returns false",
198+
input: system.Collection{&dtpb.String{
199+
Id: fhir.String("some-id"),
200+
Extension: []*dtpb.Extension{
201+
{Url: &dtpb.Uri{Value: "http://example.com"}},
202+
},
203+
}},
204+
want: system.Collection{system.Boolean(false)},
205+
},
206+
}
207+
for _, tc := range testCases {
208+
t.Run(tc.name, func(t *testing.T) {
209+
got, err := impl.HasValue(&expr.Context{}, tc.input, tc.args...)
210+
if (err != nil) != tc.wantErr {
211+
t.Fatalf("HasValue() error = %v, wantErr %v", err, tc.wantErr)
212+
}
213+
if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
214+
t.Errorf("HasValue() returned unexpected diff (-want, +got)\n%s", diff)
215+
}
216+
})
217+
}
218+
}

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)