Skip to content

Commit

Permalink
Add a Letterbox class to handle envelopes.
Browse files Browse the repository at this point in the history
  • Loading branch information
tdecaluwe committed Nov 15, 2016
1 parent a5c9047 commit 3386e43
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 0 deletions.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
114 changes: 114 additions & 0 deletions letterbox.js
Original file line number Diff line number Diff line change
@@ -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;
85 changes: 85 additions & 0 deletions spec/LetterboxSpec.js
Original file line number Diff line number Diff line change
@@ -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();
});
});

0 comments on commit 3386e43

Please sign in to comment.