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
65 changes: 65 additions & 0 deletions fhirpath/fhirpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,16 @@ func TestFunctionInvocation_Evaluates(t *testing.T) {
wantCollection: system.Collection{system.String("Chu-Chu")},
compileOptions: []fhirpath.CompileOption{compopts.WithExperimentalFuncs()},
},
{
name: "returns the distinct union of two sets of names",
inputPath: "Patient.name.given | Patient.name.family",
inputCollection: []fhirpath.Resource{patientChu},
wantCollection: system.Collection{
fhir.String("Senpai"),
fhir.String("Kang"),
fhir.String("Chu"),
},
},
}

testEvaluate(t, testCases)
Expand Down Expand Up @@ -1220,6 +1230,61 @@ func TestTypeExpression_Evaluates(t *testing.T) {
testEvaluate(t, testCases)
}

func TestMembershipExpression_Evaluates(t *testing.T) {
testCases := []evaluateTestCase{
{
name: "returns true for 'in' operator with present value",
inputPath: "'a' in ('a' | 'b' | 'c')",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{system.Boolean(true)},
},
{
name: "returns false for 'in' operator with absent value",
inputPath: "'d' in ('a' | 'b' | 'c')",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{system.Boolean(false)},
},
{
name: "returns empty for 'in' operator with empty left side",
inputPath: "{} in ('a' | 'b' | 'c')",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{},
},
{
name: "returns false for 'in' operator with empty right side",
inputPath: "'a' in {}",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{system.Boolean(false)},
},
{
name: "returns true for 'contains' operator with present value",
inputPath: "('a' | 'b' | 'c') contains 'b'",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{system.Boolean(true)},
},
{
name: "returns false for 'contains' operator with absent value",
inputPath: "('a' | 'b' | 'c') contains 'd'",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{system.Boolean(false)},
},
{
name: "returns false for 'contains' operator with empty left side",
inputPath: "{} contains 'a'",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{system.Boolean(false)},
},
{
name: "returns empty for 'contains' operator with empty right side",
inputPath: "('a' | 'b' | 'c') contains {}",
inputCollection: []fhirpath.Resource{},
wantCollection: system.Collection{},
},
}

testEvaluate(t, testCases)
}

func TestBooleanExpression_Evaluates(t *testing.T) {
testCases := []evaluateTestCase{
{
Expand Down
73 changes: 73 additions & 0 deletions fhirpath/internal/expr/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -808,3 +808,76 @@ func (e *NegationExpression) Evaluate(ctx *Context, input system.Collection) (sy
}

var _ Expression = (*NegationExpression)(nil)

// UnionExpression combines two collections into a single collection, retaining only distinct elements.
type UnionExpression struct {
Left Expression
Right Expression
}

// MembershipExpression enables evaluation of the "in" and "contains" operators.
type MembershipExpression struct {
Left Expression
Right Expression
Operator Operator
}

// Evaluate evaluates the left and right subexpressions and performs a membership check
// according to the FHIRPath specification for 'in' and 'contains'.
func (e *MembershipExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) {
leftResult, err := e.Left.Evaluate(ctx.Clone(), input)
if err != nil {
return nil, err
}
rightResult, err := e.Right.Evaluate(ctx.Clone(), input)
if err != nil {
return nil, err
}

switch e.Operator {
case In:
if leftResult.IsEmpty() {
return system.Collection{}, nil
}
if !leftResult.IsSingleton() {
return nil, fmt.Errorf("%w: 'in' operator requires the left operand to be a singleton, but got %d items", ErrNotSingleton, len(leftResult))
}
return system.Collection{system.Boolean(rightResult.Contains(leftResult[0]))}, nil
case Contains:
if rightResult.IsEmpty() {
return system.Collection{}, nil
}
if !rightResult.IsSingleton() {
return nil, fmt.Errorf("%w: 'contains' operator requires the right operand to be a singleton, but got %d items", ErrNotSingleton, len(rightResult))
}
return system.Collection{system.Boolean(leftResult.Contains(rightResult[0]))}, nil
default:
return nil, fmt.Errorf("%w: %s", ErrInvalidOperator, e.Operator)
}
}

var _ Expression = (*MembershipExpression)(nil)

// Evaluate executes the union expression by evaluating both the left and right expressions,
// and then combining their results into a single collection with duplicates removed.
func (e *UnionExpression) Evaluate(ctx *Context, input system.Collection) (system.Collection, error) {
leftResult, err := e.Left.Evaluate(ctx.Clone(), input)
if err != nil {
return nil, err
}
rightResult, err := e.Right.Evaluate(ctx.Clone(), input)
if err != nil {
return nil, err
}

// makes sure result is not nil if both inputs are empty or nil
result := append(system.Collection{}, leftResult...)
for _, item := range rightResult {
if !result.Contains(item) {
result = append(result, item)
}
}
return result, nil
}

var _ Expression = (*UnionExpression)(nil)
184 changes: 184 additions & 0 deletions fhirpath/internal/expr/expressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1463,3 +1463,187 @@ func TestNegationExpression(t *testing.T) {
})
}
}

func TestUnionExpression(t *testing.T) {
testCases := []struct {
name string
expr *expr.UnionExpression
want system.Collection
wantErr error
}{
{
name: "unions two distinct collections",
expr: &expr.UnionExpression{
Left: exprtest.Return(system.Integer(1), system.Integer(2)),
Right: exprtest.Return(system.Integer(3), system.Integer(4)),
},
want: system.Collection{system.Integer(1), system.Integer(2), system.Integer(3), system.Integer(4)},
},
{
name: "unions two collections with overlap",
expr: &expr.UnionExpression{
Left: exprtest.Return(system.Integer(1), system.Integer(2)),
Right: exprtest.Return(system.Integer(2), system.Integer(3)),
},
want: system.Collection{system.Integer(1), system.Integer(2), system.Integer(3)},
},
{
name: "unions with an empty collection",
expr: &expr.UnionExpression{
Left: exprtest.Return(system.Integer(1), system.Integer(2)),
Right: exprtest.Return(),
},
want: system.Collection{system.Integer(1), system.Integer(2)},
},
{
name: "unions two empty collections",
expr: &expr.UnionExpression{
Left: exprtest.Return(),
Right: exprtest.Return(),
},
want: system.Collection{},
},
{
name: "propagates error from left expression",
expr: &expr.UnionExpression{
Left: exprtest.Error(errMock),
Right: exprtest.Return(system.Integer(1)),
},
wantErr: errMock,
},
{
name: "propagates error from right expression",
expr: &expr.UnionExpression{
Left: exprtest.Return(system.Integer(1)),
Right: exprtest.Error(errMock),
},
wantErr: errMock,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.expr.Evaluate(&expr.Context{}, system.Collection{})

if !errors.Is(err, tc.wantErr) {
t.Fatalf("UnionExpression.Evaluate returned unexpected error: got %v, want %v", err, tc.wantErr)
}
if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
t.Errorf("UnionExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff)
}
})
}
}

func TestMembershipExpression(t *testing.T) {
testCases := []struct {
name string
expr *expr.MembershipExpression
want system.Collection
wantErr error
}{
{
name: "item is in collection",
expr: &expr.MembershipExpression{
Left: exprtest.Return(system.Integer(1)),
Right: exprtest.Return(system.Integer(1), system.Integer(2)),
Operator: expr.In,
},
want: system.Collection{system.Boolean(true)},
},
{
name: "item is not in collection",
expr: &expr.MembershipExpression{
Left: exprtest.Return(system.Integer(3)),
Right: exprtest.Return(system.Integer(1), system.Integer(2)),
Operator: expr.In,
},
want: system.Collection{system.Boolean(false)},
},
{
name: "left operand is empty",
expr: &expr.MembershipExpression{
Left: exprtest.Return(),
Right: exprtest.Return(system.Integer(1), system.Integer(2)),
Operator: expr.In,
},
want: system.Collection{},
},
{
name: "left operand is not a singleton",
expr: &expr.MembershipExpression{
Left: exprtest.Return(system.Integer(1), system.Integer(2)),
Right: exprtest.Return(system.Integer(1), system.Integer(2)),
Operator: expr.In,
},
wantErr: expr.ErrNotSingleton,
},
{
name: "collection contains item",
expr: &expr.MembershipExpression{
Left: exprtest.Return(system.Integer(1), system.Integer(2)),
Right: exprtest.Return(system.Integer(1)),
Operator: expr.Contains,
},
want: system.Collection{system.Boolean(true)},
},
{
name: "collection does not contain item",
expr: &expr.MembershipExpression{
Left: exprtest.Return(system.Integer(1), system.Integer(2)),
Right: exprtest.Return(system.Integer(3)),
Operator: expr.Contains,
},
want: system.Collection{system.Boolean(false)},
},
{
name: "right operand is empty",
expr: &expr.MembershipExpression{
Left: exprtest.Return(system.Integer(1), system.Integer(2)),
Right: exprtest.Return(),
Operator: expr.Contains,
},
want: system.Collection{},
},
{
name: "right operand is not a singleton",
expr: &expr.MembershipExpression{
Left: exprtest.Return(system.Integer(1), system.Integer(2)),
Right: exprtest.Return(system.Integer(1), system.Integer(2)),
Operator: expr.Contains,
},
wantErr: expr.ErrNotSingleton,
},
{
name: "propagates error from left expression",
expr: &expr.MembershipExpression{
Left: exprtest.Error(errMock),
Right: exprtest.Return(system.Integer(1)),
Operator: expr.In,
},
wantErr: errMock,
},
{
name: "propagates error from right expression",
expr: &expr.MembershipExpression{
Left: exprtest.Return(system.Integer(1)),
Right: exprtest.Error(errMock),
Operator: expr.In,
},
wantErr: errMock,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.expr.Evaluate(&expr.Context{}, system.Collection{})

if !errors.Is(err, tc.wantErr) {
t.Fatalf("MembershipExpression.Evaluate returned unexpected error: got %v, want %v", err, tc.wantErr)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("MembershipExpression.Evaluate returned unexpected diff: (-want, +got)\n%s", diff)
}
})
}
}
2 changes: 2 additions & 0 deletions fhirpath/internal/expr/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const (
Div = "/"
FloorDiv = "div"
Mod = "mod"
In = "in"
Contains = "contains"
)

// Operator represents a valid expression operator.
Expand Down
4 changes: 2 additions & 2 deletions fhirpath/internal/funcs/impl/terminology.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var (
ErrNotSupported = errors.New("Not Supported, memberOf must be called on a single Coding or a CodeableConcept")
)

// MemberOf takes a Coding or a Codeable concept and a ValueSet internal id
// MemberOf takes a Coding or a CodeableConcept and a ValueSet internal ID
// and determines if the coding of any of the coding is inside the ValueSet
func MemberOf(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) {
if length := len(args); length != 1 {
Expand Down Expand Up @@ -56,7 +56,7 @@ func MemberOf(ctx *expr.Context, input system.Collection, args ...expr.Expressio
validateResult = result

case *dtpb.CodeableConcept:
// If it's a Codeable Concept, we will checking the coding inside one by one
// If it's a CodeableConcept, check the coding inside one by one
for _, coding := range res.GetCoding() {
result, err := validateCoding(ctx, coding.GetCode().GetValue(), coding.GetSystem().GetValue(), valueSetId)
if err != nil {
Expand Down
7 changes: 6 additions & 1 deletion fhirpath/internal/funcs/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ var baseTable = FunctionTable{
1,
false,
},
"supersetOf": notImplemented,
"supersetOf": Function{
impl.SupersetOf,
1,
1,
false,
},
"count": Function{
impl.Count,
0,
Expand Down
Loading