feat: normalize and validate component model structure

This commit is contained in:
MatthieuTD
2025-10-01 11:47:45 +02:00
parent f48e7aad30
commit 1a4cedb431
8 changed files with 383 additions and 25 deletions

View File

@@ -0,0 +1,152 @@
import { normalizeComponentModelStructure } from '../../component-models/structure.normalizer';
import type { ComponentModelStructure } from '../types/inventory';
export class ComponentModelStructureValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ComponentModelStructureValidationError';
}
}
function assertString(value: unknown, context: string): string {
if (typeof value !== 'string') {
throw new ComponentModelStructureValidationError(
`${context} doit être une chaîne de caractères`,
);
}
return value;
}
function sanitizeOptionalString(value: unknown): string | undefined {
if (value === undefined || value === null) {
return undefined;
}
return String(value);
}
function validatePieces(
pieces: ComponentModelStructure['pieces'],
): ComponentModelStructure['pieces'] {
return pieces.map((piece, index) => {
if ('familyCode' in piece) {
const familyCode = assertString(
piece.familyCode,
`pieces[${index}].familyCode`,
).trim();
if (!familyCode) {
throw new ComponentModelStructureValidationError(
`pieces[${index}].familyCode ne peut pas être vide`,
);
}
return {
familyCode,
role: sanitizeOptionalString(piece.role),
};
}
if ('typePieceId' in piece) {
const typePieceId = assertString(
piece.typePieceId,
`pieces[${index}].typePieceId`,
).trim();
if (!typePieceId) {
throw new ComponentModelStructureValidationError(
`pieces[${index}].typePieceId ne peut pas être vide`,
);
}
return {
typePieceId,
role: sanitizeOptionalString(piece.role),
};
}
throw new ComponentModelStructureValidationError(
`pieces[${index}] doit définir "familyCode" ou "typePieceId"`,
);
});
}
function validateCustomFields(
customFields: ComponentModelStructure['customFields'],
): ComponentModelStructure['customFields'] {
return customFields.map((field, index) => {
const key = assertString(field.key, `customFields[${index}].key`).trim();
if (!key) {
throw new ComponentModelStructureValidationError(
`customFields[${index}].key ne peut pas être vide`,
);
}
return { key, value: field.value };
});
}
function validateSubcomponents(
subcomponents: ComponentModelStructure['subcomponents'],
): ComponentModelStructure['subcomponents'] {
return subcomponents.map((subcomponent, index) => {
if ('modelId' in subcomponent) {
const modelId = assertString(
subcomponent.modelId,
`subcomponents[${index}].modelId`,
).trim();
if (!modelId) {
throw new ComponentModelStructureValidationError(
`subcomponents[${index}].modelId ne peut pas être vide`,
);
}
return {
modelId,
alias: sanitizeOptionalString(subcomponent.alias),
};
}
if ('familyCode' in subcomponent) {
const familyCode = assertString(
subcomponent.familyCode,
`subcomponents[${index}].familyCode`,
).trim();
if (!familyCode) {
throw new ComponentModelStructureValidationError(
`subcomponents[${index}].familyCode ne peut pas être vide`,
);
}
return {
familyCode,
alias: sanitizeOptionalString(subcomponent.alias),
};
}
if ('typeComposantId' in subcomponent) {
const typeComposantId = assertString(
subcomponent.typeComposantId,
`subcomponents[${index}].typeComposantId`,
).trim();
if (!typeComposantId) {
throw new ComponentModelStructureValidationError(
`subcomponents[${index}].typeComposantId ne peut pas être vide`,
);
}
return {
typeComposantId,
alias: sanitizeOptionalString(subcomponent.alias),
};
}
throw new ComponentModelStructureValidationError(
`subcomponents[${index}] doit définir "modelId", "familyCode" ou "typeComposantId"`,
);
});
}
export const ComponentModelStructureSchema = {
parse(input: unknown): ComponentModelStructure {
const normalized = normalizeComponentModelStructure(input);
return {
pieces: validatePieces(normalized.pieces),
customFields: validateCustomFields(normalized.customFields),
subcomponents: validateSubcomponents(normalized.subcomponents),
};
},
};