@featurevisor/core 2.10.0 → 2.12.0
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.
- package/CHANGELOG.md +22 -0
- package/coverage/clover.xml +684 -3
- package/coverage/coverage-final.json +4 -0
- package/coverage/lcov-report/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
- package/coverage/lcov-report/builder/index.html +1 -1
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/index.html +25 -10
- package/coverage/lcov-report/linter/conditionSchema.ts.html +775 -0
- package/coverage/lcov-report/linter/featureSchema.ts.html +4924 -0
- package/coverage/lcov-report/linter/index.html +161 -0
- package/coverage/lcov-report/linter/schema.ts.html +1471 -0
- package/coverage/lcov-report/linter/segmentSchema.ts.html +130 -0
- package/coverage/lcov-report/list/index.html +1 -1
- package/coverage/lcov-report/list/matrix.ts.html +1 -1
- package/coverage/lcov-report/parsers/index.html +1 -1
- package/coverage/lcov-report/parsers/json.ts.html +1 -1
- package/coverage/lcov-report/parsers/yml.ts.html +1 -1
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +1 -1
- package/coverage/lcov.info +1471 -0
- package/lib/builder/buildDatafile.js +15 -1
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/config/projectConfig.d.ts +2 -0
- package/lib/config/projectConfig.js +3 -1
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/datasource/datasource.d.ts +6 -1
- package/lib/datasource/datasource.js +16 -0
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.js +10 -0
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/generate-code/typescript.js +283 -49
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/conditionSchema.spec.d.ts +1 -0
- package/lib/linter/conditionSchema.spec.js +331 -0
- package/lib/linter/conditionSchema.spec.js.map +1 -0
- package/lib/linter/featureSchema.d.ts +153 -17
- package/lib/linter/featureSchema.js +536 -49
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.d.ts +1 -0
- package/lib/linter/featureSchema.spec.js +978 -0
- package/lib/linter/featureSchema.spec.js.map +1 -0
- package/lib/linter/lintProject.js +67 -1
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/schema.d.ts +42 -0
- package/lib/linter/schema.js +417 -0
- package/lib/linter/schema.js.map +1 -0
- package/lib/linter/schema.spec.d.ts +1 -0
- package/lib/linter/schema.spec.js +483 -0
- package/lib/linter/schema.spec.js.map +1 -0
- package/lib/linter/segmentSchema.spec.d.ts +1 -0
- package/lib/linter/segmentSchema.spec.js +231 -0
- package/lib/linter/segmentSchema.spec.js.map +1 -0
- package/lib/tester/testFeature.js +5 -3
- package/lib/tester/testFeature.js.map +1 -1
- package/lib/utils/git.js +3 -0
- package/lib/utils/git.js.map +1 -1
- package/package.json +5 -5
- package/src/builder/buildDatafile.ts +17 -1
- package/src/config/projectConfig.ts +3 -0
- package/src/datasource/datasource.ts +23 -0
- package/src/datasource/filesystemAdapter.ts +7 -0
- package/src/generate-code/typescript.ts +333 -52
- package/src/linter/conditionSchema.spec.ts +446 -0
- package/src/linter/featureSchema.spec.ts +1218 -0
- package/src/linter/featureSchema.ts +747 -70
- package/src/linter/lintProject.ts +84 -0
- package/src/linter/schema.spec.ts +617 -0
- package/src/linter/schema.ts +462 -0
- package/src/linter/segmentSchema.spec.ts +273 -0
- package/src/tester/testFeature.ts +5 -3
- package/src/utils/git.ts +2 -0
- package/lib/linter/propertySchema.d.ts +0 -5
- package/lib/linter/propertySchema.js +0 -43
- package/lib/linter/propertySchema.js.map +0 -1
- package/src/linter/propertySchema.ts +0 -47
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import type { Schema } from "@featurevisor/types";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import { ProjectConfig } from "../config";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
valueZodSchema,
|
|
7
|
+
propertyTypeEnum,
|
|
8
|
+
getSchemaZodSchema,
|
|
9
|
+
refineEnumMatchesType,
|
|
10
|
+
refineMinimumMaximum,
|
|
11
|
+
refineStringLengthPattern,
|
|
12
|
+
refineArrayItems,
|
|
13
|
+
} from "./schema";
|
|
5
14
|
|
|
6
15
|
const tagRegex = /^[a-z0-9-]+$/;
|
|
7
16
|
|
|
@@ -26,6 +35,297 @@ function getVariableLabel(variableSchema, variableKey, path) {
|
|
|
26
35
|
);
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Resolve variable schema to the Schema used for value validation.
|
|
40
|
+
* When variable has `schema` (reference), returns the parsed Schema from schemasByKey; otherwise returns the inline variable schema.
|
|
41
|
+
*/
|
|
42
|
+
function resolveVariableSchema(
|
|
43
|
+
variableSchema: {
|
|
44
|
+
schema?: string;
|
|
45
|
+
type?: string;
|
|
46
|
+
items?: unknown;
|
|
47
|
+
properties?: unknown;
|
|
48
|
+
required?: string[];
|
|
49
|
+
enum?: unknown[];
|
|
50
|
+
const?: unknown;
|
|
51
|
+
oneOf?: unknown[];
|
|
52
|
+
minimum?: number;
|
|
53
|
+
maximum?: number;
|
|
54
|
+
minLength?: number;
|
|
55
|
+
maxLength?: number;
|
|
56
|
+
pattern?: string;
|
|
57
|
+
minItems?: number;
|
|
58
|
+
maxItems?: number;
|
|
59
|
+
uniqueItems?: boolean;
|
|
60
|
+
},
|
|
61
|
+
schemasByKey?: Record<string, Schema>,
|
|
62
|
+
): {
|
|
63
|
+
type?: string;
|
|
64
|
+
items?: unknown;
|
|
65
|
+
properties?: unknown;
|
|
66
|
+
required?: string[];
|
|
67
|
+
enum?: unknown[];
|
|
68
|
+
const?: unknown;
|
|
69
|
+
oneOf?: unknown[];
|
|
70
|
+
minimum?: number;
|
|
71
|
+
maximum?: number;
|
|
72
|
+
minLength?: number;
|
|
73
|
+
maxLength?: number;
|
|
74
|
+
pattern?: string;
|
|
75
|
+
minItems?: number;
|
|
76
|
+
maxItems?: number;
|
|
77
|
+
uniqueItems?: boolean;
|
|
78
|
+
} | null {
|
|
79
|
+
if (variableSchema.schema) {
|
|
80
|
+
return schemasByKey?.[variableSchema.schema] ?? null;
|
|
81
|
+
}
|
|
82
|
+
return variableSchema as {
|
|
83
|
+
type?: string;
|
|
84
|
+
items?: unknown;
|
|
85
|
+
properties?: unknown;
|
|
86
|
+
required?: string[];
|
|
87
|
+
enum?: unknown[];
|
|
88
|
+
const?: unknown;
|
|
89
|
+
oneOf?: unknown[];
|
|
90
|
+
minimum?: number;
|
|
91
|
+
maximum?: number;
|
|
92
|
+
minLength?: number;
|
|
93
|
+
maxLength?: number;
|
|
94
|
+
pattern?: string;
|
|
95
|
+
minItems?: number;
|
|
96
|
+
maxItems?: number;
|
|
97
|
+
uniqueItems?: boolean;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Resolve a schema by following schema references (schema: key). Used for nested schemas that may have oneOf. */
|
|
102
|
+
function resolveSchemaRefs(
|
|
103
|
+
schema: { schema?: string; [k: string]: unknown },
|
|
104
|
+
schemasByKey?: Record<string, Schema>,
|
|
105
|
+
): { [k: string]: unknown } {
|
|
106
|
+
if (schema.schema && schemasByKey?.[schema.schema]) {
|
|
107
|
+
return resolveSchemaRefs(
|
|
108
|
+
schemasByKey[schema.schema] as { schema?: string; [k: string]: unknown },
|
|
109
|
+
schemasByKey,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return schema;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns true if the value matches the given schema (const, enum, type, object properties, array items, or exactly one of oneOf).
|
|
117
|
+
* Used for oneOf validation: value must match exactly one branch.
|
|
118
|
+
*/
|
|
119
|
+
function valueMatchesSchema(
|
|
120
|
+
schema: { [k: string]: unknown },
|
|
121
|
+
value: unknown,
|
|
122
|
+
schemasByKey?: Record<string, Schema>,
|
|
123
|
+
): boolean {
|
|
124
|
+
const resolved = resolveSchemaRefs(schema, schemasByKey) as {
|
|
125
|
+
type?: string;
|
|
126
|
+
const?: unknown;
|
|
127
|
+
enum?: unknown[];
|
|
128
|
+
oneOf?: unknown[];
|
|
129
|
+
properties?: Record<string, unknown>;
|
|
130
|
+
required?: string[];
|
|
131
|
+
items?: unknown;
|
|
132
|
+
minimum?: number;
|
|
133
|
+
maximum?: number;
|
|
134
|
+
minLength?: number;
|
|
135
|
+
maxLength?: number;
|
|
136
|
+
pattern?: string;
|
|
137
|
+
minItems?: number;
|
|
138
|
+
maxItems?: number;
|
|
139
|
+
uniqueItems?: boolean;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (resolved.oneOf && Array.isArray(resolved.oneOf) && resolved.oneOf.length > 0) {
|
|
143
|
+
const matchCount = resolved.oneOf.filter((branch) =>
|
|
144
|
+
valueMatchesSchema(branch as { [k: string]: unknown }, value, schemasByKey),
|
|
145
|
+
).length;
|
|
146
|
+
return matchCount === 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (resolved.const !== undefined) {
|
|
150
|
+
return valueDeepEqual(value, resolved.const);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (resolved.enum !== undefined && Array.isArray(resolved.enum)) {
|
|
154
|
+
return resolved.enum.some((e) => valueDeepEqual(value, e));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const type = resolved.type;
|
|
158
|
+
if (!type) return false;
|
|
159
|
+
|
|
160
|
+
if (type === "string") {
|
|
161
|
+
if (typeof value !== "string") return false;
|
|
162
|
+
const s = value as string;
|
|
163
|
+
if (resolved.minLength !== undefined && s.length < resolved.minLength) return false;
|
|
164
|
+
if (resolved.maxLength !== undefined && s.length > resolved.maxLength) return false;
|
|
165
|
+
if (resolved.pattern !== undefined) {
|
|
166
|
+
try {
|
|
167
|
+
if (!new RegExp(resolved.pattern).test(s)) return false;
|
|
168
|
+
} catch {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (type === "boolean") return typeof value === "boolean";
|
|
175
|
+
if (type === "integer") {
|
|
176
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return false;
|
|
177
|
+
if (resolved.minimum !== undefined && (value as number) < resolved.minimum) return false;
|
|
178
|
+
if (resolved.maximum !== undefined && (value as number) > resolved.maximum) return false;
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
if (type === "double") {
|
|
182
|
+
if (typeof value !== "number") return false;
|
|
183
|
+
if (resolved.minimum !== undefined && (value as number) < resolved.minimum) return false;
|
|
184
|
+
if (resolved.maximum !== undefined && (value as number) > resolved.maximum) return false;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (type === "json") return typeof value === "string";
|
|
188
|
+
|
|
189
|
+
if (type === "object") {
|
|
190
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
191
|
+
const props = resolved.properties;
|
|
192
|
+
if (!props || typeof props !== "object") return true;
|
|
193
|
+
const obj = value as Record<string, unknown>;
|
|
194
|
+
const required = new Set(resolved.required || []);
|
|
195
|
+
for (const key of required) {
|
|
196
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) return false;
|
|
197
|
+
if (!valueMatchesSchema(props[key] as { [k: string]: unknown }, obj[key], schemasByKey))
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
for (const key of Object.keys(obj)) {
|
|
201
|
+
const propSchema = props[key];
|
|
202
|
+
if (!propSchema) return false;
|
|
203
|
+
if (!valueMatchesSchema(propSchema as { [k: string]: unknown }, obj[key], schemasByKey))
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (type === "array") {
|
|
210
|
+
if (!Array.isArray(value)) return false;
|
|
211
|
+
const arr = value as unknown[];
|
|
212
|
+
if (resolved.minItems !== undefined && arr.length < resolved.minItems) return false;
|
|
213
|
+
if (resolved.maxItems !== undefined && arr.length > resolved.maxItems) return false;
|
|
214
|
+
if (resolved.uniqueItems) {
|
|
215
|
+
for (let i = 0; i < arr.length; i++) {
|
|
216
|
+
for (let j = i + 1; j < arr.length; j++) {
|
|
217
|
+
if (valueDeepEqual(arr[i], arr[j])) return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const itemSchema = resolved.items;
|
|
222
|
+
if (!itemSchema || typeof itemSchema !== "object")
|
|
223
|
+
return arr.every((v) => typeof v === "string");
|
|
224
|
+
return arr.every((item) =>
|
|
225
|
+
valueMatchesSchema(itemSchema as { [k: string]: unknown }, item, schemasByKey),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Deep equality for variable values (primitives, plain objects, arrays). */
|
|
233
|
+
function valueDeepEqual(a: unknown, b: unknown): boolean {
|
|
234
|
+
if (a === b) return true;
|
|
235
|
+
if (typeof a !== typeof b) return false;
|
|
236
|
+
if (a === null || b === null) return a === b;
|
|
237
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
238
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
239
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
240
|
+
if (a.length !== b.length) return false;
|
|
241
|
+
return a.every((v, i) => valueDeepEqual(v, b[i]));
|
|
242
|
+
}
|
|
243
|
+
const keysA = Object.keys(a as object).sort();
|
|
244
|
+
const keysB = Object.keys(b as object).sort();
|
|
245
|
+
if (keysA.length !== keysB.length || keysA.some((k, i) => k !== keysB[i])) return false;
|
|
246
|
+
return keysA.every((k) =>
|
|
247
|
+
valueDeepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Recursively validates that every `required` array (at this level and in nested
|
|
255
|
+
* object/array schemas) only contains keys that exist in the same level's `properties`.
|
|
256
|
+
* Adds Zod issues with the correct path for invalid required field names.
|
|
257
|
+
*/
|
|
258
|
+
function refineRequiredKeysInSchema(
|
|
259
|
+
schema: {
|
|
260
|
+
type?: string;
|
|
261
|
+
properties?: Record<string, unknown>;
|
|
262
|
+
required?: string[];
|
|
263
|
+
items?: unknown;
|
|
264
|
+
},
|
|
265
|
+
pathPrefix: (string | number)[],
|
|
266
|
+
ctx: z.RefinementCtx,
|
|
267
|
+
): void {
|
|
268
|
+
if (!schema || typeof schema !== "object") return;
|
|
269
|
+
|
|
270
|
+
const effectiveType = schema.type;
|
|
271
|
+
const properties = schema.properties;
|
|
272
|
+
const required = schema.required;
|
|
273
|
+
const items = schema.items;
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
effectiveType === "object" &&
|
|
277
|
+
Array.isArray(required) &&
|
|
278
|
+
required.length > 0 &&
|
|
279
|
+
properties &&
|
|
280
|
+
typeof properties === "object"
|
|
281
|
+
) {
|
|
282
|
+
const allowedKeys = Object.keys(properties);
|
|
283
|
+
required.forEach((key, index) => {
|
|
284
|
+
if (!allowedKeys.includes(key)) {
|
|
285
|
+
ctx.addIssue({
|
|
286
|
+
code: z.ZodIssueCode.custom,
|
|
287
|
+
message: `Unknown required field "${key}". \`required\` must only contain property names defined in \`properties\`. Allowed: ${allowedKeys.length ? allowedKeys.join(", ") : "(none)"}.`,
|
|
288
|
+
path: [...pathPrefix, "required", index],
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (properties && typeof properties === "object") {
|
|
295
|
+
for (const key of Object.keys(properties)) {
|
|
296
|
+
const nested = properties[key];
|
|
297
|
+
if (nested && typeof nested === "object") {
|
|
298
|
+
refineRequiredKeysInSchema(
|
|
299
|
+
nested as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
300
|
+
[...pathPrefix, "properties", key],
|
|
301
|
+
ctx,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (items && typeof items === "object" && !Array.isArray(items)) {
|
|
308
|
+
refineRequiredKeysInSchema(
|
|
309
|
+
items as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
310
|
+
[...pathPrefix, "items"],
|
|
311
|
+
ctx,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const oneOf = (schema as { oneOf?: unknown[] }).oneOf;
|
|
316
|
+
if (oneOf && Array.isArray(oneOf)) {
|
|
317
|
+
oneOf.forEach((branch, i) => {
|
|
318
|
+
if (branch && typeof branch === "object") {
|
|
319
|
+
refineRequiredKeysInSchema(
|
|
320
|
+
branch as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
321
|
+
[...pathPrefix, "oneOf", i],
|
|
322
|
+
ctx,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
29
329
|
function typeOfValue(value: unknown): string {
|
|
30
330
|
if (value === null) return "null";
|
|
31
331
|
if (value === undefined) return "undefined";
|
|
@@ -36,21 +336,68 @@ function typeOfValue(value: unknown): string {
|
|
|
36
336
|
/**
|
|
37
337
|
* Validates a variable value against an array schema. Recursively validates each item
|
|
38
338
|
* when the schema defines `items` (nested arrays/objects use the same refinement).
|
|
339
|
+
* Enforces minItems, maxItems, and uniqueItems when set.
|
|
39
340
|
*/
|
|
40
341
|
function refineVariableValueArray(
|
|
41
342
|
projectConfig: ProjectConfig,
|
|
42
|
-
variableSchema: {
|
|
343
|
+
variableSchema: {
|
|
344
|
+
items?: unknown;
|
|
345
|
+
type: string;
|
|
346
|
+
minItems?: number;
|
|
347
|
+
maxItems?: number;
|
|
348
|
+
uniqueItems?: boolean;
|
|
349
|
+
},
|
|
43
350
|
variableValue: unknown[],
|
|
44
351
|
path: (string | number)[],
|
|
45
352
|
ctx: z.RefinementCtx,
|
|
46
353
|
variableKey?: string,
|
|
354
|
+
schemasByKey?: Record<string, Schema>,
|
|
47
355
|
): void {
|
|
48
356
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
357
|
+
const minItems = variableSchema.minItems;
|
|
358
|
+
const maxItems = variableSchema.maxItems;
|
|
359
|
+
const uniqueItems = variableSchema.uniqueItems;
|
|
360
|
+
if (minItems !== undefined && variableValue.length < minItems) {
|
|
361
|
+
ctx.addIssue({
|
|
362
|
+
code: z.ZodIssueCode.custom,
|
|
363
|
+
message: `Variable "${label}" (type array) length (${variableValue.length}) is less than \`minItems\` (${minItems}).`,
|
|
364
|
+
path,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (maxItems !== undefined && variableValue.length > maxItems) {
|
|
368
|
+
ctx.addIssue({
|
|
369
|
+
code: z.ZodIssueCode.custom,
|
|
370
|
+
message: `Variable "${label}" (type array) length (${variableValue.length}) is greater than \`maxItems\` (${maxItems}).`,
|
|
371
|
+
path,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (uniqueItems) {
|
|
375
|
+
for (let i = 0; i < variableValue.length; i++) {
|
|
376
|
+
for (let j = i + 1; j < variableValue.length; j++) {
|
|
377
|
+
if (valueDeepEqual(variableValue[i], variableValue[j])) {
|
|
378
|
+
ctx.addIssue({
|
|
379
|
+
code: z.ZodIssueCode.custom,
|
|
380
|
+
message: `Variable "${label}" (type array) has duplicate items at indices ${i} and ${j} but \`uniqueItems\` is true.`,
|
|
381
|
+
path,
|
|
382
|
+
});
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
49
388
|
const itemSchema = variableSchema.items;
|
|
50
389
|
|
|
51
390
|
if (itemSchema) {
|
|
52
391
|
variableValue.forEach((item, index) => {
|
|
53
|
-
superRefineVariableValue(
|
|
392
|
+
superRefineVariableValue(
|
|
393
|
+
projectConfig,
|
|
394
|
+
itemSchema,
|
|
395
|
+
item,
|
|
396
|
+
[...path, index],
|
|
397
|
+
ctx,
|
|
398
|
+
variableKey,
|
|
399
|
+
schemasByKey,
|
|
400
|
+
);
|
|
54
401
|
});
|
|
55
402
|
} else {
|
|
56
403
|
if (!isArrayOfStrings(variableValue)) {
|
|
@@ -89,6 +436,7 @@ function refineVariableValueObject(
|
|
|
89
436
|
path: (string | number)[],
|
|
90
437
|
ctx: z.RefinementCtx,
|
|
91
438
|
variableKey?: string,
|
|
439
|
+
schemasByKey?: Record<string, Schema>,
|
|
92
440
|
): void {
|
|
93
441
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
94
442
|
const schemaProperties = variableSchema.properties;
|
|
@@ -96,7 +444,9 @@ function refineVariableValueObject(
|
|
|
96
444
|
if (schemaProperties && typeof schemaProperties === "object") {
|
|
97
445
|
const requiredKeys =
|
|
98
446
|
variableSchema.required && variableSchema.required.length > 0
|
|
99
|
-
? variableSchema.required
|
|
447
|
+
? variableSchema.required.filter((k) =>
|
|
448
|
+
Object.prototype.hasOwnProperty.call(schemaProperties, k),
|
|
449
|
+
)
|
|
100
450
|
: Object.keys(schemaProperties);
|
|
101
451
|
|
|
102
452
|
for (const key of requiredKeys) {
|
|
@@ -125,6 +475,7 @@ function refineVariableValueObject(
|
|
|
125
475
|
[...path, key],
|
|
126
476
|
ctx,
|
|
127
477
|
key,
|
|
478
|
+
schemasByKey,
|
|
128
479
|
);
|
|
129
480
|
}
|
|
130
481
|
}
|
|
@@ -160,6 +511,7 @@ function superRefineVariableValue(
|
|
|
160
511
|
path,
|
|
161
512
|
ctx,
|
|
162
513
|
variableKey?: string,
|
|
514
|
+
schemasByKey?: Record<string, Schema>,
|
|
163
515
|
) {
|
|
164
516
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
165
517
|
|
|
@@ -179,6 +531,66 @@ function superRefineVariableValue(
|
|
|
179
531
|
return;
|
|
180
532
|
}
|
|
181
533
|
|
|
534
|
+
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
535
|
+
if (variableSchema.schema && effectiveSchema === null) {
|
|
536
|
+
ctx.addIssue({
|
|
537
|
+
code: z.ZodIssueCode.custom,
|
|
538
|
+
message: `Schema "${variableSchema.schema}" could not be loaded for value validation.`,
|
|
539
|
+
path,
|
|
540
|
+
});
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (!effectiveSchema) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const effectiveOneOf = (effectiveSchema as { oneOf?: unknown[] }).oneOf;
|
|
549
|
+
if (effectiveOneOf !== undefined && Array.isArray(effectiveOneOf) && effectiveOneOf.length > 0) {
|
|
550
|
+
const matchCount = effectiveOneOf.filter((branch) =>
|
|
551
|
+
valueMatchesSchema(branch as { [k: string]: unknown }, variableValue, schemasByKey),
|
|
552
|
+
).length;
|
|
553
|
+
if (matchCount === 0) {
|
|
554
|
+
ctx.addIssue({
|
|
555
|
+
code: z.ZodIssueCode.custom,
|
|
556
|
+
message: `Variable "${label}" must match exactly one of the \`oneOf\` schemas (got ${JSON.stringify(variableValue)}; matched none).`,
|
|
557
|
+
path,
|
|
558
|
+
});
|
|
559
|
+
} else if (matchCount > 1) {
|
|
560
|
+
ctx.addIssue({
|
|
561
|
+
code: z.ZodIssueCode.custom,
|
|
562
|
+
message: `Variable "${label}" must match exactly one of the \`oneOf\` schemas (matched ${matchCount}).`,
|
|
563
|
+
path,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const effectiveConst = (effectiveSchema as { const?: unknown }).const;
|
|
570
|
+
if (effectiveConst !== undefined) {
|
|
571
|
+
if (!valueDeepEqual(variableValue, effectiveConst)) {
|
|
572
|
+
ctx.addIssue({
|
|
573
|
+
code: z.ZodIssueCode.custom,
|
|
574
|
+
message: `Variable "${label}" must equal the constant value defined in schema (got ${JSON.stringify(variableValue)}).`,
|
|
575
|
+
path,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const effectiveEnum = (effectiveSchema as { enum?: unknown[] }).enum;
|
|
582
|
+
if (effectiveEnum !== undefined && Array.isArray(effectiveEnum) && effectiveEnum.length > 0) {
|
|
583
|
+
const allowed = effectiveEnum.some((v) => valueDeepEqual(variableValue, v));
|
|
584
|
+
if (!allowed) {
|
|
585
|
+
ctx.addIssue({
|
|
586
|
+
code: z.ZodIssueCode.custom,
|
|
587
|
+
message: `Variable "${label}" must be one of the allowed enum values (got ${JSON.stringify(variableValue)}).`,
|
|
588
|
+
path,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
182
594
|
// Require a value (no undefined) for every variable usage
|
|
183
595
|
if (variableValue === undefined) {
|
|
184
596
|
ctx.addIssue({
|
|
@@ -189,10 +601,10 @@ function superRefineVariableValue(
|
|
|
189
601
|
return;
|
|
190
602
|
}
|
|
191
603
|
|
|
192
|
-
const expectedType =
|
|
604
|
+
const expectedType = effectiveSchema.type;
|
|
193
605
|
const gotType = typeOfValue(variableValue);
|
|
194
606
|
|
|
195
|
-
// string — only string allowed
|
|
607
|
+
// string — only string allowed; schema minLength/maxLength/pattern applied when set
|
|
196
608
|
if (expectedType === "string") {
|
|
197
609
|
if (typeof variableValue !== "string") {
|
|
198
610
|
ctx.addIssue({
|
|
@@ -203,6 +615,37 @@ function superRefineVariableValue(
|
|
|
203
615
|
return;
|
|
204
616
|
}
|
|
205
617
|
|
|
618
|
+
const strMinLen = (effectiveSchema as { minLength?: number }).minLength;
|
|
619
|
+
const strMaxLen = (effectiveSchema as { maxLength?: number }).maxLength;
|
|
620
|
+
const strPattern = (effectiveSchema as { pattern?: string }).pattern;
|
|
621
|
+
if (strMinLen !== undefined && variableValue.length < strMinLen) {
|
|
622
|
+
ctx.addIssue({
|
|
623
|
+
code: z.ZodIssueCode.custom,
|
|
624
|
+
message: `Variable "${label}" (type string) length (${variableValue.length}) is less than \`minLength\` (${strMinLen}).`,
|
|
625
|
+
path,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (strMaxLen !== undefined && variableValue.length > strMaxLen) {
|
|
629
|
+
ctx.addIssue({
|
|
630
|
+
code: z.ZodIssueCode.custom,
|
|
631
|
+
message: `Variable "${label}" (type string) length (${variableValue.length}) is greater than \`maxLength\` (${strMaxLen}).`,
|
|
632
|
+
path,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (strPattern !== undefined) {
|
|
636
|
+
try {
|
|
637
|
+
if (!new RegExp(strPattern).test(variableValue)) {
|
|
638
|
+
ctx.addIssue({
|
|
639
|
+
code: z.ZodIssueCode.custom,
|
|
640
|
+
message: `Variable "${label}" (type string) does not match \`pattern\`.`,
|
|
641
|
+
path,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
// invalid regex already reported at schema parse time
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
206
649
|
if (
|
|
207
650
|
projectConfig.maxVariableStringLength &&
|
|
208
651
|
variableValue.length > projectConfig.maxVariableStringLength
|
|
@@ -241,6 +684,23 @@ function superRefineVariableValue(
|
|
|
241
684
|
message: `Variable "${label}" (type integer) must be an integer; got ${variableValue}.`,
|
|
242
685
|
path,
|
|
243
686
|
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const intMin = (effectiveSchema as { minimum?: number }).minimum;
|
|
690
|
+
const intMax = (effectiveSchema as { maximum?: number }).maximum;
|
|
691
|
+
if (intMin !== undefined && variableValue < intMin) {
|
|
692
|
+
ctx.addIssue({
|
|
693
|
+
code: z.ZodIssueCode.custom,
|
|
694
|
+
message: `Variable "${label}" (type integer) must be >= minimum (${intMin}); got ${variableValue}.`,
|
|
695
|
+
path,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
if (intMax !== undefined && variableValue > intMax) {
|
|
699
|
+
ctx.addIssue({
|
|
700
|
+
code: z.ZodIssueCode.custom,
|
|
701
|
+
message: `Variable "${label}" (type integer) must be <= maximum (${intMax}); got ${variableValue}.`,
|
|
702
|
+
path,
|
|
703
|
+
});
|
|
244
704
|
}
|
|
245
705
|
return;
|
|
246
706
|
}
|
|
@@ -261,6 +721,23 @@ function superRefineVariableValue(
|
|
|
261
721
|
message: `Variable "${label}" (type double) must be a finite number; got ${variableValue}.`,
|
|
262
722
|
path,
|
|
263
723
|
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const doubleMin = (effectiveSchema as { minimum?: number }).minimum;
|
|
727
|
+
const doubleMax = (effectiveSchema as { maximum?: number }).maximum;
|
|
728
|
+
if (doubleMin !== undefined && variableValue < doubleMin) {
|
|
729
|
+
ctx.addIssue({
|
|
730
|
+
code: z.ZodIssueCode.custom,
|
|
731
|
+
message: `Variable "${label}" (type double) must be >= minimum (${doubleMin}); got ${variableValue}.`,
|
|
732
|
+
path,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (doubleMax !== undefined && variableValue > doubleMax) {
|
|
736
|
+
ctx.addIssue({
|
|
737
|
+
code: z.ZodIssueCode.custom,
|
|
738
|
+
message: `Variable "${label}" (type double) must be <= maximum (${doubleMax}); got ${variableValue}.`,
|
|
739
|
+
path,
|
|
740
|
+
});
|
|
264
741
|
}
|
|
265
742
|
return;
|
|
266
743
|
}
|
|
@@ -287,7 +764,21 @@ function superRefineVariableValue(
|
|
|
287
764
|
});
|
|
288
765
|
return;
|
|
289
766
|
}
|
|
290
|
-
refineVariableValueArray(
|
|
767
|
+
refineVariableValueArray(
|
|
768
|
+
projectConfig,
|
|
769
|
+
effectiveSchema as {
|
|
770
|
+
items?: unknown;
|
|
771
|
+
type: string;
|
|
772
|
+
minItems?: number;
|
|
773
|
+
maxItems?: number;
|
|
774
|
+
uniqueItems?: boolean;
|
|
775
|
+
},
|
|
776
|
+
variableValue,
|
|
777
|
+
path,
|
|
778
|
+
ctx,
|
|
779
|
+
variableKey,
|
|
780
|
+
schemasByKey,
|
|
781
|
+
);
|
|
291
782
|
return;
|
|
292
783
|
}
|
|
293
784
|
|
|
@@ -307,11 +798,16 @@ function superRefineVariableValue(
|
|
|
307
798
|
}
|
|
308
799
|
refineVariableValueObject(
|
|
309
800
|
projectConfig,
|
|
310
|
-
|
|
801
|
+
effectiveSchema as {
|
|
802
|
+
properties?: Record<string, unknown>;
|
|
803
|
+
required?: string[];
|
|
804
|
+
type: string;
|
|
805
|
+
},
|
|
311
806
|
variableValue as Record<string, unknown>,
|
|
312
807
|
path,
|
|
313
808
|
ctx,
|
|
314
809
|
variableKey,
|
|
810
|
+
schemasByKey,
|
|
315
811
|
);
|
|
316
812
|
return;
|
|
317
813
|
}
|
|
@@ -366,6 +862,7 @@ function refineForce({
|
|
|
366
862
|
force,
|
|
367
863
|
pathPrefix,
|
|
368
864
|
projectConfig,
|
|
865
|
+
schemasByKey,
|
|
369
866
|
}) {
|
|
370
867
|
force.forEach((f, fN) => {
|
|
371
868
|
// force[n].variation
|
|
@@ -382,14 +879,24 @@ function refineForce({
|
|
|
382
879
|
// force[n].variables[key]
|
|
383
880
|
if (f.variables) {
|
|
384
881
|
Object.keys(f.variables).forEach((variableKey) => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
882
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
883
|
+
if (!variableSchema) {
|
|
884
|
+
ctx.addIssue({
|
|
885
|
+
code: z.ZodIssueCode.custom,
|
|
886
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
887
|
+
path: pathPrefix.concat([fN, "variables", variableKey]),
|
|
888
|
+
});
|
|
889
|
+
} else {
|
|
890
|
+
superRefineVariableValue(
|
|
891
|
+
projectConfig,
|
|
892
|
+
variableSchema,
|
|
893
|
+
f.variables[variableKey],
|
|
894
|
+
pathPrefix.concat([fN, "variables", variableKey]),
|
|
895
|
+
ctx,
|
|
896
|
+
variableKey,
|
|
897
|
+
schemasByKey,
|
|
898
|
+
);
|
|
899
|
+
}
|
|
393
900
|
});
|
|
394
901
|
}
|
|
395
902
|
});
|
|
@@ -403,19 +910,30 @@ function refineRules({
|
|
|
403
910
|
rules,
|
|
404
911
|
pathPrefix,
|
|
405
912
|
projectConfig,
|
|
913
|
+
schemasByKey,
|
|
406
914
|
}) {
|
|
407
915
|
rules.forEach((rule, ruleN) => {
|
|
408
916
|
// rules[n].variables[key]
|
|
409
917
|
if (rule.variables) {
|
|
410
918
|
Object.keys(rule.variables).forEach((variableKey) => {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
919
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
920
|
+
if (!variableSchema) {
|
|
921
|
+
ctx.addIssue({
|
|
922
|
+
code: z.ZodIssueCode.custom,
|
|
923
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
924
|
+
path: pathPrefix.concat([ruleN, "variables", variableKey]),
|
|
925
|
+
});
|
|
926
|
+
} else {
|
|
927
|
+
superRefineVariableValue(
|
|
928
|
+
projectConfig,
|
|
929
|
+
variableSchema,
|
|
930
|
+
rule.variables[variableKey],
|
|
931
|
+
pathPrefix.concat([ruleN, "variables", variableKey]),
|
|
932
|
+
ctx,
|
|
933
|
+
variableKey,
|
|
934
|
+
schemasByKey,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
419
937
|
});
|
|
420
938
|
}
|
|
421
939
|
|
|
@@ -491,8 +1009,10 @@ export function getFeatureZodSchema(
|
|
|
491
1009
|
availableAttributeKeys: [string, ...string[]],
|
|
492
1010
|
availableSegmentKeys: [string, ...string[]],
|
|
493
1011
|
availableFeatureKeys: [string, ...string[]],
|
|
1012
|
+
availableSchemaKeys: string[] = [],
|
|
1013
|
+
schemasByKey: Record<string, Schema> = {},
|
|
494
1014
|
) {
|
|
495
|
-
const
|
|
1015
|
+
const schemaZodSchema = getSchemaZodSchema(availableSchemaKeys);
|
|
496
1016
|
const variableValueZodSchema = valueZodSchema;
|
|
497
1017
|
|
|
498
1018
|
const variationValueZodSchema = z.string().min(1);
|
|
@@ -674,11 +1194,31 @@ export function getFeatureZodSchema(
|
|
|
674
1194
|
.object({
|
|
675
1195
|
deprecated: z.boolean().optional(),
|
|
676
1196
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1197
|
+
// Reference to a reusable schema (mutually exclusive with type/properties/required/items)
|
|
1198
|
+
schema: z
|
|
1199
|
+
.string()
|
|
1200
|
+
.refine(
|
|
1201
|
+
(value) => availableSchemaKeys.includes(value),
|
|
1202
|
+
(value) => ({ message: `Unknown schema "${value}"` }),
|
|
1203
|
+
)
|
|
1204
|
+
.optional(),
|
|
1205
|
+
|
|
1206
|
+
// Inline schema (mutually exclusive with schema)
|
|
1207
|
+
type: z.union([z.literal("json"), propertyTypeEnum]).optional(),
|
|
1208
|
+
items: schemaZodSchema.optional(),
|
|
1209
|
+
properties: z.record(schemaZodSchema).optional(),
|
|
1210
|
+
required: z.array(z.string()).optional(),
|
|
1211
|
+
enum: z.array(variableValueZodSchema).optional(),
|
|
1212
|
+
const: variableValueZodSchema.optional(),
|
|
1213
|
+
oneOf: z.array(schemaZodSchema).min(1).optional(),
|
|
1214
|
+
minimum: z.number().optional(),
|
|
1215
|
+
maximum: z.number().optional(),
|
|
1216
|
+
minLength: z.number().optional(),
|
|
1217
|
+
maxLength: z.number().optional(),
|
|
1218
|
+
pattern: z.string().optional(),
|
|
1219
|
+
minItems: z.number().optional(),
|
|
1220
|
+
maxItems: z.number().optional(),
|
|
1221
|
+
uniqueItems: z.boolean().optional(),
|
|
682
1222
|
|
|
683
1223
|
description: z.string().optional(),
|
|
684
1224
|
|
|
@@ -687,7 +1227,85 @@ export function getFeatureZodSchema(
|
|
|
687
1227
|
|
|
688
1228
|
useDefaultWhenDisabled: z.boolean().optional(),
|
|
689
1229
|
})
|
|
690
|
-
.strict()
|
|
1230
|
+
.strict()
|
|
1231
|
+
.superRefine((variableSchema, ctx) => {
|
|
1232
|
+
const hasRef = "schema" in variableSchema && variableSchema.schema != null;
|
|
1233
|
+
const hasInline =
|
|
1234
|
+
"type" in variableSchema &&
|
|
1235
|
+
variableSchema.type != null &&
|
|
1236
|
+
variableSchema.type !== undefined;
|
|
1237
|
+
const hasOneOf =
|
|
1238
|
+
"oneOf" in variableSchema &&
|
|
1239
|
+
Array.isArray(variableSchema.oneOf) &&
|
|
1240
|
+
variableSchema.oneOf.length > 0;
|
|
1241
|
+
if (hasRef && (hasInline || hasOneOf)) {
|
|
1242
|
+
ctx.addIssue({
|
|
1243
|
+
code: z.ZodIssueCode.custom,
|
|
1244
|
+
message:
|
|
1245
|
+
"Variable schema cannot have both `schema` (reference) and inline properties (`type`, `oneOf`, `properties`, `required`, `items`). Use one or the other.",
|
|
1246
|
+
path: [],
|
|
1247
|
+
});
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (hasRef) {
|
|
1251
|
+
const hasInlineStructure =
|
|
1252
|
+
("type" in variableSchema && variableSchema.type != null) ||
|
|
1253
|
+
("properties" in variableSchema && variableSchema.properties != null) ||
|
|
1254
|
+
("required" in variableSchema && variableSchema.required != null) ||
|
|
1255
|
+
("items" in variableSchema && variableSchema.items != null) ||
|
|
1256
|
+
("oneOf" in variableSchema && variableSchema.oneOf != null);
|
|
1257
|
+
const hasInlineValidation =
|
|
1258
|
+
("minimum" in variableSchema && variableSchema.minimum !== undefined) ||
|
|
1259
|
+
("maximum" in variableSchema && variableSchema.maximum !== undefined) ||
|
|
1260
|
+
("minLength" in variableSchema && variableSchema.minLength !== undefined) ||
|
|
1261
|
+
("maxLength" in variableSchema && variableSchema.maxLength !== undefined) ||
|
|
1262
|
+
("pattern" in variableSchema && variableSchema.pattern !== undefined) ||
|
|
1263
|
+
("minItems" in variableSchema && variableSchema.minItems !== undefined) ||
|
|
1264
|
+
("maxItems" in variableSchema && variableSchema.maxItems !== undefined) ||
|
|
1265
|
+
("uniqueItems" in variableSchema && variableSchema.uniqueItems !== undefined);
|
|
1266
|
+
if (hasInlineStructure) {
|
|
1267
|
+
ctx.addIssue({
|
|
1268
|
+
code: z.ZodIssueCode.custom,
|
|
1269
|
+
message:
|
|
1270
|
+
"When `schema` is set, do not set `type`, `oneOf`, `properties`, `required`, or `items`.",
|
|
1271
|
+
path: [],
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if (hasInlineValidation) {
|
|
1275
|
+
ctx.addIssue({
|
|
1276
|
+
code: z.ZodIssueCode.custom,
|
|
1277
|
+
message:
|
|
1278
|
+
"When `schema` is set, do not set `minimum`, `maximum`, `minLength`, `maxLength`, `pattern`, `minItems`, `maxItems`, or `uniqueItems`; use the referenced schema to define these.",
|
|
1279
|
+
path: [],
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (!hasInline && !hasOneOf) {
|
|
1285
|
+
ctx.addIssue({
|
|
1286
|
+
code: z.ZodIssueCode.custom,
|
|
1287
|
+
message:
|
|
1288
|
+
"Variable schema must have either `schema` (reference to a schema key), `type` (inline schema), or `oneOf` (inline oneOf schemas).",
|
|
1289
|
+
path: [],
|
|
1290
|
+
});
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (hasInline && hasOneOf) {
|
|
1294
|
+
ctx.addIssue({
|
|
1295
|
+
code: z.ZodIssueCode.custom,
|
|
1296
|
+
message:
|
|
1297
|
+
"Variable schema cannot have both `type` and `oneOf` at the top level. Use one or the other.",
|
|
1298
|
+
path: [],
|
|
1299
|
+
});
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
// Validate required ⊆ properties at this level and in all nested object schemas
|
|
1303
|
+
refineRequiredKeysInSchema(
|
|
1304
|
+
variableSchema as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
1305
|
+
[],
|
|
1306
|
+
ctx,
|
|
1307
|
+
);
|
|
1308
|
+
}),
|
|
691
1309
|
)
|
|
692
1310
|
.optional(),
|
|
693
1311
|
|
|
@@ -777,6 +1395,10 @@ export function getFeatureZodSchema(
|
|
|
777
1395
|
return;
|
|
778
1396
|
}
|
|
779
1397
|
|
|
1398
|
+
// Every variable value is validated against its schema from variablesSchema. Sources covered:
|
|
1399
|
+
// 1. variablesSchema[key].defaultValue 2. variablesSchema[key].disabledValue
|
|
1400
|
+
// 3. variations[n].variables[key] 4. variations[n].variableOverrides[key][].value
|
|
1401
|
+
// 5. rules[env][n].variables[key] 6. force[env][n].variables[key]
|
|
780
1402
|
const variableSchemaByKey = value.variablesSchema;
|
|
781
1403
|
const variationValues: string[] = [];
|
|
782
1404
|
|
|
@@ -791,6 +1413,41 @@ export function getFeatureZodSchema(
|
|
|
791
1413
|
variableKeys.forEach((variableKey) => {
|
|
792
1414
|
const variableSchema = variableSchemaByKey[variableKey];
|
|
793
1415
|
|
|
1416
|
+
// When type and enum are both present, all enum values must match the type
|
|
1417
|
+
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
1418
|
+
if (
|
|
1419
|
+
effectiveSchema &&
|
|
1420
|
+
effectiveSchema.type &&
|
|
1421
|
+
Array.isArray(effectiveSchema.enum) &&
|
|
1422
|
+
effectiveSchema.enum.length > 0
|
|
1423
|
+
) {
|
|
1424
|
+
refineEnumMatchesType(
|
|
1425
|
+
effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
|
|
1426
|
+
["variablesSchema", variableKey],
|
|
1427
|
+
ctx,
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
|
|
1432
|
+
if (!("schema" in variableSchema) || !variableSchema.schema) {
|
|
1433
|
+
const pathPrefix = ["variablesSchema", variableKey];
|
|
1434
|
+
refineMinimumMaximum(
|
|
1435
|
+
variableSchema as Parameters<typeof refineMinimumMaximum>[0],
|
|
1436
|
+
pathPrefix,
|
|
1437
|
+
ctx,
|
|
1438
|
+
);
|
|
1439
|
+
refineStringLengthPattern(
|
|
1440
|
+
variableSchema as Parameters<typeof refineStringLengthPattern>[0],
|
|
1441
|
+
pathPrefix,
|
|
1442
|
+
ctx,
|
|
1443
|
+
);
|
|
1444
|
+
refineArrayItems(
|
|
1445
|
+
variableSchema as Parameters<typeof refineArrayItems>[0],
|
|
1446
|
+
pathPrefix,
|
|
1447
|
+
ctx,
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
794
1451
|
if (variableKey === "variation") {
|
|
795
1452
|
ctx.addIssue({
|
|
796
1453
|
code: z.ZodIssueCode.custom,
|
|
@@ -807,6 +1464,7 @@ export function getFeatureZodSchema(
|
|
|
807
1464
|
["variablesSchema", variableKey, "defaultValue"],
|
|
808
1465
|
ctx,
|
|
809
1466
|
variableKey,
|
|
1467
|
+
schemasByKey,
|
|
810
1468
|
);
|
|
811
1469
|
|
|
812
1470
|
// disabledValue (only when present)
|
|
@@ -818,54 +1476,69 @@ export function getFeatureZodSchema(
|
|
|
818
1476
|
["variablesSchema", variableKey, "disabledValue"],
|
|
819
1477
|
ctx,
|
|
820
1478
|
variableKey,
|
|
1479
|
+
schemasByKey,
|
|
821
1480
|
);
|
|
822
1481
|
}
|
|
823
1482
|
});
|
|
824
1483
|
|
|
825
|
-
// variations
|
|
1484
|
+
// variations: validate variation.variables and variation.variableOverrides (each value against its variable schema)
|
|
826
1485
|
if (value.variations) {
|
|
827
1486
|
value.variations.forEach((variation, variationN) => {
|
|
828
|
-
if (!variation.variables) {
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
1487
|
// variations[n].variables[key]
|
|
833
|
-
|
|
834
|
-
const
|
|
1488
|
+
if (variation.variables) {
|
|
1489
|
+
for (const variableKey of Object.keys(variation.variables)) {
|
|
1490
|
+
const variableValue = variation.variables[variableKey];
|
|
1491
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
1492
|
+
if (!variableSchema) {
|
|
1493
|
+
ctx.addIssue({
|
|
1494
|
+
code: z.ZodIssueCode.custom,
|
|
1495
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
1496
|
+
path: ["variations", variationN, "variables", variableKey],
|
|
1497
|
+
});
|
|
1498
|
+
} else {
|
|
1499
|
+
superRefineVariableValue(
|
|
1500
|
+
projectConfig,
|
|
1501
|
+
variableSchema,
|
|
1502
|
+
variableValue,
|
|
1503
|
+
["variations", variationN, "variables", variableKey],
|
|
1504
|
+
ctx,
|
|
1505
|
+
variableKey,
|
|
1506
|
+
schemasByKey,
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
835
1511
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
"variations",
|
|
858
|
-
variationN,
|
|
859
|
-
"variableOverrides",
|
|
860
|
-
variableKey,
|
|
861
|
-
overrideN,
|
|
862
|
-
"value",
|
|
863
|
-
],
|
|
864
|
-
ctx,
|
|
1512
|
+
// variations[n].variableOverrides[key][].value (validated even when variation.variables is absent)
|
|
1513
|
+
if (variation.variableOverrides) {
|
|
1514
|
+
for (const variableKey of Object.keys(variation.variableOverrides)) {
|
|
1515
|
+
const overrides = variation.variableOverrides[variableKey];
|
|
1516
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
1517
|
+
if (!variableSchema) {
|
|
1518
|
+
ctx.addIssue({
|
|
1519
|
+
code: z.ZodIssueCode.custom,
|
|
1520
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
1521
|
+
path: ["variations", variationN, "variableOverrides", variableKey],
|
|
1522
|
+
});
|
|
1523
|
+
} else if (Array.isArray(overrides)) {
|
|
1524
|
+
overrides.forEach((override, overrideN) => {
|
|
1525
|
+
superRefineVariableValue(
|
|
1526
|
+
projectConfig,
|
|
1527
|
+
variableSchema,
|
|
1528
|
+
override.value,
|
|
1529
|
+
[
|
|
1530
|
+
"variations",
|
|
1531
|
+
variationN,
|
|
1532
|
+
"variableOverrides",
|
|
865
1533
|
variableKey,
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1534
|
+
overrideN,
|
|
1535
|
+
"value",
|
|
1536
|
+
],
|
|
1537
|
+
ctx,
|
|
1538
|
+
variableKey,
|
|
1539
|
+
schemasByKey,
|
|
1540
|
+
);
|
|
1541
|
+
});
|
|
869
1542
|
}
|
|
870
1543
|
}
|
|
871
1544
|
}
|
|
@@ -885,6 +1558,7 @@ export function getFeatureZodSchema(
|
|
|
885
1558
|
pathPrefix: ["rules", environmentKey],
|
|
886
1559
|
ctx,
|
|
887
1560
|
projectConfig,
|
|
1561
|
+
schemasByKey,
|
|
888
1562
|
});
|
|
889
1563
|
}
|
|
890
1564
|
|
|
@@ -898,6 +1572,7 @@ export function getFeatureZodSchema(
|
|
|
898
1572
|
pathPrefix: ["force", environmentKey],
|
|
899
1573
|
ctx,
|
|
900
1574
|
projectConfig,
|
|
1575
|
+
schemasByKey,
|
|
901
1576
|
});
|
|
902
1577
|
}
|
|
903
1578
|
}
|
|
@@ -914,6 +1589,7 @@ export function getFeatureZodSchema(
|
|
|
914
1589
|
pathPrefix: ["rules"],
|
|
915
1590
|
ctx,
|
|
916
1591
|
projectConfig,
|
|
1592
|
+
schemasByKey,
|
|
917
1593
|
});
|
|
918
1594
|
}
|
|
919
1595
|
|
|
@@ -927,6 +1603,7 @@ export function getFeatureZodSchema(
|
|
|
927
1603
|
pathPrefix: ["force"],
|
|
928
1604
|
ctx,
|
|
929
1605
|
projectConfig,
|
|
1606
|
+
schemasByKey,
|
|
930
1607
|
});
|
|
931
1608
|
}
|
|
932
1609
|
}
|