forked from jappel/leistungsbilanz-ts
Define Revit CSV contracts
This commit is contained in:
parent
927b868dcb
commit
43cee890e7
4 changed files with 409 additions and 4 deletions
185
src/external-model/csv/external-csv-configuration.ts
Normal file
185
src/external-model/csv/external-csv-configuration.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
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",
|
||||
"quantity",
|
||||
] 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<string>();
|
||||
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 (!Array.isArray(value.additionalSourceMappings)) {
|
||||
throw new Error("External CSV additional source mappings must be an array.");
|
||||
}
|
||||
const targetFields = new Set<string>();
|
||||
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<string>();
|
||||
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);
|
||||
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<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue