Repository: FHIR/sushi (ts)

Issue:
Setting extensions by URL/id does not work correctly for CodeSystems and ValueSets
If you set an extension on a profile or extension, it works as expected.  E.g.,
```
Profile: MyPatient
Parent: Patient
* ^extension[structuredefinition-fmm].valueInteger = 1
```
results in
```json5
"extension": [
  // Some extensions omitted for brevity
  {
    "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm",
    "valueInteger": 1
  }
]
```

But if you do the same for a CodeSystem or ValueSet, it creates an incorrect extension.  E.g.,
```
CodeSystem: MyCodeSystem
* ^extension[structuredefinition-fmm].valueInteger = 1
* #foo "Foo" "Foo"
```
results in
```json
"extension": [
  {
    "_sliceName": "structuredefinition-fmm",
    "valueInteger": {
      "assignedValue": 1,
      "_primitive": true
    }
  }
]
```

The following FSH Online snippet shows where this works (Profile, Extension, Instance) and where it doesn't (CodeSystem, ValueSet): https://fshschool.org/FSHOnline/#/share/3jZbrHQ


Pull request:
Extension setting on ValueSets and CodeSystems
Fixes #1069, allowing for extensions to be set on `CodeSystem` and `ValueSet` resources. This [FSH Online link](https://fshschool.org/FSHOnline/#/share/3jZbrHQ) provides relevant test cases.

Patch:
diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts
index ade992c..0674081 100644
--- a/src/export/CodeSystemExporter.ts
+++ b/src/export/CodeSystemExporter.ts
@@ -1,6 +1,10 @@
 import { FSHTank } from '../import/FSHTank';
 import { CodeSystem, CodeSystemConcept, StructureDefinition } from '../fhirtypes';
-import { setPropertyOnInstance, applyInsertRules } from '../fhirtypes/common';
+import {
+  setPropertyOnDefinitionInstance,
+  applyInsertRules,
+  cleanResource
+} from '../fhirtypes/common';
 import { FshCodeSystem } from '../fshtypes';
 import { CaretValueRule, ConceptRule } from '../fshtypes/rules';
 import { logger } from '../utils/FSHLogger';
@@ -74,9 +78,9 @@ export class CodeSystemExporter {
       try {
         rule.path = this.findConceptPath(codeSystem, rule.pathArray);
         successfulRules.push(rule);
-        // We can only know that a CaretRule is meant to represent codes
-        // after it has been applied on a CodeSystem, so set the flag here
-        rule.isCodeCaretRule = true;
+        if (rule.path) {
+          rule.isCodeCaretRule = true;
+        }
       } catch (e) {
         logger.error(e.message, rule.sourceInfo);
       }
@@ -84,12 +88,12 @@ export class CodeSystemExporter {
     resolveSoftIndexing(successfulRules);
     for (const rule of successfulRules) {
       try {
-        const { assignedValue, pathParts } = csStructureDefinition.validateValueAtPath(
+        setPropertyOnDefinitionInstance(
+          codeSystem,
           rule.path.length > 1 ? `${rule.path}.${rule.caretPath}` : rule.caretPath,
           rule.value,
           this.fisher
         );
-        setPropertyOnInstance(codeSystem, pathParts, assignedValue, this.fisher);
       } catch (e) {
         logger.error(e.message, rule.sourceInfo);
       }
@@ -176,6 +180,7 @@ export class CodeSystemExporter {
       );
     }
 
+    cleanResource(codeSystem, (prop: string) => ['_sliceName', '_primitive'].includes(prop));
     this.updateCount(codeSystem, fshDefinition);
     this.pkg.codeSystems.push(codeSystem);
     return codeSystem;
diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts
index 1ca2180..0e901b1 100644
--- a/src/export/ValueSetExporter.ts
+++ b/src/export/ValueSetExporter.ts
@@ -1,9 +1,4 @@
-import {
-  ValueSet,
-  ValueSetComposeIncludeOrExclude,
-  ValueSetComposeConcept,
-  StructureDefinition
-} from '../fhirtypes';
+import { ValueSet, ValueSetComposeIncludeOrExclude, ValueSetComposeConcept } from '../fhirtypes';
 import { FSHTank } from '../import/FSHTank';
 import { FshValueSet, FshCode, ValueSetFilterValue, FshCodeSystem, Instance } from '../fshtypes';
 import { logger } from '../utils/FSHLogger';
@@ -17,9 +12,10 @@ import {
   ValueSetFilterComponentRule
 } from '../fshtypes/rules';
 import {
-  setPropertyOnInstance,
   applyInsertRules,
-  listUndefinedLocalCodes
+  listUndefinedLocalCodes,
+  setPropertyOnDefinitionInstance,
+  cleanResource
 } from '../fhirtypes/common';
 import { isUri } from 'valid-url';
 import { flatMap } from 'lodash';
@@ -169,19 +165,11 @@ export class ValueSetExporter {
   }
 
   private setCaretRules(valueSet: ValueSet, rules: CaretValueRule[]) {
-    const vsStructureDefinition = StructureDefinition.fromJSON(
-      this.fisher.fishForFHIR('ValueSet', Type.Resource)
-    );
     resolveSoftIndexing(rules);
     for (const rule of rules) {
       try {
         if (rule instanceof CaretValueRule) {
-          const { assignedValue, pathParts } = vsStructureDefinition.validateValueAtPath(
-            rule.caretPath,
-            rule.value,
-            this.fisher
-          );
-          setPropertyOnInstance(valueSet, pathParts, assignedValue, this.fisher);
+          setPropertyOnDefinitionInstance(valueSet, rule.caretPath, rule.value, this.fisher);
         }
       } catch (e) {
         logger.error(e.message, rule.sourceInfo);
@@ -245,6 +233,7 @@ export class ValueSetExporter {
       );
     }
 
+    cleanResource(vs, (prop: string) => ['_sliceName', '_primitive'].includes(prop));
     this.pkg.valueSets.push(vs);
     return vs;
   }
diff --git a/src/fhirtypes/CodeSystem.ts b/src/fhirtypes/CodeSystem.ts
index da316f1..ba2476d 100644
--- a/src/fhirtypes/CodeSystem.ts
+++ b/src/fhirtypes/CodeSystem.ts
@@ -6,6 +6,8 @@ import { Narrative, Resource, Identifier, CodeableConcept, Coding } from './data
 import { ContactDetail, UsageContext } from './metaDataTypes';
 import { HasName, HasId } from './mixins';
 import { applyMixins } from '../utils/Mixin';
+import { StructureDefinition } from './StructureDefinition';
+import { Fishable, Type } from '../utils';
 
 /**
  * Class representing a FHIR R4 CodeSystem
@@ -48,6 +50,15 @@ export class CodeSystem {
   property?: CodeSystemProperty[];
   concept?: CodeSystemConcept[];
 
+  /**
+   * Get the Structure Definition for Code System
+   * @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
+   * @returns {StructureDefinition} the StructureDefinition of Code System
+   */
+  getOwnStructureDefinition(fisher: Fishable): StructureDefinition {
+    return StructureDefinition.fromJSON(fisher.fishForFHIR('CodeSystem', Type.Resource));
+  }
+
   /**
    * Get the file name for serializing to disk.
    * @returns {string} the filename
@@ -91,6 +102,7 @@ export type CodeSystemConcept = {
   designation?: CodeSystemConceptDesignation[];
   property?: CodeSystemConceptProperty[];
   concept?: CodeSystemConcept[];
+  extension?: Extension[];
 };
 
 export type CodeSystemConceptDesignation = {
diff --git a/src/fhirtypes/ValueSet.ts b/src/fhirtypes/ValueSet.ts
index 4b8bece..5b67ff6 100644
--- a/src/fhirtypes/ValueSet.ts
+++ b/src/fhirtypes/ValueSet.ts
@@ -6,6 +6,8 @@ import { ContactDetail, UsageContext } from './metaDataTypes';
 import { cloneDeep } from 'lodash';
 import { HasName, HasId } from './mixins';
 import { applyMixins } from '../utils/Mixin';
+import { StructureDefinition } from './StructureDefinition';
+import { Fishable, Type } from '../utils';
 
 /**
  * Class representing a FHIR R4 ValueSet.
@@ -41,6 +43,15 @@ export class ValueSet {
   copyright: string;
   compose: ValueSetCompose;
 
+  /**
+   * Get the Structure Definition for ValueSet
+   * @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
+   * @returns {StructureDefinition} the StructureDefinition of ValueSet
+   */
+  getOwnStructureDefinition(fisher: Fishable): StructureDefinition {
+    return StructureDefinition.fromJSON(fisher.fishForFHIR('ValueSet', Type.Resource));
+  }
+
   /**
    * Get the file name for serializing to disk.
    * @returns {string} the filename
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 22202c1..087fc06 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -40,14 +40,14 @@ export function splitOnPathPeriods(path: string): string[] {
 }
 
 /**
- * This function sets an instance property of an SD or ED if possible
- * @param {StructureDefinition | ElementDefinition} instance - The instance to assign a value on
+ * This function sets an instance property of a resource if possible
+ * @param {StructureDefinition | ElementDefinition | CodeSystem | ValueSet} instance - The instance to assign a value on
  * @param {string} path - The path to assign a value at
  * @param {any} value - The value to assign
  * @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
  */
 export function setPropertyOnDefinitionInstance(
-  instance: StructureDefinition | ElementDefinition,
+  instance: StructureDefinition | ElementDefinition | CodeSystem | ValueSet,
   path: string,
   value: any,
   fisher: Fishable
@@ -59,7 +59,7 @@ export function setPropertyOnDefinitionInstance(
 }
 
 export function setImpliedPropertiesOnInstance(
-  instanceDef: StructureDefinition | ElementDefinition | InstanceDefinition,
+  instanceDef: StructureDefinition | ElementDefinition | InstanceDefinition | CodeSystem | ValueSet,
   instanceOfStructureDefinition: StructureDefinition,
   paths: string[],
   fisher: Fishable
@@ -436,11 +436,11 @@ export function replaceField(
 
 /**
  * Cleans up temporary properties that were added to the resource definition during processing
- * @param {StructureDefinition | InstanceDefinition} resourceDef - The resource definition to clean
+ * @param {StructureDefinition | InstanceDefinition | CodeSystem | ValueSet} resourceDef - The resource definition to clean
  * @param {string => boolean} skipFn - A function that returns true if a property should not be traversed
  */
 export function cleanResource(
-  resourceDef: StructureDefinition | InstanceDefinition,
+  resourceDef: StructureDefinition | InstanceDefinition | CodeSystem | ValueSet,
   skipFn: (prop: string) => boolean = () => false
 ): void {
   // Remove all _sliceName fields


Test patch:
diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts
index a1099dc..a3324da 100644
--- a/test/export/CodeSystemExporter.test.ts
+++ b/test/export/CodeSystemExporter.test.ts
@@ -587,6 +587,32 @@ describe('CodeSystemExporter', () => {
     });
   });
 
+  it('should export a code system with extensions', () => {
+    const codeSystem = new FshCodeSystem('Strange.Code.System')
+      .withFile('Strange.fsh')
+      .withLocation([3, 4, 8, 24]);
+    const extensionRule = new CaretValueRule('');
+    extensionRule.caretPath = 'extension[structuredefinition-fmm].valueInteger';
+    extensionRule.value = 1;
+    const conceptRule = new ConceptRule('bar', 'Bar', 'Bar');
+    const conceptExtensionRule = new CaretValueRule('bar');
+    conceptExtensionRule.pathArray = ['bar'];
+    conceptExtensionRule.caretPath = 'extension[structuredefinition-fmm].valueInteger';
+    conceptExtensionRule.value = 2;
+    codeSystem.rules.push(extensionRule, conceptRule, conceptExtensionRule);
+    doc.codeSystems.set(codeSystem.name, codeSystem);
+    const exported = exporter.export().codeSystems;
+    expect(exported.length).toBe(1);
+    expect(exported[0].extension).toContainEqual({
+      url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm',
+      valueInteger: 1
+    });
+    expect(exported[0].concept[0].extension).toContainEqual({
+      url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm',
+      valueInteger: 2
+    });
+  });
+
   it('should not override count when ^count is provided by user', () => {
     const codeSystem = new FshCodeSystem('MyCodeSystem');
     const rule = new CaretValueRule('');
diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts
index c757348..460940b 100644
--- a/test/export/ValueSetExporter.test.ts
+++ b/test/export/ValueSetExporter.test.ts
@@ -1621,6 +1621,22 @@ describe('ValueSetExporter', () => {
     });
   });
 
+  it('should export a value set with an extension', () => {
+    const valueSet = new FshValueSet('BreakfastVS');
+    valueSet.title = 'Breakfast Values';
+    const extensionRule = new CaretValueRule('');
+    extensionRule.caretPath = 'extension[structuredefinition-fmm].valueInteger';
+    extensionRule.value = 1;
+    valueSet.rules.push(extensionRule);
+    doc.valueSets.set(valueSet.name, valueSet);
+    const exported = exporter.export().valueSets;
+    expect(exported.length).toBe(1);
+    expect(exported[0].extension).toContainEqual({
+      url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm',
+      valueInteger: 1
+    });
+  });
+
   it('should log a message when applying invalid CaretValueRule', () => {
     const valueSet = new FshValueSet('DinnerVS');
     const rule = new CaretValueRule('').withFile('InvalidValue.fsh').withLocation([6, 3, 6, 12]);
diff --git a/test/testhelpers/testdefs/r4-definitions/package/StructureDefinition-fmm.json b/test/testhelpers/testdefs/r4-definitions/package/StructureDefinition-fmm.json
new file mode 100644
index 0000000..9b583f2
--- /dev/null
+++ b/test/testhelpers/testdefs/r4-definitions/package/StructureDefinition-fmm.json
@@ -0,0 +1,276 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "structuredefinition-fmm",
+  "extension": [
+    {
+      "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg",
+      "valueCode": "fhir"
+    },
+    {
+      "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm",
+      "valueInteger": 1
+    }
+  ],
+  "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm",
+  "version": "4.6.0",
+  "name": "fmm",
+  "status": "draft",
+  "date": "2014-01-31",
+  "publisher": "Health Level Seven, Inc. - [WG Name] WG",
+  "contact": [
+    {
+      "telecom": [
+        {
+          "system": "url",
+          "value": "http://hl7.org/special/committees/FHIR"
+        }
+      ]
+    }
+  ],
+  "description": "The FMM level assigned to the artifact.",
+  "fhirVersion": "4.6.0",
+  "mapping": [
+    {
+      "identity": "rim",
+      "uri": "http://hl7.org/v3",
+      "name": "RIM Mapping"
+    }
+  ],
+  "kind": "complex-type",
+  "abstract": false,
+  "context": [
+    {
+      "type": "element",
+      "expression": "Element"
+    }
+  ],
+  "type": "Extension",
+  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension",
+  "derivation": "constraint",
+  "snapshot": {
+    "element": [
+      {
+        "id": "Extension",
+        "path": "Extension",
+        "short": "FMM Level",
+        "definition": "The FMM level assigned to the artifact.",
+        "comment": "Though this is defined for resources, it can be used for any artifact.",
+        "min": 0,
+        "max": "1",
+        "base": {
+          "path": "Extension",
+          "min": 0,
+          "max": "*"
+        },
+        "constraint": [
+          {
+            "key": "ele-1",
+            "severity": "error",
+            "human": "All FHIR elements must have a @value or children",
+            "expression": "hasValue() or (children().count() > id.count())",
+            "xpath": "@value|f:*|h:div",
+            "source": "http://hl7.org/fhir/StructureDefinition/Element"
+          },
+          {
+            "key": "ext-1",
+            "severity": "error",
+            "human": "Must have either extensions or value[x], not both",
+            "expression": "extension.exists() != value.exists()",
+            "xpath": "exists(f:extension)!=exists(f:*[starts-with(local-name(.), 'value')])",
+            "source": "http://hl7.org/fhir/StructureDefinition/Extension"
+          }
+        ],
+        "isModifier": false
+      },
+      {
+        "id": "Extension.id",
+        "path": "Extension.id",
+        "representation": [
+          "xmlAttr"
+        ],
+        "short": "Unique id for inter-element referencing",
+        "definition": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
+        "min": 0,
+        "max": "1",
+        "base": {
+          "path": "Element.id",
+          "min": 0,
+          "max": "1"
+        },
+        "type": [
+          {
+            "extension": [
+              {
+                "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type",
+                "valueUrl": "string"
+              }
+            ],
+            "code": "http://hl7.org/fhirpath/System.String"
+          }
+        ],
+        "isModifier": false,
+        "isSummary": false,
+        "mapping": [
+          {
+            "identity": "rim",
+            "map": "n/a"
+          }
+        ]
+      },
+      {
+        "id": "Extension.extension",
+        "path": "Extension.extension",
+        "slicing": {
+          "discriminator": [
+            {
+              "type": "value",
+              "path": "url"
+            }
+          ],
+          "description": "Extensions are always sliced by (at least) url",
+          "rules": "open"
+        },
+        "short": "Extension",
+        "definition": "An Extension",
+        "min": 0,
+        "max": "0",
+        "base": {
+          "path": "Element.extension",
+          "min": 0,
+          "max": "*"
+        },
+        "type": [
+          {
+            "code": "Extension"
+          }
+        ],
+        "constraint": [
+          {
+            "key": "ele-1",
+            "severity": "error",
+            "human": "All FHIR elements must have a @value or children",
+            "expression": "hasValue() or (children().count() > id.count())",
+            "xpath": "@value|f:*|h:div",
+            "source": "http://hl7.org/fhir/StructureDefinition/Element"
+          },
+          {
+            "key": "ext-1",
+            "severity": "error",
+            "human": "Must have either extensions or value[x], not both",
+            "expression": "extension.exists() != value.exists()",
+            "xpath": "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])",
+            "source": "http://hl7.org/fhir/StructureDefinition/Extension"
+          }
+        ],
+        "isModifier": false,
+        "isSummary": false
+      },
+      {
+        "id": "Extension.url",
+        "path": "Extension.url",
+        "representation": [
+          "xmlAttr"
+        ],
+        "short": "identifies the meaning of the extension",
+        "definition": "Source of the definition for the extension code - a logical name or a URL.",
+        "comment": "The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.",
+        "min": 1,
+        "max": "1",
+        "base": {
+          "path": "Extension.url",
+          "min": 1,
+          "max": "1"
+        },
+        "type": [
+          {
+            "extension": [
+              {
+                "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type",
+                "valueUrl": "uri"
+              }
+            ],
+            "code": "http://hl7.org/fhirpath/System.String"
+          }
+        ],
+        "fixedUri": "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm",
+        "isModifier": false,
+        "isSummary": false,
+        "mapping": [
+          {
+            "identity": "rim",
+            "map": "N/A"
+          }
+        ]
+      },
+      {
+        "id": "Extension.value[x]",
+        "path": "Extension.value[x]",
+        "short": "Value of extension",
+        "definition": "Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list).",
+        "min": 1,
+        "max": "1",
+        "base": {
+          "path": "Extension.value[x]",
+          "min": 0,
+          "max": "1"
+        },
+        "type": [
+          {
+            "code": "integer"
+          }
+        ],
+        "constraint": [
+          {
+            "key": "ele-1",
+            "severity": "error",
+            "human": "All FHIR elements must have a @value or children",
+            "expression": "hasValue() or (children().count() > id.count())",
+            "xpath": "@value|f:*|h:div",
+            "source": "http://hl7.org/fhir/StructureDefinition/Element"
+          }
+        ],
+        "isModifier": false,
+        "isSummary": false,
+        "mapping": [
+          {
+            "identity": "rim",
+            "map": "N/A"
+          }
+        ]
+      }
+    ]
+  },
+  "differential": {
+    "element": [
+      {
+        "id": "Extension",
+        "path": "Extension",
+        "short": "FMM Level",
+        "definition": "The FMM level assigned to the artifact.",
+        "comment": "Though this is defined for resources, it can be used for any artifact.",
+        "min": 0,
+        "max": "1"
+      },
+      {
+        "id": "Extension.extension",
+        "path": "Extension.extension",
+        "max": "0"
+      },
+      {
+        "id": "Extension.url",
+        "path": "Extension.url",
+        "fixedUri": "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm"
+      },
+      {
+        "id": "Extension.value[x]",
+        "path": "Extension.value[x]",
+        "min": 1,
+        "type": [
+          {
+            "code": "integer"
+          }
+        ]
+      }
+    ]
+  }
+}
\ No newline at end of file