Repository: FHIR/sushi (ts)

Issue:
Mapping Source should allow Logicals and Resources
When defining a `Mapping`, the `Source` indicates where the `StructureDefinition` where the mapping statements will be applied.  Currently, SUSHI only allows Profiles/Extensions to be referenced as the `Source`, but it should also allow Logicals and Resources since they are also StructureDefinitions.

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Logical.20Model.20Mapping

Pull request description:
Mapping source may be Logical or Resource
Fixes #940 and completes task [CIMPL-842](https://standardhealthrecord.atlassian.net/browse/CIMPL-842).

When exporting a mapping, allow Logical and Resource as types when fishing for the source. Allow Logical when fishing for the parent.

I think this change is actually as simple as it sounds: just include the types when fishing. If there's something more subtle going on, I completely missed it.

Patch:
diff --git a/src/export/MappingExporter.ts b/src/export/MappingExporter.ts
index 38095e5..8f435e3 100644
--- a/src/export/MappingExporter.ts
+++ b/src/export/MappingExporter.ts
@@ -70,7 +70,9 @@ export class MappingExporter {
     const sourceStructDef = this.pkg.fish(
       fshDefinition.source,
       Type.Profile,
-      Type.Extension
+      Type.Extension,
+      Type.Resource,
+      Type.Logical
     ) as StructureDefinition;
     if (sourceStructDef) {
       const parent = this.fisher.fishForFHIR(
@@ -78,7 +80,8 @@ export class MappingExporter {
         Type.Resource,
         Type.Type,
         Type.Profile,
-        Type.Extension
+        Type.Extension,
+        Type.Logical
       );
       const matchingParentMapping = parent?.mapping.find(
         (m: StructureDefinitionMapping) => m.identity === fshDefinition.id


Test patch:
diff --git a/test/export/MappingExporter.test.ts b/test/export/MappingExporter.test.ts
index c20e97b..936410c 100644
--- a/test/export/MappingExporter.test.ts
+++ b/test/export/MappingExporter.test.ts
@@ -16,6 +16,8 @@ describe('MappingExporter', () => {
   let exporter: MappingExporter;
   let observation: StructureDefinition;
   let practitioner: StructureDefinition;
+  let logical: StructureDefinition;
+  let resource: StructureDefinition;
 
   beforeAll(() => {
     defs = new FHIRDefinitions();
@@ -39,6 +41,12 @@ describe('MappingExporter', () => {
     practitioner = fisher.fishForStructureDefinition('Practitioner');
     practitioner.id = 'MyPractitioner';
     pkg.profiles.push(practitioner);
+    logical = fisher.fishForStructureDefinition('eLTSSServiceModel');
+    logical.id = 'MyLogical';
+    pkg.logicals.push(logical);
+    resource = fisher.fishForStructureDefinition('Duration');
+    resource.id = 'MyResource';
+    pkg.resources.push(resource);
   });
 
   it('should log an error when the mapping source does not exist', () => {
@@ -349,20 +357,57 @@ describe('MappingExporter', () => {
       /**
        * Mapping: MyMapping
        * Source: MyObservation
-       * * status -> "Observation.otherStatus"
+       * * -> "OtherObservation"
        */
       const mapRule = new MappingRule('');
       mapRule.map = 'OtherObservation';
       mapping.rules.push(mapRule);
-      const status = observation.elements.find(e => e.id === 'Observation');
-      const originalLength = status.mapping.length;
+      const baseElement = observation.elements.find(e => e.id === 'Observation');
+      const originalLength = baseElement.mapping.length;
       exporter.export();
-      expect(status.mapping.length).toBe(originalLength + 1);
-      const exported = status.mapping.slice(-1)[0];
+      expect(baseElement.mapping.length).toBe(originalLength + 1);
+      const exported = baseElement.mapping.slice(-1)[0];
       expect(exported.map).toBe('OtherObservation');
       expect(exported.identity).toBe('MyMapping');
     });
 
+    it('should apply a valid mapping rule with a Logical source', () => {
+      /**
+       * Mapping: MyMapping
+       * Source: MyLogical
+       * * name -> "Something.provider.name"
+       */
+      mapping.source = 'MyLogical';
+      const mapRule = new MappingRule('name');
+      mapRule.map = 'Something.provider.name';
+      mapping.rules.push(mapRule);
+      const baseName = logical.elements.find(e => e.id === 'eLTSSServiceModel.name');
+      expect(baseName.mapping).toBeUndefined();
+      exporter.export();
+      expect(baseName.mapping).toHaveLength(1);
+      expect(baseName.mapping[0].identity).toBe('MyMapping');
+      expect(baseName.mapping[0].map).toBe('Something.provider.name');
+    });
+
+    it('should apply a valid mapping rule with a Resource source', () => {
+      /**
+       * Mapping: MyMapping
+       * Source: MyResource
+       * * comparator -> "Something.operator"
+       */
+      mapping.source = 'MyResource';
+      const mapRule = new MappingRule('comparator');
+      mapRule.map = 'Something.operator';
+      mapping.rules.push(mapRule);
+      const baseComparator = resource.elements.find(e => e.id === 'Duration.comparator');
+      const originalMappingLength = baseComparator.mapping.length;
+      exporter.export();
+      expect(baseComparator.mapping).toHaveLength(originalMappingLength + 1);
+      const exported = baseComparator.mapping.slice(-1)[0];
+      expect(exported.identity).toBe('MyMapping');
+      expect(exported.map).toBe('Something.operator');
+    });
+
     it('should log an error and skip rules with paths that cannot be found', () => {
       /**
        * Mapping: MyMapping