Skip to content
Open
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
7 changes: 7 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]{
{
Expand Down
69 changes: 69 additions & 0 deletions interpreter/operator_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<Any>
// 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<T>, number Integer) List<T>
// https://cql.hl7.org/09-b-cqlreference.html#take
// Removes the last n elements from the list.
Expand Down
9 changes: 9 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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" }

Expand Down
55 changes: 55 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
128 changes: 128 additions & 0 deletions tests/enginetests/operator_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion tests/spectests/exclusions/exclusions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down