diff --git a/index.js b/index.js index 182e447..743c11a 100644 --- a/index.js +++ b/index.js @@ -5,3 +5,4 @@ module.exports.Validator = require('./validator.js'); module.exports.Parser = require('./parser.js'); module.exports.Tracker = require('./tracker.js'); module.exports.Reader = require('./reader.js'); +module.exports.Letterbox = require('./reader.js'); diff --git a/interchange.js b/interchange.js new file mode 100644 index 0000000..1c5b65b --- /dev/null +++ b/interchange.js @@ -0,0 +1,124 @@ +/** + * @author Tom De Caluwé + * @copyright 2016 Tom De Caluwé + * @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. + */ + +'use strict' + +var Writable = require('stream').Writable; +var Tracker = require('./tracker.js'); + +/** + * Construct a new letterbox accepting EDIFACT envelopes. A letterbox is a + * Writable stream accepting segment objects. Enveloping is optional, so a + * single message will also be accepted. Groups inside an envelope are optional + * as well. When used however, every message should be in a group. + * + * @constructs Interchange + */ +var Interchange = function (path) { + var that = this; + + Writable.call(this, { + write: this.accept, + objectMode: true + }); + + this.path = path; + + this.depth = {}; + this.depth.current = 0; + this.depth.minimum = 0; + this.depth.maximum = 2; + + this.next = function (segment) { + that.depth.current -= 1; + }; +} + +Interchange.prototype = Object.create(Writable.prototype); + +function openEnvelope(name, level) { + var message = ''; + var depth = this.depth.current + 1; + + if (this.depth.current !== level) { + message += 'Cannot open the ' + name + ' at the current enveloping level'; + throw Error(message); + } else if (depth > this.depth.maximum) { + message += 'Cannot open the ' + name + ' since it has been omitted before'; + throw Error(message); + } else { + this.depth.current = depth; + this.depth.minimum = depth; + } +} + +function closeEnvelope(name, level) { + var message = ''; + var depth = this.depth.current - 1; + + if (depth !== level) { + message += 'Cannot close the ' + name + ' at the current enveloping level'; + throw Error(message); + } else { + this.depth.current = depth; + } +} + +Interchange.prototype.accept = function (segment) { + // Most of the time we are tracking segments in a message. To optimize for + // this case we start by detecting if we are currently in the middle of a + // message. We can do this with only one comparison. + if (this.depth.current > this.depth.maximum) { + this.tracker.accept(segment.name); + } else { + switch (segment.name) { + case 'UNB': + openEnvelope.call(this, 'interchange', 0); + break; + case 'UNG': + openEnvelope.call(this, 'group', 1); + break; + case 'UNH': + if (this.depth.current < this.depth.minimum) { + throw Error('Cannot omit an envelope'); + } else { + this.depth.maximum = this.depth.current; + this.depth.current += 1; + this.setup(segment); + } + break; + case 'UNE': + closeEnvelope.call(this, 'group', 1); + break; + case 'UNZ': + closeEnvelope.call(this, 'interchange', 0); + break; + default: + throw Error('Did not expect a ' + segment + ' segment'); + } + } +} + +Interchange.prototype.setup = function (segment) { + this.cork(); + this.tracker = new Tracker(require(this.path + '/' + segment.components[1])); + this.tracker.accept(segment.name); + this.uncork(); +}; + +module.exports = Interchange; diff --git a/spec/InterchangeSpec.js b/spec/InterchangeSpec.js new file mode 100644 index 0000000..1ee05db --- /dev/null +++ b/spec/InterchangeSpec.js @@ -0,0 +1,89 @@ +'use strict' + +let Interchange = require('../interchange.js'); + +describe('Interchange', function () { + let interchange; + beforeEach(function () { + interchange = new Interchange(); + interchange.tracker = { accept: function () {} }; + spyOn(interchange, 'setup'); + spyOn(interchange.tracker, 'accept'); + }); + it('should accept a message without an envelope', function () { + expect(function () { interchange.accept({ name: 'UNH' }); }).not.toThrow(); + expect(interchange.depth.maximum).toEqual(0); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.next(); }).not.toThrow(); + expect(interchange.depth.current).toEqual(0); + }); + it('cannot nest interchanges', function () { + expect(function () { interchange.accept({ name: 'UNB' }); }).not.toThrow(); + expect(interchange.depth.minimum).toEqual(1); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.accept({ name: 'UNB' }); }).toThrow(); + }); + it('cannot open an interchange after a single message', function () { + expect(function () { interchange.accept({ name: 'UNH' }); }).not.toThrow(); + expect(interchange.depth.maximum).toEqual(0); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.next(); }).not.toThrow(); + expect(interchange.depth.current).toEqual(0); + expect(function () { interchange.accept({ name: 'UNB' }); }).toThrow(); + }); + it('cannot open a group without an interchange', function () { + expect(function () { interchange.accept({ name: 'UNG' }); }).toThrow(); + }); + for (var name of ['UNB', 'UNZ', 'UNG', 'UNE']) { + it('should not validate a ' + name + ' segment while tracking a message', function () { + // While no valid message contains an enveloping segment in it's segment + // table, this test is the equivalence of allowing such a messsage + // definition. Prohibiting enveloping segments in messages should be + // done by providing a correct segment table, not by algorithm design. + expect(function () { + interchange.accept({ name: 'UNH' }); + }).not.toThrow(); + expect(interchange.depth.maximum).toEqual(0); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.accept({ name: name }); }).not.toThrow(); + expect(interchange.tracker.accept).toHaveBeenCalled(); + }); + } + it('should accept a message in an interchange', function () { + expect(function () { interchange.accept({ name: 'UNB' }); }).not.toThrow(); + expect(interchange.depth.minimum).toEqual(1); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.accept({ name: 'UNH' }); }).not.toThrow(); + expect(interchange.depth.maximum).toEqual(1); + expect(interchange.depth.current).toEqual(2); + expect(function () { interchange.next(); }).not.toThrow(); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.accept({ name: 'UNZ' }); }).not.toThrow(); + expect(interchange.depth.current).toEqual(0); + }); + it('should not accept a group after a message', function () { + expect(function () { interchange.accept({ name: 'UNB' }); }).not.toThrow(); + expect(interchange.depth.minimum).toEqual(1); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.accept({ name: 'UNH' }); }).not.toThrow(); + expect(interchange.depth.maximum).toEqual(1); + expect(interchange.depth.current).toEqual(2); + expect(function () { interchange.next(); }).not.toThrow(); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.accept({ name: 'UNG' }); }).toThrow(); + }); + it('should not accept a group without an interchange', function () { + expect(function () { interchange.accept({ name: 'UNG' }); }).toThrow(); + }); + it('should not accept a message after a group', function () { + expect(function () { interchange.accept({ name: 'UNB' }); }).not.toThrow(); + expect(interchange.depth.minimum).toEqual(1); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.accept({ name: 'UNG' }); }).not.toThrow(); + expect(interchange.depth.minimum).toEqual(2); + expect(interchange.depth.current).toEqual(2); + expect(function () { interchange.accept({ name: 'UNE' }); }).not.toThrow(); + expect(interchange.depth.current).toEqual(1); + expect(function () { interchange.accept({ name: 'UNH' }); }).toThrow(); + }); +});