lib/modelToModel/compatibilityLayer.js
//////////////////////////////////////////
// Requirements //
//////////////////////////////////////////
var _ = require('lodash');
var semlog = require('semlog');
var log = semlog.log;
var moboUtil = require('./../util/moboUtil');
//////////////////////////////////////////
// Main //
//////////////////////////////////////////
/**
* Global Message Set, containing each error message only once and a counter how often it appeared
* Add new Messages via exports.addMessage(msg)
* @type {{}}
*/
exports.messages = {};
exports.exec = function(settings, registry) {
exports.settings = settings;
exports.registry = registry;
exports.messages = {};
var todo = ['field', 'model', 'form'];
for (var i = 0; i < todo.length; i++) {
var modelPartName = todo[i];
var modelPart = registry[modelPartName];
for (var key in modelPart) {
var obj = registry[modelPartName][key];
// Remove nulls from YAML
exports.removeNulls(obj);
// Upgrade old, deprecated property names to the newest standard
exports.upgradeChangedProperties(obj, modelPartName);
exports.upgradeHideInForm(obj);
// Add implicit properties
exports.applyImplicit(obj);
exports.upgradePropertyNotation(obj);
}
}
// Print compatibility messages
for (var msgText in exports.messages) {
log(msgText + ' (' + exports.messages[msgText] + ')');
}
return registry;
};
//////////////////////////////////////////
// Functions //
//////////////////////////////////////////
/**
* Adds a message to the exports.messages set
* This takes care that a deprecated warning is only shown once, with a counter how often it triggered
*
* @param msg
*/
exports.addMessage = function(msg) {
if (!exports.messages[msg]) {
exports.messages[msg] = 1;
} else {
exports.messages[msg] += 1;
}
};
/**
* This helper function removes/adjusts properties that have a value of null.
* They may be introduced by the YAML parser, in cases like:
* properties=
*
* @param obj
* @returns {*}
*/
exports.removeNulls = function(obj) {
for (var key in obj) {
if (obj[key] === null) {
delete obj[key];
}
}
return obj;
};
/**
* Upgrade old, deprecated property names to the newest standard
*
* @param obj
*/
exports.upgradeChangedProperties = function(obj, modelPartName) {
var messages = {}; // A message Set, that contains each message only once, the value beeing the counter
//////////////////////////////////////////
// Simple Renames //
//////////////////////////////////////////
var renamed = [];
// 2015.05.15 mobo v1.4.2
renamed.push(['smw_output', 'smw_overwriteOutput']);
renamed.push(['smw_outputLink', 'smw_overwriteOutputToLink']);
// 2015.06.13 mobo 1.5.0
renamed.push(['smw_categoryPrefix', 'smw_prefixCategory']);
renamed.push(['smw_categoryPostfix', 'smw_postfixCategory']);
// 2015.06.15 mobo 1.6.0 release
renamed.push(['smw_prefix', 'smw_prepend']);
renamed.push(['smw_prefixCategory', 'smw_prependCategory']);
renamed.push(['smw_prefixForm', 'smw_prependForm']);
renamed.push(['smw_prefixPage', 'smw_prependPage']);
renamed.push(['smw_postfix', 'smw_append']);
renamed.push(['smw_postfixForm', 'smw_appendForm']);
renamed.push(['smw_postfixPage', 'smw_appendPage']);
renamed.push(['smw_postfixCategory', 'smw_appendCategory']);
renamed.push(['smw_naming', 'naming']);
renamed.push(['smw_form', 'sf_form']);
renamed.push(['smw_forminfo', 'sf_forminfo']);
renamed.push(['smw_forminput', 'sf_forminput']);
renamed.push(['smw_freetext', 'sf_freetext']);
renamed.push(['smw_summary', 'sf_summary']);
// 2015.06.15 mobo 1.6.0 release
renamed.push(['propertyOrder', 'itemsOrder']);
// 2015.06.18 mobo 1.6.2 release
renamed.push(['ignore', '$ignore']);
renamed.push(['abstract', '$abstract']);
for (var key in obj) {
// Rename properties
for (var i = 0; i < renamed.length; i++) {
var rename = renamed[i];
var msg = '';
if (key === rename[0]) {
// http://stackoverflow.com/a/14592469
Object.defineProperty(obj, rename[1], Object.getOwnPropertyDescriptor(obj, rename[0]));
delete obj[rename[0]];
exports.addMessage('[i] Renaming deprecated properties "' + rename[0] + '" to "' + rename[1] + '"');
if (!messages[msg]) {
messages[msg] = 1;
} else {
messages[msg] += 1;
}
if (exports.settings.verbose) {
log('[D] ' + obj.$path + ' > Renaming deprecated property "' + rename[0] + '" to "' + rename[1] + '"');
}
}
}
}
//////////////////////////////////////////
// Transformations //
//////////////////////////////////////////
if (modelPartName === 'field') {
// 2015.06.13 Moving smw_prefix.showForm & smw_prefix.showPage
exports.upgradeFormLinks(obj);
}
if (modelPartName === 'model') {
// 2015.06.13 Moving smw_prefix.showForm & smw_prefix.showPage
exports.upgradePrePostfixes(obj, 'smw_prefix');
exports.upgradePrePostfixes(obj, 'smw_postfix');
}
};
/**
* If the "type" attribute is missing and there is a "properties" attribute
* add the missing type depending on the property type
*
* @param {object} obj
* @returns {object}
*/
exports.applyImplicit = function(obj) {
'use strict';
var inspect = obj.items || obj;
// If the object has "items", it must be an array
// If the object has "properties", it must be an object
if (obj.items) {
obj.type = 'array';
}
if (inspect.items) {
inspect.type = 'array';
}
// Convert single strings to arrays (if the type is ['array', 'string'])
if (inspect.form && _.isString(inspect.form)) {
inspect.form = [inspect.form];
}
// If a field links to one or more form, it is always of type string and format Page!
if (inspect.form && !inspect.format) {
inspect.type = 'string';
inspect.format = 'Page';
}
// For all items:
if (obj.items) {
for (var i = 0; i < obj.items.length; i++) {
var item = obj.items[i];
// Add implicit .wikitext file extension to $extends to /smw_tempate/*
var $extend = item.$extend;
if ($extend && _.isString($extend) && $extend.indexOf('smw_template') > -1) {
if ($extend.indexOf('.wikitext') === -1) {
obj.items[i].$extend = $extend + '.wikitext';
}
}
}
}
// If there is still no type derived, use sane defaults.
// Models and Forms are always arrays. For fields, "string" is a good default.
if (!obj.type) {
if (obj.$modelPart === 'model' || obj.$modelPart === 'form') {
obj.type = 'array';
if (!obj.items) {
obj.items = [];
}
} else {
obj.type = 'string';
}
if (exports.settings.verbose) {
log('[D] ' + obj.id + ': No type given, assuming "' + obj.type + '"');
}
}
if (!obj.title) {
obj.title = obj.id;
if (exports.settings.verbose) {
log('[D] ' + obj.id + ': No title given, using ID instead: "' + obj.id + '"');
}
}
return obj;
};
exports.upgradeHideInForm = function(obj) {
if (obj.smw_hideInForm) {
exports.addMessage('[i] "smw_hideInForm": true is deprecated, please use "smw_showForm": false" instead');
obj.showForm = false;
delete obj.smw_hideInForm;
}
if (obj.smw_hideInPage) {
exports.addMessage('[i] "smw_hideInPage": true is deprecated, please use "smw_showPage": false" instead');
obj.showPage = false;
delete obj.smw_hideInPage;
}
};
/**
* Convert the deprecated property notation to the item (array) notation
* Converts the array structure back to an object structure, because it's much easier to access
* TODO: This has to be refactored to only use the array notation internally, but it will be a BIG project
*
* @param obj
*/
exports.upgradePropertyNotation = function(obj) {
// This only applies to models and forms.
// fields already use the item notation. Don't change anything there!
if (obj.$modelPart === 'model' || obj.$modelPart === 'form') {
// If the property notation is still used, rename it to items
if (obj.properties) {
exports.addMessage('[i] The "properties" array notation is now deprecated. Rename it to "items"');
obj.items = obj.properties;
delete obj.properties;
}
// Now convert all items/properties arrays to property objects (for internal use)
if (obj.items) {
if (_.isArray(obj.items)) {
obj.type = 'object';
obj.properties = exports.convertPropertyArray(obj.items, obj.id);
} else if (_.isObject(obj.items)) {
// @deprecated
exports.addMessage('[i] The "properties" object notation is deprecated. Rename it to "items" and use the array notation instead of the object notation.');
obj.properties = obj.items;
} else {
log('[E] "items" is neither array nor object!');
}
delete obj.items;
}
}
};
/**
* Helper function that will convert an property array to a property object/map (including keys)
* JSON Schema does support only property objects, but in case of mobo they include redundant information
* since the property key is almost always auto-genrated from the filename.
*
* This conversion ensures that JSON Schema compatibility is ensured while providing the more concise array notation
*
* @param {array} items Property array notation
* @param {string} objId id / name of the "mother" object
*
* @returns {object} Property object notation
*/
exports.convertPropertyArray = function(items, objId) {
'use strict';
var propertyObject = {};
for (var i = 0; i < items.length; i++) {
var prop = items[i];
var id = objId + '-i' + i; // Hack to fix: http://stackoverflow.com/a/3186907/776425
var $extend = prop.$extend || false;
if (prop.items) {
// To be consistent, the 2nd level items array needs to be a true array, too
// Since a multiple instance template can only have one template to instantiate, it can only support one item
// This is only
if (prop.items[0]) {
if (prop.items.length > 1) {
log('[E] ' + objId + ': A multiple instance (items) can only reference exactly one template!');
}
prop.items = prop.items[0];
}
if (prop.items.$extend) {
$extend = prop.items.$extend;
}
}
if (prop.items && prop.items.$extend) {
$extend = prop.items.$extend;
}
if (prop.id) {
// If an ID is given, use this as key
id = prop.id;
} else if ($extend) {
// If a $extend is given, do a lookup for the id in the resolve object
var ref = moboUtil.resolveReference($extend, prop.$filepath);
id = ref.id;
}
propertyObject[id] = prop;
}
return propertyObject;
};
exports.upgradeFormLinks = function(obj) {
var inspect = obj.items || obj;
if (inspect.format && inspect.format.indexOf('/form/') > -1) {
inspect.form = inspect.format.replace('/form/', '');
inspect.format = 'Page';
exports.addMessage('[i] Linking to a form with "format": "/form/formName" is now deprecated. Use "form": "formName" instead.');
}
if (inspect.oneOf) {
var formats = [];
for (var i = 0; i < inspect.oneOf.length; i++) {
formats.push(inspect.oneOf[i].format.replace('/form/', ''));
}
exports.addMessage('[i] The use of oneOf is now deprecated. Use "form": ["formName1", "formName2"] instead.');
inspect.format = 'Page';
inspect.type = 'string';
delete inspect.oneOf;
inspect.form = formats;
}
};
exports.upgradePrePostfixes = function(obj, prePostFix) {
// Prefix
if (obj[prePostFix]) {
if (obj[prePostFix].hasOwnProperty('showPage') || obj[prePostFix].hasOwnProperty('showForm')) {
exports.addMessage('[i] ' + prePostFix + '.showPage / ' + prePostFix + '.showForm are deprecated. Use the simpler ' + prePostFix + 'Page / ' + prePostFix + 'Form properties instead');
}
var prefixPage = !obj[prePostFix].hasOwnProperty('showPage') || obj[prePostFix].showPage;
var prefixForm = !obj[prePostFix].hasOwnProperty('showForm') || obj[prePostFix].showForm;
if (prefixPage && prefixForm) {
// Has already the new default behaviour
delete obj[prePostFix].showPage;
delete obj[prePostFix].showForm;
} else if (prefixForm && !prefixPage) {
delete obj[prePostFix].showPage;
obj[prePostFix + 'Form'] = obj[prePostFix];
delete obj[prePostFix];
} else if (prefixPage && !prefixForm) {
delete obj[prePostFix].showForm;
obj[prePostFix + 'Page'] = obj[prePostFix];
delete obj[prePostFix];
} else {
log('[E] ' + obj.$path + ': ' + prePostFix + ' is neither displayed in form or page!');
}
}
};