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 committed Jan 9, 2024
2 parents 235bba9 + 225d702 commit ef075d6
Show file tree
Hide file tree
Showing 22 changed files with 592 additions and 474 deletions.
506 changes: 100 additions & 406 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"eslint": "^8.5.0",
"eslint-config-prettier": "^6.10.1",
"jest": "^28.1.3",
"jest-extended": "^1.2.0",
"jest-extended": "^3.0.2",
"opener": "^1.5.1",
"prettier": "^2.0.2",
"ts-jest": "^28.0.7",
Expand All @@ -76,7 +76,7 @@
"fhir-package-loader": "^0.5.0",
"flat": "^5.0.2",
"fs-extra": "^9.0.1",
"fsh-sushi": "^3.5.0",
"fsh-sushi": "^3.6.0",
"ini": "^1.3.8",
"lodash": "^4.17.21",
"readline-sync": "^1.4.10",
Expand Down
13 changes: 7 additions & 6 deletions src/extractor/CaretValueRuleExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,9 @@ export class CaretValueRuleExtractor {

static processConcept(
input: fhirtypes.CodeSystemConcept,
conceptHierarchy: string[],
codeSystemName: string,
pathArray: string[],
entityName: string,
entityType: 'CodeSystem' | 'ValueSet',
fisher: utils.Fishable
): ExportableCaretValueRule[] {
const caretValueRules: ExportableCaretValueRule[] = [];
Expand All @@ -273,12 +274,12 @@ export class CaretValueRuleExtractor {
caretValueRule.caretPath = key;
caretValueRule.value = getFSHValue(i, flatArray, 'Concept', fisher);
caretValueRule.isCodeCaretRule = true;
caretValueRule.pathArray = conceptHierarchy;
caretValueRule.pathArray = [...pathArray];
if (isFSHValueEmpty(caretValueRule.value)) {
logger.error(
`Value in CodeSytem ${codeSystemName} at concept ${conceptHierarchy.join(
'.'
)} for element ${caretValueRule.caretPath} is empty. No caret value rule will be created.`
`Value in ${entityType} ${entityName} at concept ${pathArray.join('.')} for element ${
caretValueRule.caretPath
} is empty. No caret value rule will be created.`
);
} else {
caretValueRules.push(caretValueRule);
Expand Down
29 changes: 29 additions & 0 deletions src/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { utils } from 'fsh-sushi';
import { OptimizerPlugin } from '../OptimizerPlugin';
import { optimizeURL } from '../utils';
import { Package } from '../../processor';
import { MasterFisher, ProcessingOptions } from '../../utils';
import { ExportableCaretValueRule } from '../../exportable';

export default {
name: 'resolve_value_set_caret_rule_urls',
description: 'Replace URLs in value set caret rules with their names or aliases',
runAfter: ['resolve_value_set_component_rule_urls'],
optimize(pkg: Package, fisher: MasterFisher, options: ProcessingOptions = {}): void {
pkg.valueSets.forEach(vs => {
vs.rules.forEach(rule => {
if (rule instanceof ExportableCaretValueRule && rule.pathArray.length > 0) {
const [system, ...code] = rule.pathArray[0].split('#');
const resolvedSystem = optimizeURL(
system,
pkg.aliases,
[utils.Type.CodeSystem],
fisher,
options.alias ?? true
);
rule.pathArray[0] = [resolvedSystem, code.join('#')].join('#');
}
});
});
}
} as OptimizerPlugin;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

export default {
name: 'resolve_value_set_component_rule_urls',
description: 'Replace URLs in value set rules with their names or aliases',
description: 'Replace URLs in value set component rules with their names or aliases',

optimize(pkg: Package, fisher: MasterFisher, options: ProcessingOptions = {}): void {
pkg.valueSets.forEach(vs => {
Expand Down
71 changes: 71 additions & 0 deletions src/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { isEmpty, isEqual } from 'lodash';
import { ExportableCaretValueRule, ExportableValueSetConceptComponentRule } from '../../exportable';
import { Package } from '../../processor';
import { OptimizerPlugin } from '../OptimizerPlugin';

// a ValueSetConceptComponentRule will print as multiple consecutive rules
// if there is a system, but no valuesets.
// normally, this is fine, but if more than one of those concepts has caret rules,
// split them manually so that the caret rules appear immediately after the concept. for example:
// * #BEAR from system http://example.org/zoo
// * #BEAR ^designation.value = "ourse"
// * #BEAR ^designation.language = #fr
// * #PEL from system http://example.org/zoo
// * #PEL ^designation.value = "pelícano"
// * #PEL ^designation.language = #es
export default {
name: 'separate_concepts_with_caret_rules',
description: 'Separate concepts in ValueSets from the same system if they also have caret rules.',
runBefore: ['resolve_value_set_component_rule_urls'],
optimize(pkg: Package): void {
pkg.valueSets.forEach(vs => {
const systemRulesToCheck = vs.rules.filter(rule => {
return (
rule instanceof ExportableValueSetConceptComponentRule &&
rule.from.system != null &&
isEmpty(rule.from.valueSets) &&
rule.concepts.length > 1
);
}) as ExportableValueSetConceptComponentRule[];
const allCodeCaretRules = vs.rules.filter(rule => {
return rule instanceof ExportableCaretValueRule && rule.pathArray.length > 0;
}) as ExportableCaretValueRule[];
if (allCodeCaretRules.length > 0) {
systemRulesToCheck.forEach(conceptRule => {
// for each concept in the rule, see if there are any caret value rules.
const caretRulesForSystem = new Map<string, ExportableCaretValueRule[]>();
conceptRule.concepts.forEach(concept => {
caretRulesForSystem.set(
concept.code,
allCodeCaretRules.filter(caretRule =>
isEqual(caretRule.pathArray, [`${conceptRule.from.system ?? ''}#${concept.code}`])
)
);
});
if (caretRulesForSystem.size > 1) {
// split apart the codes so that the ones with caret rules can be next to their concept rule
const reorganizedRules: (
| ExportableValueSetConceptComponentRule
| ExportableCaretValueRule
)[] = [];
for (const concept of conceptRule.concepts) {
const singleConceptRule = new ExportableValueSetConceptComponentRule(
conceptRule.inclusion
);
singleConceptRule.from.system = conceptRule.from.system;
singleConceptRule.concepts = [concept];
// don't need to copy indent since it will always be 0
reorganizedRules.push(singleConceptRule);
for (const caretRule of caretRulesForSystem.get(concept.code)) {
reorganizedRules.push(caretRule);
vs.rules.splice(vs.rules.indexOf(caretRule), 1);
}
}
const originalConceptRuleIndex = vs.rules.indexOf(conceptRule);
vs.rules.splice(originalConceptRuleIndex, 1, ...reorganizedRules);
}
});
}
});
}
} as OptimizerPlugin;
4 changes: 4 additions & 0 deletions src/optimizer/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import ResolveOnlyRuleURLsOptimizer from './ResolveOnlyRuleURLsOptimizer';
import ResolveParentURLsOptimizer from './ResolveParentURLsOptimizer';
import ResolveReferenceAssignmentsOptimizer from './ResolveReferenceAssignmentsOptimizer';
import ResolveValueRuleURLsOptimizer from './ResolveValueRuleURLsOptimizer';
import ResolveValueSetCaretRuleURLsOptimizer from './ResolveValueSetCaretRuleURLsOptimizer';
import ResolveValueSetComponentRuleURLsOptimizer from './ResolveValueSetComponentRuleURLsOptimizer';
import SeparateConceptsWithCaretRulesOptimizer from './SeparateConceptsWithCaretRulesOptimizer';
import SimplifyArrayIndexingOptimizer from './SimplifyArrayIndexingOptimizer';
import SimplifyInstanceNameOptimizer from './SimplifyInstanceNameOptimizer';
import SimplifyMappingNamesOptimizer from './SimplifyMappingNamesOptimizer';
Expand Down Expand Up @@ -52,7 +54,9 @@ export {
ResolveParentURLsOptimizer,
ResolveReferenceAssignmentsOptimizer,
ResolveValueRuleURLsOptimizer,
ResolveValueSetCaretRuleURLsOptimizer,
ResolveValueSetComponentRuleURLsOptimizer,
SeparateConceptsWithCaretRulesOptimizer,
SimplifyArrayIndexingOptimizer,
SimplifyInstanceNameOptimizer,
SimplifyMappingNamesOptimizer,
Expand Down
3 changes: 2 additions & 1 deletion src/processor/CodeSystemProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ export class CodeSystemProcessor {
newConceptRule,
...CaretValueRuleExtractor.processConcept(
concept,
[...newConceptRule.hierarchy, concept.code],
[...newConceptRule.hierarchy, concept.code].map(code => `#${code}`),
codeSystemName,
'CodeSystem',
fisher
)
);
Expand Down
28 changes: 24 additions & 4 deletions src/processor/ValueSetProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ const SUPPORTED_COMPONENT_PATHS = [
'system',
'version',
'concept',
'concept.code',
'concept.display',
'filter',
'filter.property',
'filter.op',
Expand Down Expand Up @@ -46,13 +44,33 @@ export class ValueSetProcessor {
...CaretValueRuleExtractor.processResource(input, fisher, input.resourceType, config)
);
if (input.compose) {
input.compose.include?.forEach((vsComponent: any) => {
input.compose.include?.forEach((vsComponent: fhirtypes.ValueSetComposeIncludeOrExclude) => {
newRules.push(ValueSetFilterComponentRuleExtractor.process(vsComponent, input, true));
newRules.push(ValueSetConceptComponentRuleExtractor.process(vsComponent, true));
vsComponent.concept?.forEach(includedConcept => {
const conceptCaretRules = CaretValueRuleExtractor.processConcept(
includedConcept,
[`${vsComponent.system ?? ''}#${includedConcept.code}`],
target.name,
'ValueSet',
fisher
);
newRules.push(...conceptCaretRules);
});
});
input.compose.exclude?.forEach((vsComponent: any) => {
input.compose.exclude?.forEach((vsComponent: fhirtypes.ValueSetComposeIncludeOrExclude) => {
newRules.push(ValueSetFilterComponentRuleExtractor.process(vsComponent, input, false));
newRules.push(ValueSetConceptComponentRuleExtractor.process(vsComponent, false));
vsComponent.concept?.forEach(excludedConcept => {
const conceptCaretRules = CaretValueRuleExtractor.processConcept(
excludedConcept,
[`${vsComponent.system ?? ''}#${excludedConcept.code}`],
target.name,
'ValueSet',
fisher
);
newRules.push(...conceptCaretRules);
});
});
}
target.rules = compact(newRules);
Expand Down Expand Up @@ -100,6 +118,8 @@ export class ValueSetProcessor {
.filter(k => isNaN(parseInt(k)))
.join('.');
});
// any path that starts with "concept." is okay, since those can use code caret rules
flatPaths = flatPaths.filter(p => !p.startsWith('concept.'));
// Check if there are any paths that are not a supported path
return difference(flatPaths, SUPPORTED_COMPONENT_PATHS).length === 0;
}
Expand Down
25 changes: 25 additions & 0 deletions test/exportable/ExportableCaretValueRule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ describe('ExportableCaretValueRule', () => {
expect(rule.toFSH()).toBe('* . ^short = "Another important summary."');
});

it('should export a code caret rule with a code path', () => {
// this type of rule appears on CodeSystems
const rule = new ExportableCaretValueRule('');
rule.isCodeCaretRule = true;
rule.caretPath = 'designation.value';
rule.pathArray = ['#bear', '#brown bear'];
rule.value = 'Brown Bear';

expect(rule.toFSH()).toBe('* #bear #"brown bear" ^designation.value = "Brown Bear"');
});

it('should export a code caret rule with a code and system path', () => {
// this type of rule appears on ValueSets
const rule = new ExportableCaretValueRule('');
rule.isCodeCaretRule = true;
rule.caretPath = 'designation.value';
rule.pathArray = ['http://example.org/zoo#brown bear'];
// rule.fromSystem = 'http://example.org/zoo';
rule.value = 'Brown Bear';

expect(rule.toFSH()).toBe(
'* http://example.org/zoo#"brown bear" ^designation.value = "Brown Bear"'
);
});

it('should export a caret rule assigning a boolean', () => {
const rule = new ExportableCaretValueRule('');
rule.caretPath = 'abstract';
Expand Down
2 changes: 1 addition & 1 deletion test/exportable/ExportableConceptRule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('ExportableConceptRule', () => {
rule.display = 'bar';
rule.definition = 'baz';

const expectedResult = '* #"foo\twith\ta\ttab" "bar" "baz"';
const expectedResult = '* #"foo\\twith\\ta\\ttab" "bar" "baz"';
const result = rule.toFSH();
expect(result).toBe(expectedResult);
});
Expand Down
12 changes: 6 additions & 6 deletions test/exportable/ExportableInvariant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ describe('ExportableInvariant', () => {
const expectedResult = [
'Invariant: inv-2',
'Description: "This is an important condition."',
'Severity: #error',
'Expression: "requirement.exists()"',
'XPath: "f:requirement"'
'* severity = #error',
'* expression = "requirement.exists()"',
'* xpath = "f:requirement"'
].join(EOL);
const result = input.toFSH();
expect(result).toBe(expectedResult);
Expand All @@ -39,9 +39,9 @@ describe('ExportableInvariant', () => {
const expectedResult = [
'Invariant: inv-3',
'Description: """Please do this.\nPlease always do this with a \\ character."""',
'Severity: #warning',
'Expression: "requirement.contains(\\"\\\\\\")"',
'XPath: "f:requirement"'
'* severity = #warning',
'* expression = "requirement.contains(\\"\\\\\\")"',
'* xpath = "f:requirement"'
].join(EOL);
const result = input.toFSH();
expect(result).toBe(expectedResult);
Expand Down
39 changes: 6 additions & 33 deletions test/exportable/ExportableValueSetComponentRule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('ExportableValueSetConceptComponentRule', () => {
rule.concepts.push(new fshtypes.FshCode('foo', 'bar'));
rule.from.valueSets = ['someValueSet'];

expect(rule.toFSH()).toBe('* include bar#foo from valueset someValueSet');
expect(rule.toFSH()).toBe('* bar#foo from valueset someValueSet');
});

it('should export a ValueSetConceptComponentRule with a concept excluded from a valueset', () => {
Expand All @@ -76,7 +76,7 @@ describe('ExportableValueSetConceptComponentRule', () => {
rule.concepts.push(new fshtypes.FshCode('foo', 'bar'));
rule.from.valueSets = ['someValueSet', 'otherValueSet'];

expect(rule.toFSH()).toBe('* include bar#foo from valueset someValueSet and otherValueSet');
expect(rule.toFSH()).toBe('* bar#foo from valueset someValueSet and otherValueSet');
});

it('should export a ValueSetConceptComponentRule with a concept excluded from several valuesets', () => {
Expand All @@ -89,23 +89,21 @@ describe('ExportableValueSetConceptComponentRule', () => {

it('should export a ValueSetConceptComponentRule with a concept included from a system and several valuesets', () => {
const rule = new ExportableValueSetConceptComponentRule(true);
rule.concepts.push(new fshtypes.FshCode('foo'));
rule.concepts.push(new fshtypes.FshCode('foo', 'someSystem'));
rule.from.system = 'someSystem';
rule.from.valueSets = ['someValueSet', 'otherValueSet'];

expect(rule.toFSH()).toBe(
'* include #foo from system someSystem and valueset someValueSet and otherValueSet'
);
expect(rule.toFSH()).toBe('* someSystem#foo from valueset someValueSet and otherValueSet');
});

it('should export a ValueSetConceptComponentRule with a concept excluded from a system and several valuesets', () => {
const rule = new ExportableValueSetConceptComponentRule(false);
rule.concepts.push(new fshtypes.FshCode('foo'));
rule.concepts.push(new fshtypes.FshCode('foo', 'someSystem'));
rule.from.system = 'someSystem';
rule.from.valueSets = ['someValueSet', 'otherValueSet'];

expect(rule.toFSH()).toBe(
'* exclude #foo from system someSystem and valueset someValueSet and otherValueSet'
'* exclude someSystem#foo from valueset someValueSet and otherValueSet'
);
});
});
Expand Down Expand Up @@ -273,31 +271,6 @@ describe('ExportableValueSetFilterComponentRule', () => {
);
});

it('should format a long ValueSetConceptComponentRule to take up multiple lines', () => {
const rule = new ExportableValueSetConceptComponentRule(true);
rule.concepts = [
new FshCode('cookies', undefined, 'Cookies'),
new FshCode('candy', undefined, 'Candy'),
new FshCode('chips', undefined, 'Chips'),
new FshCode('cakes', undefined, 'Cakes'),
new FshCode('verylargecakes', undefined, 'Very Large Cakes')
];
rule.from.system = 'http://fhir.food-pyramid.org/FoodPyramidGuide/CodeSystems/FoodGroupsCS';
rule.from.valueSets = ['http://fhir.food-pyramid.org/FoodPyramidGuide/ValueSets/DeliciousVS'];

const result = rule.toFSH();
const expectedResult = [
'* include #cookies "Cookies" and',
' #candy "Candy" and',
' #chips "Chips" and',
' #cakes "Cakes" and',
' #verylargecakes "Very Large Cakes"',
' from system http://fhir.food-pyramid.org/FoodPyramidGuide/CodeSystems/FoodGroupsCS and',
' valueset http://fhir.food-pyramid.org/FoodPyramidGuide/ValueSets/DeliciousVS'
].join(EOL);
expect(result).toEqual(expectedResult);
});

it('should format a long ValueSetFilterComponentRule to take up multiple lines', () => {
const rule = new ExportableValueSetFilterComponentRule(false);
rule.from.system = 'http://fhir.example.org/myImplementationGuide/CodeSystem/AppleCS';
Expand Down
Loading

0 comments on commit ef075d6

Please sign in to comment.