@messagevisor/core 0.0.1 → 0.1.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.
Files changed (211) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +7 -0
  4. package/jest.config.js +8 -0
  5. package/lib/benchmark/index.d.ts +2 -0
  6. package/lib/benchmark/index.js +417 -0
  7. package/lib/benchmark/index.js.map +1 -0
  8. package/lib/builder/index.d.ts +70 -0
  9. package/lib/builder/index.js +831 -0
  10. package/lib/builder/index.js.map +1 -0
  11. package/lib/cli/index.d.ts +28 -0
  12. package/lib/cli/index.js +182 -0
  13. package/lib/cli/index.js.map +1 -0
  14. package/lib/config/index.d.ts +61 -0
  15. package/lib/config/index.js +255 -0
  16. package/lib/config/index.js.map +1 -0
  17. package/lib/create/index.d.ts +2 -0
  18. package/lib/create/index.js +405 -0
  19. package/lib/create/index.js.map +1 -0
  20. package/lib/datasource/filesystemAdapter.d.ts +44 -0
  21. package/lib/datasource/filesystemAdapter.js +424 -0
  22. package/lib/datasource/filesystemAdapter.js.map +1 -0
  23. package/lib/datasource/index.d.ts +39 -0
  24. package/lib/datasource/index.js +96 -0
  25. package/lib/datasource/index.js.map +1 -0
  26. package/lib/error.d.ts +6 -0
  27. package/lib/error.js +49 -0
  28. package/lib/error.js.map +1 -0
  29. package/lib/evaluate/cli.d.ts +8 -0
  30. package/lib/evaluate/cli.js +179 -0
  31. package/lib/evaluate/cli.js.map +1 -0
  32. package/lib/evaluate/index.d.ts +10 -0
  33. package/lib/evaluate/index.js +131 -0
  34. package/lib/evaluate/index.js.map +1 -0
  35. package/lib/examples/coerceExampleIsoDates.d.ts +12 -0
  36. package/lib/examples/coerceExampleIsoDates.js +81 -0
  37. package/lib/examples/coerceExampleIsoDates.js.map +1 -0
  38. package/lib/examples/index.d.ts +63 -0
  39. package/lib/examples/index.js +713 -0
  40. package/lib/examples/index.js.map +1 -0
  41. package/lib/exporter/index.d.ts +60 -0
  42. package/lib/exporter/index.js +610 -0
  43. package/lib/exporter/index.js.map +1 -0
  44. package/lib/find-duplicates/index.d.ts +41 -0
  45. package/lib/find-duplicates/index.js +297 -0
  46. package/lib/find-duplicates/index.js.map +1 -0
  47. package/lib/generate-code/index.d.ts +11 -0
  48. package/lib/generate-code/index.js +157 -0
  49. package/lib/generate-code/index.js.map +1 -0
  50. package/lib/generate-code/typescript.d.ts +14 -0
  51. package/lib/generate-code/typescript.js +307 -0
  52. package/lib/generate-code/typescript.js.map +1 -0
  53. package/lib/importer/index.d.ts +64 -0
  54. package/lib/importer/index.js +1092 -0
  55. package/lib/importer/index.js.map +1 -0
  56. package/lib/index.d.ts +18 -0
  57. package/lib/index.js +35 -0
  58. package/lib/index.js.map +1 -0
  59. package/lib/info/index.d.ts +17 -0
  60. package/lib/info/index.js +132 -0
  61. package/lib/info/index.js.map +1 -0
  62. package/lib/init/index.d.ts +30 -0
  63. package/lib/init/index.js +348 -0
  64. package/lib/init/index.js.map +1 -0
  65. package/lib/lint/index.d.ts +1 -0
  66. package/lib/lint/index.js +6 -0
  67. package/lib/lint/index.js.map +1 -0
  68. package/lib/linter/attributeSchema.d.ts +7 -0
  69. package/lib/linter/attributeSchema.js +36 -0
  70. package/lib/linter/attributeSchema.js.map +1 -0
  71. package/lib/linter/checkLocaleCircularDependency.d.ts +7 -0
  72. package/lib/linter/checkLocaleCircularDependency.js +42 -0
  73. package/lib/linter/checkLocaleCircularDependency.js.map +1 -0
  74. package/lib/linter/conditionSchema.d.ts +3 -0
  75. package/lib/linter/conditionSchema.js +283 -0
  76. package/lib/linter/conditionSchema.js.map +1 -0
  77. package/lib/linter/formatSchema.d.ts +325 -0
  78. package/lib/linter/formatSchema.js +165 -0
  79. package/lib/linter/formatSchema.js.map +1 -0
  80. package/lib/linter/icuStyleLint.d.ts +6 -0
  81. package/lib/linter/icuStyleLint.js +226 -0
  82. package/lib/linter/icuStyleLint.js.map +1 -0
  83. package/lib/linter/index.d.ts +34 -0
  84. package/lib/linter/index.js +557 -0
  85. package/lib/linter/index.js.map +1 -0
  86. package/lib/linter/localeSchema.d.ts +672 -0
  87. package/lib/linter/localeSchema.js +50 -0
  88. package/lib/linter/localeSchema.js.map +1 -0
  89. package/lib/linter/messageSchema.d.ts +35 -0
  90. package/lib/linter/messageSchema.js +115 -0
  91. package/lib/linter/messageSchema.js.map +1 -0
  92. package/lib/linter/printError.d.ts +8 -0
  93. package/lib/linter/printError.js +41 -0
  94. package/lib/linter/printError.js.map +1 -0
  95. package/lib/linter/schema.d.ts +33 -0
  96. package/lib/linter/schema.js +192 -0
  97. package/lib/linter/schema.js.map +1 -0
  98. package/lib/linter/segmentSchema.d.ts +8 -0
  99. package/lib/linter/segmentSchema.js +18 -0
  100. package/lib/linter/segmentSchema.js.map +1 -0
  101. package/lib/linter/targetSchema.d.ts +337 -0
  102. package/lib/linter/targetSchema.js +39 -0
  103. package/lib/linter/targetSchema.js.map +1 -0
  104. package/lib/linter/testSchema.d.ts +71 -0
  105. package/lib/linter/testSchema.js +165 -0
  106. package/lib/linter/testSchema.js.map +1 -0
  107. package/lib/linter/zodHelpers.d.ts +2 -0
  108. package/lib/linter/zodHelpers.js +15 -0
  109. package/lib/linter/zodHelpers.js.map +1 -0
  110. package/lib/list/index.d.ts +8 -0
  111. package/lib/list/index.js +524 -0
  112. package/lib/list/index.js.map +1 -0
  113. package/lib/matrix.d.ts +4 -0
  114. package/lib/matrix.js +66 -0
  115. package/lib/matrix.js.map +1 -0
  116. package/lib/promoter/index.d.ts +65 -0
  117. package/lib/promoter/index.js +1208 -0
  118. package/lib/promoter/index.js.map +1 -0
  119. package/lib/prune/index.d.ts +37 -0
  120. package/lib/prune/index.js +673 -0
  121. package/lib/prune/index.js.map +1 -0
  122. package/lib/sets.d.ts +10 -0
  123. package/lib/sets.js +120 -0
  124. package/lib/sets.js.map +1 -0
  125. package/lib/tester/cliFormat.d.ts +8 -0
  126. package/lib/tester/cliFormat.js +15 -0
  127. package/lib/tester/cliFormat.js.map +1 -0
  128. package/lib/tester/index.d.ts +35 -0
  129. package/lib/tester/index.js +713 -0
  130. package/lib/tester/index.js.map +1 -0
  131. package/lib/tester/matrix.d.ts +14 -0
  132. package/lib/tester/matrix.js +76 -0
  133. package/lib/tester/matrix.js.map +1 -0
  134. package/lib/tester/prettyDuration.d.ts +1 -0
  135. package/lib/tester/prettyDuration.js +30 -0
  136. package/lib/tester/prettyDuration.js.map +1 -0
  137. package/lib/tester/printTestResult.d.ts +2 -0
  138. package/lib/tester/printTestResult.js +32 -0
  139. package/lib/tester/printTestResult.js.map +1 -0
  140. package/lib/tester/types.d.ts +29 -0
  141. package/lib/tester/types.js +3 -0
  142. package/lib/tester/types.js.map +1 -0
  143. package/package.json +41 -13
  144. package/src/benchmark/index.spec.ts +375 -0
  145. package/src/benchmark/index.ts +433 -0
  146. package/src/builder/index.spec.ts +822 -0
  147. package/src/builder/index.ts +920 -0
  148. package/src/cli/index.spec.ts +54 -0
  149. package/src/cli/index.ts +150 -0
  150. package/src/config/index.spec.ts +70 -0
  151. package/src/config/index.ts +259 -0
  152. package/src/create/index.spec.ts +272 -0
  153. package/src/create/index.ts +295 -0
  154. package/src/datasource/filesystemAdapter.ts +313 -0
  155. package/src/datasource/index.ts +135 -0
  156. package/src/error.ts +33 -0
  157. package/src/evaluate/cli.spec.ts +368 -0
  158. package/src/evaluate/cli.ts +130 -0
  159. package/src/evaluate/index.ts +161 -0
  160. package/src/examples/coerceExampleIsoDates.spec.ts +81 -0
  161. package/src/examples/coerceExampleIsoDates.ts +98 -0
  162. package/src/examples/index.spec.ts +453 -0
  163. package/src/examples/index.ts +854 -0
  164. package/src/exporter/index.spec.ts +443 -0
  165. package/src/exporter/index.ts +643 -0
  166. package/src/find-duplicates/index.spec.ts +289 -0
  167. package/src/find-duplicates/index.ts +314 -0
  168. package/src/generate-code/index.ts +92 -0
  169. package/src/generate-code/typescript.spec.ts +241 -0
  170. package/src/generate-code/typescript.ts +284 -0
  171. package/src/importer/index.spec.ts +1101 -0
  172. package/src/importer/index.ts +1190 -0
  173. package/src/index.ts +18 -0
  174. package/src/info/index.ts +67 -0
  175. package/src/init/index.spec.ts +279 -0
  176. package/src/init/index.ts +292 -0
  177. package/src/lint/index.ts +1 -0
  178. package/src/linter/attributeSchema.ts +38 -0
  179. package/src/linter/checkLocaleCircularDependency.ts +51 -0
  180. package/src/linter/conditionSchema.ts +386 -0
  181. package/src/linter/formatSchema.ts +170 -0
  182. package/src/linter/icuStyleLint.ts +312 -0
  183. package/src/linter/index.spec.ts +824 -0
  184. package/src/linter/index.ts +460 -0
  185. package/src/linter/localeSchema.ts +70 -0
  186. package/src/linter/messageSchema.ts +152 -0
  187. package/src/linter/printError.ts +52 -0
  188. package/src/linter/schema.ts +230 -0
  189. package/src/linter/segmentSchema.ts +15 -0
  190. package/src/linter/targetSchema.ts +50 -0
  191. package/src/linter/testSchema.spec.ts +405 -0
  192. package/src/linter/testSchema.ts +239 -0
  193. package/src/linter/zodHelpers.ts +16 -0
  194. package/src/list/index.spec.ts +431 -0
  195. package/src/list/index.ts +463 -0
  196. package/src/matrix.ts +69 -0
  197. package/src/promoter/index.spec.ts +584 -0
  198. package/src/promoter/index.ts +1267 -0
  199. package/src/prune/index.spec.ts +418 -0
  200. package/src/prune/index.ts +693 -0
  201. package/src/sets.ts +74 -0
  202. package/src/tester/cliFormat.ts +11 -0
  203. package/src/tester/featurevisorIntegration.spec.ts +101 -0
  204. package/src/tester/index.spec.ts +577 -0
  205. package/src/tester/index.ts +679 -0
  206. package/src/tester/matrix.ts +106 -0
  207. package/src/tester/prettyDuration.ts +34 -0
  208. package/src/tester/printTestResult.ts +40 -0
  209. package/src/tester/types.ts +32 -0
  210. package/tsconfig.cjs.json +11 -0
  211. package/tsconfig.typecheck.json +4 -0
@@ -0,0 +1,51 @@
1
+ import type { Locale } from "@messagevisor/types";
2
+
3
+ export type LocaleInheritanceField =
4
+ | "inheritFormatsFrom"
5
+ | "inheritTranslationsFrom"
6
+ | "mergeExamplesFrom";
7
+
8
+ export interface LocaleCircularDependency {
9
+ field: LocaleInheritanceField;
10
+ cycle: string[];
11
+ }
12
+
13
+ export function checkLocaleCircularDependency(
14
+ localesByKey: Record<string, Locale>,
15
+ field: LocaleInheritanceField,
16
+ ): LocaleCircularDependency[] {
17
+ const cycles: LocaleCircularDependency[] = [];
18
+ const reportedCycles = new Set<string>();
19
+
20
+ for (const localeKey of Object.keys(localesByKey)) {
21
+ const path: string[] = [];
22
+ const seenInPath = new Map<string, number>();
23
+ let currentKey: string | undefined = localeKey;
24
+
25
+ while (currentKey) {
26
+ if (seenInPath.has(currentKey)) {
27
+ const cycle = [...path.slice(seenInPath.get(currentKey)), currentKey];
28
+ const normalizedKey = cycle.slice(0, -1).sort().join(">");
29
+
30
+ if (!reportedCycles.has(normalizedKey)) {
31
+ reportedCycles.add(normalizedKey);
32
+ cycles.push({ field, cycle });
33
+ }
34
+
35
+ break;
36
+ }
37
+
38
+ const locale = localesByKey[currentKey];
39
+
40
+ if (!locale) {
41
+ break;
42
+ }
43
+
44
+ seenInPath.set(currentKey, path.length);
45
+ path.push(currentKey);
46
+ currentKey = locale[field];
47
+ }
48
+ }
49
+
50
+ return cycles;
51
+ }
@@ -0,0 +1,386 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import type { Attribute } from "@messagevisor/types";
3
+ import { z } from "zod";
4
+
5
+ import { refineWithMessage } from "./zodHelpers";
6
+
7
+ const commonOperators = ["equals", "notEquals"];
8
+ const numericOperators = ["greaterThan", "greaterThanOrEquals", "lessThan", "lessThanOrEquals"];
9
+ const stringOperators = ["contains", "notContains", "startsWith", "endsWith"];
10
+ const dateOperators = ["before", "after"];
11
+ const arrayOperators = ["includes", "notIncludes"];
12
+ const membershipOperators = ["in", "notIn"];
13
+ const operatorsWithoutValue = ["exists", "notExists"];
14
+ const featureOperators = ["isEnabled", "isDisabled"];
15
+ const experimentOperators = ["hasVariation"];
16
+
17
+ type SchemaNode = Attribute & {
18
+ type?: string;
19
+ properties?: Record<string, SchemaNode>;
20
+ additionalProperties?: SchemaNode;
21
+ oneOf?: SchemaNode[];
22
+ };
23
+ type ResolvedLeaf = { kind: "schema"; schema: SchemaNode } | { kind: "flat-object-property" };
24
+
25
+ function isPrimitiveValue(value: unknown): boolean {
26
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
27
+ }
28
+
29
+ function valuesEqual(left: unknown, right: unknown): boolean {
30
+ return JSON.stringify(left) === JSON.stringify(right);
31
+ }
32
+
33
+ function resolveAttributePath(
34
+ attributePath: string,
35
+ attributesByKey: Record<string, Attribute>,
36
+ ): ResolvedLeaf | null {
37
+ const [rootKey, ...rest] = attributePath.split(".");
38
+ const rootAttribute = attributesByKey[rootKey];
39
+
40
+ if (!rootAttribute) {
41
+ return null;
42
+ }
43
+
44
+ let current: SchemaNode = rootAttribute;
45
+
46
+ if (rest.length === 0) {
47
+ return { kind: "schema", schema: current };
48
+ }
49
+
50
+ for (let index = 0; index < rest.length; index++) {
51
+ const segment = rest[index];
52
+
53
+ if (current.type !== "object") {
54
+ return null;
55
+ }
56
+
57
+ if (current.properties && current.properties[segment]) {
58
+ current = current.properties[segment];
59
+ continue;
60
+ }
61
+
62
+ if (!current.properties && !current.additionalProperties) {
63
+ return index === rest.length - 1 ? { kind: "flat-object-property" } : null;
64
+ }
65
+
66
+ if (current.additionalProperties) {
67
+ current = current.additionalProperties;
68
+ continue;
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ return { kind: "schema", schema: current };
75
+ }
76
+
77
+ function getLeafTypes(leaf: ResolvedLeaf): string[] {
78
+ if (leaf.kind === "flat-object-property") {
79
+ return ["primitive"];
80
+ }
81
+
82
+ if (leaf.schema.oneOf) {
83
+ return Array.from(
84
+ new Set(
85
+ leaf.schema.oneOf.flatMap((branch) =>
86
+ getLeafTypes({ kind: "schema", schema: branch as SchemaNode }),
87
+ ),
88
+ ),
89
+ );
90
+ }
91
+
92
+ return leaf.schema.type ? [leaf.schema.type] : [];
93
+ }
94
+
95
+ function matchesSchemaValue(schema: SchemaNode, value: unknown): boolean {
96
+ if (schema.oneOf) {
97
+ return (
98
+ schema.oneOf.filter((branch) => matchesSchemaValue(branch as SchemaNode, value)).length === 1
99
+ );
100
+ }
101
+
102
+ if (schema.const !== undefined) {
103
+ return valuesEqual(schema.const, value);
104
+ }
105
+
106
+ if (schema.enum) {
107
+ return schema.enum.some((entry) => valuesEqual(entry, value));
108
+ }
109
+
110
+ if (!schema.type) {
111
+ return true;
112
+ }
113
+
114
+ if (schema.type === "string" || schema.type === "date") {
115
+ if (typeof value !== "string" && !(value instanceof Date)) return false;
116
+ const stringValue = value instanceof Date ? value.toISOString() : value;
117
+ if (schema.minLength !== undefined && stringValue.length < schema.minLength) return false;
118
+ if (schema.maxLength !== undefined && stringValue.length > schema.maxLength) return false;
119
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(stringValue)) return false;
120
+ return true;
121
+ }
122
+
123
+ if (schema.type === "boolean") {
124
+ return typeof value === "boolean";
125
+ }
126
+
127
+ if (schema.type === "integer" || schema.type === "double") {
128
+ if (typeof value !== "number") return false;
129
+ if (schema.type === "integer" && !Number.isInteger(value)) return false;
130
+ if (schema.minimum !== undefined && value < schema.minimum) return false;
131
+ if (schema.maximum !== undefined && value > schema.maximum) return false;
132
+ return true;
133
+ }
134
+
135
+ if (schema.type === "array") {
136
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
137
+ }
138
+
139
+ if (schema.type === "object") {
140
+ return typeof value === "object" && value !== null && !Array.isArray(value);
141
+ }
142
+
143
+ return true;
144
+ }
145
+
146
+ function matchesLeafValue(leaf: ResolvedLeaf, value: unknown): boolean {
147
+ if (leaf.kind === "flat-object-property") {
148
+ return isPrimitiveValue(value);
149
+ }
150
+
151
+ return matchesSchemaValue(leaf.schema, value);
152
+ }
153
+
154
+ function addIssue(ctx: z.RefinementCtx, message: string, path: (string | number)[] = ["value"]) {
155
+ ctx.addIssue({
156
+ code: z.ZodIssueCode.custom,
157
+ message,
158
+ path,
159
+ });
160
+ }
161
+
162
+ function validateAttributeAwareCondition(
163
+ data: { attribute: string; operator: string; value?: unknown },
164
+ ctx: z.RefinementCtx,
165
+ attributesByKey: Record<string, Attribute>,
166
+ ) {
167
+ const leaf = resolveAttributePath(data.attribute, attributesByKey);
168
+
169
+ if (!leaf) {
170
+ return;
171
+ }
172
+
173
+ const leafTypes = getLeafTypes(leaf);
174
+
175
+ if (operatorsWithoutValue.includes(data.operator)) {
176
+ return;
177
+ }
178
+
179
+ if (leaf.kind === "schema" && leaf.schema.type === "object") {
180
+ addIssue(
181
+ ctx,
182
+ `Attribute "${data.attribute}" resolves to an object. Use a nested attribute path or \`exists\`/\`notExists\`.`,
183
+ );
184
+ return;
185
+ }
186
+
187
+ if (
188
+ numericOperators.includes(data.operator) &&
189
+ !leafTypes.some((type) => ["integer", "double"].includes(type))
190
+ ) {
191
+ addIssue(
192
+ ctx,
193
+ `Operator "${data.operator}" can only be used with integer or double attributes.`,
194
+ );
195
+ return;
196
+ }
197
+
198
+ if (
199
+ stringOperators.includes(data.operator) &&
200
+ !leafTypes.some((type) => ["string", "date"].includes(type))
201
+ ) {
202
+ addIssue(ctx, `Operator "${data.operator}" can only be used with string or date attributes.`);
203
+ return;
204
+ }
205
+
206
+ if (
207
+ dateOperators.includes(data.operator) &&
208
+ !leafTypes.some((type) => ["string", "date"].includes(type))
209
+ ) {
210
+ addIssue(ctx, `Operator "${data.operator}" can only be used with string or date attributes.`);
211
+ return;
212
+ }
213
+
214
+ if (arrayOperators.includes(data.operator)) {
215
+ if (!leafTypes.includes("array")) {
216
+ addIssue(ctx, `Operator "${data.operator}" can only be used with array attributes.`);
217
+ return;
218
+ }
219
+
220
+ if (typeof data.value !== "string") {
221
+ addIssue(ctx, `Operator "${data.operator}" only supports string values.`);
222
+ }
223
+
224
+ return;
225
+ }
226
+
227
+ if (membershipOperators.includes(data.operator)) {
228
+ if (!Array.isArray(data.value)) {
229
+ return;
230
+ }
231
+
232
+ data.value.forEach((entry, index) => {
233
+ if (!matchesLeafValue(leaf, entry)) {
234
+ addIssue(
235
+ ctx,
236
+ `Value at index ${index} does not match the schema of attribute "${data.attribute}".`,
237
+ ["value", index],
238
+ );
239
+ }
240
+ });
241
+
242
+ return;
243
+ }
244
+
245
+ if (
246
+ commonOperators.includes(data.operator) &&
247
+ data.value !== null &&
248
+ !matchesLeafValue(leaf, data.value)
249
+ ) {
250
+ addIssue(ctx, `Value does not match the schema of attribute "${data.attribute}".`);
251
+ }
252
+ }
253
+
254
+ export function getConditionsZodSchema(attributesByKey: Record<string, Attribute>) {
255
+ type ConditionInput = any;
256
+
257
+ const conditionZodSchema: z.ZodType<ConditionInput> = z.lazy(() => {
258
+ const attributeCondition = z
259
+ .object({
260
+ attribute: refineWithMessage(
261
+ z.string(),
262
+ (value) => resolveAttributePath(value, attributesByKey) !== null,
263
+ (value) => `Unknown attribute "${value}"`,
264
+ ),
265
+ operator: z.enum([
266
+ ...commonOperators,
267
+ ...numericOperators,
268
+ ...stringOperators,
269
+ ...dateOperators,
270
+ ...arrayOperators,
271
+ ...membershipOperators,
272
+ ...operatorsWithoutValue,
273
+ ]),
274
+ value: z
275
+ .union([
276
+ z.string(),
277
+ z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])),
278
+ z.number(),
279
+ z.boolean(),
280
+ z.date(),
281
+ z.null(),
282
+ ])
283
+ .optional(),
284
+ regexFlags: z.never().optional(),
285
+ })
286
+ .strict()
287
+ .superRefine((data, ctx) => {
288
+ if (operatorsWithoutValue.includes(data.operator) && typeof data.value !== "undefined") {
289
+ addIssue(ctx, `when operator is "${data.operator}", value must not be provided`);
290
+ }
291
+
292
+ if (!operatorsWithoutValue.includes(data.operator) && typeof data.value === "undefined") {
293
+ addIssue(ctx, `when operator is "${data.operator}", value must be provided`);
294
+ }
295
+
296
+ if (numericOperators.includes(data.operator) && typeof data.value !== "number") {
297
+ addIssue(ctx, `when operator is "${data.operator}", value must be a number`);
298
+ }
299
+
300
+ if (
301
+ [...stringOperators, ...dateOperators, ...arrayOperators].includes(data.operator) &&
302
+ typeof data.value !== "string"
303
+ ) {
304
+ addIssue(ctx, `when operator is "${data.operator}", value must be a string`);
305
+ }
306
+
307
+ if (membershipOperators.includes(data.operator) && !Array.isArray(data.value)) {
308
+ addIssue(ctx, `when operator is "${data.operator}", value must be an array`);
309
+ }
310
+
311
+ validateAttributeAwareCondition(data, ctx, attributesByKey);
312
+ });
313
+
314
+ const featureCondition = z
315
+ .object({
316
+ feature: z.string(),
317
+ operator: z.string(),
318
+ attribute: z.never().optional(),
319
+ experiment: z.never().optional(),
320
+ value: z.unknown().optional(),
321
+ regexFlags: z.never().optional(),
322
+ })
323
+ .strict()
324
+ .superRefine((data, ctx) => {
325
+ if (!featureOperators.includes(data.operator)) {
326
+ addIssue(ctx, `Feature conditions only support operators "isEnabled" and "isDisabled".`, [
327
+ "operator",
328
+ ]);
329
+ }
330
+
331
+ if (typeof data.value !== "undefined") {
332
+ addIssue(
333
+ ctx,
334
+ `Feature conditions must not define \`value\`; the flag state comes from resolveFlag.`,
335
+ ["value"],
336
+ );
337
+ }
338
+ });
339
+
340
+ const experimentCondition = z
341
+ .object({
342
+ experiment: z.string(),
343
+ operator: z.string(),
344
+ value: z.unknown().optional(),
345
+ attribute: z.never().optional(),
346
+ feature: z.never().optional(),
347
+ regexFlags: z.never().optional(),
348
+ })
349
+ .strict()
350
+ .superRefine((data, ctx) => {
351
+ if (data.operator !== "hasVariation") {
352
+ addIssue(ctx, `Experiment conditions only support operator "hasVariation".`, [
353
+ "operator",
354
+ ]);
355
+ }
356
+
357
+ if (typeof data.value === "undefined") {
358
+ addIssue(
359
+ ctx,
360
+ `Experiment conditions must define \`value\` with the expected variation.`,
361
+ ["value"],
362
+ );
363
+ return;
364
+ }
365
+
366
+ if (typeof data.value !== "string") {
367
+ addIssue(ctx, `Experiment condition \`value\` must be a string variation.`, ["value"]);
368
+ }
369
+ });
370
+
371
+ const andCondition = z.object({ and: z.array(conditionZodSchema).min(1) }).strict();
372
+ const orCondition = z.object({ or: z.array(conditionZodSchema).min(1) }).strict();
373
+ const notCondition = z.object({ not: z.array(conditionZodSchema).min(1) }).strict();
374
+
375
+ return z.union([
376
+ attributeCondition,
377
+ featureCondition,
378
+ experimentCondition,
379
+ andCondition,
380
+ orCondition,
381
+ notCondition,
382
+ ]);
383
+ });
384
+
385
+ return z.union([z.literal("*"), conditionZodSchema, z.array(conditionZodSchema).min(1)]);
386
+ }
@@ -0,0 +1,170 @@
1
+ import { z } from "zod";
2
+
3
+ const numberShared = {
4
+ useGrouping: z.union([z.boolean(), z.enum(["min2", "auto", "always"])]).optional(),
5
+ minimumIntegerDigits: z.number().int().nonnegative().optional(),
6
+ minimumFractionDigits: z.number().int().nonnegative().optional(),
7
+ maximumFractionDigits: z.number().int().nonnegative().optional(),
8
+ minimumSignificantDigits: z.number().int().nonnegative().optional(),
9
+ maximumSignificantDigits: z.number().int().nonnegative().optional(),
10
+ notation: z.enum(["standard", "scientific", "engineering", "compact"]).optional(),
11
+ compactDisplay: z.enum(["short", "long"]).optional(),
12
+ signDisplay: z.enum(["auto", "never", "always", "exceptZero", "negative"]).optional(),
13
+ roundingPriority: z.enum(["auto", "morePrecision", "lessPrecision"]).optional(),
14
+ roundingIncrement: z
15
+ .union([
16
+ z.literal(1),
17
+ z.literal(2),
18
+ z.literal(5),
19
+ z.literal(10),
20
+ z.literal(20),
21
+ z.literal(25),
22
+ z.literal(50),
23
+ z.literal(100),
24
+ z.literal(200),
25
+ z.literal(250),
26
+ z.literal(500),
27
+ z.literal(1000),
28
+ z.literal(2000),
29
+ z.literal(2500),
30
+ z.literal(5000),
31
+ ])
32
+ .optional(),
33
+ roundingMode: z
34
+ .enum([
35
+ "ceil",
36
+ "floor",
37
+ "expand",
38
+ "trunc",
39
+ "halfCeil",
40
+ "halfFloor",
41
+ "halfExpand",
42
+ "halfTrunc",
43
+ "halfEven",
44
+ ])
45
+ .optional(),
46
+ trailingZeroDisplay: z.enum(["auto", "stripIfInteger"]).optional(),
47
+ numberingSystem: z.string().optional(),
48
+ };
49
+
50
+ const numberPresetZodSchema = z
51
+ .object({
52
+ ...numberShared,
53
+ style: z.enum(["decimal", "currency", "percent", "unit"]).optional(),
54
+ currency: z.string().optional(),
55
+ currencyDisplay: z.enum(["code", "symbol", "narrowSymbol", "name"]).optional(),
56
+ currencySign: z.enum(["standard", "accounting"]).optional(),
57
+ unit: z.string().optional(),
58
+ unitDisplay: z.enum(["short", "narrow", "long"]).optional(),
59
+ })
60
+ .strict()
61
+ .superRefine((data, ctx) => {
62
+ if (data.style !== "currency" && (data.currency || data.currencyDisplay || data.currencySign)) {
63
+ ctx.addIssue({
64
+ code: z.ZodIssueCode.custom,
65
+ message: `Currency options can only be used when \`style\` is "currency".`,
66
+ path: ["style"],
67
+ });
68
+ }
69
+
70
+ if (data.style === "unit" && !data.unit) {
71
+ ctx.addIssue({
72
+ code: z.ZodIssueCode.custom,
73
+ message: `Unit number formats must define \`unit\`.`,
74
+ path: ["unit"],
75
+ });
76
+ }
77
+
78
+ if (data.style !== "unit" && (data.unit || data.unitDisplay)) {
79
+ ctx.addIssue({
80
+ code: z.ZodIssueCode.custom,
81
+ message: `Unit options can only be used when \`style\` is "unit".`,
82
+ path: ["style"],
83
+ });
84
+ }
85
+
86
+ if (data.notation !== "compact" && data.compactDisplay) {
87
+ ctx.addIssue({
88
+ code: z.ZodIssueCode.custom,
89
+ message: `\`compactDisplay\` can only be used when \`notation\` is "compact".`,
90
+ path: ["notation"],
91
+ });
92
+ }
93
+ });
94
+
95
+ const dateTimePresetZodSchema = z
96
+ .object({
97
+ timeZone: z.string().optional(),
98
+ calendar: z.string().optional(),
99
+ numberingSystem: z.string().optional(),
100
+ hour12: z.boolean().optional(),
101
+ hourCycle: z.enum(["h11", "h12", "h23", "h24"]).optional(),
102
+ dateStyle: z.enum(["full", "long", "medium", "short"]).optional(),
103
+ timeStyle: z.enum(["full", "long", "medium", "short"]).optional(),
104
+ formatMatcher: z.enum(["basic", "best fit"]).optional(),
105
+ fractionalSecondDigits: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(),
106
+ weekday: z.enum(["long", "short", "narrow"]).optional(),
107
+ era: z.enum(["long", "short", "narrow"]).optional(),
108
+ year: z.enum(["numeric", "2-digit"]).optional(),
109
+ month: z.enum(["numeric", "2-digit", "long", "short", "narrow"]).optional(),
110
+ day: z.enum(["numeric", "2-digit"]).optional(),
111
+ dayPeriod: z.enum(["long", "short", "narrow"]).optional(),
112
+ hour: z.enum(["numeric", "2-digit"]).optional(),
113
+ minute: z.enum(["numeric", "2-digit"]).optional(),
114
+ second: z.enum(["numeric", "2-digit"]).optional(),
115
+ timeZoneName: z
116
+ .enum(["long", "short", "shortOffset", "longOffset", "shortGeneric", "longGeneric"])
117
+ .optional(),
118
+ })
119
+ .strict()
120
+ .superRefine((data, ctx) => {
121
+ const hasStyleShortcut =
122
+ typeof data.dateStyle !== "undefined" || typeof data.timeStyle !== "undefined";
123
+ const hasGranularFields = [
124
+ "weekday",
125
+ "era",
126
+ "year",
127
+ "month",
128
+ "day",
129
+ "dayPeriod",
130
+ "hour",
131
+ "minute",
132
+ "second",
133
+ "fractionalSecondDigits",
134
+ "timeZoneName",
135
+ ].some((field) => typeof data[field as keyof typeof data] !== "undefined");
136
+
137
+ if (hasStyleShortcut && hasGranularFields) {
138
+ ctx.addIssue({
139
+ code: z.ZodIssueCode.custom,
140
+ message:
141
+ "`dateStyle` / `timeStyle` cannot be combined with granular date/time component fields.",
142
+ path: ["dateStyle"],
143
+ });
144
+ }
145
+
146
+ if (hasStyleShortcut && typeof data.formatMatcher !== "undefined") {
147
+ ctx.addIssue({
148
+ code: z.ZodIssueCode.custom,
149
+ message: "`formatMatcher` cannot be combined with `dateStyle` / `timeStyle`.",
150
+ path: ["formatMatcher"],
151
+ });
152
+ }
153
+ });
154
+
155
+ const relativeTimePresetZodSchema = z
156
+ .object({
157
+ numeric: z.enum(["always", "auto"]).optional(),
158
+ style: z.enum(["long", "short", "narrow"]).optional(),
159
+ })
160
+ .strict();
161
+
162
+ export const formatPresetsZodSchema = z
163
+ .object({
164
+ number: z.record(z.string(), numberPresetZodSchema).optional(),
165
+ date: z.record(z.string(), dateTimePresetZodSchema).optional(),
166
+ time: z.record(z.string(), dateTimePresetZodSchema).optional(),
167
+ relative: z.record(z.string(), relativeTimePresetZodSchema).optional(),
168
+ dateTimeRange: z.record(z.string(), dateTimePresetZodSchema).optional(),
169
+ })
170
+ .strict();