Repository: FHIR/sushi (ts)

Issue:
Invariant names are not validated as ids
The invariant name, so `myInvariant` in the snippet below:
```
Invariant: myInvariant
```
is used to fill the `constraint.key` field on a StructureDefinition. The `constraint.key` field is of type `id`, which has a special regex pattern that is must conform to https://www.hl7.org/fhir/datatypes.html#id. For things like Instances or Profiles, we check that `id`s correctly conform to this pattern, but we don't check that this pattern is matched for an Invariant. So we should check that the name of an Invariant correctly conforms to this pattern, and log an error if it doesn't.

Pull request:
Log error when invariant has invalid name
Fixes #290 and completes task https://standardhealthrecord.atlassian.net/browse/CIMPL-311.

The constraint keys on an element are required to be valid FHIR ids. These keys come from the name of the invariant, so show an error message when they don't match the FHIR id regex. The invariant will
still be added to the list of constraints.

I know during standup I said that I was originally planning to not add the invalid id to the list of constraints, but I was convinced otherwise in the spirit of "SUSHI should do as much as it can."

Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index c17dfd1..baf43bf 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -2,11 +2,16 @@ import { upperFirst, words } from 'lodash';
 import {
   StructureDefinition,
   ElementDefinition,
-  ElementDefinitionBindingStrength
+  ElementDefinitionBindingStrength,
+  idRegex
 } from '../fhirtypes';
 import { Profile, Extension, Invariant } from '../fshtypes';
 import { FSHTank } from '../import';
-import { ParentNotDefinedError, ParentDeclaredAsProfileNameError } from '../errors';
+import {
+  ParentNotDefinedError,
+  ParentDeclaredAsProfileNameError,
+  InvalidFHIRIdError
+} from '../errors';
 import {
   CardRule,
   FixedValueRule,
@@ -168,6 +173,9 @@ export class StructureDefinitionExporter implements Fishable {
               );
             } else {
               element.applyConstraint(invariant, structDef.url);
+              if (!idRegex.test(invariant.name)) {
+                throw new InvalidFHIRIdError(invariant.name);
+              }
             }
           }
         } catch (e) {


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 93d2b5c..f94c35e 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -2069,6 +2069,36 @@ describe('StructureDefinitionExporter', () => {
     expect(loggerSpy.getAllLogs('error')).toHaveLength(1);
   });
 
+  it('should log an error when applying an ObeysRule on an invariant with an invalid id', () => {
+    // Profile: StrangeObservation
+    // Parent: Observation
+    // * category obeys strange_1
+    // Invariant: strange_1
+    // Severity: #error
+    // Description: "This is a strange one."
+
+    const profile = new Profile('StrangeObservation');
+    profile.parent = 'Observation';
+    doc.profiles.set(profile.name, profile);
+
+    const invariant = new Invariant('strange_1');
+    invariant.severity = new FshCode('error');
+    invariant.description = 'This is a strange one.';
+    doc.invariants.set(invariant.name, invariant);
+
+    const rule = new ObeysRule('category').withFile('StrangeOne.fsh').withLocation([3, 8, 3, 24]);
+    rule.invariant = 'strange_1';
+    profile.rules.push(rule);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const category = sd.findElement('Observation.category');
+    expect(category.constraint).toHaveLength(2);
+    expect(category.constraint[1].key).toBe(invariant.name);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/does not represent a valid FHIR id/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: StrangeOne\.fsh.*Line: 3\D*/s);
+  });
+
   // Extension preprocessing
   it('should zero out Extension.value[x] when Extension.extension is used', () => {
     const extension = new Extension('MyInferredComplexExtension');