Repository: FHIR/sushi (ts)

Issue:
Sushi doesn't like primitives with extensions but no value as a valid instance of the primitive when counting required quantities
This example demonstrates the problem:

Profile: MeasureReportProfile
Parent: MeasureReport
* group.population.count 1..1
* group.population.code 1..1

Instance: SampleMeasureReport
InstanceOf: MeasureReportProfile
* status = #complete
* type = #summary
* measure = "fake"
* period.start = "2020-04-26"
* period.end = "2020-04-26"
* group[0].population.count = 1
* group[0].population.code.coding = http://terminology.hl7.org/CodeSystem/measure-population#measure-population

// The following doesn't get counted as an instance of count, I suspect because in json, this is recorded
// in the _count property since count is a primitive.
* group[0].population[1].count.extension.url = "http://hl7.org/fhir/StructureDefinition/data-absent-reason"
* group[0].population[1].count.extension.valueCode = http://terminology.hl7.org/CodeSystem/data-absent-reason#unsupported

// This, on the other hand, does work, but NOTE that code is CodeableConcept, and so doesn't do the _code weirdness
* group[0].population[1].code.extension.url = "http://hl7.org/fhir/StructureDefinition/data-absent-reason"
* group[0].population[1].code.extension.valueCode = http://terminology.hl7.org/CodeSystem/data-absent-reason#unsupported

If you run just this file, it will barf with:
error Element MeasureReport.group.population.count has minimum cardinality 1 but occurs 0 time(s).
  File: C:\Users\kboone\eclipse-workspace\test\fsh\MissingIntegerTest.fsh
  Line: 6 - 19

But clearly that isn't true.  There really is a count.  Unfortunately, sushi doesn't see it b/c the extension is stored in _count, not in count is a primitive, and primitives have to work another way because "reasons".




Pull request description:
Fix checking required children of primitives on instances
Fixes #393.

When validating that all required properties are present on an instance. SUSHI was incorrectly emitting an error saying that children of primitives with min card > 1 were not present on an instance, when those elements were in fact present. This PR fixes that bug by correctly accounting for elements prefixed with a `_` on an instance, since children of a primitive element are placed under a `_` prefixed version of the element.

Commit https://github.com/FHIR/sushi/commit/abbba0f7abf9757d1441f14e8d0f46b95343e318 adds four tests, three of which will fail if you check out this commit and run the tests. One of the tests does pass, but I wanted to keep it in for completeness sake. Then on https://github.com/FHIR/sushi/commit/882bb2039ea659a6979712a95c104d426175bbb1, the tests will pass.

Another way to test is to use the examples referenced in the issue. If you run SUSHI on master against those examples, you will see errors indicating that required elements are not present, but then in the instance that is exported, you will see that those elements are correctly present. On this branch, the extraneous errors will go away, but the exported instances will remain the same.

Patch:
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index c452856..6568ef7 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -189,7 +189,9 @@ export class InstanceExporter implements Fishable {
     children.forEach(child => {
       // Get the last part of the path, A.B.C => C
       const childPathEnd = child.path.split('.').slice(-1)[0];
-      let instanceChild = instance[childPathEnd];
+      // Note that in most cases the _ prefixed element will not exist. But if it does exist, we validate
+      // using the _ prefixed element, since it will contain any children of the primitive
+      let instanceChild = instance[`_${childPathEnd}`] ?? instance[childPathEnd];
       // If the element is a choice, we will fail to find it, we need to use the choice name
       if (instanceChild == null && childPathEnd.endsWith('[x]')) {
         const choiceSlices = children.filter(c => c.path === child.path && c.sliceName);


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 52d18a1..3357386 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -137,6 +137,7 @@ describe('InstanceExporter', () => {
     let respRateInstance: Instance;
     let bundleInstance: Instance;
     beforeEach(() => {
+      loggerSpy.reset();
       patient = new Profile('TestPatient');
       patient.parent = 'Patient';
       doc.profiles.set(patient.name, patient);
@@ -381,7 +382,6 @@ describe('InstanceExporter', () => {
     });
 
     it('should fix an element to a value the same as the fixed value on the Structure Definition', () => {
-      loggerSpy.reset();
       const fixedValRule = new FixedValueRule('active');
       fixedValRule.fixedValue = true;
       fixedValRule.exactly = true;
@@ -395,7 +395,6 @@ describe('InstanceExporter', () => {
     });
 
     it('should fix an element to a value the same as the fixed pattern on the Structure Definition', () => {
-      loggerSpy.reset();
       const fixedValRule = new FixedValueRule('maritalStatus');
       const fixedFshCode = new FshCode('foo', 'http://foo.com');
       fixedValRule.fixedValue = fixedFshCode;
@@ -561,7 +560,6 @@ describe('InstanceExporter', () => {
     });
 
     it('should not get confused by matching path parts when fixing deeply nested elements', () => {
-      loggerSpy.reset();
       // * maritalStatus, maritalStatus.coding, maritalStatus.coding.system 1..1
       // * maritalStatus.coding.system = "http://itscomplicated.com"
       const statCard = new CardRule('maritalStatus');
@@ -1405,6 +1403,85 @@ describe('InstanceExporter', () => {
       );
     });
 
+    it('should log an error when a required primitive child element is not present', () => {
+      const cardRule1 = new CardRule('active.id');
+      cardRule1.min = 1;
+      cardRule1.max = '1';
+      const cardRule2 = new CardRule('active');
+      cardRule2.min = 1;
+      cardRule2.max = '1';
+      // * active.id 1..1
+      // * active 1..1
+      patient.rules.push(cardRule1, cardRule2);
+      const activeRule = new FixedValueRule('active');
+      activeRule.fixedValue = true;
+      // * active = true
+      patientInstance.rules.push(activeRule);
+      exportInstance(patientInstance);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Patient.active.id has minimum cardinality 1 but occurs 0 time\(s\).*File: PatientInstance\.fsh.*Line: 10 - 20/s
+      );
+    });
+
+    it('should not log an error when a required primitive child element is present', () => {
+      const cardRule1 = new CardRule('active.id');
+      cardRule1.min = 1;
+      cardRule1.max = '1';
+      const cardRule2 = new CardRule('active');
+      cardRule2.min = 1;
+      cardRule2.max = '1';
+      // * active.id 1..1
+      // * active 1..1
+      patient.rules.push(cardRule1, cardRule2);
+      const idRule = new FixedValueRule('active.id');
+      idRule.fixedValue = 'foo';
+      // * active.id = "foo"
+      patientInstance.rules.push(idRule);
+      exportInstance(patientInstance);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
+    it('should log an error when a required primitive child array is not large enough', () => {
+      const cardRule1 = new CardRule('active.extension');
+      cardRule1.min = 2;
+      cardRule1.max = '*';
+      const cardRule2 = new CardRule('active');
+      cardRule2.min = 1;
+      cardRule2.max = '1';
+      // * active.extension 2..*
+      // * active 1..1
+      patient.rules.push(cardRule1, cardRule2);
+      const activeRule = new FixedValueRule('active');
+      activeRule.fixedValue = true;
+      const extensionRule = new FixedValueRule('active.extension.url');
+      extensionRule.fixedValue = 'http://example.com';
+      // * active = true
+      // * active.extension.url = "http://example.com"
+      patientInstance.rules.push(activeRule, extensionRule);
+      exportInstance(patientInstance);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Patient.active.extension has minimum cardinality 2 but occurs 1 time\(s\).*File: PatientInstance\.fsh.*Line: 10 - 20/s
+      );
+    });
+
+    it('should not log an error when a required primitive child array is large enough', () => {
+      const cardRule1 = new CardRule('active.extension');
+      cardRule1.min = 1;
+      cardRule1.max = '*';
+      const cardRule2 = new CardRule('active');
+      cardRule2.min = 1;
+      cardRule2.max = '1';
+      // * active.extension 1..*
+      // * active 1..1
+      patient.rules.push(cardRule1, cardRule2);
+      const idRule = new FixedValueRule('active.extension.url');
+      idRule.fixedValue = 'http://example.com';
+      // * active.extension.url = "http://example.com"
+      patientInstance.rules.push(idRule);
+      exportInstance(patientInstance);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
     it('should only export an instance once', () => {
       const bundleInstance = new Instance('MyBundle');
       bundleInstance.instanceOf = 'Bundle';