Skip to content

Commit 2325413

Browse files
committed
refactor: finish refactor of Zod4
1 parent 85fa967 commit 2325413

1 file changed

Lines changed: 85 additions & 216 deletions

File tree

apps/web/src/utils/upload2.ts

Lines changed: 85 additions & 216 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import type { Group } from '@opendatacapture/schemas/group';
88
import type { UnilingualInstrumentInfo } from '@opendatacapture/schemas/instrument';
99
import type { UploadInstrumentRecordsData } from '@opendatacapture/schemas/instrument-records';
1010
import { encodeScopedSubjectId } from '@opendatacapture/subject-utils';
11+
import { mapValues } from 'lodash-es';
1112
import { parse, unparse } from 'papaparse';
13+
import type { Merge } from 'type-fest';
1214
import { z as z3 } from 'zod/v3';
1315
import z, { z as z4 } from 'zod/v4';
1416

@@ -82,88 +84,46 @@ function extractSetEntry(entry: string) {
8284
);
8385
}
8486

85-
function extractRecordArrayEntry(entry: string) {
87+
function extractRecordArrayEntry(entry: string): { [key: string]: string }[] {
8688
const result = /RECORD_ARRAY\(\s*(.*?)[\s;]*\)/.exec(entry);
8789
if (!result?.[1]) {
8890
throw new Error(
8991
`Failed to extract record array value from entry: '${entry}' / Échec de l'extraction de la valeur du tableau d'enregistrements de l'entrée : '${entry}'`
9092
);
9193
}
94+
9295
const recordArrayDataList = result[1].split(';');
9396

9497
if (recordArrayDataList.at(-1) === '') {
9598
recordArrayDataList.pop();
9699
}
97100

98-
for (const listData of recordArrayDataList) {
99-
const recordArrayObject: { [key: string]: any } = {};
100-
101-
const record = listData.split(',');
102-
103-
if (!record) {
104-
return {
105-
message: {
106-
en: `Record in the record array was left undefined`,
107-
fr: `L'enregistrement dans le tableau d'enregistrements n'est pas défini`
108-
},
109-
success: false
110-
};
111-
}
112-
if (record.some((str) => str === '')) {
113-
return {
114-
message: {
115-
en: `One or more of the record array fields was left empty`,
116-
fr: `Un ou plusieurs champs du tableau d'enregistrements ont été laissés vides`
117-
},
118-
success: false
119-
};
101+
return recordArrayDataList.map((listData) => {
102+
const recordsList = listData.split(',');
103+
if (!recordsList) {
104+
throw new UploadError({
105+
en: `Record in the record array was left undefined`,
106+
fr: `L'enregistrement dans le tableau d'enregistrements n'est pas défini`
107+
});
120108
}
121-
if (!(zList.length === zKeys.length && zList.length === record.length)) {
122-
return {
123-
message: {
124-
en: `Incorrect number of entries for record array`,
125-
fr: `Nombre incorrect d'entrées pour le tableau d'enregistrements`
126-
},
127-
success: false
128-
};
109+
if (recordsList.some((str) => str === '')) {
110+
throw new UploadError({
111+
en: `One or more of the record array fields was left empty`,
112+
fr: `Un ou plusieurs champs du tableau d'enregistrements ont été laissés vides`
113+
});
129114
}
130-
for (let i = 0; i < record.length; i++) {
131-
if (!record[i]) {
132-
return {
133-
message: { en: `Failed to interpret field '${i}'`, fr: `Échec de l'interprétation du champ '${i}'` },
134-
success: false
135-
};
136-
}
137-
138-
const recordValue = record[i]!.split(':')[1]!.trim();
139-
140-
const zListResult = zList[i]!;
141-
if (!(zListResult && zListResult.typeName !== 'ZodArray' && zListResult.typeName !== 'ZodObject')) {
142-
return {
143-
message: { en: `Failed to interpret field '${i}'`, fr: `Échec de l'interprétation du champ '${i}'` },
144-
success: false
145-
};
146-
}
147-
const interpretZodValueResult: UploadOperationResult<FormTypes.FieldValue> = interpretZodValue(
148-
recordValue,
149-
zListResult.typeName,
150-
zListResult.isOptional
151-
);
152-
if (!interpretZodValueResult.success) {
153-
return {
154-
message: {
155-
en: `failed to interpret value at entry ${i} in record array row ${listData}`,
156-
fr: `échec de l'interprétation de la valeur à l'entrée ${i} dans la ligne de tableau d'enregistrements ${listData}`
157-
},
158-
success: false
159-
};
115+
const records: { [key: string]: string } = {};
116+
recordsList.forEach((rawRecord, i) => {
117+
const [recordKey, recordValue] = rawRecord.split(':').map((s) => s.trim());
118+
if (!(recordKey && recordValue)) {
119+
throw new UploadError({
120+
en: `Malformed record at index ${i}`
121+
});
160122
}
161-
162-
recordArrayObject[zKeys[i]!] = interpretZodValueResult.value;
163-
}
164-
recordArray.push(recordArrayObject);
165-
}
166-
return result[1];
123+
records[recordKey] = recordValue;
124+
});
125+
return records;
126+
});
167127
}
168128

169129
namespace Zod3 {
@@ -755,25 +715,55 @@ namespace Zod3 {
755715
}
756716

757717
namespace Zod4 {
758-
function parseZodSchema(schema: unknown, isOptional = false): Zod3.ZodTypeNameResult {
718+
type BaseZodConvertResult = {
719+
isOptional: boolean;
720+
typeName: Exclude<Zod3.RequiredZodTypeName, 'ZodArray' | 'ZodEnum' | 'ZodObject'>;
721+
};
722+
723+
type ArrayZodConvertResult = Merge<
724+
BaseZodConvertResult,
725+
{
726+
innerType: ZodConvertResult;
727+
typeName: 'ZodArray';
728+
}
729+
>;
730+
731+
type EnumZodConvertResult = Merge<
732+
BaseZodConvertResult,
733+
{
734+
enumValues?: readonly string[];
735+
typeName: 'ZodEnum';
736+
}
737+
>;
738+
739+
type ObjectZodConvertResult = Merge<
740+
BaseZodConvertResult,
741+
{
742+
dimensions: {
743+
[key: string]: ZodConvertResult;
744+
};
745+
typeName: 'ZodObject';
746+
}
747+
>;
748+
749+
type ZodConvertResult = ArrayZodConvertResult | BaseZodConvertResult | EnumZodConvertResult | ObjectZodConvertResult;
750+
751+
function parseZodSchema(schema: unknown, isOptional = false): ZodConvertResult {
759752
switch (true) {
760753
case schema instanceof z4.ZodArray:
761754
return {
755+
innerType: parseZodSchema(schema.element),
762756
isOptional,
763-
multiValues: [parseZodSchema(schema.element)],
764757
typeName: 'ZodArray'
765758
};
766759
case schema instanceof z4.ZodObject:
767-
const multiValues: Zod3.ZodTypeNameResult[] = [];
768-
const multiKeys: string[] = [];
760+
const dimensions: { [key: string]: ZodConvertResult } = {};
769761
Object.entries(schema.shape).forEach(([key, value]) => {
770-
multiKeys.push(key);
771-
multiValues.push(parseZodSchema(value));
762+
dimensions[key] = parseZodSchema(value);
772763
});
773764
return {
765+
dimensions,
774766
isOptional,
775-
multiKeys,
776-
multiValues,
777767
typeName: 'ZodObject'
778768
};
779769
case schema instanceof z4.ZodBoolean:
@@ -815,117 +805,29 @@ namespace Zod4 {
815805
});
816806
}
817807
}
818-
819-
//insert zodObjectValue function
820-
export function interpretZodObjectValue(
821-
entry: string,
822-
isOptional: boolean,
823-
zList: ZodTypeNameResult[],
824-
zKeys: string[]
825-
): UploadOperationResult<FormTypes.FieldValue> {
826-
try {
827-
if (entry === '' && isOptional) {
828-
return { success: true, value: undefined };
829-
} else if (!entry.startsWith('RECORD_ARRAY(')) {
830-
return { message: { en: `Invalid ZodType`, fr: `ZodType invalide` }, success: false };
831-
}
832-
833-
const recordArray: { [key: string]: any }[] = [];
834-
const recordArrayDataEntry = extractRecordArrayEntry(entry);
835-
const recordArrayDataList = recordArrayDataEntry.split(';');
836-
837-
if (recordArrayDataList.at(-1) === '') {
838-
recordArrayDataList.pop();
839-
}
840-
841-
for (const listData of recordArrayDataList) {
842-
const recordArrayObject: { [key: string]: any } = {};
843-
844-
const record = listData.split(',');
845-
846-
if (!record) {
847-
return {
848-
message: {
849-
en: `Record in the record array was left undefined`,
850-
fr: `L'enregistrement dans le tableau d'enregistrements n'est pas défini`
851-
},
852-
success: false
853-
};
854-
}
855-
if (record.some((str) => str === '')) {
808+
function interpetZodConvertResult(convertResult: ZodConvertResult, entry: string) {
809+
if (entry === '' && convertResult.isOptional) {
810+
return { success: true, value: undefined };
811+
}
812+
switch (convertResult.typeName) {
813+
case 'ZodArray':
814+
try {
815+
const parsedRecords = extractRecordArrayEntry(entry).map((parsedRecord) => {
816+
return mapValues(parsedRecord, (entry): unknown => interpetZodTypeResult(convertResult.innerType, entry));
817+
});
856818
return {
857-
message: {
858-
en: `One or more of the record array fields was left empty`,
859-
fr: `Un ou plusieurs champs du tableau d'enregistrements ont été laissés vides`
860-
},
861-
success: false
819+
success: true,
820+
value: parsedRecords
862821
};
863-
}
864-
if (!(zList.length === zKeys.length && zList.length === record.length)) {
822+
} catch {
865823
return {
866824
message: {
867-
en: `Incorrect number of entries for record array`,
868-
fr: `Nombre incorrect d'entrées pour le tableau d'enregistrements`
825+
en: `failed to interpret record array entries`,
826+
fr: `échec de l'interprétation des entrées du tableau d'enregistrements`
869827
},
870828
success: false
871829
};
872830
}
873-
for (let i = 0; i < record.length; i++) {
874-
if (!record[i]) {
875-
return {
876-
message: { en: `Failed to interpret field '${i}'`, fr: `Échec de l'interprétation du champ '${i}'` },
877-
success: false
878-
};
879-
}
880-
881-
const recordValue = record[i]!.split(':')[1]!.trim();
882-
883-
const zListResult = zList[i]!;
884-
if (!(zListResult && zListResult.typeName !== 'ZodArray' && zListResult.typeName !== 'ZodObject')) {
885-
return {
886-
message: { en: `Failed to interpret field '${i}'`, fr: `Échec de l'interprétation du champ '${i}'` },
887-
success: false
888-
};
889-
}
890-
const interpretZodValueResult: UploadOperationResult<FormTypes.FieldValue> = interpretZodValue(
891-
recordValue,
892-
zListResult.typeName,
893-
zListResult.isOptional
894-
);
895-
if (!interpretZodValueResult.success) {
896-
return {
897-
message: {
898-
en: `failed to interpret value at entry ${i} in record array row ${listData}`,
899-
fr: `échec de l'interprétation de la valeur à l'entrée ${i} dans la ligne de tableau d'enregistrements ${listData}`
900-
},
901-
success: false
902-
};
903-
}
904-
905-
recordArrayObject[zKeys[i]!] = interpretZodValueResult.value;
906-
}
907-
recordArray.push(recordArrayObject);
908-
}
909-
910-
return { success: true, value: recordArray };
911-
} catch {
912-
return {
913-
message: {
914-
en: `failed to interpret record array entries`,
915-
fr: `échec de l'interprétation des entrées du tableau d'enregistrements`
916-
},
917-
success: false
918-
};
919-
}
920-
}
921-
922-
function interpetZodTypeResult(zodTypeNameResult: Zod3.ZodTypeNameResult, entry: string) {
923-
if (entry === '' && zodTypeNameResult.isOptional) {
924-
return { success: true, value: undefined };
925-
}
926-
switch (zodTypeNameResult.typeName) {
927-
case 'ZodArray':
928-
929831
case 'ZodBoolean':
930832
if (entry.toLowerCase() === 'true') {
931833
return { success: true, value: true };
@@ -980,8 +882,8 @@ namespace Zod4 {
980882
default:
981883
return {
982884
message: {
983-
en: `Invalid ZodType: ${zodTypeNameResult.typeName satisfies never}`,
984-
fr: `ZodType invalide : ${zodTypeNameResult.typeName satisfies never}`
885+
en: `Invalid ZodType: ${convertResult.typeName}`,
886+
fr: `ZodType invalide : ${convertResult.typeName}`
985887
},
986888
success: false
987889
};
@@ -1079,41 +981,8 @@ namespace Zod4 {
1079981
});
1080982
}
1081983
try {
1082-
const typeNameResult = parseZodSchema(shape[key]);
1083-
1084-
let interpreterResult: UploadOperationResult<FormTypes.FieldValue> = {
1085-
message: {
1086-
en: 'Could not interpret a correct value',
1087-
fr: "Impossible d'interpréter une valeur correcte"
1088-
},
1089-
success: false
1090-
};
1091-
1092-
if (typeNameResult.typeName === 'ZodArray' || typeNameResult.typeName === 'ZodObject') {
1093-
// eslint-disable-next-line max-depth
1094-
if (typeNameResult.multiKeys && typeNameResult.multiValues) {
1095-
interpreterResult = Zod3.interpretZodObjectValue(
1096-
rawValue,
1097-
typeNameResult.isOptional,
1098-
typeNameResult.multiValues,
1099-
typeNameResult.multiKeys
1100-
);
1101-
// TODO - what if this is not the case? Once generics are handled correctly should not be a problem
1102-
// Dealt with via else statement for now
1103-
} else {
1104-
interpreterResult.message = {
1105-
en: 'Record Array keys do not exist',
1106-
fr: "Les clés du tableau d'enregistrements n'existent pas"
1107-
};
1108-
}
1109-
} else {
1110-
interpreterResult = Zod3.interpretZodValue(
1111-
rawValue,
1112-
typeNameResult.typeName,
1113-
typeNameResult.isOptional
1114-
);
1115-
}
1116-
if (interpreterResult.success) jsonLine[headers[j]!] = interpreterResult.value;
984+
const result = interpetZodConvertResult(parseZodSchema(shape[key]), rawValue);
985+
jsonLine[headers[i]!] = result;
1117986
} catch (error: unknown) {
1118987
if (error instanceof UploadError) {
1119988
return resolve({
@@ -1135,7 +1004,7 @@ namespace Zod4 {
11351004
}
11361005
}
11371006

1138-
const zodCheck = instrumentSchemaWithInternal.safeParse(jsonLine);
1007+
const zodCheck = instrumentSchema.safeParse(jsonLine);
11391008
if (!zodCheck.success) {
11401009
console.error(zodCheck.error.issues);
11411010
const zodIssues = zodCheck.error.issues.map((issue) => {

0 commit comments

Comments
 (0)