UNPKG

@makakwastaken/ts-edifact

Version:
450 lines 17.3 kB
/** * @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