@kravc/schema 2.7.6 → 2.8.0-alpha.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.
Files changed (176) hide show
  1. package/README.md +19 -14
  2. package/dist/CredentialFactory.d.ts +345 -0
  3. package/dist/CredentialFactory.d.ts.map +1 -0
  4. package/dist/CredentialFactory.js +381 -0
  5. package/dist/CredentialFactory.js.map +1 -0
  6. package/dist/Schema.d.ts +448 -0
  7. package/dist/Schema.d.ts.map +1 -0
  8. package/dist/Schema.js +506 -0
  9. package/dist/Schema.js.map +1 -0
  10. package/dist/ValidationError.d.ts +70 -0
  11. package/dist/ValidationError.d.ts.map +1 -0
  12. package/dist/ValidationError.js +78 -0
  13. package/dist/ValidationError.js.map +1 -0
  14. package/dist/Validator.d.ts +483 -0
  15. package/dist/Validator.d.ts.map +1 -0
  16. package/dist/Validator.js +570 -0
  17. package/dist/Validator.js.map +1 -0
  18. package/dist/helpers/JsonSchema.d.ts +99 -0
  19. package/dist/helpers/JsonSchema.d.ts.map +1 -0
  20. package/dist/helpers/JsonSchema.js +3 -0
  21. package/dist/helpers/JsonSchema.js.map +1 -0
  22. package/dist/helpers/cleanupAttributes.d.ts +34 -0
  23. package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
  24. package/dist/helpers/cleanupAttributes.js +113 -0
  25. package/dist/helpers/cleanupAttributes.js.map +1 -0
  26. package/dist/helpers/cleanupNulls.d.ts +27 -0
  27. package/dist/helpers/cleanupNulls.d.ts.map +1 -0
  28. package/dist/helpers/cleanupNulls.js +96 -0
  29. package/dist/helpers/cleanupNulls.js.map +1 -0
  30. package/dist/helpers/createSchemasMap.d.ts +67 -0
  31. package/dist/helpers/createSchemasMap.d.ts.map +1 -0
  32. package/dist/helpers/createSchemasMap.js +200 -0
  33. package/dist/helpers/createSchemasMap.js.map +1 -0
  34. package/dist/helpers/getReferenceIds.d.ts +169 -0
  35. package/dist/helpers/getReferenceIds.d.ts.map +1 -0
  36. package/dist/helpers/getReferenceIds.js +241 -0
  37. package/dist/helpers/getReferenceIds.js.map +1 -0
  38. package/dist/helpers/got.d.ts +60 -0
  39. package/dist/helpers/got.d.ts.map +1 -0
  40. package/dist/helpers/got.js +72 -0
  41. package/dist/helpers/got.js.map +1 -0
  42. package/dist/helpers/mapObjectProperties.d.ts +150 -0
  43. package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
  44. package/dist/helpers/mapObjectProperties.js +229 -0
  45. package/dist/helpers/mapObjectProperties.js.map +1 -0
  46. package/dist/helpers/normalizeAttributes.d.ts +213 -0
  47. package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
  48. package/dist/helpers/normalizeAttributes.js +243 -0
  49. package/dist/helpers/normalizeAttributes.js.map +1 -0
  50. package/dist/helpers/normalizeProperties.d.ts +168 -0
  51. package/dist/helpers/normalizeProperties.d.ts.map +1 -0
  52. package/dist/helpers/normalizeProperties.js +223 -0
  53. package/dist/helpers/normalizeProperties.js.map +1 -0
  54. package/dist/helpers/normalizeRequired.d.ts +159 -0
  55. package/dist/helpers/normalizeRequired.d.ts.map +1 -0
  56. package/dist/helpers/normalizeRequired.js +206 -0
  57. package/dist/helpers/normalizeRequired.js.map +1 -0
  58. package/dist/helpers/normalizeType.d.ts +81 -0
  59. package/dist/helpers/normalizeType.d.ts.map +1 -0
  60. package/dist/helpers/normalizeType.js +210 -0
  61. package/dist/helpers/normalizeType.js.map +1 -0
  62. package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
  63. package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
  64. package/dist/helpers/nullifyEmptyValues.js +191 -0
  65. package/dist/helpers/nullifyEmptyValues.js.map +1 -0
  66. package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
  67. package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
  68. package/dist/helpers/removeRequiredAndDefault.js +138 -0
  69. package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
  70. package/dist/helpers/validateId.d.ts +39 -0
  71. package/dist/helpers/validateId.d.ts.map +1 -0
  72. package/dist/helpers/validateId.js +51 -0
  73. package/dist/helpers/validateId.js.map +1 -0
  74. package/dist/index.d.ts +9 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +21 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/ld/documentLoader.d.ts +8 -0
  79. package/dist/ld/documentLoader.d.ts.map +1 -0
  80. package/dist/ld/documentLoader.js +24 -0
  81. package/dist/ld/documentLoader.js.map +1 -0
  82. package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
  83. package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
  84. package/dist/ld/getLinkedDataAttributeType.js +32 -0
  85. package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
  86. package/dist/ld/getLinkedDataContext.d.ts +19 -0
  87. package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
  88. package/dist/ld/getLinkedDataContext.js +50 -0
  89. package/dist/ld/getLinkedDataContext.js.map +1 -0
  90. package/eslint.config.mjs +32 -52
  91. package/examples/credentials/createAccountCredential.ts +27 -0
  92. package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
  93. package/examples/index.ts +7 -0
  94. package/examples/schemas/FavoriteItemSchema.ts +27 -0
  95. package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
  96. package/examples/schemas/PreferencesSchema.ts +29 -0
  97. package/examples/schemas/ProfileSchema.ts +91 -0
  98. package/examples/schemas/Status.yaml +3 -0
  99. package/examples/schemas/StatusSchema.ts +12 -0
  100. package/jest.config.mjs +5 -0
  101. package/package.json +27 -20
  102. package/src/CredentialFactory.ts +392 -0
  103. package/src/Schema.ts +583 -0
  104. package/src/ValidationError.ts +90 -0
  105. package/src/Validator.ts +603 -0
  106. package/src/__tests__/CredentialFactory.test.ts +588 -0
  107. package/src/__tests__/Schema.test.ts +371 -0
  108. package/src/__tests__/ValidationError.test.ts +235 -0
  109. package/src/__tests__/Validator.test.ts +787 -0
  110. package/src/helpers/JsonSchema.ts +119 -0
  111. package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
  112. package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
  113. package/src/helpers/__tests__/createSchemasMap.test.ts +238 -0
  114. package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
  115. package/src/helpers/__tests__/got.test.ts +193 -0
  116. package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
  117. package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
  118. package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
  119. package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
  120. package/src/helpers/__tests__/normalizeType.test.ts +772 -0
  121. package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
  122. package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
  123. package/src/helpers/__tests__/validateId.test.ts +118 -0
  124. package/src/helpers/cleanupAttributes.ts +151 -0
  125. package/src/helpers/cleanupNulls.ts +106 -0
  126. package/src/helpers/createSchemasMap.ts +212 -0
  127. package/src/helpers/getReferenceIds.ts +273 -0
  128. package/src/helpers/got.ts +73 -0
  129. package/src/helpers/mapObjectProperties.ts +272 -0
  130. package/src/helpers/normalizeAttributes.ts +247 -0
  131. package/src/helpers/normalizeProperties.ts +249 -0
  132. package/src/helpers/normalizeRequired.ts +233 -0
  133. package/src/helpers/normalizeType.ts +235 -0
  134. package/src/helpers/nullifyEmptyValues.ts +207 -0
  135. package/src/helpers/removeRequiredAndDefault.ts +151 -0
  136. package/src/helpers/validateId.ts +53 -0
  137. package/src/index.ts +17 -0
  138. package/src/ld/__tests__/documentLoader.test.ts +57 -0
  139. package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
  140. package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
  141. package/src/ld/documentLoader.ts +28 -0
  142. package/src/ld/getLinkedDataAttributeType.ts +46 -0
  143. package/src/ld/getLinkedDataContext.ts +80 -0
  144. package/tsconfig.json +27 -0
  145. package/types/credentials-context.d.ts +14 -0
  146. package/types/security-context.d.ts +6 -0
  147. package/examples/Status.yaml +0 -3
  148. package/examples/createAccountCredential.js +0 -27
  149. package/examples/createMineSweeperScoreCredential.js +0 -63
  150. package/examples/index.js +0 -9
  151. package/src/CredentialFactory.js +0 -67
  152. package/src/CredentialFactory.spec.js +0 -131
  153. package/src/Schema.js +0 -104
  154. package/src/Schema.spec.js +0 -172
  155. package/src/ValidationError.js +0 -31
  156. package/src/Validator.js +0 -128
  157. package/src/Validator.spec.js +0 -355
  158. package/src/helpers/cleanupAttributes.js +0 -71
  159. package/src/helpers/cleanupNulls.js +0 -42
  160. package/src/helpers/getReferenceIds.js +0 -71
  161. package/src/helpers/mapObject.js +0 -65
  162. package/src/helpers/normalizeAttributes.js +0 -28
  163. package/src/helpers/normalizeProperties.js +0 -61
  164. package/src/helpers/normalizeRequired.js +0 -37
  165. package/src/helpers/normalizeType.js +0 -41
  166. package/src/helpers/nullifyEmptyValues.js +0 -57
  167. package/src/helpers/removeRequiredAndDefault.js +0 -30
  168. package/src/helpers/validateId.js +0 -19
  169. package/src/index.d.ts +0 -25
  170. package/src/index.js +0 -8
  171. package/src/ld/documentLoader.js +0 -25
  172. package/src/ld/documentLoader.spec.js +0 -12
  173. package/src/ld/getLinkedDataContext.js +0 -63
  174. package/src/ld/getLinkedDataType.js +0 -38
  175. /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
  176. /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
@@ -0,0 +1,371 @@
1
+ import { load } from 'js-yaml';
2
+ import { readFileSync } from 'fs';
3
+ import { Schema } from '../../src';
4
+ import type {
5
+ PropertiesSchema,
6
+ EnumSchema,
7
+ StringPropertySchema,
8
+ ArrayPropertySchema,
9
+ ObjectPropertySchema,
10
+ ObjectSchema
11
+ } from '../../src/helpers/JsonSchema';
12
+
13
+ // eslint-disable-next-line jsdoc/require-jsdoc
14
+ const loadSync = (yamlPath: string): Schema => {
15
+ const id = yamlPath.split('.')[0].split('/').reverse()[0];
16
+ // Path is relative to project root
17
+ const fullPath = yamlPath.startsWith('/') ? yamlPath : `${process.cwd()}/${yamlPath}`;
18
+ const source = load(readFileSync(fullPath, 'utf8')) as PropertiesSchema | EnumSchema;
19
+
20
+ return new Schema(source, id);
21
+ };
22
+
23
+ describe('Schema', () => {
24
+ describe('Schema.constructor(source = {}, id = UNDEFINED_SCHEMA_ID)', () => {
25
+ it('creates empty schema with default id', () => {
26
+ const schema = new Schema({});
27
+
28
+ expect(schema.id).toBe('UNDEFINED_SCHEMA_ID');
29
+ expect(Object.keys(schema.source)).toHaveLength(0);
30
+ });
31
+
32
+ it('extends schema properties with types', () => {
33
+ const schema = loadSync('examples/schemas/Profile.yaml');
34
+ const source = schema.source as PropertiesSchema;
35
+
36
+ expect((source.name as StringPropertySchema).type).toEqual('string');
37
+ expect((source.gender as StringPropertySchema).type).toEqual('string');
38
+ // items can be various property schema types, assert as StringPropertySchema for this test
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ expect(((source.tags as ArrayPropertySchema).items as any as StringPropertySchema)?.type).toEqual('string');
41
+ expect((source.favoriteItems as ArrayPropertySchema).type).toEqual('array');
42
+ // items may not have type property depending on the schema structure
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ expect(((source.favoriteItems as ArrayPropertySchema).items as any)?.type).toBeUndefined();
45
+ expect((source.locations as ArrayPropertySchema).type).toEqual('array');
46
+ const locationsItems = (source.locations as ArrayPropertySchema).items as ObjectPropertySchema;
47
+ expect(locationsItems?.type).toEqual('object');
48
+ expect((locationsItems.properties?.name as StringPropertySchema)?.type).toEqual('string');
49
+ expect((locationsItems.properties?.address as ObjectPropertySchema)?.type).toEqual('object');
50
+ const addressProperty = locationsItems.properties?.address as ObjectPropertySchema;
51
+ expect((addressProperty.properties?.zip as StringPropertySchema)?.type).toEqual('string');
52
+ expect((addressProperty.properties?.city as StringPropertySchema)?.type).toEqual('string');
53
+ expect((addressProperty.properties?.country as StringPropertySchema)?.type).toEqual('string');
54
+ expect((addressProperty.properties?.addressLine1 as StringPropertySchema)?.type).toEqual('string');
55
+ expect((addressProperty.properties?.addressLine2 as StringPropertySchema)?.type).toEqual('string');
56
+
57
+ const stringEnumSchema = new Schema({ enum: ['L', 'M', 'S'] } as EnumSchema, 'Size');
58
+
59
+ expect((stringEnumSchema.source as EnumSchema).type).toEqual('string');
60
+
61
+ // Testing number enum - EnumSchema type only allows string[], but the implementation supports number[]
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ const numbersEnumSchema = new Schema({ enum: [1, 2, 3] as any, type: 'number' } as EnumSchema, 'Points');
64
+
65
+ expect((numbersEnumSchema.source as EnumSchema).type).toEqual('number');
66
+ });
67
+
68
+ it('creates schema from other schemas source', () => {
69
+ const entitySchema = new Schema({ name: { type: 'string' } }, 'Entity');
70
+
71
+ const schema = new Schema(entitySchema, 'EntityClone');
72
+
73
+ expect(schema.id).toBe('EntityClone');
74
+ expect(schema.source).toEqual(entitySchema.source);
75
+ });
76
+ });
77
+
78
+ describe('.pure(id)', () => {
79
+ it('returns schema without required and default attributes', () => {
80
+ const profileSchema = loadSync('examples/schemas/Profile.yaml');
81
+ const updateProfileSchema = profileSchema.pure('UpdateProfile');
82
+
83
+ expect(updateProfileSchema.id).toEqual('UpdateProfile');
84
+
85
+ const source = updateProfileSchema.source as PropertiesSchema;
86
+
87
+ expect((source.name as StringPropertySchema).required).toBeUndefined();
88
+ expect((source.gender as StringPropertySchema).default).toBeUndefined();
89
+ const contactDetails = source.contactDetails as ObjectPropertySchema;
90
+ expect(contactDetails.required).toBeUndefined();
91
+ expect(contactDetails.properties?.email?.required).toBeUndefined();
92
+ expect(contactDetails.properties?.mobileNumber?.default).toBeUndefined();
93
+ const locations = source.locations as ArrayPropertySchema;
94
+ const locationsItems = locations.items as ObjectPropertySchema;
95
+ // Note: 'require' is a typo in the original test, but keeping it to match expected behavior
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ expect((locationsItems.properties?.name as any)?.require).toBeUndefined();
98
+ const addressProperty = locationsItems.properties?.address as ObjectPropertySchema;
99
+ expect((addressProperty.properties?.country as StringPropertySchema)?.required).toBeUndefined();
100
+ expect((addressProperty.properties?.country as StringPropertySchema)?.default).toBeUndefined();
101
+ expect((addressProperty.properties?.city as StringPropertySchema)?.required).toBeUndefined();
102
+ expect((addressProperty.properties?.addressLine1 as StringPropertySchema)?.required).toBeUndefined();
103
+ expect((addressProperty.properties?.addressLine2 as StringPropertySchema)?.required).toBeUndefined();
104
+ expect((addressProperty.properties?.zip as StringPropertySchema)?.required).toBeUndefined();
105
+ });
106
+ });
107
+
108
+ describe('.clone(id)', () => {
109
+ it('returns schema clone', () => {
110
+ const profileSchema = loadSync('examples/schemas/Profile.yaml');
111
+
112
+ const schema = profileSchema.clone('ProfileClone');
113
+ expect(schema.id).toEqual('ProfileClone');
114
+ });
115
+ });
116
+
117
+ describe('.only(propertyNames, id)', () => {
118
+ it('returns schema with only requested properties', () => {
119
+ const profileSchema = loadSync('examples/schemas/Profile.yaml');
120
+
121
+ const schema = profileSchema.only(['name', 'gender'], 'ProfileClone');
122
+ expect(schema.id).toEqual('ProfileClone');
123
+ });
124
+ });
125
+
126
+ describe('.extend(properties, id)', () => {
127
+ it('returns schema extended with specified properties', () => {
128
+ const profileSchema = loadSync('examples/schemas/Profile.yaml');
129
+
130
+ const documentSource: PropertiesSchema = {
131
+ createdAt: {
132
+ type: 'string',
133
+ format: 'date-time',
134
+ required: true
135
+ }
136
+ };
137
+
138
+ const profileDocumentSchema = profileSchema.extend(documentSource, 'ProfileDocument');
139
+
140
+ expect(profileDocumentSchema.id).toEqual('ProfileDocument');
141
+ expect((profileDocumentSchema.source as PropertiesSchema).createdAt).toBeDefined();
142
+ });
143
+ });
144
+
145
+ describe('.wrap(propertyName, options = { required: true }, id)', () => {
146
+ it('returns schema that wraps source schema with object property', () => {
147
+ const profileSchema = loadSync('examples/schemas/Profile.yaml');
148
+
149
+ // Test default behavior when id is undefined (should default to UNDEFINED_SCHEMA_ID)
150
+ // wrap() requires id: string, but we're testing the default behavior with undefined
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ const dataSchema = profileSchema.wrap('data', { required: true }, undefined as any);
153
+ const dataSource = dataSchema.source as PropertiesSchema;
154
+ expect(dataSchema.id).toBe('UNDEFINED_SCHEMA_ID');
155
+ expect(dataSource.data).toBeDefined();
156
+ expect((dataSource.data as ObjectPropertySchema).required).toBeDefined();
157
+
158
+ // wrap() expects default to be string, but we're testing with object-like string
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ const alternativeDataSchema = profileSchema.wrap('data', { default: '{}' as any }, 'ResponseOutput');
161
+ const altSource = alternativeDataSchema.source as PropertiesSchema;
162
+ expect(alternativeDataSchema.id).toBe('ResponseOutput');
163
+ expect(altSource.data).toBeDefined();
164
+ expect((altSource.data as ObjectPropertySchema).default).toBeDefined();
165
+ expect((altSource.data as ObjectPropertySchema).required).toBeUndefined();
166
+ });
167
+ });
168
+
169
+ describe('.jsonSchema', () => {
170
+ it('returns json schema for enum type', () => {
171
+ const source: EnumSchema = {
172
+ type: 'string',
173
+ enum: ['L', 'M', 'S']
174
+ };
175
+
176
+ const sizeSchema = new Schema(source, 'Size');
177
+ const jsonSchema = sizeSchema.jsonSchema as EnumSchema;
178
+
179
+ expect(jsonSchema.id).toEqual('Size');
180
+ expect(jsonSchema.type).toEqual('string');
181
+ expect(jsonSchema.enum).toEqual(source.enum);
182
+ });
183
+
184
+ it('returns json schema with normalized required attributes', () => {
185
+ const profileSchema = loadSync('examples/schemas/Profile.yaml');
186
+
187
+ const { jsonSchema } = profileSchema;
188
+ const objectSchema = jsonSchema as ObjectSchema;
189
+
190
+ expect(objectSchema).toHaveProperty('type', 'object');
191
+ expect(objectSchema).toHaveProperty('properties');
192
+ expect(objectSchema.required).toEqual(['name', 'contactDetails']);
193
+ expect(objectSchema.properties.contactDetails.required).toEqual(['email']);
194
+ });
195
+
196
+ it('returns json schema without required attributes', () => {
197
+ const schema = new Schema({ name: { type: 'string' } }, 'Entity');
198
+
199
+ const { jsonSchema } = schema;
200
+
201
+ expect(jsonSchema.required).toBeUndefined();
202
+ });
203
+ });
204
+
205
+ describe('.url', () => {
206
+ it('returns undefined when url is not provided', () => {
207
+ const schema = new Schema({ name: { type: 'string' } }, 'Entity');
208
+
209
+ expect(schema.url).toBeUndefined();
210
+ });
211
+
212
+ it('returns url when provided in constructor', () => {
213
+ const schema = new Schema(
214
+ { name: { type: 'string' } },
215
+ 'Entity',
216
+ 'https://example.com/'
217
+ );
218
+
219
+ expect(schema.url).toBe('https://example.com/');
220
+ });
221
+ });
222
+
223
+ describe('.isEnum', () => {
224
+ it('returns true for enum schema', () => {
225
+ const enumSchema = new Schema({ enum: ['L', 'M', 'S'] } as EnumSchema, 'Size');
226
+
227
+ expect(enumSchema.isEnum).toBe(true);
228
+ });
229
+
230
+ it('returns false for properties schema', () => {
231
+ const schema = new Schema({ name: { type: 'string' } }, 'Entity');
232
+
233
+ expect(schema.isEnum).toBe(false);
234
+ });
235
+ });
236
+
237
+ describe('.linkedDataType', () => {
238
+ it('returns undefined when url is not provided', () => {
239
+ const schema = new Schema({ name: { type: 'string' } }, 'Entity');
240
+
241
+ expect(schema.linkedDataType).toBeUndefined();
242
+ });
243
+
244
+ it('returns linked data type when url is provided', () => {
245
+ const schema = new Schema(
246
+ { name: { type: 'string' } },
247
+ 'Entity',
248
+ 'https://example.com/'
249
+ );
250
+
251
+ const linkedDataType = schema.linkedDataType;
252
+
253
+ expect(linkedDataType).toBeDefined();
254
+ expect(linkedDataType?.['@id']).toBe('https://example.com/Entity');
255
+ expect(linkedDataType?.['@context']).toBeDefined();
256
+ });
257
+
258
+ it('creates linked data type with url ending in slash', () => {
259
+ const schema = new Schema(
260
+ { name: { type: 'string' } },
261
+ 'Entity',
262
+ 'https://example.com/'
263
+ );
264
+
265
+ expect(schema.linkedDataType?.['@id']).toBe('https://example.com/Entity');
266
+ });
267
+
268
+ it('creates linked data type with url ending in hash', () => {
269
+ const schema = new Schema(
270
+ { name: { type: 'string' } },
271
+ 'Entity',
272
+ 'https://example.com#'
273
+ );
274
+
275
+ expect(schema.linkedDataType?.['@id']).toBe('https://example.com#Entity');
276
+ });
277
+
278
+ it('creates linked data type with url not ending in slash or hash', () => {
279
+ const schema = new Schema(
280
+ { name: { type: 'string' } },
281
+ 'Entity',
282
+ 'https://example.com'
283
+ );
284
+
285
+ expect(schema.linkedDataType?.['@id']).toBe('https://example.com#Entity');
286
+ });
287
+
288
+ it('handles source with id property when creating linked data type', () => {
289
+ const source: PropertiesSchema = {
290
+ id: { type: 'string' },
291
+ name: { type: 'string' }
292
+ };
293
+
294
+ const schema = new Schema(source, 'Entity', 'https://example.com/');
295
+
296
+ expect(schema.linkedDataType).toBeDefined();
297
+ const sourceAfter = schema.source as PropertiesSchema;
298
+ expect((sourceAfter.id as StringPropertySchema).format).toBe('url');
299
+ expect((sourceAfter.id as StringPropertySchema).required).toBe(true);
300
+ });
301
+
302
+ it('does not create linked data type for enum schemas', () => {
303
+ const enumSchema = new Schema(
304
+ { enum: ['L', 'M', 'S'] } as EnumSchema,
305
+ 'Size',
306
+ 'https://example.com/'
307
+ );
308
+
309
+ expect(enumSchema.linkedDataType).toBeUndefined();
310
+ });
311
+ });
312
+
313
+ describe('error cases for enum schemas', () => {
314
+ it('throws error when calling pure() on enum schema', () => {
315
+ const enumSchema = new Schema({ enum: ['L', 'M', 'S'] } as EnumSchema, 'Size');
316
+
317
+ expect(() => {
318
+ enumSchema.pure('UpdateSize');
319
+ }).toThrow('The "pure" method is not supported for enum schemas.');
320
+ });
321
+
322
+ it('throws error when calling only() on enum schema', () => {
323
+ const enumSchema = new Schema({ enum: ['L', 'M', 'S'] } as EnumSchema, 'Size');
324
+
325
+ expect(() => {
326
+ enumSchema.only(['value'], 'SizeOnly');
327
+ }).toThrow('The "only" method is not supported for enum schemas.');
328
+ });
329
+
330
+ it('throws error when calling extend() on enum schema', () => {
331
+ const enumSchema = new Schema({ enum: ['L', 'M', 'S'] } as EnumSchema, 'Size');
332
+
333
+ expect(() => {
334
+ enumSchema.extend({ extra: { type: 'string' } }, 'ExtendedSize');
335
+ }).toThrow('The "extend" method is not supported for enum schemas.');
336
+ });
337
+
338
+ it('throws error when calling wrap() on enum schema', () => {
339
+ const enumSchema = new Schema({ enum: ['L', 'M', 'S'] } as EnumSchema, 'Size');
340
+
341
+ expect(() => {
342
+ enumSchema.wrap('data', { required: true }, 'WrappedSize');
343
+ }).toThrow('The "wrap" method is not supported for enum schemas.');
344
+ });
345
+ });
346
+
347
+ describe('.wrap() with default attributes', () => {
348
+ it('uses default required: true when attributes is falsy', () => {
349
+ const profileSchema = loadSync('examples/schemas/Profile.yaml');
350
+
351
+ // Test the attributes || { required: true } branch by passing null
352
+ // This tests line 169 where falsy attributes trigger the default
353
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
354
+ const dataSchema = profileSchema.wrap('data', null as any, 'WrappedData');
355
+ const dataSource = dataSchema.source as PropertiesSchema;
356
+
357
+ expect((dataSource.data as ObjectPropertySchema).required).toBe(true);
358
+ });
359
+
360
+ it('spreads attributes when provided', () => {
361
+ const profileSchema = loadSync('examples/schemas/Profile.yaml');
362
+
363
+ const dataSchema = profileSchema.wrap('data', { default: 'test' }, 'WrappedData');
364
+ const dataSource = dataSchema.source as PropertiesSchema;
365
+
366
+ expect((dataSource.data as ObjectPropertySchema).default).toBe('test');
367
+ // When attributes is provided, required is not set by default
368
+ expect((dataSource.data as ObjectPropertySchema).required).toBeUndefined();
369
+ });
370
+ });
371
+ });
@@ -0,0 +1,235 @@
1
+ import ValidationError from '../ValidationError';
2
+ import type { SchemaErrorDetail } from 'z-schema';
3
+ import type { TargetObject } from '../helpers/JsonSchema';
4
+
5
+ describe('ValidationError', () => {
6
+ // eslint-disable-next-line jsdoc/require-jsdoc
7
+ const createMockError = (
8
+ code: string,
9
+ path: string,
10
+ message: string,
11
+ params: string[] = []
12
+ ): SchemaErrorDetail => {
13
+ return {
14
+ code,
15
+ path,
16
+ message,
17
+ params,
18
+ description: message,
19
+ inner: [],
20
+ } as SchemaErrorDetail;
21
+ };
22
+
23
+ describe('constructor', () => {
24
+ it('should create an error with correct message format', () => {
25
+ const schemaId = 'TestSchema';
26
+ const invalidObject: TargetObject = { name: 'test' };
27
+ const validationErrors: SchemaErrorDetail[] = [];
28
+
29
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
30
+
31
+ expect(error.message).toBe('"TestSchema" validation failed');
32
+ expect(error).toBeInstanceOf(Error);
33
+ });
34
+
35
+ it('should store schemaId, object, and validationErrors', () => {
36
+ const schemaId = 'UserSchema';
37
+ const invalidObject: TargetObject = { email: 'invalid' };
38
+ const validationErrors: SchemaErrorDetail[] = [
39
+ createMockError('INVALID_TYPE', '#/email', 'Expected string but got number', []),
40
+ ];
41
+
42
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
43
+
44
+ // Access private properties through toJSON
45
+ const json = error.toJSON();
46
+ expect(json.schemaId).toBe('UserSchema');
47
+ expect(json.object).toEqual({ email: 'invalid' });
48
+ expect(json.validationErrors).toHaveLength(1);
49
+ });
50
+
51
+ it('should map validationErrors correctly', () => {
52
+ const schemaId = 'ProductSchema';
53
+ const invalidObject: TargetObject = { price: -10 };
54
+ const validationErrors: SchemaErrorDetail[] = [
55
+ createMockError('MINIMUM', '#/price', 'Value must be >= 0', ['0']),
56
+ createMockError('REQUIRED', '#/name', 'Property is required', []),
57
+ ];
58
+
59
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
60
+ const json = error.toJSON();
61
+
62
+ expect(json.validationErrors).toHaveLength(2);
63
+ expect(json.validationErrors[0]).toEqual({
64
+ path: '#/price',
65
+ code: 'MINIMUM',
66
+ params: ['0'],
67
+ message: 'Value must be >= 0',
68
+ });
69
+ expect(json.validationErrors[1]).toEqual({
70
+ path: '#/name',
71
+ code: 'REQUIRED',
72
+ params: [],
73
+ message: 'Property is required',
74
+ });
75
+ });
76
+
77
+ it('should handle empty validationErrors array', () => {
78
+ const schemaId = 'EmptySchema';
79
+ const invalidObject: TargetObject = {};
80
+ const validationErrors: SchemaErrorDetail[] = [];
81
+
82
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
83
+ const json = error.toJSON();
84
+
85
+ expect(json.validationErrors).toHaveLength(0);
86
+ expect(Array.isArray(json.validationErrors)).toBe(true);
87
+ });
88
+
89
+ it('should handle validationErrors with multiple params', () => {
90
+ const schemaId = 'ComplexSchema';
91
+ const invalidObject: TargetObject = { value: 'test' };
92
+ const validationErrors: SchemaErrorDetail[] = [
93
+ createMockError('ENUM_MISMATCH', '#/value', 'Value must be one of: a, b, c', ['a', 'b', 'c']),
94
+ ];
95
+
96
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
97
+ const json = error.toJSON();
98
+
99
+ expect(json.validationErrors[0].params).toEqual(['a', 'b', 'c']);
100
+ });
101
+
102
+ it('should handle nested object paths in validationErrors', () => {
103
+ const schemaId = 'NestedSchema';
104
+ const invalidObject: TargetObject = { user: { email: 'invalid' } };
105
+ const validationErrors: SchemaErrorDetail[] = [
106
+ createMockError('INVALID_FORMAT', '#/user/email', 'Invalid email format', []),
107
+ ];
108
+
109
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
110
+ const json = error.toJSON();
111
+
112
+ expect(json.validationErrors[0].path).toBe('#/user/email');
113
+ });
114
+ });
115
+
116
+ describe('toJSON', () => {
117
+ it('should return a JSON serializable object', () => {
118
+ const schemaId = 'TestSchema';
119
+ const invalidObject: TargetObject = { field: 'value' };
120
+ const validationErrors: SchemaErrorDetail[] = [
121
+ createMockError('INVALID_TYPE', '#/field', 'Type error', []),
122
+ ];
123
+
124
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
125
+ const json = error.toJSON();
126
+
127
+ expect(json).toEqual({
128
+ code: 'ValidationError',
129
+ object: { field: 'value' },
130
+ message: '"TestSchema" validation failed',
131
+ schemaId: 'TestSchema',
132
+ validationErrors: [
133
+ {
134
+ path: '#/field',
135
+ code: 'INVALID_TYPE',
136
+ params: [],
137
+ message: 'Type error',
138
+ },
139
+ ],
140
+ });
141
+ });
142
+
143
+ it('should be JSON serializable', () => {
144
+ const schemaId = 'SerializableSchema';
145
+ const invalidObject: TargetObject = { data: { nested: 'value' } };
146
+ const validationErrors: SchemaErrorDetail[] = [
147
+ createMockError('REQUIRED', '#/data/nested', 'Required field', []),
148
+ ];
149
+
150
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
151
+ const json = error.toJSON();
152
+
153
+ // Should not throw when stringifying
154
+ expect(() => JSON.stringify(json)).not.toThrow();
155
+ const stringified = JSON.stringify(json);
156
+ const parsed = JSON.parse(stringified);
157
+
158
+ expect(parsed.code).toBe('ValidationError');
159
+ expect(parsed.schemaId).toBe('SerializableSchema');
160
+ expect(parsed.validationErrors).toHaveLength(1);
161
+ });
162
+
163
+ it('should preserve complex object structures', () => {
164
+ const schemaId = 'ComplexObjectSchema';
165
+ const invalidObject: TargetObject = {
166
+ user: {
167
+ name: 'John',
168
+ age: 30,
169
+ tags: ['admin', 'user'],
170
+ },
171
+ };
172
+ const validationErrors: SchemaErrorDetail[] = [];
173
+
174
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
175
+ const json = error.toJSON();
176
+
177
+ expect(json.object).toEqual({
178
+ user: {
179
+ name: 'John',
180
+ age: 30,
181
+ tags: ['admin', 'user'],
182
+ },
183
+ });
184
+ });
185
+
186
+ it('should include all validation errors in the JSON output', () => {
187
+ const schemaId = 'MultipleErrorsSchema';
188
+ const invalidObject: TargetObject = { a: 1, b: 2, c: 3 };
189
+ const validationErrors: SchemaErrorDetail[] = [
190
+ createMockError('ERROR_1', '#/a', 'Error A', []),
191
+ createMockError('ERROR_2', '#/b', 'Error B', ['param1']),
192
+ createMockError('ERROR_3', '#/c', 'Error C', ['param1', 'param2']),
193
+ ];
194
+
195
+ const error = new ValidationError(schemaId, invalidObject, validationErrors);
196
+ const json = error.toJSON();
197
+
198
+ expect(json.validationErrors).toHaveLength(3);
199
+ expect(json.validationErrors.map(e => e.code)).toEqual(['ERROR_1', 'ERROR_2', 'ERROR_3']);
200
+ });
201
+ });
202
+
203
+ describe('Error inheritance', () => {
204
+ it('should be an instance of Error', () => {
205
+ const error = new ValidationError('Test', {}, []);
206
+
207
+ expect(error).toBeInstanceOf(Error);
208
+ expect(error).toBeInstanceOf(ValidationError);
209
+ });
210
+
211
+ it('should have Error properties', () => {
212
+ const error = new ValidationError('TestSchema', { field: 'value' }, []);
213
+
214
+ expect(error.name).toBe('Error');
215
+ expect(error.message).toBe('"TestSchema" validation failed');
216
+ expect(typeof error.stack).toBe('string');
217
+ });
218
+
219
+ it('should be throwable and catchable', () => {
220
+ const schemaId = 'ThrowableSchema';
221
+ const invalidObject: TargetObject = {};
222
+ const validationErrors: SchemaErrorDetail[] = [
223
+ createMockError('REQUIRED', '#/field', 'Required', []),
224
+ ];
225
+
226
+ expect(() => {
227
+ throw new ValidationError(schemaId, invalidObject, validationErrors);
228
+ }).toThrow(ValidationError);
229
+
230
+ expect(() => {
231
+ throw new ValidationError(schemaId, invalidObject, validationErrors);
232
+ }).toThrow('"ThrowableSchema" validation failed');
233
+ });
234
+ });
235
+ });