Skip to content

Commit 12e2566

Browse files
committed
Unit tests, scope updates, and other library traversal
1 parent dd5f177 commit 12e2566

3 files changed

Lines changed: 389 additions & 72 deletions

File tree

src/helpers/DataRequirementHelpers.ts

Lines changed: 133 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ import { GracefulError } from '../types/errors/GracefulError';
44
import { EqualsFilter, InFilter, DuringFilter, codeFilterQuery, AttributeFilter } from '../types/QueryFilterTypes';
55
import { PatientParameters } from '../compartment-definition/PatientParameters';
66
import { SearchParameters } from '../compartment-definition/SearchParameters';
7-
import { AnyELMExpression, ELM, ELMIdentifier, ELMProperty, ELMQuery } from '../types/ELMTypes';
7+
import {
8+
AnyELMExpression,
9+
ELM,
10+
ELMAliasedQuerySource,
11+
ELMIdentifier,
12+
ELMLast,
13+
ELMProperty,
14+
ELMQuery
15+
} from '../types/ELMTypes';
816
import { ExtractedLibrary } from '../types/CQLTypes';
917
import * as Execution from '../execution/Execution';
1018
import { UnexpectedResource } from '../types/errors/CustomErrors';
@@ -20,6 +28,8 @@ import { uniqBy, isEqual } from 'lodash';
2028
import { DateTime, Interval } from 'cql-execution';
2129
import { parseTimeStringAsUTC } from '../execution/ValueSetHelper';
2230
import * as MeasureBundleHelpers from './MeasureBundleHelpers';
31+
import { findLibraryReference } from './elm/ELMDependencyHelpers';
32+
import { findClauseInExpression, findClauseInLibrary, findNamedClausesInExpression } from './elm/ELMHelpers';
2333
const FHIR_QUERY_PATTERN_URL = 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-fhirQueryPattern';
2434

2535
/**
@@ -423,7 +433,7 @@ function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulEr
423433
// addMustSupport: find any fields as part of this statement.expression,
424434
// then search the allRetrieves for that field's context, and add the field to the correct retrieve's mustSupport
425435
function addMustSupport(allRetrieves: DataTypeQuery[], expression: AnyELMExpression, rootLib: ELM, allELM: ELM[]) {
426-
const propertyExpressions = findPropertyExpressions(expression, [], rootLib.library.identifier.id);
436+
const propertyExpressions = findPropertyExpressions(expression, [], rootLib, allELM);
427437

428438
propertyExpressions.forEach(prop => {
429439
// find all matches for this property in allRetrieves
@@ -441,7 +451,7 @@ function addMustSupport(allRetrieves: DataTypeQuery[], expression: AnyELMExpress
441451
});
442452
}
443453

444-
interface PropertyTracker {
454+
export interface PropertyTracker {
445455
property: ELMProperty;
446456
stack: ExpressionStackEntry[];
447457
}
@@ -452,62 +462,110 @@ interface PropertyTracker {
452462
*
453463
* @param exp the current expression (top node) of the tree to search for Properties
454464
* @param currentStack stack entries that led to this expression (not including this expression)
455-
* @param lib name of library context for this expression
465+
* @param lib library context for this expression
466+
* @param allLib all elm libraries
456467
* @returns array of all properties found in this expression's tree
457468
*/
458-
function findPropertyExpressions(exp: object, currentStack: ExpressionStackEntry[], lib: string): PropertyTracker[] {
469+
export function findPropertyExpressions(
470+
exp: object,
471+
currentStack: ExpressionStackEntry[],
472+
lib: ELM,
473+
allLib: ELM[]
474+
): PropertyTracker[] {
475+
if (typeof exp !== 'object') {
476+
return [];
477+
}
478+
// ... only do this for objects TODO next
479+
const thisStackEntry: ExpressionStackEntry = {
480+
type: 'type' in exp && exp.type ? (exp.type as string) : 'unknown',
481+
localId: 'localId' in exp && exp.localId ? (exp.localId as string) : 'unknown',
482+
libraryName: lib.library.identifier.id
483+
};
484+
459485
if ('type' in exp && exp.type && exp.type === 'Property') {
460486
// base case found property expression
461487
const prop = exp as ELMProperty;
462488
if (prop.source) {
463489
// add this expression to current stack before recursing on .source
464-
const thisStackEntry: ExpressionStackEntry = {
465-
type: exp.type,
466-
localId: 'localId' in exp && exp.localId ? (exp.localId as string) : 'unknown',
467-
libraryName: lib
468-
};
469490
return [
470491
{ property: prop, stack: currentStack },
471-
...findPropertyExpressions(
472-
prop.source,
473-
currentStack.concat([thisStackEntry]),
474-
checkLibChange(prop.source) ?? lib
475-
)
492+
...findPropertyExpressions(prop.source, currentStack.concat([thisStackEntry]), lib, allLib)
476493
];
477494
} else {
478495
return [{ property: prop, stack: currentStack }];
479496
}
480-
} else {
481-
// not a property expression, recurse on all array members or all children values
482-
return Object.values(exp).flatMap(v => {
483-
const thisStackEntry: ExpressionStackEntry = {
484-
type: 'type' in exp && exp.type ? (exp.type as string) : 'unknown',
485-
localId: 'localId' in exp && exp.localId ? (exp.localId as string) : 'unknown',
486-
libraryName: lib
487-
};
488-
if (Array.isArray(v)) {
489-
return v.flatMap(elem =>
490-
findPropertyExpressions(elem, currentStack.concat([thisStackEntry]), checkLibChange(elem) ?? lib)
491-
);
492-
} else if (typeof v === 'object') {
493-
return findPropertyExpressions(v, currentStack.concat([thisStackEntry]), checkLibChange(v) ?? lib);
494-
} else {
495-
return [];
497+
} else if (
498+
'type' in exp &&
499+
exp.type &&
500+
(exp.type === 'FunctionRef' || exp.type === 'ExpressionRef') &&
501+
'libraryName' in exp &&
502+
exp.libraryName &&
503+
'name' in exp &&
504+
exp.name
505+
) {
506+
// handle references that go to different libraries
507+
508+
// TODO: do we have to worry about ParameterRef as well?
509+
// TODO: if there isn't a library name, are we good to not poke around for a new expression to explode?
510+
511+
// TODO: do we need to search the entire operand tree, or is the top level okay?
512+
if ('operand' in exp && exp.operand) {
513+
const properties = findPropertyExpressions(exp.operand, currentStack.concat([thisStackEntry]), lib, allLib);
514+
if (properties.length > 0) {
515+
// if we find the property(s) in the operand, we can short-circuit the search without going to a different library
516+
return properties;
496517
}
497-
});
518+
}
519+
520+
const newLib = findLibraryReference(lib, allLib, exp.libraryName as string);
521+
if (!newLib) {
522+
throw new UnexpectedResource(`Cannot Find Referenced Library: ${exp.libraryName}`);
523+
}
524+
const newExp = findNameinLib(exp.name as string, newLib);
525+
if (!newExp) {
526+
// If we can't uniquely identify the reference, warn and explode immediate expression in current library context
527+
console.warn(
528+
`Issue with searching for properties within ${exp.name} in library ${lib.library.identifier.id}. Could not identify reference because it is overloaded or doesn't exist.`
529+
);
530+
return findPropertyExpressions(exp, currentStack.concat([thisStackEntry]), lib, allLib);
531+
}
532+
return findPropertyExpressions(newExp, currentStack.concat([thisStackEntry]), newLib, allLib);
533+
} else if (Array.isArray(exp)) {
534+
return exp.flatMap(elem => findPropertyExpressions(elem, currentStack, lib, allLib));
535+
} else {
536+
// non property object, recurse all children values
537+
return findPropertyExpressions(Object.values(exp), currentStack.concat([thisStackEntry]), lib, allLib);
498538
}
499539
}
500540

501-
function checkLibChange(value: object): string | null {
502-
// for ExpressionRef and FunctionRef we need the new library context
503-
if ('libraryName' in value && value.libraryName) {
504-
return value.libraryName as string;
541+
// find the expression in this library that matches the passed name
542+
// if there are issues uniquely identifying one, return null
543+
export function findNameinLib(name: string, lib: ELM): AnyELMExpression | null {
544+
// search statements first and return expression
545+
const namedStatement = lib.library.statements.def.filter(statement => statement.name === name);
546+
if (namedStatement.length > 0) {
547+
if (namedStatement.length === 1) {
548+
return namedStatement[0].expression;
549+
} else {
550+
// if multiple come up, then it's overloaded (we can't handle)
551+
return null;
552+
}
505553
}
506-
return null;
554+
555+
// search expressions in statements
556+
const foundNames = lib.library.statements.def.flatMap(statement =>
557+
findNamedClausesInExpression(statement.expression, name)
558+
);
559+
if (foundNames.length !== 1) {
560+
// if multiple come up, then it's overloaded (we can't handle). If 0 come up, it's ill-formed.
561+
return null;
562+
}
563+
// TODO: fix statement return (don't want to search annotations)
564+
return foundNames[0];
507565
}
508566

509567
// search retrieves for any that match this property's stack and alias context
510-
function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[], allELM: ELM[]): DataTypeQuery[] {
568+
export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[], allELM: ELM[]): DataTypeQuery[] {
511569
return retrieves.filter(retrieve => {
512570
const stackMatch = prop.stack.findLast(ps => {
513571
// find the last property stack entry that matches any entry in the retrieve stack
@@ -519,25 +577,19 @@ function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[],
519577
if (stackMatch) {
520578
// find stackMatch in allELM
521579
const library = allELM.find(lib => lib.library.identifier.id === stackMatch.libraryName);
522-
523-
// statement definition expression should match first of the stack
524-
const topExpression = library?.library.statements.def.find(
525-
d => d.expression.localId === prop.stack[0].localId
526-
)?.expression;
527-
if (!topExpression) {
528-
throw Error(`Could not find expression ${prop.stack[0].localId} in library with id ${stackMatch.libraryName}`);
580+
if (!library) {
581+
throw new Error(`Could not find library with id ${stackMatch.libraryName}`);
529582
}
530-
531-
const localExpression = findExpressionwithLocalId(topExpression, stackMatch.localId);
532-
if (localExpression?.type === 'Query') {
583+
const localExpression = findClauseInLibrary(library, stackMatch.localId);
584+
if (!localExpression) {
585+
throw new Error(`Could not find expression ${stackMatch.localId} in library with id ${stackMatch.libraryName}`);
586+
}
587+
// TODO: handle property source? Or I think if property has source, it's always irrelevant
588+
if (localExpression?.type === 'Query' && prop.property.scope) {
533589
const query = localExpression as ELMQuery;
534590
// confirm alias matches scope
535-
const source = query.source.find(s => s.alias === prop.property.scope);
536-
if (
537-
source &&
538-
retrieve.retrieveLocalId &&
539-
findExpressionwithLocalId(source.expression, retrieve.retrieveLocalId)
540-
) {
591+
const source = findSourcewithScope(query, prop.property.scope);
592+
if (source && retrieve.retrieveLocalId && findClauseInExpression(source.expression, retrieve.retrieveLocalId)) {
541593
return true;
542594
} else {
543595
return false;
@@ -554,29 +606,39 @@ function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[],
554606
});
555607
}
556608

557-
// exp is top expression with tree of children to search
558-
function findExpressionwithLocalId(exp: object, localId: string): AnyELMExpression | undefined {
559-
if ('localId' in exp && exp.localId && exp.localId === localId) {
560-
return exp as AnyELMExpression;
561-
} else {
562-
let found;
563-
for (let i = 0; i < Object.values(exp).length; i++) {
564-
const v = Object.values(exp)[i];
565-
if (Array.isArray(v)) {
566-
for (let i = 0; i < v.length; i++) {
567-
const elem = v[i];
568-
found = findExpressionwithLocalId(elem, localId);
569-
if (found) break;
609+
// TODO: this is incomplete, needs more cases! and less strict type assumptions
610+
export function findSourcewithScope(
611+
exp: ELMQuery | ELMLast | ELMProperty,
612+
scope: string
613+
): ELMAliasedQuerySource | undefined {
614+
let source;
615+
if (exp.type === 'Query') {
616+
source = exp.source?.find(s => s.alias === scope);
617+
} else if (exp.type === 'Last') {
618+
const lastQuery = exp.source as ELMQuery; //TODO: is this okay? -> no
619+
source = lastQuery.source?.find(s => s.alias === scope);
620+
} else if (exp.type === 'Property') {
621+
// TODO: handle this case??
622+
}
623+
if (!source) {
624+
// also check any let expression sources
625+
if ('let' in exp && exp.let) {
626+
for (let i = 0; !source && i < exp.let.length; i++) {
627+
const letClause = exp.let[i];
628+
if ('source' in letClause.expression && letClause.expression.source) {
629+
source = findSourcewithScope(letClause.expression, scope);
570630
}
571-
} else if (typeof v === 'object') {
572-
found = findExpressionwithLocalId(v, localId);
573631
}
574-
if (found) break;
575632
}
576-
return found;
577633
}
634+
// check across non-query types?
635+
636+
return source;
578637
}
579638

580639
// Special case TODO: function ref madness
581640
// Special case TODO 2: expression ref layers (pair and debug cases with Hoss)
582641
// Special case TODO 3: last of... means that matching up the source will require special handling
642+
643+
// ... start making unit tests (cql to elm test data)
644+
// think about what else needs to be tested (i.e. which create function to identify which retrieve is providing the results for an expression ref)

src/helpers/elm/ELMHelpers.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,28 @@ export function findClauseInExpression(expression: any, localId: string): ELMExp
3636
}
3737
}
3838
return null;
39-
} else if (expression.localId == localId) {
39+
} else if (expression.localId === localId) {
4040
return expression as ELMExpression;
4141
} else {
4242
return findClauseInExpression(Object.values(expression), localId);
4343
}
4444
}
45+
46+
/**
47+
* Recursively search an ELM tree for all expression (clause) with a given name
48+
*
49+
* @param expression The expression tree to search for the clause in.
50+
* @param name The name to look for.
51+
* @returns The expression if found or null.
52+
*/
53+
export function findNamedClausesInExpression(expression: any, name: string): ELMExpression[] {
54+
if (typeof expression === 'string' || typeof expression === 'number' || typeof expression === 'boolean') {
55+
return [];
56+
} else if (Array.isArray(expression)) {
57+
return expression.flatMap(elem => findNamedClausesInExpression(elem, name));
58+
} else if (expression.name === name) {
59+
return [expression as ELMExpression];
60+
} else {
61+
return findNamedClausesInExpression(Object.values(expression), name);
62+
}
63+
}

0 commit comments

Comments
 (0)