Skip to content

Commit

Permalink
Merge branch 'master' into cimpl-1167-characteristics-keyword
Browse files Browse the repository at this point in the history
  • Loading branch information
mint-thompson authored Jan 12, 2024
2 parents ef075d6 + a8eee62 commit 557d7a2
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 27 deletions.
4 changes: 3 additions & 1 deletion src/exportable/ExportableInvariant.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
63 changes: 50 additions & 13 deletions src/extractor/InvariantExtractor.ts
Original file line number Diff line number Diff line change
@@ -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)) {
Expand Down
13 changes: 10 additions & 3 deletions src/processor/StructureDefinitionProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,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);
Expand Down Expand Up @@ -259,12 +260,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;
Expand Down
31 changes: 30 additions & 1 deletion test/exportable/ExportableInvariant.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EOL } from 'os';
import { fshtypes } from 'fsh-sushi';
import { ExportableInvariant } from '../../src/exportable';
import { ExportableAssignmentRule, ExportableInvariant } from '../../src/exportable';

describe('ExportableInvariant', () => {
it('should export the simplest invariant', () => {
Expand Down Expand Up @@ -46,4 +46,33 @@ describe('ExportableInvariant', () => {
const result = input.toFSH();
expect(result).toBe(expectedResult);
});

it('should produce FSH for an invariant with additional rules', () => {
const input = new ExportableInvariant('inv-4');
input.description = 'This is an important condition.';
input.severity = new fshtypes.FshCode('error');
input.expression = 'requirement.exists()';
input.xpath = 'f:requirement';

const requirements = new ExportableAssignmentRule('requirements');
requirements.value = 'This is necessary because it is important.';
const extensionUrl = new ExportableAssignmentRule('human.extension[0].url');
extensionUrl.value = 'http://example.org/SomeExtension';
const extensionValue = new ExportableAssignmentRule('human.extension[0].valueString');
extensionValue.value = 'ExtensionValue';
input.rules.push(requirements, extensionUrl, extensionValue);

const expectedResult = [
'Invariant: inv-4',
'Description: "This is an important condition."',
'* severity = #error',
'* expression = "requirement.exists()"',
'* xpath = "f:requirement"',
'* requirements = "This is necessary because it is important."',
'* human.extension[0].url = "http://example.org/SomeExtension"',
'* human.extension[0].valueString = "ExtensionValue"'
].join(EOL);
const result = input.toFSH();
expect(result).toBe(expectedResult);
});
});
37 changes: 30 additions & 7 deletions test/extractor/InvariantExtractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@ 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()
);
});

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.';
Expand All @@ -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.';
Expand All @@ -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');
Expand All @@ -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);
});
Expand All @@ -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');
Expand All @@ -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);
});
Expand Down
15 changes: 15 additions & 0 deletions test/extractor/fixtures/obeys-profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
},
Expand Down
4 changes: 2 additions & 2 deletions test/processor/StructureDefinitionProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,9 +763,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);
});
});

Expand Down

0 comments on commit 557d7a2

Please sign in to comment.