Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CodeableReference keyword #1292

Merged
merged 7 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
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 @@ -590,21 +590,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 @@ -622,10 +629,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 @@ -947,6 +960,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 @@ -1135,7 +1161,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 @@ -1147,7 +1173,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 @@ -1193,6 +1219,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 @@ -1227,6 +1262,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