/*jshint laxcomma:true, smarttabs: true, node: true, eqnull:true, esnext: true, forin: false */
'use strict';
/**
* Provides standard data serialization & deserialization functionality
* @module tastypie/lib/serializer
* @author Eric Satterwhite
* @since 0.1.0
* @requires xml2js
* @requires jstoxml
* @requires events
* @requires util
* @requires debug
* @requires tastypie/lib/class
* @requires tastypie/lib/class/options
* @requires mout/lang/kindOf
* @requires prim-util/prime/parentize
* @requires mout/lang/isObject
* @requires tastypie/lib/utility
* @requires tastypie/lib/exceptions
**/
var xml2js = require( 'xml2js' )
, jstoxml = require( 'jstoxml' )
, events = require( 'events' )
, util = require( 'util' )
, boom = require( 'boom' )
, kindOf = require( 'mout/lang/kindOf' )
, isObject = require( 'mout/lang/isObject' )
, isString = require( 'mout/lang/isString' )
, isFunction = require( 'mout/lang/isFunction' )
, toArray = require( 'mout/lang/toArray' )
, map = require( 'mout/object/map' )
, debug = require( 'debug')('tastypie:serializer' )
, Mime = require( './mime' )
, Class = require( './class' )
, Options = require( './class/options' )
, Parent = require( './class/parent' )
, exceptions = require( './exceptions' )
, createCallback = require( './utility' ).createCallback
, Serializer
;
const TOP_LEVEL = "response"
, OBJECT = "object"
, OBJECTS = "objects"
, VALUE = "value"
, FUNCTION = 'function'
, ARRAY = 'array'
;
/**
* @constructor
* @tutorial serializers
* @alias module:tastypie/lib/serializer
* @param {Object} [options] Serializer instance options options
* @param {String} [options.callbackKey=callback] callback key to be used for jsop responsed
* @param {String} [options.defaultFormat='application/json'] the default serialziion format if one is not specified
* @param {Object} [options.xml] configuration objects for xml serialization vis xml2js
* @param {Boolean} [options.xml.strict=false] if set to true, arrays will be serialize as a repeating elements with the same tag name.
* When set to false, arrays are serialized as an object of type array with repeating elements
* @param {Object} [options.content_types] A mapping of Content Type header names to short formats ie `{ 'text/xml': 'xml' }`
* @example serializer = new require("tastypie/lib/serializer")();
serializer.serialize({key:value}, "text/xml", null, console.log );
* @example
* // define new acceptable content types & formats
* FakeSerializer = new tastypie.Class({
* inherits:Serializer
* ,options:{
* content_types:{
* 'application/vnd+fakescript':['fake', 'vnd-fake']
* }
* }
*
* ,constructor: function( options ){
* this.setOptions( options )
* }
* ,to_fake: function(data, callback){ ... }
* ,from_fake:function(data,callback){ ... }
* });
* // ?format=fake
*/
Serializer = new Class({
mixin:[ Options, Parent ]
,options:{
callbackKey:'callback'
,defaultFormat:"application/json"
,xml:{
explicitCharKey : false
, trim : true
, normalize : false
, explicitArray : false
, explicitRoot : false
, ignoreAttrs : true
, mergeAttrs : false
, validator : null
, timeout : 20000
, strict : true
}
, content_types :{
'application/json':'json'
,'text/javascript':'jsonp'
,'text/xml':'xml'
}
}
,constructor:function( options ){
this.setOptions( options );
const _parser = new xml2js.Parser( this.options.xml );
const _mime = new Mime();
_mime.default_type = null;
Object.defineProperties(this,{
_mime:{
enumerable: false
,configurable: false
,get: function(){
return _mime;
}
}
,_parser:{
enumerable: false
,configurable: false
,get: function(){
return _parser;
}
}
});
this._mime.define(
map( this.options.content_types, function( val, key, obj){
return toArray( val );
})
);
}
/**
* Serialized an object into a string of a specified format
* @method module:tastypie/lib/serializer#serialize
* @param {Object} data The data object to be serialized into a string
* @param {String} format The format to serialize the object down to
* @param {Function} callback A callback function to be called when serialization is complete
* @throws SerializationError
**/
,serialize:function( data = '' , format, callback ){
var method_name
, desired_format
, mime
, _data
, _format
, _cb
;
_data = data;
mime = this._mime;
_format = isString( format ) ? format : this.options.defaultFormat;
_cb = createCallback(...arguments);
// if It is already a string
// there is nothing to serialize
if( isString( _data ) ) return _cb( null, _data );
desired_format = mime.extension( _format );
method_name = `to_${desired_format}`;
if( typeof this[method_name] === 'function'){
return this[method_name]( _data, _cb );
}
_cb( boom.unsupportedMediaType(`unsupported format ${desired_format}` ), null );
}
,convertFormat: function convertFormat( format ){
return this._mime.lookup( format ) || null;
}
/**
* Converts a data string into a javascript object based on a specified content type
* @method module:tastypie/lib/serializer#deserialize
* @param {String} Data string to convert into an object
* @param {String} contentType the content type of the string to be deserialzie
* @param {Function} callback a function to call when deserialization is complete
**/
,deserialize: function(data={}, format=this.options.defaultFormat, ...args ){
var desired_format
, method_name
, mime
, callback
;
mime = this._mime;
format = isString( format ) ? format : this.options.defaultFormat;
callback = createCallback( ...args );
if( isObject( data ) ){
return setImmediate( callback, null, data );
}
desired_format = mime.extension( format );
method_name = `from_${desired_format}`;
if( typeof this[method_name] == 'function' ){
return this[method_name]( data, callback );
}
callback( boom.unsupportedMediaType(`unsupported format ${desired_format}`), null );
}
/**
* Converts an object into a valid JSON string
* @method module:tastypie/lib/serializer#to_json
* @param {String} json valid json string to parse into an object
* @param {Function} callback callback function to call when deserialization is complete
**/
,to_json:function( data, callback ){
setImmediate(callback, null, JSON.stringify( data ) );
}
/**
* Converts a json string into an object
* @method module:tastypie/lib/serializer#from_json
* @param {String} json valid json string to parse into an object
* @param {Function} callback callback function to call when deserialization is complete
**/
,from_json: function( data, callback ){
setImmediate(callback, null, JSON.parse( data ) );
}
/**
* Converts an object to an xml string
* @method module:tastypie/lib/serializer#to_xml
* @param {String} data object to convert into an xml string
* @param {Function} callback callback function to call when deserialization is complete
**/
,to_xml: function( data, callback ){
setImmediate(() => {
callback(null, data == null ? data : jstoxml.toXML( this.to_jstree( data) , {header:true, indent:" "} ) );
});
}
/**
* converts an xml string into an object
* @method module:tastypie/lib/serializer#from_xml
* @param {String} xml valid xml string to parse into an object
* @param {Function} callback callback function to call when deserialization is complete
**/
,from_xml: function( data, callback ){
this._parser.parseString(data, callback);
}
/**
* Generates a tree of for xml serialization
* @private
* @method module:tastypie/lib/serializer#to_jstree
* @return {Object} An xml root element with sub tree
**/
,to_jstree: function( data, name, depth, element){
let _data = data || '';
depth = depth == null ? 0 : depth;
if(typeof _data.toJSON === 'function' ){
_data = _data.toJSON();
}
if(typeof _data.toObject === 'function' ){
_data = _data.toObject();
}
var data_type = kindOf( _data ).toLowerCase();
switch( data_type ){
case ARRAY:
if( this.options.xml.strict ){
element = [];
for(let idx=0,len = _data.length; idx < len; idx++){
element.push( this.to_jstree( _data[idx], name, depth+1 ) );
}
} else{
element = {
_name: name || OBJECTS
,_attrs:{
type:ARRAY
}
,_content:[]
};
for(let item of _data){
element._content.push( this.to_jstree( item, null, depth+1 ));
}
}
break;
case OBJECT:
if(depth === 0){
element = {
_name:TOP_LEVEL
,_content:[]
};
} else {
name = name || OBJECT;
element = {
_content:[]
,_name:name
,_attrs:{
type:OBJECT
}
};
if( name !== data_type ){
element._attrs.type = data_type;
}
}
for( let key in _data ){
element._content.push( this.to_jstree( data[key], key, depth+1, element ) );
}
break;
case FUNCTION:
let result = data();
element = {
_attrs:{
type:kindOf( result ).toLowerCase()
}
,_content: String( result )
,_name: name || VALUE
};
break;
default:
element = {
_attrs:{
type: data_type
}
,_content: String( _data )
,_name: name || VALUE
};
break;
}
return element;
}
/**
* Converts an object in to a jsonp string
* @method module:tastypie/lib/serializer#to_jsonp
* @param {Object} data data object to serialzie
* @param {Function} callback a callback function to call when serialization is complete. Must accept an error object and data payload
**/
,to_jsonp:function( data, callback ){
setImmediate(function(key){
callback(null
, util.format(
"%s(%j)"
.replace('\u2028', '\\u2028')
.replace('\u2029', '\\u2029')
, (key)
, data )
);
}, 0, this.options.callbackKey);
}
});
Object.defineProperties(Serializer.prototype,{
types: {
get: function(){
var types = this.options.content_types;
return Object.keys( types ).filter(function( key ){
return !!types[ key ];
});
}
}
});
module.exports = Serializer;