Skip to content

Commit

Permalink
Add CodeableReference keyword (#1292)
Browse files Browse the repository at this point in the history
* Add CodeableReference keyword for Only Rules and Add Element Rules

* Fix InvalidTypeError

* Fix condition to log warning with. Add test to ensure assignment with multiple types

* Update warning message

* Update test summaries and include more spaces in importer test

* Don't duplicate tests between FSHImporter.Profile and FSHImporter.SDRules
  • Loading branch information
jafeltra authored Jun 22, 2023
1 parent e2b155a commit d58c5e7
Show file tree
Hide file tree
Showing 24 changed files with 2,701 additions and 2,035 deletions.
3 changes: 2 additions & 1 deletion antlr/src/main/antlr/FSH.g4
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,11 @@ quantity: NUMBER (UNIT | CODE) STRING?;
ratio: ratioPart COLON ratioPart;
reference: REFERENCE STRING?;
referenceType: REFERENCE;
codeableReferenceType: CODEABLE_REFERENCE;
canonical: CANONICAL;
ratioPart: NUMBER | quantity;
bool: KW_TRUE | KW_FALSE;
targetType: name | referenceType | canonical;
targetType: name | referenceType | canonical | codeableReferenceType;
mostAlphaKeywords: KW_MS
| KW_SU
| KW_TU
Expand Down
3 changes: 3 additions & 0 deletions antlr/src/main/antlr/FSHLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ CARD: ([0-9]+)? '..' ([0-9]+ | '*')?;
// Reference ( ITEM | ITEM )
REFERENCE: 'Reference' WS* '(' WS* SEQUENCE WS* (WS 'or' WS+ SEQUENCE WS*)* ')';
// CodeableReference ( ITEM or ITEM )
CODEABLE_REFERENCE: 'CodeableReference' WS* '(' WS* SEQUENCE WS* (WS 'or' WS+ SEQUENCE WS*)* ')';
// Canonical ( URL|VERSION or URL|VERSION )
CANONICAL : 'Canonical' WS* '(' WS* SEQUENCE ('|' SEQUENCE)? WS* (WS 'or' WS+ SEQUENCE ('|' SEQUENCE)? WS*)* ')';
Expand Down
5 changes: 3 additions & 2 deletions src/errors/InvalidTypeError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Annotated } from './Annotated';
import { ElementDefinitionType } from '../fhirtypes';
import { isReferenceType } from '../fhirtypes/common';

export class InvalidTypeError extends Error implements Annotated {
specReferences = [
Expand Down Expand Up @@ -29,8 +28,10 @@ function allowedTypesToString(types: ElementDefinitionType[]): string {
}
const strings: string[] = [];
types.forEach(t => {
if (isReferenceType(t.code)) {
if (t.code === 'Reference') {
strings.push(`Reference(${(t.targetProfile ?? []).join(' | ')})`);
} else if (t.code === 'CodeableReference') {
strings.push(`CodeableReference(${(t.targetProfile ?? []).join(' | ')})`);
} else if (t.code === 'canonical') {
strings.push(`Canonical(${(t.targetProfile ?? []).join(' | ')})`);
} else if (t.profile?.length > 0) {
Expand Down
49 changes: 43 additions & 6 deletions src/fhirtypes/ElementDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
Invariant,
SourceInfo
} from '../fshtypes';
import { AddElementRule, AssignmentValueType, OnlyRule } from '../fshtypes/rules';
import { AddElementRule, AssignmentValueType, OnlyRule, OnlyRuleType } from '../fshtypes/rules';
import {
AssignmentToCodeableReferenceError,
BindingStrengthError,
Expand Down Expand Up @@ -615,21 +615,28 @@ export class ElementDefinition {
*/
private initializeElementType(rule: AddElementRule, fisher: Fishable): ElementDefinitionType[] {
if (rule.types.length > 1 && !rule.path.endsWith('[x]')) {
// Reference/Canonical data type with multiple targets is not considered a choice data type.
if (!rule.types.every(t => t.isReference) && !rule.types.every(t => t.isCanonical)) {
// Reference/Canonical/CodeableReference data type with multiple targets is not considered a choice data type.
if (
!rule.types.every(t => t.isReference) &&
!rule.types.every(t => t.isCanonical) &&
!rule.types.every(t => t.isCodeableReference)
) {
throw new InvalidChoiceTypeRulePathError(rule);
}
}

let refTypeCnt = 0;
let canTypeCnt = 0;
let codeableRefTypeCnt = 0;

const initialTypes: ElementDefinitionType[] = [];
rule.types.forEach(t => {
if (t.isReference) {
refTypeCnt++;
} else if (t.isCanonical) {
canTypeCnt++;
} else if (t.isCodeableReference) {
codeableRefTypeCnt++;
} else {
const metadata = fisher.fishForMetadata(t.type);
initialTypes.push(new ElementDefinitionType(metadata.sdType));
Expand All @@ -647,10 +654,16 @@ export class ElementDefinition {
// will contain the URLs for each canonical.
finalTypes.push(new ElementDefinitionType('canonical'));
}
if (codeableRefTypeCnt > 0) {
// We only need to capture a single CodeableReference type. The targetProfiles attribute
// will contain the URLs for each CodeableReference reference.
finalTypes.push(new ElementDefinitionType('CodeableReference'));
}

const refCheckCnt = refTypeCnt > 0 ? refTypeCnt - 1 : 0;
const canCheckCnt = canTypeCnt > 0 ? canTypeCnt - 1 : 0;
if (rule.types.length !== finalTypes.length + refCheckCnt + canCheckCnt) {
const codableRefCheckCnt = codeableRefTypeCnt > 0 ? codeableRefTypeCnt - 1 : 0;
if (rule.types.length !== finalTypes.length + refCheckCnt + canCheckCnt + codableRefCheckCnt) {
logger.warn(
`${rule.path} includes duplicate types. Duplicates have been ignored.`,
rule.sourceInfo
Expand Down Expand Up @@ -972,6 +985,19 @@ export class ElementDefinition {
const targetType = this.getTargetType(target, fisher);
const targetTypes: ElementDefinitionType[] = targetType ? [targetType] : this.type;

// If the target type is a CodeableReference but the rule types were set via the Reference() keyword,
// log a warning to use CodeableReference keyword
if (
targetTypes.some(t => t.code === 'CodeableReference') &&
!targetTypes.some(t => t.code === 'Reference') &&
rule.types.some(t => t.isReference)
) {
logger.warn(
'The CodeableReference() keyword should be used to constrain references of a CodeableReference',
rule.sourceInfo
);
}

// Setup a map to store how each existing element type maps to the input types
const typeMatches: Map<string, ElementTypeMatchInfo[]> = new Map();
targetTypes.forEach(t => typeMatches.set(t.code, []));
Expand Down Expand Up @@ -1160,7 +1186,7 @@ export class ElementDefinition {
/**
* Given an input type (the constraint) and a set of target types (the things to potentially
* constrain), find the match and return information about it.
* @param {{ type: string; isReference?: boolean; isCanonical?: boolean }} type - the constrained
* @param {OnlyRuleType} type - the constrained
* types, identified by id/type/url string and an optional reference/canonical flags (defaults false)
* @param {ElementDefinitionType[]} targetTypes - the element types that the constrained type
* can be potentially applied to
Expand All @@ -1172,7 +1198,7 @@ export class ElementDefinition {
* @throws {InvalidTypeError} when the type doesn't match any of the targetTypes
*/
private findTypeMatch(
type: { type: string; isReference?: boolean; isCanonical?: boolean },
type: OnlyRuleType,
targetTypes: ElementDefinitionType[],
fisher: Fishable
): ElementTypeMatchInfo {
Expand Down Expand Up @@ -1218,6 +1244,15 @@ export class ElementDefinition {
t2.code === 'canonical' &&
(t2.targetProfile == null || t2.targetProfile.includes(md.url))
);
} else if (type.isCodeableReference) {
// CodeableReferences always have a code 'CodeableReference' w/ the referenced type's defining URL set as
// one of the targetProfiles. If the targetProfile property is null, that means any
// CodeableReference reference is allowed.
matchedType = targetTypes.find(
t2 =>
t2.code === 'CodeableReference' &&
(t2.targetProfile == null || t2.targetProfile.includes(md.url))
);
} else {
// Look for exact match on the code (w/ no profile) or a match on an allowed base type with
// a matching profile
Expand Down Expand Up @@ -1252,6 +1287,8 @@ export class ElementDefinition {
typeString = `Reference(${type.type})`;
} else if (type.isCanonical) {
typeString = `Canonical(${type.type})`;
} else if (type.isCodeableReference) {
typeString = `CodeableReference(${type.type})`;
} else {
typeString = type.type;
}
Expand Down
10 changes: 9 additions & 1 deletion src/fshtypes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { findLast } from 'lodash';
export function typeString(types: OnlyRuleType[]): string {
const references: OnlyRuleType[] = [];
const canonicals: OnlyRuleType[] = [];
const codeableReferences: OnlyRuleType[] = [];
const normals: OnlyRuleType[] = [];
types.forEach(t => {
if (t.isReference) {
references.push(t);
} else if (t.isCanonical) {
canonicals.push(t);
} else if (t.isCodeableReference) {
codeableReferences.push(t);
} else {
normals.push(t);
}
Expand All @@ -21,7 +24,12 @@ export function typeString(types: OnlyRuleType[]): string {
const canonicalString = canonicals.length
? `Canonical(${canonicals.map(t => t.type).join(' or ')})`
: '';
return [normalString, referenceString, canonicalString].filter(s => s).join(' or ');
const codeableReferenceString = codeableReferences.length
? `CodeableReference(${codeableReferences.map(t => t.type).join(' or ')})`
: '';
return [normalString, referenceString, canonicalString, codeableReferenceString]
.filter(s => s)
.join(' or ');
}

// Adds expected backslash-escapes to a string to make it a FSH string
Expand Down
1 change: 1 addition & 0 deletions src/fshtypes/rules/OnlyRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export type OnlyRuleType = {
type: string;
isReference?: boolean;
isCanonical?: boolean;
isCodeableReference?: boolean;
};
9 changes: 9 additions & 0 deletions src/import/FSHImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,15 @@ export class FSHImporter extends FSHVisitor {
isReference: true
})
);
} else if (t.codeableReferenceType()) {
const codeableReferenceToken = t.codeableReferenceType().CODEABLE_REFERENCE();
const codeableReferences = this.parseOrReference(codeableReferenceToken.getText());
codeableReferences.forEach(r =>
orTypes.push({
type: this.aliasAwareValue(codeableReferenceToken, r),
isCodeableReference: true
})
);
} else if (t.canonical()) {
const canonicals = this.visitCanonical(t.canonical());
canonicals.forEach(c =>
Expand Down
5 changes: 4 additions & 1 deletion src/import/generated/FSH.interp

Large diffs are not rendered by default.

37 changes: 19 additions & 18 deletions src/import/generated/FSH.tokens
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,25 @@ DATETIME=63
TIME=64
CARD=65
REFERENCE=66
CANONICAL=67
CARET_SEQUENCE=68
REGEX=69
BLOCK_COMMENT=70
SEQUENCE=71
WHITESPACE=72
LINE_COMMENT=73
PARAM_RULESET_REFERENCE=74
RULESET_REFERENCE=75
BRACKETED_PARAM=76
LAST_BRACKETED_PARAM=77
PLAIN_PARAM=78
LAST_PLAIN_PARAM=79
QUOTED_CONTEXT=80
LAST_QUOTED_CONTEXT=81
UNQUOTED_CONTEXT=82
LAST_UNQUOTED_CONTEXT=83
CONTEXT_WHITESPACE=84
CODEABLE_REFERENCE=67
CANONICAL=68
CARET_SEQUENCE=69
REGEX=70
BLOCK_COMMENT=71
SEQUENCE=72
WHITESPACE=73
LINE_COMMENT=74
PARAM_RULESET_REFERENCE=75
RULESET_REFERENCE=76
BRACKETED_PARAM=77
LAST_BRACKETED_PARAM=78
PLAIN_PARAM=79
LAST_PLAIN_PARAM=80
QUOTED_CONTEXT=81
LAST_QUOTED_CONTEXT=82
UNQUOTED_CONTEXT=83
LAST_UNQUOTED_CONTEXT=84
CONTEXT_WHITESPACE=85
'?!'=24
'MS'=25
'SU'=26
Expand Down
5 changes: 4 additions & 1 deletion src/import/generated/FSHLexer.interp

Large diffs are not rendered by default.

Loading

0 comments on commit d58c5e7

Please sign in to comment.