@featurevisor/core 2.9.0 → 2.11.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 +2 -2
- 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 +1 -1
- 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/lib/generate-code/typescript.js +150 -16
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/featureSchema.d.ts +142 -101
- package/lib/linter/featureSchema.js +269 -81
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/propertySchema.d.ts +5 -0
- package/lib/linter/propertySchema.js +43 -0
- package/lib/linter/propertySchema.js.map +1 -0
- package/package.json +5 -5
- package/src/generate-code/typescript.ts +168 -18
- package/src/linter/featureSchema.ts +358 -96
- package/src/linter/propertySchema.ts +47 -0
|
@@ -1,23 +1,221 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import { ProjectConfig } from "../config";
|
|
4
|
+
import { valueZodSchema, propertyTypeEnum, getPropertyZodSchema } from "./propertySchema";
|
|
4
5
|
|
|
5
6
|
const tagRegex = /^[a-z0-9-]+$/;
|
|
6
7
|
|
|
7
|
-
function
|
|
8
|
-
|
|
8
|
+
function isArrayOfStrings(value: unknown): value is string[] {
|
|
9
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isFlatObjectValue(value: unknown): boolean {
|
|
13
|
+
return (
|
|
14
|
+
value === null ||
|
|
15
|
+
typeof value === "string" ||
|
|
16
|
+
typeof value === "number" ||
|
|
17
|
+
typeof value === "boolean"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getVariableLabel(variableSchema, variableKey, path) {
|
|
22
|
+
return (
|
|
23
|
+
variableKey ??
|
|
24
|
+
variableSchema?.key ??
|
|
25
|
+
(path.length > 0 ? String(path[path.length - 1]) : "variable")
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Recursively validates that every `required` array (at this level and in nested
|
|
31
|
+
* object/array schemas) only contains keys that exist in the same level's `properties`.
|
|
32
|
+
* Adds Zod issues with the correct path for invalid required field names.
|
|
33
|
+
*/
|
|
34
|
+
function refineRequiredKeysInSchema(
|
|
35
|
+
schema: {
|
|
36
|
+
type?: string;
|
|
37
|
+
properties?: Record<string, unknown>;
|
|
38
|
+
required?: string[];
|
|
39
|
+
items?: unknown;
|
|
40
|
+
},
|
|
41
|
+
pathPrefix: (string | number)[],
|
|
42
|
+
ctx: z.RefinementCtx,
|
|
43
|
+
): void {
|
|
44
|
+
if (!schema || typeof schema !== "object") return;
|
|
45
|
+
|
|
46
|
+
const effectiveType = schema.type;
|
|
47
|
+
const properties = schema.properties;
|
|
48
|
+
const required = schema.required;
|
|
49
|
+
const items = schema.items;
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
effectiveType === "object" &&
|
|
53
|
+
Array.isArray(required) &&
|
|
54
|
+
required.length > 0 &&
|
|
55
|
+
properties &&
|
|
56
|
+
typeof properties === "object"
|
|
57
|
+
) {
|
|
58
|
+
const allowedKeys = Object.keys(properties);
|
|
59
|
+
required.forEach((key, index) => {
|
|
60
|
+
if (!allowedKeys.includes(key)) {
|
|
61
|
+
ctx.addIssue({
|
|
62
|
+
code: z.ZodIssueCode.custom,
|
|
63
|
+
message: `Unknown required field "${key}". \`required\` must only contain property names defined in \`properties\`. Allowed: ${allowedKeys.length ? allowedKeys.join(", ") : "(none)"}.`,
|
|
64
|
+
path: [...pathPrefix, "required", index],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
9
69
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
70
|
+
if (properties && typeof properties === "object") {
|
|
71
|
+
for (const key of Object.keys(properties)) {
|
|
72
|
+
const nested = properties[key];
|
|
73
|
+
if (nested && typeof nested === "object") {
|
|
74
|
+
refineRequiredKeysInSchema(
|
|
75
|
+
nested as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
76
|
+
[...pathPrefix, "properties", key],
|
|
77
|
+
ctx,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
13
80
|
}
|
|
14
|
-
}
|
|
81
|
+
}
|
|
15
82
|
|
|
16
|
-
|
|
83
|
+
if (items && typeof items === "object" && !Array.isArray(items)) {
|
|
84
|
+
refineRequiredKeysInSchema(
|
|
85
|
+
items as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
86
|
+
[...pathPrefix, "items"],
|
|
87
|
+
ctx,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
17
90
|
}
|
|
18
91
|
|
|
19
|
-
function
|
|
20
|
-
|
|
92
|
+
function typeOfValue(value: unknown): string {
|
|
93
|
+
if (value === null) return "null";
|
|
94
|
+
if (value === undefined) return "undefined";
|
|
95
|
+
if (Array.isArray(value)) return "array";
|
|
96
|
+
return typeof value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates a variable value against an array schema. Recursively validates each item
|
|
101
|
+
* when the schema defines `items` (nested arrays/objects use the same refinement).
|
|
102
|
+
*/
|
|
103
|
+
function refineVariableValueArray(
|
|
104
|
+
projectConfig: ProjectConfig,
|
|
105
|
+
variableSchema: { items?: unknown; type: string },
|
|
106
|
+
variableValue: unknown[],
|
|
107
|
+
path: (string | number)[],
|
|
108
|
+
ctx: z.RefinementCtx,
|
|
109
|
+
variableKey?: string,
|
|
110
|
+
): void {
|
|
111
|
+
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
112
|
+
const itemSchema = variableSchema.items;
|
|
113
|
+
|
|
114
|
+
if (itemSchema) {
|
|
115
|
+
variableValue.forEach((item, index) => {
|
|
116
|
+
superRefineVariableValue(projectConfig, itemSchema, item, [...path, index], ctx, variableKey);
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
if (!isArrayOfStrings(variableValue)) {
|
|
120
|
+
ctx.addIssue({
|
|
121
|
+
code: z.ZodIssueCode.custom,
|
|
122
|
+
message: `Variable "${label}" (type array): when \`items\` is not set, array must contain only strings; found non-string element.`,
|
|
123
|
+
path,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (projectConfig.maxVariableArrayStringifiedLength) {
|
|
129
|
+
const stringified = JSON.stringify(variableValue);
|
|
130
|
+
if (stringified.length > projectConfig.maxVariableArrayStringifiedLength) {
|
|
131
|
+
ctx.addIssue({
|
|
132
|
+
code: z.ZodIssueCode.custom,
|
|
133
|
+
message: `Variable "${label}" array is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableArrayStringifiedLength}`,
|
|
134
|
+
path,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validates a variable value against an object schema. Recursively validates each property
|
|
142
|
+
* when the schema defines `properties` (nested objects/arrays use the same refinement).
|
|
143
|
+
*/
|
|
144
|
+
function refineVariableValueObject(
|
|
145
|
+
projectConfig: ProjectConfig,
|
|
146
|
+
variableSchema: {
|
|
147
|
+
properties?: Record<string, unknown>;
|
|
148
|
+
required?: string[];
|
|
149
|
+
type: string;
|
|
150
|
+
},
|
|
151
|
+
variableValue: Record<string, unknown>,
|
|
152
|
+
path: (string | number)[],
|
|
153
|
+
ctx: z.RefinementCtx,
|
|
154
|
+
variableKey?: string,
|
|
155
|
+
): void {
|
|
156
|
+
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
157
|
+
const schemaProperties = variableSchema.properties;
|
|
158
|
+
|
|
159
|
+
if (schemaProperties && typeof schemaProperties === "object") {
|
|
160
|
+
const requiredKeys =
|
|
161
|
+
variableSchema.required && variableSchema.required.length > 0
|
|
162
|
+
? variableSchema.required.filter((k) =>
|
|
163
|
+
Object.prototype.hasOwnProperty.call(schemaProperties, k),
|
|
164
|
+
)
|
|
165
|
+
: Object.keys(schemaProperties);
|
|
166
|
+
|
|
167
|
+
for (const key of requiredKeys) {
|
|
168
|
+
if (!Object.prototype.hasOwnProperty.call(variableValue, key)) {
|
|
169
|
+
ctx.addIssue({
|
|
170
|
+
code: z.ZodIssueCode.custom,
|
|
171
|
+
message: `Missing required property "${key}" in variable "${label}"`,
|
|
172
|
+
path: [...path, key],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const key of Object.keys(variableValue)) {
|
|
178
|
+
const propSchema = schemaProperties[key];
|
|
179
|
+
if (!propSchema) {
|
|
180
|
+
ctx.addIssue({
|
|
181
|
+
code: z.ZodIssueCode.custom,
|
|
182
|
+
message: `Unknown property "${key}" in variable "${label}" (not in schema)`,
|
|
183
|
+
path: [...path, key],
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
superRefineVariableValue(
|
|
187
|
+
projectConfig,
|
|
188
|
+
propSchema,
|
|
189
|
+
variableValue[key],
|
|
190
|
+
[...path, key],
|
|
191
|
+
ctx,
|
|
192
|
+
key,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
for (const key of Object.keys(variableValue)) {
|
|
198
|
+
const propValue = variableValue[key];
|
|
199
|
+
if (!isFlatObjectValue(propValue)) {
|
|
200
|
+
ctx.addIssue({
|
|
201
|
+
code: z.ZodIssueCode.custom,
|
|
202
|
+
message: `Variable "${label}" is a flat object (no \`properties\` in schema); property "${key}" must be a primitive (string, number, boolean, or null), got: ${typeof propValue}`,
|
|
203
|
+
path: [...path, key],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (projectConfig.maxVariableObjectStringifiedLength) {
|
|
210
|
+
const stringified = JSON.stringify(variableValue);
|
|
211
|
+
if (stringified.length > projectConfig.maxVariableObjectStringifiedLength) {
|
|
212
|
+
ctx.addIssue({
|
|
213
|
+
code: z.ZodIssueCode.custom,
|
|
214
|
+
message: `Variable "${label}" object is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableObjectStringifiedLength}`,
|
|
215
|
+
path,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
21
219
|
}
|
|
22
220
|
|
|
23
221
|
function superRefineVariableValue(
|
|
@@ -26,17 +224,16 @@ function superRefineVariableValue(
|
|
|
26
224
|
variableValue,
|
|
27
225
|
path,
|
|
28
226
|
ctx,
|
|
227
|
+
variableKey?: string,
|
|
29
228
|
) {
|
|
30
|
-
|
|
31
|
-
let message = `Unknown variable with value: ${variableValue}`;
|
|
229
|
+
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
32
230
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
231
|
+
if (!variableSchema) {
|
|
232
|
+
const variableName =
|
|
233
|
+
path.length > 0 && typeof path[path.length - 1] === "string"
|
|
234
|
+
? String(path[path.length - 1])
|
|
235
|
+
: "variable";
|
|
236
|
+
const message = `Variable "${variableName}" is used but not defined in variablesSchema. Define it under variablesSchema first, then use it here.`;
|
|
40
237
|
|
|
41
238
|
ctx.addIssue({
|
|
42
239
|
code: z.ZodIssueCode.custom,
|
|
@@ -47,14 +244,28 @@ function superRefineVariableValue(
|
|
|
47
244
|
return;
|
|
48
245
|
}
|
|
49
246
|
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
247
|
+
// Require a value (no undefined) for every variable usage
|
|
248
|
+
if (variableValue === undefined) {
|
|
249
|
+
ctx.addIssue({
|
|
250
|
+
code: z.ZodIssueCode.custom,
|
|
251
|
+
message: `Variable "${label}" value is required (got undefined).`,
|
|
252
|
+
path,
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const expectedType = variableSchema.type;
|
|
258
|
+
const gotType = typeOfValue(variableValue);
|
|
259
|
+
|
|
260
|
+
// string — only string allowed
|
|
261
|
+
if (expectedType === "string") {
|
|
52
262
|
if (typeof variableValue !== "string") {
|
|
53
263
|
ctx.addIssue({
|
|
54
264
|
code: z.ZodIssueCode.custom,
|
|
55
|
-
message: `
|
|
265
|
+
message: `Variable "${label}" (type string) must be a string; got ${gotType}.`,
|
|
56
266
|
path,
|
|
57
267
|
});
|
|
268
|
+
return;
|
|
58
269
|
}
|
|
59
270
|
|
|
60
271
|
if (
|
|
@@ -63,7 +274,7 @@ function superRefineVariableValue(
|
|
|
63
274
|
) {
|
|
64
275
|
ctx.addIssue({
|
|
65
276
|
code: z.ZodIssueCode.custom,
|
|
66
|
-
message: `Variable "${
|
|
277
|
+
message: `Variable "${label}" value is too long (${variableValue.length} characters), max length is ${projectConfig.maxVariableStringLength}`,
|
|
67
278
|
path,
|
|
68
279
|
});
|
|
69
280
|
}
|
|
@@ -71,94 +282,123 @@ function superRefineVariableValue(
|
|
|
71
282
|
return;
|
|
72
283
|
}
|
|
73
284
|
|
|
74
|
-
// integer,
|
|
75
|
-
if (
|
|
285
|
+
// integer — only integer number allowed (no NaN, no Infinity, no float)
|
|
286
|
+
if (expectedType === "integer") {
|
|
76
287
|
if (typeof variableValue !== "number") {
|
|
77
288
|
ctx.addIssue({
|
|
78
289
|
code: z.ZodIssueCode.custom,
|
|
79
|
-
message: `
|
|
290
|
+
message: `Variable "${label}" (type integer) must be a number; got ${gotType}.`,
|
|
291
|
+
path,
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (!Number.isFinite(variableValue)) {
|
|
296
|
+
ctx.addIssue({
|
|
297
|
+
code: z.ZodIssueCode.custom,
|
|
298
|
+
message: `Variable "${label}" (type integer) must be a finite number; got ${variableValue}.`,
|
|
299
|
+
path,
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (!Number.isInteger(variableValue)) {
|
|
304
|
+
ctx.addIssue({
|
|
305
|
+
code: z.ZodIssueCode.custom,
|
|
306
|
+
message: `Variable "${label}" (type integer) must be an integer; got ${variableValue}.`,
|
|
80
307
|
path,
|
|
81
308
|
});
|
|
82
309
|
}
|
|
83
|
-
|
|
84
310
|
return;
|
|
85
311
|
}
|
|
86
312
|
|
|
87
|
-
//
|
|
88
|
-
if (
|
|
89
|
-
if (typeof variableValue !== "
|
|
313
|
+
// double — only finite number allowed
|
|
314
|
+
if (expectedType === "double") {
|
|
315
|
+
if (typeof variableValue !== "number") {
|
|
90
316
|
ctx.addIssue({
|
|
91
317
|
code: z.ZodIssueCode.custom,
|
|
92
|
-
message: `
|
|
318
|
+
message: `Variable "${label}" (type double) must be a number; got ${gotType}.`,
|
|
319
|
+
path,
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!Number.isFinite(variableValue)) {
|
|
324
|
+
ctx.addIssue({
|
|
325
|
+
code: z.ZodIssueCode.custom,
|
|
326
|
+
message: `Variable "${label}" (type double) must be a finite number; got ${variableValue}.`,
|
|
93
327
|
path,
|
|
94
328
|
});
|
|
95
329
|
}
|
|
96
|
-
|
|
97
330
|
return;
|
|
98
331
|
}
|
|
99
332
|
|
|
100
|
-
//
|
|
101
|
-
if (
|
|
102
|
-
if (
|
|
333
|
+
// boolean — only boolean allowed
|
|
334
|
+
if (expectedType === "boolean") {
|
|
335
|
+
if (typeof variableValue !== "boolean") {
|
|
103
336
|
ctx.addIssue({
|
|
104
337
|
code: z.ZodIssueCode.custom,
|
|
105
|
-
message: `
|
|
338
|
+
message: `Variable "${label}" (type boolean) must be a boolean; got ${gotType}.`,
|
|
106
339
|
path,
|
|
107
340
|
});
|
|
108
341
|
}
|
|
109
|
-
|
|
110
|
-
if (projectConfig.maxVariableArrayStringifiedLength) {
|
|
111
|
-
const stringified = JSON.stringify(variableValue);
|
|
112
|
-
|
|
113
|
-
if (stringified.length > projectConfig.maxVariableArrayStringifiedLength) {
|
|
114
|
-
ctx.addIssue({
|
|
115
|
-
code: z.ZodIssueCode.custom,
|
|
116
|
-
message: `Variable "${variableSchema.key}" array is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableArrayStringifiedLength}`,
|
|
117
|
-
path,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
342
|
return;
|
|
123
343
|
}
|
|
124
344
|
|
|
125
|
-
//
|
|
126
|
-
if (
|
|
127
|
-
if (
|
|
345
|
+
// array — only array allowed; without items schema = array of strings
|
|
346
|
+
if (expectedType === "array") {
|
|
347
|
+
if (!Array.isArray(variableValue)) {
|
|
128
348
|
ctx.addIssue({
|
|
129
349
|
code: z.ZodIssueCode.custom,
|
|
130
|
-
message: `
|
|
350
|
+
message: `Variable "${label}" (type array) must be an array; got ${gotType}.`,
|
|
131
351
|
path,
|
|
132
352
|
});
|
|
353
|
+
return;
|
|
133
354
|
}
|
|
355
|
+
refineVariableValueArray(projectConfig, variableSchema, variableValue, path, ctx, variableKey);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
134
358
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
359
|
+
// object — only plain object allowed (no null, no array)
|
|
360
|
+
if (expectedType === "object") {
|
|
361
|
+
if (
|
|
362
|
+
typeof variableValue !== "object" ||
|
|
363
|
+
variableValue === null ||
|
|
364
|
+
Array.isArray(variableValue)
|
|
365
|
+
) {
|
|
366
|
+
ctx.addIssue({
|
|
367
|
+
code: z.ZodIssueCode.custom,
|
|
368
|
+
message: `Variable "${label}" (type object) must be a plain object; got ${gotType}.`,
|
|
369
|
+
path,
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
145
372
|
}
|
|
146
|
-
|
|
373
|
+
refineVariableValueObject(
|
|
374
|
+
projectConfig,
|
|
375
|
+
variableSchema,
|
|
376
|
+
variableValue as Record<string, unknown>,
|
|
377
|
+
path,
|
|
378
|
+
ctx,
|
|
379
|
+
variableKey,
|
|
380
|
+
);
|
|
147
381
|
return;
|
|
148
382
|
}
|
|
149
383
|
|
|
150
|
-
// json
|
|
151
|
-
if (
|
|
384
|
+
// json — only string containing valid JSON allowed
|
|
385
|
+
if (expectedType === "json") {
|
|
386
|
+
if (typeof variableValue !== "string") {
|
|
387
|
+
ctx.addIssue({
|
|
388
|
+
code: z.ZodIssueCode.custom,
|
|
389
|
+
message: `Variable "${label}" (type json) must be a string (JSON string); got ${gotType}.`,
|
|
390
|
+
path,
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
152
394
|
try {
|
|
153
|
-
JSON.parse(variableValue
|
|
395
|
+
JSON.parse(variableValue);
|
|
154
396
|
|
|
155
397
|
if (projectConfig.maxVariableJSONStringifiedLength) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (stringified.length > projectConfig.maxVariableJSONStringifiedLength) {
|
|
398
|
+
if (variableValue.length > projectConfig.maxVariableJSONStringifiedLength) {
|
|
159
399
|
ctx.addIssue({
|
|
160
400
|
code: z.ZodIssueCode.custom,
|
|
161
|
-
message: `Variable "${
|
|
401
|
+
message: `Variable "${label}" JSON is too long (${variableValue.length} characters), max length is ${projectConfig.maxVariableJSONStringifiedLength}`,
|
|
162
402
|
path,
|
|
163
403
|
});
|
|
164
404
|
}
|
|
@@ -167,13 +407,20 @@ function superRefineVariableValue(
|
|
|
167
407
|
} catch (e) {
|
|
168
408
|
ctx.addIssue({
|
|
169
409
|
code: z.ZodIssueCode.custom,
|
|
170
|
-
message: `
|
|
410
|
+
message: `Variable "${label}" (type json) must be a valid JSON string; parse failed.`,
|
|
171
411
|
path,
|
|
172
412
|
});
|
|
173
413
|
}
|
|
174
414
|
|
|
175
415
|
return;
|
|
176
416
|
}
|
|
417
|
+
|
|
418
|
+
// Unknown variable type — schema is invalid or unsupported
|
|
419
|
+
ctx.addIssue({
|
|
420
|
+
code: z.ZodIssueCode.custom,
|
|
421
|
+
message: `Variable "${label}" has unknown or unsupported type "${String(expectedType)}" in variablesSchema.`,
|
|
422
|
+
path,
|
|
423
|
+
});
|
|
177
424
|
}
|
|
178
425
|
|
|
179
426
|
function refineForce({
|
|
@@ -206,6 +453,7 @@ function refineForce({
|
|
|
206
453
|
f.variables[variableKey],
|
|
207
454
|
pathPrefix.concat([fN, "variables", variableKey]),
|
|
208
455
|
ctx,
|
|
456
|
+
variableKey,
|
|
209
457
|
);
|
|
210
458
|
});
|
|
211
459
|
}
|
|
@@ -231,6 +479,7 @@ function refineRules({
|
|
|
231
479
|
rule.variables[variableKey],
|
|
232
480
|
pathPrefix.concat([ruleN, "variables", variableKey]),
|
|
233
481
|
ctx,
|
|
482
|
+
variableKey,
|
|
234
483
|
);
|
|
235
484
|
});
|
|
236
485
|
}
|
|
@@ -308,21 +557,10 @@ export function getFeatureZodSchema(
|
|
|
308
557
|
availableSegmentKeys: [string, ...string[]],
|
|
309
558
|
availableFeatureKeys: [string, ...string[]],
|
|
310
559
|
) {
|
|
560
|
+
const propertyZodSchema = getPropertyZodSchema();
|
|
561
|
+
const variableValueZodSchema = valueZodSchema;
|
|
562
|
+
|
|
311
563
|
const variationValueZodSchema = z.string().min(1);
|
|
312
|
-
const variableValueZodSchema = z.union([
|
|
313
|
-
z.string(),
|
|
314
|
-
z.number(),
|
|
315
|
-
z.boolean(),
|
|
316
|
-
z.array(z.string()),
|
|
317
|
-
z.record(z.unknown()).refine(
|
|
318
|
-
(value) => {
|
|
319
|
-
return isFlatObject(value);
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
message: "object is not flat",
|
|
323
|
-
},
|
|
324
|
-
),
|
|
325
|
-
]);
|
|
326
564
|
|
|
327
565
|
const plainGroupSegment = z.string().refine(
|
|
328
566
|
(value) => value === "*" || availableSegmentKeys.includes(value),
|
|
@@ -500,13 +738,31 @@ export function getFeatureZodSchema(
|
|
|
500
738
|
z
|
|
501
739
|
.object({
|
|
502
740
|
deprecated: z.boolean().optional(),
|
|
503
|
-
|
|
741
|
+
|
|
742
|
+
type: z.union([z.literal("json"), propertyTypeEnum]),
|
|
743
|
+
// array: when omitted, treated as array of strings
|
|
744
|
+
items: propertyZodSchema.optional(),
|
|
745
|
+
// object: when omitted, treated as flat object (primitive values only)
|
|
746
|
+
properties: z.record(propertyZodSchema).optional(),
|
|
747
|
+
// object: optional list of required property names
|
|
748
|
+
required: z.array(z.string()).optional(),
|
|
749
|
+
|
|
504
750
|
description: z.string().optional(),
|
|
751
|
+
|
|
505
752
|
defaultValue: variableValueZodSchema,
|
|
506
|
-
useDefaultWhenDisabled: z.boolean().optional(),
|
|
507
753
|
disabledValue: variableValueZodSchema.optional(),
|
|
754
|
+
|
|
755
|
+
useDefaultWhenDisabled: z.boolean().optional(),
|
|
508
756
|
})
|
|
509
|
-
.strict()
|
|
757
|
+
.strict()
|
|
758
|
+
.superRefine((variableSchema, ctx) => {
|
|
759
|
+
// Validate required ⊆ properties at this level and in all nested object schemas
|
|
760
|
+
refineRequiredKeysInSchema(
|
|
761
|
+
variableSchema as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
762
|
+
[],
|
|
763
|
+
ctx,
|
|
764
|
+
);
|
|
765
|
+
}),
|
|
510
766
|
)
|
|
511
767
|
.optional(),
|
|
512
768
|
|
|
@@ -625,16 +881,20 @@ export function getFeatureZodSchema(
|
|
|
625
881
|
variableSchema.defaultValue,
|
|
626
882
|
["variablesSchema", variableKey, "defaultValue"],
|
|
627
883
|
ctx,
|
|
884
|
+
variableKey,
|
|
628
885
|
);
|
|
629
886
|
|
|
630
|
-
// disabledValue
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
887
|
+
// disabledValue (only when present)
|
|
888
|
+
if (variableSchema.disabledValue !== undefined) {
|
|
889
|
+
superRefineVariableValue(
|
|
890
|
+
projectConfig,
|
|
891
|
+
variableSchema,
|
|
892
|
+
variableSchema.disabledValue,
|
|
893
|
+
["variablesSchema", variableKey, "disabledValue"],
|
|
894
|
+
ctx,
|
|
895
|
+
variableKey,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
638
898
|
});
|
|
639
899
|
|
|
640
900
|
// variations
|
|
@@ -654,6 +914,7 @@ export function getFeatureZodSchema(
|
|
|
654
914
|
variableValue,
|
|
655
915
|
["variations", variationN, "variables", variableKey],
|
|
656
916
|
ctx,
|
|
917
|
+
variableKey,
|
|
657
918
|
);
|
|
658
919
|
|
|
659
920
|
// variations[n].variableOverrides[n].value
|
|
@@ -676,6 +937,7 @@ export function getFeatureZodSchema(
|
|
|
676
937
|
"value",
|
|
677
938
|
],
|
|
678
939
|
ctx,
|
|
940
|
+
variableKey,
|
|
679
941
|
);
|
|
680
942
|
});
|
|
681
943
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PropertySchema, Value } from "@featurevisor/types";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// Recursive schema for Value: boolean | string | number | ObjectValue | Value[]
|
|
5
|
+
export const valueZodSchema: z.ZodType<Value> = z.lazy(() =>
|
|
6
|
+
z.union([
|
|
7
|
+
z.boolean(),
|
|
8
|
+
z.string(),
|
|
9
|
+
z.number(),
|
|
10
|
+
// | Date // @TODO: support in future
|
|
11
|
+
z.record(z.string(), valueZodSchema),
|
|
12
|
+
z.array(valueZodSchema),
|
|
13
|
+
]),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// @TODO: support "date" in future
|
|
17
|
+
// @TODO: consider "semver" in future
|
|
18
|
+
// @TODO: consider "url" in future
|
|
19
|
+
export const propertyTypeEnum = z.enum([
|
|
20
|
+
"boolean",
|
|
21
|
+
"string",
|
|
22
|
+
"integer",
|
|
23
|
+
"double",
|
|
24
|
+
"object",
|
|
25
|
+
"array",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function getPropertyZodSchema() {
|
|
29
|
+
const propertyZodSchema: z.ZodType<PropertySchema> = z.lazy(() =>
|
|
30
|
+
z
|
|
31
|
+
.object({
|
|
32
|
+
description: z.string().optional(),
|
|
33
|
+
type: propertyTypeEnum.optional(),
|
|
34
|
+
// enum?: Value[]; const?: Value;
|
|
35
|
+
// Numeric: maximum?, minimum?
|
|
36
|
+
// String: maxLength?, minLength?, pattern?
|
|
37
|
+
items: propertyZodSchema.optional(),
|
|
38
|
+
// maxItems?, minItems?, uniqueItems?
|
|
39
|
+
required: z.array(z.string()).optional(),
|
|
40
|
+
properties: z.record(z.string(), propertyZodSchema).optional(),
|
|
41
|
+
// Annotations: default?: Value; examples?: Value[];
|
|
42
|
+
})
|
|
43
|
+
.strict(),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return propertyZodSchema;
|
|
47
|
+
}
|