From 1c78ec0c282698a25b0b01f2f423758da38a2f46 Mon Sep 17 00:00:00 2001 From: Mint Thompson Date: Tue, 21 Nov 2023 15:00:28 -0500 Subject: [PATCH] Add rules to Invariants When extracting an Invariant, create assignment rules for properties that are not represented using keywords. Use the same functions as the InstanceProcessor for this. Add the paths for these elements to the returned list of paths so that caret rules will not be created for these elements. --- src/exportable/ExportableInvariant.ts | 4 +- src/extractor/InvariantExtractor.ts | 63 +++++++++++++++---- src/processor/StructureDefinitionProcessor.ts | 13 +++- test/extractor/InvariantExtractor.test.ts | 37 ++++++++--- test/extractor/fixtures/obeys-profile.json | 15 +++++ .../StructureDefinitionProcessor.test.ts | 4 +- 6 files changed, 110 insertions(+), 26 deletions(-) diff --git a/src/exportable/ExportableInvariant.ts b/src/exportable/ExportableInvariant.ts index ef622501..ea417149 100644 --- a/src/exportable/ExportableInvariant.ts +++ b/src/exportable/ExportableInvariant.ts @@ -1,7 +1,9 @@ import { fshtypes } from 'fsh-sushi'; -import { Exportable } from '.'; +import { Exportable, ExportableAssignmentRule, ExportableInsertRule } from '.'; export class ExportableInvariant extends fshtypes.Invariant implements Exportable { + rules: (ExportableAssignmentRule | ExportableInsertRule)[]; + constructor(name: string) { super(name); } diff --git a/src/extractor/InvariantExtractor.ts b/src/extractor/InvariantExtractor.ts index 2f64597b..89788fc1 100644 --- a/src/extractor/InvariantExtractor.ts +++ b/src/extractor/InvariantExtractor.ts @@ -1,47 +1,84 @@ -import { fshtypes } from 'fsh-sushi'; -import { isEqual } from 'lodash'; +import { fshtypes, utils } from 'fsh-sushi'; +import { cloneDeep, isEqual, toPairs } from 'lodash'; import { ExportableInvariant } from '../exportable/ExportableInvariant'; -import { ProcessableElementDefinition, ProcessableStructureDefinition } from '../processor'; +import { + ProcessableElementDefinition, + ProcessableStructureDefinition, + switchQuantityRules +} from '../processor'; +import { getFSHValue, getPathValuePairs, isFSHValueEmpty } from '../utils'; +import { ExportableAssignmentRule } from '../exportable'; export class InvariantExtractor { static process( input: ProcessableElementDefinition, structDef: ProcessableStructureDefinition, - existingInvariants: ExportableInvariant[] + existingInvariants: ExportableInvariant[], + fisher: utils.Fishable ): ExportableInvariant[] { const invariants: ExportableInvariant[] = []; if (input.constraint?.length > 0) { input.constraint.forEach((constraint, i) => { + // clone the constraint so we can take it apart as we work with it + const workingConstraint = cloneDeep(constraint); const constraintPaths: string[] = []; // required: key, human, severity - const invariant = new ExportableInvariant(constraint.key); - invariant.description = constraint.human; - invariant.severity = new fshtypes.FshCode(constraint.severity); + const invariant = new ExportableInvariant(workingConstraint.key); + invariant.description = workingConstraint.human; + invariant.severity = new fshtypes.FshCode(workingConstraint.severity); constraintPaths.push( `constraint[${i}].key`, `constraint[${i}].human`, `constraint[${i}].severity` ); + delete workingConstraint.key; + delete workingConstraint.human; + delete workingConstraint.severity; // optional: expression, xpath - if (constraint.expression) { - invariant.expression = constraint.expression; + if (workingConstraint.expression) { + invariant.expression = workingConstraint.expression; constraintPaths.push(`constraint[${i}].expression`); + delete workingConstraint.expression; } - if (constraint.xpath) { - invariant.xpath = constraint.xpath; + if (workingConstraint.xpath) { + invariant.xpath = workingConstraint.xpath; constraintPaths.push(`constraint[${i}].xpath`); + delete workingConstraint.xpath; } // SUSHI autopopulates source to the current SD URL, so as long as it matches, mark that path as processed - if (constraint.source == null || constraint.source === structDef.url) { + if (workingConstraint.source == null || workingConstraint.source === structDef.url) { constraintPaths.push(`constraint[${i}].source`); + delete workingConstraint.source; } + // other properties are created with rules on the invariant + // since we're already inside the ElementDefinition, we have to manually prepend "constraint" to the path + // so that we can get the FSH value correctly. + // but, we want the original path for the rule itself. + const flatPropertyArray = toPairs( + getPathValuePairs(workingConstraint, x => `constraint.${x}`) + ); + flatPropertyArray.forEach(([path], propertyIdx) => { + const originalPath = path.replace('constraint.', ''); + const assignmentRule = new ExportableAssignmentRule(originalPath); + assignmentRule.value = getFSHValue( + propertyIdx, + flatPropertyArray, + 'ElementDefinition', + fisher + ); + if (!isFSHValueEmpty(assignmentRule.value)) { + invariant.rules.push(assignmentRule); + } + constraintPaths.push(`constraint[${i}].${originalPath}`); + }); + switchQuantityRules(invariant.rules); // if an invariant with this key already exists, don't make a new invariant with the same key. // if the new invariant would be an exact match of the existing invariant, mark the paths as // processed so an ObeysRule is created and no CaretValueRules are created. // if the new invariant has a key match but isn't an exact match, it will be created using CaretValueRules. const matchingKeyInvariant = [...existingInvariants, ...invariants].find( - inv => inv.name === constraint.key + inv => inv.name === invariant.name ); if (matchingKeyInvariant) { if (isEqual(matchingKeyInvariant, invariant)) { diff --git a/src/processor/StructureDefinitionProcessor.ts b/src/processor/StructureDefinitionProcessor.ts index 8941aba8..394a718e 100644 --- a/src/processor/StructureDefinitionProcessor.ts +++ b/src/processor/StructureDefinitionProcessor.ts @@ -73,7 +73,8 @@ export class StructureDefinitionProcessor { const invariants = StructureDefinitionProcessor.extractInvariants( input, elements, - existingInvariants + existingInvariants, + fisher ); const mappings = StructureDefinitionProcessor.extractMappings(elements, input, fisher); StructureDefinitionProcessor.extractRules(input, elements, sd, fisher, config); @@ -239,12 +240,18 @@ export class StructureDefinitionProcessor { static extractInvariants( input: ProcessableStructureDefinition, elements: ProcessableElementDefinition[], - existingInvariants: ExportableInvariant[] + existingInvariants: ExportableInvariant[], + fisher: utils.Fishable ): ExportableInvariant[] { const invariants: ExportableInvariant[] = []; elements.forEach(element => { invariants.push( - ...InvariantExtractor.process(element, input, [...existingInvariants, ...invariants]) + ...InvariantExtractor.process( + element, + input, + [...existingInvariants, ...invariants], + fisher + ) ); }); return invariants; diff --git a/test/extractor/InvariantExtractor.test.ts b/test/extractor/InvariantExtractor.test.ts index 17147816..4557cec3 100644 --- a/test/extractor/InvariantExtractor.test.ts +++ b/test/extractor/InvariantExtractor.test.ts @@ -2,14 +2,18 @@ import path from 'path'; import fs from 'fs-extra'; import { ProcessableElementDefinition, ProcessableStructureDefinition } from '../../src/processor'; import { InvariantExtractor } from '../../src/extractor'; -import { ExportableInvariant } from '../../src/exportable'; +import { ExportableAssignmentRule, ExportableInvariant } from '../../src/exportable'; import { fshtypes } from 'fsh-sushi'; +import { FHIRDefinitions } from '../../src/utils'; +import { loadTestDefinitions } from '../helpers/loadTestDefinitions'; const { FshCode } = fshtypes; describe('InvariantExtractor', () => { + let defs: FHIRDefinitions; let looseSD: ProcessableStructureDefinition; beforeAll(() => { + defs = loadTestDefinitions(); looseSD = JSON.parse( fs.readFileSync(path.join(__dirname, 'fixtures', 'obeys-profile.json'), 'utf-8').trim() ); @@ -17,7 +21,7 @@ describe('InvariantExtractor', () => { it('should extract invariants from an element with one constraint', () => { const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[0]); - const invariants = InvariantExtractor.process(element, looseSD, []); + const invariants = InvariantExtractor.process(element, looseSD, [], defs); const rootInvariant = new ExportableInvariant('zig-1'); rootInvariant.severity = new FshCode('warning'); rootInvariant.description = 'This is a constraint on the root element.'; @@ -32,7 +36,7 @@ describe('InvariantExtractor', () => { it('should extract invariants from an element with multiple constraints', () => { const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[1]); - const invariants = InvariantExtractor.process(element, looseSD, []); + const invariants = InvariantExtractor.process(element, looseSD, [], defs); const expressionInvariant = new ExportableInvariant('zig-2'); expressionInvariant.severity = new FshCode('error'); expressionInvariant.description = 'This constraint has an expression.'; @@ -46,11 +50,23 @@ describe('InvariantExtractor', () => { bothInvariant.description = 'This constraint has an expression and an xpath.'; bothInvariant.expression = 'category.double.exists()'; bothInvariant.xpath = 'f:category/double'; + const complexInvariant = new ExportableInvariant('zig-5'); + complexInvariant.severity = new FshCode('warning'); + complexInvariant.description = 'This constraint has some extra rules.'; + complexInvariant.expression = 'category.triple.exists()'; + const invExtensionUrl = new ExportableAssignmentRule('human.extension[0].url'); + invExtensionUrl.value = 'http://example.org/SomeExtension'; + const invExtensionValue = new ExportableAssignmentRule('human.extension[0].valueString'); + invExtensionValue.value = 'ExtensionValue'; + const invRequirements = new ExportableAssignmentRule('requirements'); + invRequirements.value = 'This is an additional requirement'; + complexInvariant.rules.push(invExtensionUrl, invExtensionValue, invRequirements); - expect(invariants).toHaveLength(3); + expect(invariants).toHaveLength(4); expect(invariants).toContainEqual(expressionInvariant); expect(invariants).toContainEqual(xpathInvariant); expect(invariants).toContainEqual(bothInvariant); + expect(invariants).toContainEqual(complexInvariant); expect(element.processedPaths).toContain('constraint[0].key'); expect(element.processedPaths).toContain('constraint[0].severity'); expect(element.processedPaths).toContain('constraint[0].human'); @@ -67,11 +83,18 @@ describe('InvariantExtractor', () => { expect(element.processedPaths).toContain('constraint[2].expression'); expect(element.processedPaths).toContain('constraint[2].xpath'); expect(element.processedPaths).toContain('constraint[2].source'); + expect(element.processedPaths).toContain('constraint[3].key'); + expect(element.processedPaths).toContain('constraint[3].severity'); + expect(element.processedPaths).toContain('constraint[3].human'); + expect(element.processedPaths).toContain('constraint[3].human.extension[0].url'); + expect(element.processedPaths).toContain('constraint[3].human.extension[0].valueString'); + expect(element.processedPaths).toContain('constraint[3].expression'); + expect(element.processedPaths).toContain('constraint[3].requirements'); }); it('should extract no invariants from an element with no constraints', () => { const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[2]); - const invariants = InvariantExtractor.process(element, looseSD, []); + const invariants = InvariantExtractor.process(element, looseSD, [], defs); expect(invariants).toHaveLength(0); expect(element.processedPaths).toHaveLength(0); }); @@ -81,7 +104,7 @@ describe('InvariantExtractor', () => { existingInvariant.severity = new FshCode('warning'); existingInvariant.description = 'This is a constraint on the root element.'; const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[0]); - const invariants = InvariantExtractor.process(element, looseSD, [existingInvariant]); + const invariants = InvariantExtractor.process(element, looseSD, [existingInvariant], defs); expect(invariants).toHaveLength(0); expect(element.processedPaths).toHaveLength(4); expect(element.processedPaths).toContainEqual('constraint[0].key'); @@ -93,7 +116,7 @@ describe('InvariantExtractor', () => { it('should not extract an invariant nor process paths if a non-equal invariant with a matching key already exists', () => { const existingInvariant = new ExportableInvariant('zig-1'); const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[0]); - const invariants = InvariantExtractor.process(element, looseSD, [existingInvariant]); + const invariants = InvariantExtractor.process(element, looseSD, [existingInvariant], defs); expect(invariants).toHaveLength(0); expect(element.processedPaths).toHaveLength(0); }); diff --git a/test/extractor/fixtures/obeys-profile.json b/test/extractor/fixtures/obeys-profile.json index 8fefe363..c01ce747 100644 --- a/test/extractor/fixtures/obeys-profile.json +++ b/test/extractor/fixtures/obeys-profile.json @@ -41,6 +41,21 @@ "human": "This constraint has an expression and an xpath.", "expression": "category.double.exists()", "xpath": "f:category/double" + }, + { + "key": "zig-5", + "severity": "warning", + "human": "This constraint has some extra rules.", + "_human": { + "extension": [ + { + "url": "http://example.org/SomeExtension", + "valueString": "ExtensionValue" + } + ] + }, + "expression": "category.triple.exists()", + "requirements": "This is an additional requirement" } ] }, diff --git a/test/processor/StructureDefinitionProcessor.test.ts b/test/processor/StructureDefinitionProcessor.test.ts index a64f923a..643381e2 100644 --- a/test/processor/StructureDefinitionProcessor.test.ts +++ b/test/processor/StructureDefinitionProcessor.test.ts @@ -743,9 +743,9 @@ describe('StructureDefinitionProcessor', () => { input.differential?.element?.map(rawElement => { return ProcessableElementDefinition.fromJSON(rawElement, false); }) ?? []; - const result = StructureDefinitionProcessor.extractInvariants(input, elements, []); + const result = StructureDefinitionProcessor.extractInvariants(input, elements, [], defs); - expect(result).toHaveLength(4); + expect(result).toHaveLength(5); }); });