Skip to content

Commit

Permalink
Change Concept definition to allow for null codes.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 662972325
  • Loading branch information
evan-gordon authored and copybara-github committed Aug 14, 2024
1 parent 540a79c commit e817783
Show file tree
Hide file tree
Showing 14 changed files with 218 additions and 133 deletions.
12 changes: 9 additions & 3 deletions interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,17 +175,23 @@ func (i *interpreter) evalLibrary(lib *model.Library, passedParams map[result.De
}

for _, c := range lib.Concepts {
var codes []result.Code
for _, code := range c.Codes {
codes := make([]*result.Code, len(c.Codes))
for index, code := range c.Codes {
codeRef, err := i.evalCodeRef(code)
if err != nil {
return err
}
// Note: the CQL grammar does not allow null codes in a concept using the `concept foo: {}`
// syntax.
if result.IsNull(codeRef) {
codes[index] = nil
continue
}
codeVal, err := result.ToCode(codeRef)
if err != nil {
return err
}
codes = append(codes, codeVal)
codes[index] = &codeVal
}
cObj, err := result.New(result.Concept{Codes: codes, Display: c.Display})
if err != nil {
Expand Down
8 changes: 6 additions & 2 deletions interpreter/literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,17 @@ func (i *interpreter) evalInstance(in *model.Instance) (result.Value, error) {
if err != nil {
return result.Value{}, err
}
codes := make([]result.Code, len(codeObjs))
codes := make([]*result.Code, len(codeObjs))
for i, codeObj := range codeObjs {
if result.IsNull(codeObj) {
codes[i] = nil
continue
}
code, err := result.ToCode(codeObj)
if err != nil {
return result.Value{}, err
}
codes[i] = code
codes[i] = &code
}
cv.Codes = codes
}
Expand Down
5 changes: 3 additions & 2 deletions interpreter/operator_clinical.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func (i *interpreter) evalInValueSet(b model.IBinaryExpression, lObj, rObj resul
// valueToCodes is the helper to convert a value to a list of terminology.Code. Returns an error for
// value types that are not valid clinical values. Currently only supports Code, Concept,
// List<Code>, List<Concept>.
// In cases where null codes can be present, they should be filtered out.
func valueToCodes(o result.Value) ([]terminology.Code, error) {
var termCodes []terminology.Code
if rt := o.RuntimeType(); rt.Equal(types.Code) {
Expand All @@ -181,7 +182,7 @@ func valueToCodes(o result.Value) ([]terminology.Code, error) {
if err != nil {
return nil, err
}
for _, c := range concept.Codes {
for _, c := range concept.NonNullCodeValues() {
termCodes = append(termCodes, terminology.Code{System: c.System, Code: c.Code})
}
return termCodes, nil
Expand Down Expand Up @@ -210,7 +211,7 @@ func valueToCodes(o result.Value) ([]terminology.Code, error) {
if err != nil {
return nil, err
}
for _, c := range concept.Codes {
for _, c := range concept.NonNullCodeValues() {
termCodes = append(termCodes, terminology.Code{System: c.System, Code: c.Code})
}
}
Expand Down
19 changes: 8 additions & 11 deletions interpreter/operator_comparison.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,7 @@ func evalEquivalentDateTime(_ model.IBinaryExpression, lObj, rObj result.Value)
// Some Equivalent overloads are categorized in the clinical operator section, like this one, but
// are included in operator_comparison.go to keep all equivalent overloads together.
func (i *interpreter) evalEquivalentConceptCode(b model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) && result.IsNull(rObj) {
return result.New(true)
}
if result.IsNull(lObj) != result.IsNull(rObj) {
if result.IsNull(lObj) {
return result.New(false)
}

Expand All @@ -299,14 +296,14 @@ func (i *interpreter) evalEquivalentConceptCode(b model.IBinaryExpression, lObj,
return result.Value{}, err
}

// Sanity check right hand type.
_, err = result.ToCode(rObj)
if err != nil {
return result.Value{}, err
}

for _, conCode := range con.Codes {
conCodeObj, err := result.New(conCode)
if conCode == nil && result.IsNull(rObj) {
return result.New(true)
} else if conCode == nil {
continue
}

conCodeObj, err := result.New(*conCode)
if err != nil {
return result.Value{}, err
}
Expand Down
14 changes: 7 additions & 7 deletions interpreter/operator_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ func evalToConceptCode(m model.IUnaryExpression, obj result.Value) (result.Value
if err != nil {
return result.Value{}, err
}
return result.New(result.Concept{Codes: []result.Code{code}, Display: code.Display})
return result.New(result.Concept{Codes: []*result.Code{&code}, Display: code.Display})
}

// ToConcept(argument List<Code>) Concept
Expand All @@ -361,24 +361,24 @@ func evalToConceptList(m model.IUnaryExpression, obj result.Value) (result.Value
if result.IsNull(obj) {
return result.New(nil)
}
codes, err := result.ToSlice(obj)
codeObjs, err := result.ToSlice(obj)
if err != nil {
return result.Value{}, err
}

cv := result.Concept{}
for _, code := range codes {
codes := make([]*result.Code, len(codeObjs))
for i, code := range codeObjs {
if result.IsNull(code) {
codes[i] = nil
continue
}
c, err := result.ToCode(code)
if err != nil {
return result.Value{}, err
}
cv.Codes = append(cv.Codes, c)
codes[i] = &c
}

return result.New(cv)
return result.New(result.Concept{Codes: codes})
}

// ToQuantity(argument Decimal) Quantity
Expand Down
18 changes: 17 additions & 1 deletion interpreter/property.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,23 @@ func (i *interpreter) valueProperty(v result.Value, property string, staticResul
case result.Concept:
switch property {
case "codes":
return result.New(ot.Codes)
var codeValues []result.Value
for _, c := range ot.Codes {
if c == nil {
nilVal, err := result.New(nil)
if err != nil {
return result.Value{}, err
}
codeValues = append(codeValues, nilVal)
continue
}
codeVal, err := result.New(*c)
if err != nil {
return result.Value{}, err
}
codeValues = append(codeValues, codeVal)
}
return result.New(result.List{Value: codeValues, StaticType: &types.List{ElementType: types.Code}})
case "display":
return result.New(ot.Display)
default:
Expand Down
97 changes: 64 additions & 33 deletions interpreter/property_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,72 @@ import (
"github.com/google/cql/result"
"github.com/google/cql/types"
r4patientpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto"
"github.com/google/go-cmp/cmp"
)

func TestEvalPropertyValue(t *testing.T) {
tests := []struct {
name string
property string
value result.Value
resultType types.IType
wantResult result.Value
}{
{
name: "property on null input value",
property: "apple",
value: newOrFatal(t, nil),
wantResult: newOrFatal(t, nil),
},
{
name: "code",
property: "code",
value: newOrFatal(t, result.Code{Code: "code1", System: "system1", Version: "version1"}),
wantResult: newOrFatal(t, "code1"),
},
{
name: "code",
property: "system",
value: newOrFatal(t, result.Code{Code: "code1", System: "system1", Version: "version1"}),
wantResult: newOrFatal(t, "system1"),
},
{
name: "concept",
property: "codes",
value: newOrFatal(t, result.Concept{Codes: []*result.Code{&result.Code{Code: "code1", System: "system1", Version: "version1"}, nil}, Display: "display"}),
wantResult: newOrFatal(t, result.List{
Value: []result.Value{
newOrFatal(t, result.Code{Code: "code1", System: "system1", Version: "version1"}),
newOrFatal(t, nil),
},
StaticType: &types.List{ElementType: types.Code},
}),
},
{
name: "concept",
property: "display",
value: newOrFatal(t, result.Concept{Codes: []*result.Code{&result.Code{Code: "code1", System: "system1", Version: "version1"}, nil}, Display: "display1"}),
wantResult: newOrFatal(t, "display1"),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
i := &interpreter{
refs: reference.NewResolver[result.Value, *model.FunctionDef](),
modelInfo: newFHIRModelInfo(t),
evaluationTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}
propVal, err := i.valueProperty(tc.value, tc.property, tc.resultType)
if err != nil {
t.Errorf("evalPropertyValue(%q) returned unexpected error, %v", tc.property, err)
}
if diffResult := cmp.Diff(tc.wantResult, propVal); diffResult != "" {
t.Errorf("evalPropertyValue(%q) = %v, returned unexpected diff (-want +got):\n%v", tc.property, propVal, diffResult)
}
})
}
}

func TestEvalPropertyValue_Errors(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -114,39 +178,6 @@ func TestEvalPropertyValue_Errors(t *testing.T) {
}
}

func TestEvalPropertyValue(t *testing.T) {
tests := []struct {
name string
property string
value result.Value
resultType types.IType
wantValue result.Value
}{
{
name: "property on null input value",
property: "apple",
value: newOrFatal(t, nil),
wantValue: newOrFatal(t, nil),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
i := &interpreter{
refs: reference.NewResolver[result.Value, *model.FunctionDef](),
modelInfo: newFHIRModelInfo(t),
evaluationTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}
got, err := i.valueProperty(tc.value, tc.property, tc.resultType)
if err != nil {
t.Errorf("evalPropertyValue(%q) unexpected error: %v", tc.property, err)
}
if !got.Equal(tc.wantValue) {
t.Errorf("evalPropertyValue(%q) = %v, want %v", tc.property, got, tc.wantValue)
}
})
}
}

func newFHIRModelInfo(t *testing.T) *modelinfo.ModelInfos {
t.Helper()
fhirMIBytes, err := embeddata.ModelInfos.ReadFile("third_party/cqframework/fhir-modelinfo-4.0.1.xml")
Expand Down
36 changes: 28 additions & 8 deletions result/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -1224,9 +1224,10 @@ func CodeSystemFromProto(pb *crpb.CodeSystem) CodeSystem {
}

// Concept is the Golang representation of a CQL Concept.
// We use a pointer to Code in order to support null values.
type Concept struct {
Codes []Code // 1..*
Display string // 0..1
Codes []*Code // 0..*
Display string // 0..1
}

// Equal returns true if this Concept matches the provided one, otherwise false.
Expand All @@ -1237,7 +1238,7 @@ func (c Concept) Equal(v Concept) bool {
slices.SortFunc(c.Codes, compareCode)
slices.SortFunc(v.Codes, compareCode)
for i, c := range c.Codes {
if c != v.Codes[i] {
if compareCode(c, v.Codes[i]) != 0 {
return false
}
}
Expand All @@ -1258,13 +1259,25 @@ func (c Concept) Proto() *crpb.Concept {

// ConceptFromProto converts a proto to a Concept.
func ConceptFromProto(pb *crpb.Concept) Concept {
codes := make([]Code, 0, len(pb.Codes))
codes := make([]*Code, 0, len(pb.Codes))
for _, c := range pb.Codes {
codes = append(codes, CodeFromProto(c))
code := CodeFromProto(c)
codes = append(codes, &code)
}
return Concept{Codes: codes, Display: pb.GetDisplay()}
}

// NonNullCodeValues returns all non null Codes from the Concept.
func (c Concept) NonNullCodeValues() []Code {
var codes []Code
for _, code := range c.Codes {
if code != nil {
codes = append(codes, *code)
}
}
return codes
}

func (c Concept) marshalJSON(runtimeType json.RawMessage) ([]byte, error) {
codeType, err := types.Code.MarshalJSON()
if err != nil {
Expand Down Expand Up @@ -1315,9 +1328,16 @@ func (c Code) marshalJSON(runtimeType json.RawMessage) ([]byte, error) {
}

// compareCode is used for sorting for go Equal() implementation. This is different from CQL
// equality where display is ignored.
func compareCode(a, b Code) int {
if a.Code != b.Code {
// equality where display is ignored. It accepts pointers to Codes so that it can support null
// values.
func compareCode(a, b *Code) int {
if a == nil && b == nil {
return 0
} else if a == nil {
return -1
} else if b == nil {
return 1
} else if a.Code != b.Code {
return strings.Compare(a.Code, b.Code)
} else if a.System != b.System {
return strings.Compare(a.System, b.System)
Expand Down
Loading

0 comments on commit e817783

Please sign in to comment.