@makakwastaken/ts-edifact
Version:
Edifact parser library
450 lines • 17.3 kB
JavaScript
/**
* @author Roman Vottner
* @copyright 2020 Roman Vottner
* @license Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class Dictionary {
entries;
constructor(data) {
this.entries = {};
if (data) {
for (const key in data) {
if (key !== '') {
this.add(key, data[key]);
}
}
}
}
contains(key) {
if (Object.prototype.hasOwnProperty.call(this.entries, key)) {
return true;
}
return false;
}
get(key) {
if (this.contains(key)) {
return this.entries[key];
}
return undefined;
}
keys() {
return Object.keys(this.entries);
}
add(key, value) {
this.entries[key] = value;
return value;
}
length() {
return this.keys().length;
}
}
export var ValidatorStates;
(function (ValidatorStates) {
/**
* Setting validation to none will disable the validator completely. The
* validator will not even try to obtain a segment description for segments
* encountered. Almost all overhead is eliminated in this state.
*/
ValidatorStates[ValidatorStates["NONE"] = 0] = "NONE";
/**
* The segments state implies no segment definition was found for the current
* segment, so validation should be disabled for its elements and components.
* Normal validation should be resumed, however, as of the next segment.
*/
ValidatorStates[ValidatorStates["SEGMENTS"] = 1] = "SEGMENTS";
/**
* The elements state is equivalent to the segments state, but validation is
* only temporary disabled for the current element. Normal validation resumes
* as of the next element.
*/
ValidatorStates[ValidatorStates["ELEMENTS"] = 2] = "ELEMENTS";
/**
* Validation is enabled for all entities, including segments, elements and
* components.
*/
ValidatorStates[ValidatorStates["ALL"] = 3] = "ALL";
ValidatorStates[ValidatorStates["ENTER"] = 4] = "ENTER";
ValidatorStates[ValidatorStates["ENABLE"] = 5] = "ENABLE";
})(ValidatorStates || (ValidatorStates = {}));
export class NullValidator {
onOpenSegment() {
return undefined;
}
onElement() {
return undefined;
}
onOpenComponent() { }
onCloseComponent() {
return undefined;
}
onCloseSegment() { }
disable() { }
enable() { }
define() { }
format() {
return undefined;
}
}
/**
* The `Validator` can be used as an add-on to `Parser` class, to enable
* validation of segments, elements and components. This class implements a
* tolerant validator, only segments and elemens for which definitions are
* provided will be validated. Other segments or elements will pass through
* untouched. Validation includes:
* * Checking data element counts, including mandatory elements.
* * Checking component counts, including mandatory components.
* * Checking components against they're required format.
*/
export class ValidatorImpl {
segments = new Dictionary();
formats = new Dictionary();
counts = {
segment: 0,
element: 0,
component: 0,
};
state;
segment = undefined;
element = undefined;
component = undefined;
required = 0;
minimum = 0;
maximum = 0;
throwOnMissingDefinitions;
constructor(throwOnMissingDefinitions = false) {
this.state = ValidatorStates.ALL;
this.throwOnMissingDefinitions = throwOnMissingDefinitions;
}
/**
* @summary Enable validation on the next segment.
*/
disable() {
this.state = ValidatorStates.NONE;
}
/**
* @summary Enable validation on the next segment.
*/
enable() {
this.state = ValidatorStates.SEGMENTS;
}
define(definitions) {
for (const key of definitions.keys()) {
const entry = definitions.get(key);
if (entry) {
this.segments.add(key, entry);
}
}
}
/**
* @summary Request a component definition associated with a format string.
* @returns A component definition.
*/
format(formatString) {
// Check if we have a component definition in cache for this format string.
if (this.formats.contains(formatString)) {
return this.formats.get(formatString);
}
let parts;
if ((parts = /^(a|an|n)(\.\.)?([1-9][0-9]*)?$/.exec(formatString))) {
const max = Number.parseInt(parts[3]);
const min = parts[2] === '..' ? 0 : max;
let alpha = false;
let numeric = false;
switch (parts[1]) {
case 'a':
alpha = true;
break;
case 'n':
numeric = true;
break;
case 'an':
alpha = true;
numeric = true;
break;
}
return this.formats.add(formatString, {
alpha: alpha,
numeric: numeric,
minimum: min,
maximum: max,
});
}
throw this.errors.invalidFormatString(formatString);
}
/**
* Called when a adding a new segment to the parser
* @param segment The segment as a string
* @returns The segment entry
*/
onOpenSegment(segment) {
switch (this.state) {
case ValidatorStates.ALL:
case ValidatorStates.ELEMENTS:
case ValidatorStates.SEGMENTS:
case ValidatorStates.ENABLE:
// Try to retrieve a segment definition if validation is not turned off.
if ((this.segment = this.segments.get(segment))) {
// The onelement function will close the previous element, however we
// don't want the component counts to be checked. To disable them we put
// the validator in the elements state.
this.state = ValidatorStates.ELEMENTS;
}
else {
const error = this.errors.missingSegmentDefinition(segment, this.throwOnMissingDefinitions);
if (error) {
throw error;
}
}
}
this.counts.segment += 1;
this.counts.element = 0;
return this.segment;
}
onElement() {
let name;
switch (this.state) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fall through to continue with element count validation
case ValidatorStates.ALL:
if (this.segment === undefined) {
const error = this.errors.missingSegmentStart(undefined, this.throwOnMissingDefinitions);
if (error) {
throw error;
}
return;
}
name = this.segment.elements[this.counts.element]?.id;
if (this.element === undefined) {
throw this.errors.missingElementStart(name);
}
// Check component of the previous enter
if (this.counts.component < this.element.requires ||
this.counts.component > this.element.components.length) {
throw this.errors.countError('Element', name, this.element, this.counts.component);
}
case ValidatorStates.ENTER:
// Skip component count checks for the first element
case ValidatorStates.ELEMENTS:
if (this.segment === undefined) {
const error = this.errors.missingSegmentStart(undefined, this.throwOnMissingDefinitions);
if (error) {
throw error;
}
return;
}
// Get the current element
if ((this.element = this.segment.elements[this.counts.element])) {
this.state = ValidatorStates.ALL;
}
else {
this.state = ValidatorStates.ELEMENTS;
if (this.throwOnMissingDefinitions) {
throw this.errors.missingElementDefinition(this.counts.element.toString(), this.counts.segment.toString());
}
}
}
// Move to the next element
this.counts.element += 1;
// Reset component, as we are done with this element
this.counts.component = 0;
return this.element;
}
/**
* @summary Start validation for a new component.
* @param buffer - An object which implements the buffer interface.
*
* The buffer object should allow the mode to be set to alpha, numeric or
* alphanumeric with their corresponding methods.
*/
onOpenComponent(buffer) {
if (this.segment === undefined) {
const error = this.errors.missingSegmentStart(undefined, this.throwOnMissingDefinitions);
if (error) {
throw error;
}
return;
}
switch (this.state) {
case ValidatorStates.ALL: {
// Used to display the error message
const currentElement = this.segment.elements[this.counts.element];
if (this.element === undefined) {
throw this.errors.missingElementStart(currentElement?.id);
}
if (typeof this.element === 'string') {
throw new Error(`Element is a string ${currentElement?.id}` || this.element);
}
// Retrieve a component definition if validation is set to all
this.component = this.format(this.element.components[this.counts.component]?.format || '');
if (this.component === undefined) {
return;
}
this.required = this.element.requires;
this.minimum = this.component.minimum;
this.maximum = this.component.maximum;
// Set the corresponding buffer mode
if (this.component.alpha) {
if (this.component.numeric) {
buffer.alphanumeric();
}
else {
buffer.alpha();
}
}
else {
if (this.component.numeric) {
buffer.numeric();
}
else {
buffer.alphanumeric();
}
}
break;
}
default:
// Set the buffer to its default mode
buffer.alphanumeric();
}
this.counts.component += 1;
}
onCloseComponent(buffer) {
let length;
// Hold the component we are closing
let currentComponent;
switch (this.state) {
case ValidatorStates.ALL:
// Component validation is only needed when validation is set to all
length = buffer.length();
if (this.segment) {
if (this.element) {
currentComponent = this.element.components[this.counts.component];
}
else {
console.error('Element not found');
}
}
else {
const error = this.errors.missingSegmentStart(this.segment, this.throwOnMissingDefinitions);
if (error) {
throw error;
}
return;
}
// We perform validation if either the required component count is greater than
// or equal to the current component count or if a non-empty value was found
if (this.required >= this.counts.component || length > 0) {
if (length < this.minimum) {
throw this.errors.invalidData(this.element, `'${buffer?.content()}' length is less than minimum length ${this.minimum}`);
}
if (length > this.maximum) {
throw this.errors.invalidData(this.element, `'${buffer?.content()}' exceeds maximum length ${this.maximum}`);
}
}
}
return currentComponent;
}
/**
* @summary Finish validation for the current segment.
*/
onCloseSegment(segment) {
switch (this.state) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Needed
case ValidatorStates.ALL:
if (this.segment === undefined) {
const error = this.errors.missingSegmentStart(segment, this.throwOnMissingDefinitions);
if (error) {
throw error;
}
return;
}
if (this.element === undefined) {
throw this.errors.missingElementStart(segment);
}
if (this.counts.component < this.element.requires ||
this.counts.component > this.element.components.length) {
throw this.errors.countError('Element', this.segment.elements[this.counts.element].id, this.element, this.counts.component);
}
// Fall through to continue with element cound validation
case ValidatorStates.ELEMENTS:
if (this.segment === undefined) {
const error = this.errors.missingSegmentStart(segment, this.throwOnMissingDefinitions);
if (error) {
throw error;
}
return;
}
if (this.counts.element < this.segment.requires ||
this.counts.element > this.segment.elements.length) {
throw this.errors.countError('Segment', segment, this.segment, this.counts.element);
}
}
}
errors = {
invalidData: (element, msg) => new Error(`Could not accept data on element ${element?.id || 'undefined'}: ${msg}`),
invalidFormatString: (formatString) => new Error(`Invalid format string ${formatString}`),
countError: (type, name, definition, count) => {
let array;
let start = `${type} ${name}`;
let end;
let length = 0;
if (type === 'Segment') {
array = 'elements';
const entry = definition;
length = entry.elements.length;
}
else {
array = 'components';
const entry = definition;
length = entry.components.length;
}
if (count < definition.requires) {
start += ' only';
end = ` but requires at least ${definition.requires}`;
}
else {
end = ` but accepts at most ${length}`;
}
return new Error(`${start} got ${count} ${array}${end}${JSON.stringify(definition)}`);
},
missingElementStart: (segment) => {
const message = `Active open element expected on segment ${segment}`;
return new Error(message);
},
missingElementDefinition: (element, segment) => {
const message = `No definition found for element ${element}${segment ? ` on segment ${segment}` : ''}`;
return new Error(message);
},
missingSegmentStart: (segment, throwOnMissingDefinitions) => {
if (!throwOnMissingDefinitions) {
return undefined;
}
let name;
if (segment) {
name = `'${segment}'`;
}
else {
name = "''";
}
return new Error(`Active open segment ${name} expected. Found none`);
},
missingSegmentDefinition: (segment, throwOnMissingDefinitions) => {
if (throwOnMissingDefinitions) {
return new Error(`No segment definition found for segment name ${segment}`);
}
console.warn(`No segment definition found for segment name ${segment}`);
return undefined;
},
};
}
//# sourceMappingURL=validator.js.map