Skip to content

Commit 36e378a

Browse files
authored
Merge pull request #1006 from david-roper/set-sample-data
2 parents 5f669cf + 33465b1 commit 36e378a

File tree

3 files changed

+72
-24
lines changed

3 files changed

+72
-24
lines changed

apps/web/src/features/upload/hooks/useProcessUploadData.ts

Whitespace-only changes.

apps/web/src/features/upload/utils.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ describe('getZodTypeName', () => {
1717
it('should parse a z.boolean()', () => {
1818
expect(getZodTypeName(z.boolean())).toMatchObject({ isOptional: false, success: true, typeName: 'ZodBoolean' });
1919
});
20-
it('should parse a z.set(z.string())', () => {
21-
expect(getZodTypeName(z.set(z.string()))).toMatchObject({ isOptional: false, success: true, typeName: 'ZodSet' });
20+
it('should parse a z.set(z.enum([]))', () => {
21+
expect(getZodTypeName(z.set(z.enum(['a', 'b', 'c'])))).toMatchObject({
22+
isOptional: false,
23+
success: true,
24+
typeName: 'ZodSet'
25+
});
2226
});
2327
it('should parse a z.string().optional()', () => {
2428
expect(getZodTypeName(z.string().optional())).toMatchObject({

apps/web/src/features/upload/utils.ts

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const ZOD_TYPE_NAMES = [
2020
'ZodDate',
2121
'ZodEnum',
2222
'ZodArray',
23-
'ZodObject'
23+
'ZodObject',
24+
'ZodEffects'
2425
] as const;
2526

2627
const INTERNAL_HEADERS = ['subjectID', 'date'];
@@ -31,7 +32,7 @@ const INTERNAL_HEADERS_SAMPLE_DATA = [MONGOLIAN_VOWEL_SEPARATOR + 'string', MONG
3132

3233
type ZodTypeName = Extract<`${z.ZodFirstPartyTypeKind}`, (typeof ZOD_TYPE_NAMES)[number]>;
3334

34-
type RequiredZodTypeName = Exclude<ZodTypeName, 'ZodOptional'>;
35+
type RequiredZodTypeName = Exclude<ZodTypeName, 'ZodEffects' | 'ZodOptional'>;
3536

3637
type ZodTypeNameResult =
3738
| {
@@ -71,8 +72,20 @@ function isZodEnumDef(def: AnyZodTypeDef): def is z.ZodEnumDef {
7172
return def.typeName === z.ZodFirstPartyTypeKind.ZodEnum;
7273
}
7374

75+
function isZodSetDef(def: AnyZodTypeDef): def is z.ZodSetDef {
76+
return def.typeName === z.ZodFirstPartyTypeKind.ZodSet;
77+
}
78+
7479
function isZodArrayDef(def: AnyZodTypeDef): def is z.ZodArrayDef {
75-
return def.typeName === 'ZodArray';
80+
return def.typeName === z.ZodFirstPartyTypeKind.ZodArray;
81+
}
82+
83+
function isZodEffectsDef(def: AnyZodTypeDef): def is z.ZodEffectsDef {
84+
return def.typeName === z.ZodFirstPartyTypeKind.ZodEffects;
85+
}
86+
87+
function isZodObjectDef(def: AnyZodTypeDef): def is z.ZodObjectDef {
88+
return def.typeName === z.ZodFirstPartyTypeKind.ZodObject;
7689
}
7790

7891
// TODO - fix extract set and record array functions to handle whitespace and trailing semicolon (present or included)
@@ -136,7 +149,26 @@ export function getZodTypeName(schema: z.ZodTypeAny, isOptional?: boolean): ZodT
136149
};
137150
} else if (isZodArrayDef(def)) {
138151
return interpretZodArray(schema, def.typeName, isOptional);
152+
} else if (isZodSetDef(def)) {
153+
const innerDef: unknown = def.valueType._def;
154+
155+
if (!isZodTypeDef(innerDef)) {
156+
return {
157+
message: 'Invalid inner type: ZodSet value type must have a valid type definition',
158+
success: false
159+
};
160+
}
161+
162+
if (isZodEnumDef(innerDef)) {
163+
return {
164+
enumValues: innerDef.values,
165+
isOptional: Boolean(isOptional),
166+
success: true,
167+
typeName: def.typeName
168+
};
169+
}
139170
}
171+
140172
return {
141173
isOptional: Boolean(isOptional),
142174
success: true,
@@ -194,7 +226,7 @@ export function interpretZodArray(
194226

195227
export function interpretZodValue(
196228
entry: string,
197-
zType: Exclude<ZodTypeName, 'ZodArray' | 'ZodObject' | 'ZodOptional'>,
229+
zType: Exclude<ZodTypeName, 'ZodArray' | 'ZodEffects' | 'ZodObject' | 'ZodOptional'>,
198230
isOptional: boolean
199231
): UploadOperationResult<FormTypes.FieldValue> {
200232
if (entry === '' && isOptional) {
@@ -222,11 +254,14 @@ export function interpretZodValue(
222254
return { success: true, value: parseNumber(entry) };
223255
}
224256
return { message: `Invalid number type: ${entry}`, success: false };
225-
//TODO if ZodSet has a enum see if those values can be shown in template data if possible
226257
case 'ZodSet':
227258
if (entry.startsWith('SET(')) {
228259
const setData = extractSetEntry(entry);
229-
return { success: true, value: new Set(setData.split(',')) };
260+
const values = setData.split(',').map((s) => s.trim()).filter(Boolean);
261+
if (values.length === 0) {
262+
return { message: 'Empty set is not allowed', success: false };
263+
}
264+
return { success: true, value: new Set(values) };
230265
}
231266
return { message: `Invalid ZodSet: ${entry}`, success: false };
232267
case 'ZodString':
@@ -268,7 +303,7 @@ export function interpretZodObjectValue(
268303
}
269304
for (let i = 0; i < record.length; i++) {
270305
// TODO - make sure this is defined
271-
const recordValue = record[i]!.split(':')[1]!;
306+
const recordValue = record[i]!.split(':')[1]!.trim();
272307

273308
const zListResult = zList[i]!;
274309
if (!(zListResult.success && zListResult.typeName !== 'ZodArray' && zListResult.typeName !== 'ZodObject')) {
@@ -285,7 +320,7 @@ export function interpretZodObjectValue(
285320
success: false
286321
};
287322
}
288-
// TODO - how do we know that `zKeys` is the same length as record? What if the user forgets to add a element
323+
289324
recordArrayObject[zKeys[i]!] = interpretZodValueResult.value;
290325
}
291326
recordArray.push(recordArrayObject);
@@ -304,7 +339,7 @@ function generateSampleData({
304339
multiKeys,
305340
multiValues,
306341
typeName
307-
}: Extract<ZodTypeNameResult, { success: true }>) {
342+
}: Extract<Exclude<ZodTypeNameResult, 'ZodEffects'>, { success: true }>) {
308343
switch (typeName) {
309344
case 'ZodBoolean':
310345
return formatTypeInfo('true/false', isOptional);
@@ -313,7 +348,14 @@ function generateSampleData({
313348
case 'ZodNumber':
314349
return formatTypeInfo('number', isOptional);
315350
case 'ZodSet':
316-
return formatTypeInfo('SET(a,b,c)', isOptional);
351+
try {
352+
if (enumValues) return formatTypeInfo(`SET(${enumValues.join('/')}, ...)`, isOptional);
353+
354+
return formatTypeInfo('SET(a,b,c)', isOptional);
355+
} catch {
356+
throw new Error(`Failed to generate sample data for ZodSet`);
357+
}
358+
317359
case 'ZodString':
318360
return formatTypeInfo('string', isOptional);
319361
case 'ZodEnum':
@@ -353,7 +395,6 @@ function generateSampleData({
353395
} catch {
354396
throw new Error('Invalid Record Array Error');
355397
}
356-
357398
default:
358399
throw new Error(`Invalid zod schema: unexpected type name '${typeName satisfies never}'`);
359400
}
@@ -363,12 +404,15 @@ export function createUploadTemplateCSV(instrument: AnyUnilingualFormInstrument)
363404
// TODO - type validationSchema as object
364405
const instrumentSchema = instrument.validationSchema as z.AnyZodObject;
365406

407+
const instrumentSchemaDef: unknown = instrument.validationSchema._def;
408+
366409
let shape: { [key: string]: z.ZodTypeAny } = {};
367-
// TODO - include ZodEffect as a typename like our other types
368-
if ((instrumentSchema._def.typeName as string) === 'ZodEffects') {
369-
// @ts-expect-error - TODO - find a type safe way to call this
370-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
371-
shape = instrumentSchema._def.schema._def.shape() as { [key: string]: z.ZodTypeAny };
410+
411+
if (isZodTypeDef(instrumentSchemaDef) && isZodEffectsDef(instrumentSchemaDef)) {
412+
const innerSchema: unknown = instrumentSchemaDef.schema._def;
413+
if (isZodTypeDef(innerSchema) && isZodObjectDef(innerSchema)) {
414+
shape = innerSchema.shape() as { [key: string]: z.ZodTypeAny };
415+
}
372416
} else {
373417
shape = instrumentSchema.shape as { [key: string]: z.ZodTypeAny };
374418
}
@@ -402,17 +446,17 @@ export async function processInstrumentCSV(
402446
let shape: { [key: string]: z.ZodTypeAny } = {};
403447
let instrumentSchemaWithInternal: z.AnyZodObject;
404448

405-
if ((instrumentSchema._def.typeName as string) === 'ZodEffects') {
406-
// @ts-expect-error - TODO - find a type safe way to call this
407-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
408-
instrumentSchemaWithInternal = instrumentSchema._def.schema.extend({
449+
const instrumentSchemaDef: unknown = instrumentSchema._def;
450+
451+
if (isZodTypeDef(instrumentSchemaDef) && isZodEffectsDef(instrumentSchemaDef)) {
452+
//TODO make this type safe without having to cast z.AnyZodObject
453+
instrumentSchemaWithInternal = (instrumentSchemaDef.schema as z.AnyZodObject).extend({
409454
date: z.coerce.date(),
410455
subjectID: z.string()
411-
}) as z.AnyZodObject;
456+
});
412457

413458
shape = instrumentSchemaWithInternal._def.shape() as { [key: string]: z.ZodTypeAny };
414459
} else {
415-
//const shape2 = instrumentSchema.shape as { [key: string]: z.ZodTypeAny };
416460
instrumentSchemaWithInternal = instrumentSchema.extend({
417461
date: z.coerce.date(),
418462
subjectID: z.string()

0 commit comments

Comments
 (0)