-
Notifications
You must be signed in to change notification settings - Fork 42
/
ElementDefinition.ts
2201 lines (2069 loc) · 87.2 KB
/
ElementDefinition.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import {
isEmpty,
isEqual,
isMatch,
cloneDeep,
upperFirst,
intersectionWith,
flatten,
differenceWith
} from 'lodash';
import sax = require('sax');
import { minify } from 'html-minifier';
import { isUri } from 'valid-url';
import { StructureDefinition } from './StructureDefinition';
import { CodeableConcept, Coding, Quantity, Ratio, Reference } from './dataTypes';
import { FshCanonical, FshCode, FshRatio, FshQuantity, FshReference, Invariant } from '../fshtypes';
import { AssignmentValueType, OnlyRule } from '../fshtypes/rules';
import {
BindingStrengthError,
CodedTypeNotFoundError,
ValueAlreadyAssignedError,
NoSingleTypeError,
MismatchedTypeError,
InvalidCanonicalUrlError,
InvalidCardinalityError,
InvalidTypeError,
SlicingDefinitionError,
SlicingNotDefinedError,
TypeNotFoundError,
WideningCardinalityError,
InvalidSumOfSliceMinsError,
InvalidMaxOfSliceError,
NarrowingRootCardinalityError,
SliceTypeRemovalError,
InvalidUriError,
FixedToPatternError,
MultipleStandardsStatusError,
InvalidMappingError,
InvalidFHIRIdError,
DuplicateSliceError,
NonAbstractParentOfSpecializationError
} from '../errors';
import { setPropertyOnDefinitionInstance, splitOnPathPeriods } from './common';
import { Fishable, Type, Metadata, logger } from '../utils';
import { InstanceDefinition } from './InstanceDefinition';
import { idRegex } from './primitiveTypes';
export class ElementDefinitionType {
private _code: string;
profile?: string[];
targetProfile?: string[];
aggregation?: string[];
versioning?: string;
extension?: ElementDefinitionExtension[];
constructor(code: string) {
this._code = code;
}
/**
* Element.id, Extension.url, and primitive types are specified in the valueUrl of an extension.
* This function returns the fhir-type extension's valueUrl if available, else returns the code.
* @see {@link http://hl7.org/fhir/extension-structuredefinition-fhir-type.html}
*/
get code(): string {
const fhirTypeExtension = this.extension?.find(
ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type'
);
// R4 uses valueUrl; R5 uses valueUri
return fhirTypeExtension?.valueUrl ?? fhirTypeExtension?.valueUri ?? this._code;
}
set code(c: string) {
this._code = c;
}
getActualCode(): string {
return this._code;
}
withProfiles(...profiles: string[]): this {
this.profile = profiles;
return this;
}
withTargetProfiles(...targetProfiles: string[]): this {
this.targetProfile = targetProfiles;
return this;
}
toJSON(): ElementDefinitionTypeJSON {
// Remove the _code key specific to ElementDefinitionType
const elDefTypeClone = cloneDeep(this);
delete elDefTypeClone._code;
// Create ElementDefinitionTypeJSON with a code and any properties present on the ElementDefinitionType
const elDefTypeJSON: ElementDefinitionTypeJSON = {
code: this.getActualCode(),
...elDefTypeClone
};
return elDefTypeJSON;
}
static fromJSON(json: any): ElementDefinitionType {
const elDefType = new ElementDefinitionType(json.code);
elDefType.profile = json.profile;
elDefType.targetProfile = json.targetProfile;
elDefType.aggregation = json.aggregation;
elDefType.versioning = json.versioning;
elDefType.extension = json.extension;
return elDefType;
}
}
/**
* A class representing a FHIR R4 ElementDefinition. For the most part, each allowable property in an ElementDefinition
* is represented via a get/set in this class, and the value is expected to be the FHIR-compliant JSON that would go
* in the StructureDefinition JSON file (w/ translation for R3).
* @see {@link http://hl7.org/fhir/R4/elementdefinition.html}
*/
export class ElementDefinition {
private _id: string;
path: string;
extension: any[];
modifierExtension: any[];
representation: string[];
sliceName: string;
sliceIsConstraining: boolean;
label: string;
code: Coding[];
slicing: ElementDefinitionSlicing;
short: string;
definition: string;
comment: string;
requirements: string;
alias: string[];
min: number;
max: string;
base: ElementDefinitionBase;
contentReference: string;
type: ElementDefinitionType[];
meaningWhenMissing: string;
// defaultValue[x] can be literally almost any field name (e.g., defaultValueCode, etc.),
// so we can't easily use a getter/setter. It will be just an unspecified property. For now.
orderMeaning: string;
// fixed[x] can be literally almost any field name (e.g., fixedCode, fixedFoo, etc.).
// pattern[x] can be literally almost any field name (e.g., patternCoding, patternFoo, etc.).
// We'll define the ones we are using, but leave the others as unspecified properties. For now.
fixedCode: string;
patternCode: string;
fixedString: string;
patternString: string;
fixedUri: string;
patternUri: string;
fixedUrl: string;
patternUrl: string;
fixedCanonical: string;
patternCanonical: string;
fixedInstant: string;
patternInstant: string;
fixedBase64Binary: string;
patternBase64Binary: string;
fixedDate: string;
patternDate: string;
fixedDateTime: string;
patternDateTime: string;
fixedTime: string;
patternTime: string;
fixedOid: string;
patternOid: string;
fixedId: string;
patternId: string;
fixedMarkdown: string;
patternMarkdown: string;
fixedUuid: string;
patternUuid: string;
fixedXhtml: string;
patternXhtml: string;
fixedBoolean: boolean;
patternBoolean: boolean;
fixedDecimal: number;
patternDecimal: number;
fixedInteger: number;
patternInteger: number;
fixedInteger64: string;
patternInteger64: string;
fixedUnsignedInt: number;
patternUnsignedInt: number;
fixedPositiveInt: number;
patternPositiveInt: number;
fixedCodeableConcept: CodeableConcept;
patternCodeableConcept: CodeableConcept;
fixedCoding: Coding;
patternCoding: Coding;
fixedQuantity: Quantity;
patternQuantity: Quantity;
fixedAge: Quantity;
patternAge: Quantity;
fixedAddress: InstanceDefinition;
patternAddress: InstanceDefinition;
fixedPeriod: InstanceDefinition;
patternPeriod: InstanceDefinition;
fixedRatio: Ratio;
patternRatio: Ratio;
fixedReference: Reference;
patternReference: Reference;
example: ElementDefinitionExample[];
// minValue[x] can be many different field names (e.g., minValueDate, minValueQuantity, etc.),
// so we can't easily use a getter/setter. It will be just an unspecified property. For now.
// maxValue[x] can be many different field names (e.g., maxValueDate, maxValueQuantity, etc.),
// so we can't easily use a getter/setter. It will be just an unspecified property. For now.
maxLength: number;
condition: string[];
constraint: ElementDefinitionConstraint[];
mustSupport: boolean;
isModifier: boolean;
isModifierReason: string;
isSummary: boolean;
binding: ElementDefinitionBinding;
mapping: ElementDefinitionMapping[];
structDef: StructureDefinition;
private _original: ElementDefinition;
private _edStructureDefinition: StructureDefinition;
/**
* Constructs a new ElementDefinition with the given ID.
* @param {string} id - the ID of the ElementDefinition
*/
constructor(id = '') {
this.id = id;
}
get id(): string {
return this._id;
}
/**
* Sets the id of the ElementDefinition and updates the path accordingly.
* NOTE: This does not automatically update child ids/paths. That is currently up to the library user.
* @param {string} id - the ElementDefinition id
*/
set id(id: string) {
this._id = id;
// After setting the id, we should re-set the path, which is based on the id
this.path = this._id
.split('.')
.map(s => {
// Usually the path part is just the name without the slice.
const [name] = s.split(':', 2);
// The spec is unclear on if there is an exception in parts representing
// a specific choice type, in which case, the path is the slice name (e.g., ) if the id is
// Observation.value[x]:valueQuantity, then path is Observation.valueQuantity.
// The code to make the exception is commented below, and will remain until we can clarify
// const [name, slice] = s.split(':', 2);
// if (
// slice &&
// name.endsWith('[x]') &&
// this.type &&
// this.type.some(t => slice === `${name.slice(0, -3)}${capitalize(t.code)}`)
// ) {
// return slice;
// }
return name;
})
.join('.');
}
getPathWithoutBase(): string {
return this.path.slice(this.structDef.type.length + 1);
}
/**
* Get the StructureDefinition for ElementDefinition
* @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
* @returns {StructureDefinition} the StructureDefinition of ElementDefinition
*/
getOwnStructureDefinition(fisher: Fishable): StructureDefinition {
if (this._edStructureDefinition == null) {
this._edStructureDefinition = StructureDefinition.fromJSON(
fisher.fishForFHIR('ElementDefinition', Type.Type)
);
}
return this._edStructureDefinition;
}
/**
* Returns the Types that have the given code(s).
* @param {string[]} codes - the codes to match Types against
* @returns {ElementDefinitionType[]} the matched Types
*/
findTypesByCode(...codes: string[]): ElementDefinitionType[] {
if (!this.type) {
return [];
}
return this.type.filter(t => codes.includes(t.code));
}
/**
* Creates a new element with an id/path indicating it is a child of the current element.
* Defaults to '$UNKNOWN' if no name is passed in, as it needs a value, but usually a name should be passed in.
* NOTE: This function does not automatically add the child element to the StructureDefinition.
* @param {string} name - the name of the child element, to be appended to the parent ID/path
* @returns {ElementDefinition} the new child element
*/
newChildElement(name = '$UNKNOWN'): ElementDefinition {
const el = new ElementDefinition(`${this.id}.${name}`);
el.structDef = this.structDef;
return el;
}
/**
* ElementDefinition is capable of producing its own differential, based on differences from a stored "original".
* This function captures the current state as the "original", so any further changes made would be captured in
* the generated differential.
*/
captureOriginal(): void {
this._original = this.clone();
}
/**
* Clears the stored "original" state, resulting in every property being considered new, and reflected in the
* generated differential.
*/
clearOriginal(): void {
this._original = undefined;
}
/**
* Determines if the state of the current element differs from the stored "original".
* @returns {boolean} true if the state of the current element differs from the stored "original", false otherwise
*/
hasDiff(): boolean {
const original = this._original ? this._original : new ElementDefinition();
return (
PROPS.some(prop => {
if (prop.endsWith('[x]')) {
const re = new RegExp(`^${prop.slice(0, -3)}[A-Z].*$`);
prop = Object.keys(this).find(p => re.test(p));
if (prop == null) {
prop = Object.keys(original).find(p => re.test(p));
}
}
// @ts-ignore
return prop && !isEqual(this[prop], original[prop]);
}) ||
// When a slice or a sliced element has children that changed, we must treat the slice or sliced element
// as if it differs from the original. The IG Publisher requires slices or sliced elements with changed
// children to be in the differential, or the snapshot is incorrectly generated
((this.sliceName || this.getSlices().length > 0) && this.children().some(c => c.hasDiff()))
);
}
/**
* Calculates the differential based on changes in data from the stored "original" state and returns the differential
* as a new ElementDefinition containing only the id, path, and changed data.
* @returns {ElementDefinition} an ElementDefinition representing the changed data since the stored "original" state
*/
calculateDiff(): ElementDefinition {
const original = this._original ? this._original : new ElementDefinition();
const diff = new ElementDefinition(this.id);
diff.structDef = this.structDef;
for (let prop of PROPS) {
if (prop.endsWith('[x]')) {
const re = new RegExp(`^${prop.slice(0, -3)}[A-Z].*$`);
prop = Object.keys(this).find(p => re.test(p));
if (prop == null) {
prop = Object.keys(original).find(p => re.test(p));
}
}
// @ts-ignore
if (prop && !isEqual(this[prop], original[prop])) {
if (ADDITIVE_PROPS.includes(prop)) {
// @ts-ignore
diff[prop] = differenceWith(this[prop], original[prop], isEqual);
// @ts-ignore
if (isEmpty(diff[prop])) {
// @ts-ignore
delete diff[prop];
}
} else {
// @ts-ignore
diff[prop] = cloneDeep(this[prop]);
}
}
}
// Set the diff id, which may be different than snapshot id in the case of choices (e.g., value[x] -> valueString)
// NOTE: The path also gets set automatically when setting id
diff.id = diff.diffId();
// If the snapshot is a choice (e.g., value[x]), but the diff is a specific choice (e.g., valueString), then
// remove the slicename property from the diff (it is implied and not required in the diff)
// If the snapshot is not a choice, the diff needs to have a sliceName, so use the original.
if (this.path.endsWith('[x]') && !diff.path.endsWith('[x]')) {
delete diff.sliceName;
} else if (original.sliceName && diff.sliceName == null) {
diff.sliceName = original.sliceName;
}
return diff;
}
/**
* Gets the id of an element on the differential using the shortcut syntax described here
* https://blog.fire.ly/2019/09/13/type-slicing-in-fhir-r4/
* @returns {string} the id for the differential
*/
diffId(): string {
return this.id
.split('.')
.map(p => {
const i = p.indexOf('[x]:');
return i > -1 ? p.slice(i + 4) : p;
})
.join('.');
}
/**
* Apply invariant to the Element.constraint
* @see {@link http://hl7.org/fhir/R4/elementdefinition-definitions.html#ElementDefinition.constraint}
* @param invariant The invariant to be applied to the constraint
* @param source Source URL for the constraint
*/
applyConstraint(invariant: Invariant, source?: string): void {
const constraint: ElementDefinitionConstraint = {
...(invariant.name && { key: invariant.name }),
...(invariant.severity && { severity: invariant.severity.code }),
...(invariant.description && { human: invariant.description }),
...(invariant.expression && { expression: invariant.expression }),
...(invariant.xpath && { xpath: invariant.xpath }),
...(source && { source })
};
if (this.constraint) {
this.constraint.push(constraint);
} else {
this.constraint = [constraint];
}
this.findConnectedElements().forEach(ce => ce.applyConstraint(invariant, source));
}
/**
* This function sets an instance property of an ED if possible
* @param {string} path - The path to the ElementDefinition to assign
* @param {any} value - The value to assign
* @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
*/
setInstancePropertyByPath(path: string, value: any, fisher: Fishable): void {
setPropertyOnDefinitionInstance(this, path, value, fisher);
}
getSlices() {
return this.structDef.elements.filter(
e => e.id !== this.id && e.path === this.path && e.id.startsWith(`${this.id}:`)
);
}
/**
* Constrains the cardinality of this element. Cardinality constraints can only narrow
* cardinality. Attempts to constrain to a wider cardinality will throw.
* @see {@link http://hl7.org/fhir/R4/profiling.html#cardinality}
* @see {@link http://hl7.org/fhir/R4/conformance-rules.html#cardinality}
* @see {@link http://hl7.org/fhir/R4/elementdefinition-definitions.html#ElementDefinition.min}
* @see {@link http://hl7.org/fhir/R4/elementdefinition-definitions.html#ElementDefinition.max}
* @param {number} min - the minimum cardinality
* @param {number|string} max - the maximum cardinality
* @throws {InvalidCardinalityError} when min > max
* @throws {WideningCardinalityError} when new cardinality is wider than existing cardinality
* @throws {InvalidSumOfSliceMinsError} when the mins of slice elements > max of sliced element
* @throws {InvalidMaxOfSliceError} when a sliced element's max is < an individual slice's max
* @throws {NarrowingRootCardinalityError} when the new cardinality on an element is narrower than
* the cardinality on a connected element
*/
constrainCardinality(min: number, max: string): void {
// If only one side of the cardinality is set by the rule, use element's current cardinality
if (isNaN(min)) min = this.min;
if (max === '') max = this.max;
const isUnbounded = max === '*';
const maxInt = !isUnbounded ? parseInt(max) : null;
// Check to ensure it is valid (min <= max)
if (!isUnbounded && min > maxInt) {
throw new InvalidCardinalityError(min, max);
}
// Check to ensure min >= existing min
if (this.min != null && min < this.min) {
throw new WideningCardinalityError(this.min, this.max, min, max);
}
// Check to ensure max <= existing max
if (this.max != null && this.max !== '*' && (maxInt > parseInt(this.max) || isUnbounded)) {
throw new WideningCardinalityError(this.min, this.max, min, max);
}
// Sliced elements and slices have special card rules described here:
// http://www.hl7.org/fhiR/profiling.html#slice-cardinality
// If element is slice definition
if (this.slicing) {
// Check that new max >= sum of mins of children
this.checkSumOfSliceMins(max);
// Check that new max >= every individual child max
const slices = this.getSlices();
const overMaxChild = slices.find(child => child.max === '*' || parseInt(child.max) > maxInt);
if (!isUnbounded && overMaxChild) {
throw new InvalidMaxOfSliceError(overMaxChild.max, overMaxChild.sliceName, max);
}
}
const connectedElements = this.findConnectedElements();
if (connectedElements.length > 0) {
// check to see if the card constraint would actually be a problem for the connected element
// that is to say, if the new card is narrower than the connected card
connectedElements
.filter(ce => !(ce.path === this.path && ce.id.startsWith(this.id)))
// Filter out elements that are directly slices of this, since they may have min < this.min
// Children of slices however must have min >= this.min
.forEach(ce => {
if (ce.min != null && ce.min < min) {
throw new NarrowingRootCardinalityError(
this.path,
ce.id,
min,
max,
ce.min,
ce.max ?? '*'
);
}
});
connectedElements.forEach(ce => {
// if the connected element's max is not null and is not *, we can't make the max smaller than its max
if (ce.max != null && ce.max != '*' && maxInt != null && maxInt < parseInt(ce.max)) {
throw new NarrowingRootCardinalityError(this.path, ce.id, min, max, ce.min ?? 0, ce.max);
}
});
}
// If element is a slice
const slicedElement = this.slicedElement();
if (slicedElement) {
const parentSlice = this.findParentSlice();
const sliceSiblings = this.structDef.elements.filter(
el =>
this !== el &&
slicedElement === el.slicedElement() &&
parentSlice === el.findParentSlice()
);
const newParentMin = min + sliceSiblings.reduce((sum, el) => sum + el.min, 0);
// if this is a reslice, the parent element will also be a slice of the sliced element.
// if this is not a reslice, the parent element is the sliced element.
const parentElement = parentSlice ?? slicedElement;
// Check that parentElement max >= new sum of mins
if (parentElement.max !== '*' && newParentMin > parseInt(parentElement.max)) {
throw new InvalidSumOfSliceMinsError(newParentMin, parentElement.max, parentElement.id);
}
// If new sum of mins > parentElement min, increase parentElement min
if (newParentMin > parentElement.min) {
parentElement.constrainCardinality(newParentMin, '');
}
}
[this.min, this.max] = [min, max];
}
/**
* Tries to find all connected elements based on slicing.
* When an element that has children is sliced, there can be constraints on that element's children,
* as well as the children of any defined slices. Depending on the order that slices and rules are
* defined, a rule may be applied to an element after slices of that element have already been
* created. Therefore, to determine the full effect of that rule, the elements that are inside
* slices must be found. The rule's path may contain many sliced elements, so it is necessary
* to recursively search the StructureDefinition for ancestors of the element on the rule's path
* that contain slice definitions. These sliced ancestors may in turn contain child elements that
* match the rule's path.
* In summary: find elements that have the same path, but are slicier.
* @param {string} postPath The path to append to the parent in order to try to find a connected element
* @returns {ElementDefinition[]} The elements at or inside of slices whose path matches the original element
*/
findConnectedElements(postPath = ''): ElementDefinition[] {
const connectedElements = this.getSlices()
.filter(e => e.max !== '0') // we don't need zeroed-out slices
.map(slice => {
return this.structDef.findElement(`${slice.id}${postPath}`);
})
.filter(e => e);
if (this.parent()) {
const [parentPath] = splitOnPathPeriods(this.path).slice(-1);
return connectedElements.concat(
this.parent().findConnectedElements(`.${parentPath}${postPath}`)
);
} else {
return connectedElements;
}
}
findConnectedSliceElement(postPath = ''): ElementDefinition {
const slicingRoot = this.slicedElement();
if (slicingRoot) {
return this.structDef.findElement(`${slicingRoot.id}${postPath}`);
} else if (this.parent()) {
return this.parent().findConnectedSliceElement(
`.${this.path.split('.').slice(-1)[0]}${postPath}`
);
}
}
findParentSlice(): ElementDefinition {
if (this.sliceName) {
const slicedElement = this.slicedElement();
const parentNameParts = this.sliceName.split('/').slice(0, -1);
const potentialParentNames = parentNameParts
.map((_part, i) => {
return parentNameParts.slice(0, i + 1).join('/');
})
.reverse();
for (const parentName of potentialParentNames) {
const potentialParent = this.structDef.elements.find(el => {
return el.sliceName === parentName && el.slicedElement() === slicedElement;
});
if (potentialParent) {
return potentialParent;
}
}
}
}
/**
* Checks if the sum of slice mins exceeds the max of sliced element, and returns
* the sum if so.
* @param {string} slicedElementMax - The max of the sliced element
* @param {number} newSliceMin - An optional new minimum if the minimum of this is being constrained
* @returns {number} the sum of the mins of the slices, or 0 if the sum is less than the sliced max
* @throws {InvalidSumOfSliceMinsError} when the sum of mins of the slices exceeds max of sliced element
*/
private checkSumOfSliceMins(newSlicedElementMax: string, sliceMinIncrease = 0) {
const slices = this.getSlices();
const sumOfMins = sliceMinIncrease + slices.reduce((prev, curr) => (prev += curr.min), 0);
if (newSlicedElementMax !== '*' && sumOfMins > parseInt(newSlicedElementMax)) {
throw new InvalidSumOfSliceMinsError(sumOfMins, newSlicedElementMax, this.id);
} else {
return sumOfMins;
}
}
/**
* Constrains the type of this element to the requested type(s). When this element's type is a
* choice, this function will reduce the choice to only those types provided -- unless a
* targetType is provided, in which case, only that type will be affected and other options in
* the choice will be left unchanged. This function should allow the following scenarios:
* - constrain a choice of types to a smaller subset of types (including a single type)
* - constrain a type to one or more profiles on that type
* - constrain a supertype (e.g., Resource) to one or more subtypes (e.g., Condition)
* - constrain a reference of multiple types to a reference of a smaller subset of types
* - constrain a reference of a type or profile to one or more profiles of that type/profile
* - constrain a reference of a supertype to one or more references of subtypes
* - any combinaton of the above
* This function will throw when:
* - attempting to add a base type (e.g., `type.code`) that wasn't already a choice in the type
* - attempting to add a profile that doesn't match any of the existing types
* - attempting to add a base reference that wasn't already a reference
* - attempting to add a reference to a profile that doesn't match any of the existing references
* - specifying a target that does not match any of the existing type choices
* - specifying types or a target whose definition cannot be found
* @see {@link http://hl7.org/fhir/R4/elementdefinition-definitions.html#ElementDefinition.type}
* @param {OnlyRule} rule - The rule specifying the types to apply
* @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
* @param {string} [target] - a specific target type to constrain. If supplied, will attempt to
* constrain only that type without affecting other types (in a choice or reference to a choice).
* @throws {TypeNotFoundError} when a passed in type's definition cannot be found
* @throws {InvalidTypeError} when a passed in type or the targetType doesn't match any existing
* types
* @throws {SliceTypeRemovalError} when a rule would eliminate all types on a slice
*/
constrainType(rule: OnlyRule, fisher: Fishable, target?: string): void {
const types = rule.types;
// Establish the target types (if applicable)
const targetType = this.getTargetType(target, fisher);
const targetTypes: ElementDefinitionType[] = targetType ? [targetType] : this.type;
// Setup a map to store how each existing element type maps to the input types
const typeMatches: Map<string, ElementTypeMatchInfo[]> = new Map();
targetTypes.forEach(t => typeMatches.set(t.code, []));
// Loop through the input types and associate them to the element types in the map
for (const type of types) {
const typeMatch = this.findTypeMatch(type, targetTypes, fisher);
typeMatches.get(typeMatch.code).push(typeMatch);
}
// Loop through the existing element types building the new set of element types w/ constraints
const newTypes: ElementDefinitionType[] = [];
const oldTypes: ElementDefinitionType[] = [];
for (const type of this.type) {
// If the typeMatches map doesn't have the type code at all, this means that a target was
// specified, and this element type wasn't the target. In this case, we want to keep it.
if (!typeMatches.has(type.code)) {
newTypes.push(cloneDeep(type));
continue;
}
// Get the associated input type matches. If no input types matched against it, then this
// element type should be filtered out of the results, so just skip to the next one.
const matches = typeMatches.get(type.code);
if (isEmpty(matches)) {
oldTypes.push(type);
continue;
}
newTypes.push(...this.applyTypeIntersection(type, targetType, matches));
}
// Let user know if other rules have been made obsolete
const obsoleteChoices = this.structDef.findObsoleteChoices(this, oldTypes);
if (obsoleteChoices.length > 0) {
logger.error(
`Type constraint on ${this.path} makes rules in ${
this.structDef.name
} obsolete for choices: ${obsoleteChoices.join(', ')}`,
rule.sourceInfo
);
}
// new types for this element have been determined
// if there are any connected elements, make sure that nothing invalid will happen
let connectedElements = this.findConnectedElements();
// however, we don't need to apply this to elements representing a choice of types
// for example, if this is being applied to value[x], and valueString exists, we can remove the string type.
if (this.path.endsWith('[x]')) {
connectedElements = connectedElements.filter(ce => ce.id.endsWith('[x]'));
}
if (connectedElements.length > 0) {
// if all connected elements have a non-empty intersection, we can safely apply the rule
const connectedTypeChanges: Map<ElementDefinition, ElementDefinitionType[]> = new Map();
connectedElements.forEach(ce => {
const intersection = this.findTypeIntersection(newTypes, ce.type, targetType, fisher);
if (intersection.length > 0) {
connectedTypeChanges.set(ce, intersection);
} else {
const obsoleteConnections = ce.structDef.findObsoleteChoices(ce, oldTypes);
if (obsoleteConnections.length > 0) {
logger.error(
`Type constraint on ${rule.path} makes rules in ${
ce.structDef.name
} obsolete for choices: ${obsoleteConnections.join(', ')}`,
rule.sourceInfo
);
} else {
throw new SliceTypeRemovalError(rule.path, ce.id);
}
}
});
if (connectedElements.length == connectedTypeChanges.size) {
connectedTypeChanges.forEach((ceType, ce) => (ce.type = ceType));
}
}
// Finally, reset this element's types to the new types
this.type = newTypes;
}
/**
* Given a string representing a type or profile, will return this element's matching type, if
* found -- with all other profiles or targetProfiles (e.g. references) removed from the type.
* @param {string} target - the target to find a matching type for
* @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
* @returns {ElementDefinitionType} the element's type that matches the target
* @throws {TypeNotFoundError} when the target's definition cannot be found
* @throws {InvalidTypeError} when the target doesn't match any existing types
*/
private getTargetType(target: string, fisher: Fishable): ElementDefinitionType {
let targetType: ElementDefinitionType;
if (target) {
const targetSD = fisher.fishForMetadata(
target,
Type.Resource,
Type.Type,
Type.Profile,
Type.Extension
);
if (targetSD == null) {
throw new TypeNotFoundError(target);
}
// Try to match on types by an exact match on the code (applies to resources),
// the profiles (applies to profiles), or targetProfiles (applies to references).
// Clone it since we will filter out the non-target profiles/targetProfiles.
targetType = cloneDeep(
this.type.find(
t =>
t.code === targetSD.id ||
t.profile?.includes(targetSD.url) ||
t.targetProfile?.includes(targetSD.url)
)
);
if (!targetType) {
throw new InvalidTypeError(target, this.type);
}
// Re-assign the targetProfiles/profiles as appopriate to remove non-targets
if (targetType.profile?.includes(targetSD.url)) {
targetType.profile = [targetSD.url];
} else if (targetType.targetProfile?.includes(targetSD.url)) {
targetType.targetProfile = [targetSD.url];
}
}
return targetType;
}
/**
* Given an input type (the constraint) and a set of target types (the things to potentially
* constrain), find the match and return information about it.
* @param {{ type: string; isReference?: boolean }} type - the constrained types, identified by
* id/type/url string and an optional reference flag (defaults false)
* @param {ElementDefinitionType[]} targetTypes - the element types that the constrained type
* can be potentially applied to
* @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
* @param {string} [target] - a specific target type to constrain. If supplied, will attempt to
* constrain only that type without affecting other types (in a choice or reference to a choice).
* @returns {ElementTypeMatchInfo} the information about the match
* @throws {TypeNotFoundError} when the type's definition cannot be found
* @throws {InvalidTypeError} when the type doesn't match any of the targetTypes
*/
private findTypeMatch(
type: { type: string; isReference?: boolean },
targetTypes: ElementDefinitionType[],
fisher: Fishable
): ElementTypeMatchInfo {
let matchedType: ElementDefinitionType;
// Get the lineage (type hierarchy) so we can walk up it when attempting to match
const lineage = this.getTypeLineage(type.type, fisher);
if (isEmpty(lineage)) {
throw new TypeNotFoundError(type.type);
}
// Walk up the lineage, one StructureDefinition at a time. We can potentially match on the
// type itself or any of its parents. For example, a BloodPressure profile could match on
// an Observation already having a BP profile, an Observation type w/ no profiles, a
// DomainResource type w/ no profiles, or a Resource type w/ no profiles.
let specializationOfNonAbstractType = false;
for (const md of lineage) {
if (type.isReference) {
// References always have a code 'Reference' w/ the referenced type's defining URL set as
// one of the targetProfiles. If the targetProfile property is null, that means any
// reference is allowed.
matchedType = targetTypes.find(
t2 =>
t2.code === 'Reference' &&
(t2.targetProfile == null || t2.targetProfile.includes(md.url))
);
} else {
// Look for exact match on the code (w/ no profile) or a match on the same base type with
// a matching profile
matchedType = targetTypes.find(t2 => {
const matchesUnprofiledResource = t2.code === md.id && isEmpty(t2.profile);
const matchesProfile = t2.code === md.sdType && t2.profile?.includes(md.url);
// True if we match an unprofiled type that is not abstract, is a parent, and that we are
// specializing (the type does not match the sdType of the type to match)
specializationOfNonAbstractType =
matchesUnprofiledResource &&
!md.abstract &&
md.id !== lineage[0].id &&
md.id !== lineage[0].sdType;
return matchesUnprofiledResource || matchesProfile;
});
}
if (matchedType) {
break;
}
}
if (!matchedType) {
throw new InvalidTypeError(
type.isReference ? `Reference(${type.type})` : type.type,
targetTypes
);
} else if (specializationOfNonAbstractType) {
throw new NonAbstractParentOfSpecializationError(type.type, matchedType.code);
}
return {
metadata: lineage[0],
code: matchedType.code
};
}
/**
* Gets the full lineage of the type, w/ the item at index 0 being the type's own Metadata,
* the item at index 1 being its parent's, 2 being its grandparent's, etc. If a definition can't be
* found, it stops and returns as much lineage as is found thus far.
* @param {string} type - the type to get the lineage for
* @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
* @returns {Metadata[]} representing the lineage of the type
*/
private getTypeLineage(type: string, fisher: Fishable): Metadata[] {
const results: Metadata[] = [];
// Start with the current type and walk up the base definitions.
// Stop when we can't find a definition or the base definition is blank.
let currentType = type;
while (currentType != null) {
const result = fisher.fishForMetadata(currentType);
if (result) {
results.push(result);
}
currentType = result?.parent;
}
return results;
}
/**
* Given a new ElementTypeDefinition (based on the existing one), will apply the matching
* profiles and targetProfiles as appropriate. If a targetType was specified, will filter out
* the other profiles or targetProfiles.
* @param {ElementDefinitionType} newType - the new type to apply the profiles/targetProfiles to
* @param {ElementDefinitionType} [targetType] - the (potentially null) target type for the
* type constraint
* @param {ElementTypeMatchInfo[]} matches - the information about how type constraints map
* to element types
*/
private applyProfiles(
newType: ElementDefinitionType,
targetType: ElementDefinitionType,
matches: ElementTypeMatchInfo[]
): void {
const matchedProfiles: string[] = [];
const matchedTargetProfiles: string[] = [];
for (const match of matches) {
if (match.metadata.id === newType.code) {
continue;
} else if (match.code === 'Reference' && match.metadata.sdType !== 'Reference') {
matchedTargetProfiles.push(match.metadata.url);
} else {
matchedProfiles.push(match.metadata.url);
}
}
if (targetType) {
if (!isEmpty(matchedTargetProfiles)) {
const targetIdx = newType.targetProfile?.indexOf(targetType.targetProfile[0]);
if (targetIdx != null && targetIdx > -1) {
newType.targetProfile.splice(targetIdx, 1, ...matchedTargetProfiles);
} else {
newType.targetProfile = newType.profile ?? [];
newType.targetProfile.push(...matchedTargetProfiles);
}
}
if (!isEmpty(matchedProfiles)) {
const targetIdx = newType.profile?.indexOf(targetType.profile[0]);
if (targetIdx != null && targetIdx > -1) {
newType.profile.splice(targetIdx, 1, ...matchedProfiles);
} else {
newType.profile = newType.profile ?? [];
newType.profile.push(...matchedProfiles);
}
}
} else {
if (!isEmpty(matchedTargetProfiles)) {
newType.targetProfile = matchedTargetProfiles;
}
if (!isEmpty(matchedProfiles)) {
newType.profile = matchedProfiles;
}
}
}
private findTypeIntersection(
leftTypes: ElementDefinitionType[],
rightTypes: ElementDefinitionType[],
targetType: ElementDefinitionType,
fisher: Fishable
): ElementDefinitionType[] {
const intersection: ElementDefinitionType[] = [];
let match: ElementTypeMatchInfo;
leftTypes.forEach(left => {
const matches: ElementTypeMatchInfo[] = [];
try {
match = this.findTypeMatch({ type: left.code }, rightTypes, fisher);
matches.push(match);
} catch (ex) {
// it's okay if a given type doesn't have any matches.
}
intersection.push(...this.applyTypeIntersection(left, targetType, matches));
});
return intersection;
}
// In the case of an element type whose code is a supertype (e.g., 'Resource'), we need to
// break that up into a new set of element types corresponding to the subtypes. For example,
// if a 'Resource' type is constrained to 'Condition' and 'Procedure', then in the resulting
// StructureDefinition, there should be element types with codes 'Condition' and 'Procedure',
// and no element type with the code 'Resource` any longer. So... we create a special
// map to store the current subtypes (or if not applicable, just store the original type).
private applyTypeIntersection(
type: ElementDefinitionType,
targetType: ElementDefinitionType,
matches: ElementTypeMatchInfo[]
) {
const intersection: ElementDefinitionType[] = [];
const currentTypeMatches: Map<string, ElementTypeMatchInfo[]> = new Map();
const fhirPathPrimitive = /^http:\/\/hl7\.org\/fhirpath\/System\./;
for (const match of matches) {