Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement enveloping support #14

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
124 changes: 124 additions & 0 deletions interchange.js
Original file line number Diff line number Diff line change
@@ -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;
89 changes: 89 additions & 0 deletions spec/InterchangeSpec.js
Original file line number Diff line number Diff line change
@@ -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();
});
});