Skip to content

Commit

Permalink
Process ValueSet that uses components from a contained system (#275)
Browse files Browse the repository at this point in the history
When a ValueSet component references a contained system, the
valueset-system extension should be present at compose.system.extension.
When it is present, it is safe to throw it out, since SUSHI will add it
back in automatically. When it is removed, also remove the extension
list and containing underscore property if those become empty. This will
often allow the input to be processed as a ValueSet.
  • Loading branch information
mint-thompson authored Nov 13, 2024
1 parent 7011bfc commit e6b0082
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 4 deletions.
27 changes: 25 additions & 2 deletions src/processor/ValueSetProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { utils, fhirtypes, fshtypes } from 'fsh-sushi';
import { capitalize, compact, difference } from 'lodash';
import { capitalize, compact, difference, isEmpty } from 'lodash';
import { flatten } from 'flat';
import { ExportableValueSet } from '../exportable';
import {
Expand All @@ -20,6 +20,8 @@ const SUPPORTED_COMPONENT_PATHS = [
'valueSet'
];

const VALUESET_SYSTEM_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/valueset-system';

export class ValueSetProcessor {
static extractKeywords(input: ProcessableValueSet, target: ExportableValueSet): void {
if (input.id) {
Expand Down Expand Up @@ -97,6 +99,10 @@ export class ValueSetProcessor {
// For example, if there is no name or id we cannot process it. In addition, if compose.include
// or compose.exclude have extensions, or concepts have designations, etc., then we can't
// represent it in FSH ValueSet syntax. It must be represented using Instance instead.
// One extension is allowed, though: the valueset-system extension, which may be present on
// component.include.system or component.exclude.system. This extension is present when the system
// refers to a contained CodeSystem. SUSHI adds it automatically, so we can safely ignore it here
// and SUSHI will handle it correctly.
// NOTE: by FHIR spec, if the include list exists, it must contain at least one element
// but we can still do some processing without that as long as other criteria holds.
// See http://hl7.org/fhir/r4/valueset-definitions.html#ValueSet.compose.include
Expand All @@ -107,7 +113,24 @@ export class ValueSetProcessor {
// We support all higher-level paths via caret rules. We only need to worry about the
// input.compose.include and input.compose.exclude components because there is no easy way
// to associate caret rules with them when the special FSH include/exclude syntax is used.
// First get the flat paths of input.compose.include and input.compose.exclude
// First thing to do is to remove the VALUESET_SYSTEM_EXTENSION. If it's the only extension,
// we can remove the extension array, and if _system has no properties left, we can remove it, too.
[...(input.compose?.include ?? []), ...(input.compose?.exclude ?? [])].forEach(
(component: any) => {
if (component._system?.extension) {
component._system.extension = component._system.extension.filter(
(ext: any) => ext.url !== VALUESET_SYSTEM_EXTENSION
);
if (component._system.extension.length === 0) {
delete component._system.extension;
}
if (isEmpty(component._system)) {
delete component._system;
}
}
}
);
// Second, get the flat paths of input.compose.include and input.compose.exclude
let flatPaths = Object.keys(
flatten([...(input.compose?.include ?? []), ...(input.compose?.exclude ?? [])])
);
Expand Down
30 changes: 28 additions & 2 deletions test/processor/ValueSetProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,17 @@ describe('ValueSetProcessor', () => {
expect(result).toBeUndefined();
});

it('should not convert a ValueSet with a compose.include.system extension', () => {
it('should not convert a ValueSet with an arbitrary compose.include.system extension', () => {
const input = JSON.parse(
fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8')
);
input.compose.include[0]._system = {
extension: {}
extension: [
{
url: 'http://example.org/SomeExtension',
valueString: 'arbitrary'
}
]
};
const result = ValueSetProcessor.process(input, defs, config);
expect(result).toBeUndefined();
Expand All @@ -125,6 +130,27 @@ describe('ValueSetProcessor', () => {
const result = ValueSetProcessor.process(input, defs, config);
expect(result.rules.length).toBeGreaterThan(0);
});

it('should process a valueset with components from a contained code system and using the valueset-system extension', () => {
const input = JSON.parse(
fs.readFileSync(
path.join(__dirname, 'fixtures', 'valueset-with-contained-codesystem.json'),
'utf-8'
)
);
const workingValueSet = ValueSetProcessor.process(input, defs, config);

const rules = workingValueSet.rules;
const expectedInclusion = new ExportableValueSetFilterComponentRule(true);
expectedInclusion.from = { system: 'http://example.org/codesystem' };
const expectedExclusion = new ExportableValueSetConceptComponentRule(false);
expectedExclusion.from = { system: 'http://example.org/codesystem' };
expectedExclusion.concepts.push(
new FshCode('example-code-2', 'http://example.org/codesystem')
);
expect(rules).toContainEqual(expectedInclusion);
expect(rules).toContainEqual(expectedExclusion);
});
});

describe('#extractKeywords', () => {
Expand Down
65 changes: 65 additions & 0 deletions test/processor/fixtures/valueset-with-contained-codesystem.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"resourceType": "ValueSet",
"id": "example-valueset",
"name": "ValueSetWithContainedSystem",
"contained": [
{
"resourceType": "CodeSystem",
"id": "example-codesystem",
"url": "http://example.org/codesystem",
"version": "1.0.0",
"content": "complete",
"status": "active",
"concept": [
{
"code": "example-code-1",
"display": "Example Code 1"
},
{
"code": "example-code-2",
"display": "Example Code 2"
},
{
"code": "example-code-3",
"display": "Example Code 3"
}
]
}
],
"url": "http://example.org/valueset",
"version": "1.0.0",
"status": "active",
"compose": {
"include": [
{
"system": "http://example.org/codesystem",
"_system": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/valueset-system",
"valueCanonical": "#example-codesystem"
}
]
}
}
],
"exclude": [
{
"system": "http://example.org/codesystem",
"_system": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/valueset-system",
"valueCanonical": "#example-codesystem"
}
]
},
"concept": [
{
"code": "example-code-2"
}
]
}
]
}
}

0 comments on commit e6b0082

Please sign in to comment.