diff --git a/src/extractor/CaretValueRuleExtractor.ts b/src/extractor/CaretValueRuleExtractor.ts index 204f80be..443d51f7 100644 --- a/src/extractor/CaretValueRuleExtractor.ts +++ b/src/extractor/CaretValueRuleExtractor.ts @@ -1,6 +1,11 @@ import { fhirtypes, fshtypes, utils } from 'fsh-sushi'; import { cloneDeep, isEqual, differenceWith, toPairs } from 'lodash'; -import { ProcessableElementDefinition, ProcessableStructureDefinition } from '../processor'; +import { + LOGICAL_TARGET_EXTENSION, + ProcessableElementDefinition, + ProcessableStructureDefinition, + TYPE_CHARACTERISTICS_EXTENSION +} from '../processor'; import { ExportableCaretValueRule } from '../exportable'; import { getFSHValue, getPath, getPathValuePairs, logger, isFSHValueEmpty } from '../utils'; @@ -125,6 +130,16 @@ export class CaretValueRuleExtractor { delete parent.context; } + // if this is a Logical, ignore characteristic-setting extensions, since those are covered by a keyword + if (sd.kind === 'logical' && sd.derivation === 'specialization') { + sd.extension = sd.extension?.filter((ext: fhirtypes.Extension) => { + return !( + ext.url === TYPE_CHARACTERISTICS_EXTENSION || + (ext.url === LOGICAL_TARGET_EXTENSION && ext.valueBoolean === true) + ); + }); + } + // Remove properties that are covered by other extractors or keywords RESOURCE_IGNORED_PROPERTIES['StructureDefinition'].forEach(prop => { delete sd[prop]; diff --git a/src/processor/StructureDefinitionProcessor.ts b/src/processor/StructureDefinitionProcessor.ts index 394a718e..0370cafc 100644 --- a/src/processor/StructureDefinitionProcessor.ts +++ b/src/processor/StructureDefinitionProcessor.ts @@ -28,6 +28,11 @@ import { getAncestorSliceDefinition, getPath, logger } from '../utils'; import { fshifyString } from '../exportable/common'; import { isUri } from 'valid-url'; +export const TYPE_CHARACTERISTICS_EXTENSION = + 'http://hl7.org/fhir/StructureDefinition/structuredefinition-type-characteristics'; +export const LOGICAL_TARGET_EXTENSION = + 'http://hl7.org/fhir/tools/StructureDefinition/logical-target'; + export class StructureDefinitionProcessor { static process( input: any, @@ -144,6 +149,21 @@ export class StructureDefinitionProcessor { } }); } + if (target instanceof ExportableLogical && input.extension) { + // most characteristics use TYPE_CHARACTERISTICS_EXTENSION, + // but there may also be LOGICAL_TARGET_EXTENSION with valueBoolean true for the can-be-target characteristic. + const characteristics: string[] = []; + input.extension.forEach(ext => { + if (ext.url === TYPE_CHARACTERISTICS_EXTENSION && ext.valueCode != null) { + characteristics.push(ext.valueCode); + } else if (ext.url === LOGICAL_TARGET_EXTENSION && ext.valueBoolean === true) { + characteristics.push('can-be-target'); + } + }); + if (characteristics.length) { + target.characteristics = characteristics; + } + } } static extractRules( @@ -283,6 +303,7 @@ export interface ProcessableStructureDefinition { derivation?: string; mapping?: fhirtypes.StructureDefinitionMapping[]; context?: fhirtypes.StructureDefinitionContext[]; + extension?: fhirtypes.Extension[]; differential?: { element: any[]; }; diff --git a/test/extractor/CaretValueRuleExtractor.test.ts b/test/extractor/CaretValueRuleExtractor.test.ts index 25fa5986..afb5230e 100644 --- a/test/extractor/CaretValueRuleExtractor.test.ts +++ b/test/extractor/CaretValueRuleExtractor.test.ts @@ -16,6 +16,7 @@ describe('CaretValueRuleExtractor', () => { let looseBSSD: any; let looseTPESD: any; let looseExtSD: any; + let looseLogicalSD: any; let config: fshtypes.Configuration; let defs: FHIRDefinitions; @@ -54,6 +55,14 @@ describe('CaretValueRuleExtractor', () => { .readFileSync(path.join(__dirname, 'fixtures', 'extension-with-context.json'), 'utf-8') .trim() ); + looseLogicalSD = JSON.parse( + fs + .readFileSync( + path.join(__dirname, 'fixtures', 'logical-with-characteristics.json'), + 'utf-8' + ) + .trim() + ); }); beforeEach(() => { @@ -75,6 +84,33 @@ describe('CaretValueRuleExtractor', () => { expect(caretRules).toEqual([]); }); + it('should not extract any SD caret rules for characteristics on a Logical', () => { + const caretRules = CaretValueRuleExtractor.processStructureDefinition( + looseLogicalSD, + defs, + config + ); + // the only "extension" rules should be for the non-characteristic extension + expect(caretRules).toContainEqual( + expect.objectContaining({ + caretPath: expect.stringMatching(/^extension\[\d+\]\.url/), + value: 'http://hl7.org/fhir/sushi-test/StructureDefinition/other-extension' + }) + ); + expect(caretRules).not.toContainEqual( + expect.objectContaining({ + caretPath: expect.stringMatching(/^extension\[\d+\]\.url/), + value: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-type-characteristics' + }) + ); + expect(caretRules).not.toContainEqual( + expect.objectContaining({ + caretPath: expect.stringMatching(/^extension\[\d+\]\.url/), + value: 'http://hl7.org/fhir/tools/StructureDefinition/logical-target' + }) + ); + }); + it('should extract a url-setting caret rules when a non-standard url is included on a StructureDefinition', () => { const urlSD = cloneDeep(looseSD); urlSD.url = 'http://diferenturl.com'; diff --git a/test/extractor/fixtures/logical-with-characteristics.json b/test/extractor/fixtures/logical-with-characteristics.json new file mode 100644 index 00000000..069a6e98 --- /dev/null +++ b/test/extractor/fixtures/logical-with-characteristics.json @@ -0,0 +1,30 @@ +{ + "resourceType": "StructureDefinition", + "id": "logical-with-characteristics", + "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/logical-with-characteristics", + "kind": "logical", + "derivation": "specialization", + "type": "Base", + "name": "LogicalWithCharacteristics", + "title": "My Logical Model with Characteristics", + "description": "This is my fancy new logical model that has several characteristics.", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Base", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-type-characteristics", + "valueCode": "has-units" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-type-characteristics", + "valueCode": "has-length" + }, + { + "url": "http://hl7.org/fhir/tools/StructureDefinition/logical-target", + "valueBoolean": true + }, + { + "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/other-extension", + "valueString": "something else" + } + ] +} diff --git a/test/processor/StructureDefinitionProcessor.test.ts b/test/processor/StructureDefinitionProcessor.test.ts index 643381e2..6760442d 100644 --- a/test/processor/StructureDefinitionProcessor.test.ts +++ b/test/processor/StructureDefinitionProcessor.test.ts @@ -354,6 +354,27 @@ describe('StructureDefinitionProcessor', () => { expect(workingLogical.title).toBe('My Logical Model'); expect(workingLogical.description).toBe('This is my fancy new logical model.'); }); + + it('should get characteristics for a Logical with characteristics', () => { + const input = JSON.parse( + fs.readFileSync( + path.join(__dirname, 'fixtures', 'logical-with-characteristics.json'), + 'utf-8' + ) + ); + const workingLogical = new ExportableLogical(input.name); + StructureDefinitionProcessor.extractKeywords(input, workingLogical); + + expect(workingLogical.id).toBe('logical-with-characteristics'); + expect(workingLogical.title).toBe('My Logical Model with Characteristics'); + expect(workingLogical.description).toBe( + 'This is my fancy new logical model that has several characteristics.' + ); + expect(workingLogical.characteristics).toHaveLength(3); + expect(workingLogical.characteristics).toContain('can-be-target'); + expect(workingLogical.characteristics).toContain('has-units'); + expect(workingLogical.characteristics).toContain('has-length'); + }); }); describe('#extractRules', () => { diff --git a/test/processor/fixtures/logical-with-characteristics.json b/test/processor/fixtures/logical-with-characteristics.json new file mode 100644 index 00000000..069a6e98 --- /dev/null +++ b/test/processor/fixtures/logical-with-characteristics.json @@ -0,0 +1,30 @@ +{ + "resourceType": "StructureDefinition", + "id": "logical-with-characteristics", + "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/logical-with-characteristics", + "kind": "logical", + "derivation": "specialization", + "type": "Base", + "name": "LogicalWithCharacteristics", + "title": "My Logical Model with Characteristics", + "description": "This is my fancy new logical model that has several characteristics.", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Base", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-type-characteristics", + "valueCode": "has-units" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-type-characteristics", + "valueCode": "has-length" + }, + { + "url": "http://hl7.org/fhir/tools/StructureDefinition/logical-target", + "valueBoolean": true + }, + { + "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/other-extension", + "valueString": "something else" + } + ] +}