Skip to content

Commit

Permalink
feat: allow to configure custom nsMap
Browse files Browse the repository at this point in the history
This addition now allows for lightweight declaration of additional
important namespaces, to be used to parse a moddle XML document.

One of these is `xmi`, used, i.e. in UML schema definitions. To properly
handle it we must be able to explicitly define the mapping (to it)
without declaring it as an actual Moddle package.

Moddle packages and their `uri -> prefix` mapping still take precedence.
  • Loading branch information
nikku authored and fake-join[bot] committed Jan 10, 2023
1 parent dfd7357 commit 1c37db9
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 8 deletions.
9 changes: 6 additions & 3 deletions lib/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -835,9 +835,12 @@ Reader.prototype.fromXML = function(xml, options, done) {
uriMap[p.uri] = p.prefix;

return uriMap;
}, {
'http://www.w3.org/XML/1998/namespace': 'xml' // add default xml ns
});
}, Object.entries(DEFAULT_NS_MAP).reduce(function(map, [ prefix, url ]) {
map[url] = prefix;

return map;
}, model.config && model.config.nsMap || {}));

parser
.ns(uriMap)
.on('openTag', function(obj, decodeStr, selfClosing, getContext) {
Expand Down
57 changes: 52 additions & 5 deletions lib/write.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,18 @@ export function Namespaces(parent) {
this.wellknown = [];
this.custom = [];
this.parent = parent;

this.defaultPrefixMap = parent && parent.defaultPrefixMap || {};
}

Namespaces.prototype.mapDefaultPrefixes = function(defaultPrefixMap) {
this.defaultPrefixMap = defaultPrefixMap;
};

Namespaces.prototype.defaultUriByPrefix = function(prefix) {
return this.defaultPrefixMap[prefix];
};

Namespaces.prototype.byUri = function(uri) {
return this.uriMap[uri] || (
this.parent && this.parent.byUri(uri)
Expand Down Expand Up @@ -591,9 +601,7 @@ ElementSerializer.prototype.logNamespace = function(ns, wellknown, local) {
};

ElementSerializer.prototype.logNamespaceUsed = function(ns, local) {
var element = this.element,
model = element.$model,
namespaces = this.getNamespaces(local);
var namespaces = this.getNamespaces(local);

// ns may be
//
Expand All @@ -611,7 +619,7 @@ ElementSerializer.prototype.logNamespaceUsed = function(ns, local) {
return { localName: ns.localName };
}

wellknownUri = DEFAULT_NS_MAP[prefix] || model && (model.getPackage(prefix) || {}).uri;
wellknownUri = namespaces.defaultUriByPrefix(prefix);

uri = uri || wellknownUri || namespaces.uriByPrefix(prefix);

Expand Down Expand Up @@ -858,7 +866,13 @@ export function Writer(options) {
formatingWriter.append(XML_PREAMBLE);
}

new ElementSerializer().build(tree).serializeTo(formatingWriter);
var serializer = new ElementSerializer();

var model = tree.$model;

serializer.getNamespaces().mapDefaultPrefixes(getDefaultPrefixMappings(model));

serializer.build(tree).serializeTo(formatingWriter);

if (!writer) {
return internalWriter.value;
Expand All @@ -869,3 +883,36 @@ export function Writer(options) {
toXML: toXML
};
}


// helpers ///////////

/**
* @param {Moddle} model
*
* @return { Record<string, string> } map from prefix to URI
*/
function getDefaultPrefixMappings(model) {

const nsMap = model.config && model.config.nsMap || {};

const prefixMap = {};

// { prefix -> uri }
for (const prefix in DEFAULT_NS_MAP) {
prefixMap[prefix] = DEFAULT_NS_MAP[prefix];
}

// { uri -> prefix }
for (const uri in nsMap) {
const prefix = nsMap[uri];

prefixMap[prefix] = uri;
}

for (const pkg of model.getPackages()) {
prefixMap[pkg.prefix] = pkg.uri;
}

return prefixMap;
}
100 changes: 100 additions & 0 deletions test/spec/reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -2593,4 +2593,104 @@ describe('Reader', function() {

});


describe('custom namespace mapping', function() {

it('should read remapped xmi:type', async function() {

// given
var datatypesModel = createModel([
'datatype',
'datatype-external'
], {
nsMap: {
'http://www.omg.org/spec/XMI/20131001': 'xmi'
}
});

var reader = new Reader(datatypesModel);
var rootHandler = reader.handler('dt:Root');

var xml =
'<dt:root xmlns:dt="http://datatypes">' +
'<dt:xmiBounds xmlns:do="http://datatypes2" ' +
'xmlns:foo="http://www.omg.org/spec/XMI/20131001" ' +
'xmlns:f="http://foo" foo:type="do:Rect" ' +
'x="100" f:bar="BAR" />' +
'</dt:root>';

// when
var {
rootElement
} = await reader.fromXML(xml, rootHandler);

// then
expect(rootElement).to.jsonEqual({
$type: 'dt:Root',
xmiBounds: {
$type: 'do:Rect',
x: 100
}
});

});


it('should read remapped generic prefix', async function() {

// given
var extensionModel = createModel([ 'extensions' ], {
nsMap: {
'http://other': 'o',
'http://foo': 'f'
}
});

// given
var reader = new Reader(extensionModel);
var rootHandler = reader.handler('e:Root');

var xml =
'<e:root xmlns:e="http://extensions">' +
'<bar:bar xmlns:bar="http://bar">' +
'<other:child b="B" xmlns:other="http://other" />' +
'</bar:bar>' +
'<foo xmlns="http://foo">' +
'<child a="A" />' +
'</foo>' +
'</e:root>';

// when
var {
rootElement
} = await reader.fromXML(xml, rootHandler);

// then
expect(rootElement).to.jsonEqual({
$type: 'e:Root',
extensions: [
{
$type: 'bar:bar',
'xmlns:bar': 'http://bar',
$children: [
{
$type: 'o:child',
'xmlns:other': 'http://other',
b: 'B'
}
]
},
{
$type: 'f:foo',
'xmlns': 'http://foo',
$children: [
{ $type: 'f:child', a: 'A' }
]
}
]
});
});

});

});
42 changes: 42 additions & 0 deletions test/spec/rountrip.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,46 @@ describe('Roundtrip', function() {
'</base:Root>');
});


describe('custom namespace mapping', function() {

it('should keep remapped generic prefix', async function() {

// given
var extensionModel = createModel([ 'extensions' ], {
nsMap: {
'http://other': 'o',
'http://foo': 'f'
}
});

// given
var reader = new Reader(extensionModel);
var writer = createWriter(extensionModel);

var rootHandler = reader.handler('e:Root');

var input =
'<e:root xmlns:e="http://extensions">' +
'<bar:bar xmlns:bar="http://bar">' +
'<other:child xmlns:other="http://other" b="B" />' +
'</bar:bar>' +
'<foo xmlns="http://foo">' +
'<child a="A" />' +
'</foo>' +
'</e:root>';

// when
var {
rootElement
} = await reader.fromXML(input, rootHandler);

var output = writer.toXML(rootElement);

// then
expect(output).to.eql(input);
});

});

});
37 changes: 37 additions & 0 deletions test/spec/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1868,4 +1868,41 @@ describe('Writer', function() {

});


describe('custom namespace mapping', function() {

var datatypesModel = createModel([
'datatype',
'datatype-external'
]);


it('should write explicitly remapped xsi:type', function() {

// given
var writer = createWriter(datatypesModel);

var root = datatypesModel.create('dt:Root');

root.set('bounds', datatypesModel.create('do:Rect', {
x: 100,
'xmlns:foo': 'http://www.w3.org/2001/XMLSchema-instance'
}));

// when
var xml = writer.toXML(root);

// then
expect(xml).to.eql(
'<dt:root xmlns:dt="http://datatypes">' +
'<dt:bounds xmlns:do="http://datatypes2" ' +
'xmlns:foo="http://www.w3.org/2001/XMLSchema-instance" ' +
'foo:type="do:Rect" ' +
'x="100" />' +
'</dt:root>'
);
});

});

});

0 comments on commit 1c37db9

Please sign in to comment.