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);
|
||||
}
|
||||
106
src/external-model/csv/external-csv-contracts.ts
Normal file
106
src/external-model/csv/external-csv-contracts.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
|
||||
|
||||
export const externalCsvConfigurationSchemaVersion = 1 as const;
|
||||
|
||||
export type ExternalCsvEncoding = "utf-8";
|
||||
export type ExternalCsvDelimiter = ";" | "," | "\t";
|
||||
export type ExternalCsvDecimalSeparator = "," | ".";
|
||||
export type ExternalCsvPowerUnit = "W" | "kW";
|
||||
|
||||
export interface ExternalCsvColumnMapping {
|
||||
ifcGuid: string;
|
||||
roomNumber: string;
|
||||
roomName: string;
|
||||
familyAndType: string;
|
||||
selectionMarker: string;
|
||||
circuitIdentifier: string;
|
||||
power: string;
|
||||
quantity: string;
|
||||
}
|
||||
|
||||
export interface ExternalCsvAdditionalSourceMapping {
|
||||
sourceColumn: string;
|
||||
targetField: string;
|
||||
}
|
||||
|
||||
export type ExternalCsvQuantityRule =
|
||||
| { kind: "fixed"; quantity: number }
|
||||
| { kind: "mapped-column" };
|
||||
|
||||
export type ExternalCsvDisplayNameSuggestion =
|
||||
| { kind: "fixed"; value: string }
|
||||
| { kind: "selection-marker" }
|
||||
| { kind: "family-and-type" };
|
||||
|
||||
export interface ExternalCsvFamilyTypeRule {
|
||||
exactFamilyAndType: string;
|
||||
internalDeviceType: string;
|
||||
connectionKind: string | null;
|
||||
category: CircuitGroupCategory;
|
||||
quantityRule: ExternalCsvQuantityRule;
|
||||
displayNameSuggestion: ExternalCsvDisplayNameSuggestion | null;
|
||||
}
|
||||
|
||||
export interface ExternalCsvConfiguration {
|
||||
schemaVersion: typeof externalCsvConfigurationSchemaVersion;
|
||||
encoding: ExternalCsvEncoding;
|
||||
delimiter: ExternalCsvDelimiter;
|
||||
decimalSeparator: ExternalCsvDecimalSeparator;
|
||||
powerUnit: ExternalCsvPowerUnit;
|
||||
wattsPerSourceUnit: number;
|
||||
columns: ExternalCsvColumnMapping;
|
||||
additionalSourceMappings: ExternalCsvAdditionalSourceMapping[];
|
||||
familyTypeRules: ExternalCsvFamilyTypeRule[];
|
||||
}
|
||||
|
||||
export type ExternalCsvLineEnding = "\r\n" | "\n" | "\r";
|
||||
|
||||
export interface ExternalCsvDialect {
|
||||
encoding: ExternalCsvEncoding;
|
||||
hasBom: boolean;
|
||||
delimiter: ExternalCsvDelimiter;
|
||||
lineEnding: ExternalCsvLineEnding;
|
||||
quoteCharacter: '"';
|
||||
quoteAllFields: boolean;
|
||||
hasTrailingLineEnding: boolean;
|
||||
}
|
||||
|
||||
export interface ExternalCsvCell {
|
||||
value: string;
|
||||
wasQuoted: boolean;
|
||||
}
|
||||
|
||||
export type ExternalCsvRowClassification =
|
||||
| "metadata"
|
||||
| "header"
|
||||
| "passthrough"
|
||||
| "object"
|
||||
| "suspect-object";
|
||||
|
||||
export interface ExternalCsvRow {
|
||||
index: number;
|
||||
cells: ExternalCsvCell[];
|
||||
classification: ExternalCsvRowClassification;
|
||||
}
|
||||
|
||||
export interface ExternalCsvDocument {
|
||||
dialect: ExternalCsvDialect;
|
||||
headerRowIndex: number;
|
||||
rows: ExternalCsvRow[];
|
||||
}
|
||||
|
||||
export function createDefaultExternalCsvConfiguration(
|
||||
columns: ExternalCsvColumnMapping
|
||||
): ExternalCsvConfiguration {
|
||||
return {
|
||||
schemaVersion: externalCsvConfigurationSchemaVersion,
|
||||
encoding: "utf-8",
|
||||
delimiter: ";",
|
||||
decimalSeparator: ",",
|
||||
powerUnit: "W",
|
||||
wattsPerSourceUnit: 1,
|
||||
columns,
|
||||
additionalSourceMappings: [],
|
||||
familyTypeRules: [],
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue