219 lines
7.8 KiB
JavaScript
219 lines
7.8 KiB
JavaScript
/**
|
||
* EDIFACT Validator & Alternative Parser (Main Process only)
|
||
* Uses ts-edifact library as a parallel layer alongside the custom DelforParser.
|
||
*
|
||
* IMPORTANT: This file runs in the Node.js Main Process only.
|
||
* Do NOT include this file in index.html or use window.EDIBridge here.
|
||
*
|
||
* Exposed via IPC (see main.js):
|
||
* 'validate-edifact' → validateEdifact(content)
|
||
* 'parse-edifact-lib' → parseWithLib(content)
|
||
*
|
||
* ts-edifact API:
|
||
* const { Reader } = require('ts-edifact');
|
||
* const reader = new Reader(); // instanciate per call (stateful)
|
||
* const segments = reader.parse(str); // returns { name, elements: string[][] }[]
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
/**
|
||
* Parse EDIFACT content using ts-edifact Reader.
|
||
* Returns a clean segment array for comparison with the custom DelforParser.
|
||
*
|
||
* Segment format: { name: 'UNH', elements: [['1'], ['DELFOR','D','04A','UN']] }
|
||
*
|
||
* @param {string} content - Raw EDIFACT content (string)
|
||
* @returns {{ segments: Array, segmentCount: number, error?: string }}
|
||
*/
|
||
function parseWithLib(content) {
|
||
try {
|
||
const { Reader, NullValidator, Parser } = require('ts-edifact');
|
||
|
||
// Normalize line endings
|
||
const normalized = content.replace(/\r\n/g, '').replace(/\r/g, '').trim();
|
||
|
||
// Each Reader instance is stateful — create a new one per call
|
||
const reader = new Reader();
|
||
|
||
// --- FIX: Disable strict segment validation & Enable UNOC ---
|
||
const validator = new NullValidator();
|
||
if (typeof validator.define !== 'function') validator.define = () => { };
|
||
|
||
reader.validator = validator;
|
||
reader.parser = new Parser(validator);
|
||
reader.defined = true;
|
||
reader.encoding('UNOC'); // Allow lowercase and special chars
|
||
|
||
// Re-setup Reader callbacks for the new parser
|
||
const result = reader.result;
|
||
let elements = reader.elements;
|
||
let components = reader.components;
|
||
reader.parser.onOpenSegment = function (segment) {
|
||
elements = [];
|
||
result.push({ name: segment, elements: elements });
|
||
};
|
||
reader.parser.onElement = function () {
|
||
components = [];
|
||
elements.push(components);
|
||
};
|
||
reader.parser.onComponent = function (value) {
|
||
components.push(value);
|
||
};
|
||
|
||
const segments = reader.parse(normalized);
|
||
|
||
return {
|
||
segments,
|
||
segmentCount: segments.length
|
||
};
|
||
|
||
} catch (e) {
|
||
return {
|
||
segments: [],
|
||
segmentCount: 0,
|
||
error: e.message
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Validate EDIFACT content using ts-edifact library + structural checks.
|
||
*
|
||
* Performs:
|
||
* 1. Syntax parsing via ts-edifact Reader (catches malformed EDI)
|
||
* 2. Structural checks: UNB/UNZ envelope, UNH/UNT balance, BGM presence
|
||
* 3. UNT segment count verification
|
||
* 4. Message type & version detection
|
||
*
|
||
* @param {string} content - Raw EDIFACT content
|
||
* @returns {{ valid: boolean, errors: string[], warnings: string[], stats: object }}
|
||
*/
|
||
function validateEdifact(content) {
|
||
const errors = [];
|
||
const warnings = [];
|
||
const stats = {};
|
||
|
||
try {
|
||
const { Reader, NullValidator, Parser } = require('ts-edifact');
|
||
|
||
// --- Step 1: Parse with ts-edifact ---
|
||
let segments = null;
|
||
try {
|
||
const normalized = content.replace(/\r\n/g, '').replace(/\r/g, '').trim();
|
||
const reader = new Reader();
|
||
|
||
// --- FIX: Disable strict segment validation & Enable UNOC ---
|
||
const validator = new NullValidator();
|
||
if (typeof validator.define !== 'function') validator.define = () => { };
|
||
|
||
reader.validator = validator;
|
||
reader.parser = new Parser(validator);
|
||
reader.defined = true;
|
||
reader.encoding('UNOC');
|
||
|
||
// Re-setup Reader callbacks
|
||
const result = reader.result;
|
||
let elements = reader.elements;
|
||
let components = reader.components;
|
||
reader.parser.onOpenSegment = function (segment) {
|
||
elements = [];
|
||
result.push({ name: segment, elements: elements });
|
||
};
|
||
reader.parser.onElement = function () {
|
||
components = [];
|
||
elements.push(components);
|
||
};
|
||
reader.parser.onComponent = function (value) {
|
||
components.push(value);
|
||
};
|
||
|
||
segments = reader.parse(normalized);
|
||
} catch (parseErr) {
|
||
errors.push('Syntaxfehler: ' + parseErr.message);
|
||
}
|
||
|
||
if (!segments || !Array.isArray(segments)) {
|
||
return { valid: errors.length === 0, errors, warnings, stats };
|
||
}
|
||
|
||
// Build a flat tag list for quick lookups
|
||
const tags = segments.map(s => s.name);
|
||
stats.segmentCount = segments.length;
|
||
stats.allSegments = tags;
|
||
|
||
// --- Step 2: Interchange envelope (UNB / UNZ) ---
|
||
if (!tags.includes('UNB')) {
|
||
warnings.push('Kein UNB-Segment (Interchange-Header) gefunden.');
|
||
} else {
|
||
const unb = segments.find(s => s.name === 'UNB');
|
||
if (unb && unb.elements[1]) stats.sender = unb.elements[1][0] || '';
|
||
if (unb && unb.elements[2]) stats.receiver = unb.elements[2][0] || '';
|
||
}
|
||
if (!tags.includes('UNZ')) warnings.push('Kein UNZ-Segment (Interchange-Trailer) gefunden.');
|
||
|
||
// --- Step 3: Message envelope (UNH / UNT) ---
|
||
const unhCount = tags.filter(t => t === 'UNH').length;
|
||
const untCount = tags.filter(t => t === 'UNT').length;
|
||
stats.messageCount = unhCount;
|
||
|
||
if (unhCount === 0) {
|
||
errors.push('Kein UNH-Segment (Message-Header) gefunden.');
|
||
} else {
|
||
// Detect message type from first UNH+<ref>+<type>:<ver>:<release>:<ctrl>
|
||
const unh = segments.find(s => s.name === 'UNH');
|
||
if (unh && unh.elements[1]) {
|
||
stats.messageType = unh.elements[1][0] || '';
|
||
stats.messageVersion = (unh.elements[1][1] || '') + (unh.elements[1][2] || '');
|
||
stats.controlRef = unh.elements[0]?.[0] || '';
|
||
}
|
||
}
|
||
|
||
if (untCount === 0) {
|
||
errors.push('Kein UNT-Segment (Message-Trailer) gefunden.');
|
||
}
|
||
|
||
if (unhCount !== untCount) {
|
||
errors.push(`UNH/UNT-Anzahl stimmt nicht überein: ${unhCount} × UNH vs ${untCount} × UNT.`);
|
||
}
|
||
|
||
// --- Step 4: UNT segment count check ---
|
||
// UNT+<segCount>+<msgRef>' — segCount includes UNH and UNT itself
|
||
const unt = segments.find(s => s.name === 'UNT');
|
||
if (unt && unt.elements[0]) {
|
||
const declared = parseInt(unt.elements[0][0]);
|
||
// Find the slice from UNH to UNT (inclusive)
|
||
const unhIdx = segments.findIndex(s => s.name === 'UNH');
|
||
const untIdx = segments.findIndex(s => s.name === 'UNT');
|
||
if (!isNaN(declared) && unhIdx >= 0 && untIdx >= 0) {
|
||
const actual = untIdx - unhIdx + 1;
|
||
if (declared !== actual) {
|
||
warnings.push(
|
||
`UNT deklariert ${declared} Segmente, gezählt (UNH→UNT): ${actual}.`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Step 5: BGM check ---
|
||
if (!tags.includes('BGM')) {
|
||
warnings.push('Kein BGM-Segment (Nachrichtenbeginn) gefunden.');
|
||
} else {
|
||
const bgm = segments.find(s => s.name === 'BGM');
|
||
if (bgm && bgm.elements[0]) stats.documentCode = bgm.elements[0][0] || '';
|
||
}
|
||
|
||
} catch (e) {
|
||
errors.push('Unerwarteter Validierungsfehler: ' + e.message);
|
||
}
|
||
|
||
return {
|
||
valid: errors.length === 0,
|
||
errors,
|
||
warnings,
|
||
stats
|
||
};
|
||
}
|
||
|
||
module.exports = { validateEdifact, parseWithLib };
|