diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts index 36ef0509..8280404a 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 f2a9bb6f..108c042e 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 09ff205f..a144e377 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 27ef5463..b61f042e 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 d87303ab..85deebe3 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 84c6c904..9ecae808 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 d1e5b9c2..dee93fa8 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 676feee9..78743474 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 2ff78d28..1bac89e6 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 ea410ede..e806b7f0 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 e66042e8..5f9e1231 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 34c348c2..8f640cb0 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';