Add Revit CSV transport
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
ExternalCsvParseError,
|
||||
parseExternalCsv,
|
||||
serializeExternalCsv,
|
||||
} from "../src/external-model/csv/external-csv-transport.js";
|
||||
import {
|
||||
externalCsvTestConfiguration,
|
||||
firstIfcGuid,
|
||||
quotedRevitCsv,
|
||||
utf8Bytes,
|
||||
} from "./fixtures/revit-csv-fixtures.js";
|
||||
|
||||
describe("external CSV transport", () => {
|
||||
it("detects the observed Revit dialect and classifies every row", () => {
|
||||
const document = parseExternalCsv(
|
||||
utf8Bytes(quotedRevitCsv, true),
|
||||
externalCsvTestConfiguration
|
||||
);
|
||||
|
||||
assert.deepEqual(document.dialect, {
|
||||
encoding: "utf-8",
|
||||
hasBom: true,
|
||||
delimiter: ";",
|
||||
lineEnding: "\r\n",
|
||||
quoteCharacter: '"',
|
||||
quoteAllFields: true,
|
||||
hasTrailingLineEnding: true,
|
||||
});
|
||||
assert.equal(document.headerRowIndex, 1);
|
||||
assert.deepEqual(
|
||||
document.rows.map((row) => row.classification),
|
||||
[
|
||||
"metadata",
|
||||
"header",
|
||||
"passthrough",
|
||||
"object",
|
||||
"passthrough",
|
||||
"suspect-object",
|
||||
"object",
|
||||
]
|
||||
);
|
||||
assert.equal(document.rows[6].cells[2].value, 'Büro "Nord"');
|
||||
});
|
||||
|
||||
it("round-trips BOM, CRLF, quotation, order and cell values byte for byte", () => {
|
||||
const source = utf8Bytes(quotedRevitCsv, true);
|
||||
const document = parseExternalCsv(source, externalCsvTestConfiguration);
|
||||
|
||||
assert.deepEqual(serializeExternalCsv(document), source);
|
||||
});
|
||||
|
||||
it("preserves embedded delimiters and line endings inside quoted cells", () => {
|
||||
const csv = quotedRevitCsv.replace(
|
||||
'"Steckdose: Doppelsteckdose"',
|
||||
'"Steckdose; Doppel\r\nsteckdose"'
|
||||
);
|
||||
const source = utf8Bytes(csv, true);
|
||||
const document = parseExternalCsv(source, externalCsvTestConfiguration);
|
||||
|
||||
assert.equal(
|
||||
document.rows[3].cells[3].value,
|
||||
"Steckdose; Doppel\r\nsteckdose"
|
||||
);
|
||||
assert.deepEqual(serializeExternalCsv(document), source);
|
||||
});
|
||||
|
||||
it("blocks missing and ambiguous configured headers", () => {
|
||||
assertParseError(
|
||||
utf8Bytes(quotedRevitCsv.replace("IfcGUID", "Andere ID")),
|
||||
"header-not-found"
|
||||
);
|
||||
const duplicatedHeader = quotedRevitCsv.replace(
|
||||
'"";"";"";"";"";"";"";""',
|
||||
quotedRevitCsv.split("\r\n")[1]
|
||||
);
|
||||
assertParseError(utf8Bytes(duplicatedHeader), "ambiguous-header");
|
||||
|
||||
const duplicateConfiguredColumn = quotedRevitCsv.replace(
|
||||
'"Stromkreis";"Raumnummer"',
|
||||
'"Stromkreis";"Raumnummer";"Raumnummer"'
|
||||
);
|
||||
assertParseError(
|
||||
utf8Bytes(duplicateConfiguredColumn),
|
||||
"ambiguous-header"
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks invalid and duplicate non-empty IfcGUIDs", () => {
|
||||
assertParseError(
|
||||
utf8Bytes(quotedRevitCsv.replace(firstIfcGuid, "not-an-ifc-guid")),
|
||||
"invalid-ifc-guid"
|
||||
);
|
||||
assertParseError(
|
||||
utf8Bytes(quotedRevitCsv.replace(firstIfcGuid, ` ${firstIfcGuid}`)),
|
||||
"invalid-ifc-guid"
|
||||
);
|
||||
const duplicate = quotedRevitCsv.replace(
|
||||
"1AbCdEfGhIjKlMnOpQrStu",
|
||||
firstIfcGuid
|
||||
);
|
||||
assertParseError(utf8Bytes(duplicate), "duplicate-ifc-guid");
|
||||
});
|
||||
|
||||
it("blocks invalid UTF-8, unterminated quotes and mixed row endings", () => {
|
||||
assertParseError(new Uint8Array([0xff, 0xfe, 0xfd]), "invalid-encoding");
|
||||
assertParseError(
|
||||
utf8Bytes(quotedRevitCsv.replace(/"\r\n$/, "\r\n")),
|
||||
"invalid-csv"
|
||||
);
|
||||
assertParseError(
|
||||
utf8Bytes(quotedRevitCsv.replace("\r\n", "\n")),
|
||||
"invalid-csv"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function assertParseError(input: Uint8Array, code: ExternalCsvParseError["code"]) {
|
||||
assert.throws(
|
||||
() => parseExternalCsv(input, externalCsvTestConfiguration),
|
||||
(error: unknown) => error instanceof ExternalCsvParseError && error.code === code
|
||||
);
|
||||
}
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
createDefaultExternalCsvConfiguration,
|
||||
type ExternalCsvColumnMapping,
|
||||
} from "../../src/external-model/csv/external-csv-contracts.js";
|
||||
|
||||
export const externalCsvTestColumns: ExternalCsvColumnMapping = {
|
||||
ifcGuid: "IfcGUID",
|
||||
roomNumber: "Raumnummer",
|
||||
roomName: "Raumname",
|
||||
familyAndType: "Familie und Typ",
|
||||
selectionMarker: "CAx_Auswahlkenner",
|
||||
circuitIdentifier: "Stromkreis",
|
||||
power: "Elektrische Leistung",
|
||||
quantity: "Anzahl",
|
||||
};
|
||||
|
||||
export const externalCsvTestConfiguration =
|
||||
createDefaultExternalCsvConfiguration(externalCsvTestColumns);
|
||||
|
||||
export const firstIfcGuid = "0AbCdEfGhIjKlMnOpQrStu";
|
||||
export const secondIfcGuid = "1AbCdEfGhIjKlMnOpQrStu";
|
||||
|
||||
export const quotedRevitCsv = [
|
||||
'"Projekt-Export";"";"";"";"";"";"";""',
|
||||
'"Stromkreis";"Raumnummer";"Raumname";"Familie und Typ";"CAx_Auswahlkenner";"Elektrische Leistung";"Anzahl";"IfcGUID"',
|
||||
'"";"";"";"";"";"";"";""',
|
||||
`"UV_AV_01-2F1";"01/101";"Technik";"Steckdose: Doppelsteckdose";"Arbeitsplatz";"120,5";"2";"${firstIfcGuid}"`,
|
||||
'"UV_AV_01-2F1: 2";"";"";"";"";"";"";""',
|
||||
'"";"01/102";"Büro";"Steckdose: Standard";"Arbeitsplatz";"100";"1";""',
|
||||
`"";"01/103";"Büro \"\"Nord\"\"";"Steckdose: Standard";"Arbeitsplatz";"100";"1";"${secondIfcGuid}"`,
|
||||
].join("\r\n") + "\r\n";
|
||||
|
||||
export function utf8Bytes(value: string, withBom = false) {
|
||||
const encoded = new TextEncoder().encode(value);
|
||||
if (!withBom) {
|
||||
return encoded;
|
||||
}
|
||||
const result = new Uint8Array(encoded.length + 3);
|
||||
result.set([0xef, 0xbb, 0xbf]);
|
||||
result.set(encoded, 3);
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user