Repository: FHIR/sushi (ts)

Issue:
SUSHI should provide more helpful error message when authors try to bind to a Code System
Attempting to bind a property to a Code System is a common enough mistake, that SUSHI should detect this and provide a more helpful error message.  For example, see: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Is.20not.20a.20valid.20URI

Similarly, there are cases where authors might try to use a Value Set where a Code System is needed (for example `MyVS#mycode`).  We should also consider more helpful error messages here.

Pull request:
More helpful error message on incorrect bindings
Fixes #538 and addressed [CIMPL-470](https://standardhealthrecord.atlassian.net/browse/CIMPL-470). This PR makes it so that sushi outputs a more helpful error message when a user attempts to bind a property to a Code System rather than a Value Set. This more helpful error message is also output when a user attempts to assign the system on a coding to a Value Set rather than a Code System. 

The existing error messages that are output can be viewed by [running this example FSH on FshOnline](https://fshschool.org/FSHOnline/#/share/3cZW2nT). When run on this branch, you should get two error messages that let you know if you need a CS or a VS on that line.

Patch:
diff --git a/src/errors/MismatchedBindingTypeError.ts b/src/errors/MismatchedBindingTypeError.ts
new file mode 100644
index 0000000..a0c5e91
--- /dev/null
+++ b/src/errors/MismatchedBindingTypeError.ts
@@ -0,0 +1,9 @@
+export class MismatchedBindingTypeError extends Error {
+  constructor(public id: string, public path: string, public correctedType: string) {
+    super(
+      `Cannot ${
+        correctedType === 'CodeSystem' ? 'assign' : 'bind'
+      } ${id} at path ${path}. A ${correctedType} must be used.`
+    );
+  }
+}
diff --git a/src/errors/index.ts b/src/errors/index.ts
index aa3c1ff..f2806cb 100644
--- a/src/errors/index.ts
+++ b/src/errors/index.ts
@@ -46,3 +46,4 @@ export * from './InvalidTypeAccessError';
 export * from './NonAbstractParentOfSpecializationError';
 export * from './ValueConflictsWithClosedSlicingError';
 export * from './AssignmentToCodeableReferenceError';
+export * from './MismatchedBindingTypeError';
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 3f6aec9..764377c 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -14,7 +14,8 @@ import {
   InvalidFHIRIdError,
   InvalidExtensionParentError,
   ParentDeclaredAsProfileIdError,
-  DuplicateSliceError
+  DuplicateSliceError,
+  MismatchedBindingTypeError
 } from '../errors';
 import {
   CardRule,
@@ -36,6 +37,7 @@ import {
   getUrlFromFshDefinition
 } from '../fhirtypes/common';
 import { Package } from './Package';
+import { isUri } from 'valid-url';
 
 // Extensions that should not be inherited by derived profiles
 // See: https://jira.hl7.org/browse/FHIR-27535
@@ -205,6 +207,10 @@ export class StructureDefinitionExporter implements Fishable {
             element.constrainType(rule, this, target);
           } else if (rule instanceof BindingRule) {
             const vsURI = this.fishForMetadata(rule.valueSet, Type.ValueSet)?.url ?? rule.valueSet;
+            const csURI = this.fishForMetadata(rule.valueSet, Type.CodeSystem)?.url;
+            if (csURI && !isUri(vsURI)) {
+              throw new MismatchedBindingTypeError(rule.valueSet, rule.path, 'ValueSet');
+            }
             element.bindToVS(vsURI, rule.strength as ElementDefinitionBindingStrength);
           } else if (rule instanceof ContainsRule) {
             const isExtension =
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index f63a333..c2a6c35 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -40,7 +40,8 @@ import {
   DuplicateSliceError,
   NonAbstractParentOfSpecializationError,
   ValueConflictsWithClosedSlicingError,
-  AssignmentToCodeableReferenceError
+  AssignmentToCodeableReferenceError,
+  MismatchedBindingTypeError
 } from '../errors';
 import { setPropertyOnDefinitionInstance, splitOnPathPeriods, isReferenceType } from './common';
 import { Fishable, Type, Metadata, logger } from '../utils';
@@ -1644,8 +1645,14 @@ export class ElementDefinition {
    * @throws {InvalidUriError} when the system being assigned is not a valid uri
    */
   private assignFshCode(code: FshCode, exactly = false, fisher?: Fishable): void {
-    if (code.system && !isUri(code.system.split('|')[0])) {
-      throw new InvalidUriError(code.system);
+    if (code.system) {
+      const csURI = code.system.split('|')[0];
+      const vsURI = fisher?.fishForMetadata(code.system, Type.ValueSet)?.url ?? '';
+      if (vsURI) {
+        throw new MismatchedBindingTypeError(code.system, this.path, 'CodeSystem');
+      } else if (!isUri(csURI)) {
+        throw new InvalidUriError(code.system);
+      }
     }
 
     const type = this.type[0].code;


Test patch:
diff --git a/test/export/StructureDefinition.ProfileExporter.test.ts b/test/export/StructureDefinition.ProfileExporter.test.ts
index 3ef0349..79792c0 100644
--- a/test/export/StructureDefinition.ProfileExporter.test.ts
+++ b/test/export/StructureDefinition.ProfileExporter.test.ts
@@ -6,7 +6,7 @@ import { Profile, Instance } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { TestFisher } from '../testhelpers';
 import { minimalConfig } from '../utils/minimalConfig';
-import { CaretValueRule, ContainsRule } from '../../src/fshtypes/rules';
+import { BindingRule, CaretValueRule, ContainsRule } from '../../src/fshtypes/rules';
 
 describe('ProfileExporter', () => {
   let defs: FHIRDefinitions;
@@ -158,6 +158,19 @@ describe('ProfileExporter', () => {
     expect(exporter.deferredRules.get(exported[0])).toContainEqual(caretValueRule);
   });
 
+  it('should throw a MismatchedBindingTypeError when a code property is bound to a code system', () => {
+    const profile = new Profile('TestProfile');
+    profile.parent = 'Patient';
+    const bindingRule = new BindingRule('identifier.type');
+    bindingRule.valueSet = 'W3cProvenanceActivityType';
+    bindingRule.strength = 'required';
+    profile.rules.push(bindingRule);
+    doc.profiles.set(profile.name, profile);
+    exporter.export();
+
+    expect(loggerSpy.getLastMessage('error')).toMatch(/A ValueSet must be used./);
+  });
+
   it('should log an error when an inline extension is used', () => {
     const profile = new Profile('MyObservation');
     profile.parent = 'Observation';
diff --git a/test/fhirtypes/ElementDefinition.assignFshCode.test.ts b/test/fhirtypes/ElementDefinition.assignFshCode.test.ts
index 06d146a..da4290e 100644
--- a/test/fhirtypes/ElementDefinition.assignFshCode.test.ts
+++ b/test/fhirtypes/ElementDefinition.assignFshCode.test.ts
@@ -182,6 +182,25 @@ describe('ElementDefinition', () => {
       }).toThrow(/notAUri/);
     });
 
+    it('should throw MismatchedBindingTypeError when binding with a ValueSet as a system', () => {
+      const category = observation.elements.find(e => e.id === 'Observation.category');
+      const clone = cloneDeep(category);
+      expect(() => {
+        clone.assignValue(
+          new FshCode('code', 'http://hl7.org/fhir/ValueSet/observation-category'),
+          false,
+          fisher
+        );
+      }).toThrow(/CodeSystem/);
+      expect(() => {
+        clone.assignValue(
+          new FshCode('code', 'http://hl7.org/fhir/ValueSet/observation-category'),
+          true,
+          fisher
+        );
+      }).toThrow(/CodeSystem/);
+    });
+
     it('should assign a code to a Coding', () => {
       const concept = observation.elements.find(e => e.id === 'Observation.code');
       concept.unfold(fisher);
diff --git a/test/testhelpers/testdefs/package/ValueSet-observation-category.json b/test/testhelpers/testdefs/package/ValueSet-observation-category.json
new file mode 100644
index 0000000..0e8425c
--- /dev/null
+++ b/test/testhelpers/testdefs/package/ValueSet-observation-category.json
@@ -0,0 +1,1 @@
+{"resourceType":"ValueSet","id":"observation-category","meta":{"lastUpdated":"2019-11-01T09:29:23.356+11:00","profile":["http://hl7.org/fhir/StructureDefinition/shareablevalueset"]},"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-wg","valueCode":"oo"},{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status","valueCode":"draft"},{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm","valueInteger":1}],"url":"http://hl7.org/fhir/ValueSet/observation-category","identifier":[{"system":"urn:ietf:rfc:3986","value":"urn:oid:2.16.840.1.113883.4.642.3.403"}],"version":"4.0.1","name":"ObservationCategoryCodes","title":"Observation Category Codes","status":"draft","experimental":false,"date":"2019-11-01T09:29:23+11:00","publisher":"FHIR Project team","contact":[{"telecom":[{"system":"url","value":"http://hl7.org/fhir"}]}],"description":"Observation Category codes.","compose":{"include":[{"system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}}
\ No newline at end of file