Skip to content

Commit 7cb0b57

Browse files
nikkufake-join[bot]
authored andcommitted
feat: support alternative serialization methods
Until now type serialization was hard-coded to default, `property` and `xsi:type` with no ability to extend it, i.e. serialize types using `xmi:type`, typically found in UML and related documents. This PR generalizes our serialization infrastructure. Other hints but `property` will make the reader/writer look into the designated property to deduce the actual element type. This also in-sources our `*:type` attribute manipulation that previously was handled by `saxen`, our XML parser. We must normalize it where we import actually `moddle` elements, we must keep it untouched where we import `any` elements.
1 parent 1c37db9 commit 7cb0b57

File tree

9 files changed

+522
-55
lines changed

9 files changed

+522
-55
lines changed

lib/common.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,14 @@ export var DEFAULT_NS_MAP = {
77
'xml': 'http://www.w3.org/XML/1998/namespace'
88
};
99

10-
export var XSI_TYPE = 'xsi:type';
10+
export var SERIALIZE_PROPERTY = 'property';
1111

12-
function serializeFormat(element) {
12+
export function getSerialization(element) {
1313
return element.xml && element.xml.serialize;
1414
}
1515

16-
export function serializeAsType(element) {
17-
return serializeFormat(element) === XSI_TYPE;
18-
}
16+
export function getSerializationType(element) {
17+
const type = getSerialization(element);
1918

20-
export function serializeAsProperty(element) {
21-
return serializeFormat(element) === 'property';
19+
return type !== SERIALIZE_PROPERTY && (type || null);
2220
}

lib/read.js

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
} from 'moddle';
1717

1818
import {
19-
XSI_TYPE,
20-
serializeAsType,
19+
DEFAULT_NS_MAP,
20+
getSerializationType,
2121
hasLowerCaseAlias
2222
} from './common.js';
2323

@@ -34,12 +34,20 @@ function aliasToName(aliasNs, pkg) {
3434
return aliasNs.prefix + ':' + capitalize(aliasNs.localName);
3535
}
3636

37+
/**
38+
* Un-prefix a potentially prefixed type name.
39+
*
40+
* @param {NsName} nameNs
41+
* @param {Object} [pkg]
42+
*
43+
* @return {string}
44+
*/
3745
function prefixedToName(nameNs, pkg) {
3846

3947
var name = nameNs.name,
4048
localName = nameNs.localName;
4149

42-
var typePrefix = pkg.xml && pkg.xml.typePrefix;
50+
var typePrefix = pkg && pkg.xml && pkg.xml.typePrefix;
4351

4452
if (typePrefix && localName.indexOf(typePrefix) === 0) {
4553
return nameNs.prefix + ':' + localName.slice(typePrefix.length);
@@ -48,12 +56,19 @@ function prefixedToName(nameNs, pkg) {
4856
}
4957
}
5058

51-
function normalizeXsiTypeName(name, model) {
59+
function normalizeTypeName(name, nsMap, model) {
5260

53-
var nameNs = parseNameNS(name);
54-
var pkg = model.getPackage(nameNs.prefix);
61+
// normalize against actual NS
62+
const nameNs = parseNameNS(name, nsMap.xmlns);
63+
64+
const normalizedName = `${ nsMap[nameNs.prefix] || nameNs.prefix }:${ nameNs.localName }`;
65+
66+
const normalizedNameNs = parseNameNS(normalizedName);
5567

56-
return prefixedToName(nameNs, pkg);
68+
// determine actual type name, based on package-defined prefix
69+
var pkg = model.getPackage(normalizedNameNs.prefix);
70+
71+
return prefixedToName(normalizedNameNs, pkg);
5772
}
5873

5974
function error(message) {
@@ -378,26 +393,27 @@ ElementHandler.prototype.getPropertyForNode = function(node) {
378393
descriptor = getModdleDescriptor(type);
379394

380395
var propertyName = nameNs.name,
381-
property = descriptor.propertiesByName[propertyName],
382-
elementTypeName,
383-
elementType;
396+
property = descriptor.propertiesByName[propertyName];
384397

385398
// search for properties by name first
386399

387400
if (property && !property.isAttr) {
388401

389-
if (serializeAsType(property)) {
390-
elementTypeName = node.attributes[XSI_TYPE];
402+
const serializationType = getSerializationType(property);
403+
404+
if (serializationType) {
405+
const elementTypeName = node.attributes[serializationType];
391406

392-
// xsi type is optional, if it does not exists the
407+
// type is optional, if it does not exists the
393408
// default type is assumed
394409
if (elementTypeName) {
395410

411+
// convert the prefix used to the mapped form, but also
396412
// take possible type prefixes from XML
397-
// into account, i.e.: xsi:type="t{ActualType}"
398-
elementTypeName = normalizeXsiTypeName(elementTypeName, model);
413+
// into account, i.e.: xsi:type="t{ActualType}",
414+
const normalizedTypeName = normalizeTypeName(elementTypeName, node.ns, model);
399415

400-
elementType = model.getType(elementTypeName);
416+
const elementType = model.getType(normalizedTypeName);
401417

402418
return assign({}, property, {
403419
effectiveType: getModdleDescriptor(elementType).name
@@ -412,8 +428,8 @@ ElementHandler.prototype.getPropertyForNode = function(node) {
412428
var pkg = model.getPackage(nameNs.prefix);
413429

414430
if (pkg) {
415-
elementTypeName = aliasToName(nameNs, pkg);
416-
elementType = model.getType(elementTypeName);
431+
const elementTypeName = aliasToName(nameNs, pkg);
432+
const elementType = model.getType(elementTypeName);
417433

418434
// search for collection members later
419435
property = find(descriptor.properties, function(p) {

lib/write.js

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ import {
1414

1515
import {
1616
hasLowerCaseAlias,
17-
serializeAsType,
18-
serializeAsProperty,
19-
DEFAULT_NS_MAP,
20-
XSI_TYPE
17+
getSerialization,
18+
SERIALIZE_PROPERTY,
19+
DEFAULT_NS_MAP
2120
} from './common.js';
2221

2322
var XML_PREAMBLE = '<?xml version="1.0" encoding="UTF-8"?>\n';
@@ -492,12 +491,6 @@ ElementSerializer.prototype.parseGenericAttributes = function(element, attribute
492491

493492
forEach(attributes, function(attr) {
494493

495-
// do not serialize xsi:type attribute
496-
// it is set manually based on the actual implementation type
497-
if (attr.name === XSI_TYPE) {
498-
return;
499-
}
500-
501494
try {
502495
self.addAttribute(self.nsAttributeName(attr.name), attr.value);
503496
} catch (e) {
@@ -543,17 +536,17 @@ ElementSerializer.prototype.parseContainments = function(properties) {
543536

544537
// allow serialization via type
545538
// rather than element name
546-
var asType = serializeAsType(p),
547-
asProperty = serializeAsProperty(p);
539+
var serialization = getSerialization(p);
548540

549541
forEach(value, function(v) {
550542
var serializer;
551543

552-
if (asType) {
553-
serializer = new TypeSerializer(self, p);
554-
} else
555-
if (asProperty) {
556-
serializer = new ElementSerializer(self, p);
544+
if (serialization) {
545+
if (serialization === SERIALIZE_PROPERTY) {
546+
serializer = new ElementSerializer(self, p);
547+
} else {
548+
serializer = new TypeSerializer(self, p, serialization);
549+
}
557550
} else {
558551
serializer = new ElementSerializer(self);
559552
}
@@ -765,20 +758,25 @@ ElementSerializer.prototype.serializeTo = function(writer) {
765758
/**
766759
* A serializer for types that handles serialization of data types
767760
*/
768-
function TypeSerializer(parent, propertyDescriptor) {
761+
function TypeSerializer(parent, propertyDescriptor, serialization) {
769762
ElementSerializer.call(this, parent, propertyDescriptor);
763+
764+
this.serialization = serialization;
770765
}
771766

772767
inherits(TypeSerializer, ElementSerializer);
773768

774769
TypeSerializer.prototype.parseNsAttributes = function(element) {
775770

776-
// extracted attributes
777-
var attributes = ElementSerializer.prototype.parseNsAttributes.call(this, element);
771+
// extracted attributes with serialization attribute
772+
// <type=typeName> stripped; it may be later
773+
var attributes = ElementSerializer.prototype.parseNsAttributes.call(this, element).filter(
774+
attr => attr.name !== this.serialization
775+
);
778776

779777
var descriptor = element.$descriptor;
780778

781-
// only serialize xsi:type if necessary
779+
// only serialize <type=typeName> if necessary
782780
if (descriptor.name === this.propertyDescriptor.type) {
783781
return attributes;
784782
}
@@ -793,7 +791,7 @@ TypeSerializer.prototype.parseNsAttributes = function(element) {
793791
typePrefix = (pkg.xml && pkg.xml.typePrefix) || '';
794792

795793
this.addAttribute(
796-
this.nsAttributeName(XSI_TYPE),
794+
this.nsAttributeName(this.serialization),
797795
(typeNs.prefix ? typeNs.prefix + ':' : '') + typePrefix + descriptor.ns.localName
798796
);
799797

package-lock.json

Lines changed: 7 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,6 @@
6060
"dependencies": {
6161
"min-dash": "^4.0.0",
6262
"moddle": "^6.2.0",
63-
"saxen": "^8.1.2"
63+
"saxen": "^9.0.0"
6464
}
6565
}

test/fixtures/model/datatype.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"name": "Root",
1111
"properties": [
1212
{ "name": "bounds", "type": "Rect", "xml": { "serialize" : "xsi:type" } },
13-
{ "name": "otherBounds", "type": "Rect", "xml": { "serialize" : "xsi:type" }, "isMany": true }
13+
{ "name": "otherBounds", "type": "Rect", "xml": { "serialize" : "xsi:type" }, "isMany": true },
14+
{ "name": "xmiBounds", "type": "Rect", "xml": { "serialize" : "xmi:type" } },
15+
{ "name": "xmiManyBounds", "type": "Rect", "xml": { "serialize" : "xmi:type" }, "isMany": true }
1416
]
1517
},
1618
{

test/spec/reader.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,36 @@ describe('Reader', function() {
129129
});
130130

131131

132+
it('simple / xmi:type', async function() {
133+
134+
var datatypeModel = createModel([ 'datatype', 'datatype-external' ]);
135+
136+
// given
137+
var reader = new Reader(datatypeModel);
138+
var rootHandler = reader.handler('dt:Root');
139+
140+
var xml =
141+
'<dt:root xmlns:dt="http://datatypes" xmlns:do="http://datatypes2" ' +
142+
'xmlns:xmi="http://www.omg.org/spec/XMI/20131001">' +
143+
'<dt:xmiBounds xmi:type="dt:Rect" y="100" />' +
144+
'</dt:root>';
145+
146+
// when
147+
var {
148+
rootElement
149+
} = await reader.fromXML(xml, rootHandler);
150+
151+
// then
152+
expect(rootElement).to.jsonEqual({
153+
$type: 'dt:Root',
154+
xmiBounds: {
155+
$type: 'dt:Rect',
156+
y: 100
157+
}
158+
});
159+
});
160+
161+
132162
it('simple / default xml ns', async function() {
133163

134164
// given
@@ -312,6 +342,37 @@ describe('Reader', function() {
312342
});
313343

314344

345+
it('collection / xmi:type / from other namespace', async function() {
346+
347+
var datatypeModel = createModel([ 'datatype', 'datatype-external' ]);
348+
349+
// given
350+
var reader = new Reader(datatypeModel);
351+
var rootHandler = reader.handler('dt:Root');
352+
353+
var xml =
354+
'<dt:root xmlns:dt="http://datatypes" xmlns:do="http://datatypes2" ' +
355+
'xmlns:xmi="http://www.omg.org/spec/XMI/20131001">' +
356+
'<dt:xmiManyBounds xmi:type="dt:Rect" y="100" />' +
357+
'<dt:xmiManyBounds xmi:type="do:Rect" x="200" />' +
358+
'</dt:root>';
359+
360+
// when
361+
var {
362+
rootElement
363+
} = await reader.fromXML(xml, rootHandler);
364+
365+
// then
366+
expect(rootElement).to.jsonEqual({
367+
$type: 'dt:Root',
368+
xmiManyBounds: [
369+
{ $type: 'dt:Rect', y: 100 },
370+
{ $type: 'do:Rect', x: 200 }
371+
]
372+
});
373+
});
374+
375+
315376
it('collection / xsi:type / from other namespace / default ns)', async function() {
316377

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

0 commit comments

Comments
 (0)