Repository: FHIR/sushi (ts)

Issue:
Cannot refer to inline CodeSystems in ValueSet compose rules
The following code does not work, but should work (resulting in `compose > include > system` value `#example-codesystem`):
```
ValueSet: ExampleValueset
Id: example-valueset
* ^contained = example-codesystem
* ^url = "http://example.org/valueset"
* ^version = "1.0.0"
* ^status = #active
* include codes from system example-codesystem

Instance: example-codesystem
InstanceOf: CodeSystem
Usage: #inline
* url = "http://example.org/codesystem"
* version = "1.0.0"
* status = #active
* content = #complete
// more rules
```
Similarly, it should work correctly if `example-codesystem` is defined using `CodeSystem` syntax (and still use the fragment instead of the full URL).

We should also check to see if you can refer to inline value sets from compose rules (and fix it if you can't).

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Contained.20code.20system.20in.20the.20value.20set/near/407685349

Pull request:
Allow ValueSet to reference contained inline CodeSystem
**Description:**
If a ValueSetComponentRule specifies a system, check if that system is present in the list of contained resources. If so, add the [valueset-system extension](https://hl7.org/fhir/extensions/StructureDefinition-valueset-system.html) to the system, using the relative reference as the extension's value. If the system is not present in the list of contained resources, and the system is an inline Instance, log an error and do not add the component to the ValueSet.

Fishing in the tank for a CodeSystem will now return inline instances of CodeSystem. This matches the operation of fishing in the tank for a ValueSet.

Add `_system.extension` to the type definition for elements of a ValueSet's include and exclude lists. Use the Extension fhirtype as part of this definition. Replace the Extension fshtype with the Extension fhirtype in other parts of the ValueSet definition.

**Testing Instructions:**
Tests are added to ValueSetExporter and FHIRExporter as part of this change set.

As part of this implementation, I chose to make the failure case log an error and skip the component, but not throw anything. This means that processing of the ValueSet will continue. Please let me know if you think that this should instead throw an error and halt processing the ValueSet.

**Related Issue:**
Fixes #1402. Note that this does _not_ resolve the somewhat related issues #1403 (for allowing fragments as references to contained ValueSets) and #1404 (which would allow an inline CodeSystem to be constructed using caret rules on the ValueSet).

Additional information that led to this issue available in [this zulip thread](https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Contained.20code.20system.20in.20the.20value.20set/near/424938537). Additionally, I checked the list of FHIR core extensions for a similar extension to be used when a contained ValueSet is referenced within another ValueSet, and I did not find any such extension.

Patch:
diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts
index 19c1b77..27ef546 100644
--- a/src/export/ValueSetExporter.ts
+++ b/src/export/ValueSetExporter.ts
@@ -76,6 +76,30 @@ export class ValueSetExporter {
             .replace(/^([^|]+)/, csMetadata?.url ?? '$1')
             .split('|');
           composeElement.system = foundSystem[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) {
+            composeElement._system = {
+              extension: [
+                {
+                  url: 'http://hl7.org/fhir/StructureDefinition/valueset-system',
+                  valueCanonical: `#${csMetadata.id}`
+                }
+              ]
+            };
+          } else if (csMetadata?.instanceUsage === 'Inline') {
+            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
+            );
+            return;
+          }
+
           // if the rule specified a version, use that version.
           composeElement.version = systemParts.slice(1).join('|') || undefined;
           if (!isUri(composeElement.system)) {
diff --git a/src/fhirtypes/ValueSet.ts b/src/fhirtypes/ValueSet.ts
index 5730578..555b7d9 100644
--- a/src/fhirtypes/ValueSet.ts
+++ b/src/fhirtypes/ValueSet.ts
@@ -1,7 +1,14 @@
 import sanitize from 'sanitize-filename';
 import { Meta } from './specialTypes';
-import { Extension } from '../fshtypes';
-import { Narrative, Resource, Identifier, CodeableConcept, Coding } from './dataTypes';
+import {
+  Narrative,
+  Resource,
+  Identifier,
+  CodeableConcept,
+  Coding,
+  Extension,
+  Element
+} from './dataTypes';
 import { ContactDetail, UsageContext } from './metaDataTypes';
 import { HasName, HasId } from './mixins';
 import { applyMixins } from '../utils/Mixin';
@@ -83,6 +90,7 @@ export type ValueSetCompose = {
 
 export type ValueSetComposeIncludeOrExclude = {
   system?: string;
+  _system?: Element;
   version?: string;
   valueSet?: string[];
   concept?: ValueSetComposeConcept[];
diff --git a/src/import/FSHTank.ts b/src/import/FSHTank.ts
index 8755d67..8f9b1ff 100644
--- a/src/import/FSHTank.ts
+++ b/src/import/FSHTank.ts
@@ -356,7 +356,7 @@ export class FSHTank implements Fishable {
             result = this.getAllInstances().find(
               csInstance =>
                 csInstance?.instanceOf === 'CodeSystem' &&
-                csInstance?.usage === 'Definition' &&
+                (csInstance?.usage === 'Definition' || csInstance?.usage === 'Inline') &&
                 (csInstance?.name === base ||
                   csInstance.id === base ||
                   getUrlFromFshDefinition(csInstance, this.config.canonical) === base ||


Test patch:
diff --git a/test/export/FHIRExporter.test.ts b/test/export/FHIRExporter.test.ts
index af036d3..676feee 100644
--- a/test/export/FHIRExporter.test.ts
+++ b/test/export/FHIRExporter.test.ts
@@ -4,8 +4,13 @@ import { exportFHIR, Package, FHIRExporter } from '../../src/export';
 import { FSHTank, FSHDocument } from '../../src/import';
 import { FHIRDefinitions } from '../../src/fhirdefs';
 import { minimalConfig } from '../utils/minimalConfig';
-import { FshValueSet, Instance, Profile } from '../../src/fshtypes';
-import { AssignmentRule, BindingRule, CaretValueRule } from '../../src/fshtypes/rules';
+import { FshCodeSystem, FshValueSet, Instance, Profile } from '../../src/fshtypes';
+import {
+  AssignmentRule,
+  BindingRule,
+  CaretValueRule,
+  ValueSetConceptComponentRule
+} from '../../src/fshtypes/rules';
 import { TestFisher, loggerSpy } from '../testhelpers';
 
 describe('FHIRExporter', () => {
@@ -507,6 +512,62 @@ describe('FHIRExporter', () => {
       });
     });
 
+    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
+      const foodCS = new FshCodeSystem('FoodCS');
+      foodCS.id = 'food';
+      doc.codeSystems.set(foodCS.name, foodCS);
+      // ValueSet: DinnerVS
+      // * ^contained[0] = FoodCS
+      // * include codes from system food
+      const valueSet = new FshValueSet('DinnerVS');
+      const containedCS = new CaretValueRule('');
+      containedCS.caretPath = 'contained[0]';
+      containedCS.value = 'FoodCS';
+      containedCS.isInstance = true;
+      const component = new ValueSetConceptComponentRule(true);
+      component.from = { system: 'FoodCS' };
+      valueSet.rules.push(containedCS, component);
+      doc.valueSets.set(valueSet.name, valueSet);
+
+      const exported = exporter.export();
+      expect(exported.valueSets.length).toBe(1);
+      expect(exported.valueSets[0]).toEqual({
+        resourceType: 'ValueSet',
+        name: 'DinnerVS',
+        id: 'DinnerVS',
+        status: 'draft',
+        url: 'http://hl7.org/fhir/us/minimal/ValueSet/DinnerVS',
+        contained: [
+          {
+            content: 'complete',
+            id: 'food',
+            name: 'FoodCS',
+            resourceType: 'CodeSystem',
+            status: 'draft',
+            url: 'http://hl7.org/fhir/us/minimal/CodeSystem/food'
+          }
+        ],
+        compose: {
+          include: [
+            {
+              system: 'http://hl7.org/fhir/us/minimal/CodeSystem/food',
+              _system: {
+                extension: [
+                  {
+                    url: 'http://hl7.org/fhir/StructureDefinition/valueset-system',
+                    valueCanonical: '#food'
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      });
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
     it('should log a message when trying to assign a value that is numeric and refers to an Instance, but both types are wrong', () => {
       // Profile: MyObservation
       // Parent: Observation
diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts
index 78baa7b..34c348c 100644
--- a/test/export/ValueSetExporter.test.ts
+++ b/test/export/ValueSetExporter.test.ts
@@ -380,6 +380,187 @@ describe('ValueSetExporter', () => {
     });
   });
 
+  it('should export a value set that includes a component from a contained inline instance of code system and add the valueset-system extension', () => {
+    // Instance: example-codesystem
+    // InstanceOf: CodeSystem
+    // Usage: #inline
+    // * url = "http://example.org/codesystem"
+    // * version = "1.0.0"
+    // * status = #active
+    // * content = #complete
+    const inlineCodeSystem = new Instance('example-codesystem');
+    inlineCodeSystem.instanceOf = 'CodeSystem';
+    inlineCodeSystem.usage = 'Inline';
+    const urlRule = new AssignmentRule('url');
+    urlRule.value = 'http://example.org/codesystem';
+    const versionRule = new AssignmentRule('version');
+    versionRule.value = '1.0.0';
+    const statusRule = new AssignmentRule('status');
+    statusRule.value = new FshCode('active');
+    const contentRule = new AssignmentRule('content');
+    contentRule.value = new FshCode('complete');
+    inlineCodeSystem.rules.push(urlRule, versionRule, statusRule, contentRule);
+    doc.instances.set(inlineCodeSystem.name, inlineCodeSystem);
+    // ValueSet: ExampleValueset
+    // Id: example-valueset
+    // * ^contained = example-codesystem
+    // * include codes from system example-codesystem
+    const valueSet = new FshValueSet('ExampleValueset');
+    valueSet.id = 'example-valueset';
+    const containedSystem = new CaretValueRule('');
+    containedSystem.caretPath = 'contained';
+    containedSystem.value = 'example-codesystem';
+    containedSystem.isInstance = true;
+    const component = new ValueSetConceptComponentRule(true);
+    component.from = { system: 'example-codesystem' };
+    valueSet.rules.push(containedSystem, component);
+    doc.valueSets.set(valueSet.name, valueSet);
+
+    const exported = exporter.export().valueSets;
+    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);
+  });
+
+  it('should log an error and not add the component when attempting to reference an inline instance of code system that is not contained', () => {
+    // Instance: example-codesystem
+    // InstanceOf: CodeSystem
+    // Usage: #inline
+    // * url = "http://example.org/codesystem"
+    // * version = "1.0.0"
+    // * status = #active
+    // * content = #complete
+    const inlineCodeSystem = new Instance('example-codesystem');
+    inlineCodeSystem.instanceOf = 'CodeSystem';
+    inlineCodeSystem.usage = 'Inline';
+    const urlRule = new AssignmentRule('url');
+    urlRule.value = 'http://example.org/codesystem';
+    const versionRule = new AssignmentRule('version');
+    versionRule.value = '1.0.0';
+    const statusRule = new AssignmentRule('status');
+    statusRule.value = new FshCode('active');
+    const contentRule = new AssignmentRule('content');
+    contentRule.value = new FshCode('complete');
+    inlineCodeSystem.rules.push(urlRule, versionRule, statusRule, contentRule);
+    doc.instances.set(inlineCodeSystem.name, inlineCodeSystem);
+    // ValueSet: ExampleValueset
+    // Id: example-valueset
+    // * include codes from system example-codesystem
+    // * include codes from system http://hl7.org/fhir/us/minimal/CodeSystem/food
+    const valueSet = new FshValueSet('ExampleValueset');
+    valueSet.id = 'example-valueset';
+    const exampleComponent = new ValueSetConceptComponentRule(true)
+      .withFile('ExampleVS.fsh')
+      .withLocation([5, 3, 5, 48]);
+    exampleComponent.from = { system: 'example-codesystem' };
+    const foodComponent = new ValueSetConceptComponentRule(true);
+    foodComponent.from = { system: 'http://hl7.org/fhir/us/minimal/CodeSystem/food' };
+    valueSet.rules.push(exampleComponent, foodComponent);
+    doc.valueSets.set(valueSet.name, valueSet);
+
+    const exported = exporter.export().valueSets;
+    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',
+      compose: {
+        include: [
+          {
+            system: 'http://hl7.org/fhir/us/minimal/CodeSystem/food'
+          }
+        ]
+      }
+    });
+    expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Can not reference CodeSystem example-codesystem/s
+    );
+    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', () => {
+    // Instance: example-codesystem
+    // InstanceOf: CodeSystem
+    // Usage: #example
+    // * url = "http://example.org/codesystem"
+    // * version = "1.0.0"
+    // * status = #active
+    // * content = #complete
+    const inlineCodeSystem = new Instance('example-codesystem');
+    inlineCodeSystem.instanceOf = 'CodeSystem';
+    inlineCodeSystem.usage = 'Example';
+    const urlRule = new AssignmentRule('url');
+    urlRule.value = 'http://example.org/codesystem';
+    const versionRule = new AssignmentRule('version');
+    versionRule.value = '1.0.0';
+    const statusRule = new AssignmentRule('status');
+    statusRule.value = new FshCode('active');
+    const contentRule = new AssignmentRule('content');
+    contentRule.value = new FshCode('complete');
+    inlineCodeSystem.rules.push(urlRule, versionRule, statusRule, contentRule);
+    doc.instances.set(inlineCodeSystem.name, inlineCodeSystem);
+
+    // ValueSet: ExampleValueset
+    // 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]);
+    valueSet.id = 'example-valueset';
+    const containedSystem = new CaretValueRule('');
+    containedSystem.caretPath = 'contained';
+    containedSystem.value = 'example-codesystem';
+    containedSystem.isInstance = true;
+    const exampleComponent = new ValueSetConceptComponentRule(true);
+
+    exampleComponent.from = { system: 'example-codesystem' };
+    valueSet.rules.push(containedSystem, exampleComponent);
+    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(loggerSpy.getLastMessage('error')).toMatch(/File: ExampleVS\.fsh.*Line: 2 - 7\D*/s);
+  });
+
   it('should export a value set that includes a component from a value set', () => {
     const valueSet = new FshValueSet('DinnerVS');
     const component = new ValueSetConceptComponentRule(true);
@@ -473,6 +654,75 @@ describe('ValueSetExporter', () => {
     });
   });
 
+  // TODO: as part of a later task, confirm that this is in fact correct. it seems to be what the IG publisher expects,
+  // but doesn't quite fit the spec for ValueSet.compose.include.valueSet
+  it.skip('should export a value set that includes a component from a contained inline instance of value set', () => {
+    // Instance: inline-valueset
+    // InstanceOf: ValueSet
+    // Usage: #inline
+    // * url = "http://example.org/inline-value-set"
+    // * status = #draft
+    // * compose.include[0].system = "http://example.org/SomeCS"
+    const inlineValueSet = new Instance('inline-valueset');
+    inlineValueSet.instanceOf = 'ValueSet';
+    inlineValueSet.usage = 'Inline';
+    const urlRule = new AssignmentRule('url');
+    urlRule.value = 'http://example.org/inline-value-set';
+    const statusRule = new AssignmentRule('status');
+    statusRule.value = new FshCode('draft');
+    const systemRule = new AssignmentRule('compose.include[0].system');
+    systemRule.value = 'http://example.org/SomeCS';
+    inlineValueSet.rules.push(urlRule, statusRule, systemRule);
+    doc.instances.set(inlineValueSet.name, inlineValueSet);
+
+    // ValueSet: ExampleValueset
+    // Id: example-valueset
+    // * ^contained = inline-valueset
+    // * include codes from valueset inline-valueset
+    const valueSet = new FshValueSet('ExampleValueset');
+    valueSet.id = 'example-valueset';
+    const containedVS = new CaretValueRule('');
+    containedVS.caretPath = 'contained';
+    containedVS.value = 'inline-valueset';
+    containedVS.isInstance = true;
+    const component = new ValueSetConceptComponentRule(true);
+    component.from = { valueSets: ['inline-valueset'] };
+    valueSet.rules.push(containedVS, component);
+    doc.valueSets.set(valueSet.name, valueSet);
+
+    const exported = exporter.export().valueSets;
+    expect(exported.length).toBe(1);
+    expect(exported[0]).toEqual({
+      resourceType: 'ValueSet',
+      id: 'example-valueset',
+      name: 'ExampleValueset',
+      url: 'http://hl7.org/fhir/us/minimal/ValueSet/example-valueset',
+      status: 'draft',
+      contained: [
+        {
+          resourceType: 'ValueSet',
+          id: 'inline-valueset',
+          url: 'http://example.org/inline-value-set',
+          status: 'draft',
+          compose: {
+            include: [
+              {
+                system: 'http://example.org/SomeCS'
+              }
+            ]
+          }
+        }
+      ],
+      compose: {
+        include: [
+          {
+            valueSet: ['#inline-valueset']
+          }
+        ]
+      }
+    });
+  });
+
   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';