import { externalCsvConfigurationSchemaVersion, type ExternalCsvConfiguration, type ExternalCsvDisplayNameSuggestion, type ExternalCsvQuantityRule, } from "./external-csv-contracts.js"; const supportedCategories = new Set([ "lighting", "single_phase", "three_phase", ]); const requiredColumnKeys = [ "ifcGuid", "roomNumber", "roomName", "familyAndType", "selectionMarker", "circuitIdentifier", "power", ] as const; export function assertExternalCsvConfiguration( value: unknown ): asserts value is ExternalCsvConfiguration { if (!isRecord(value)) { throw new Error("External CSV configuration must be an object."); } if (value.schemaVersion !== externalCsvConfigurationSchemaVersion) { throw new Error("Unsupported external CSV configuration schema version."); } if (value.encoding !== "utf-8") { throw new Error("External CSV encoding must be utf-8."); } if (value.delimiter !== ";" && value.delimiter !== "," && value.delimiter !== "\t") { throw new Error("External CSV delimiter is unsupported."); } if (value.decimalSeparator !== "," && value.decimalSeparator !== ".") { throw new Error("External CSV decimal separator is unsupported."); } if (value.powerUnit !== "W" && value.powerUnit !== "kW") { throw new Error("External CSV power unit is unsupported."); } assertPositiveFiniteNumber(value.wattsPerSourceUnit, "wattsPerSourceUnit"); if (!isRecord(value.columns)) { throw new Error("External CSV columns must be an object."); } const mappedColumns = new Set(); for (const key of requiredColumnKeys) { const columnName = assertTrimmedString(value.columns[key], `columns.${key}`); if (mappedColumns.has(columnName)) { throw new Error(`External CSV column is mapped more than once: ${columnName}`); } mappedColumns.add(columnName); } if (value.columns.quantity !== null) { const quantityColumn = assertTrimmedString( value.columns.quantity, "columns.quantity" ); if (mappedColumns.has(quantityColumn)) { throw new Error(`External CSV column is mapped more than once: ${quantityColumn}`); } mappedColumns.add(quantityColumn); } if (!Array.isArray(value.additionalSourceMappings)) { throw new Error("External CSV additional source mappings must be an array."); } const targetFields = new Set(); for (const [index, mapping] of value.additionalSourceMappings.entries()) { if (!isRecord(mapping)) { throw new Error(`additionalSourceMappings.${index} must be an object.`); } const sourceColumn = assertTrimmedString( mapping.sourceColumn, `additionalSourceMappings.${index}.sourceColumn` ); const targetField = assertTrimmedString( mapping.targetField, `additionalSourceMappings.${index}.targetField` ); if (mappedColumns.has(sourceColumn)) { throw new Error(`External CSV column is mapped more than once: ${sourceColumn}`); } if (targetFields.has(targetField)) { throw new Error(`External target field is mapped more than once: ${targetField}`); } mappedColumns.add(sourceColumn); targetFields.add(targetField); } if (!Array.isArray(value.familyTypeRules)) { throw new Error("External CSV family/type rules must be an array."); } const exactFamilyValues = new Set(); for (const [index, rule] of value.familyTypeRules.entries()) { if (!isRecord(rule)) { throw new Error(`familyTypeRules.${index} must be an object.`); } const exactFamilyAndType = assertTrimmedString( rule.exactFamilyAndType, `familyTypeRules.${index}.exactFamilyAndType` ); if (exactFamilyValues.has(exactFamilyAndType)) { throw new Error( `External CSV family/type rule is duplicated: ${exactFamilyAndType}` ); } exactFamilyValues.add(exactFamilyAndType); assertTrimmedString( rule.internalDeviceType, `familyTypeRules.${index}.internalDeviceType` ); if (rule.connectionKind !== null) { assertTrimmedString( rule.connectionKind, `familyTypeRules.${index}.connectionKind` ); } if (typeof rule.category !== "string" || !supportedCategories.has(rule.category)) { throw new Error(`familyTypeRules.${index}.category is unsupported.`); } assertQuantityRule(rule.quantityRule, index); if (rule.quantityRule.kind === "mapped-column" && value.columns.quantity === null) { throw new Error( `familyTypeRules.${index}.quantityRule requires a configured quantity column.` ); } assertDisplayNameSuggestion(rule.displayNameSuggestion, index); } } export function validateExternalCsvConfiguration( value: unknown ): { success: true; data: ExternalCsvConfiguration } | { success: false; error: string } { try { assertExternalCsvConfiguration(value); return { success: true, data: value }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Invalid external CSV configuration.", }; } } function assertQuantityRule(value: unknown, index: number): asserts value is ExternalCsvQuantityRule { if (!isRecord(value)) { throw new Error(`familyTypeRules.${index}.quantityRule must be an object.`); } if (value.kind === "mapped-column") { return; } if (value.kind === "fixed") { assertPositiveFiniteNumber(value.quantity, `familyTypeRules.${index}.quantityRule.quantity`); return; } throw new Error(`familyTypeRules.${index}.quantityRule is unsupported.`); } function assertDisplayNameSuggestion( value: unknown, index: number ): asserts value is ExternalCsvDisplayNameSuggestion | null { if (value === null) { return; } if (!isRecord(value)) { throw new Error(`familyTypeRules.${index}.displayNameSuggestion must be an object or null.`); } if (value.kind === "selection-marker" || value.kind === "family-and-type") { return; } if (value.kind === "fixed") { assertTrimmedString(value.value, `familyTypeRules.${index}.displayNameSuggestion.value`); return; } throw new Error(`familyTypeRules.${index}.displayNameSuggestion is unsupported.`); } function assertPositiveFiniteNumber(value: unknown, field: string) { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { throw new Error(`${field} must be a positive finite number.`); } } function assertTrimmedString(value: unknown, field: string) { if (typeof value !== "string" || !value.trim() || value !== value.trim()) { throw new Error(`${field} must be a non-empty trimmed string.`); } return value; } function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }