Fix serialization of a leaf-list field.
[osm/UI.git] / skyquake / plugins / composer / src / src / libraries / model / DescriptorModelMetaFactory.js
index d3ef200..1ba8912 100644 (file)
@@ -4,10 +4,11 @@
  * This class provides methods to get the metadata about descriptor models.
  */
 
-'use strict';
-
-import _ from 'lodash'
-import utils from './../utils'
+import _cloneDeep from 'lodash/cloneDeep'
+import _isEmpty from 'lodash/isEmpty'
+import _pick from 'lodash/pick'
+import _get from 'lodash/get'
+import _set from 'lodash/set'
 import DescriptorModelMetaProperty from './DescriptorModelMetaProperty'
 import CommonUtils from 'utils/utils';
 const assign = Object.assign;
@@ -24,6 +25,152 @@ function getPathForType(type) {
        return type;
 }
 
+const uiStateToSave = ['containerPositionMap'];
+
+//////
+// Data serialization will be done on a meta model basis. That is,
+// given a schema and data, retrieve from the data only that which is 
+// defined by the schema.
+//
+
+// serialize data for a list of properties
+function serializeAll(properties, data) {
+       if (data) {
+               return properties.reduce((obj, p) => {
+                       return Object.assign(obj, p.serialize(data));
+               }, {});
+       }
+       return null;
+}
+
+function serialize_container(data) {
+       data = data[this.name];
+       if (_isEmpty(data)) {
+               return null;
+       }
+       let obj = {};
+       obj[this.name] = serializeAll(this.properties, data);
+       return obj;
+}
+
+function serialize_list(data) {
+       data = data[this.name];
+       if (data) {
+               if (!Array.isArray(data)) {
+                       return serializeAll(this.properties, data);
+               } else if (data.length) {
+                       let list = data.reduce((c, d) => {
+                               let obj = serializeAll(this.properties, d);
+                               if (!_isEmpty(obj)) {
+                                       c.push(obj);
+                               }
+                               return c;
+                       }, []);
+                       if (!_isEmpty(list)){
+                               let obj = {};
+                               obj[this.name] = list;
+                               return obj;
+                       }
+               }
+       }
+       return null;
+}
+
+function serialize_leaf(data) {
+       let value = data[this.name];
+       if (value === null || typeof value === 'undefined' || value === '') {
+               return null;
+       }
+       let obj = {};
+       if (this['data-type'] === 'empty') {
+               value = ''; // empty string does get sent as value
+       }
+       obj[this.name] = value;
+       return obj;
+}
+
+function serialize_leaf_empty(data) {
+       let value = data[this.name];
+       if (value) {
+               let obj = {};
+               obj[this.name] = "";
+               return obj;
+       }
+       return null;
+}
+
+function serialize_leaf_list(data) {
+       data = data[this.name];
+       if (data) {
+               data = data.reduce((result, value) => {
+                       if (value !== '' && value !== null && value !== undefined && typeof value !== 'object') {
+                               result.push(value);
+                       }
+                       return result;
+               }, []);
+               if (data.length){
+                       let obj = {};
+                       obj[this.name] = data;
+                       return obj;
+               }
+       }
+       return null;
+}
+
+function serialize_choice(data) {
+       let keys = Object.keys(data);
+       if (keys) {
+               const chosen = this.properties.find(
+                       c => c.type === 'case' && c.properties && c.properties.some(p => keys.indexOf(p.name) > -1));
+               return chosen && serializeAll(chosen.properties, data);
+       }
+       return null;
+}
+
+function serialize_case(data) {
+       return Serializer.container.call(this, data);
+}
+
+// special ui data handler for leaf of type string named 'meta' 
+function serialize_meta(data) {
+       let uiState = data['uiState'];
+       let meta = uiState && _pick(uiState, uiStateToSave);
+       // if there is no uiState to save perhaps this was not a ui state property
+       return _isEmpty(meta) ? null : {meta: JSON.stringify(meta)};
+}
+
+function serialize_unsupported(data) {
+       console.error('unsupported property', property);
+       return null;
+}
+
+function getSerializer(property) {
+       switch (property.name) {
+               case 'rw-nsd:meta':
+               case 'rw-vnfd:meta':
+                       return serialize_meta.bind(property);
+       }
+       switch (property.type) {
+               case 'list':
+               return serialize_list.bind(property);
+               case 'container':
+               return serialize_container.bind(property);
+               case 'choice':
+               return serialize_choice.bind(property);
+               case 'case':
+               return serialize_case.bind(property);
+               case 'leaf_list':
+               return serialize_leaf_list.bind(property);
+               case 'leaf':
+               switch (property['data-type']){
+                       case 'empty':
+                       return serialize_leaf_empty.bind(property);
+               }
+               return serialize_leaf.bind(property);
+       }
+       return serialize_unsupported.bind(property);
+}
+
 let modelMetaByPropertyNameMap = [];
 
 let cachedDescriptorModelMetaRequest = null;
@@ -31,32 +178,38 @@ let cachedDescriptorModelMetaRequest = null;
 export default {
        init() {
                if (!cachedDescriptorModelMetaRequest) {
-                       cachedDescriptorModelMetaRequest = new Promise(function(resolve, reject) {
-                               CommonUtils.getDescriptorModelMeta().then(function(data) {
+                       cachedDescriptorModelMetaRequest = new Promise(function (resolve, reject) {
+                               CommonUtils.getDescriptorModelMeta().then(function (data) {
                                        let DescriptorModelMetaJSON = data;
                                        modelMetaByPropertyNameMap = Object.keys(DescriptorModelMetaJSON).reduce((map, key) => {
                                                function mapProperties(parentMap, parentObj) {
+                                                       // let's beef up the meta info with a helper (more to come?)
+                                                       parentObj.serialize = getSerializer(parentObj);
                                                        parentMap[':meta'] = parentObj;
                                                        const properties = parentObj && parentObj.properties ? parentObj.properties : [];
                                                        properties.forEach(p => {
-                                                               parentMap[p.name] = mapProperties({}, assign(p, {':qualified-type': parentObj[':qualified-type'] + '.' + p.name}));
+                                                               parentMap[p.name] = mapProperties({}, assign(p, {
+                                                                       ':qualified-type': parentObj[':qualified-type'] + '.' + p.name
+                                                               }));
                                                                return map;
                                                        }, parentMap);
                                                        return parentMap;
                                                }
-                                               map[key] = mapProperties({}, assign(DescriptorModelMetaJSON[key], {':qualified-type': key}));
+                                               map[key] = mapProperties({}, assign(DescriptorModelMetaJSON[key], {
+                                                       ':qualified-type': key
+                                               }));
                                                return map;
                                        }, {});
 
                                        (() => {
                                                // initialize the UI centric properties that CONFD could care less about
-                                               utils.assignPathValue(modelMetaByPropertyNameMap, 'nsd.meta.:meta.preserve-line-breaks', true);
-                                               utils.assignPathValue(modelMetaByPropertyNameMap, 'vnfd.meta.:meta.preserve-line-breaks', true);
-                                               utils.assignPathValue(modelMetaByPropertyNameMap, 'vnfd.vdu.cloud-init.:meta.preserve-line-breaks', true);
-                                               utils.assignPathValue(modelMetaByPropertyNameMap, 'nsd.constituent-vnfd.vnf-configuration.config-template.:meta.preserve-line-breaks', true);
+                                               _set(modelMetaByPropertyNameMap, 'nsd.meta.:meta.preserve-line-breaks', true);
+                                               _set(modelMetaByPropertyNameMap, 'vnfd.meta.:meta.preserve-line-breaks', true);
+                                               _set(modelMetaByPropertyNameMap, 'vnfd.vdu.cloud-init.:meta.preserve-line-breaks', true);
+                                               _set(modelMetaByPropertyNameMap, 'nsd.constituent-vnfd.vnf-configuration.config-template.:meta.preserve-line-breaks', true);
                                        })();
                                        resolve();
-                               }, function(error) {
+                               }, function (error) {
                                        cachedDescriptorModelMetaRequest = null;
                                })
                        })
@@ -64,15 +217,23 @@ export default {
 
                return cachedDescriptorModelMetaRequest;
        },
-       createModelInstanceForType(typeOrPath) {
+       /**
+        * Create a new instance of the indicated property and, if relevent, use the given
+        * unique name for the instance's key (see generateItemUniqueName)
+        * 
+        * @param {Object | string} typeOrPath a property definition object or a path to a property 
+        * @param [{string}] uniqueName optional 
+        * @returns 
+        */
+       createModelInstanceForType(typeOrPath, uniqueName) {
                const modelMeta = this.getModelMetaForType(typeOrPath);
-               return DescriptorModelMetaProperty.createModelInstance(modelMeta);
+               return DescriptorModelMetaProperty.createModelInstance(modelMeta, uniqueName);
        },
        getModelMetaForType(typeOrPath, filterProperties = () => true) {
                // resolve paths like 'nsd' or 'vnfd.vdu' or 'nsd.constituent-vnfd'
-               const found = utils.resolvePath(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
+               const found = _get(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
                if (found) {
-                       const uiState = _.cloneDeep(found[':meta']);
+                       const uiState = _cloneDeep(found[':meta']);
                        uiState.properties = uiState.properties.filter(filterProperties);
                        return uiState;
                }
@@ -80,23 +241,65 @@ export default {
        },
        getModelFieldNamesForType(typeOrPath) {
                // resolve paths like 'nsd' or 'vnfd.vdu' or 'nsd.constituent-vnfd'
-               const found = utils.resolvePath(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
+               const found = _get(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
                if (found) {
                        let result = [];
                        found[':meta'].properties.map((p) => {
                                // if(false) {
-                               if(p.type == 'choice') {
+                               if (p.type == 'choice') {
                                        result.push(p.name)
-                                       return p.properties.map(function(q){
+                                       return p.properties.map(function (q) {
                                                result.push(q.properties[0].name);
                                        })
 
-                               } else  {
+                               } else {
                                        return result.push(p.name);
                                }
                        })
                        return result;
                }
                console.warn('no model uiState found for type', typeOrPath);
+       },
+       /**
+        * For a list with a single valued key that is of type string, generate a unique name
+        * for a new entry to be added to the indicated list. This name will use the provided
+        * prefix (or the list's name) followed by a number. The number will be based on the
+        * current length of the array but will insure there is no collision with an existing
+        * name.
+        * 
+        * @param {Array} list the list model data
+        * @param {prooerty} property the schema definition of the list 
+        * @param [{any} prefix] the perferred prefix for the name. If not provide property.name
+        *  will be used. 
+        * @returns {string}
+        */
+       generateItemUniqueName(list, property, prefix) {
+               if (property.type !== 'list' ||
+                       property.key.length !== 1 ||
+                       property.properties.find(prop => prop.name === property.key[0])['data-type'] !== 'string') {
+                       // only support list with a single key of type string
+                       return null;
+               }
+               if (!prefix) {
+                       prefix = property.name;
+               }
+               let key = property.key[0];
+               let suffix = list ? list.length + 1 : 1
+               let keyValue = prefix + '-' + suffix;
+
+               function makeUniqueName() {
+                       if (list) {
+                               for (let i = 0; i < list.length; i = ++i) {
+                                       if (list[i][key] === keyValue) {
+                                               keyValue = keyValue + '-' + (i + 1);
+                                               makeUniqueName(); // not worried about recursing too deep (chances ??)
+                                               break;
+                                       }
+                               }
+                       }
+               }
+               makeUniqueName();
+               return keyValue;
        }
-}
+
+}
\ No newline at end of file