Repository: FHIR/sushi (ts)

Issue:
Sushi does not allow a StructureDefinition name or id = FHIR resource type
These  fail:
```
# Patient.fsh

Profile:        Patient
Parent:         Patient
Id:             kidsfirst-patient
Title:          "Kids First Patient"
Description:    "Kids First Patient Structure Definition."
```

```
# Patient.fsh

Profile:        KidsFirstPatient
Parent:         Patient
Id:             patient
Title:          "Kids First Patient"
Description:    "Kids First Patient Structure Definition."
```

![image](https://user-images.githubusercontent.com/10283712/73601312-730e4900-45b3-11ea-987b-c28a34751c57.png)

This works:
```
# Patient.fsh

Profile:        KidsFirstPatient
Parent:         Patient
Id:             kidsfirst-patient
Title:          "Kids First Patient"
Description:    "Kids First Patient Structure Definition."
```
![image](https://user-images.githubusercontent.com/10283712/73601317-9e913380-45b3-11ea-9ffc-2c2be5021bb6.png)

The error seems to be due to the StructureDefinition's `name` or `id` being one of the FHIR resource types (`Patient`). However, the FHIR spec for `StructureDefinition` does not indicate this is a violation of the spec. 


Pull request:
Throw error when a profile has the same id and parent
Fixes #175 and completes task [CIMPL-371](https://standardhealthrecord.atlassian.net/browse/CIMPL-371)

This is related to the problem of setting the same name and parent for a profile: it gets into an infinite loop of fishing for itself, since things can be fished up by id. When the id also corresponds to a FHIR resource, using the url is suggested, similarly to the same-name-and-parent case.

Patch:
diff --git a/src/errors/ParentDeclaredAsProfileIdError.ts b/src/errors/ParentDeclaredAsProfileIdError.ts
new file mode 100644
index 0000000..4a5f1e4
--- /dev/null
+++ b/src/errors/ParentDeclaredAsProfileIdError.ts
@@ -0,0 +1,19 @@
+import { WithSource } from './WithSource';
+import { SourceInfo } from '../fshtypes';
+
+export class ParentDeclaredAsProfileIdError extends Error implements WithSource {
+  constructor(
+    public name: string,
+    public id: string,
+    public sourceInfo: SourceInfo,
+    public fhirResourceUrl: string
+  ) {
+    super(
+      `Profile "${name}" cannot declare "${id}" as both Parent and Id.${
+        fhirResourceUrl
+          ? ` It looks like the parent is an external resource; use its URL (e.g., ${fhirResourceUrl}).`
+          : ''
+      }`
+    );
+  }
+}
diff --git a/src/errors/index.ts b/src/errors/index.ts
index 3bddbe0..296b7d9 100644
--- a/src/errors/index.ts
+++ b/src/errors/index.ts
@@ -38,6 +38,7 @@ export * from './MultipleStandardsStatusError';
 export * from './InvalidMappingError';
 export * from './InvalidFHIRIdError';
 export * from './ParentDeclaredAsProfileNameError';
+export * from './ParentDeclaredAsProfileIdError';
 export * from './InvalidResourceTypeError';
 export * from './FixingNonResourceError';
 export * from './InvalidExtensionParentError';
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 2255bfc..628bdbf 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -11,7 +11,8 @@ import {
   ParentNotDefinedError,
   ParentDeclaredAsProfileNameError,
   InvalidFHIRIdError,
-  InvalidExtensionParentError
+  InvalidExtensionParentError,
+  ParentDeclaredAsProfileIdError
 } from '../errors';
 import {
   CardRule,
@@ -426,6 +427,16 @@ export class StructureDefinitionExporter implements Fishable {
       );
     }
 
+    if (fshDefinition.id === parentName) {
+      const result = this.fishForMetadata(parentName, Type.Resource);
+      throw new ParentDeclaredAsProfileIdError(
+        fshDefinition.name,
+        fshDefinition.id,
+        fshDefinition.sourceInfo,
+        result?.url
+      );
+    }
+
     const json = this.fishForFHIR(parentName);
 
     // If we still don't have a resolution, then it's not defined


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 5efa26a..c0d1d98 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -192,6 +192,35 @@ describe('StructureDefinitionExporter', () => {
     );
   });
 
+  it('should throw ParentDeclaredAsProfileIdError when a profile sets the same value for parent and id', () => {
+    const parentProfile = new Profile('InitialProfile');
+    parentProfile.id = 'ParentProfile';
+    doc.profiles.set(parentProfile.name, parentProfile);
+
+    const childProfile = new Profile('OverlappingProfile');
+    childProfile.parent = 'InitialProfile';
+    childProfile.id = 'InitialProfile';
+    doc.profiles.set(childProfile.name, childProfile);
+
+    expect(() => {
+      exporter.exportStructDef(childProfile);
+    }).toThrow(
+      'Profile "OverlappingProfile" cannot declare "InitialProfile" as both Parent and Id.'
+    );
+  });
+
+  it('should throw ParentDeclaredAsProfileIdError and suggest resource URL when a profile sets the same value for parent and id and the parent is a FHIR resource', () => {
+    const profile = new Profile('KidsFirstPatient');
+    profile.parent = 'Patient';
+    profile.id = 'Patient';
+    doc.profiles.set(profile.name, profile);
+    expect(() => {
+      exporter.exportStructDef(profile);
+    }).toThrow(
+      'Profile "KidsFirstPatient" cannot declare "Patient" as both Parent and Id. It looks like the parent is an external resource; use its URL (e.g., http://hl7.org/fhir/StructureDefinition/Patient).'
+    );
+  });
+
   // Extension
   it('should set all user-provided metadata for an extension', () => {
     const extension = new Extension('Foo');