Repository: FHIR/sushi (ts)

Issue:
SUSHI is too strict when constraining profiled types
This is easiest to see via an example. If we have the following FSH:
```
Profile: MyDoc
Parent: ch-core-document
* entry[Composition].resource only MyComp

Profile: MyComp
Parent: http://fhir.ch/ig/ch-core/StructureDefinition/ch-core-composition
```
where the `Composition` slice is defined as:
```
   {
        "id" : "Bundle.entry:Composition.resource",
        "path" : "Bundle.entry.resource",
        "min" : 1,
        "type" : [
          {
            "code" : "Resource",
            "profile" : [
              "http://fhir.ch/ig/ch-core/StructureDefinition/ch-core-composition"
            ]
          }
        ]
      }
```
Then SUSHI does not allow the constraint on the `MyDoc` profile, giving the error:
```
The type "MyComp" does not match any of the allowed types: http://fhir.ch/ig/ch-core/StructureDefinition/ch-core-composition
```

This type constraint really should be allowed, because the `MyComp` profile is a sub-type of Resource, and it conforms to the desired profile. We are too strict in our type checking, since we require that `MyComp` is exactly the same type as the `type.code` (So in this case, changing that from `Resource` to `Composition` works), when we should probably just require that `MyComp` is a sub-type of the `type.code`.

Pull request description:
Loosen restrictions on constraining profiled types
Fixes #789 

If we have a situation like the one below:
```
Profile: MyBundle
Parent: Bundle
* entry.resource only http://hl7.org/fhir/StructureDefinition/resprate
* entry.resource ^type.code = "Resource"

Profile: MyResp
Parent: http://hl7.org/fhir/StructureDefinition/resprate

Profile: MyBundleChild
Parent: MyBundle
* entry.resource only MyResp
```
Then we get an error that looks like:
```
The type "MyResp" does not match any of the allowed types: http://hl7.org/fhir/StructureDefinition/resprate
```

This is because when we check if the `MyResp` profile can be applied as a type to `entry.resource`, we check that inherits from the current profile on `entry.resource`, and it does, since it is a child of `resprate`. But we also check that `MyResp`'s type exactly matches the `type.code` on the type constraint applied to `entry.resource`. That is not the case here, because `MyResp` is of type `Observation`, and the `type.code` is `Resource`. But, since `Observation` is a child of `Resource`, this should really still be allowed. This PR fixes this issue so that this type constraint is now allowed.

To test this, you can use the example above on `master` and observer the error, and then see that on this branch the error goes away. You can also run the tests added in 3f8bf05 and see that one fails (the other test which passes is just an implementation of an old skipped test which we had no reason to skip anymore). Then on this branch, all tests should pass.

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 5147d7a..f63a333 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -847,11 +847,13 @@ export class ElementDefinition {
             (t2.targetProfile == null || t2.targetProfile.includes(md.url))
         );
       } else {
-        // Look for exact match on the code (w/ no profile) or a match on the same base type with
+        // Look for exact match on the code (w/ no profile) or a match on an allowed base type with
         // a matching profile
         matchedType = targetTypes.find(t2 => {
           const matchesUnprofiledResource = t2.code === md.id && isEmpty(t2.profile);
-          const matchesProfile = t2.code === md.sdType && t2.profile?.includes(md.url);
+          const matchesProfile =
+            this.getTypeLineage(md.sdType, fisher).some(ancestor => ancestor.sdType === t2.code) &&
+            t2.profile?.includes(md.url);
           // True if we match an unprofiled type that is not abstract, is a parent, and that we are
           // specializing (the type does not match the sdType of the type to match)
           specializationOfNonAbstractType =


Test patch:
diff --git a/test/fhirtypes/ElementDefinition.constrainType.test.ts b/test/fhirtypes/ElementDefinition.constrainType.test.ts
index 73356ce..5ef0993 100644
--- a/test/fhirtypes/ElementDefinition.constrainType.test.ts
+++ b/test/fhirtypes/ElementDefinition.constrainType.test.ts
@@ -4,10 +4,14 @@ import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions';
 import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition';
 import { ElementDefinitionType } from '../../src/fhirtypes';
 import { Type } from '../../src/utils';
-import cloneDeep from 'lodash/cloneDeep';
-import path from 'path';
 import { OnlyRule } from '../../src/fshtypes/rules';
 import { readFileSync } from 'fs-extra';
+import { Package, StructureDefinitionExporter } from '../../src/export';
+import { minimalConfig } from '../utils/minimalConfig';
+import { Profile } from '../../src/fshtypes';
+import { FSHTank } from '../../src/import';
+import cloneDeep from 'lodash/cloneDeep';
+import path from 'path';
 
 describe('ElementDefinition', () => {
   let defs: FHIRDefinitions;
@@ -15,6 +19,9 @@ describe('ElementDefinition', () => {
   let extension: StructureDefinition;
   let r5CarePlan: StructureDefinition;
   let fisher: TestFisher;
+  let exporter: StructureDefinitionExporter;
+  let pkg: Package;
+
   beforeAll(() => {
     defs = new FHIRDefinitions();
     loadFromPath(
@@ -27,7 +34,9 @@ describe('ElementDefinition', () => {
       'r5',
       defs
     );
-    fisher = new TestFisher().withFHIR(defs);
+    pkg = new Package(minimalConfig);
+    fisher = new TestFisher().withFHIR(defs).withPackage(pkg);
+    exporter = new StructureDefinitionExporter(new FSHTank([], minimalConfig), pkg, fisher);
   });
   beforeEach(() => {
     observation = fisher.fishForStructureDefinition('Observation');
@@ -134,10 +143,6 @@ describe('ElementDefinition', () => {
       );
     });
 
-    it.skip('should allow a profile to be constrained to a more specific profile', () => {
-      // Cannot find any examples to use for testing.  Will revisit when we can reference FSH profiles
-    });
-
     it('should allow Resource to be constrained to a resource', () => {
       const bundle = fisher.fishForStructureDefinition('Bundle');
       const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
@@ -162,6 +167,50 @@ describe('ElementDefinition', () => {
       );
     });
 
+    it('should allow a profile to be constrained to a more specific profile', () => {
+      const bundle = fisher.fishForStructureDefinition('Bundle');
+      const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
+      entryResource.type[0] = new ElementDefinitionType('Observation').withProfiles(
+        'http://hl7.org/fhir/StructureDefinition/bp'
+      );
+
+      const profile = new Profile('Foo');
+      profile.parent = 'http://hl7.org/fhir/StructureDefinition/bp';
+      exporter.exportStructDef(profile);
+
+      const profileConstraint = new OnlyRule('entry.resource');
+      profileConstraint.types = [{ type: 'Foo' }];
+      entryResource.constrainType(profileConstraint, fisher);
+      expect(entryResource.type).toHaveLength(1);
+      expect(entryResource.type[0]).toEqual(
+        new ElementDefinitionType('Observation').withProfiles(
+          'http://hl7.org/fhir/us/minimal/StructureDefinition/Foo'
+        )
+      );
+    });
+
+    it('should allow a profile to be constrained to a more specific profile of a child type', () => {
+      const bundle = fisher.fishForStructureDefinition('Bundle');
+      const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
+      entryResource.type[0] = new ElementDefinitionType('Resource').withProfiles(
+        'http://hl7.org/fhir/StructureDefinition/bp'
+      );
+
+      const profile = new Profile('Foo');
+      profile.parent = 'http://hl7.org/fhir/StructureDefinition/bp';
+      exporter.exportStructDef(profile);
+
+      const profileConstraint = new OnlyRule('entry.resource');
+      profileConstraint.types = [{ type: 'Foo' }];
+      entryResource.constrainType(profileConstraint, fisher);
+      expect(entryResource.type).toHaveLength(1);
+      expect(entryResource.type[0]).toEqual(
+        new ElementDefinitionType('Observation').withProfiles(
+          'http://hl7.org/fhir/us/minimal/StructureDefinition/Foo'
+        )
+      );
+    });
+
     it('should allow Resource to be constrained to multiple resources and profiles', () => {
       const bundle = fisher.fishForStructureDefinition('Bundle');
       const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');