From 1d43112b9b4dc15f59571654bb7a9e09743c7b2f Mon Sep 17 00:00:00 2001 From: Mint Thompson Date: Fri, 25 Oct 2024 09:01:17 -0400 Subject: [PATCH] Support fully defining resources using caret rules A contained resource can be defined within another resource one rule at a time using caret rules. By setting the resourceType of the contained resource, SUSHI will be able to correctly validate other rules that build up the contained resource. Most of the logic for this already existed, but some additional code was needed in order to collect information about inline resource types. Additionally, if an instance is assigned with a caret rule, further caret rules may be used to modify the contained resource. This gets a little complicated when exporting a StructureDefinition. A ValueSet may refer to a CodeSystem that is defined with caret rules on the ValueSet. The resulting compose element will look the same as if the inline CodeSystem were defined separately. Log a warning when an example instance is assigned within a conformance resource. Update object definitions for inline instance tests so that the Instances are, in fact, inline. --- src/export/CodeSystemExporter.ts | 67 ++- src/export/InstanceExporter.ts | 18 + src/export/StructureDefinitionExporter.ts | 195 ++++-- src/export/ValueSetExporter.ts | 111 +++- src/fhirtypes/StructureDefinition.ts | 9 +- src/fhirtypes/common.ts | 20 +- test/export/CodeSystemExporter.test.ts | 289 +++++++++ test/export/FHIRExporter.test.ts | 283 ++++++++- test/export/InstanceExporter.test.ts | 107 +++- ...tructureDefinition.ProfileExporter.test.ts | 110 +++- .../StructureDefinitionExporter.test.ts | 100 ++++ test/export/ValueSetExporter.test.ts | 561 +++++++++++++++++- 12 files changed, 1782 insertions(+), 88 deletions(-) diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts index 36ef05091..8280404a7 100644 --- a/src/export/CodeSystemExporter.ts +++ b/src/export/CodeSystemExporter.ts @@ -8,7 +8,8 @@ import { setImpliedPropertiesOnInstance, validateInstanceFromRawValue, isExtension, - replaceReferences + replaceReferences, + splitOnPathPeriods } from '../fhirtypes/common'; import { FshCodeSystem } from '../fshtypes'; import { CaretValueRule, ConceptRule } from '../fshtypes/rules'; @@ -130,6 +131,18 @@ export class CodeSystemExporter { // so, we only need to track rules that involve an extension. const ruleMap: Map = new Map(); const codeSystemSD = codeSystem.getOwnStructureDefinition(this.fisher); + const inlineResourcePaths: { path: string; caretPath: string; instanceOf: string }[] = []; + // first, collect the information we can from rules that set a resourceType + // if instances are directly assigned, we'll get information from them upon validation + successfulRules.forEach((r: CaretValueRule) => { + if (r.caretPath.endsWith('.resourceType') && typeof r.value === 'string' && !r.isInstance) { + inlineResourcePaths.push({ + path: r.path, + caretPath: splitOnPathPeriods(r.caretPath).slice(0, -1).join('.'), + instanceOf: r.value + }); + } + }); const successfulRulesWithInstances = successfulRules .map(rule => { if (rule.isInstance) { @@ -142,20 +155,43 @@ export class CodeSystemExporter { ); return null; } + if (instance._instanceMeta.usage === 'Example') { + logger.warn( + `Contained instance "${rule.value}" is an example and probably should not be included in a conformance resource.`, + rule.sourceInfo + ); + } rule.value = instance; + inlineResourcePaths.push({ + path: rule.path, + caretPath: rule.caretPath, + instanceOf: instance.resourceType + }); } + const matchingInlineResourcePaths = inlineResourcePaths.filter(i => { + return ( + rule.path == i.path && + rule.caretPath.startsWith(`${i.caretPath}.`) && + rule.caretPath !== `${i.caretPath}.resourceType` + ); + }); + const inlineResourceTypes: string[] = []; + matchingInlineResourcePaths.forEach(match => { + inlineResourceTypes[splitOnPathPeriods(match.caretPath).length - 1] = match.instanceOf; + }); const path = rule.path.length > 1 ? `${rule.path}.${rule.caretPath}` : rule.caretPath; try { const replacedRule = replaceReferences(rule, this.tank, this.fisher); const { pathParts } = codeSystemSD.validateValueAtPath( path, replacedRule.value, - this.fisher + this.fisher, + inlineResourceTypes ); if (pathParts.some(part => isExtension(part.base))) { ruleMap.set(assembleFSHPath(pathParts).replace(/\[0+\]/g, ''), { pathParts }); } - return replacedRule; + return { rule: replacedRule, inlineResourceTypes }; } catch (originalErr) { // if an Instance has an id that looks like a number, bigint, or boolean, // we may have tried to assign that value instead of an Instance. @@ -171,13 +207,27 @@ export class CodeSystemExporter { rule, instanceExporter, this.fisher, - originalErr + originalErr, + inlineResourceTypes ); + if (instance?._instanceMeta.usage === 'Example') { + logger.warn( + `Contained instance "${rule.rawValue}" is an example and probably should not be included in a conformance resource.`, + rule.sourceInfo + ); + } rule.value = instance; + if (instance != null) { + inlineResourcePaths.push({ + path: rule.path, + caretPath: rule.caretPath, + instanceOf: instance.resourceType + }); + } if (pathParts.some(part => isExtension(part.base))) { ruleMap.set(assembleFSHPath(pathParts).replace(/\[0+\]/g, ''), { pathParts }); } - return rule; + return { rule, inlineResourceTypes }; } else { logger.error(originalErr.message, rule.sourceInfo); if (originalErr.stack) { @@ -193,18 +243,19 @@ export class CodeSystemExporter { codeSystem, codeSystemSD, [...ruleMap.keys()], - [], + inlineResourcePaths.map(i => i.caretPath), this.fisher, knownSlices ); - for (const rule of successfulRulesWithInstances) { + for (const { rule, inlineResourceTypes } of successfulRulesWithInstances) { try { setPropertyOnDefinitionInstance( codeSystem, rule.path.length > 1 ? `${rule.path}.${rule.caretPath}` : rule.caretPath, rule.value, - this.fisher + this.fisher, + inlineResourceTypes ); } catch (err) { logger.error(err.message, rule.sourceInfo); diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts index f2a9bb6f6..108c042e6 100644 --- a/src/export/InstanceExporter.ts +++ b/src/export/InstanceExporter.ts @@ -112,6 +112,15 @@ export class InstanceExporter implements Fishable { if (r instanceof AssignmentRule && r.isInstance) { const instance: InstanceDefinition = this.fishForFHIR(r.value as string); if (instance != null) { + if ( + instance._instanceMeta.usage === 'Example' && + instanceDef._instanceMeta.usage === 'Definition' + ) { + logger.warn( + `Contained instance "${r.value}" is an example and probably should not be included in a conformance resource.`, + r.sourceInfo + ); + } r.value = instance; return true; } else { @@ -254,6 +263,15 @@ export class InstanceExporter implements Fishable { } } else { try { + if ( + instanceToAssign._instanceMeta.usage === 'Example' && + instanceDef._instanceMeta.usage === 'Definition' + ) { + logger.warn( + `Contained instance "${rule.rawValue}" is an example and probably should not be included in a conformance resource.`, + rule.sourceInfo + ); + } doRuleValidation(instanceToAssign); } catch (instanceErr) { if (instanceErr instanceof MismatchedTypeError) { diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts index 09ff205f9..a144e3777 100644 --- a/src/export/StructureDefinitionExporter.ts +++ b/src/export/StructureDefinitionExporter.ts @@ -103,7 +103,7 @@ const UNINHERITED_EXTENSIONS = [ export class StructureDefinitionExporter implements Fishable { deferredCaretRules = new Map< StructureDefinition, - { rule: CaretValueRule; originalErr?: MismatchedTypeError }[] + { rule: CaretValueRule; tryFish: boolean; originalErr?: MismatchedTypeError }[] >(); knownBindingRules = new Map< StructureDefinition, @@ -723,6 +723,27 @@ export class StructureDefinitionExporter implements Fishable { // When we process obeys rules, we may add rules we don't want reflected in preprocessed // output, so make a shallow copy of the array and iterate over that instead of the original const rules = fshDefinition.rules.slice(); + // if instances are assigned directly or constructed with a set of caret rules, + // we need to keep track of their paths. + const directResourcePaths: string[] = []; + const inlineResourcePaths: { path: string; caretPath: string; instanceOf: string }[] = []; + rules + .filter(r => r instanceof CaretValueRule) + .forEach((r: CaretValueRule) => { + if (r.path === '' && r.isInstance) { + directResourcePaths.push(r.caretPath); + } else if ( + r.caretPath.endsWith('.resourceType') && + typeof r.value === 'string' && + !r.isInstance + ) { + inlineResourcePaths.push({ + path: r.path, + caretPath: splitOnPathPeriods(r.caretPath).slice(0, -1).join('.'), + instanceOf: r.value + }); + } + }); for (let i = 0; i < rules.length; i++) { const rule = rules[i]; @@ -797,6 +818,12 @@ export class StructureDefinitionExporter implements Fishable { } continue; } + if (instance._instanceMeta.usage === 'Example') { + logger.warn( + `Contained instance "${rule.value}" is an example and probably should not be included in a conformance resource.`, + rule.sourceInfo + ); + } rule.value = instance; } const replacedRule = replaceReferences(rule, this.tank, this); @@ -818,6 +845,12 @@ export class StructureDefinitionExporter implements Fishable { } else { try { element.assignValue(instance, rule.exactly, this); + if (instance._instanceMeta.usage === 'Example') { + logger.warn( + `Contained instance "${rule.value}" is an example and probably should not be included in a conformance resource.`, + rule.sourceInfo + ); + } rule.value = instance; } catch (instanceErr) { // if it's still the wrong type, the assignment will fail. @@ -913,18 +946,46 @@ export class StructureDefinitionExporter implements Fishable { if (replacedRule.path !== '') { element.setInstancePropertyByPath(replacedRule.caretPath, replacedRule.value, this); } else { + const matchingInlineResourcePaths = inlineResourcePaths.filter(i => { + return ( + replacedRule.path === i.path && + replacedRule.caretPath.startsWith(`${i.caretPath}.`) && + replacedRule.caretPath !== `${i.caretPath}.resourceType` + ); + }); + const inlineResourceTypes: string[] = []; + matchingInlineResourcePaths.forEach(match => { + inlineResourceTypes[splitOnPathPeriods(match.caretPath).length - 1] = + match.instanceOf; + }); + const matchingDirectResourcePaths = directResourcePaths.filter(i => { + return replacedRule.caretPath.startsWith(`${i}.`); + }); if (replacedRule.isInstance) { if (this.deferredCaretRules.has(structDef)) { - this.deferredCaretRules.get(structDef).push({ rule: replacedRule }); + this.deferredCaretRules + .get(structDef) + .push({ rule: replacedRule, tryFish: true }); + } else { + this.deferredCaretRules.set(structDef, [{ rule: replacedRule, tryFish: true }]); + } + } else if (matchingDirectResourcePaths.length > 0) { + // we may be assigning a caret rule of a non-instance value on top of an assigned instance + // if so, defer + if (this.deferredCaretRules.has(structDef)) { + this.deferredCaretRules + .get(structDef) + .push({ rule: replacedRule, tryFish: false }); } else { - this.deferredCaretRules.set(structDef, [{ rule: replacedRule }]); + this.deferredCaretRules.set(structDef, [{ rule: replacedRule, tryFish: false }]); } } else { try { structDef.setInstancePropertyByPath( replacedRule.caretPath, replacedRule.value, - this + this, + inlineResourceTypes ); } catch (originalErr) { if ( @@ -936,9 +997,11 @@ export class StructureDefinitionExporter implements Fishable { if (this.deferredCaretRules.has(structDef)) { this.deferredCaretRules .get(structDef) - .push({ rule: replacedRule, originalErr }); + .push({ rule: replacedRule, tryFish: true, originalErr }); } else { - this.deferredCaretRules.set(structDef, [{ rule: replacedRule, originalErr }]); + this.deferredCaretRules.set(structDef, [ + { rule: replacedRule, tryFish: true, originalErr } + ]); } } else { throw originalErr; @@ -999,53 +1062,115 @@ export class StructureDefinitionExporter implements Fishable { } applyDeferredRules() { + const successfulInstanceAssignments = new Map< + StructureDefinition, + { caretPath: string; resourceType: string }[] + >(); + const sdsToCleanAgain = new Set(); this.deferredCaretRules.forEach((rules, sd) => { - for (const { rule, originalErr } of rules) { - let fishItem: string; - if (typeof rule.value === 'string') { - fishItem = rule.value; - } else if (['number', 'bigint', 'boolean'].includes(typeof rule.value)) { - fishItem = rule.rawValue; - } + for (const { rule, tryFish, originalErr } of rules) { + if (tryFish) { + let fishItem: string; + if (typeof rule.value === 'string') { + fishItem = rule.value; + } else if (['number', 'bigint', 'boolean'].includes(typeof rule.value)) { + fishItem = rule.rawValue; + } - const instanceExporter = new InstanceExporter(this.tank, this.pkg, this.fisher); - let fishedValue = instanceExporter.fishForFHIR(fishItem); - if (fishedValue == null) { - const result = this.fishForFHIR(fishItem); - if (!(result instanceof InstanceDefinition) && result instanceof Object) { - fishedValue = InstanceDefinition.fromJSON(fishedValue); + const instanceExporter = new InstanceExporter(this.tank, this.pkg, this.fisher); + let fishedValue = instanceExporter.fishForFHIR(fishItem); + if (fishedValue == null) { + const result = this.fishForFHIR(fishItem); + if (!(result instanceof InstanceDefinition) && result instanceof Object) { + fishedValue = InstanceDefinition.fromJSON(fishedValue); + } } - } - if (fishedValue instanceof InstanceDefinition) { - try { - sd.setInstancePropertyByPath(rule.caretPath, fishedValue, this); - } catch (e) { - if (e instanceof MismatchedTypeError && originalErr != null) { + if (fishedValue instanceof InstanceDefinition) { + try { + if (fishedValue._instanceMeta.usage === 'Example') { + logger.warn( + `Contained instance "${rule.value}" is an example and probably should not be included in a conformance resource.`, + rule.sourceInfo + ); + } + sd.setInstancePropertyByPath(rule.caretPath, fishedValue, this); + if (successfulInstanceAssignments.has(sd)) { + successfulInstanceAssignments.get(sd).push({ + caretPath: rule.caretPath, + resourceType: fishedValue.resourceType + }); + } else { + successfulInstanceAssignments.set(sd, [ + { + caretPath: rule.caretPath, + resourceType: + fishedValue.meta?.profile?.[0] ?? + fishedValue._instanceMeta.instanceOfUrl ?? + fishedValue._instanceMeta.sdType ?? + fishedValue.resourceType + } + ]); + } + } catch (e) { + if (e instanceof MismatchedTypeError && originalErr != null) { + logger.error(originalErr.message, rule.sourceInfo); + if (originalErr.stack) { + logger.debug(originalErr.stack); + } + } else { + logger.error(e.message, rule.sourceInfo); + if (e.stack) { + logger.debug(e.stack); + } + } + } + } else { + if (originalErr != null) { logger.error(originalErr.message, rule.sourceInfo); if (originalErr.stack) { logger.debug(originalErr.stack); } } else { - logger.error(e.message, rule.sourceInfo); - if (e.stack) { - logger.debug(e.stack); - } + logger.error(`Could not find a resource named ${rule.value}`, rule.sourceInfo); } } } else { - if (originalErr != null) { - logger.error(originalErr.message, rule.sourceInfo); - if (originalErr.stack) { - logger.debug(originalErr.stack); + // when assigning a non-instance value within the contained resource, we expect the resource type to be in place + const matchingInstancePaths = (successfulInstanceAssignments.get(sd) ?? []).filter(i => { + return ( + rule.caretPath.startsWith(`${i.caretPath}.`) && + rule.caretPath !== `${i.caretPath}.resourceType` + ); + }); + const inlineResourceTypes: string[] = []; + matchingInstancePaths.forEach(match => { + inlineResourceTypes[splitOnPathPeriods(match.caretPath).length - 1] = + match.resourceType; + }); + try { + if (inlineResourceTypes.length > 0) { + // the resource was cleaned during export, but since we are going to modify it, now we have to clean it again. + sdsToCleanAgain.add(sd); + } + sd.setInstancePropertyByPath(rule.caretPath, rule.value, this, inlineResourceTypes); + } catch (e) { + logger.error(e.message, rule.sourceInfo); + if (e.stack) { + logger.debug(e.stack); } - } else { - logger.error(`Could not find a resource named ${rule.value}`, rule.sourceInfo); } } } }); + // for any sd that has contained instances assigned and then modified, we need to re-clean + // this cleans all contained instances, not just modified ones. should we try to be more targeted with cleaning? + sdsToCleanAgain.forEach(sd => { + sd.contained?.forEach(containedResource => { + cleanResource(containedResource as InstanceDefinition); + }); + }); // we need to double-check all our bindings in case we now contain the bound valueset. // for inline instances, we should give a special error if they're not contained. // for anything else, it's okay if they're not contained. but if they are, use a relative reference. diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts index 27ef54633..b61f042e3 100644 --- a/src/export/ValueSetExporter.ts +++ b/src/export/ValueSetExporter.ts @@ -23,7 +23,8 @@ import { cleanResource, validateInstanceFromRawValue, determineKnownSlices, - setImpliedPropertiesOnInstance + setImpliedPropertiesOnInstance, + splitOnPathPeriods } from '../fhirtypes/common'; import { isUri } from 'valid-url'; import { flatMap, partition, xor } from 'lodash'; @@ -72,27 +73,50 @@ export class ValueSetExporter { if (component.from.system) { const systemParts = component.from.system.split('|'); const csMetadata = this.fisher.fishForMetadata(component.from.system, Type.CodeSystem); - const foundSystem = component.from.system - .replace(/^([^|]+)/, csMetadata?.url ?? '$1') - .split('|'); - composeElement.system = foundSystem[0]; + // 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; + let systemIsInlineInstance = false; + let systemId: string; + if (csMetadata) { + composeElement.system = csMetadata.url ?? systemParts[0]; + isContainedSystem = valueSet.contained?.some((resource: any) => { + return resource?.id === csMetadata.id && resource.resourceType === 'CodeSystem'; + }); + systemIsInlineInstance = csMetadata.instanceUsage === 'Inline'; + systemId = csMetadata.id; + } 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?.resourceType === 'CodeSystem' + ); + }); + if (directSystem) { + isContainedSystem = true; + composeElement.system = directSystem.url; + systemId = directSystem.id; + } else { + isContainedSystem = false; + composeElement.system = systemParts[0]; + } + } // if the code system is also a contained resource, add the valueset-system extension // this zulip thread contains a discussion of the issue and an example using this extension: // https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Contained.20code.20system.20in.20the.20value.20set/near/424938537 // additionally, if it's not a contained resource, and the system we found is an inline instance, that's a problem - const containedSystem = valueSet.contained?.find((resource: any) => { - return resource?.id === csMetadata?.id && resource.resourceType === 'CodeSystem'; - }); - if (containedSystem != null) { + if (isContainedSystem) { composeElement._system = { extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/valueset-system', - valueCanonical: `#${csMetadata.id}` + valueCanonical: `#${systemId}` } ] }; - } else if (csMetadata?.instanceUsage === 'Inline') { + } else if (systemIsInlineInstance) { logger.error( `Can not reference CodeSystem ${component.from.system}: this CodeSystem is an inline instance, but it is not present in the list of contained resources.`, component.sourceInfo @@ -250,6 +274,17 @@ export class ValueSetExporter { const ruleMap: Map = new Map(); const valueSetSD = valueSet.getOwnStructureDefinition(this.fisher); + const inlineResourcePaths: { caretPath: string; instanceOf: string }[] = []; + // first, collect the information we can from rules that set a resourceType + // if instances are directly assigned, we'll get information from them upon validation + rules.forEach((r: CaretValueRule) => { + if (r.caretPath.endsWith('.resourceType') && typeof r.value === 'string' && !r.isInstance) { + inlineResourcePaths.push({ + caretPath: splitOnPathPeriods(r.caretPath).slice(0, -1).join('.'), + instanceOf: r.value + }); + } + }); const rulesWithInstances = rules .filter(rule => rule instanceof CaretValueRule) .map(rule => { @@ -263,16 +298,37 @@ export class ValueSetExporter { ); return null; } + if (instance._instanceMeta.usage === 'Example') { + logger.warn( + `Contained instance "${rule.value}" is an example and probably should not be included in a conformance resource.`, + rule.sourceInfo + ); + } rule.value = instance; + inlineResourcePaths.push({ + caretPath: rule.caretPath, + instanceOf: instance.resourceType + }); } + const matchingInlineResourcePaths = inlineResourcePaths.filter(i => { + return ( + rule.caretPath.startsWith(`${i.caretPath}.`) && + rule.caretPath !== `${i.caretPath}.resourceType` + ); + }); + const inlineResourceTypes: string[] = []; + matchingInlineResourcePaths.forEach(match => { + inlineResourceTypes[splitOnPathPeriods(match.caretPath).length - 1] = match.instanceOf; + }); try { const { pathParts } = valueSetSD.validateValueAtPath( rule.caretPath, rule.value, - this.fisher + this.fisher, + inlineResourceTypes ); ruleMap.set(assembleFSHPath(pathParts).replace(/\[0+\]/g, ''), { pathParts }); - return rule; + return { rule, inlineResourceTypes }; } catch (originalErr) { // if an Instance has an id that looks like a number, bigint, or boolean, // we may have tried to assign that value instead of an Instance. @@ -288,11 +344,24 @@ export class ValueSetExporter { rule, instanceExporter, this.fisher, - originalErr + originalErr, + inlineResourceTypes ); + if (instance?._instanceMeta.usage === 'Example') { + logger.warn( + `Contained instance "${rule.rawValue}" is an example and probably should not be included in a conformance resource.`, + rule.sourceInfo + ); + } rule.value = instance; + if (instance != null) { + inlineResourcePaths.push({ + caretPath: rule.caretPath, + instanceOf: instance.resourceType + }); + } ruleMap.set(assembleFSHPath(pathParts).replace(/\[0+\]/g, ''), { pathParts }); - return rule; + return { rule, inlineResourceTypes }; } else { logger.error(originalErr.message, rule.sourceInfo); if (originalErr.stack) { @@ -308,14 +377,20 @@ export class ValueSetExporter { valueSet, valueSetSD, [...ruleMap.keys()], - [], + inlineResourcePaths.map(i => i.caretPath), this.fisher, knownSlices ); - for (const rule of rulesWithInstances) { + for (const { rule, inlineResourceTypes } of rulesWithInstances) { try { - setPropertyOnDefinitionInstance(valueSet, rule.caretPath, rule.value, this.fisher); + setPropertyOnDefinitionInstance( + valueSet, + rule.caretPath, + rule.value, + this.fisher, + inlineResourceTypes + ); } catch (err) { logger.error(err.message, rule.sourceInfo); if (err.stack) { diff --git a/src/fhirtypes/StructureDefinition.ts b/src/fhirtypes/StructureDefinition.ts index d87303aba..85deebe37 100644 --- a/src/fhirtypes/StructureDefinition.ts +++ b/src/fhirtypes/StructureDefinition.ts @@ -411,14 +411,19 @@ export class StructureDefinition { * @param {any} value - The value to assign * @param {Fishable} fisher - A fishable implementation for finding definitions and metadata */ - setInstancePropertyByPath(path: string, value: any, fisher: Fishable): void { + setInstancePropertyByPath( + path: string, + value: any, + fisher: Fishable, + inlineResourceTypes: string[] = [] + ): void { if (path.startsWith('snapshot') || path.startsWith('differential')) { throw new InvalidElementAccessError(path); } if (path === 'type' && value !== this.type) { throw new InvalidTypeAccessError(); } - setPropertyOnDefinitionInstance(this, path, value, fisher); + setPropertyOnDefinitionInstance(this, path, value, fisher, inlineResourceTypes); } /** diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts index 84c6c9044..9ecae8088 100644 --- a/src/fhirtypes/common.ts +++ b/src/fhirtypes/common.ts @@ -88,10 +88,16 @@ export function setPropertyOnDefinitionInstance( instance: StructureDefinition | ElementDefinition | CodeSystem | ValueSet, path: string, value: any, - fisher: Fishable + fisher: Fishable, + inlineResourceTypes: string[] = [] ): void { const instanceSD = instance.getOwnStructureDefinition(fisher); - const { assignedValue, pathParts } = instanceSD.validateValueAtPath(path, value, fisher); + const { assignedValue, pathParts } = instanceSD.validateValueAtPath( + path, + value, + fisher, + inlineResourceTypes + ); if (instance instanceof ElementDefinition) { instance.clearOriginalProperty(pathParts); } @@ -1471,7 +1477,8 @@ export function validateInstanceFromRawValue( rule: CaretValueRule, instanceExporter: InstanceExporter, fisher: Fishable, - originalErr: MismatchedTypeError + originalErr: MismatchedTypeError, + inlineResourceTypes: string[] = [] ): { instance: InstanceDefinition; pathParts: PathPart[] } { const instance = instanceExporter.fishForFHIR(rule.rawValue); if (instance == null) { @@ -1483,7 +1490,12 @@ export function validateInstanceFromRawValue( try { const targetSD = target.getOwnStructureDefinition(fisher); const path = rule.path.length > 1 ? `${rule.path}.${rule.caretPath}` : rule.caretPath; - const { pathParts } = targetSD.validateValueAtPath(path, instance, fisher); + const { pathParts } = targetSD.validateValueAtPath( + path, + instance, + fisher, + inlineResourceTypes + ); return { instance, pathParts }; } catch (instanceErr) { if (instanceErr instanceof MismatchedTypeError) { diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts index d1e5b9c24..dee93fa86 100644 --- a/test/export/CodeSystemExporter.test.ts +++ b/test/export/CodeSystemExporter.test.ts @@ -759,6 +759,295 @@ describe('CodeSystemExporter', () => { }); }); + it('should apply CaretValueRules that create a contained resource', () => { + // CodeSystem: CaretCodeSystem + // * #someCode "Some Code" + // * ^contained.resourceType = "Observation" + // * ^contained.id = "my-observation" + // * ^contained.status = #draft + // * ^contained.code = #123 + // * ^contained.valueString = "contained observation" + const codeSystem = new FshCodeSystem('CaretCodeSystem'); + const someCode = new ConceptRule('someCode', 'Some Code'); + const containedResourceType = new CaretValueRule(''); + containedResourceType.caretPath = 'contained.resourceType'; + containedResourceType.value = 'Observation'; + const containedId = new CaretValueRule(''); + containedId.caretPath = 'contained.id'; + containedId.value = 'my-observation'; + const containedStatus = new CaretValueRule(''); + containedStatus.caretPath = 'contained.status'; + containedStatus.value = new FshCode('draft'); + const containedCode = new CaretValueRule(''); + containedCode.caretPath = 'contained.code'; + containedCode.value = new FshCode('123'); + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + codeSystem.rules.push( + someCode, + containedResourceType, + containedId, + containedStatus, + containedCode, + containedValue + ); + doc.codeSystems.set(codeSystem.name, codeSystem); + + const exported = exporter.export().codeSystems; + expect(exported.length).toBe(1); + expect(exported[0]).toEqual({ + resourceType: 'CodeSystem', + id: 'CaretCodeSystem', + name: 'CaretCodeSystem', + content: 'complete', + url: 'http://hl7.org/fhir/us/minimal/CodeSystem/CaretCodeSystem', + count: 1, + status: 'draft', + contained: [ + { + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation' + } + ], + concept: [ + { + code: 'someCode', + display: 'Some Code' + } + ] + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should apply CaretValueRules that modify a contained resource', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #inline + // * id = "my-observation" + // * status = #draft + // * code = #123 + const instance = new Instance('MyObservation'); + instance.instanceOf = 'Observation'; + instance.usage = 'Inline'; + const instanceId = new AssignmentRule('id'); + instanceId.value = 'my-observation'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceId, instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // CodeSystem: CaretCodeSystem + // * #someCode "Some Code" + // * ^contained = my-observation + // * ^contained.valueString = "contained observation" + const codeSystem = new FshCodeSystem('CaretCodeSystem'); + const someCode = new ConceptRule('someCode', 'Some Code'); + const containedInstance = new CaretValueRule(''); + containedInstance.caretPath = 'contained'; + containedInstance.value = 'my-observation'; + containedInstance.isInstance = true; + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + codeSystem.rules.push(someCode, containedInstance, containedValue); + doc.codeSystems.set(codeSystem.name, codeSystem); + + const exported = exporter.export().codeSystems; + expect(exported.length).toBe(1); + expect(exported[0]).toEqual({ + resourceType: 'CodeSystem', + id: 'CaretCodeSystem', + name: 'CaretCodeSystem', + content: 'complete', + url: 'http://hl7.org/fhir/us/minimal/CodeSystem/CaretCodeSystem', + count: 1, + status: 'draft', + contained: [ + { + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation' + } + ], + concept: [ + { + code: 'someCode', + display: 'Some Code' + } + ] + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should log a warning when applying a CaretValueRule that assigns an example Instance', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #example + // * id = "my-observation" + // * status = #draft + // * code = #123 + const instance = new Instance('MyObservation'); + instance.instanceOf = 'Observation'; + instance.usage = 'Example'; + const instanceId = new AssignmentRule('id'); + instanceId.value = 'my-observation'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceId, instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // CodeSystem: CaretCodeSystem + // * #someCode "Some Code" + // * ^contained = my-observation + // * ^contained.valueString = "contained observation" + const codeSystem = new FshCodeSystem('CaretCodeSystem'); + const someCode = new ConceptRule('someCode', 'Some Code'); + const containedInstance = new CaretValueRule('') + .withFile('CodeSystem.fsh') + .withLocation([3, 3, 3, 24]); + containedInstance.caretPath = 'contained'; + containedInstance.value = 'my-observation'; + containedInstance.isInstance = true; + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + codeSystem.rules.push(someCode, containedInstance, containedValue); + doc.codeSystems.set(codeSystem.name, codeSystem); + + const exported = exporter.export().codeSystems; + expect(exported.length).toBe(1); + expect(exported[0]).toEqual({ + resourceType: 'CodeSystem', + id: 'CaretCodeSystem', + name: 'CaretCodeSystem', + content: 'complete', + url: 'http://hl7.org/fhir/us/minimal/CodeSystem/CaretCodeSystem', + count: 1, + status: 'draft', + contained: [ + { + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation' + } + ], + concept: [ + { + code: 'someCode', + display: 'Some Code' + } + ] + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + expect(loggerSpy.getLastMessage('warn')).toMatch( + /Contained instance "my-observation" is an example/s + ); + expect(loggerSpy.getLastMessage('warn')).toMatch(/File: CodeSystem\.fsh.*Line: 3\D*/s); + }); + + it('should log a warning when applying a CaretValueRule that assigns an example Instance with a numeric id', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #example + // * id = "555" + // * status = #draft + // * code = #123 + const instance = new Instance('MyObservation'); + instance.instanceOf = 'Observation'; + instance.usage = 'Example'; + const instanceId = new AssignmentRule('id'); + instanceId.value = '555'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceId, instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // CodeSystem: CaretCodeSystem + // * #someCode "Some Code" + // * ^contained = 555 + // * ^contained.valueString = "contained observation" + const codeSystem = new FshCodeSystem('CaretCodeSystem'); + const someCode = new ConceptRule('someCode', 'Some Code'); + const containedInstance = new CaretValueRule('') + .withFile('CodeSystem.fsh') + .withLocation([3, 3, 3, 24]); + containedInstance.caretPath = 'contained'; + containedInstance.value = BigInt(555); + containedInstance.rawValue = '555'; + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + codeSystem.rules.push(someCode, containedInstance, containedValue); + doc.codeSystems.set(codeSystem.name, codeSystem); + + const exported = exporter.export().codeSystems; + expect(exported.length).toBe(1); + expect(exported[0]).toEqual({ + resourceType: 'CodeSystem', + id: 'CaretCodeSystem', + name: 'CaretCodeSystem', + content: 'complete', + url: 'http://hl7.org/fhir/us/minimal/CodeSystem/CaretCodeSystem', + count: 1, + status: 'draft', + contained: [ + { + resourceType: 'Observation', + id: '555', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation' + } + ], + concept: [ + { + code: 'someCode', + display: 'Some Code' + } + ] + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + expect(loggerSpy.getLastMessage('warn')).toMatch(/Contained instance "555" is an example/s); + expect(loggerSpy.getLastMessage('warn')).toMatch(/File: CodeSystem\.fsh.*Line: 3\D*/s); + }); + it('should replace references when applying a CaretValueRule', () => { const codeSystem = new FshCodeSystem('CaretCodeSystem'); const someCode = new ConceptRule('someCode', 'Some Code'); diff --git a/test/export/FHIRExporter.test.ts b/test/export/FHIRExporter.test.ts index 676feee9e..787434741 100644 --- a/test/export/FHIRExporter.test.ts +++ b/test/export/FHIRExporter.test.ts @@ -4,7 +4,7 @@ import { exportFHIR, Package, FHIRExporter } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { minimalConfig } from '../utils/minimalConfig'; -import { FshCodeSystem, FshValueSet, Instance, Profile } from '../../src/fshtypes'; +import { FshCode, FshCodeSystem, FshValueSet, Instance, Profile } from '../../src/fshtypes'; import { AssignmentRule, BindingRule, @@ -180,6 +180,245 @@ describe('FHIRExporter', () => { ]); }); + it('should allow a profile to contain a resource and to apply caret rules within the contained resource', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #inline + // * id = "my-observation" + // * status = #draft + // * code = #123 + const instance = new Instance('MyObservation'); + instance.instanceOf = 'Observation'; + instance.usage = 'Inline'; + const instanceId = new AssignmentRule('id'); + instanceId.value = 'my-observation'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceId, instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // Profile: ContainingProfile + // Parent: Patient + // * ^contained = MyObservation + // * ^contained.valueString = "contained observation" + // * ^contained.category = #exam + const profile = new Profile('ContainingProfile'); + profile.parent = 'Patient'; + const containedInstance = new CaretValueRule(''); + containedInstance.caretPath = 'contained'; + containedInstance.value = 'MyObservation'; + containedInstance.isInstance = true; + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + const containedCategory = new CaretValueRule(''); + containedCategory.caretPath = 'contained.category'; + containedCategory.value = new FshCode('exam'); + profile.rules.push(containedInstance, containedValue, containedCategory); + doc.profiles.set(profile.name, profile); + + const result = exporter.export(); + expect(result.profiles[0].contained).toEqual([ + { + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation', + category: [ + { + coding: [ + { + code: 'exam' + } + ] + } + ] + } + ]); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should log an error when a deferred rule assigns something of the wrong type', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #inline + // * id = "my-observation" + // * status = #draft + // * code = #123 + const instance = new Instance('MyObservation'); + instance.instanceOf = 'Observation'; + instance.usage = 'Inline'; + const instanceId = new AssignmentRule('id'); + instanceId.value = 'my-observation'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceId, instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // Profile: ContainingProfile + // Parent: Patient + // * ^contained = MyObservation + // * ^contained.interpretation = "contained observation" + const profile = new Profile('ContainingProfile'); + profile.parent = 'Patient'; + const containedInstance = new CaretValueRule(''); + containedInstance.caretPath = 'contained'; + containedInstance.value = 'MyObservation'; + containedInstance.isInstance = true; + const containedValue = new CaretValueRule('') + .withFile('Contained.fsh') + .withLocation([15, 3, 15, 33]); + containedValue.caretPath = 'contained.interpretation'; + containedValue.value = 'contained observation'; + profile.rules.push(containedInstance, containedValue); + doc.profiles.set(profile.name, profile); + const result = exporter.export(); + expect(result.profiles[0].contained).toEqual([ + { + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + } + } + ]); + expect(loggerSpy.getLastMessage('error')).toMatch( + 'Cannot assign string value: contained observation. Value does not match element type: CodeableConcept' + ); + expect(loggerSpy.getLastMessage('error')).toMatch(/File: Contained\.fsh.*Line: 15\D*/s); + }); + + it('should not get confused when there are contained resources of different types', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #inline + // * id = "my-observation" + // * status = #draft + // * code = #123 + const observationInstance = new Instance('MyObservation'); + observationInstance.instanceOf = 'Observation'; + observationInstance.usage = 'Inline'; + const observationId = new AssignmentRule('id'); + observationId.value = 'my-observation'; + const observationStatus = new AssignmentRule('status'); + observationStatus.value = new FshCode('draft'); + const observationCode = new AssignmentRule('code'); + observationCode.value = new FshCode('123'); + observationInstance.rules.push(observationId, observationStatus, observationCode); + doc.instances.set(observationInstance.name, observationInstance); + // Instance: MyPatient + // InstanceOf: Patient + // Usage: #inline + // * id = "my-patient" + // * name.given = "Marisa" + const patientInstance = new Instance('MyPatient'); + patientInstance.instanceOf = 'Patient'; + patientInstance.usage = 'Inline'; + const patientId = new AssignmentRule('id'); + patientId.value = 'my-patient'; + const patientName = new AssignmentRule('name.given'); + patientName.value = 'Marisa'; + patientInstance.rules.push(patientId, patientName); + doc.instances.set(patientInstance.name, patientInstance); + // Profile: ContainingProfile + // Parent: Patient + // * ^contained = MyObservation + // * ^contained[1] = MyPatient + // * ^contained.valueString = "contained observation" + // * ^contained[1].name.family = "Kirisame" + const profile = new Profile('ContainingProfile'); + profile.parent = 'Patient'; + const containedObservation = new CaretValueRule(''); + containedObservation.caretPath = 'contained'; + containedObservation.value = 'MyObservation'; + containedObservation.isInstance = true; + const containedPatient = new CaretValueRule(''); + containedPatient.caretPath = 'contained[1]'; + containedPatient.value = 'MyPatient'; + containedPatient.isInstance = true; + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + const containedFamily = new CaretValueRule(''); + containedFamily.caretPath = 'contained[1].name.family'; + containedFamily.value = 'Kirisame'; + profile.rules.push(containedObservation, containedPatient, containedValue, containedFamily); + doc.profiles.set(profile.name, profile); + + const result = exporter.export(); + expect(result.profiles[0].contained).toHaveLength(2); + expect(result.profiles[0].contained[0]).toEqual({ + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { coding: [{ code: '123' }] }, + valueString: 'contained observation' + }); + expect(result.profiles[0].contained[1]).toEqual({ + resourceType: 'Patient', + id: 'my-patient', + name: [{ given: ['Marisa'], family: 'Kirisame' }] + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should allow a profile to contain a profiled resource and to apply a caret rule within the contained resource', () => { + // Instance: some-patient + // InstanceOf: gendered-patient + // Usage: #inline + // * gender = #unknown + const instance = new Instance('some-patient'); + instance.instanceOf = 'gendered-patient'; + instance.usage = 'Inline'; + const instanceGender = new AssignmentRule('gender'); + instanceGender.value = new FshCode('unknown'); + instance.rules.push(instanceGender); + doc.instances.set(instance.name, instance); + // Profile: ContainingProfile + // Parent: Patient + // * ^contained = some-patient + // * ^contained.name.given = "mint" + const profile = new Profile('ContainingProfile'); + profile.parent = 'Patient'; + const containedInstance = new CaretValueRule(''); + containedInstance.caretPath = 'contained'; + containedInstance.value = 'some-patient'; + containedInstance.isInstance = true; + const containedName = new CaretValueRule(''); + containedName.caretPath = 'contained.name.given'; + containedName.value = 'mint'; + profile.rules.push(containedInstance, containedName); + doc.profiles.set(profile.name, profile); + + const result = exporter.export(); + expect(result.profiles[0].contained).toEqual([ + { + resourceType: 'Patient', + meta: { + profile: ['http://example.org/impose/StructureDefinition/gendered-patient'] + }, + id: 'some-patient', + gender: 'unknown', + name: [{ given: ['mint'] }] + } + ]); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + it('should allow a profile to bind an element to a contained ValueSet using a relative reference', () => { const valueSet = new FshValueSet('MyValueSet'); valueSet.id = 'my-value-set'; @@ -512,6 +751,48 @@ describe('FHIRExporter', () => { }); }); + it('should let a profile assign and modify an Inline instance that is not a resource', () => { + // Profile: MyObservation + // Parent: Observation + // ^contact = MyContact + // ^contact.telecom.value = "bearington@bear.zoo" + const profile = new Profile('MyObservation'); + profile.parent = 'Observation'; + const contactRule = new CaretValueRule(''); + contactRule.caretPath = 'contact'; + contactRule.value = 'MyContact'; + contactRule.isInstance = true; + const telecomRule = new CaretValueRule(''); + telecomRule.caretPath = 'contact.telecom.value'; + telecomRule.value = 'bearington@bear.zoo'; + profile.rules.push(contactRule, telecomRule); + doc.profiles.set(profile.name, profile); + // Instance: MyContact + // InstanceOf: ContactDetail + // Usage: #inline + // name = "Bearington" + const instance = new Instance('MyContact'); + instance.instanceOf = 'ContactDetail'; + instance.usage = 'Inline'; + const contactName = new AssignmentRule('name'); + contactName.value = 'Bearington'; + instance.rules.push(contactName); + doc.instances.set(instance.name, instance); + + const result = exporter.export(); + + expect(result.profiles.length).toBe(1); + expect(result.profiles[0].contact.length).toBe(1); + expect(result.profiles[0].contact[0]).toEqual({ + name: 'Bearington', + telecom: [ + { + value: 'bearington@bear.zoo' + } + ] + }); + }); + it('should export a value set that includes a component from a contained FSH code system and add the valueset-system extension', () => { // CodeSystem: FoodCS // Id: food diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts index 2ff78d280..1bac89e6f 100644 --- a/test/export/InstanceExporter.test.ts +++ b/test/export/InstanceExporter.test.ts @@ -10496,27 +10496,36 @@ describe('InstanceExporter', () => { describe('#Inline Instances', () => { beforeEach(() => { - const inlineInstance = new Instance('MyInlinePatient'); - inlineInstance.instanceOf = 'Patient'; + const inlinePatient = new Instance('MyInlinePatient'); + inlinePatient.instanceOf = 'Patient'; + inlinePatient.usage = 'Inline'; + const patientId = new AssignmentRule('id'); + patientId.value = 'MyInlinePatient'; const assignedValRule = new AssignmentRule('active'); assignedValRule.value = true; - inlineInstance.rules.push(assignedValRule); + inlinePatient.rules.push(patientId, assignedValRule); // * active = true - doc.instances.set(inlineInstance.name, inlineInstance); + doc.instances.set(inlinePatient.name, inlinePatient); const inlineObservation = new Instance('MyInlineObservation'); inlineObservation.instanceOf = 'Observation'; + inlineObservation.usage = 'Inline'; + const observationId = new AssignmentRule('id'); + observationId.value = 'MyInlineObservation'; const observationValueRule = new AssignmentRule('valueString'); observationValueRule.value = 'Some Observation'; - inlineObservation.rules.push(observationValueRule); + inlineObservation.rules.push(observationId, observationValueRule); // * valueString = "Some Observation" doc.instances.set(inlineObservation.name, inlineObservation); const inlineOrganization = new Instance('MyInlineOrganization'); inlineOrganization.instanceOf = 'Organization'; + inlineOrganization.usage = 'Inline'; + const organizationId = new AssignmentRule('id'); + organizationId.value = 'MyInlineOrganization'; const organizationName = new AssignmentRule('name'); organizationName.value = 'Everyone'; - inlineOrganization.rules.push(organizationName); + inlineOrganization.rules.push(organizationId, organizationName); // * name = "Everyone" doc.instances.set(inlineOrganization.name, inlineOrganization); @@ -11002,6 +11011,92 @@ describe('InstanceExporter', () => { expect(exported.telecom).toEqual([{ value: 'Nine Nine E One' }]); }); + it('should log a warning and assign an example instance within a definition instance', () => { + // Instance: my-observation + // InstanceOf: Observation + // Usage: #example + // * status = #draft + // * code = #123 + const numericObservation = new Instance('my-observation'); + numericObservation.instanceOf = 'Observation'; + numericObservation.usage = 'Example'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + numericObservation.rules.push(instanceStatus, instanceCode); + doc.instances.set(numericObservation.name, numericObservation); + + const contained = new AssignmentRule('contained') + .withFile('Patient.fsh') + .withLocation([6, 3, 6, 18]); + contained.value = 'my-observation'; + contained.isInstance = true; + patientInstance.rules.push(contained); + patientInstance.usage = 'Definition'; + + const exported = exportInstance(patientInstance); + expect(exported.contained[0]).toEqual({ + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + } + }); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + expect(loggerSpy.getLastMessage('warn')).toMatch( + /Contained instance "my-observation" is an example/s + ); + expect(loggerSpy.getLastMessage('warn')).toMatch(/File: Patient\.fsh.*Line: 6\D*/s); + }); + + it('should log a warning and assign an example instance with a numeric id within a definition instance', () => { + // Instance: 765 + // InstanceOf: Observation + // Usage: #example + // * status = #draft + // * code = #123 + const numericObservation = new Instance('765'); + numericObservation.instanceOf = 'Observation'; + numericObservation.usage = 'Example'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + numericObservation.rules.push(instanceStatus, instanceCode); + doc.instances.set(numericObservation.name, numericObservation); + + const contained = new AssignmentRule('contained') + .withFile('Patient.fsh') + .withLocation([5, 3, 5, 18]); + contained.value = BigInt(765); + contained.rawValue = '765'; + patientInstance.rules.push(contained); + patientInstance.usage = 'Definition'; + + const exported = exportInstance(patientInstance); + expect(exported.contained[0]).toEqual({ + resourceType: 'Observation', + id: '765', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + } + }); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + expect(loggerSpy.getLastMessage('warn')).toMatch(/Contained instance "765" is an example/s); + expect(loggerSpy.getLastMessage('warn')).toMatch(/File: Patient\.fsh.*Line: 5\D*/s); + }); + it('should assign an inline instance with an id that resembles a boolean', () => { // Instance: false // InstanceOf: ContactPoint diff --git a/test/export/StructureDefinition.ProfileExporter.test.ts b/test/export/StructureDefinition.ProfileExporter.test.ts index ea410ede8..e806b7f09 100644 --- a/test/export/StructureDefinition.ProfileExporter.test.ts +++ b/test/export/StructureDefinition.ProfileExporter.test.ts @@ -293,7 +293,10 @@ describe('ProfileExporter', () => { expect(exported[0].contained).toBeUndefined(); expect(exporter.deferredCaretRules.size).toBe(1); expect(exporter.deferredCaretRules.get(exported[0]).length).toBe(1); - expect(exporter.deferredCaretRules.get(exported[0])).toContainEqual({ rule: caretValueRule }); + expect(exporter.deferredCaretRules.get(exported[0])).toContainEqual({ + rule: caretValueRule, + tryFish: true + }); }); it('should defer adding an instance with a numeric id to a profile as a contained resource', () => { @@ -317,7 +320,8 @@ describe('ProfileExporter', () => { expect(exporter.deferredCaretRules.get(exported[0]).length).toBe(1); expect(exporter.deferredCaretRules.get(exported[0])).toContainEqual({ rule: caretValueRule, - originalErr: expect.any(MismatchedTypeError) + originalErr: expect.any(MismatchedTypeError), + tryFish: true }); }); @@ -342,7 +346,8 @@ describe('ProfileExporter', () => { expect(exporter.deferredCaretRules.get(exported[0]).length).toBe(1); expect(exporter.deferredCaretRules.get(exported[0])).toContainEqual({ rule: caretValueRule, - originalErr: expect.any(MismatchedTypeError) + originalErr: expect.any(MismatchedTypeError), + tryFish: true }); }); @@ -371,7 +376,10 @@ describe('ProfileExporter', () => { expect(codeElement.binding.valueSet).not.toBe('#MyValueSet'); expect(exporter.deferredCaretRules.size).toBe(1); expect(exporter.deferredCaretRules.get(exported[0]).length).toBe(1); - expect(exporter.deferredCaretRules.get(exported[0])).toContainEqual({ rule: caretValueRule }); + expect(exporter.deferredCaretRules.get(exported[0])).toContainEqual({ + rule: caretValueRule, + tryFish: true + }); expect(exporter.knownBindingRules.size).toBe(1); expect(exporter.knownBindingRules.get(exported[0]).length).toBe(1); expect(exporter.knownBindingRules.get(exported[0])).toContainEqual({ @@ -380,6 +388,100 @@ describe('ProfileExporter', () => { }); }); + it('should allow a contained resource with a resourceType to be built from several caret rules', () => { + // Profile: ContainingProfile + // Parent: Patient + // * ^contained.resourceType = "Observation" + // * ^contained.id = "my-observation" + // * ^contained.status = #draft + // * ^contained.code = #123 + // * ^contained.valueString = "contained observation" + const profile = new Profile('ContainingProfile'); + profile.parent = 'Patient'; + const containedResourceType = new CaretValueRule(''); + containedResourceType.caretPath = 'contained.resourceType'; + containedResourceType.value = 'Observation'; + const containedId = new CaretValueRule(''); + containedId.caretPath = 'contained.id'; + containedId.value = 'my-observation'; + const containedStatus = new CaretValueRule(''); + containedStatus.caretPath = 'contained.status'; + containedStatus.value = new FshCode('draft'); + const containedCode = new CaretValueRule(''); + containedCode.caretPath = 'contained.code'; + containedCode.value = new FshCode('123'); + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + profile.rules.push( + containedResourceType, + containedId, + containedStatus, + containedCode, + containedValue + ); + doc.profiles.set(profile.name, profile); + + const exported = exporter.export().profiles; + expect(exported.length).toBe(1); + expect(exported[0].contained[0]).toEqual({ + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation' + }); + }); + + it('should defer applying a caret rule that would be applied within a contained instance', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #inline + // * id = "my-observation" + // * status = #draft + // * code = #123 + const instance = new Instance('MyObservation'); + instance.instanceOf = 'Observation'; + instance.usage = 'Inline'; + doc.instances.set(instance.name, instance); + // Profile: ContainingProfile + // Parent: Patient + // * ^contained = MyObservation + // * ^contained.valueString = "contained observation" + const profile = new Profile('ContainingProfile'); + profile.parent = 'Patient'; + const containedInstance = new CaretValueRule(''); + containedInstance.caretPath = 'contained'; + containedInstance.value = 'MyObservation'; + containedInstance.isInstance = true; + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + profile.rules.push(containedInstance, containedValue); + doc.profiles.set(profile.name, profile); + + const exported = exporter.export().profiles; + expect(exported.length).toBe(1); + expect(exported[0].contained).toBeUndefined(); + expect(exporter.deferredCaretRules.size).toBe(1); + expect(exporter.deferredCaretRules.get(exported[0])).toEqual([ + { + rule: containedInstance, + tryFish: true + }, + { + rule: containedValue, + tryFish: false + } + ]); + }); + it('should NOT export a profile of an R5 resource in an R4 project', () => { // Although instances of ActorDefinition are allowed in R4, profiles of ActorDefinition are not! const adProfile = new Profile('ADProfile'); diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts index e66042e87..5f9e12315 100644 --- a/test/export/StructureDefinitionExporter.test.ts +++ b/test/export/StructureDefinitionExporter.test.ts @@ -6213,6 +6213,57 @@ describe('StructureDefinitionExporter R4', () => { expect(loggerSpy.getAllMessages('error')).toHaveLength(0); }); + it('should log a warning and apply an instance AssignmentRule and replace the instance when the instance is an example', () => { + // Instance: example-obs + // InstanceOf: Observation + // Usage: #example + // * status = #draft + // * code = #123 + const instance = new Instance('example-obs'); + instance.instanceOf = 'Observation'; + instance.usage = 'Example'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // Profile: MyObs + // Parent: Observation + // * contained = example-obs + const profile = new Profile('MyObs'); + profile.parent = 'Observation'; + const profileContained = new AssignmentRule('contained') + .withFile('MyObs.fsh') + .withLocation([5, 3, 5, 18]); + profileContained.value = 'example-obs'; + profileContained.isInstance = true; + profile.rules.push(profileContained); + + exporter.exportStructDef(profile); + const sd = pkg.profiles[0]; + const assignedResource = sd.findElement('Observation.contained'); + // @ts-ignore + expect(assignedResource.patternObservation).toEqual({ + resourceType: 'Observation', + id: 'example-obs', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + expect(loggerSpy.getLastMessage('warn')).toMatch( + /Contained instance "example-obs" is an example/s + ); + expect(loggerSpy.getLastMessage('warn')).toMatch(/File: MyObs\.fsh.*Line: 5\D*/s); + }); + it('should apply an instance AssignmentRule when the instance has a numeric id', () => { // Profile: USPatient // Parent: Patient @@ -6243,6 +6294,55 @@ describe('StructureDefinitionExporter R4', () => { expect(loggerSpy.getAllMessages('error')).toHaveLength(0); }); + it('should log a warning and apply an instance AssignmentRule when the instance has a numeric id', () => { + // Instance: 765 + // InstanceOf: Observation + // Usage: #example + // * status = #draft + // * code = #123 + const instance = new Instance('765'); + instance.instanceOf = 'Observation'; + instance.usage = 'Example'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // Profile: MyObs + // Parent: Observation + // * contained = 765 + const profile = new Profile('MyObs'); + profile.parent = 'Observation'; + const profileContained = new AssignmentRule('contained') + .withFile('MyObs.fsh') + .withLocation([5, 3, 5, 18]); + profileContained.value = BigInt(765); + profileContained.rawValue = '765'; + profile.rules.push(profileContained); + + exporter.exportStructDef(profile); + const sd = pkg.profiles[0]; + const assignedResource = sd.findElement('Observation.contained'); + // @ts-ignore + expect(assignedResource.patternObservation).toEqual({ + resourceType: 'Observation', + id: '765', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + expect(loggerSpy.getLastMessage('warn')).toMatch(/Contained instance "765" is an example/s); + expect(loggerSpy.getLastMessage('warn')).toMatch(/File: MyObs\.fsh.*Line: 5\D*/s); + }); + it('should apply an instance AssignmentRule when the instance has an id that resembles a boolean', () => { // Profile: USPatient // Parent: Patient diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts index 34c348c2d..8f640cb04 100644 --- a/test/export/ValueSetExporter.test.ts +++ b/test/export/ValueSetExporter.test.ts @@ -512,7 +512,7 @@ describe('ValueSetExporter', () => { expect(loggerSpy.getLastMessage('error')).toMatch(/File: ExampleVS\.fsh.*Line: 5\D*/s); }); - it('should log an error and not export the value set when attempting to reference a contained example instance of code system', () => { + it('should log a warning and export the value set when containing an example instance of code system', () => { // Instance: example-codesystem // InstanceOf: CodeSystem // Usage: #example @@ -538,11 +538,12 @@ describe('ValueSetExporter', () => { // Id: example-valueset // * ^contained = example-codesystem // * include codes from system example-codesystem - const valueSet = new FshValueSet('ExampleValueset') - .withFile('ExampleVS.fsh') - .withLocation([2, 3, 7, 48]); + const valueSet = new FshValueSet('ExampleValueset'); + valueSet.id = 'example-valueset'; - const containedSystem = new CaretValueRule(''); + const containedSystem = new CaretValueRule('') + .withFile('ExampleVS.fsh') + .withLocation([3, 3, 3, 48]); containedSystem.caretPath = 'contained'; containedSystem.value = 'example-codesystem'; containedSystem.isInstance = true; @@ -553,12 +554,45 @@ describe('ValueSetExporter', () => { doc.valueSets.set(valueSet.name, valueSet); const exported = exporter.export().valueSets; - expect(exported.length).toBe(0); - expect(loggerSpy.getAllMessages('error')).toHaveLength(1); - expect(loggerSpy.getLastMessage('error')).toMatch( - /Resolved value "example-codesystem" is not a valid URI/s + expect(exported.length).toBe(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'ExampleValueset', + id: 'example-valueset', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/example-valueset', + contained: [ + { + resourceType: 'CodeSystem', + id: 'example-codesystem', + url: 'http://example.org/codesystem', + version: '1.0.0', + status: 'active', + content: 'complete' + } + ], + compose: { + include: [ + { + system: 'http://example.org/codesystem', + _system: { + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/valueset-system', + valueCanonical: '#example-codesystem' + } + ] + } + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + expect(loggerSpy.getLastMessage('warn')).toMatch( + /Contained instance "example-codesystem" is an example/s ); - expect(loggerSpy.getLastMessage('error')).toMatch(/File: ExampleVS\.fsh.*Line: 2 - 7\D*/s); + expect(loggerSpy.getLastMessage('warn')).toMatch(/File: ExampleVS\.fsh.*Line: 3\D*/s); }); it('should export a value set that includes a component from a value set', () => { @@ -723,6 +757,513 @@ describe('ValueSetExporter', () => { }); }); + it('should export a value set with a contained resource created on the value set', () => { + // ValueSet: DinnerVS + // * ^contained.resourceType = "Observation" + // * ^contained.id = "my-observation" + // * ^contained.status = #draft + // * ^contained.code = #123 + // * ^contained.valueString = "contained observation" + // * include codes from system http://food.org/food + const valueSet = new FshValueSet('DinnerVS'); + const containedResourceType = new CaretValueRule(''); + containedResourceType.caretPath = 'contained.resourceType'; + containedResourceType.value = 'Observation'; + const containedId = new CaretValueRule(''); + containedId.caretPath = 'contained.id'; + containedId.value = 'my-observation'; + const containedStatus = new CaretValueRule(''); + containedStatus.caretPath = 'contained.status'; + containedStatus.value = new FshCode('draft'); + const containedCode = new CaretValueRule(''); + containedCode.caretPath = 'contained.code'; + containedCode.value = new FshCode('123'); + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + const foodCodes = new ValueSetConceptComponentRule(true); + foodCodes.from = { system: 'http://food.org/food' }; + valueSet.rules.push( + containedResourceType, + containedId, + containedStatus, + containedCode, + containedValue, + foodCodes + ); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported).toHaveLength(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'DinnerVS', + id: 'DinnerVS', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/DinnerVS', + contained: [ + { + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation' + } + ], + compose: { + include: [{ system: 'http://food.org/food' }] + } + }); + }); + + it('should export a value set with a contained resource modified on the value set', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #inline + // * id = "my-observation" + // * status = #draft + // * code = #123 + const instance = new Instance('MyObservation'); + instance.instanceOf = 'Observation'; + instance.usage = 'Inline'; + const instanceId = new AssignmentRule('id'); + instanceId.value = 'my-observation'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceId, instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // ValueSet: DinnerVS + // * ^contained = MyObservation + // * ^contained.valueString = "contained observation" + // * include codes from system http://food.org/food + const valueSet = new FshValueSet('DinnerVS'); + const containedInstance = new CaretValueRule(''); + containedInstance.caretPath = 'contained'; + containedInstance.value = 'MyObservation'; + containedInstance.isInstance = true; + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + const foodCodes = new ValueSetConceptComponentRule(true); + foodCodes.from = { system: 'http://food.org/food' }; + valueSet.rules.push(containedInstance, containedValue, foodCodes); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported).toHaveLength(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'DinnerVS', + id: 'DinnerVS', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/DinnerVS', + contained: [ + { + resourceType: 'Observation', + id: 'my-observation', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation' + } + ], + compose: { + include: [{ system: 'http://food.org/food' }] + } + }); + }); + + it('should log a warning and export a value set with a contained example resource with a numeric id modified on the value set', () => { + // Instance: MyObservation + // InstanceOf: Observation + // Usage: #example + // * id = "555" + // * status = #draft + // * code = #123 + const instance = new Instance('MyObservation'); + instance.instanceOf = 'Observation'; + instance.usage = 'Example'; + const instanceId = new AssignmentRule('id'); + instanceId.value = '555'; + const instanceStatus = new AssignmentRule('status'); + instanceStatus.value = new FshCode('draft'); + const instanceCode = new AssignmentRule('code'); + instanceCode.value = new FshCode('123'); + instance.rules.push(instanceId, instanceStatus, instanceCode); + doc.instances.set(instance.name, instance); + // ValueSet: DinnerVS + // * ^contained = 555 + // * ^contained.valueString = "contained observation" + // * include codes from system http://food.org/food + const valueSet = new FshValueSet('DinnerVS'); + const containedInstance = new CaretValueRule('') + .withFile('ValueSet.fsh') + .withLocation([2, 3, 2, 24]); + containedInstance.caretPath = 'contained'; + containedInstance.value = BigInt(555); + containedInstance.rawValue = '555'; + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + const foodCodes = new ValueSetConceptComponentRule(true); + foodCodes.from = { system: 'http://food.org/food' }; + valueSet.rules.push(containedInstance, containedValue, foodCodes); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported).toHaveLength(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'DinnerVS', + id: 'DinnerVS', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/DinnerVS', + contained: [ + { + resourceType: 'Observation', + id: '555', + status: 'draft', + code: { + coding: [ + { + code: '123' + } + ] + }, + valueString: 'contained observation' + } + ], + compose: { + include: [{ system: 'http://food.org/food' }] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + expect(loggerSpy.getLastMessage('warn')).toMatch(/Contained instance "555" is an example/s); + expect(loggerSpy.getLastMessage('warn')).toMatch(/File: ValueSet\.fsh.*Line: 2\D*/s); + }); + + it('should export a value set that includes a component from a contained code system created on the value set and referenced by id', () => { + // ValueSet: ExampleValueset + // Id: example-valueset + // * ^contained.resourceType = "CodeSystem" + // * ^contained.id = "example-codesystem" + // * ^contained.name = "ExampleCodesystem" + // * ^contained.url = "http://example.org/codesystem" + // * ^contained.content = #complete + // * ^contained.concept[0].code = #example-code-1 + // * ^contained.concept[0].display = "Example Code 1" + // * include codes from system example-codesystem + const valueSet = new FshValueSet('ExampleValueset'); + valueSet.id = 'example-valueset'; + const containedResourceType = new CaretValueRule(''); + containedResourceType.caretPath = 'contained.resourceType'; + containedResourceType.value = 'CodeSystem'; + const containedId = new CaretValueRule(''); + containedId.caretPath = 'contained.id'; + containedId.value = 'example-codesystem'; + const containedName = new CaretValueRule(''); + containedName.caretPath = 'contained.name'; + containedName.value = 'ExampleCodesystem'; + const containedUrl = new CaretValueRule(''); + containedUrl.caretPath = 'contained.url'; + containedUrl.value = 'http://example.org/codesystem'; + const containedContent = new CaretValueRule(''); + containedContent.caretPath = 'contained.content'; + containedContent.value = new FshCode('complete'); + const containedCode = new CaretValueRule(''); + containedCode.caretPath = 'contained.concept[0].code'; + containedCode.value = new FshCode('example-code-1'); + const containedDisplay = new CaretValueRule(''); + containedDisplay.caretPath = 'contained.concept[0].display'; + containedDisplay.value = 'Example Code 1'; + const includeCodes = new ValueSetConceptComponentRule(true); + includeCodes.from = { system: 'example-codesystem' }; + valueSet.rules.push( + containedResourceType, + containedId, + containedName, + containedUrl, + containedContent, + containedCode, + containedDisplay, + includeCodes + ); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported).toHaveLength(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'ExampleValueset', + id: 'example-valueset', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/example-valueset', + contained: [ + { + resourceType: 'CodeSystem', + id: 'example-codesystem', + name: 'ExampleCodesystem', + url: 'http://example.org/codesystem', + content: 'complete', + concept: [{ code: 'example-code-1', display: 'Example Code 1' }] + } + ], + compose: { + include: [ + { + system: 'http://example.org/codesystem', + _system: { + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/valueset-system', + valueCanonical: '#example-codesystem' + } + ] + } + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should export a value set that includes a component from a contained code system created on the value set and referenced by name', () => { + // ValueSet: ExampleValueset + // Id: example-valueset + // * ^contained.resourceType = "CodeSystem" + // * ^contained.id = "example-codesystem" + // * ^contained.name = "ExampleCodesystem" + // * ^contained.url = "http://example.org/codesystem" + // * ^contained.content = #complete + // * ^contained.concept[0].code = #example-code-1 + // * ^contained.concept[0].display = "Example Code 1" + // * include codes from system ExampleCodesystem + const valueSet = new FshValueSet('ExampleValueset'); + valueSet.id = 'example-valueset'; + const containedResourceType = new CaretValueRule(''); + containedResourceType.caretPath = 'contained.resourceType'; + containedResourceType.value = 'CodeSystem'; + const containedId = new CaretValueRule(''); + containedId.caretPath = 'contained.id'; + containedId.value = 'example-codesystem'; + const containedName = new CaretValueRule(''); + containedName.caretPath = 'contained.name'; + containedName.value = 'ExampleCodesystem'; + const containedUrl = new CaretValueRule(''); + containedUrl.caretPath = 'contained.url'; + containedUrl.value = 'http://example.org/codesystem'; + const containedContent = new CaretValueRule(''); + containedContent.caretPath = 'contained.content'; + containedContent.value = new FshCode('complete'); + const containedCode = new CaretValueRule(''); + containedCode.caretPath = 'contained.concept[0].code'; + containedCode.value = new FshCode('example-code-1'); + const containedDisplay = new CaretValueRule(''); + containedDisplay.caretPath = 'contained.concept[0].display'; + containedDisplay.value = 'Example Code 1'; + const includeCodes = new ValueSetConceptComponentRule(true); + includeCodes.from = { system: 'ExampleCodesystem' }; + valueSet.rules.push( + containedResourceType, + containedId, + containedName, + containedUrl, + containedContent, + containedCode, + containedDisplay, + includeCodes + ); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported).toHaveLength(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'ExampleValueset', + id: 'example-valueset', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/example-valueset', + contained: [ + { + resourceType: 'CodeSystem', + id: 'example-codesystem', + name: 'ExampleCodesystem', + url: 'http://example.org/codesystem', + content: 'complete', + concept: [ + { + code: 'example-code-1', + display: 'Example Code 1' + } + ] + } + ], + compose: { + include: [ + { + system: 'http://example.org/codesystem', + _system: { + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/valueset-system', + valueCanonical: '#example-codesystem' + } + ] + } + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should export a value set that includes a component from a contained code system created on the value set and referenced by url', () => { + // ValueSet: ExampleValueset + // Id: example-valueset + // * ^contained.resourceType = "CodeSystem" + // * ^contained.id = "example-codesystem" + // * ^contained.name = "ExampleCodesystem" + // * ^contained.url = "http://example.org/codesystem" + // * ^contained.content = #complete + // * ^contained.concept[0].code = #example-code-1 + // * ^contained.concept[0].display = "Example Code 1" + // * include codes from system http://example.org/codesystem + const valueSet = new FshValueSet('ExampleValueset'); + valueSet.id = 'example-valueset'; + const containedResourceType = new CaretValueRule(''); + containedResourceType.caretPath = 'contained.resourceType'; + containedResourceType.value = 'CodeSystem'; + const containedId = new CaretValueRule(''); + containedId.caretPath = 'contained.id'; + containedId.value = 'example-codesystem'; + const containedName = new CaretValueRule(''); + containedName.caretPath = 'contained.name'; + containedName.value = 'ExampleCodesystem'; + const containedUrl = new CaretValueRule(''); + containedUrl.caretPath = 'contained.url'; + containedUrl.value = 'http://example.org/codesystem'; + const containedContent = new CaretValueRule(''); + containedContent.caretPath = 'contained.content'; + containedContent.value = new FshCode('complete'); + const containedCode = new CaretValueRule(''); + containedCode.caretPath = 'contained.concept[0].code'; + containedCode.value = new FshCode('example-code-1'); + const containedDisplay = new CaretValueRule(''); + containedDisplay.caretPath = 'contained.concept[0].display'; + containedDisplay.value = 'Example Code 1'; + const includeCodes = new ValueSetConceptComponentRule(true); + includeCodes.from = { system: 'http://example.org/codesystem' }; + valueSet.rules.push( + containedResourceType, + containedId, + containedName, + containedUrl, + containedContent, + containedCode, + containedDisplay, + includeCodes + ); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported).toHaveLength(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'ExampleValueset', + id: 'example-valueset', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/example-valueset', + contained: [ + { + resourceType: 'CodeSystem', + id: 'example-codesystem', + name: 'ExampleCodesystem', + url: 'http://example.org/codesystem', + content: 'complete', + concept: [{ code: 'example-code-1', display: 'Example Code 1' }] + } + ], + compose: { + include: [ + { + system: 'http://example.org/codesystem', + _system: { + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/valueset-system', + valueCanonical: '#example-codesystem' + } + ] + } + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should not use a contained resource created on the value set as a component system when that resource is not a CodeSystem', () => { + // ValueSet: ExampleValueset + // Id: example-valueset + // * ^contained.resourceType = "Observation" + // * ^contained.id = "my-observation" + // * ^contained.status = #draft + // * ^contained.code = #123 + // * ^contained.valueString = "contained observation" + // * include codes from system my-observation + const valueSet = new FshValueSet('ExampleValueset') + .withFile('ValueSet.fsh') + .withLocation([1, 3, 9, 29]); + valueSet.id = 'example-valueset'; + const containedResourceType = new CaretValueRule(''); + containedResourceType.caretPath = 'contained.resourceType'; + containedResourceType.value = 'Observation'; + const containedId = new CaretValueRule(''); + containedId.caretPath = 'contained.id'; + containedId.value = 'my-observation'; + const containedStatus = new CaretValueRule(''); + containedStatus.caretPath = 'contained.status'; + containedStatus.value = new FshCode('draft'); + const containedCode = new CaretValueRule(''); + containedCode.caretPath = 'contained.code'; + containedCode.value = new FshCode('123'); + const containedValue = new CaretValueRule(''); + containedValue.caretPath = 'contained.valueString'; + containedValue.value = 'contained observation'; + const observationCodes = new ValueSetConceptComponentRule(true); + observationCodes.from = { system: 'my-observation' }; + valueSet.rules.push( + containedResourceType, + containedId, + containedStatus, + containedCode, + containedValue, + observationCodes + ); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported).toHaveLength(0); + expect(loggerSpy.getLastMessage('error')).toMatch( + /Resolved value "my-observation" is not a valid URI/s + ); + expect(loggerSpy.getLastMessage('error')).toMatch(/File: ValueSet\.fsh.*Line: 1 - 9\D*/s); + }); + it('should remove and log error when exporting a value set that includes a component from a self referencing value set', () => { const valueSet = new FshValueSet('DinnerVS'); valueSet.id = 'dinner-vs';