@peerbits/fhir-validator 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ import { validateCodeableConcept, validateCoding } from "../coding.js";
2
+ import { validateReference } from "../reference-rules.js";
3
+ const VALID_ENCOUNTER_STATUSES = [
4
+ "planned",
5
+ "arrived",
6
+ "triaged",
7
+ "in-progress",
8
+ "onleave",
9
+ "finished",
10
+ "cancelled",
11
+ "entered-in-error",
12
+ "unknown",
13
+ ];
14
+ /**
15
+ * Validates base structural rules for FHIR R4 Encounter resource.
16
+ */
17
+ export function validateEncounter(resource, path = "Encounter") {
18
+ const issues = [];
19
+ if (resource.resourceType !== "Encounter") {
20
+ issues.push({
21
+ severity: "error",
22
+ path: `${path}.resourceType`,
23
+ code: "invalid-resource-type",
24
+ message: `Expected resourceType 'Encounter', found '${String(resource.resourceType)}'.`,
25
+ });
26
+ }
27
+ // status (Required)
28
+ if (resource.status === undefined || resource.status === null || resource.status === "") {
29
+ issues.push({
30
+ severity: "error",
31
+ path: `${path}.status`,
32
+ code: "required",
33
+ message: `Missing required 'status' in '${path}'.`,
34
+ });
35
+ }
36
+ else if (typeof resource.status !== "string" || !VALID_ENCOUNTER_STATUSES.includes(resource.status)) {
37
+ issues.push({
38
+ severity: "error",
39
+ path: `${path}.status`,
40
+ code: "invalid-value",
41
+ message: `Invalid Encounter status '${String(resource.status)}'. Allowed values: ${VALID_ENCOUNTER_STATUSES.join(", ")}.`,
42
+ });
43
+ }
44
+ // class (Required Coding / Coding-like object)
45
+ if (resource.class === undefined || resource.class === null) {
46
+ issues.push({
47
+ severity: "error",
48
+ path: `${path}.class`,
49
+ code: "required",
50
+ message: `Missing required 'class' in '${path}'.`,
51
+ });
52
+ }
53
+ else {
54
+ // In FHIR R4, Encounter.class is a Coding
55
+ issues.push(...validateCoding(resource.class, `${path}.class`));
56
+ }
57
+ // subject (Reference: Patient | Group)
58
+ if (resource.subject !== undefined) {
59
+ issues.push(...validateReference(resource.subject, `${path}.subject`, ["Patient", "Group"]));
60
+ }
61
+ // period (Period)
62
+ if (resource.period !== undefined) {
63
+ if (typeof resource.period !== "object" || resource.period === null || Array.isArray(resource.period)) {
64
+ issues.push({
65
+ severity: "error",
66
+ path: `${path}.period`,
67
+ code: "invalid-structure",
68
+ message: `Expected '${path}.period' to be an object.`,
69
+ });
70
+ }
71
+ else {
72
+ const p = resource.period;
73
+ if (p.start !== undefined && typeof p.start !== "string") {
74
+ issues.push({
75
+ severity: "error",
76
+ path: `${path}.period.start`,
77
+ code: "invalid-type",
78
+ message: `Expected '${path}.period.start' to be a string.`,
79
+ });
80
+ }
81
+ if (p.end !== undefined && typeof p.end !== "string") {
82
+ issues.push({
83
+ severity: "error",
84
+ path: `${path}.period.end`,
85
+ code: "invalid-type",
86
+ message: `Expected '${path}.period.end' to be a string.`,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ // participant
92
+ if (resource.participant !== undefined) {
93
+ if (!Array.isArray(resource.participant)) {
94
+ issues.push({
95
+ severity: "error",
96
+ path: `${path}.participant`,
97
+ code: "invalid-type",
98
+ message: `Expected '${path}.participant' to be an array.`,
99
+ });
100
+ }
101
+ else {
102
+ resource.participant.forEach((part, idx) => {
103
+ const partPath = `${path}.participant[${idx}]`;
104
+ if (typeof part !== "object" || part === null || Array.isArray(part)) {
105
+ issues.push({
106
+ severity: "error",
107
+ path: partPath,
108
+ code: "invalid-structure",
109
+ message: `Expected '${partPath}' to be an object.`,
110
+ });
111
+ }
112
+ else {
113
+ const p = part;
114
+ if (p.individual !== undefined) {
115
+ issues.push(...validateReference(p.individual, `${partPath}.individual`, ["Practitioner", "PractitionerRole", "RelatedPerson"]));
116
+ }
117
+ if (p.type !== undefined && Array.isArray(p.type)) {
118
+ p.type.forEach((t, tIdx) => {
119
+ issues.push(...validateCodeableConcept(t, `${partPath}.type[${tIdx}]`));
120
+ });
121
+ }
122
+ }
123
+ });
124
+ }
125
+ }
126
+ // serviceProvider (Reference: Organization)
127
+ if (resource.serviceProvider !== undefined) {
128
+ issues.push(...validateReference(resource.serviceProvider, `${path}.serviceProvider`, ["Organization"]));
129
+ }
130
+ // type (CodeableConcept[])
131
+ if (resource.type !== undefined && Array.isArray(resource.type)) {
132
+ resource.type.forEach((t, idx) => {
133
+ issues.push(...validateCodeableConcept(t, `${path}.type[${idx}]`));
134
+ });
135
+ }
136
+ return issues;
137
+ }
@@ -0,0 +1,5 @@
1
+ import { ValidationIssue } from "../types.js";
2
+ /**
3
+ * Validates base structural rules for FHIR R4 Observation resource.
4
+ */
5
+ export declare function validateObservation(resource: Record<string, unknown>, path?: string): ValidationIssue[];
@@ -0,0 +1,213 @@
1
+ import { validateCodeableConcept } from "../coding.js";
2
+ import { validateReference } from "../reference-rules.js";
3
+ const VALID_OBSERVATION_STATUSES = [
4
+ "registered",
5
+ "preliminary",
6
+ "final",
7
+ "amended",
8
+ "corrected",
9
+ "cancelled",
10
+ "entered-in-error",
11
+ "unknown",
12
+ ];
13
+ /**
14
+ * Validates base structural rules for FHIR R4 Observation resource.
15
+ */
16
+ export function validateObservation(resource, path = "Observation") {
17
+ const issues = [];
18
+ if (resource.resourceType !== "Observation") {
19
+ issues.push({
20
+ severity: "error",
21
+ path: `${path}.resourceType`,
22
+ code: "invalid-resource-type",
23
+ message: `Expected resourceType 'Observation', found '${String(resource.resourceType)}'.`,
24
+ });
25
+ }
26
+ // status (Required)
27
+ if (resource.status === undefined || resource.status === null || resource.status === "") {
28
+ issues.push({
29
+ severity: "error",
30
+ path: `${path}.status`,
31
+ code: "required",
32
+ message: `Missing required 'status' in '${path}'.`,
33
+ });
34
+ }
35
+ else if (typeof resource.status !== "string" || !VALID_OBSERVATION_STATUSES.includes(resource.status)) {
36
+ issues.push({
37
+ severity: "error",
38
+ path: `${path}.status`,
39
+ code: "invalid-value",
40
+ message: `Invalid Observation status '${String(resource.status)}'. Allowed values: ${VALID_OBSERVATION_STATUSES.join(", ")}.`,
41
+ });
42
+ }
43
+ // code (Required CodeableConcept)
44
+ if (resource.code === undefined || resource.code === null) {
45
+ issues.push({
46
+ severity: "error",
47
+ path: `${path}.code`,
48
+ code: "required",
49
+ message: `Missing required 'code' in '${path}'.`,
50
+ });
51
+ }
52
+ else {
53
+ issues.push(...validateCodeableConcept(resource.code, `${path}.code`, { requireCoding: true }));
54
+ }
55
+ // category (CodeableConcept[])
56
+ if (resource.category !== undefined) {
57
+ if (!Array.isArray(resource.category)) {
58
+ issues.push({
59
+ severity: "error",
60
+ path: `${path}.category`,
61
+ code: "invalid-type",
62
+ message: `Expected '${path}.category' to be an array, got ${typeof resource.category}.`,
63
+ });
64
+ }
65
+ else {
66
+ resource.category.forEach((cat, idx) => {
67
+ issues.push(...validateCodeableConcept(cat, `${path}.category[${idx}]`));
68
+ });
69
+ }
70
+ }
71
+ // subject (Reference: Patient | Group | Device | Location)
72
+ if (resource.subject !== undefined) {
73
+ issues.push(...validateReference(resource.subject, `${path}.subject`, ["Patient", "Group", "Device", "Location"]));
74
+ }
75
+ // encounter (Reference: Encounter)
76
+ if (resource.encounter !== undefined) {
77
+ issues.push(...validateReference(resource.encounter, `${path}.encounter`, ["Encounter"]));
78
+ }
79
+ // performer (Reference[])
80
+ if (resource.performer !== undefined) {
81
+ if (!Array.isArray(resource.performer)) {
82
+ issues.push({
83
+ severity: "error",
84
+ path: `${path}.performer`,
85
+ code: "invalid-type",
86
+ message: `Expected '${path}.performer' to be an array, got ${typeof resource.performer}.`,
87
+ });
88
+ }
89
+ else {
90
+ resource.performer.forEach((perf, idx) => {
91
+ issues.push(...validateReference(perf, `${path}.performer[${idx}]`, ["Practitioner", "PractitionerRole", "Organization", "CareTeam", "Patient", "RelatedPerson"]));
92
+ });
93
+ }
94
+ }
95
+ // valueQuantity
96
+ if (resource.valueQuantity !== undefined) {
97
+ if (typeof resource.valueQuantity !== "object" || resource.valueQuantity === null || Array.isArray(resource.valueQuantity)) {
98
+ issues.push({
99
+ severity: "error",
100
+ path: `${path}.valueQuantity`,
101
+ code: "invalid-structure",
102
+ message: `Expected '${path}.valueQuantity' to be an object.`,
103
+ });
104
+ }
105
+ else {
106
+ const q = resource.valueQuantity;
107
+ if (q.value !== undefined && typeof q.value !== "number") {
108
+ issues.push({
109
+ severity: "error",
110
+ path: `${path}.valueQuantity.value`,
111
+ code: "invalid-type",
112
+ message: `Expected '${path}.valueQuantity.value' to be a number, got ${typeof q.value}.`,
113
+ });
114
+ }
115
+ if (q.unit !== undefined && typeof q.unit !== "string") {
116
+ issues.push({
117
+ severity: "error",
118
+ path: `${path}.valueQuantity.unit`,
119
+ code: "invalid-type",
120
+ message: `Expected '${path}.valueQuantity.unit' to be a string.`,
121
+ });
122
+ }
123
+ if (q.system !== undefined && typeof q.system !== "string") {
124
+ issues.push({
125
+ severity: "error",
126
+ path: `${path}.valueQuantity.system`,
127
+ code: "invalid-type",
128
+ message: `Expected '${path}.valueQuantity.system' to be a string.`,
129
+ });
130
+ }
131
+ if (q.code !== undefined && typeof q.code !== "string") {
132
+ issues.push({
133
+ severity: "error",
134
+ path: `${path}.valueQuantity.code`,
135
+ code: "invalid-type",
136
+ message: `Expected '${path}.valueQuantity.code' to be a string.`,
137
+ });
138
+ }
139
+ }
140
+ }
141
+ // valueCodeableConcept
142
+ if (resource.valueCodeableConcept !== undefined) {
143
+ issues.push(...validateCodeableConcept(resource.valueCodeableConcept, `${path}.valueCodeableConcept`));
144
+ }
145
+ // component (Array of components)
146
+ if (resource.component !== undefined) {
147
+ if (!Array.isArray(resource.component)) {
148
+ issues.push({
149
+ severity: "error",
150
+ path: `${path}.component`,
151
+ code: "invalid-type",
152
+ message: `Expected '${path}.component' to be an array, got ${typeof resource.component}.`,
153
+ });
154
+ }
155
+ else {
156
+ resource.component.forEach((comp, idx) => {
157
+ const compPath = `${path}.component[${idx}]`;
158
+ if (typeof comp !== "object" || comp === null || Array.isArray(comp)) {
159
+ issues.push({
160
+ severity: "error",
161
+ path: compPath,
162
+ code: "invalid-structure",
163
+ message: `Expected '${compPath}' to be an object.`,
164
+ });
165
+ }
166
+ else {
167
+ const c = comp;
168
+ if (c.code === undefined || c.code === null) {
169
+ issues.push({
170
+ severity: "error",
171
+ path: `${compPath}.code`,
172
+ code: "required",
173
+ message: `Missing required 'code' in '${compPath}'.`,
174
+ });
175
+ }
176
+ else {
177
+ issues.push(...validateCodeableConcept(c.code, `${compPath}.code`, { requireCoding: true }));
178
+ }
179
+ if (c.valueQuantity !== undefined) {
180
+ const q = c.valueQuantity;
181
+ if (q && typeof q.value !== "number" && q.value !== undefined) {
182
+ issues.push({
183
+ severity: "error",
184
+ path: `${compPath}.valueQuantity.value`,
185
+ code: "invalid-type",
186
+ message: `Expected '${compPath}.valueQuantity.value' to be a number.`,
187
+ });
188
+ }
189
+ }
190
+ if (c.valueCodeableConcept !== undefined) {
191
+ issues.push(...validateCodeableConcept(c.valueCodeableConcept, `${compPath}.valueCodeableConcept`));
192
+ }
193
+ }
194
+ });
195
+ }
196
+ }
197
+ // hasMember / derivedFrom
198
+ if (resource.hasMember !== undefined) {
199
+ if (Array.isArray(resource.hasMember)) {
200
+ resource.hasMember.forEach((hm, idx) => {
201
+ issues.push(...validateReference(hm, `${path}.hasMember[${idx}]`, ["Observation", "QuestionnaireResponse", "MolecularSequence"]));
202
+ });
203
+ }
204
+ }
205
+ if (resource.derivedFrom !== undefined) {
206
+ if (Array.isArray(resource.derivedFrom)) {
207
+ resource.derivedFrom.forEach((df, idx) => {
208
+ issues.push(...validateReference(df, `${path}.derivedFrom[${idx}]`, ["DocumentReference", "ImagingStudy", "Media", "QuestionnaireResponse", "Observation", "MolecularSequence"]));
209
+ });
210
+ }
211
+ }
212
+ return issues;
213
+ }
@@ -0,0 +1,5 @@
1
+ import { ValidationIssue } from "../types.js";
2
+ /**
3
+ * Validates base structural rules for FHIR R4 Patient resource.
4
+ */
5
+ export declare function validatePatient(resource: Record<string, unknown>, path?: string): ValidationIssue[];
@@ -0,0 +1,163 @@
1
+ import { validateCodeableConcept } from "../coding.js";
2
+ import { validateReference } from "../reference-rules.js";
3
+ const VALID_GENDERS = ["male", "female", "other", "unknown"];
4
+ const DATE_REGEX = /^\d{4}(-\d{2}(-\d{2})?)?$/;
5
+ /**
6
+ * Validates base structural rules for FHIR R4 Patient resource.
7
+ */
8
+ export function validatePatient(resource, path = "Patient") {
9
+ const issues = [];
10
+ if (resource.resourceType !== "Patient") {
11
+ issues.push({
12
+ severity: "error",
13
+ path: `${path}.resourceType`,
14
+ code: "invalid-resource-type",
15
+ message: `Expected resourceType 'Patient', found '${String(resource.resourceType)}'.`,
16
+ });
17
+ }
18
+ // active
19
+ if (resource.active !== undefined && typeof resource.active !== "boolean") {
20
+ issues.push({
21
+ severity: "error",
22
+ path: `${path}.active`,
23
+ code: "invalid-type",
24
+ message: `Expected '${path}.active' to be a boolean, got ${typeof resource.active}.`,
25
+ });
26
+ }
27
+ // gender
28
+ if (resource.gender !== undefined) {
29
+ if (typeof resource.gender !== "string" || !VALID_GENDERS.includes(resource.gender)) {
30
+ issues.push({
31
+ severity: "error",
32
+ path: `${path}.gender`,
33
+ code: "invalid-value",
34
+ message: `Invalid gender '${String(resource.gender)}'. Allowed values: ${VALID_GENDERS.join(", ")}.`,
35
+ });
36
+ }
37
+ }
38
+ // birthDate
39
+ if (resource.birthDate !== undefined) {
40
+ if (typeof resource.birthDate !== "string" || !DATE_REGEX.test(resource.birthDate)) {
41
+ issues.push({
42
+ severity: "error",
43
+ path: `${path}.birthDate`,
44
+ code: "invalid-format",
45
+ message: `Expected '${path}.birthDate' to match YYYY, YYYY-MM, or YYYY-MM-DD format.`,
46
+ });
47
+ }
48
+ }
49
+ // name (HumanName[])
50
+ if (resource.name !== undefined) {
51
+ if (!Array.isArray(resource.name)) {
52
+ issues.push({
53
+ severity: "error",
54
+ path: `${path}.name`,
55
+ code: "invalid-type",
56
+ message: `Expected '${path}.name' to be an array, got ${typeof resource.name}.`,
57
+ });
58
+ }
59
+ else {
60
+ resource.name.forEach((n, idx) => {
61
+ const namePath = `${path}.name[${idx}]`;
62
+ if (typeof n !== "object" || n === null) {
63
+ issues.push({
64
+ severity: "error",
65
+ path: namePath,
66
+ code: "invalid-structure",
67
+ message: `Expected '${namePath}' to be a HumanName object.`,
68
+ });
69
+ }
70
+ else {
71
+ const hn = n;
72
+ if (hn.family !== undefined && typeof hn.family !== "string") {
73
+ issues.push({
74
+ severity: "error",
75
+ path: `${namePath}.family`,
76
+ code: "invalid-type",
77
+ message: `Expected '${namePath}.family' to be a string.`,
78
+ });
79
+ }
80
+ if (hn.given !== undefined && (!Array.isArray(hn.given) || hn.given.some((g) => typeof g !== "string"))) {
81
+ issues.push({
82
+ severity: "error",
83
+ path: `${namePath}.given`,
84
+ code: "invalid-type",
85
+ message: `Expected '${namePath}.given' to be an array of strings.`,
86
+ });
87
+ }
88
+ }
89
+ });
90
+ }
91
+ }
92
+ // identifier (Identifier[])
93
+ if (resource.identifier !== undefined) {
94
+ if (!Array.isArray(resource.identifier)) {
95
+ issues.push({
96
+ severity: "error",
97
+ path: `${path}.identifier`,
98
+ code: "invalid-type",
99
+ message: `Expected '${path}.identifier' to be an array, got ${typeof resource.identifier}.`,
100
+ });
101
+ }
102
+ else {
103
+ resource.identifier.forEach((ident, idx) => {
104
+ const idPath = `${path}.identifier[${idx}]`;
105
+ if (typeof ident !== "object" || ident === null) {
106
+ issues.push({
107
+ severity: "error",
108
+ path: idPath,
109
+ code: "invalid-structure",
110
+ message: `Expected '${idPath}' to be an Identifier object.`,
111
+ });
112
+ }
113
+ else {
114
+ const idObj = ident;
115
+ if (idObj.system !== undefined && typeof idObj.system !== "string") {
116
+ issues.push({
117
+ severity: "error",
118
+ path: `${idPath}.system`,
119
+ code: "invalid-type",
120
+ message: `Expected '${idPath}.system' to be a string.`,
121
+ });
122
+ }
123
+ if (idObj.value !== undefined && typeof idObj.value !== "string") {
124
+ issues.push({
125
+ severity: "error",
126
+ path: `${idPath}.value`,
127
+ code: "invalid-type",
128
+ message: `Expected '${idPath}.value' to be a string.`,
129
+ });
130
+ }
131
+ if (idObj.type !== undefined) {
132
+ issues.push(...validateCodeableConcept(idObj.type, `${idPath}.type`));
133
+ }
134
+ }
135
+ });
136
+ }
137
+ }
138
+ // generalPractitioner (Reference[])
139
+ if (resource.generalPractitioner !== undefined) {
140
+ if (!Array.isArray(resource.generalPractitioner)) {
141
+ issues.push({
142
+ severity: "error",
143
+ path: `${path}.generalPractitioner`,
144
+ code: "invalid-type",
145
+ message: `Expected '${path}.generalPractitioner' to be an array.`,
146
+ });
147
+ }
148
+ else {
149
+ resource.generalPractitioner.forEach((gp, idx) => {
150
+ issues.push(...validateReference(gp, `${path}.generalPractitioner[${idx}]`, ["Organization", "Practitioner", "PractitionerRole"]));
151
+ });
152
+ }
153
+ }
154
+ // managingOrganization (Reference)
155
+ if (resource.managingOrganization !== undefined) {
156
+ issues.push(...validateReference(resource.managingOrganization, `${path}.managingOrganization`, ["Organization"]));
157
+ }
158
+ // maritalStatus (CodeableConcept)
159
+ if (resource.maritalStatus !== undefined) {
160
+ issues.push(...validateCodeableConcept(resource.maritalStatus, `${path}.maritalStatus`));
161
+ }
162
+ return issues;
163
+ }
@@ -0,0 +1,17 @@
1
+ import { ValidationIssue } from "./types.js";
2
+ /**
3
+ * Validates the structural shape of a FHIR Coding element.
4
+ * Checks for non-empty string code and valid URI system format.
5
+ */
6
+ export declare function validateCoding(coding: unknown, path: string, options?: {
7
+ requireSystem?: boolean;
8
+ requireCode?: boolean;
9
+ }): ValidationIssue[];
10
+ /**
11
+ * Validates the structural shape of a FHIR CodeableConcept element.
12
+ * Checks for coding array structure and validates individual Coding elements.
13
+ */
14
+ export declare function validateCodeableConcept(concept: unknown, path: string, options?: {
15
+ requireCoding?: boolean;
16
+ minCodings?: number;
17
+ }): ValidationIssue[];
package/dist/coding.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Validates the structural shape of a FHIR Coding element.
3
+ * Checks for non-empty string code and valid URI system format.
4
+ */
5
+ export function validateCoding(coding, path, options) {
6
+ const issues = [];
7
+ if (!coding || typeof coding !== "object" || Array.isArray(coding)) {
8
+ issues.push({
9
+ severity: "error",
10
+ path,
11
+ code: "invalid-structure",
12
+ message: `Expected '${path}' to be a Coding object, got ${Array.isArray(coding) ? "array" : typeof coding}.`,
13
+ });
14
+ return issues;
15
+ }
16
+ const c = coding;
17
+ const requireSystem = options?.requireSystem ?? true;
18
+ const requireCode = options?.requireCode ?? true;
19
+ if (requireCode) {
20
+ if (c.code === undefined || c.code === null || c.code === "") {
21
+ issues.push({
22
+ severity: "error",
23
+ path: `${path}.code`,
24
+ code: "required",
25
+ message: `Missing required 'code' in '${path}'.`,
26
+ });
27
+ }
28
+ else if (typeof c.code !== "string") {
29
+ issues.push({
30
+ severity: "error",
31
+ path: `${path}.code`,
32
+ code: "invalid-type",
33
+ message: `Expected 'code' in '${path}' to be a string, got ${typeof c.code}.`,
34
+ });
35
+ }
36
+ }
37
+ else if (c.code !== undefined && typeof c.code !== "string") {
38
+ issues.push({
39
+ severity: "error",
40
+ path: `${path}.code`,
41
+ code: "invalid-type",
42
+ message: `Expected 'code' in '${path}' to be a string, got ${typeof c.code}.`,
43
+ });
44
+ }
45
+ if (requireSystem) {
46
+ if (c.system === undefined || c.system === null || c.system === "") {
47
+ issues.push({
48
+ severity: "error",
49
+ path: `${path}.system`,
50
+ code: "required",
51
+ message: `Missing required 'system' URI in '${path}'.`,
52
+ });
53
+ }
54
+ else if (typeof c.system !== "string") {
55
+ issues.push({
56
+ severity: "error",
57
+ path: `${path}.system`,
58
+ code: "invalid-type",
59
+ message: `Expected 'system' in '${path}' to be a string URI, got ${typeof c.system}.`,
60
+ });
61
+ }
62
+ }
63
+ else if (c.system !== undefined && typeof c.system !== "string") {
64
+ issues.push({
65
+ severity: "error",
66
+ path: `${path}.system`,
67
+ code: "invalid-type",
68
+ message: `Expected 'system' in '${path}' to be a string URI, got ${typeof c.system}.`,
69
+ });
70
+ }
71
+ if (c.display !== undefined && typeof c.display !== "string") {
72
+ issues.push({
73
+ severity: "error",
74
+ path: `${path}.display`,
75
+ code: "invalid-type",
76
+ message: `Expected 'display' in '${path}' to be a string, got ${typeof c.display}.`,
77
+ });
78
+ }
79
+ return issues;
80
+ }
81
+ /**
82
+ * Validates the structural shape of a FHIR CodeableConcept element.
83
+ * Checks for coding array structure and validates individual Coding elements.
84
+ */
85
+ export function validateCodeableConcept(concept, path, options) {
86
+ const issues = [];
87
+ if (!concept || typeof concept !== "object" || Array.isArray(concept)) {
88
+ issues.push({
89
+ severity: "error",
90
+ path,
91
+ code: "invalid-structure",
92
+ message: `Expected '${path}' to be a CodeableConcept object, got ${Array.isArray(concept) ? "array" : typeof concept}.`,
93
+ });
94
+ return issues;
95
+ }
96
+ const cc = concept;
97
+ const requireCoding = options?.requireCoding ?? false;
98
+ const minCodings = options?.minCodings ?? (requireCoding ? 1 : 0);
99
+ if (cc.coding !== undefined) {
100
+ if (!Array.isArray(cc.coding)) {
101
+ issues.push({
102
+ severity: "error",
103
+ path: `${path}.coding`,
104
+ code: "invalid-type",
105
+ message: `Expected '${path}.coding' to be an array, got ${typeof cc.coding}.`,
106
+ });
107
+ }
108
+ else {
109
+ if (cc.coding.length < minCodings) {
110
+ issues.push({
111
+ severity: "error",
112
+ path: `${path}.coding`,
113
+ code: "cardinality",
114
+ message: `Expected at least ${minCodings} coding(s) in '${path}.coding', found ${cc.coding.length}.`,
115
+ });
116
+ }
117
+ cc.coding.forEach((item, idx) => {
118
+ issues.push(...validateCoding(item, `${path}.coding[${idx}]`));
119
+ });
120
+ }
121
+ }
122
+ else if (requireCoding || minCodings > 0) {
123
+ issues.push({
124
+ severity: "error",
125
+ path: `${path}.coding`,
126
+ code: "required",
127
+ message: `Missing required 'coding' array in '${path}'.`,
128
+ });
129
+ }
130
+ if (cc.text !== undefined && typeof cc.text !== "string") {
131
+ issues.push({
132
+ severity: "error",
133
+ path: `${path}.text`,
134
+ code: "invalid-type",
135
+ message: `Expected 'text' in '${path}' to be a string, got ${typeof cc.text}.`,
136
+ });
137
+ }
138
+ return issues;
139
+ }