Skip to content

Commit

Permalink
Allow fragment references in Canonical, Reference, and system
Browse files Browse the repository at this point in the history
A fragment reference may be used with the Canonical or Reference
keywords when referring to a contained resource. A fragment reference to
a CodeSystem may be used when defining a ValueSet.

The current implementation is not yet complete. There is still a need
to support fragment references to contained ValueSets when defining a
ValueSet. Additional tests may also be needed for the existing
functionality.
  • Loading branch information
mint-thompson committed Dec 30, 2024
1 parent 09b2585 commit 5e76d5a
Show file tree
Hide file tree
Showing 16 changed files with 1,863 additions and 826 deletions.
5 changes: 3 additions & 2 deletions antlr/src/main/antlr/FSH.g4
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ vsComponent: STAR ( KW_INCLUDE | KW_EXCLUDE )? ( vsConceptComponent | vsF
vsConceptComponent: code vsComponentFrom?;
vsFilterComponent: KW_CODES vsComponentFrom (KW_WHERE vsFilterList)?;
vsComponentFrom: KW_FROM (vsFromSystem (KW_AND vsFromValueset)? | vsFromValueset (KW_AND vsFromSystem)?);
vsFromSystem: KW_SYSTEM name;
vsFromValueset: KW_VSREFERENCE name (KW_AND name)*;
vsFromSystem: KW_SYSTEM vsFromTarget;
vsFromValueset: KW_VSREFERENCE vsFromTarget (KW_AND vsFromTarget)*;
vsFromTarget: (name | CODE);
vsFilterList: vsFilterDefinition (KW_AND vsFilterDefinition)*;
vsFilterDefinition: name vsFilterOperator vsFilterValue?;
vsFilterOperator: EQUAL | SEQUENCE;
Expand Down
49 changes: 42 additions & 7 deletions src/export/InstanceExporter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FSHTank } from '../import/FSHTank';
import { StructureDefinition, InstanceDefinition, ElementDefinition, PathPart } from '../fhirtypes';
import { FshCanonical, Instance, SourceInfo } from '../fshtypes';
import { FshCanonical, FshReference, FshCode, Instance, SourceInfo } from '../fshtypes';
import {
logger,
Fishable,
Expand All @@ -24,7 +24,8 @@ import {
determineKnownSlices,
setImpliedPropertiesOnInstance,
getMatchingContainedReferenceId,
checkForMultipleChoice
checkForMultipleChoice,
getMatchingContainedReferenceInfo
} from '../fhirtypes/common';
import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError';
import { AbstractInstanceOfError } from '../errors/AbstractInstanceOfError';
Expand Down Expand Up @@ -105,9 +106,12 @@ export class InstanceExporter implements Fishable {
rules.forEach(r => {
r.path = r.path.replace(/\[0+\]/g, '');
});
rules = rules.map(r =>
r instanceof PathRule ? r : replaceReferences(r, this.tank, this.fisher)
);
// rules = rules.map(r =>
// r instanceof PathRule ? r : replaceReferences(r, this.tank, this.fisher)
// );
// maybe i replace the reference later? and maybe replaceReferences needs to know about contained resources
// or maybe we want a separate check before replaceReferences that looks in contained resources

// Convert strings in AssignmentRules to instances
rules = rules.filter(r => {
if (r instanceof AssignmentRule && r.isInstance) {
Expand Down Expand Up @@ -173,20 +177,48 @@ export class InstanceExporter implements Fishable {
> = new Map();
// Keep track specifically of the rules on contained (path could be contained[index], contained.some-path, or contained)
const containedRules: { pathParts: PathPart[]; assignedValue: any }[] = [];
// Keep track specifically of the rules on contained.resourceType
const containedResourceTypes: { pathParts: PathPart[]; assignedValue: any }[] = [];
rules.forEach(rule => {
const inlineResourceTypes: string[] = [];
// define function that will be re-used in attempting to assign a value or inline instance
const doRuleValidation = (value: AssignmentValueType) => {
// Before validating the rule, check if the Canonical keyword was used to reference a contained value set
if (value instanceof FshCanonical) {
const entityName = value.entityName;
if (rule instanceof AssignmentRule && value instanceof FshCanonical) {
// there may be a leading # for a local reference
let entityName = value.entityName;
if (entityName.startsWith('#')) {
entityName = entityName.slice(1);
}
const matchingContainedReferenceId = getMatchingContainedReferenceId(
entityName,
containedRules
);
if (matchingContainedReferenceId) {
value = `#${matchingContainedReferenceId}`;
} else {
value = replaceReferences(rule, this.tank, this.fisher).value;
}
} else if (rule instanceof AssignmentRule && value instanceof FshReference) {
let foundLocalReference = false;
if (value.reference.startsWith('#')) {
// local reference
const matchingContainedInfo = getMatchingContainedReferenceInfo(
value.reference.slice(1),
containedRules,
containedResourceTypes
);
if (matchingContainedInfo) {
foundLocalReference = true;
value.reference = `#${matchingContainedInfo.id}`;
value.sdType = matchingContainedInfo.sdType;
}
}
if (!foundLocalReference) {
value = replaceReferences(rule, this.tank, this.fisher).value;
}
} else if (rule instanceof AssignmentRule && rule.value instanceof FshCode) {
value = replaceReferences(rule, this.tank, this.fisher).value;
}
const validatedRule = instanceOfStructureDefinition.validateValueAtPath(
rule.path,
Expand Down Expand Up @@ -218,8 +250,11 @@ export class InstanceExporter implements Fishable {
});
// Check if the rule we just validated was at a valid contained path to keep track for resolving Canonical references
// Only check if the rule was a directly contained resources (aka 'contained' or 'contained.name/url/id', optionally with slice names or indices)
// we also track rules on resourceType to help resolve Reference keywords.
if (/^contained(\[[^\]]+\])*(\.url|\.name|\.id)?$/.test(rule.path)) {
containedRules.push(validatedRule);
} else if (/^contained(\[[^\]]+\])*\.resourceType$/.test(rule.path)) {
containedResourceTypes.push(validatedRule);
}
}
};
Expand Down
127 changes: 120 additions & 7 deletions src/export/StructureDefinitionExporter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { isEmpty, padEnd } from 'lodash';
import { cloneDeep, isEmpty, padEnd } from 'lodash';
import {
ElementDefinition,
ElementDefinitionBindingStrength,
idRegex,
InstanceDefinition,
StructureDefinition,
CodeSystem,
Resource as FhirResource
Resource as FhirResource,
PathPart
} from '../fhirtypes';
import {
Extension,
Expand All @@ -18,7 +19,9 @@ import {
Instance,
FshCode,
FshEntity,
ExtensionContext
ExtensionContext,
FshCanonical,
FshReference
} from '../fshtypes';
import { FSHTank } from '../import';
import { InstanceExporter } from '../export';
Expand Down Expand Up @@ -54,7 +57,8 @@ import {
fishForFHIRBestVersion,
Metadata,
MasterFisher,
resolveSoftIndexing
resolveSoftIndexing,
parseFSHPath
} from '../utils';
import {
applyInsertRules,
Expand All @@ -68,7 +72,9 @@ import {
TYPE_CHARACTERISTICS_CODE,
TYPE_CHARACTERISTICS_EXTENSION,
LOGICAL_TARGET_EXTENSION,
checkForMultipleChoice
checkForMultipleChoice,
getMatchingContainedReferenceId,
getMatchingContainedReferenceInfo
} from '../fhirtypes/common';
import { Package } from './Package';
import { isUri } from 'valid-url';
Expand Down Expand Up @@ -731,6 +737,10 @@ export class StructureDefinitionExporter implements Fishable {
// therefore, rules that assign values within those instances must also be deferred.
const directResourcePaths: string[] = [];
const inlineResourcePaths: { path: string; caretPath: string; instanceOf: string }[] = [];
// Keep track specifically of the rules on contained (path could be contained[index], contained.some-path, or contained)
const containedRules: { pathParts: PathPart[]; assignedValue: any }[] = [];
// Keep track specifically of the rules on contained.resourceType
const containedResourceTypes: { pathParts: PathPart[]; assignedValue: any }[] = [];
// first, collect the information we can from rules that set a resourceType
// if instances are directly assigned, we'll get information from them when we fish up the instance.
rules
Expand Down Expand Up @@ -832,7 +842,43 @@ export class StructureDefinitionExporter implements Fishable {
}
rule.value = instance;
}
const replacedRule = replaceReferences(rule, this.tank, this);
let replacedRule: AssignmentRule;
if (rule.value instanceof FshCanonical) {
let entityName = rule.value.entityName;
// if we're trying to get the canonical using a fragment,
// this means the target could be a contained resource.
if (entityName.startsWith('#')) {
entityName = entityName.slice(1);
}
const matchingContainedReferenceId = getMatchingContainedReferenceId(
entityName,
containedRules
);
if (matchingContainedReferenceId) {
replacedRule = cloneDeep(rule);
replacedRule.value = `#${matchingContainedReferenceId}`;
} else {
replacedRule = replaceReferences(rule, this.tank, this);
}
} else if (rule.value instanceof FshReference) {
if (rule.value.reference.startsWith('#')) {
const matchingContainedInfo = getMatchingContainedReferenceInfo(
rule.value.reference.slice(1),
containedRules,
containedResourceTypes
);
if (matchingContainedInfo) {
replacedRule = cloneDeep(rule);
(replacedRule.value as FshReference).reference = `#${matchingContainedInfo.id}`;
(replacedRule.value as FshReference).sdType = matchingContainedInfo.sdType;
}
}
if (!replacedRule) {
replacedRule = replaceReferences(rule, this.tank, this);
}
} else {
replacedRule = replaceReferences(rule, this.tank, this);
}
try {
element.assignValue(replacedRule.value, replacedRule.exactly, this);
} catch (originalErr) {
Expand Down Expand Up @@ -948,7 +994,46 @@ export class StructureDefinitionExporter implements Fishable {
});
}
} else if (rule instanceof CaretValueRule) {
const replacedRule = replaceReferences(rule, this.tank, this);
// instead of replacing references immediately, we have to
// take into account fragment references
let replacedRule: CaretValueRule;
if (rule.value instanceof FshCanonical) {
let entityName = rule.value.entityName;
// if we're trying to get the canonical using a fragment,
// this means the target could be a contained resource.
if (entityName.startsWith('#')) {
entityName = entityName.slice(1);
}
const matchingContainedReferenceId = getMatchingContainedReferenceId(
entityName,
containedRules
);
if (matchingContainedReferenceId) {
replacedRule = cloneDeep(rule);
replacedRule.value = `#${matchingContainedReferenceId}`;
} else {
replacedRule = replaceReferences(rule, this.tank, this);
}
} else if (rule.value instanceof FshReference) {
if (rule.value.reference.startsWith('#')) {
const matchingContainedInfo = getMatchingContainedReferenceInfo(
rule.value.reference.slice(1),
containedRules,
containedResourceTypes
);
if (matchingContainedInfo) {
replacedRule = cloneDeep(rule);
(replacedRule.value as FshReference).reference = `#${matchingContainedInfo.id}`;
(replacedRule.value as FshReference).sdType = matchingContainedInfo.sdType;
}
}
if (!replacedRule) {
replacedRule = replaceReferences(rule, this.tank, this);
}
} else {
replacedRule = replaceReferences(rule, this.tank, this);
}

if (replacedRule.path !== '') {
element.setInstancePropertyByPath(replacedRule.caretPath, replacedRule.value, this);
} else {
Expand Down Expand Up @@ -979,6 +1064,19 @@ export class StructureDefinitionExporter implements Fishable {
return replacedRule.caretPath.startsWith(`${i}.`);
});
if (replacedRule.isInstance) {
// if this is assigning a contained resource,
// get its metadata for help in resolving fragment references.
if (
/^contained(\[[^\]]+\])*$/.test(rule.caretPath) &&
typeof replacedRule.value === 'string'
) {
const assignmentMeta = this.tank.fishForMetadata(replacedRule.value);
containedRules.push({
pathParts: parseFSHPath(rule.caretPath),
assignedValue: assignmentMeta
});
}

if (this.deferredCaretRules.has(structDef)) {
this.deferredCaretRules
.get(structDef)
Expand All @@ -998,6 +1096,21 @@ export class StructureDefinitionExporter implements Fishable {
this.deferredCaretRules.set(structDef, [{ rule: replacedRule, tryFish: false }]);
}
} else {
// if this is assigning id, name, url, or resourceType of a contained resource,
// record it to use when resolving references.
if (/^contained(\[[^\]]+\])*(\.url|\.name|\.id)$/.test(rule.caretPath)) {
containedRules.push({
pathParts: parseFSHPath(rule.caretPath),
assignedValue: rule.value
});
// containedRules.push(replacedRule);
} else if (/^contained(\[[^\]]+\])*\.resourceType$/.test(rule.caretPath)) {
// containedResourceTypes.push(replacedRule);
containedResourceTypes.push({
pathParts: parseFSHPath(rule.caretPath),
assignedValue: rule.value
});
}
try {
structDef.setInstancePropertyByPath(
replacedRule.caretPath,
Expand Down
21 changes: 16 additions & 5 deletions src/export/ValueSetExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,15 @@ export class ValueSetExporter {
components.forEach(component => {
const composeElement: ValueSetComposeIncludeOrExclude = {};
if (component.from.system) {
const systemParts = component.from.system.split('|');
const csMetadata = this.fisher.fishForMetadata(component.from.system, Type.CodeSystem);
let isFragmentReference = false;
let systemReference = component.from.system;
if (systemReference.startsWith('#')) {
isFragmentReference = true;
systemReference = systemReference.slice(1);
}
const systemParts = systemReference.split('|');

const csMetadata = this.fisher.fishForMetadata(systemReference, Type.CodeSystem);
// if we found metadata, use it.
// if we didn't find any matching metadata, the code system might be defined directly on the valueset.
let isContainedSystem: boolean;
Expand All @@ -90,9 +97,9 @@ export class ValueSetExporter {
} else {
const directSystem: any = valueSet.contained?.find((resource: any) => {
return (
(resource?.id === component.from.system ||
resource?.name === component.from.system ||
resource?.url === component.from.system) &&
(resource?.id === systemReference ||
resource?.name === systemReference ||
resource?.url === systemReference) &&
resource?.resourceType === 'CodeSystem'
);
});
Expand Down Expand Up @@ -124,6 +131,10 @@ export class ValueSetExporter {
component.sourceInfo
);
return;
} else if (isFragmentReference) {
logger.warn(
`CodeSystem ${component.from.system} is referenced using a fragment, but is not contained. It should be referenced by id, name, or url.`
);
}

// if the rule specified a version, use that version.
Expand Down
Loading

0 comments on commit 5e76d5a

Please sign in to comment.