Repository: FHIR/sushi
Pull request #849 (2021-06-18 12:44:43)

Constraints on Resource and Logical Model's own elements now allowed
Addresses [CIMPL-752](https://standardhealthrecord.atlassian.net/browse/CIMPL-752), allowing Logical Models and Resources to add constraints to elements introduced by themselves but still disallowing constraints on elements introduced by its parent. Can be tested by running [this FSH](https://fshschool.org/FSHOnline/#/share/3gBZEOP). 

julianxcarter: Commit `47cda16` adds in your suggestions, and also adds two unit tests to test that constraints on newly added elements are allowed on Logical Models and Resources. @cmoesel 
julianxcarter: Commit `16c06a2` adds the suggested test cases that @mint-thompson suggested. I've also updated this branch to be in line with master so both of your reviews have been dismissed, however everything should still be essentially the same! @cmoesel 

Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 43772dd..b349db6 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -385,6 +385,7 @@ export class StructureDefinitionExporter implements Fishable {
     fshDefinition: Profile | Extension | Logical | Resource
   ): void {
     resolveSoftIndexing(fshDefinition.rules);
+    const addElementRules = fshDefinition.rules.filter(rule => rule instanceof AddElementRule);
     for (const rule of fshDefinition.rules) {
       // Specific rules are permitted for each structure definition type
       // (i.e., Profile, Logical, etc.). Log an error for disallowed rules
@@ -414,9 +415,15 @@ export class StructureDefinitionExporter implements Fishable {
         // The FHIR spec prohibits constraining any parent element in a 'specialization'
         // (i.e., logical model and resource), therefore log an error if that is attempted
         // and continue to the next rule.
-        if (element.path !== element.base.path) {
-          // The AddElementRule always sets the newElementBase.path to the value of element.path.
-          // All parent elements will have the element.base.path pointing to the parent.
+        if (
+          rule.path &&
+          rule.path !== '.' &&
+          !addElementRules.some(
+            rule =>
+              element.path === `${element.structDef.pathType}.${rule.path}` ||
+              element.path.startsWith(`${element.structDef.pathType}.${rule.path}.`)
+          )
+        ) {
           logger.error(
             `FHIR prohibits logical models and resources from constraining parent elements. Skipping '${rule.constructorName}' at path '${rule.path}' for '${fshDefinition.name}'.`,
             rule.sourceInfo


Test patch:
diff --git a/test/export/StructureDefinition.LogicalExporter.test.ts b/test/export/StructureDefinition.LogicalExporter.test.ts
index dfea61c..9f203ed 100644
--- a/test/export/StructureDefinition.LogicalExporter.test.ts
+++ b/test/export/StructureDefinition.LogicalExporter.test.ts
@@ -6,7 +6,13 @@ import { Logical } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { TestFisher } from '../testhelpers';
 import { minimalConfig } from '../utils/minimalConfig';
-import { AddElementRule, CardRule, ContainsRule, FlagRule } from '../../src/fshtypes/rules';
+import {
+  AddElementRule,
+  CardRule,
+  CaretValueRule,
+  ContainsRule,
+  FlagRule
+} from '../../src/fshtypes/rules';
 
 describe('LogicalExporter', () => {
   let defs: FHIRDefinitions;
@@ -476,6 +482,49 @@ describe('LogicalExporter', () => {
     );
   });
 
+  it('should allow constraints on newly added elements and sub-elements', () => {
+    const logical = new Logical('ExampleModel');
+    logical.id = 'ExampleModel';
+
+    const addElementRule = new AddElementRule('name');
+    addElementRule.min = 0;
+    addElementRule.max = '*';
+    addElementRule.types = [{ type: 'HumanName' }];
+    addElementRule.short = "A person's full name";
+    logical.rules.push(addElementRule);
+
+    const topLevelCardRule = new CardRule('name');
+    topLevelCardRule.min = 1;
+    topLevelCardRule.max = '1';
+    logical.rules.push(topLevelCardRule);
+
+    const subElementCardRule = new CardRule('name.given');
+    subElementCardRule.min = 1;
+    subElementCardRule.max = '1';
+    logical.rules.push(subElementCardRule);
+
+    doc.logicals.set(logical.name, logical);
+    exporter.export();
+    const logs = loggerSpy.getAllMessages('error');
+    expect(logs).toHaveLength(0);
+  });
+
+  it('should allow constraints on root elements', () => {
+    const logical = new Logical('ExampleModel');
+    logical.id = 'ExampleModel';
+
+    const rootElementRule = new CaretValueRule('.');
+    rootElementRule.caretPath = 'alias';
+    rootElementRule.value = 'ExampleAlias';
+
+    logical.rules.push(rootElementRule);
+
+    doc.logicals.set(logical.name, logical);
+    exporter.export();
+    const logs = loggerSpy.getAllMessages('error');
+    expect(logs).toHaveLength(0);
+  });
+
   it('should log an error when constraining a parent element', () => {
     const logical = new Logical('MyTestModel');
     logical.parent = 'AlternateIdentification';
diff --git a/test/export/StructureDefinition.ResourceExporter.test.ts b/test/export/StructureDefinition.ResourceExporter.test.ts
index c545e02..8869461 100644
--- a/test/export/StructureDefinition.ResourceExporter.test.ts
+++ b/test/export/StructureDefinition.ResourceExporter.test.ts
@@ -6,7 +6,13 @@ import { Resource } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { TestFisher } from '../testhelpers';
 import { minimalConfig } from '../utils/minimalConfig';
-import { AddElementRule, CardRule, ContainsRule, FlagRule } from '../../src/fshtypes/rules';
+import {
+  AddElementRule,
+  CardRule,
+  CaretValueRule,
+  ContainsRule,
+  FlagRule
+} from '../../src/fshtypes/rules';
 
 describe('ResourceExporter', () => {
   let defs: FHIRDefinitions;
@@ -164,6 +170,49 @@ describe('ResourceExporter', () => {
     );
   });
 
+  it('should allow constraints on newly added elements and sub-elements', () => {
+    const resource = new Resource('ExampleResource');
+    resource.id = 'ExampleResource';
+
+    const addElementRule = new AddElementRule('name');
+    addElementRule.min = 0;
+    addElementRule.max = '*';
+    addElementRule.types = [{ type: 'HumanName' }];
+    addElementRule.short = "A person's full name";
+    resource.rules.push(addElementRule);
+
+    const topLevelCardRule = new CardRule('name');
+    topLevelCardRule.min = 1;
+    topLevelCardRule.max = '1';
+    resource.rules.push(topLevelCardRule);
+
+    const subElementCardRule = new CardRule('name.given');
+    subElementCardRule.min = 1;
+    subElementCardRule.max = '1';
+    resource.rules.push(subElementCardRule);
+
+    doc.resources.set(resource.name, resource);
+    exporter.export();
+    const logs = loggerSpy.getAllMessages('error');
+    expect(logs).toHaveLength(0);
+  });
+
+  it('should allow constraints on root elements', () => {
+    const resource = new Resource('ExampleResource');
+    resource.id = 'ExampleResource';
+
+    const rootElementRule = new CaretValueRule('.');
+    rootElementRule.caretPath = 'alias';
+    rootElementRule.value = 'ExampleAlias';
+
+    resource.rules.push(rootElementRule);
+
+    doc.resources.set(resource.name, resource);
+    exporter.export();
+    const logs = loggerSpy.getAllMessages('error');
+    expect(logs).toHaveLength(0);
+  });
+
   it('should log an error when constraining a parent element', () => {
     const resource = new Resource('MyTestResource');
     // Parent defaults to DomainResource