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/ + ); }); });