From 3386e430cac094a27320e1b56e9f32b3b052aa4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20De=20Caluw=C3=A9?= Date: Mon, 14 Nov 2016 17:34:42 +0100 Subject: [PATCH 1/2] Add a Letterbox class to handle envelopes. --- index.js | 1 + letterbox.js | 114 ++++++++++++++++++++++++++++++++++++++++++ spec/LetterboxSpec.js | 85 +++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 letterbox.js create mode 100644 spec/LetterboxSpec.js 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/letterbox.js b/letterbox.js new file mode 100644 index 0000000..05afc93 --- /dev/null +++ b/letterbox.js @@ -0,0 +1,114 @@ +/** + * @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' + +/** + * 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 Letterbox + */ +var Letterbox = function () { + var letterbox = this; + + this.depth = {}; + this.depth.current = 0; + this.depth.minimum = 0; + this.depth.maximum = 2; + + this.next = function () { + letterbox.depth.current -= 1; + }; +} + +module.exports = Letterbox; + +function open_envelope(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 close_envelope(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; + } +} + +/** + * Accept a segment. + * + * @param {String} segment A segment object. + */ +Letterbox.prototype.write = 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.track(segment); + } else { + switch (segment.name) { + case 'UNB': + open_envelope.call(this, 'interchange', 0); + break; + case 'UNG': + open_envelope.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.track(segment); + } + break; + case 'UNE': + close_envelope.call(this, 'group', 1); + break; + case 'UNZ': + close_envelope.call(this, 'interchange', 0); + break; + default: + throw Error('Did not expect a ' + segment + ' segment'); + } + } +}; + +Letterbox.prototype.track = function () {}; + +module.exports = Letterbox; diff --git a/spec/LetterboxSpec.js b/spec/LetterboxSpec.js new file mode 100644 index 0000000..7096f99 --- /dev/null +++ b/spec/LetterboxSpec.js @@ -0,0 +1,85 @@ +'use strict' + +let Letterbox = require('../letterbox.js'); + +describe('Letterbox', function () { + let letterbox; + beforeEach(function () { + letterbox = new Letterbox(); + spyOn(letterbox, 'track'); + }); + it('should accept a message without an envelope', function () { + expect(function () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); + expect(letterbox.depth.maximum).toEqual(0); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.next(); }).not.toThrow(); + expect(letterbox.depth.current).toEqual(0); + }); + it('cannot nest interchanges', function () { + expect(function () { letterbox.write({ name: 'UNB' }); }).not.toThrow(); + expect(letterbox.depth.minimum).toEqual(1); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.write({ name: 'UNB' }); }).toThrow(); + }); + it('cannot open an interchange after a single message', function () { + expect(function () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); + expect(letterbox.depth.maximum).toEqual(0); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.next(); }).not.toThrow(); + expect(letterbox.depth.current).toEqual(0); + expect(function () { letterbox.write({ name: 'UNB' }); }).toThrow(); + }); + it('cannot open a group without an interchange', function () { + expect(function () { letterbox.write({ 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 () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); + expect(letterbox.depth.maximum).toEqual(0); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.write({ name: name }); }).not.toThrow(); + expect(letterbox.track).toHaveBeenCalled(); + }); + } + it('should accept a message in an interchange', function () { + expect(function () { letterbox.write({ name: 'UNB' }); }).not.toThrow(); + expect(letterbox.depth.minimum).toEqual(1); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); + expect(letterbox.depth.maximum).toEqual(1); + expect(letterbox.depth.current).toEqual(2); + expect(function () { letterbox.next(); }).not.toThrow(); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.write({ name: 'UNZ' }); }).not.toThrow(); + expect(letterbox.depth.current).toEqual(0); + }); + it('should not accept a group after a message', function () { + expect(function () { letterbox.write({ name: 'UNB' }); }).not.toThrow(); + expect(letterbox.depth.minimum).toEqual(1); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); + expect(letterbox.depth.maximum).toEqual(1); + expect(letterbox.depth.current).toEqual(2); + expect(function () { letterbox.next(); }).not.toThrow(); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.write({ name: 'UNG' }); }).toThrow(); + }); + it('should not accept a group without an interchange', function () { + expect(function () { letterbox.write({ name: 'UNG' }); }).toThrow(); + }); + it('should not accept a message after a group', function () { + expect(function () { letterbox.write({ name: 'UNB' }); }).not.toThrow(); + expect(letterbox.depth.minimum).toEqual(1); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.write({ name: 'UNG' }); }).not.toThrow(); + expect(letterbox.depth.minimum).toEqual(2); + expect(letterbox.depth.current).toEqual(2); + expect(function () { letterbox.write({ name: 'UNE' }); }).not.toThrow(); + expect(letterbox.depth.current).toEqual(1); + expect(function () { letterbox.write({ name: 'UNH' }); }).toThrow(); + }); +}); From ce1442dcf2bf4d540f53ea1a4b50923107d170c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20De=20Caluw=C3=A9?= Date: Tue, 15 Nov 2016 04:05:21 +0100 Subject: [PATCH 2/2] WIP --- letterbox.js => interchange.js | 56 ++++++++++++--------- spec/InterchangeSpec.js | 89 ++++++++++++++++++++++++++++++++++ spec/LetterboxSpec.js | 85 -------------------------------- 3 files changed, 122 insertions(+), 108 deletions(-) rename letterbox.js => interchange.js (71%) create mode 100644 spec/InterchangeSpec.js delete mode 100644 spec/LetterboxSpec.js diff --git a/letterbox.js b/interchange.js similarity index 71% rename from letterbox.js rename to interchange.js index 05afc93..1c5b65b 100644 --- a/letterbox.js +++ b/interchange.js @@ -18,30 +18,40 @@ '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 Letterbox + * @constructs Interchange */ -var Letterbox = function () { - var letterbox = this; +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 () { - letterbox.depth.current -= 1; + this.next = function (segment) { + that.depth.current -= 1; }; } -module.exports = Letterbox; +Interchange.prototype = Object.create(Writable.prototype); -function open_envelope(name, level) { +function openEnvelope(name, level) { var message = ''; var depth = this.depth.current + 1; @@ -57,7 +67,7 @@ function open_envelope(name, level) { } } -function close_envelope(name, level) { +function closeEnvelope(name, level) { var message = ''; var depth = this.depth.current - 1; @@ -69,24 +79,19 @@ function close_envelope(name, level) { } } -/** - * Accept a segment. - * - * @param {String} segment A segment object. - */ -Letterbox.prototype.write = function (segment) { +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.track(segment); + this.tracker.accept(segment.name); } else { switch (segment.name) { case 'UNB': - open_envelope.call(this, 'interchange', 0); + openEnvelope.call(this, 'interchange', 0); break; case 'UNG': - open_envelope.call(this, 'group', 1); + openEnvelope.call(this, 'group', 1); break; case 'UNH': if (this.depth.current < this.depth.minimum) { @@ -94,21 +99,26 @@ Letterbox.prototype.write = function (segment) { } else { this.depth.maximum = this.depth.current; this.depth.current += 1; - this.track(segment); + this.setup(segment); } break; case 'UNE': - close_envelope.call(this, 'group', 1); + closeEnvelope.call(this, 'group', 1); break; case 'UNZ': - close_envelope.call(this, 'interchange', 0); + closeEnvelope.call(this, 'interchange', 0); break; default: throw Error('Did not expect a ' + segment + ' segment'); } } -}; +} -Letterbox.prototype.track = function () {}; +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 = Letterbox; +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(); + }); +}); diff --git a/spec/LetterboxSpec.js b/spec/LetterboxSpec.js deleted file mode 100644 index 7096f99..0000000 --- a/spec/LetterboxSpec.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict' - -let Letterbox = require('../letterbox.js'); - -describe('Letterbox', function () { - let letterbox; - beforeEach(function () { - letterbox = new Letterbox(); - spyOn(letterbox, 'track'); - }); - it('should accept a message without an envelope', function () { - expect(function () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); - expect(letterbox.depth.maximum).toEqual(0); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.next(); }).not.toThrow(); - expect(letterbox.depth.current).toEqual(0); - }); - it('cannot nest interchanges', function () { - expect(function () { letterbox.write({ name: 'UNB' }); }).not.toThrow(); - expect(letterbox.depth.minimum).toEqual(1); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.write({ name: 'UNB' }); }).toThrow(); - }); - it('cannot open an interchange after a single message', function () { - expect(function () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); - expect(letterbox.depth.maximum).toEqual(0); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.next(); }).not.toThrow(); - expect(letterbox.depth.current).toEqual(0); - expect(function () { letterbox.write({ name: 'UNB' }); }).toThrow(); - }); - it('cannot open a group without an interchange', function () { - expect(function () { letterbox.write({ 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 () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); - expect(letterbox.depth.maximum).toEqual(0); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.write({ name: name }); }).not.toThrow(); - expect(letterbox.track).toHaveBeenCalled(); - }); - } - it('should accept a message in an interchange', function () { - expect(function () { letterbox.write({ name: 'UNB' }); }).not.toThrow(); - expect(letterbox.depth.minimum).toEqual(1); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); - expect(letterbox.depth.maximum).toEqual(1); - expect(letterbox.depth.current).toEqual(2); - expect(function () { letterbox.next(); }).not.toThrow(); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.write({ name: 'UNZ' }); }).not.toThrow(); - expect(letterbox.depth.current).toEqual(0); - }); - it('should not accept a group after a message', function () { - expect(function () { letterbox.write({ name: 'UNB' }); }).not.toThrow(); - expect(letterbox.depth.minimum).toEqual(1); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.write({ name: 'UNH' }); }).not.toThrow(); - expect(letterbox.depth.maximum).toEqual(1); - expect(letterbox.depth.current).toEqual(2); - expect(function () { letterbox.next(); }).not.toThrow(); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.write({ name: 'UNG' }); }).toThrow(); - }); - it('should not accept a group without an interchange', function () { - expect(function () { letterbox.write({ name: 'UNG' }); }).toThrow(); - }); - it('should not accept a message after a group', function () { - expect(function () { letterbox.write({ name: 'UNB' }); }).not.toThrow(); - expect(letterbox.depth.minimum).toEqual(1); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.write({ name: 'UNG' }); }).not.toThrow(); - expect(letterbox.depth.minimum).toEqual(2); - expect(letterbox.depth.current).toEqual(2); - expect(function () { letterbox.write({ name: 'UNE' }); }).not.toThrow(); - expect(letterbox.depth.current).toEqual(1); - expect(function () { letterbox.write({ name: 'UNH' }); }).toThrow(); - }); -});