Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support code caret rules in ValueSets #249

Merged
merged 5 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
462 changes: 77 additions & 385 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion 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 Down
5 changes: 3 additions & 2 deletions src/extractor/CaretValueRuleExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ export class CaretValueRuleExtractor {
static processConcept(
input: fhirtypes.CodeSystemConcept,
conceptHierarchy: string[],
codeSystemName: string,
entityName: string,
entityType: 'CodeSystem' | 'ValueSet',
fisher: utils.Fishable
): ExportableCaretValueRule[] {
const caretValueRules: ExportableCaretValueRule[] = [];
Expand All @@ -261,7 +262,7 @@ export class CaretValueRuleExtractor {
caretValueRule.pathArray = conceptHierarchy;
if (isFSHValueEmpty(caretValueRule.value)) {
logger.error(
`Value in CodeSytem ${codeSystemName} at concept ${conceptHierarchy.join(
`Value in ${entityType} ${entityName} at concept ${conceptHierarchy.join(
'.'
)} for element ${caretValueRule.caretPath} is empty. No caret value rule will be created.`
);
Expand Down
1 change: 1 addition & 0 deletions src/processor/CodeSystemProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class CodeSystemProcessor {
concept,
[...newConceptRule.hierarchy, concept.code],
codeSystemName,
'CodeSystem',
fisher
)
);
Expand Down
30 changes: 26 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,35 @@ 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 => {
newRules.push(
...CaretValueRuleExtractor.processConcept(
includedConcept,
[includedConcept.code],
target.name,
'ValueSet',
fisher
)
);
});
});
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 => {
newRules.push(
...CaretValueRuleExtractor.processConcept(
excludedConcept,
[excludedConcept.code],
target.name,
'ValueSet',
fisher
)
);
});
});
}
target.rules = compact(newRules);
Expand Down Expand Up @@ -100,6 +120,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
4 changes: 3 additions & 1 deletion test/extractor/CaretValueRuleExtractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ describe('CaretValueRuleExtractor', () => {
testConcept,
['testConcept'],
'testCS',
'CodeSystem',
defs
);
expect(caretRules).toContainEqual(
Expand Down Expand Up @@ -683,6 +684,7 @@ describe('CaretValueRuleExtractor', () => {
testConcept,
['testConcept'],
'testCS',
'CodeSystem',
defs
);

Expand All @@ -693,7 +695,7 @@ describe('CaretValueRuleExtractor', () => {
})
);
expect(loggerSpy.getLastMessage('error')).toMatch(
'Value in CodeSytem testCS at concept testConcept for element property[0] is empty. No caret value rule will be created.'
'Value in CodeSystem testCS at concept testConcept for element property[0] is empty. No caret value rule will be created.'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops! Good catch!

);
});
});
Expand Down
10 changes: 9 additions & 1 deletion test/optimizer/plugins/fixtures/unsupported-valueset.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@
"id": "unsupported.valueset",
"url": "http://example.org/tests/ValueSet/unsupported.valueset",
"title": "Unsupported ValueSet",
"description": "This value set is not supported by ValueSet FSH syntax because it has a concept designation.",
"description": "This value set is not supported by ValueSet FSH syntax because it has a version extension.",
"compose": {
"include": [
{
"system": "http://example.org/zoo",
"_version": {
"extension": [
{
"url": "http://example.org/SomeExtension",
"valueString": "version things"
}
]
},
"concept": [
{
"code": "BEAR",
Expand Down
72 changes: 68 additions & 4 deletions test/processor/ValueSetProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,26 @@ describe('ValueSetProcessor', () => {
);
});

it('should not convert a ValueSet with an included concept designation', () => {
it('should convert a ValueSet with an included concept designation', () => {
const input = JSON.parse(
fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8')
);
input.compose.include[0].concept[0].designation = {
value: 'ourse'
};
const result = ValueSetProcessor.process(input, defs, config);
expect(result).toBeUndefined();
expect(result).toBeDefined();
});

it('should not convert a ValueSet with an excluded concept designation', () => {
it('should convert a ValueSet with an excluded concept designation', () => {
const input = JSON.parse(
fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8')
);
input.compose.exclude[0].concept[0].designation = {
value: 'chatte'
};
const result = ValueSetProcessor.process(input, defs, config);
expect(result).toBeUndefined();
expect(result).toBeDefined();
});

it('should not convert a ValueSet with a compose.include id', () => {
Expand Down Expand Up @@ -235,5 +235,69 @@ describe('ValueSetProcessor', () => {
expect(targetValueSet.rules).toHaveLength(4);
expect(targetValueSet.rules).toContainEqual<ExportableCaretValueRule>(experimentalRule);
});

it('should add concept caret rules to a ValueSet', () => {
const input = JSON.parse(
fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8')
);
// add some designations
input.compose.include[0].concept[0].designation = {
value: 'ourse',
language: 'fr'
};
input.compose.exclude[0].concept[0].designation = {
value: 'chatte',
language: 'fr'
};
const workingValueSet = new ExportableValueSet('ComposedValueSet');
ValueSetProcessor.extractRules(input, workingValueSet, defs, config);
const rules = workingValueSet.rules;

const expectedIncludeDesignationValue = new ExportableCaretValueRule('');
expectedIncludeDesignationValue.isCodeCaretRule = true;
expectedIncludeDesignationValue.pathArray = ['BEAR'];
expectedIncludeDesignationValue.caretPath = 'designation.value';
expectedIncludeDesignationValue.value = 'ourse';
expect(rules).toContainEqual<ExportableCaretValueRule>(expectedIncludeDesignationValue);

const expectedIncludeDesignationLanguage = new ExportableCaretValueRule('');
expectedIncludeDesignationLanguage.isCodeCaretRule = true;
expectedIncludeDesignationLanguage.pathArray = ['BEAR'];
expectedIncludeDesignationLanguage.caretPath = 'designation.language';
expectedIncludeDesignationLanguage.value = new FshCode('fr');
expect(rules).toContainEqual<ExportableCaretValueRule>(expectedIncludeDesignationLanguage);

const expectedExcludeDesignationValue = new ExportableCaretValueRule('');
expectedExcludeDesignationValue.isCodeCaretRule = true;
expectedExcludeDesignationValue.pathArray = ['CAT'];
expectedExcludeDesignationValue.caretPath = 'designation.value';
expectedExcludeDesignationValue.value = 'chatte';
expect(rules).toContainEqual<ExportableCaretValueRule>(expectedExcludeDesignationValue);

const expectedExcludeDesignationLanguage = new ExportableCaretValueRule('');
expectedExcludeDesignationLanguage.isCodeCaretRule = true;
expectedExcludeDesignationLanguage.pathArray = ['CAT'];
expectedExcludeDesignationLanguage.caretPath = 'designation.language';
expectedExcludeDesignationLanguage.value = new FshCode('fr');
expect(rules).toContainEqual<ExportableCaretValueRule>(expectedExcludeDesignationLanguage);
});

it('should log an error and not add a concept caret rule when the rule value is missing', () => {
const input = JSON.parse(
fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8')
);
// add an empty designation
input.compose.include[0].concept[0].designation = {};

const workingValueSet = new ExportableValueSet('ComposedValueSet');
ValueSetProcessor.extractRules(input, workingValueSet, defs, config);
const rules = workingValueSet.rules;

expect(rules).not.toContainEqual(expect.objectContaining({ pathArray: ['BEAR'] }));

expect(loggerSpy.getLastMessage('error')).toEqual(
'Value in ValueSet ComposedValueSet at concept BEAR for element designation is empty. No caret value rule will be created.'
);
});
});
});
10 changes: 9 additions & 1 deletion test/processor/fixtures/unsupported-valueset-missing-id.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
"resourceType": "ValueSet",
"name": "UnsupportedValueSet",
"title": "Unsupported ValueSet",
"description": "This value set is not supported by ValueSet FSH syntax because it has a concept designation.",
"description": "This value set is not supported by ValueSet FSH syntax because it has a version extension.",
"compose": {
"include": [
{
"system": "http://example.org/zoo",
"_version": {
"extension": [
{
"url": "http://example.org/SomeExtension",
"valueString": "version things"
}
]
},
"concept": [
{
"code": "BEAR",
Expand Down
10 changes: 9 additions & 1 deletion test/processor/fixtures/unsupported-valueset.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@
"name": "UnsupportedValueSet",
"id": "unsupported.valueset",
"title": "Unsupported ValueSet",
"description": "This value set is not supported by ValueSet FSH syntax because it has a concept designation.",
"description": "This value set is not supported by ValueSet FSH syntax because it has a version extension.",
"compose": {
"include": [
{
"system": "http://example.org/zoo",
"_version": {
"extension": [
{
"url": "http://example.org/SomeExtension",
"valueString": "version things"
}
]
},
"concept": [
{
"code": "BEAR",
Expand Down