From 2a3269649d7412b439395ebeca6c7d7241d2a1e8 Mon Sep 17 00:00:00 2001 From: Mint Thompson Date: Fri, 22 Nov 2024 13:38:09 -0500 Subject: [PATCH] Support "position" slicing discriminator FHIR R5 added "position" as an allowed slice discriminator, where slices are differentiated by their order within a list element. Because a consistent order is necessary for this slicing to be meaningful, emit an error if "ordered" is not set to true for the slicing. Previous versions of FHIR may not use this discriminator. Minor changes to npm-shrinkwrap.json due to issues detected by `npm audit`. --- npm-shrinkwrap.json | 13 ++--- src/fhirtypes/ElementDefinition.ts | 31 ++++++++++-- test/fhirtypes/ElementDefinition.test.ts | 64 ++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 323d2a15c..e30d0b1b1 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -848,9 +848,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2648,10 +2648,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts index e81601a55..3764f6deb 100644 --- a/src/fhirtypes/ElementDefinition.ts +++ b/src/fhirtypes/ElementDefinition.ts @@ -386,13 +386,27 @@ export class ElementDefinition { validationErrors.push( this.validateIncludes(slicing.rules, ALLOWED_SLICING_RULES, 'slicing.rules') ); - + const isR4 = this.structDef.fhirVersion.startsWith('4.'); slicing.discriminator?.forEach((d, i) => { const discriminatorPath = `slicing.discriminator[${i}]`; validationErrors.push(this.validateRequired(d.type, `${discriminatorPath}.type`)); - validationErrors.push( - this.validateIncludes(d.type, ALLOWED_DISCRIMINATOR_TYPES, `${discriminatorPath}.type`) - ); + if (isR4) { + validationErrors.push( + this.validateIncludes(d.type, ALLOWED_DISCRIMINATOR_TYPES, `${discriminatorPath}.type`) + ); + } else { + validationErrors.push( + this.validateIncludes(d.type, ALLOWED_DISCRIMINATOR_TYPES_R5, `${discriminatorPath}.type`) + ); + if (d.type === 'position' && slicing.ordered !== true) { + validationErrors.push( + new ValidationError( + 'Slicing ordering must be true when a position discriminator is used.', + 'slicing.ordered' + ) + ); + } + } validationErrors.push(this.validateRequired(d.path, `${discriminatorPath}.path`)); }); return validationErrors.filter(e => e); @@ -3076,6 +3090,15 @@ export type ElementDefinitionSlicingDiscriminator = { // Cannot constrain ElementDefinitionSlicingDiscriminator to have these values as a type // since we want to process other string values, but log an error const ALLOWED_DISCRIMINATOR_TYPES = ['value', 'exists', 'pattern', 'type', 'profile']; +// R5 introduced the 'position' discriminator +const ALLOWED_DISCRIMINATOR_TYPES_R5 = [ + 'value', + 'exists', + 'pattern', + 'type', + 'profile', + 'position' +]; export type ElementDefinitionBase = { path: string; diff --git a/test/fhirtypes/ElementDefinition.test.ts b/test/fhirtypes/ElementDefinition.test.ts index 556fd0a5c..9fbcb38d1 100644 --- a/test/fhirtypes/ElementDefinition.test.ts +++ b/test/fhirtypes/ElementDefinition.test.ts @@ -1112,5 +1112,69 @@ describe('ElementDefinition', () => { /slicing.discriminator\[0\].type: Invalid value: #foo. Value must be selected from one of the following: #value, #exists, #pattern, #type, #profile/ ); }); + + it('should be invalid when an element has a position discriminator, ordered is true, and the FHIR version is older than R5', () => { + const clone = valueX.clone(false); + clone.slicing = { + rules: 'open', + ordered: true, + discriminator: [{ path: '$this', type: 'position' }] + }; + const validationErrors = clone.validate(); + expect(validationErrors).toHaveLength(1); + expect(validationErrors[0].message).toMatch( + /slicing.discriminator\[0\].type: Invalid value: #position. Value must be selected from one of the following: #value, #exists, #pattern, #type, #profile/ + ); + }); + }); +}); + +describe('ElementDefinition R5', () => { + let defs: FHIRDefinitions; + let jsonObservation: any; + let jsonValueX: any; + let observation: StructureDefinition; + let valueX: ElementDefinition; + let fisher: TestFisher; + + beforeAll(() => { + defs = new FHIRDefinitions(); + loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); + fisher = new TestFisher().withFHIR(defs); + // resolve observation once to ensure it is present in defs + observation = fisher.fishForStructureDefinition('Observation'); + jsonObservation = defs.fishForFHIR('Observation', Type.Resource); + jsonValueX = jsonObservation.snapshot.element[21]; + }); + + beforeEach(() => { + observation = StructureDefinition.fromJSON(jsonObservation); + valueX = ElementDefinition.fromJSON(jsonValueX); + valueX.structDef = observation; + }); + + it('should be valid when an element has a position discriminator, ordered is true, and the FHIR version is R5 is newer', () => { + const clone = valueX.clone(false); + clone.slicing = { + rules: 'open', + ordered: true, + discriminator: [{ path: '$this', type: 'position' }] + }; + const validationErrors = clone.validate(); + expect(validationErrors).toHaveLength(0); + }); + + it('should be invalid when an element has a position discriminator, ordered is not true, and the FHIR version is R5 or newer', () => { + const clone = valueX.clone(false); + clone.structDef.fhirVersion = '5.0.0'; + clone.slicing = { + rules: 'open', + discriminator: [{ path: '$this', type: 'position' }] + }; + const validationErrors = clone.validate(); + expect(validationErrors).toHaveLength(1); + expect(validationErrors[0].message).toMatch( + /slicing\.ordered: Slicing ordering must be true when a position discriminator is used/ + ); }); });