Repository: FHIR/sushi
Pull request #715 (2021-01-06 15:37:39)

Allow ALIAS|VERSION
This fixes https://github.com/FHIR/sushi/issues/702 by allowing an alias to have a version appended to it. For example, the following:
```
Alias: MDR = http://terminology.hl7.org/CodeSystem/MDRAE
* event = MDR|v777#10002198 "Anaphylatic reaction"
```
should now work. The `MDR` alias will resolve to `http://terminology.hl7.org/CodeSystem/MDRAE`, and the final `system` set on `event` will be `http://terminology.hl7.org/CodeSystem/MDRAE|v777`.

This PR also makes it so that an alias cannot include a `|` character, since that character is now reserved for indicating a version.

ngfreiter: @cmoesel, also happy to have you review this if you want, but this time I am unable to add you as a reviewer.

Patch:
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index e51046b..4b7bba8 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -172,6 +172,13 @@ export class FSHImporter extends FSHVisitor {
             .alias()
             .SEQUENCE()
             .map(s => s.getText());
+          if (name.includes('|')) {
+            logger.error(
+              `Alias ${name} cannot include "|" since the "|" character is reserved for indicating a version`,
+              { file: doc.file ?? '', location: this.extractStartStop(e.alias()) }
+            );
+            return;
+          }
           if (this.allAliases.has(name) && this.allAliases.get(name) !== value) {
             logger.error(
               `Alias ${name} cannot be redefined to ${value}; it is already defined as ${this.allAliases.get(
@@ -1868,8 +1875,13 @@ export class FSHImporter extends FSHVisitor {
   }
 
   private aliasAwareValue(parentCtx: ParserRuleContext, value = parentCtx.getText()): string {
-    this.validateAliasResolves(parentCtx, value);
-    return this.allAliases.has(value) ? this.allAliases.get(value) : value;
+    const [valueWithoutVersion, version] = value.split('|');
+    this.validateAliasResolves(parentCtx, valueWithoutVersion);
+    if (this.allAliases.has(valueWithoutVersion)) {
+      return this.allAliases.get(valueWithoutVersion) + (version ? `|${version}` : '');
+    } else {
+      return value;
+    }
   }
 
   private extractString(stringCtx: ParserRuleContext): string {


Test patch:
diff --git a/test/import/FSHImporter.Alias.test.ts b/test/import/FSHImporter.Alias.test.ts
index 9848a8d..a9077b4 100644
--- a/test/import/FSHImporter.Alias.test.ts
+++ b/test/import/FSHImporter.Alias.test.ts
@@ -1,5 +1,6 @@
 import { importText, RawFSH } from '../../src/import';
-import { BindingRule } from '../../src/fshtypes/rules';
+import { AssignmentRule, BindingRule } from '../../src/fshtypes/rules';
+import { FshCode } from '../../src/fshtypes';
 import { importSingleText } from '../testhelpers/importSingleText';
 import { loggerSpy } from '../testhelpers';
 
@@ -288,5 +289,62 @@ describe('FSHImporter', () => {
       expect(loggerSpy.getLastMessage('error')).toMatch(/\$LOINCZ.*\$/);
       expect(loggerSpy.getLastMessage('error')).toMatch(/File: Loinc.fsh.*Line: 5\D*/s);
     });
+
+    it('should resolve an alias while accounting for a version', () => {
+      const input = `
+      Alias: LOINC = http://loinc.org
+
+      Profile: ObservationProfile
+      Parent: Observation
+      * code = LOINC|123#foo
+      `;
+
+      const results = importText([new RawFSH(input, 'Loinc.fsh')]);
+      expect(results.length).toBe(1);
+      const rule = results[0].profiles.get('ObservationProfile').rules[0] as AssignmentRule;
+      expect(rule.value).toEqual(
+        new FshCode('foo', 'http://loinc.org|123')
+          .withFile('Loinc.fsh')
+          .withLocation([6, 16, 6, 28])
+      );
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
+    it('should resolve an alias while ignoring an empty version', () => {
+      const input = `
+      Alias: LOINC = http://loinc.org
+
+      Profile: ObservationProfile
+      Parent: Observation
+      * code = LOINC|#foo
+      `;
+
+      const results = importText([new RawFSH(input, 'Loinc.fsh')]);
+      expect(results.length).toBe(1);
+      const rule = results[0].profiles.get('ObservationProfile').rules[0] as AssignmentRule;
+      expect(rule.value).toEqual(
+        new FshCode('foo', 'http://loinc.org').withFile('Loinc.fsh').withLocation([6, 16, 6, 25])
+      );
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
+    it('should log an error when an alias contains reserved characters', () => {
+      const input = `
+      Alias: BAD|LOINC = http://loinc.org
+
+      Profile: ObservationProfile
+      Parent: Observation
+      * code = BAD|LOINC#foo
+      `;
+
+      const results = importText([new RawFSH(input, 'Loinc.fsh')]);
+      expect(results.length).toBe(1);
+      const rule = results[0].profiles.get('ObservationProfile').rules[0] as AssignmentRule;
+      // The BAD|LOINC alias does not resolve, since it is not created
+      expect(rule.value).toEqual(
+        new FshCode('foo', 'BAD|LOINC').withFile('Loinc.fsh').withLocation([6, 16, 6, 28])
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(/BAD\|LOINC cannot include "\|"/);
+    });
   });
 });