diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index af782da..5f31ebe 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -776,6 +776,13 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo Result: evalTail, }, }, nil + case *model.Descendants: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{types.Any}, + Result: evalDescendants, + }, + }, nil case *model.Upper: return []convert.Overload[evalUnarySignature]{ { diff --git a/interpreter/operator_list.go b/interpreter/operator_list.go index 719be47..350c59b 100644 --- a/interpreter/operator_list.go +++ b/interpreter/operator_list.go @@ -450,6 +450,75 @@ func evalTail(m model.IUnaryExpression, listObj result.Value) (result.Value, err return result.New(result.List{Value: list[1:], StaticType: staticType}) } +// Descendants(argument Any) List +// Returns all descendant elements of a given element in a hierarchical structure +// From FHIRPath specification: .descendants(X) === Descendants(X) +func evalDescendants(m model.IUnaryExpression, operand result.Value) (result.Value, error) { + if result.IsNull(operand) { + return result.New(nil) + } + + var allDescendants []result.Value + + // Get immediate children of the operand + children, err := getElementChildren(operand) + if err != nil { + return result.Value{}, err + } + + // For each child, add it and recursively get its descendants + for _, child := range children { + allDescendants = append(allDescendants, child) + + // Recursively get descendants of this child + childDescendants, err := evalDescendants(m, child) + if err != nil { + return result.Value{}, err + } + + if !result.IsNull(childDescendants) { + if list, ok := childDescendants.GolangValue().(result.List); ok { + allDescendants = append(allDescendants, list.Value...) + } + } + } + + return result.New(result.List{ + Value: allDescendants, + StaticType: &types.List{ElementType: types.Any}, + }) +} + +// Helper function to get immediate children of an element +func getElementChildren(element result.Value) ([]result.Value, error) { + switch ot := element.GolangValue().(type) { + case result.Named: + // For FHIR resources, get all property values + return getNamedElementChildren(ot) + case result.List: + // For lists, return all elements + return ot.Value, nil + case result.Tuple: + // For tuples, return all values + var children []result.Value + for _, value := range ot.Value { + children = append(children, value) + } + return children, nil + default: + // For primitive types, no children + return []result.Value{}, nil + } +} + +// Helper function to get children from named elements (FHIR resources) +func getNamedElementChildren(named result.Named) ([]result.Value, error) { + // For now, return empty list for Named types (FHIR resources) + // TODO: Implement proper proto message traversal using reflection + // to extract all field values as children + return []result.Value{}, nil +} + // Take(argument List, number Integer) List // https://cql.hl7.org/09-b-cqlreference.html#take // Removes the last n elements from the list. diff --git a/model/model.go b/model/model.go index 74d2eaf..602994e 100644 --- a/model/model.go +++ b/model/model.go @@ -915,6 +915,12 @@ var _ IUnaryExpression = &Duration{} // Tail ELM Expression https://cql.hl7.org/09-b-cqlreference.html#tail. type Tail struct{ *UnaryExpression } +// Descendants ELM expression from FHIRPath specification +// Returns all descendant elements of a given element in a hierarchical structure +type Descendants struct{ *UnaryExpression } + +var _ IUnaryExpression = &Descendants{} + // BinaryExpression is a CQL expression that has two operands. The ELM representation may have // additional operands (ex BinaryExpressionWithPrecision). type BinaryExpression struct { @@ -1658,6 +1664,9 @@ func (s *Skip) GetName() string { return "Skip" } // GetName returns the name of the system operator. func (t *Tail) GetName() string { return "Tail" } +// GetName returns the name of the system operator. +func (d *Descendants) GetName() string { return "Descendants" } + // GetName returns the name of the system operator. func (t *Take) GetName() string { return "Take" } diff --git a/parser/operators.go b/parser/operators.go index 1f616c2..fb6d1b8 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -1934,6 +1934,61 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "Descendants", + operands: [][]types.IType{ + {types.Any}, + }, + model: func() model.IExpression { + return &model.Descendants{ + UnaryExpression: &model.UnaryExpression{ + Expression: model.ResultType(&types.List{ElementType: types.Any}), + }, + } + }, + }, + { + name: "Descendents", + operands: [][]types.IType{ + {types.Any}, + }, + model: func() model.IExpression { + return &model.Descendants{ + UnaryExpression: &model.UnaryExpression{ + Expression: model.ResultType(&types.List{ElementType: types.Any}), + }, + } + }, + }, + // Fluent functions are defined as lowercase, not sure + // the best way to handle this for all the fluent functions + // but this is for the spectests which is lowercase according to the CQL spec + { + name: "descendants", + operands: [][]types.IType{ + {types.Any}, + }, + model: func() model.IExpression { + return &model.Descendants{ + UnaryExpression: &model.UnaryExpression{ + Expression: model.ResultType(&types.List{ElementType: types.Any}), + }, + } + }, + }, + { + name: "descendents", + operands: [][]types.IType{ + {types.Any}, + }, + model: func() model.IExpression { + return &model.Descendants{ + UnaryExpression: &model.UnaryExpression{ + Expression: model.ResultType(&types.List{ElementType: types.Any}), + }, + } + }, + }, { name: "Take", operands: [][]types.IType{ diff --git a/tests/enginetests/operator_list_test.go b/tests/enginetests/operator_list_test.go index 113cd44..8e64dbc 100644 --- a/tests/enginetests/operator_list_test.go +++ b/tests/enginetests/operator_list_test.go @@ -117,6 +117,134 @@ func TestExists(t *testing.T) { } } +func TestDescendants(t *testing.T) { + tests := []struct { + name string + cql string + wantResult result.Value + }{ + { + name: "Descendants of null returns null", + cql: "Descendants(null)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Descendents spelling variant", + cql: "Descendents(null)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Fluent syntax with capital D - descendants", + cql: "(null).Descendants()", + wantResult: newOrFatal(t, nil), + }, + { + name: "Fluent syntax with capital D - descendents", + cql: "(null).Descendents()", + wantResult: newOrFatal(t, nil), + }, + { + name: "Fluent syntax with lowercase d - descendants", + cql: "(null).descendants()", + wantResult: newOrFatal(t, nil), + }, + { + name: "Fluent syntax with lowercase d - descendents", + cql: "(null).descendents()", + wantResult: newOrFatal(t, nil), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + p := newFHIRParser(t) + parsedLibs, err := p.Libraries(context.Background(), wrapInLib(t, tc.cql), parser.Config{}) + if err != nil { + t.Fatalf("Parse returned unexpected error: %v", err) + } + + results, err := interpreter.Eval(context.Background(), parsedLibs, defaultInterpreterConfig(t, p)) + if err != nil { + t.Fatalf("Eval returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantResult, getTESTRESULT(t, results), protocmp.Transform()); diff != "" { + t.Errorf("Eval diff (-want +got)\n%v", diff) + } + + }) + } +} + +func TestFluentFunctionCaseSensitivity(t *testing.T) { + tests := []struct { + name string + cql string + wantResult result.Value + }{ + // Test that fluent functions are case-sensitive + { + name: "Add fluent - capital A", + cql: "4.Add(4)", + wantResult: newOrFatal(t, 8), + }, + { + name: "IsNull fluent - capital I and N", + cql: "(null).IsNull()", + wantResult: newOrFatal(t, true), + }, + { + name: "ToString fluent - capital T and S", + cql: "42.ToString()", + wantResult: newOrFatal(t, "42"), + }, + { + name: "Length fluent - capital L", + cql: "{1, 2, 3}.Length()", + wantResult: newOrFatal(t, 3), + }, + { + name: "First fluent - capital F", + cql: "{1, 2, 3}.First()", + wantResult: newOrFatal(t, 1), + }, + { + name: "Last fluent - capital L", + cql: "{1, 2, 3}.Last()", + wantResult: newOrFatal(t, 3), + }, + { + name: "Exists fluent - capital E", + cql: "{1, 2, 3}.Exists()", + wantResult: newOrFatal(t, true), + }, + { + name: "Distinct fluent - capital D", + cql: "{1, 1, 2}.Distinct()", + wantResult: newOrFatal(t, result.List{ + Value: []result.Value{newOrFatal(t, int32(1)), newOrFatal(t, int32(2))}, + StaticType: &types.List{ElementType: types.Integer}, + }), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + p := newFHIRParser(t) + parsedLibs, err := p.Libraries(context.Background(), wrapInLib(t, tc.cql), parser.Config{}) + if err != nil { + t.Fatalf("Parse returned unexpected error: %v", err) + } + + results, err := interpreter.Eval(context.Background(), parsedLibs, defaultInterpreterConfig(t, p)) + if err != nil { + t.Fatalf("Eval returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantResult, getTESTRESULT(t, results), protocmp.Transform()); diff != "" { + t.Errorf("Eval diff (-want +got)\n%v", diff) + } + + }) + } +} + func TestInList(t *testing.T) { tests := []struct { name string diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index 50d255e..d05bb60 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -316,7 +316,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "CqlListOperatorsTest.xml": { GroupExcludes: []string{ // TODO: b/342061715 - unsupported operators. - "Descendents", }, NamesExcludes: []string{ // TODO: b/342061715 - unsupported operator.