@featurevisor/core 2.20.0 → 2.22.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 (87) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/coverage/clover.xml +1286 -515
  3. package/coverage/coverage-final.json +17 -16
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildDatafile.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  8. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  9. package/coverage/lcov-report/builder/convertToV1.ts.html +1 -1
  10. package/coverage/lcov-report/builder/getFeatureRanges.ts.html +1 -1
  11. package/coverage/lcov-report/builder/hashes.ts.html +1 -1
  12. package/coverage/lcov-report/builder/index.html +1 -1
  13. package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
  14. package/coverage/lcov-report/builder/mutator.ts.html +1 -1
  15. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  16. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  17. package/coverage/lcov-report/config/index.html +1 -1
  18. package/coverage/lcov-report/config/index.ts.html +1 -1
  19. package/coverage/lcov-report/config/projectConfig.ts.html +42 -42
  20. package/coverage/lcov-report/datasource/adapter.ts.html +2 -2
  21. package/coverage/lcov-report/datasource/datasource.ts.html +43 -43
  22. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +48 -48
  23. package/coverage/lcov-report/datasource/index.html +19 -19
  24. package/coverage/lcov-report/datasource/index.ts.html +4 -4
  25. package/coverage/lcov-report/generate-code/index.html +116 -0
  26. package/coverage/lcov-report/generate-code/typescript.ts.html +3004 -0
  27. package/coverage/lcov-report/index.html +44 -29
  28. package/coverage/lcov-report/linter/attributeSchema.ts.html +1815 -30
  29. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  30. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  31. package/coverage/lcov-report/linter/conditionSchema.ts.html +1606 -157
  32. package/coverage/lcov-report/linter/featureSchema.ts.html +56 -56
  33. package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
  34. package/coverage/lcov-report/linter/index.html +39 -39
  35. package/coverage/lcov-report/linter/lintProject.ts.html +189 -78
  36. package/coverage/lcov-report/linter/mutationNotation.ts.html +1 -1
  37. package/coverage/lcov-report/linter/printError.ts.html +1 -1
  38. package/coverage/lcov-report/linter/schema.ts.html +59 -59
  39. package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
  40. package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
  41. package/coverage/lcov-report/list/index.html +1 -1
  42. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  43. package/coverage/lcov-report/parsers/index.html +1 -1
  44. package/coverage/lcov-report/parsers/index.ts.html +4 -4
  45. package/coverage/lcov-report/parsers/json.ts.html +2 -2
  46. package/coverage/lcov-report/parsers/yml.ts.html +6 -6
  47. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  48. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  49. package/coverage/lcov-report/tester/index.html +1 -1
  50. package/coverage/lcov-report/utils/extractKeys.ts.html +1 -1
  51. package/coverage/lcov-report/utils/git.ts.html +3 -3
  52. package/coverage/lcov-report/utils/index.html +1 -1
  53. package/coverage/lcov-report/utils/index.ts.html +1 -1
  54. package/coverage/lcov-report/utils/pretty.ts.html +1 -1
  55. package/coverage/lcov.info +2624 -926
  56. package/lib/generate-code/typescript.js +57 -29
  57. package/lib/generate-code/typescript.js.map +1 -1
  58. package/lib/generate-code/typescript.spec.d.ts +1 -0
  59. package/lib/generate-code/typescript.spec.js +78 -0
  60. package/lib/generate-code/typescript.spec.js.map +1 -0
  61. package/lib/linter/attributeSchema.d.ts +352 -20
  62. package/lib/linter/attributeSchema.js +426 -16
  63. package/lib/linter/attributeSchema.js.map +1 -1
  64. package/lib/linter/attributeSchema.spec.d.ts +1 -0
  65. package/lib/linter/attributeSchema.spec.js +266 -0
  66. package/lib/linter/attributeSchema.spec.js.map +1 -0
  67. package/lib/linter/conditionSchema.d.ts +2 -1
  68. package/lib/linter/conditionSchema.js +370 -100
  69. package/lib/linter/conditionSchema.js.map +1 -1
  70. package/lib/linter/conditionSchema.spec.js +200 -153
  71. package/lib/linter/conditionSchema.spec.js.map +1 -1
  72. package/lib/linter/featureSchema.spec.js +114 -3
  73. package/lib/linter/featureSchema.spec.js.map +1 -1
  74. package/lib/linter/lintProject.js +45 -14
  75. package/lib/linter/lintProject.js.map +1 -1
  76. package/lib/linter/segmentSchema.spec.js +71 -1
  77. package/lib/linter/segmentSchema.spec.js.map +1 -1
  78. package/package.json +5 -5
  79. package/src/generate-code/typescript.spec.ts +108 -0
  80. package/src/generate-code/typescript.ts +80 -34
  81. package/src/linter/attributeSchema.spec.ts +342 -0
  82. package/src/linter/attributeSchema.ts +613 -18
  83. package/src/linter/conditionSchema.spec.ts +231 -189
  84. package/src/linter/conditionSchema.ts +582 -99
  85. package/src/linter/featureSchema.spec.ts +129 -4
  86. package/src/linter/lintProject.ts +55 -18
  87. package/src/linter/segmentSchema.spec.ts +82 -1
@@ -1,30 +1,625 @@
1
+ import type { Attribute, AttributeProperty } from "@featurevisor/types";
1
2
  import { z } from "zod";
2
3
 
4
+ import { valueZodSchema, refineArrayItems, refineMinimumMaximum } from "./schema";
5
+
6
+ const attributeTypeEnum = z.enum([
7
+ "boolean",
8
+ "string",
9
+ "integer",
10
+ "double",
11
+ "date",
12
+ "semver",
13
+ "object",
14
+ "array",
15
+ ]);
16
+
17
+ type AttributeSchemaLike = {
18
+ type?: Attribute["type"];
19
+ enum?: unknown[];
20
+ const?: unknown;
21
+ minimum?: number;
22
+ maximum?: number;
23
+ minLength?: number;
24
+ maxLength?: number;
25
+ pattern?: string;
26
+ items?: unknown;
27
+ properties?: Record<string, unknown>;
28
+ additionalProperties?: unknown;
29
+ required?: string[];
30
+ oneOf?: unknown[];
31
+ };
32
+
33
+ function isArrayOfStrings(value: unknown): boolean {
34
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
35
+ }
36
+
37
+ function isStringLikeAttributeType(type?: Attribute["type"]): boolean {
38
+ return type === "string" || type === "date" || type === "semver";
39
+ }
40
+
41
+ function valueMatchesAttributeType(value: unknown, type: Attribute["type"]): boolean {
42
+ if (type === "date" || type === "semver") {
43
+ return typeof value === "string";
44
+ }
45
+
46
+ switch (type) {
47
+ case "string":
48
+ return typeof value === "string";
49
+ case "boolean":
50
+ return typeof value === "boolean";
51
+ case "integer":
52
+ return typeof value === "number" && Number.isInteger(value);
53
+ case "double":
54
+ return typeof value === "number";
55
+ case "object":
56
+ return typeof value === "object" && value !== null && !Array.isArray(value);
57
+ case "array":
58
+ return isArrayOfStrings(value);
59
+ default:
60
+ return true;
61
+ }
62
+ }
63
+
64
+ function refineAttributeEnumMatchesType(
65
+ schema: AttributeSchemaLike,
66
+ pathPrefix: (string | number)[],
67
+ ctx: z.RefinementCtx,
68
+ ): void {
69
+ if (!schema || typeof schema !== "object") return;
70
+
71
+ if (schema.type && Array.isArray(schema.enum) && schema.enum.length > 0) {
72
+ for (let i = 0; i < schema.enum.length; i++) {
73
+ const value = schema.enum[i];
74
+ if (!valueMatchesAttributeType(value, schema.type)) {
75
+ ctx.addIssue({
76
+ code: z.ZodIssueCode.custom,
77
+ message: `Enum value at index ${i} (${JSON.stringify(value)}) does not match type "${schema.type}". All enum values must be of the same type as \`type\`.`,
78
+ path: [...pathPrefix, "enum", i],
79
+ });
80
+ }
81
+ }
82
+ }
83
+
84
+ if (schema.items && typeof schema.items === "object") {
85
+ refineAttributeEnumMatchesType(
86
+ schema.items as AttributeSchemaLike,
87
+ [...pathPrefix, "items"],
88
+ ctx,
89
+ );
90
+ }
91
+
92
+ if (schema.properties && typeof schema.properties === "object") {
93
+ for (const key of Object.keys(schema.properties)) {
94
+ refineAttributeEnumMatchesType(
95
+ schema.properties[key] as AttributeSchemaLike,
96
+ [...pathPrefix, "properties", key],
97
+ ctx,
98
+ );
99
+ }
100
+ }
101
+
102
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
103
+ refineAttributeEnumMatchesType(
104
+ schema.additionalProperties as AttributeSchemaLike,
105
+ [...pathPrefix, "additionalProperties"],
106
+ ctx,
107
+ );
108
+ }
109
+
110
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
111
+ schema.oneOf.forEach((branch, index) => {
112
+ if (branch && typeof branch === "object") {
113
+ refineAttributeEnumMatchesType(
114
+ branch as AttributeSchemaLike,
115
+ [...pathPrefix, "oneOf", index],
116
+ ctx,
117
+ );
118
+ }
119
+ });
120
+ }
121
+ }
122
+
123
+ function refineAttributeStringLengthPattern(
124
+ schema: AttributeSchemaLike,
125
+ pathPrefix: (string | number)[],
126
+ ctx: z.RefinementCtx,
127
+ ): void {
128
+ if (!schema || typeof schema !== "object") return;
129
+
130
+ const minLength = schema.minLength;
131
+ const maxLength = schema.maxLength;
132
+ const pattern = schema.pattern;
133
+ const isString = isStringLikeAttributeType(schema.type);
134
+
135
+ if (isString && minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
136
+ ctx.addIssue({
137
+ code: z.ZodIssueCode.custom,
138
+ message: `When \`type\` is "${schema.type}", \`minLength\` (${minLength}) must be less than or equal to \`maxLength\` (${maxLength}).`,
139
+ path: [...pathPrefix, "minLength"],
140
+ });
141
+ }
142
+
143
+ if (isString && pattern !== undefined) {
144
+ try {
145
+ new RegExp(pattern);
146
+ } catch {
147
+ ctx.addIssue({
148
+ code: z.ZodIssueCode.custom,
149
+ message: `\`pattern\` must be a valid ECMA-262 regular expression; "${pattern}" is invalid.`,
150
+ path: [...pathPrefix, "pattern"],
151
+ });
152
+ }
153
+ }
154
+
155
+ const testString = (value: string) => {
156
+ const minOk = minLength === undefined || value.length >= minLength;
157
+ const maxOk = maxLength === undefined || value.length <= maxLength;
158
+ let patternOk = true;
159
+
160
+ if (pattern !== undefined) {
161
+ try {
162
+ patternOk = new RegExp(pattern).test(value);
163
+ } catch {
164
+ patternOk = true;
165
+ }
166
+ }
167
+
168
+ return { minOk, maxOk, patternOk };
169
+ };
170
+
171
+ if (isString && typeof schema.const === "string") {
172
+ const result = testString(schema.const);
173
+ if (!result.minOk) {
174
+ ctx.addIssue({
175
+ code: z.ZodIssueCode.custom,
176
+ message: `\`const\` value length (${schema.const.length}) is less than \`minLength\` (${minLength}).`,
177
+ path: [...pathPrefix, "const"],
178
+ });
179
+ }
180
+ if (!result.maxOk) {
181
+ ctx.addIssue({
182
+ code: z.ZodIssueCode.custom,
183
+ message: `\`const\` value length (${schema.const.length}) is greater than \`maxLength\` (${maxLength}).`,
184
+ path: [...pathPrefix, "const"],
185
+ });
186
+ }
187
+ if (!result.patternOk) {
188
+ ctx.addIssue({
189
+ code: z.ZodIssueCode.custom,
190
+ message: `\`const\` value does not match \`pattern\`.`,
191
+ path: [...pathPrefix, "const"],
192
+ });
193
+ }
194
+ }
195
+
196
+ if (isString && Array.isArray(schema.enum)) {
197
+ schema.enum.forEach((value, index) => {
198
+ if (typeof value !== "string") return;
199
+ const result = testString(value);
200
+ if (!result.minOk) {
201
+ ctx.addIssue({
202
+ code: z.ZodIssueCode.custom,
203
+ message: `Enum value at index ${index} length (${value.length}) is less than \`minLength\` (${minLength}).`,
204
+ path: [...pathPrefix, "enum", index],
205
+ });
206
+ }
207
+ if (!result.maxOk) {
208
+ ctx.addIssue({
209
+ code: z.ZodIssueCode.custom,
210
+ message: `Enum value at index ${index} length (${value.length}) is greater than \`maxLength\` (${maxLength}).`,
211
+ path: [...pathPrefix, "enum", index],
212
+ });
213
+ }
214
+ if (!result.patternOk) {
215
+ ctx.addIssue({
216
+ code: z.ZodIssueCode.custom,
217
+ message: `Enum value at index ${index} does not match \`pattern\`.`,
218
+ path: [...pathPrefix, "enum", index],
219
+ });
220
+ }
221
+ });
222
+ }
223
+
224
+ if (schema.items && typeof schema.items === "object") {
225
+ refineAttributeStringLengthPattern(
226
+ schema.items as AttributeSchemaLike,
227
+ [...pathPrefix, "items"],
228
+ ctx,
229
+ );
230
+ }
231
+
232
+ if (schema.properties && typeof schema.properties === "object") {
233
+ for (const key of Object.keys(schema.properties)) {
234
+ refineAttributeStringLengthPattern(
235
+ schema.properties[key] as AttributeSchemaLike,
236
+ [...pathPrefix, "properties", key],
237
+ ctx,
238
+ );
239
+ }
240
+ }
241
+
242
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
243
+ refineAttributeStringLengthPattern(
244
+ schema.additionalProperties as AttributeSchemaLike,
245
+ [...pathPrefix, "additionalProperties"],
246
+ ctx,
247
+ );
248
+ }
249
+
250
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
251
+ schema.oneOf.forEach((branch, index) => {
252
+ if (branch && typeof branch === "object") {
253
+ refineAttributeStringLengthPattern(
254
+ branch as AttributeSchemaLike,
255
+ [...pathPrefix, "oneOf", index],
256
+ ctx,
257
+ );
258
+ }
259
+ });
260
+ }
261
+ }
262
+
263
+ function refineRequiredKeysInAttributeSchema(
264
+ schema: AttributeSchemaLike,
265
+ pathPrefix: (string | number)[],
266
+ ctx: z.RefinementCtx,
267
+ ): void {
268
+ if (!schema || typeof schema !== "object") return;
269
+
270
+ if (
271
+ schema.type === "object" &&
272
+ Array.isArray(schema.required) &&
273
+ schema.properties &&
274
+ typeof schema.properties === "object"
275
+ ) {
276
+ const allowedKeys = Object.keys(schema.properties);
277
+ schema.required.forEach((key, index) => {
278
+ if (!allowedKeys.includes(key)) {
279
+ ctx.addIssue({
280
+ code: z.ZodIssueCode.custom,
281
+ message: `Unknown required field "${key}". \`required\` must only contain property names defined in \`properties\`. Allowed: ${allowedKeys.length ? allowedKeys.join(", ") : "(none)"}.`,
282
+ path: [...pathPrefix, "required", index],
283
+ });
284
+ }
285
+ });
286
+ }
287
+
288
+ if (schema.properties && typeof schema.properties === "object") {
289
+ for (const key of Object.keys(schema.properties)) {
290
+ refineRequiredKeysInAttributeSchema(
291
+ schema.properties[key] as AttributeSchemaLike,
292
+ [...pathPrefix, "properties", key],
293
+ ctx,
294
+ );
295
+ }
296
+ }
297
+
298
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
299
+ refineRequiredKeysInAttributeSchema(
300
+ schema.additionalProperties as AttributeSchemaLike,
301
+ [...pathPrefix, "additionalProperties"],
302
+ ctx,
303
+ );
304
+ }
305
+
306
+ if (schema.items && typeof schema.items === "object") {
307
+ refineRequiredKeysInAttributeSchema(
308
+ schema.items as AttributeSchemaLike,
309
+ [...pathPrefix, "items"],
310
+ ctx,
311
+ );
312
+ }
313
+
314
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
315
+ schema.oneOf.forEach((branch, index) => {
316
+ if (branch && typeof branch === "object") {
317
+ refineRequiredKeysInAttributeSchema(
318
+ branch as AttributeSchemaLike,
319
+ [...pathPrefix, "oneOf", index],
320
+ ctx,
321
+ );
322
+ }
323
+ });
324
+ }
325
+ }
326
+
327
+ function refineNoNestedObjectProperties(
328
+ schema: AttributeSchemaLike,
329
+ pathPrefix: (string | number)[],
330
+ ctx: z.RefinementCtx,
331
+ ): void {
332
+ if (!schema || typeof schema !== "object") return;
333
+
334
+ const schemaAllowsObject = (candidate: AttributeSchemaLike | undefined): boolean => {
335
+ if (!candidate || typeof candidate !== "object") {
336
+ return false;
337
+ }
338
+
339
+ if (candidate.type === "object") {
340
+ return true;
341
+ }
342
+
343
+ if (candidate.oneOf && Array.isArray(candidate.oneOf)) {
344
+ return candidate.oneOf.some((branch) =>
345
+ schemaAllowsObject(branch as AttributeSchemaLike | undefined),
346
+ );
347
+ }
348
+
349
+ return false;
350
+ };
351
+
352
+ if (schema.type === "object" && schema.properties && typeof schema.properties === "object") {
353
+ for (const key of Object.keys(schema.properties)) {
354
+ const property = schema.properties[key] as AttributeSchemaLike;
355
+ if (schemaAllowsObject(property)) {
356
+ ctx.addIssue({
357
+ code: z.ZodIssueCode.custom,
358
+ message: `Object attributes must stay flat. Property "${key}" cannot be of type "object".`,
359
+ path: [...pathPrefix, "properties", key, "type"],
360
+ });
361
+ }
362
+ }
363
+ }
364
+
365
+ if (
366
+ schema.type === "object" &&
367
+ schema.additionalProperties &&
368
+ schemaAllowsObject(schema.additionalProperties as AttributeSchemaLike)
369
+ ) {
370
+ ctx.addIssue({
371
+ code: z.ZodIssueCode.custom,
372
+ message:
373
+ 'Object attributes must stay flat. `additionalProperties` cannot allow values of type "object".',
374
+ path: [...pathPrefix, "additionalProperties", "type"],
375
+ });
376
+ }
377
+
378
+ if (schema.items && typeof schema.items === "object") {
379
+ refineNoNestedObjectProperties(
380
+ schema.items as AttributeSchemaLike,
381
+ [...pathPrefix, "items"],
382
+ ctx,
383
+ );
384
+ }
385
+
386
+ if (schema.properties && typeof schema.properties === "object") {
387
+ for (const key of Object.keys(schema.properties)) {
388
+ refineNoNestedObjectProperties(
389
+ schema.properties[key] as AttributeSchemaLike,
390
+ [...pathPrefix, "properties", key],
391
+ ctx,
392
+ );
393
+ }
394
+ }
395
+
396
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
397
+ refineNoNestedObjectProperties(
398
+ schema.additionalProperties as AttributeSchemaLike,
399
+ [...pathPrefix, "additionalProperties"],
400
+ ctx,
401
+ );
402
+ }
403
+
404
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
405
+ schema.oneOf.forEach((branch, index) => {
406
+ if (branch && typeof branch === "object") {
407
+ refineNoNestedObjectProperties(
408
+ branch as AttributeSchemaLike,
409
+ [...pathPrefix, "oneOf", index],
410
+ ctx,
411
+ );
412
+ }
413
+ });
414
+ }
415
+ }
416
+
417
+ function refineArrayAttributesAreStringArrays(
418
+ schema: AttributeSchemaLike,
419
+ pathPrefix: (string | number)[],
420
+ ctx: z.RefinementCtx,
421
+ ): void {
422
+ if (!schema || typeof schema !== "object") return;
423
+
424
+ if (schema.type === "array") {
425
+ if (schema.items && typeof schema.items === "object") {
426
+ const itemSchema = schema.items as AttributeSchemaLike;
427
+ if (itemSchema.type !== "string") {
428
+ ctx.addIssue({
429
+ code: z.ZodIssueCode.custom,
430
+ message: `Attribute arrays must contain strings only. \`items.type\` must be "string".`,
431
+ path: [...pathPrefix, "items", "type"],
432
+ });
433
+ }
434
+ }
435
+
436
+ if (schema.const !== undefined && !isArrayOfStrings(schema.const)) {
437
+ ctx.addIssue({
438
+ code: z.ZodIssueCode.custom,
439
+ message: `Attribute arrays must contain strings only. \`const\` must be an array of strings.`,
440
+ path: [...pathPrefix, "const"],
441
+ });
442
+ }
443
+
444
+ if (Array.isArray(schema.enum)) {
445
+ schema.enum.forEach((entry, index) => {
446
+ if (!isArrayOfStrings(entry)) {
447
+ ctx.addIssue({
448
+ code: z.ZodIssueCode.custom,
449
+ message: `Attribute arrays must contain strings only. Enum value at index ${index} must be an array of strings.`,
450
+ path: [...pathPrefix, "enum", index],
451
+ });
452
+ }
453
+ });
454
+ }
455
+ }
456
+
457
+ if (schema.items && typeof schema.items === "object") {
458
+ refineArrayAttributesAreStringArrays(
459
+ schema.items as AttributeSchemaLike,
460
+ [...pathPrefix, "items"],
461
+ ctx,
462
+ );
463
+ }
464
+
465
+ if (schema.properties && typeof schema.properties === "object") {
466
+ for (const key of Object.keys(schema.properties)) {
467
+ refineArrayAttributesAreStringArrays(
468
+ schema.properties[key] as AttributeSchemaLike,
469
+ [...pathPrefix, "properties", key],
470
+ ctx,
471
+ );
472
+ }
473
+ }
474
+
475
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
476
+ refineArrayAttributesAreStringArrays(
477
+ schema.additionalProperties as AttributeSchemaLike,
478
+ [...pathPrefix, "additionalProperties"],
479
+ ctx,
480
+ );
481
+ }
482
+
483
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
484
+ schema.oneOf.forEach((branch, index) => {
485
+ if (branch && typeof branch === "object") {
486
+ refineArrayAttributesAreStringArrays(
487
+ branch as AttributeSchemaLike,
488
+ [...pathPrefix, "oneOf", index],
489
+ ctx,
490
+ );
491
+ }
492
+ });
493
+ }
494
+ }
495
+
3
496
  export function getAttributeZodSchema() {
4
- const propertySchema = z.object({
5
- type: z.enum([
6
- "boolean",
7
- "string",
8
- "integer",
9
- "double",
10
- "date",
11
- "semver",
12
- "array",
13
-
14
- // @NOTE: intentionally ignored for now to not allow nesting
15
- // "object",
16
- ]),
17
- description: z.string().optional(),
18
- });
497
+ const attributePropertySchema: z.ZodType<AttributeProperty> = z.lazy(() =>
498
+ z
499
+ .object({
500
+ description: z.string().optional(),
501
+ type: attributeTypeEnum.optional(),
502
+ enum: z.array(valueZodSchema).optional(),
503
+ const: valueZodSchema.optional(),
504
+ minimum: z.number().optional(),
505
+ maximum: z.number().optional(),
506
+ minLength: z.number().optional(),
507
+ maxLength: z.number().optional(),
508
+ pattern: z.string().optional(),
509
+ items: attributePropertySchema.optional(),
510
+ minItems: z.number().optional(),
511
+ maxItems: z.number().optional(),
512
+ uniqueItems: z.boolean().optional(),
513
+ required: z.array(z.string()).optional(),
514
+ properties: z.record(z.string(), attributePropertySchema).optional(),
515
+ additionalProperties: attributePropertySchema.optional(),
516
+ schema: z.string().optional(),
517
+ oneOf: z.array(attributePropertySchema).min(2).optional(),
518
+ })
519
+ .strict()
520
+ .superRefine((data, ctx) => {
521
+ const hasStructuredArrayDetails =
522
+ data.items !== undefined ||
523
+ data.minItems !== undefined ||
524
+ data.maxItems !== undefined ||
525
+ data.uniqueItems !== undefined ||
526
+ data.oneOf !== undefined ||
527
+ data.enum !== undefined ||
528
+ data.const !== undefined;
529
+
530
+ if (data.type === "array" && hasStructuredArrayDetails && data.items === undefined) {
531
+ ctx.addIssue({
532
+ code: z.ZodIssueCode.custom,
533
+ message:
534
+ 'When `type` is "array" and additional schema properties are provided, `items` is required.',
535
+ path: ["items"],
536
+ });
537
+ }
538
+ })
539
+ .superRefine((data, ctx) => refineAttributeEnumMatchesType(data, [], ctx))
540
+ .superRefine((data, ctx) =>
541
+ refineMinimumMaximum(data as Parameters<typeof refineMinimumMaximum>[0], [], ctx),
542
+ )
543
+ .superRefine((data, ctx) => refineAttributeStringLengthPattern(data, [], ctx))
544
+ .superRefine((data, ctx) =>
545
+ refineArrayItems(data as Parameters<typeof refineArrayItems>[0], [], ctx),
546
+ )
547
+ .superRefine((data, ctx) => refineRequiredKeysInAttributeSchema(data, [], ctx))
548
+ .superRefine((data, ctx) => refineNoNestedObjectProperties(data, [], ctx))
549
+ .superRefine((data, ctx) => refineArrayAttributesAreStringArrays(data, [], ctx)),
550
+ );
19
551
 
20
552
  const attributeZodSchema = z
21
553
  .object({
22
554
  archived: z.boolean().optional(),
23
- type: z.enum(["boolean", "string", "integer", "double", "date", "semver", "object", "array"]),
555
+ type: attributeTypeEnum.optional(),
24
556
  description: z.string(),
25
- properties: z.record(z.string(), propertySchema).optional(),
557
+ enum: z.array(valueZodSchema).optional(),
558
+ const: valueZodSchema.optional(),
559
+ minimum: z.number().optional(),
560
+ maximum: z.number().optional(),
561
+ minLength: z.number().optional(),
562
+ maxLength: z.number().optional(),
563
+ pattern: z.string().optional(),
564
+ items: attributePropertySchema.optional(),
565
+ minItems: z.number().optional(),
566
+ maxItems: z.number().optional(),
567
+ uniqueItems: z.boolean().optional(),
568
+ required: z.array(z.string()).optional(),
569
+ properties: z.record(z.string(), attributePropertySchema).optional(),
570
+ additionalProperties: attributePropertySchema.optional(),
571
+ oneOf: z.array(attributePropertySchema).min(2).optional(),
572
+ })
573
+ .strict()
574
+ .superRefine((data, ctx) => {
575
+ const hasOneOf = data.oneOf !== undefined && data.oneOf.length > 0;
576
+
577
+ if (!data.type && !hasOneOf) {
578
+ ctx.addIssue({
579
+ code: z.ZodIssueCode.custom,
580
+ message: "Attribute must have either `type` or `oneOf` at the top level.",
581
+ path: ["type"],
582
+ });
583
+ }
584
+
585
+ if (data.type && hasOneOf) {
586
+ ctx.addIssue({
587
+ code: z.ZodIssueCode.custom,
588
+ message:
589
+ "Attribute cannot have both `type` and `oneOf` at the top level. Use one or the other.",
590
+ path: ["type"],
591
+ });
592
+ }
593
+
594
+ const hasStructuredArrayDetails =
595
+ data.items !== undefined ||
596
+ data.minItems !== undefined ||
597
+ data.maxItems !== undefined ||
598
+ data.uniqueItems !== undefined ||
599
+ data.oneOf !== undefined ||
600
+ data.enum !== undefined ||
601
+ data.const !== undefined;
602
+
603
+ if (data.type === "array" && hasStructuredArrayDetails && data.items === undefined) {
604
+ ctx.addIssue({
605
+ code: z.ZodIssueCode.custom,
606
+ message:
607
+ 'When `type` is "array" and additional schema properties are provided, `items` is required.',
608
+ path: ["items"],
609
+ });
610
+ }
26
611
  })
27
- .strict();
612
+ .superRefine((data, ctx) => refineAttributeEnumMatchesType(data, [], ctx))
613
+ .superRefine((data, ctx) =>
614
+ refineMinimumMaximum(data as Parameters<typeof refineMinimumMaximum>[0], [], ctx),
615
+ )
616
+ .superRefine((data, ctx) => refineAttributeStringLengthPattern(data, [], ctx))
617
+ .superRefine((data, ctx) =>
618
+ refineArrayItems(data as Parameters<typeof refineArrayItems>[0], [], ctx),
619
+ )
620
+ .superRefine((data, ctx) => refineRequiredKeysInAttributeSchema(data, [], ctx))
621
+ .superRefine((data, ctx) => refineNoNestedObjectProperties(data, [], ctx))
622
+ .superRefine((data, ctx) => refineArrayAttributesAreStringArrays(data, [], ctx));
28
623
 
29
624
  return attributeZodSchema;
30
625
  }